mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-11-05 10:13:43 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ab7566394 | |||
| 273903b26e |
@@ -1 +0,0 @@
|
||||
openpilot/selfdrive/modeld/models/big_*_tinygrad.pkl filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -1,5 +0,0 @@
|
||||
[lfs]
|
||||
url = https://huggingface.co/sunnypilot/sunnypilot-lfs.git/info/lfs
|
||||
pushurl = https://huggingface.co/sunnypilot/sunnypilot-lfs.git/info/lfs
|
||||
|
||||
locksverify = false
|
||||
+108
-115
@@ -4,11 +4,10 @@
|
||||
|
||||
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
|
||||
|
||||
# 351 Supported Cars
|
||||
# 345 Supported Cars
|
||||
|
||||
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br> |Video|Setup Video|
|
||||
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
|Acura|ADX 2025-26|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Acura ADX 2025-26">Buy Here</a></sub></details>|||
|
||||
|Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Acura ILX 2016-18">Buy Here</a></sub></details>|||
|
||||
|Acura|ILX 2019|All|openpilot|26 mph|25 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Acura ILX 2019">Buy Here</a></sub></details>|||
|
||||
|Acura|MDX 2022-24|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|43 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Acura MDX 2022-24">Buy Here</a></sub></details>|||
|
||||
@@ -17,12 +16,12 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Acura|RDX 2019-21|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Acura RDX 2019-21">Buy Here</a></sub></details>|||
|
||||
|Acura|TLX 2021-22|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Acura TLX 2021-22">Buy Here</a></sub></details>|||
|
||||
|Acura|TLX 2025|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Acura TLX 2025">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>13</sup>](#footnotes)|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi A3 2014-19">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>13</sup>](#footnotes)|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi A3 Sportback e-tron 2017-18">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>13</sup>](#footnotes)|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi Q2 2018">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>13</sup>](#footnotes)|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi Q3 2019-24">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>13</sup>](#footnotes)|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi RS3 2018">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>13</sup>](#footnotes)|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi S3 2015-17">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>12</sup>](#footnotes)|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi A3 2014-19">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>12</sup>](#footnotes)|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi A3 Sportback e-tron 2017-18">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>12</sup>](#footnotes)|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi Q2 2018">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>12</sup>](#footnotes)|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi Q3 2019-24">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>12</sup>](#footnotes)|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi RS3 2018">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>12</sup>](#footnotes)|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi S3 2015-17">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV 2022-23">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Equinox 2019-22">Buy Here</a></sub></details>|||
|
||||
@@ -34,8 +33,8 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||
|Chrysler|Pacifica Hybrid 2019-25|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica Hybrid 2019-25">Buy Here</a></sub></details>|||
|
||||
|comma|body|All|openpilot|0 mph|0 mph|[](##)|[](##)|None|<a href="https://youtu.be/VT-i3yRsX2s?t=2736" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|CUPRA[<sup>13</sup>](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Ateca 2018-23">Buy Here</a></sub></details>|||
|
||||
|CUPRA|Born 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Born 2021-23">Buy Here</a></sub></details>|||
|
||||
|CUPRA[<sup>12</sup>](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Ateca 2018-23">Buy Here</a></sub></details>|||
|
||||
|CUPRA|Born 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Born 2021-23">Buy Here</a></sub></details>|||
|
||||
|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Dodge Durango 2020-21">Buy Here</a></sub></details>|||
|
||||
|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Bronco Sport 2021-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2020-22">Buy Here</a></sub></details>|||
|
||||
@@ -87,7 +86,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Honda|Civic 2022-26|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2022-26">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2017-18">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback 2019-21|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2019-21">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback 2022-26|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2022-26">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Honda|Civic Hatchback 2022-24|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Honda|Civic Hatchback Hybrid 2025-26|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback Hybrid 2025-26">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback Hybrid (Europe only) 2023">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hybrid 2025-26|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hybrid 2025-26">Buy Here</a></sub></details>|||
|
||||
@@ -95,7 +94,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|15 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda CR-V 2017-22">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V 2023-26|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda CR-V 2023-26">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda CR-V Hybrid 2017-22">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V Hybrid 2023-27|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda CR-V Hybrid 2023-27">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V Hybrid 2023-26|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda CR-V Hybrid 2023-26">Buy Here</a></sub></details>|||
|
||||
|Honda|e 2020|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda e 2020">Buy Here</a></sub></details>|||
|
||||
|Honda|Fit 2018-20|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Fit 2018-20">Buy Here</a></sub></details>|||
|
||||
|Honda|Freed 2020|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Freed 2020">Buy Here</a></sub></details>|||
|
||||
@@ -112,9 +111,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Honda|Passport 2026|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Passport 2026">Buy Here</a></sub></details>|||
|
||||
|Honda|Pilot 2016-22|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Pilot 2016-22">Buy Here</a></sub></details>|||
|
||||
|Honda|Pilot 2023-25|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Pilot 2023-25">Buy Here</a></sub></details>|||
|
||||
|Honda|Pilot 2026|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Pilot 2026">Buy Here</a></sub></details>|||
|
||||
|Honda|Prelude 2026|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Prelude 2026">Buy Here</a></sub></details>|||
|
||||
|Honda|Ridgeline 2017-26|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Ridgeline 2017-26">Buy Here</a></sub></details>|||
|
||||
|Honda|Ridgeline 2017-25|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Ridgeline 2017-25">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Azera 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Azera 2022">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Azera Hybrid 2019|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Azera Hybrid 2019">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Azera Hybrid 2020|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Azera Hybrid 2020">Buy Here</a></sub></details>|||
|
||||
@@ -166,7 +163,6 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Kia|Carnival 2022-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Carnival 2022-24">Buy Here</a></sub></details>|||
|
||||
|Kia|Carnival (China only) 2023|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Carnival (China only) 2023">Buy Here</a></sub></details>|||
|
||||
|Kia|Ceed 2019-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Ceed 2019-21">Buy Here</a></sub></details>|||
|
||||
|Kia|Ceed Plug-in Hybrid 2021|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Ceed Plug-in Hybrid 2021">Buy Here</a></sub></details>|||
|
||||
|Kia|EV6 (Southeast Asia only) 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai P connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia EV6 (Southeast Asia only) 2022-24">Buy Here</a></sub></details>|||
|
||||
|Kia|EV6 (with HDA II) 2022-24|Highway Driving Assist II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai P connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia EV6 (with HDA II) 2022-24">Buy Here</a></sub></details>|||
|
||||
|Kia|EV6 (without HDA II) 2022-24|Highway Driving Assist|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai L connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia EV6 (without HDA II) 2022-24">Buy Here</a></sub></details>|||
|
||||
@@ -192,7 +188,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Kia|Niro Plug-in Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Niro Plug-in Hybrid 2022">Buy Here</a></sub></details>|||
|
||||
|Kia|Optima 2017|Advanced Smart Cruise Control|Stock|0 mph|32 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Optima 2017">Buy Here</a></sub></details>|||
|
||||
|Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Optima 2019-20">Buy Here</a></sub></details>|||
|
||||
|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Optima Hybrid 2019">Buy Here</a></sub></details>|||
|
||||
|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Optima Hybrid 2019">Buy Here</a></sub></details>|||
|
||||
|Kia|Seltos 2021|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Seltos 2021">Buy Here</a></sub></details>|||
|
||||
|Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Sorento 2018">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Fkh3s6WHJz8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Sorento 2019">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Fkh3s6WHJz8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
@@ -201,7 +197,6 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Kia|Sorento Plug-in Hybrid 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Sorento Plug-in Hybrid 2022-23">Buy Here</a></sub></details>|||
|
||||
|Kia|Sportage 2023-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Sportage 2023-24">Buy Here</a></sub></details>|||
|
||||
|Kia|Sportage Hybrid 2023|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Sportage Hybrid 2023">Buy Here</a></sub></details>|||
|
||||
|Kia|Sportage Plug-in Hybrid 2023|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Sportage Plug-in Hybrid 2023">Buy Here</a></sub></details>|||
|
||||
|Kia|Stinger 2018-20|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Stinger 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=MJ94qoofYw0" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Kia|Stinger 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Stinger 2022-23">Buy Here</a></sub></details>|||
|
||||
|Kia|Telluride 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Telluride 2020-22">Buy Here</a></sub></details>|||
|
||||
@@ -230,52 +225,51 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Lexus|UX Hybrid 2019-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus UX Hybrid 2019-24">Buy Here</a></sub></details>|||
|
||||
|Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lincoln Aviator 2020-24">Buy Here</a></sub></details>|||
|
||||
|Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lincoln Aviator Plug-in Hybrid 2020-24">Buy Here</a></sub></details>|||
|
||||
|MAN[<sup>13</sup>](#footnotes)|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN eTGE 2020-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|MAN[<sup>13</sup>](#footnotes)|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN TGE 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Mazda[<sup>6</sup>](#footnotes)|CX-5 2022-25|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Mazda CX-5 2022-25">Buy Here</a></sub></details>|||
|
||||
|Mazda[<sup>6</sup>](#footnotes)|CX-9 2021-23|All|Stock|0 mph|28 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Mazda CX-9 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/dA3duO4a0O4" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Nissan[<sup>7</sup>](#footnotes)|Altima 2019-24|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Altima 2019-24">Buy Here</a></sub></details>|||
|
||||
|Nissan[<sup>7</sup>](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Leaf 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Nissan[<sup>7</sup>](#footnotes)|Leaf IC 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Leaf IC 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Nissan[<sup>7</sup>](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Rogue 2018-20">Buy Here</a></sub></details>|||
|
||||
|Nissan[<sup>7</sup>](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan X-Trail 2017">Buy Here</a></sub></details>|||
|
||||
|MAN[<sup>12</sup>](#footnotes)|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN eTGE 2020-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|MAN[<sup>12</sup>](#footnotes)|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN TGE 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Mazda CX-5 2022-25">Buy Here</a></sub></details>|||
|
||||
|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Mazda CX-9 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/dA3duO4a0O4" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Nissan[<sup>6</sup>](#footnotes)|Altima 2019-24|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Altima 2019-24">Buy Here</a></sub></details>|||
|
||||
|Nissan[<sup>6</sup>](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Leaf 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Nissan[<sup>6</sup>](#footnotes)|Leaf IC 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Leaf IC 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Nissan[<sup>6</sup>](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Rogue 2018-20">Buy Here</a></sub></details>|||
|
||||
|Nissan[<sup>6</sup>](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan X-Trail 2017">Buy Here</a></sub></details>|||
|
||||
|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 1500 2019-24">Buy Here</a></sub></details>|||
|
||||
|Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 2500 2020-24">Buy Here</a></sub></details>|||
|
||||
|Ram|3500 2019-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 3500 2019-24">Buy Here</a></sub></details>|||
|
||||
|Rivian|R1S 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1S 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/dflSSGQwYNc" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Rivian|R1S 2025|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1S 2025">Buy Here</a></sub></details>|||
|
||||
|Rivian|R1T 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/dflSSGQwYNc" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Rivian|R1T 2025|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2025">Buy Here</a></sub></details>|||
|
||||
|SEAT[<sup>13</sup>](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Ateca 2016-23">Buy Here</a></sub></details>|||
|
||||
|SEAT[<sup>13</sup>](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Leon 2014-20">Buy Here</a></sub></details>|||
|
||||
|Subaru|Ascent 2019-21|All[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2020-23">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2017-18|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2017-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2019-21|All[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Impreza 2017-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2017-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Impreza 2020-22|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Legacy 2015-18|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2015-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Legacy 2020-22|All[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2015-17|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2015-17">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2018-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2020-22|All[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>8</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2020-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Škoda|Fabia 2022-23[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Fabia 2022-23">Buy Here</a></sub></details>[<sup>18</sup>](#footnotes)|||
|
||||
|Škoda|Kamiq 2021-23[<sup>14,16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Kamiq 2021-23">Buy Here</a></sub></details>[<sup>18</sup>](#footnotes)|||
|
||||
|Škoda[<sup>13</sup>](#footnotes)|Karoq 2019-23[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Karoq 2019-23">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>13</sup>](#footnotes)|Kodiaq 2017-23[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Kodiaq 2017-23">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>13</sup>](#footnotes)|Octavia 2015-19[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Octavia 2015-19">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>13</sup>](#footnotes)|Octavia RS 2016[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Octavia RS 2016">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>13</sup>](#footnotes)|Octavia Scout 2017-19[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Octavia Scout 2017-19">Buy Here</a></sub></details>|||
|
||||
|Škoda|Scala 2020-23[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Scala 2020-23">Buy Here</a></sub></details>[<sup>18</sup>](#footnotes)|||
|
||||
|Škoda[<sup>13</sup>](#footnotes)|Superb 2015-22[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Superb 2015-22">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>11</sup>](#footnotes)|Model 3 (with HW2.5) 2019[<sup>10</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW2.5) 2019">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>11</sup>](#footnotes)|Model 3 (with HW3) 2019-23[<sup>10</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW3) 2019-23">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>11</sup>](#footnotes)|Model 3 (with HW4) 2024-25[<sup>10</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW4) 2024-25">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>11</sup>](#footnotes)|Model Y (with HW3) 2020-24[<sup>10</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW3) 2020-24">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>11</sup>](#footnotes)|Model Y (with HW4) 2023-25[<sup>10</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW4) 2023-25">Buy Here</a></sub></details>|||
|
||||
|Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 3500 2019-22">Buy Here</a></sub></details>|||
|
||||
|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1S 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/dflSSGQwYNc" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Rivian|R1S 2025|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1S 2025">Buy Here</a></sub></details>|||
|
||||
|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/dflSSGQwYNc" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Rivian|R1T 2025|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2025">Buy Here</a></sub></details>|||
|
||||
|SEAT[<sup>12</sup>](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Ateca 2016-23">Buy Here</a></sub></details>|||
|
||||
|SEAT[<sup>12</sup>](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Leon 2014-20">Buy Here</a></sub></details>|||
|
||||
|Subaru|Ascent 2019-21|All[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2020-23">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2017-18|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2017-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2019-21|All[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Impreza 2017-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2017-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Impreza 2020-22|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Legacy 2015-18|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2015-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Legacy 2020-22|All[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2015-17|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2015-17">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2020-22|All[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2020-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Škoda|Fabia 2022-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Fabia 2022-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Škoda|Kamiq 2021-23[<sup>13,15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Kamiq 2021-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Škoda[<sup>12</sup>](#footnotes)|Karoq 2019-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Karoq 2019-23">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>12</sup>](#footnotes)|Kodiaq 2017-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Kodiaq 2017-23">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>12</sup>](#footnotes)|Octavia 2015-19[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Octavia 2015-19">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>12</sup>](#footnotes)|Octavia RS 2016[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Octavia RS 2016">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>12</sup>](#footnotes)|Octavia Scout 2017-19[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Octavia Scout 2017-19">Buy Here</a></sub></details>|||
|
||||
|Škoda|Scala 2020-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Scala 2020-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Škoda[<sup>12</sup>](#footnotes)|Superb 2015-22[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Superb 2015-22">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>10</sup>](#footnotes)|Model 3 (with HW3) 2019-23[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW3) 2019-23">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>10</sup>](#footnotes)|Model 3 (with HW4) 2024-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW4) 2024-25">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW3) 2020-24[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW3) 2020-24">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW4) 2023-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW4) 2023-25">Buy Here</a></sub></details>|||
|
||||
|Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Alphard 2019-20">Buy Here</a></sub></details>|||
|
||||
|Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Alphard Hybrid 2021">Buy Here</a></sub></details>|||
|
||||
|Toyota|Avalon 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon 2016">Buy Here</a></sub></details>|||
|
||||
@@ -288,8 +282,8 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Toyota|C-HR 2021|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota C-HR 2021">Buy Here</a></sub></details>|||
|
||||
|Toyota|C-HR Hybrid 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota C-HR Hybrid 2017-20">Buy Here</a></sub></details>|||
|
||||
|Toyota|C-HR Hybrid 2021-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota C-HR Hybrid 2021-22">Buy Here</a></sub></details>|||
|
||||
|Toyota|Camry 2018-20|All|Stock|0 mph[<sup>12</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=fkcjviZY9CM" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Camry 2021-24|All|openpilot|0 mph[<sup>12</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2021-24">Buy Here</a></sub></details>|||
|
||||
|Toyota|Camry 2018-20|All|Stock|0 mph[<sup>11</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=fkcjviZY9CM" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Camry 2021-24|All|openpilot|0 mph[<sup>11</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2021-24">Buy Here</a></sub></details>|||
|
||||
|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry Hybrid 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Q2DYY0AWKgk" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Camry Hybrid 2021-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry Hybrid 2021-24">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla 2017-19">Buy Here</a></sub></details>|||
|
||||
@@ -321,44 +315,44 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Toyota|RAV4 Hybrid 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2022">Buy Here</a></sub></details>|<a href="https://youtu.be/U0nH9cnrFB0" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2023-25">Buy Here</a></sub></details>|<a href="https://youtu.be/4eIsEq4L4Ng" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Sienna 2018-20|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Sienna 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=q1UPOo4Sh68" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon eHybrid 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon R 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon Shooting Brake 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Atlas 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Atlas Cross Sport 2020-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen California 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Caravelle 2020">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen CC 2018-22">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Crafter 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen e-Crafter 2018-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen e-Golf 2014-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf Alltrack 2015-19">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf GTD 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf GTE 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf GTI 2015-21">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf R 2015-19">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf SportsVan 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Grand California 2019-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen|ID.4 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen ID.4 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|ID.4 2024-25|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen ID.4 2024-25">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta 2019-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta GLI 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Passat 2015-22[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat Alltrack 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat GTE 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Polo 2018-23">Buy Here</a></sub></details>[<sup>18</sup>](#footnotes)|||
|
||||
|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Polo GTI 2018-23">Buy Here</a></sub></details>[<sup>18</sup>](#footnotes)|||
|
||||
|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen T-Cross 2021">Buy Here</a></sub></details>[<sup>18</sup>](#footnotes)|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen T-Roc 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Taos 2022-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Teramont 2018-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Teramont Cross Sport 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Teramont X 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Tiguan 2018-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Tiguan eHybrid 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>13</sup>](#footnotes)|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Touran 2016-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon eHybrid 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon R 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon Shooting Brake 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Atlas 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Atlas Cross Sport 2020-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen California 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Caravelle 2020">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen CC 2018-22">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Crafter 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen e-Crafter 2018-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen e-Golf 2014-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf Alltrack 2015-19">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf GTD 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf GTE 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf GTI 2015-21">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf R 2015-19">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf SportsVan 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Grand California 2019-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen|ID.4 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen ID.4 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|ID.4 2024-25|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen ID.4 2024-25">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta 2019-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta GLI 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Passat 2015-22[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat Alltrack 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat GTE 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Polo 2018-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Polo GTI 2018-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen T-Cross 2021">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen T-Roc 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Taos 2022-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Teramont 2018-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Teramont Cross Sport 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Teramont X 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Tiguan 2018-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Tiguan eHybrid 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Touran 2016-23">Buy Here</a></sub></details>|||
|
||||
|
||||
### Footnotes
|
||||
<sup>1</sup>openpilot Longitudinal Control (Alpha) is available behind a toggle; the toggle is only available in non-release branches such as `nightly-dev`. <br />
|
||||
@@ -366,19 +360,18 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
<sup>3</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/gm" target="_blank">GM</a>. <br />
|
||||
<sup>4</sup>2019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph. <br />
|
||||
<sup>5</sup>Enabling longitudinal control (alpha) will disable all CMBS functionality, including AEB and FCW. <br />
|
||||
<sup>6</sup>Mazda vehicles <b>require</b> comma power installation. <br />
|
||||
<sup>7</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/nissan" target="_blank">Nissan</a>. <br />
|
||||
<sup>8</sup>In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance. <br />
|
||||
<sup>9</sup>Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB. <br />
|
||||
<sup>10</sup>Model year 2019 can have HW2.5 or HW3, and model years 2023 and 2024 can have HW3 or HW4, depending on build date and factory. To check which hardware type your vehicle has, look for <b>Autopilot computer</b> under <b>Software -> Additional Vehicle Information</b> on your vehicle's touchscreen. See <a href="https://www.notateslaapp.com/news/2173/how-to-check-if-your-tesla-has-hardware-4-ai4-or-hardware-3">this page</a> for more information. <br />
|
||||
<sup>11</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/tesla" target="_blank">Tesla</a>. <br />
|
||||
<sup>12</sup>openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control. <br />
|
||||
<sup>13</sup>The J533 harness plugs in at the CAN gateway under the dashboard, just above the steering column. More information can be found at <a href="https://docs.howtocomma.com/docs/j533-harness-install" target="_blank">this guide</a>. <br />
|
||||
<sup>14</sup>Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform. <br />
|
||||
<sup>15</sup>Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets. <br />
|
||||
<sup>16</sup>Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality. <br />
|
||||
<sup>17</sup>Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC. <br />
|
||||
<sup>18</sup>Model-years 2022 and beyond may have a combined CAN gateway and BCM, which is supported by openpilot in software, but doesn't yet have a harness available from the comma store. <br />
|
||||
<sup>6</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/nissan" target="_blank">Nissan</a>. <br />
|
||||
<sup>7</sup>In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance. <br />
|
||||
<sup>8</sup>Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB. <br />
|
||||
<sup>9</sup>Model years 2023 and 2024 can have either hardware type, depending on build date and factory. To check which hardware type your vehicle has, look for <b>Autopilot computer</b> under <b>Software -> Additional Vehicle Information</b> on your vehicle's touchscreen. See <a href="https://www.notateslaapp.com/news/2173/how-to-check-if-your-tesla-has-hardware-4-ai4-or-hardware-3">this page</a> for more information. <br />
|
||||
<sup>10</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/tesla" target="_blank">Tesla</a>. <br />
|
||||
<sup>11</sup>openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control. <br />
|
||||
<sup>12</sup>The J533 harness plugs in at the CAN gateway under the dashboard, just above the steering column. More information can be found at <a href="https://docs.howtocomma.com/docs/j533-harness-install" target="_blank">this guide</a>. <br />
|
||||
<sup>13</sup>Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform. <br />
|
||||
<sup>14</sup>Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets. <br />
|
||||
<sup>15</sup>Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality. <br />
|
||||
<sup>16</sup>Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC. <br />
|
||||
<sup>17</sup>Model-years 2022 and beyond may have a combined CAN gateway and BCM, which is supported by openpilot in software, but doesn't yet have a harness available from the comma store. <br />
|
||||
|
||||
## Community Maintained Cars
|
||||
Although they're not upstream, the community has openpilot running on other makes and models. See the 'Community Supported Models' section of each make [on our wiki](https://wiki.comma.ai/).
|
||||
|
||||
@@ -40,12 +40,12 @@ class CarController(CarControllerBase, MadsCarController, CarControllerExt, Inte
|
||||
# ACC cancellation
|
||||
if CC.cruiseControl.cancel:
|
||||
self.last_button_frame = self.frame
|
||||
can_sends.append(chryslercan.create_cruise_buttons(self.packer, CS.cruise_btns, CS.button_counter + 1, das_bus, cancel=True))
|
||||
can_sends.append(chryslercan.create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, cancel=True))
|
||||
|
||||
# ACC resume from standstill
|
||||
elif CC.cruiseControl.resume:
|
||||
self.last_button_frame = self.frame
|
||||
can_sends.append(chryslercan.create_cruise_buttons(self.packer, CS.cruise_btns, CS.button_counter + 1, das_bus, resume=True))
|
||||
can_sends.append(chryslercan.create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, resume=True))
|
||||
|
||||
# HUD alerts
|
||||
if self.frame % 25 == 0:
|
||||
|
||||
@@ -40,7 +40,7 @@ class CarState(CarStateBase, MadsCarState, CarStateExt):
|
||||
ret_sp = structs.CarStateSP()
|
||||
|
||||
prev_distance_button = self.distance_button
|
||||
self.distance_button = cp.vl[self.cruise_btns]["ACC_Distance_Dec"]
|
||||
self.distance_button = cp.vl["CRUISE_BUTTONS"]["ACC_Distance_Dec"]
|
||||
|
||||
# lock info
|
||||
ret.doorOpen = any([cp.vl["BCM_1"]["DOOR_OPEN_FL"],
|
||||
@@ -101,7 +101,7 @@ class CarState(CarStateBase, MadsCarState, CarStateExt):
|
||||
ret.rightBlindspot = cp.vl["BSM_1"]["RIGHT_STATUS"] == 1
|
||||
|
||||
self.lkas_car_model = cp_cam.vl["DAS_6"]["CAR_MODEL"]
|
||||
self.button_counter = cp.vl[self.cruise_btns]["COUNTER"]
|
||||
self.button_counter = cp.vl["CRUISE_BUTTONS"]["COUNTER"]
|
||||
|
||||
MadsCarState.update_mads(self, ret, can_parsers)
|
||||
CarStateExt.update(self, ret, ret_sp, can_parsers)
|
||||
|
||||
@@ -66,7 +66,7 @@ def create_lkas_command(packer, CP, apply_torque, lkas_control_bit):
|
||||
return packer.make_can_msg("LKAS_COMMAND", 0, values)
|
||||
|
||||
|
||||
def create_cruise_buttons(packer, cruise_msg, frame, bus, cancel=False, resume=False, accel=False, decel=False):
|
||||
def create_cruise_buttons(packer, frame, bus, cancel=False, resume=False, accel=False, decel=False):
|
||||
values = {
|
||||
"ACC_Cancel": cancel,
|
||||
"ACC_Resume": resume,
|
||||
@@ -74,7 +74,7 @@ def create_cruise_buttons(packer, cruise_msg, frame, bus, cancel=False, resume=F
|
||||
"ACC_Decel": decel,
|
||||
"COUNTER": frame % 0x10,
|
||||
}
|
||||
return packer.make_can_msg(cruise_msg, bus, values)
|
||||
return packer.make_can_msg("CRUISE_BUTTONS", bus, values)
|
||||
|
||||
|
||||
def chrysler_checksum(address: int, sig, d: bytearray) -> int:
|
||||
|
||||
@@ -5,7 +5,7 @@ from opendbc.car.chrysler.carstate import CarState
|
||||
from opendbc.car.chrysler.radar_interface import RadarInterface
|
||||
from opendbc.car.chrysler.values import CAR, CUSW_CARS, RAM_HD, RAM_DT, RAM_CARS, ChryslerFlags, ChryslerSafetyFlags
|
||||
from opendbc.car.interfaces import CarInterfaceBase
|
||||
from opendbc.sunnypilot.car.chrysler.values_ext import ChryslerFlagsSP, ChryslerSafetyFlagsSP
|
||||
from opendbc.sunnypilot.car.chrysler.values_ext import ChryslerFlagsSP
|
||||
|
||||
|
||||
class CarInterface(CarInterfaceBase):
|
||||
@@ -100,10 +100,6 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
if candidate == CAR.RAM_HD_5TH_GEN:
|
||||
stock_cp.dashcamOnly = False
|
||||
|
||||
if 0x23A not in fingerprint[0]:
|
||||
ret.flags |= ChryslerFlagsSP.RAM_HD_ALT_BUTTONS.value
|
||||
ret.safetyParam |= ChryslerSafetyFlagsSP.RAM_HD_ALT_BUTTONS
|
||||
# https://github.com/commaai/openpilot/issues/25389
|
||||
stock_cp.tireStiffnessFactor = 0.15
|
||||
stock_cp.tireStiffnessFrontPerMass, stock_cp.tireStiffnessRearPerMass = \
|
||||
|
||||
@@ -92,7 +92,7 @@ class CAR(Platforms):
|
||||
RAM_HD_5TH_GEN = ChryslerPlatformConfig(
|
||||
[
|
||||
ChryslerCarDocs("Ram 2500 2020-24", car_parts=CarParts.common([CarHarness.ram])),
|
||||
ChryslerCarDocs("Ram 3500 2019-24", car_parts=CarParts.common([CarHarness.ram])),
|
||||
ChryslerCarDocs("Ram 3500 2019-22", car_parts=CarParts.common([CarHarness.ram])),
|
||||
],
|
||||
ChryslerCarSpecs(wheelbase=3.785, steerRatio=15.61, minSteerSpeed=16.),
|
||||
{Bus.pt: 'chrysler_ram_hd_generated'},
|
||||
|
||||
@@ -663,11 +663,6 @@ class CAR(Platforms):
|
||||
HYUNDAI_KONA_EV.specs,
|
||||
flags=HyundaiFlags.EV | HyundaiFlags.ALT_LIMITS,
|
||||
)
|
||||
HYUNDAI_VENUE_2021_NON_SCC = HyundaiNonSccPlatformConfig(
|
||||
[HyundaiNonSccCarDocs("Hyundai Venue Non-SCC 2021", car_parts=CarParts.common([CarHarness.hyundai_f]))],
|
||||
CarSpecs(wheelbase=2.52, steerRatio=14.4),
|
||||
flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.TCU_GEARS,
|
||||
)
|
||||
KIA_CEED_PHEV_2022_NON_SCC = HyundaiNonSccPlatformConfig(
|
||||
[HyundaiNonSccCarDocs("Kia Ceed Plug-in Hybrid Non-SCC 2022", car_parts=CarParts.common([CarHarness.hyundai_i]))],
|
||||
CarSpecs(wheelbase=2.65, steerRatio=13.75, tireStiffnessFactor=0.5),
|
||||
|
||||
@@ -5,17 +5,15 @@ from opendbc.car.lateral import apply_std_steer_angle_limits
|
||||
from opendbc.car.interfaces import CarControllerBase
|
||||
from opendbc.car.nissan import nissancan
|
||||
from opendbc.car.nissan.values import CAR, CarControllerParams
|
||||
from opendbc.sunnypilot.car.nissan.nissancan_ext import create_cruise_throttle_msg
|
||||
from opendbc.car.common.filter_simple import FirstOrderFilter
|
||||
|
||||
from opendbc.sunnypilot.car.nissan.icbm import IntelligentCruiseButtonManagementInterface
|
||||
|
||||
VisualAlert = structs.CarControl.HUDControl.VisualAlert
|
||||
|
||||
|
||||
class CarController(CarControllerBase, IntelligentCruiseButtonManagementInterface):
|
||||
class CarController(CarControllerBase):
|
||||
def __init__(self, dbc_names, CP, CP_SP):
|
||||
CarControllerBase.__init__(self, dbc_names, CP, CP_SP)
|
||||
IntelligentCruiseButtonManagementInterface.__init__(self, CP, CP_SP)
|
||||
super().__init__(dbc_names, CP, CP_SP)
|
||||
self.car_fingerprint = CP.carFingerprint
|
||||
|
||||
self.angle_filter = FirstOrderFilter(0.0, 0.1, DT_CTRL)
|
||||
@@ -62,7 +60,8 @@ class CarController(CarControllerBase, IntelligentCruiseButtonManagementInterfac
|
||||
can_sends.append(nissancan.create_acc_cancel_cmd(self.packer, self.car_fingerprint, CS.cruise_throttle_msg))
|
||||
|
||||
if self.CP.carFingerprint != CAR.NISSAN_ALTIMA and self.frame % 2 == 0:
|
||||
can_sends.extend(IntelligentCruiseButtonManagementInterface.update(self, CS, CC_SP, self.packer, self.frame, self.last_button_frame, pcm_cancel_cmd))
|
||||
button = "CANCEL_BUTTON" if pcm_cancel_cmd else None
|
||||
can_sends.append(create_cruise_throttle_msg(self.packer, self.car_fingerprint, CS.cruise_throttle_msg, self.frame, button))
|
||||
|
||||
can_sends.append(nissancan.create_steering_control(
|
||||
self.packer, self.apply_angle_last, self.frame, CC.latActive, lkas_max_torque))
|
||||
|
||||
@@ -37,6 +37,4 @@ class CarInterface(CarInterfaceBase):
|
||||
if candidate in (CAR.NISSAN_LEAF, CAR.NISSAN_LEAF_IC):
|
||||
ret.safetyParam |= NissanSafetyFlagsSP.LEAF
|
||||
|
||||
ret.intelligentCruiseButtonManagementAvailable = candidate != CAR.NISSAN_ALTIMA
|
||||
|
||||
return ret
|
||||
|
||||
@@ -388,7 +388,5 @@ routes = [
|
||||
CarTestRoute("bec2dcfde6a64235/2022-04-08--14-21-32", HONDA.HONDA_CRV_HYBRID, segment=22),
|
||||
|
||||
# port extensions
|
||||
CarTestRoute("be7ad2b087973544/0000002b--ec261cc821", HYUNDAI.HYUNDAI_VENUE_2021_NON_SCC),
|
||||
CarTestRoute("dc7bf18c8af12e37/00000006--ea06255cd0", HYUNDAI.KIA_CEED_PHEV_2022_NON_SCC),
|
||||
CarTestRoute("8bae07514ce06ad4/00000008--9f43d90441", CHRYSLER.RAM_HD_5TH_GEN), # 2024 Ram HD with alt cruise buttons
|
||||
]
|
||||
|
||||
@@ -99,7 +99,6 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
|
||||
"HYUNDAI_ELANTRA_2022_NON_SCC" = "HYUNDAI_ELANTRA_2021"
|
||||
"GENESIS_G70_2021_NON_SCC" = "HYUNDAI_SONATA"
|
||||
"HYUNDAI_BAYON_1ST_GEN_NON_SCC" = "HYUNDAI_SONATA"
|
||||
"HYUNDAI_VENUE_2021_NON_SCC" = "HYUNDAI_KONA_EV"
|
||||
"CHEVROLET_BOLT_NON_ACC_2ND_GEN" = "CHEVROLET_BOLT_EUV"
|
||||
"CHEVROLET_BOLT_NON_ACC" = "CHEVROLET_BOLT_EUV"
|
||||
"CHEVROLET_BOLT_NON_ACC_1ST_GEN" = "CHEVROLET_BOLT_EUV"
|
||||
|
||||
@@ -13,7 +13,7 @@ from opendbc.car.toyota.values import CAR, NO_STOP_TIMER_CAR, TSS2_CAR, \
|
||||
from opendbc.can import CANPacker
|
||||
|
||||
from opendbc.sunnypilot.car.toyota.gas_interceptor import GasInterceptorCarController
|
||||
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP, TSS2_SMOOTH_KI
|
||||
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP
|
||||
|
||||
Ecu = structs.CarParams.Ecu
|
||||
LongCtrlState = structs.CarControl.Actuators.LongControlState
|
||||
@@ -37,7 +37,7 @@ MAX_STEER_RATE_FRAMES = 17 # tx control frames needed before torque can be cut
|
||||
MAX_USER_TORQUE = 500
|
||||
|
||||
|
||||
def get_long_tune(CP, params, CP_SP):
|
||||
def get_long_tune(CP, params):
|
||||
if CP.flags & ToyotaFlags.TSS2:
|
||||
kiBP = [2., 5.]
|
||||
kiV = [0.5, 0.25]
|
||||
@@ -45,9 +45,6 @@ def get_long_tune(CP, params, CP_SP):
|
||||
kiBP = [0., 5., 35.]
|
||||
kiV = [3.6, 2.4, 1.5]
|
||||
|
||||
if CP_SP.flags & ToyotaFlagsSP.TSS2_SMOOTH:
|
||||
kiBP, kiV = TSS2_SMOOTH_KI
|
||||
|
||||
return PIDController(0.0, (kiBP, kiV), k_f=1.0,
|
||||
pos_limit=params.ACCEL_MAX, neg_limit=params.ACCEL_MIN,
|
||||
rate=1 / (DT_CTRL * 3))
|
||||
@@ -68,7 +65,7 @@ class CarController(CarControllerBase, GasInterceptorCarController):
|
||||
self.distance_button = 0
|
||||
|
||||
# *** start long control state ***
|
||||
self.long_pid = get_long_tune(self.CP, self.params, self.CP_SP)
|
||||
self.long_pid = get_long_tune(self.CP, self.params)
|
||||
self.aego = FirstOrderFilter(0.0, 0.25, DT_CTRL * 3)
|
||||
self.pitch = FirstOrderFilter(0, 0.5, DT_CTRL)
|
||||
self.pitch_hp = HighPassFilter(0.0, 0.25, 1.5, DT_CTRL)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
|
||||
chrysler_to_ram: dict[str, dict[int, int | tuple[int, int]]] = {
|
||||
chrysler_to_ram = {
|
||||
"_stellantis_common_ram_dt_generated.dbc": {
|
||||
258: 35,
|
||||
264: 37,
|
||||
@@ -26,7 +26,7 @@ chrysler_to_ram: dict[str, dict[int, int | tuple[int, int]]] = {
|
||||
820: 657,
|
||||
},
|
||||
"_stellantis_common_ram_hd_generated.dbc": {
|
||||
571: (570, 571),
|
||||
571: 570,
|
||||
678: 629,
|
||||
},
|
||||
}
|
||||
@@ -42,46 +42,14 @@ def generate():
|
||||
parts = [f'CM_ "Generated from {src}"\n\n']
|
||||
|
||||
wrote_addrs = set()
|
||||
cur_msg = []
|
||||
for line in in_f.readlines():
|
||||
if line.startswith('VAL_'):
|
||||
val_sl = line.split(' ')
|
||||
val_addr = int(val_sl[1])
|
||||
val_new_addrs = addr_lookup.get(val_addr, (val_addr,))
|
||||
if not isinstance(val_new_addrs, tuple):
|
||||
val_new_addrs = (val_new_addrs,)
|
||||
for val_new_addr in val_new_addrs:
|
||||
val_sl[1] = str(val_new_addr)
|
||||
cur_msg.append(' '.join(val_sl))
|
||||
else:
|
||||
cur_msg.append(line)
|
||||
if line.strip() == '':
|
||||
if not len(cur_msg):
|
||||
continue
|
||||
|
||||
if not cur_msg[0].startswith('BO_'):
|
||||
parts.extend(cur_msg)
|
||||
cur_msg = []
|
||||
continue
|
||||
|
||||
# msg done
|
||||
sl = cur_msg[0].split(' ')
|
||||
if line.startswith(('BO_', 'VAL_')):
|
||||
sl = line.split(' ')
|
||||
addr = int(sl[1])
|
||||
|
||||
new_addrs = addr_lookup.get(addr, (addr,))
|
||||
if not isinstance(new_addrs, tuple):
|
||||
new_addrs = (new_addrs,)
|
||||
|
||||
for idx, new_addr in enumerate(new_addrs):
|
||||
if idx > 0 and cur_msg[0].startswith('BO_'):
|
||||
sl[2] = sl[2][:-1] + '_ALT:'
|
||||
sl[1] = str(new_addr)
|
||||
cur_msg[0] = ' '.join(sl)
|
||||
parts.extend(cur_msg)
|
||||
|
||||
wrote_addrs.add(addr)
|
||||
cur_msg = []
|
||||
parts.extend(cur_msg)
|
||||
sl[1] = str(addr_lookup.get(addr, addr))
|
||||
line = ' '.join(sl)
|
||||
parts.append(line)
|
||||
|
||||
missing_addrs = set(addr_lookup.keys()) - wrote_addrs
|
||||
assert len(missing_addrs) == 0, f"Missing addrs from {src}: {missing_addrs}"
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#define CHRYSLER_DAS_6 0x2A6 // LKAS HUD and auto headlight control from DASM
|
||||
#define CHRYSLER_LKAS_COMMAND 0x292 // LKAS controls from DASM
|
||||
#define CHRYSLER_CRUISE_BUTTONS 0x23B // Cruise control buttons
|
||||
#define CHRYSLER_CRUISE_BUTTONS_ALT 0x000 // Placeholder, does not exist
|
||||
#define CHRYSLER_LKAS_HEARTBIT 0x2D9 // LKAS HEARTBIT from DASM
|
||||
#define CHRYSLER_TRACTION_BUTTON 0x330 // Traction control button
|
||||
#define CHRYSLER_Center_Stack_2 0x000 // Placeholder, does not exist
|
||||
@@ -26,7 +25,6 @@
|
||||
#define CHRYSLER_RAM_DT_DAS_6 0xFA
|
||||
#define CHRYSLER_RAM_DT_LKAS_COMMAND 0xA6
|
||||
#define CHRYSLER_RAM_DT_CRUISE_BUTTONS 0xB1
|
||||
#define CHRYSLER_RAM_DT_CRUISE_BUTTONS_ALT 0x00 // Placeholder, does not exist
|
||||
#define CHRYSLER_RAM_DT_LKAS_HEARTBIT 0x00 // Placeholder, does not exist
|
||||
#define CHRYSLER_RAM_DT_TRACTION_BUTTON 0x00 // Placeholder, does not exist
|
||||
#define CHRYSLER_RAM_DT_Center_Stack_2 0x28A
|
||||
@@ -40,17 +38,10 @@
|
||||
#define CHRYSLER_RAM_HD_DAS_6 0x275
|
||||
#define CHRYSLER_RAM_HD_LKAS_COMMAND 0x276
|
||||
#define CHRYSLER_RAM_HD_CRUISE_BUTTONS 0x23A
|
||||
#define CHRYSLER_RAM_HD_CRUISE_BUTTONS_ALT 0x23B // Cruise control buttons for Ram HD alt buttons
|
||||
#define CHRYSLER_RAM_HD_LKAS_HEARTBIT 0x00 // Placeholder, does not exist
|
||||
#define CHRYSLER_RAM_HD_TRACTION_BUTTON 0x00 // Placeholder, does not exist
|
||||
#define CHRYSLER_RAM_HD_Center_Stack_2 0x28A
|
||||
|
||||
#ifdef ALLOW_DEBUG
|
||||
#define CHRYSLER_RAM_HD_COMMON_TX_MSGS \
|
||||
{CHRYSLER_RAM_HD_LKAS_COMMAND, 0, 8, .check_relay = true}, \
|
||||
{CHRYSLER_RAM_HD_DAS_6, 0, 8, .check_relay = true},
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
CHRYSLER_RAM_DT,
|
||||
CHRYSLER_RAM_HD,
|
||||
@@ -158,7 +149,7 @@ static bool chrysler_tx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
// FORCE CANCEL: only the cancel button press is allowed
|
||||
if ((msg->addr == CHRYSLER_ADDR(CRUISE_BUTTONS)) || (msg->addr == CHRYSLER_ADDR(CRUISE_BUTTONS_ALT))) {
|
||||
if (msg->addr == CHRYSLER_ADDR(CRUISE_BUTTONS)) {
|
||||
const bool is_cancel = msg->data[0] == 1U;
|
||||
const bool is_accel = msg->data[0] == 0x04U;
|
||||
const bool is_decel = msg->data[0] == 0x08U;
|
||||
@@ -216,10 +207,14 @@ static safety_config chrysler_init(uint16_t param) {
|
||||
{.msg = {{CHRYSLER_RAM_HD_Center_Stack_2, 0, 8, 1U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true, .ignore_frequency_check = true}, { 0 }, { 0 }}},
|
||||
};
|
||||
|
||||
static const CanMsg CHRYSLER_RAM_HD_TX_MSGS[] = {
|
||||
{CHRYSLER_RAM_HD_CRUISE_BUTTONS, 2, 3, .check_relay = false},
|
||||
{CHRYSLER_RAM_HD_LKAS_COMMAND, 0, 8, .check_relay = true},
|
||||
{CHRYSLER_RAM_HD_DAS_6, 0, 8, .check_relay = true},
|
||||
};
|
||||
|
||||
const uint32_t CHRYSLER_PARAM_RAM_HD = 2U; // set for Ram HD platform
|
||||
const uint16_t CHRYSLER_PARAM_SP_RAM_HD_ALT_BUTTONS = 1U;
|
||||
bool enable_ram_hd = GET_FLAG(param, CHRYSLER_PARAM_RAM_HD);
|
||||
bool ram_hd_alt_buttons = GET_FLAG(current_safety_param_sp, CHRYSLER_PARAM_SP_RAM_HD_ALT_BUTTONS);
|
||||
#endif
|
||||
|
||||
safety_config ret;
|
||||
@@ -231,23 +226,8 @@ static safety_config chrysler_init(uint16_t param) {
|
||||
ret = BUILD_SAFETY_CFG(chrysler_ram_dt_rx_checks, CHRYSLER_RAM_DT_TX_MSGS);
|
||||
#ifdef ALLOW_DEBUG
|
||||
} else if (enable_ram_hd) {
|
||||
static const CanMsg CHRYSLER_RAM_HD_TX_MSGS[] = {
|
||||
CHRYSLER_RAM_HD_COMMON_TX_MSGS
|
||||
{CHRYSLER_RAM_HD_CRUISE_BUTTONS, 2, 3, .check_relay = false},
|
||||
};
|
||||
|
||||
static const CanMsg CHRYSLER_RAM_HD_ALT_BUTTONS_TX_MSGS[] = {
|
||||
CHRYSLER_RAM_HD_COMMON_TX_MSGS
|
||||
{CHRYSLER_RAM_HD_CRUISE_BUTTONS_ALT, 2, 3, .check_relay = false},
|
||||
};
|
||||
|
||||
chrysler_platform = CHRYSLER_RAM_HD;
|
||||
SET_RX_CHECKS(chrysler_ram_hd_rx_checks, ret);
|
||||
if (ram_hd_alt_buttons) {
|
||||
SET_TX_MSGS(CHRYSLER_RAM_HD_ALT_BUTTONS_TX_MSGS, ret);
|
||||
} else {
|
||||
SET_TX_MSGS(CHRYSLER_RAM_HD_TX_MSGS, ret);
|
||||
}
|
||||
ret = BUILD_SAFETY_CFG(chrysler_ram_hd_rx_checks, CHRYSLER_RAM_HD_TX_MSGS);
|
||||
#endif
|
||||
} else {
|
||||
chrysler_platform = CHRYSLER_PACIFICA;
|
||||
|
||||
@@ -340,7 +340,7 @@ void safety_tick(void) {
|
||||
}
|
||||
|
||||
// enforce minimum frequency for safety-relevant messages
|
||||
bool frequency_invalid = !current_safety_config.rx_checks[i].msg[current_safety_config.rx_checks[i].status.index].ignore_frequency_check && (frequency < 10U);
|
||||
bool frequency_invalid = frequency < 10U;
|
||||
if (lagging || frequency_invalid || !is_msg_valid(current_safety_config.rx_checks, i)) {
|
||||
rx_checks_invalid = true;
|
||||
controls_allowed = false;
|
||||
|
||||
@@ -12,7 +12,6 @@ from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from opendbc.car.lateral import MAX_LATERAL_ACCEL, MAX_LATERAL_JERK
|
||||
|
||||
from opendbc.safety.tests.mads_common import MadsSafetyTestBase
|
||||
from opendbc.safety.tests.rx_checks_common import RxChecksSafetyTestBase
|
||||
|
||||
MAX_WRONG_COUNTERS = 5
|
||||
MAX_SAMPLE_VALS = 6
|
||||
@@ -989,8 +988,6 @@ class SafetyTest(SafetyTestBase):
|
||||
continue
|
||||
if attr.startswith('TestFord') and current_test.startswith('TestFord'):
|
||||
continue
|
||||
if attr.startswith('TestChryslerRamHD') and current_test.startswith('TestChryslerRamHD'):
|
||||
continue
|
||||
if attr.startswith('TestHyundaiCanfd') and current_test.startswith('TestHyundaiCanfd'):
|
||||
continue
|
||||
if {attr, current_test}.issubset({'TestHyundaiLongitudinalSafety', 'TestHyundaiLongitudinalSafetyCameraSCC', 'TestHyundaiSafetyFCEVLong'}):
|
||||
@@ -1043,7 +1040,7 @@ class SafetyTest(SafetyTestBase):
|
||||
|
||||
|
||||
@add_regen_tests
|
||||
class CarSafetyTest(SafetyTest, MadsSafetyTestBase, RxChecksSafetyTestBase):
|
||||
class CarSafetyTest(SafetyTest, MadsSafetyTestBase):
|
||||
STANDSTILL_THRESHOLD: float = 0.0
|
||||
GAS_PRESSED_THRESHOLD = 0
|
||||
RELAY_MALFUNCTION_ADDRS: dict[int, tuple[int, ...]] | None = None
|
||||
|
||||
@@ -46,46 +46,6 @@ void tick_mads_state(bool vm, bool acc_main, bool op_allowed, bool braking, bool
|
||||
mads_state_update(vm, acc_main, op_allowed, braking, steering_disengage);
|
||||
}
|
||||
|
||||
void set_unseen_rx_checks_valid(void){
|
||||
for (int i = 0; i < current_safety_config.rx_checks_len; i++) {
|
||||
RxStatus *status = ¤t_safety_config.rx_checks[i].status;
|
||||
if (!status->msg_seen) {
|
||||
status->msg_seen = true;
|
||||
status->index = 0;
|
||||
status->valid_checksum = true;
|
||||
status->wrong_counters = 0;
|
||||
status->valid_quality_flag = true;
|
||||
status->last_timestamp = microsecond_timer_get();
|
||||
status->lagging = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int get_ignore_frequency_check_count(void){
|
||||
int count = 0;
|
||||
for (int i = 0; i < current_safety_config.rx_checks_len; i++) {
|
||||
for (uint8_t j = 0U; (j < MAX_ADDR_CHECK_MSGS) && (current_safety_config.rx_checks[i].msg[j].addr != 0); j++) {
|
||||
if (current_safety_config.rx_checks[i].msg[j].ignore_frequency_check) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool get_rx_check_ignore_frequency(int addr, int bus){
|
||||
bool ignore = false;
|
||||
for (int i = 0; i < current_safety_config.rx_checks_len; i++) {
|
||||
for (uint8_t j = 0U; (j < MAX_ADDR_CHECK_MSGS) && (current_safety_config.rx_checks[i].msg[j].addr != 0); j++) {
|
||||
const CanMsgCheck *msg = ¤t_safety_config.rx_checks[i].msg[j];
|
||||
if ((msg->addr == addr) && ((int)msg->bus == bus) && msg->ignore_frequency_check) {
|
||||
ignore = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ignore;
|
||||
}
|
||||
|
||||
void set_relay_malfunction(bool c){
|
||||
relay_malfunction = c;
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
|
||||
class RxChecksSafetyTestBase(unittest.TestCase):
|
||||
safety: libsafety_py.LibSafety
|
||||
|
||||
def _ignore_frequency_check_msgs(self):
|
||||
return []
|
||||
|
||||
def test_ignore_frequency_check(self):
|
||||
msgs = self._ignore_frequency_check_msgs()
|
||||
self.assertEqual(len(msgs), self.safety.get_ignore_frequency_check_count())
|
||||
|
||||
for msg in msgs:
|
||||
self.assertTrue(self._rx(msg))
|
||||
self.assertTrue(self.safety.get_rx_check_ignore_frequency(msg[0].addr, msg[0].bus))
|
||||
|
||||
self.safety.set_unseen_rx_checks_valid()
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.safety.safety_tick()
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
@@ -2,9 +2,7 @@
|
||||
import unittest
|
||||
|
||||
from opendbc.car.chrysler.values import ChryslerSafetyFlags
|
||||
from opendbc.sunnypilot.car.chrysler.values_ext import ChryslerSafetyFlagsSP
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
import opendbc.safety.tests.common as common
|
||||
class TestChryslerSafety(common.CarSafetyTest, common.MotorTorqueSteeringSafetyTest):
|
||||
DBC = "chrysler_pacifica_2017_hybrid_generated"
|
||||
@@ -91,11 +89,6 @@ class TestChryslerSafety(common.CarSafetyTest, common.MotorTorqueSteeringSafetyT
|
||||
def _lkas_button_msg(self, enabled):
|
||||
values = {"TOGGLE_LKAS": enabled}
|
||||
return self.packer.make_can_msg_safety("TRACTION_BUTTON", 0, values)
|
||||
|
||||
def _ignore_frequency_check_msgs(self):
|
||||
return [self._lkas_button_msg(False)]
|
||||
|
||||
|
||||
class TestChryslerRamDTSafety(TestChryslerSafety):
|
||||
DBC = "chrysler_ram_dt_generated"
|
||||
|
||||
@@ -145,19 +138,5 @@ class TestChryslerRamHDSafety(TestChryslerSafety):
|
||||
def _lkas_button_msg(self, enabled):
|
||||
values = {"LKAS_Button": enabled}
|
||||
return self.packer.make_can_msg_safety("Center_Stack_2", 0, values)
|
||||
|
||||
|
||||
class TestChryslerRamHDAltButtonSafety(TestChryslerRamHDSafety):
|
||||
TX_MSGS = [[0x275, 0], [0x276, 0], [0x23B, 2]]
|
||||
|
||||
def setUp(self):
|
||||
libsafety_py.libsafety.set_current_safety_param_sp(ChryslerSafetyFlagsSP.RAM_HD_ALT_BUTTONS)
|
||||
super().setUp()
|
||||
|
||||
def _button_msg(self, cancel=False, resume=False, accel=False, decel=False):
|
||||
values = {"ACC_Cancel": cancel, "ACC_Resume": resume, "ACC_Accel": accel, "ACC_Decel": decel}
|
||||
return self.packer.make_can_msg_safety("CRUISE_BUTTONS_ALT", self.DAS_BUS, values)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -489,9 +489,6 @@ class TestTeslaVehicleBusSafety(TestTeslaSafetyBase):
|
||||
values = {"UI_activeTouchPoints": 3 if enabled else 0}
|
||||
return self.packer_adas.make_can_msg_safety("UI_status2", CANBUS.vehicle, values)
|
||||
|
||||
def _ignore_frequency_check_msgs(self):
|
||||
return [self._lkas_button_msg(False)]
|
||||
|
||||
def _set_mads_screen_button_config(self, finger_flag):
|
||||
param_sp = TeslaSafetyFlagsSP.HAS_VEHICLE_BUS
|
||||
if finger_flag is not None:
|
||||
|
||||
@@ -37,10 +37,6 @@ class TestToyotaSafetyBase(common.CarSafetyTest, common.LongitudinalAccelSafetyT
|
||||
|
||||
SAFETY_PARAM_SP: int = 0
|
||||
|
||||
def setUp(self):
|
||||
libsafety_py.libsafety.set_current_safety_param_sp(self.SAFETY_PARAM_SP)
|
||||
super().setUp()
|
||||
|
||||
packer: CANPackerSafety
|
||||
safety: libsafety_py.LibSafety
|
||||
|
||||
@@ -99,9 +95,6 @@ class TestToyotaSafetyBase(common.CarSafetyTest, common.LongitudinalAccelSafetyT
|
||||
values = {"MAIN_ON": enabled}
|
||||
return self.packer.make_can_msg_safety(msg, 0, values)
|
||||
|
||||
def _ignore_frequency_check_msgs(self):
|
||||
return [self._acc_state_msg(False)] if self.SAFETY_PARAM_SP & ToyotaSafetyFlagsSP.UNSUPPORTED_DSU else []
|
||||
|
||||
def test_diagnostics(self, stock_longitudinal: bool = False, ecu_disabled: bool = True):
|
||||
for should_tx, msg in ((False, b"\x6D\x02\x3E\x00\x00\x00\x00\x00"), # fwdCamera tester present
|
||||
(False, b"\x0F\x03\xAA\xAA\x00\x00\x00\x00"), # non-tester present
|
||||
|
||||
@@ -2030,16 +2030,6 @@
|
||||
],
|
||||
"package": "Smart Cruise Control (SCC)"
|
||||
},
|
||||
"Hyundai Venue Non-SCC 2021": {
|
||||
"platform": "HYUNDAI_VENUE_2021_NON_SCC",
|
||||
"make": "Hyundai",
|
||||
"brand": "hyundai",
|
||||
"model": "Venue Non-SCC",
|
||||
"year": [
|
||||
"2021"
|
||||
],
|
||||
"package": "No Smart Cruise Control (Non-SCC)"
|
||||
},
|
||||
"Jeep Grand Cherokee 2016-18": {
|
||||
"platform": "JEEP_GRAND_CHEROKEE",
|
||||
"make": "Jeep",
|
||||
@@ -3005,7 +2995,7 @@
|
||||
],
|
||||
"package": "Adaptive Cruise Control (ACC)"
|
||||
},
|
||||
"Ram 3500 2019-24": {
|
||||
"Ram 3500 2019-22": {
|
||||
"platform": "RAM_HD_5TH_GEN",
|
||||
"make": "Ram",
|
||||
"brand": "chrysler",
|
||||
@@ -3014,9 +3004,7 @@
|
||||
"2019",
|
||||
"2020",
|
||||
"2021",
|
||||
"2022",
|
||||
"2023",
|
||||
"2024"
|
||||
"2022"
|
||||
],
|
||||
"package": "Adaptive Cruise Control (ACC)"
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ from enum import StrEnum
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.can.parser import CANParser
|
||||
from opendbc.car.chrysler.values import RAM_HD
|
||||
from opendbc.sunnypilot.car.chrysler.values_ext import ChryslerFlagsSP, get_buttons
|
||||
from opendbc.sunnypilot.car.chrysler.values_ext import BUTTONS
|
||||
|
||||
|
||||
class CarStateExt:
|
||||
@@ -19,18 +19,14 @@ class CarStateExt:
|
||||
self.CP = CP
|
||||
self.CP_SP = CP_SP
|
||||
|
||||
cruise_btns_msg = "CRUISE_BUTTONS_ALT" if CP_SP.flags & ChryslerFlagsSP.RAM_HD_ALT_BUTTONS else "CRUISE_BUTTONS"
|
||||
self.cruise_btns = cruise_btns_msg
|
||||
self.buttons = get_buttons(cruise_btns_msg)
|
||||
|
||||
self.button_events = []
|
||||
self.button_states = {button.event_type: False for button in self.buttons}
|
||||
self.button_states = {button.event_type: False for button in BUTTONS}
|
||||
|
||||
def update(self, ret: structs.CarState, ret_sp: structs.CarStateSP, can_parsers: dict[StrEnum, CANParser]):
|
||||
cp = can_parsers[Bus.pt]
|
||||
|
||||
button_events = []
|
||||
for button in self.buttons:
|
||||
for button in BUTTONS:
|
||||
state = (cp.vl[button.can_addr][button.can_msg] in button.values)
|
||||
if self.button_states[button.event_type] != state:
|
||||
event = structs.CarState.ButtonEvent.new_message()
|
||||
|
||||
@@ -24,9 +24,4 @@ FW_VERSIONS_EXT = {
|
||||
b'68502996AC',
|
||||
],
|
||||
},
|
||||
CAR.RAM_HD_5TH_GEN: {
|
||||
(Ecu.engine, 0x7e0, None): [
|
||||
b'52421332AG',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -36,13 +36,13 @@ class IntelligentCruiseButtonManagementInterface(IntelligentCruiseButtonManageme
|
||||
self.last_button_frame = CS.button_counter
|
||||
|
||||
if ram_cars:
|
||||
can_sends.append(chryslercan.create_cruise_buttons(packer, CS.cruise_btns, CS.button_counter, das_bus,
|
||||
can_sends.append(chryslercan.create_cruise_buttons(packer, CS.button_counter, das_bus,
|
||||
accel=accel, decel=decel))
|
||||
else:
|
||||
self.button_frame += 1
|
||||
button_counter_offset = [1, 1, 0, None][self.button_frame % 4]
|
||||
if button_counter_offset is not None:
|
||||
can_sends.append(chryslercan.create_cruise_buttons(packer, CS.cruise_btns, CS.button_counter + button_counter_offset, das_bus,
|
||||
can_sends.append(chryslercan.create_cruise_buttons(packer, CS.button_counter + button_counter_offset, das_bus,
|
||||
accel=accel, decel=decel))
|
||||
|
||||
return can_sends
|
||||
|
||||
@@ -13,20 +13,13 @@ from opendbc.car import structs
|
||||
ButtonType = structs.CarState.ButtonEvent.Type
|
||||
Button = namedtuple('Button', ['event_type', 'can_addr', 'can_msg', 'values'])
|
||||
|
||||
|
||||
def get_buttons(cruise_btns_msg="CRUISE_BUTTONS"):
|
||||
return [
|
||||
Button(ButtonType.accelCruise, cruise_btns_msg, "ACC_Accel", [1]),
|
||||
Button(ButtonType.decelCruise, cruise_btns_msg, "ACC_Decel", [1]),
|
||||
Button(ButtonType.cancel, cruise_btns_msg, "ACC_Cancel", [1]),
|
||||
Button(ButtonType.resumeCruise, cruise_btns_msg, "ACC_Resume", [1]),
|
||||
]
|
||||
|
||||
|
||||
class ChryslerSafetyFlagsSP:
|
||||
RAM_HD_ALT_BUTTONS = 1
|
||||
BUTTONS = [
|
||||
Button(ButtonType.accelCruise, "CRUISE_BUTTONS", "ACC_Accel", [1]),
|
||||
Button(ButtonType.decelCruise, "CRUISE_BUTTONS", "ACC_Decel", [1]),
|
||||
Button(ButtonType.cancel, "CRUISE_BUTTONS", "ACC_Cancel", [1]),
|
||||
Button(ButtonType.resumeCruise, "CRUISE_BUTTONS", "ACC_Resume", [1]),
|
||||
]
|
||||
|
||||
|
||||
class ChryslerFlagsSP(IntFlag):
|
||||
NO_MIN_STEERING_SPEED = 1
|
||||
RAM_HD_ALT_BUTTONS = 2
|
||||
|
||||
@@ -9,7 +9,7 @@ from enum import StrEnum
|
||||
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.can.parser import CANParser
|
||||
from opendbc.car.hyundai.values import HyundaiFlags, CAR
|
||||
from opendbc.car.hyundai.values import HyundaiFlags
|
||||
from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP
|
||||
|
||||
|
||||
@@ -55,11 +55,9 @@ class CarStateExt:
|
||||
cruise_enabled_sig = "CC_ACT" if self.CP.flags & HyundaiFlags.EV else "CRUISE_LAMP_S"
|
||||
cruise_speed_msg = "E_EMS11" if self.CP.flags & HyundaiFlags.EV else \
|
||||
"ELECT_GEAR" if self.CP.flags & HyundaiFlags.HYBRID else \
|
||||
"EMS_H12" if self.CP.carFingerprint == CAR.HYUNDAI_VENUE_2021_NON_SCC else \
|
||||
"LVR12"
|
||||
cruise_speed_sig = "Cruise_Limit_Target" if self.CP.flags & HyundaiFlags.EV else \
|
||||
"SLC_SET_SPEED" if self.CP.flags & HyundaiFlags.HYBRID else \
|
||||
"SLD_VS" if self.CP.carFingerprint == CAR.HYUNDAI_VENUE_2021_NON_SCC else \
|
||||
"CF_Lvr_CruiseSet"
|
||||
ret.cruiseState.available = cp.vl[cruise_msg][cruise_available_sig] != 0
|
||||
ret.cruiseState.enabled = cp.vl[cruise_msg][cruise_enabled_sig] != 0
|
||||
|
||||
@@ -123,12 +123,4 @@ FW_VERSIONS_EXT = {
|
||||
b'\xf1\x00BC3 LKA AT EUR LHD 1.00 1.01 99211-Q0100 261'
|
||||
],
|
||||
},
|
||||
CAR.HYUNDAI_VENUE_2021_NON_SCC: {
|
||||
(Ecu.eps, 0x7D4, None): [
|
||||
b'\xf1\x00QX MDPS C 1.00 1.04 56340-K2000 9812',
|
||||
],
|
||||
(Ecu.fwdCamera, 0x7C4, None): [
|
||||
b'\xf1\x00QX MFC AT USA LHD 1.00 1.01 99211-K2000 191014',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -185,7 +185,6 @@ def _initialize_toyota(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params
|
||||
if CP.brand == 'toyota':
|
||||
toyota_stock_long = int(params_dict.get("ToyotaEnforceStockLongitudinal", 0)) == 1
|
||||
toyota_stop_and_go_hack = int(params_dict.get("ToyotaStopAndGoHack", 0)) == 1
|
||||
toyota_tss2_smooth = int(params_dict.get("ToyotaTSS2Smooth", 0)) == 1
|
||||
|
||||
if toyota_stock_long:
|
||||
CP_SP.flags |= ToyotaFlagsSP.STOCK_LONGITUDINAL.value
|
||||
@@ -195,6 +194,3 @@ def _initialize_toyota(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params
|
||||
|
||||
if toyota_stop_and_go_hack and CP.openpilotLongitudinalControl:
|
||||
CP_SP.flags |= ToyotaFlagsSP.STOP_AND_GO_HACK.value
|
||||
|
||||
if toyota_tss2_smooth and CP.flags & ToyotaFlags.TSS2 and CP.openpilotLongitudinalControl:
|
||||
CP_SP.flags |= ToyotaFlagsSP.TSS2_SMOOTH.value
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, sunnypilot, downquark7, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from collections import deque
|
||||
from enum import IntEnum
|
||||
|
||||
from opendbc.car import DT_CTRL, structs
|
||||
from opendbc.car.can_definitions import CanData
|
||||
from opendbc.sunnypilot.car.intelligent_cruise_button_management_interface_base import IntelligentCruiseButtonManagementInterfaceBase
|
||||
from opendbc.sunnypilot.car.nissan.nissancan_ext import create_cruise_throttle_msg
|
||||
|
||||
SendButtonState = structs.IntelligentCruiseButtonManagement.SendButtonState
|
||||
|
||||
BUTTONS = {
|
||||
SendButtonState.increase: "RES_BUTTON",
|
||||
SendButtonState.decrease: "SET_BUTTON",
|
||||
}
|
||||
|
||||
# Driver presses swallowed while ICBM owns CRUISE_THROTTLE, replayed afterward
|
||||
REPLAY_BUTTONS = ("PROPILOT_BUTTON", "FOLLOW_DISTANCE_BUTTON", "SET_BUTTON", "RES_BUTTON")
|
||||
|
||||
|
||||
class ButtonSendState(IntEnum):
|
||||
IDLE = 0
|
||||
RELEASE = 1
|
||||
PRESS = 2
|
||||
|
||||
|
||||
class IntelligentCruiseButtonManagementInterface(IntelligentCruiseButtonManagementInterfaceBase):
|
||||
def __init__(self, CP, CP_SP):
|
||||
super().__init__(CP, CP_SP)
|
||||
self.button_send_state = ButtonSendState.IDLE
|
||||
self.queued_buttons = deque()
|
||||
self.prev_buttons = dict.fromkeys(REPLAY_BUTTONS, False)
|
||||
|
||||
def reset(self):
|
||||
self.button_send_state = ButtonSendState.IDLE
|
||||
self.queued_buttons.clear()
|
||||
self.last_button_frame = self.frame
|
||||
|
||||
def update(self, CS, CC_SP, packer, frame, last_button_frame, pcm_cancel_cmd) -> list[CanData]:
|
||||
self.CC_SP = CC_SP
|
||||
self.ICBM = CC_SP.intelligentCruiseButtonManagement
|
||||
self.frame = frame
|
||||
self.last_button_frame = last_button_frame
|
||||
|
||||
buttons = {b: CS.cruise_throttle_msg[b] for b in REPLAY_BUTTONS}
|
||||
pressed = [b for b in REPLAY_BUTTONS if buttons[b] and not self.prev_buttons[b]]
|
||||
self.prev_buttons = buttons
|
||||
|
||||
send_button = None
|
||||
# Drop pending presses on cancel or disengage so nothing can resume cruise afterward
|
||||
if pcm_cancel_cmd or CS.cruise_throttle_msg["CANCEL_BUTTON"] or not CS.out.cruiseState.enabled:
|
||||
self.reset()
|
||||
if pcm_cancel_cmd:
|
||||
send_button = "CANCEL_BUTTON"
|
||||
|
||||
else:
|
||||
if self.button_send_state == ButtonSendState.IDLE and (self.queued_buttons or self.ICBM.sendButton != SendButtonState.none):
|
||||
self.button_send_state = ButtonSendState.RELEASE
|
||||
|
||||
if self.button_send_state != ButtonSendState.IDLE:
|
||||
self.queued_buttons.extend(pressed)
|
||||
|
||||
# Send no button, giving the car time to adjust the set speed
|
||||
if self.button_send_state == ButtonSendState.RELEASE:
|
||||
send_button = "NO_BUTTON_PRESSED"
|
||||
if (self.frame - self.last_button_frame) * DT_CTRL >= 0.06:
|
||||
if not self.queued_buttons and self.ICBM.sendButton != SendButtonState.none:
|
||||
self.queued_buttons.append(BUTTONS[self.ICBM.sendButton])
|
||||
if self.queued_buttons:
|
||||
self.button_send_state = ButtonSendState.PRESS
|
||||
self.last_button_frame = self.frame
|
||||
# Hold release until the driver lets go, so a swallowed press is not passed through again
|
||||
elif not any(buttons.values()):
|
||||
self.button_send_state = ButtonSendState.IDLE
|
||||
|
||||
elif self.button_send_state == ButtonSendState.PRESS:
|
||||
send_button = self.queued_buttons[0]
|
||||
if (self.frame - self.last_button_frame) * DT_CTRL >= 0.06:
|
||||
self.queued_buttons.popleft()
|
||||
self.button_send_state = ButtonSendState.RELEASE
|
||||
self.last_button_frame = self.frame
|
||||
|
||||
return [create_cruise_throttle_msg(packer, self.CP.carFingerprint, CS.cruise_throttle_msg, self.frame, send_button)]
|
||||
@@ -14,11 +14,6 @@ class ToyotaFlagsSP(IntFlag):
|
||||
ZSS = 4
|
||||
STOCK_LONGITUDINAL = 8
|
||||
STOP_AND_GO_HACK = 16
|
||||
TSS2_SMOOTH = 32
|
||||
|
||||
|
||||
# kiBP, kiV for the PCM accel PID with TSS2_SMOOTH
|
||||
TSS2_SMOOTH_KI = ([0., 3., 5., 10., 25., 36.], [0.46, 0.46, 0.31, 0.22, 0.20, 0.19])
|
||||
|
||||
|
||||
class ToyotaSafetyFlagsSP:
|
||||
|
||||
@@ -138,9 +138,6 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
|
||||
status @0 :DownloadStatus;
|
||||
progress @1 :Float32;
|
||||
eta @2 :UInt32;
|
||||
speed @3 :Float32;
|
||||
canceling @4 :Bool;
|
||||
resuming @5 :Bool;
|
||||
}
|
||||
|
||||
struct Chunk {
|
||||
@@ -468,13 +465,6 @@ struct ModelDataV2SP @0xa1680744031fdb2d {
|
||||
laneTurnDirection @0 :TurnDirection;
|
||||
leftLaneChangeEdgeBlock @1 :Bool;
|
||||
rightLaneChangeEdgeBlock @2 :Bool;
|
||||
blendfulCurvature @3 :Float32;
|
||||
blendfulTrust @4 :Float32;
|
||||
modelCurvature @5 :Float32;
|
||||
blendfulGoingAround @6 :Bool;
|
||||
blendfulLeftProb @7 :Float32;
|
||||
blendfulRightProb @8 :Float32;
|
||||
blendfulLaneWidth @9 :Float32;
|
||||
|
||||
enum TurnDirection {
|
||||
none @0;
|
||||
|
||||
@@ -1173,7 +1173,7 @@ const ::capnp::_::RawSchema s_da834d53e62048b9 = {
|
||||
};
|
||||
#endif // !CAPNP_LITE
|
||||
CAPNP_DEFINE_ENUM(DownloadStatus_da834d53e62048b9, da834d53e62048b9);
|
||||
static const ::capnp::_::AlignedData<113> b_a677b25114d64c73 = {
|
||||
static const ::capnp::_::AlignedData<66> b_a677b25114d64c73 = {
|
||||
{ 0, 0, 0, 0, 5, 0, 6, 0,
|
||||
115, 76, 214, 20, 81, 178, 119, 166,
|
||||
28, 0, 0, 0, 1, 0, 2, 0,
|
||||
@@ -1183,7 +1183,7 @@ static const ::capnp::_::AlignedData<113> b_a677b25114d64c73 = {
|
||||
21, 0, 0, 0, 106, 1, 0, 0,
|
||||
41, 0, 0, 0, 7, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
37, 0, 0, 0, 87, 1, 0, 0,
|
||||
37, 0, 0, 0, 175, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
99, 117, 115, 116, 111, 109, 46, 99,
|
||||
@@ -1193,49 +1193,28 @@ static const ::capnp::_::AlignedData<113> b_a677b25114d64c73 = {
|
||||
108, 111, 97, 100, 80, 114, 111, 103,
|
||||
114, 101, 115, 115, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 1, 0, 1, 0,
|
||||
24, 0, 0, 0, 3, 0, 4, 0,
|
||||
12, 0, 0, 0, 3, 0, 4, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 1, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
153, 0, 0, 0, 58, 0, 0, 0,
|
||||
69, 0, 0, 0, 58, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
148, 0, 0, 0, 3, 0, 1, 0,
|
||||
160, 0, 0, 0, 2, 0, 1, 0,
|
||||
64, 0, 0, 0, 3, 0, 1, 0,
|
||||
76, 0, 0, 0, 2, 0, 1, 0,
|
||||
1, 0, 0, 0, 1, 0, 0, 0,
|
||||
0, 0, 1, 0, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
157, 0, 0, 0, 74, 0, 0, 0,
|
||||
73, 0, 0, 0, 74, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
156, 0, 0, 0, 3, 0, 1, 0,
|
||||
168, 0, 0, 0, 2, 0, 1, 0,
|
||||
72, 0, 0, 0, 3, 0, 1, 0,
|
||||
84, 0, 0, 0, 2, 0, 1, 0,
|
||||
2, 0, 0, 0, 2, 0, 0, 0,
|
||||
0, 0, 1, 0, 2, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
165, 0, 0, 0, 34, 0, 0, 0,
|
||||
81, 0, 0, 0, 34, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
160, 0, 0, 0, 3, 0, 1, 0,
|
||||
172, 0, 0, 0, 2, 0, 1, 0,
|
||||
3, 0, 0, 0, 3, 0, 0, 0,
|
||||
0, 0, 1, 0, 3, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
169, 0, 0, 0, 50, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
164, 0, 0, 0, 3, 0, 1, 0,
|
||||
176, 0, 0, 0, 2, 0, 1, 0,
|
||||
4, 0, 0, 0, 16, 0, 0, 0,
|
||||
0, 0, 1, 0, 4, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
173, 0, 0, 0, 82, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
172, 0, 0, 0, 3, 0, 1, 0,
|
||||
184, 0, 0, 0, 2, 0, 1, 0,
|
||||
5, 0, 0, 0, 17, 0, 0, 0,
|
||||
0, 0, 1, 0, 5, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
181, 0, 0, 0, 74, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
180, 0, 0, 0, 3, 0, 1, 0,
|
||||
192, 0, 0, 0, 2, 0, 1, 0,
|
||||
76, 0, 0, 0, 3, 0, 1, 0,
|
||||
88, 0, 0, 0, 2, 0, 1, 0,
|
||||
115, 116, 97, 116, 117, 115, 0, 0,
|
||||
15, 0, 0, 0, 0, 0, 0, 0,
|
||||
185, 72, 32, 230, 83, 77, 131, 218,
|
||||
@@ -1260,32 +1239,6 @@ static const ::capnp::_::AlignedData<113> b_a677b25114d64c73 = {
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
8, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
115, 112, 101, 101, 100, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
99, 97, 110, 99, 101, 108, 105, 110,
|
||||
103, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
114, 101, 115, 117, 109, 105, 110, 103,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, }
|
||||
};
|
||||
::capnp::word const* const bp_a677b25114d64c73 = b_a677b25114d64c73.words;
|
||||
@@ -1293,11 +1246,11 @@ static const ::capnp::_::AlignedData<113> b_a677b25114d64c73 = {
|
||||
static const ::capnp::_::RawSchema* const d_a677b25114d64c73[] = {
|
||||
&s_da834d53e62048b9,
|
||||
};
|
||||
static const uint16_t m_a677b25114d64c73[] = {4, 2, 1, 5, 3, 0};
|
||||
static const uint16_t i_a677b25114d64c73[] = {0, 1, 2, 3, 4, 5};
|
||||
static const uint16_t m_a677b25114d64c73[] = {2, 1, 0};
|
||||
static const uint16_t i_a677b25114d64c73[] = {0, 1, 2};
|
||||
const ::capnp::_::RawSchema s_a677b25114d64c73 = {
|
||||
0xa677b25114d64c73, b_a677b25114d64c73.words, 113, d_a677b25114d64c73, m_a677b25114d64c73,
|
||||
1, 6, i_a677b25114d64c73, nullptr, nullptr, { &s_a677b25114d64c73, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
0xa677b25114d64c73, b_a677b25114d64c73.words, 66, d_a677b25114d64c73, m_a677b25114d64c73,
|
||||
1, 3, i_a677b25114d64c73, nullptr, nullptr, { &s_a677b25114d64c73, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
};
|
||||
#endif // !CAPNP_LITE
|
||||
static const ::capnp::_::AlignedData<50> b_8d6e2aaaa9978a41 = {
|
||||
@@ -5077,17 +5030,17 @@ const ::capnp::_::RawSchema s_f416ec09499d9d19 = {
|
||||
0, 6, i_f416ec09499d9d19, nullptr, nullptr, { &s_f416ec09499d9d19, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
};
|
||||
#endif // !CAPNP_LITE
|
||||
static const ::capnp::_::AlignedData<191> b_a1680744031fdb2d = {
|
||||
static const ::capnp::_::AlignedData<74> b_a1680744031fdb2d = {
|
||||
{ 0, 0, 0, 0, 5, 0, 6, 0,
|
||||
45, 219, 31, 3, 68, 7, 104, 161,
|
||||
13, 0, 0, 0, 1, 0, 4, 0,
|
||||
13, 0, 0, 0, 1, 0, 1, 0,
|
||||
89, 10, 85, 29, 102, 186, 38, 181,
|
||||
0, 0, 7, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
21, 0, 0, 0, 218, 0, 0, 0,
|
||||
33, 0, 0, 0, 23, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
45, 0, 0, 0, 55, 2, 0, 0,
|
||||
45, 0, 0, 0, 175, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
99, 117, 115, 116, 111, 109, 46, 99,
|
||||
@@ -5099,77 +5052,28 @@ static const ::capnp::_::AlignedData<191> b_a1680744031fdb2d = {
|
||||
1, 0, 0, 0, 114, 0, 0, 0,
|
||||
84, 117, 114, 110, 68, 105, 114, 101,
|
||||
99, 116, 105, 111, 110, 0, 0, 0,
|
||||
40, 0, 0, 0, 3, 0, 4, 0,
|
||||
12, 0, 0, 0, 3, 0, 4, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 1, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
9, 1, 0, 0, 146, 0, 0, 0,
|
||||
69, 0, 0, 0, 146, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
12, 1, 0, 0, 3, 0, 1, 0,
|
||||
24, 1, 0, 0, 2, 0, 1, 0,
|
||||
72, 0, 0, 0, 3, 0, 1, 0,
|
||||
84, 0, 0, 0, 2, 0, 1, 0,
|
||||
1, 0, 0, 0, 16, 0, 0, 0,
|
||||
0, 0, 1, 0, 1, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
21, 1, 0, 0, 194, 0, 0, 0,
|
||||
81, 0, 0, 0, 194, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
24, 1, 0, 0, 3, 0, 1, 0,
|
||||
36, 1, 0, 0, 2, 0, 1, 0,
|
||||
84, 0, 0, 0, 3, 0, 1, 0,
|
||||
96, 0, 0, 0, 2, 0, 1, 0,
|
||||
2, 0, 0, 0, 17, 0, 0, 0,
|
||||
0, 0, 1, 0, 2, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
33, 1, 0, 0, 202, 0, 0, 0,
|
||||
93, 0, 0, 0, 202, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
40, 1, 0, 0, 3, 0, 1, 0,
|
||||
52, 1, 0, 0, 2, 0, 1, 0,
|
||||
3, 0, 0, 0, 1, 0, 0, 0,
|
||||
0, 0, 1, 0, 3, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
49, 1, 0, 0, 146, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
52, 1, 0, 0, 3, 0, 1, 0,
|
||||
64, 1, 0, 0, 2, 0, 1, 0,
|
||||
4, 0, 0, 0, 2, 0, 0, 0,
|
||||
0, 0, 1, 0, 4, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
61, 1, 0, 0, 114, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
60, 1, 0, 0, 3, 0, 1, 0,
|
||||
72, 1, 0, 0, 2, 0, 1, 0,
|
||||
5, 0, 0, 0, 3, 0, 0, 0,
|
||||
0, 0, 1, 0, 5, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
69, 1, 0, 0, 122, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
68, 1, 0, 0, 3, 0, 1, 0,
|
||||
80, 1, 0, 0, 2, 0, 1, 0,
|
||||
6, 0, 0, 0, 18, 0, 0, 0,
|
||||
0, 0, 1, 0, 6, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
77, 1, 0, 0, 162, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
80, 1, 0, 0, 3, 0, 1, 0,
|
||||
92, 1, 0, 0, 2, 0, 1, 0,
|
||||
7, 0, 0, 0, 4, 0, 0, 0,
|
||||
0, 0, 1, 0, 7, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
89, 1, 0, 0, 138, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
92, 1, 0, 0, 3, 0, 1, 0,
|
||||
104, 1, 0, 0, 2, 0, 1, 0,
|
||||
8, 0, 0, 0, 5, 0, 0, 0,
|
||||
0, 0, 1, 0, 8, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
101, 1, 0, 0, 146, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
104, 1, 0, 0, 3, 0, 1, 0,
|
||||
116, 1, 0, 0, 2, 0, 1, 0,
|
||||
9, 0, 0, 0, 6, 0, 0, 0,
|
||||
0, 0, 1, 0, 9, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
113, 1, 0, 0, 146, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
116, 1, 0, 0, 3, 0, 1, 0,
|
||||
128, 1, 0, 0, 2, 0, 1, 0,
|
||||
100, 0, 0, 0, 3, 0, 1, 0,
|
||||
112, 0, 0, 0, 2, 0, 1, 0,
|
||||
108, 97, 110, 101, 84, 117, 114, 110,
|
||||
68, 105, 114, 101, 99, 116, 105, 111,
|
||||
110, 0, 0, 0, 0, 0, 0, 0,
|
||||
@@ -5199,74 +5103,6 @@ static const ::capnp::_::AlignedData<191> b_a1680744031fdb2d = {
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
98, 108, 101, 110, 100, 102, 117, 108,
|
||||
67, 117, 114, 118, 97, 116, 117, 114,
|
||||
101, 0, 0, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
98, 108, 101, 110, 100, 102, 117, 108,
|
||||
84, 114, 117, 115, 116, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
109, 111, 100, 101, 108, 67, 117, 114,
|
||||
118, 97, 116, 117, 114, 101, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
98, 108, 101, 110, 100, 102, 117, 108,
|
||||
71, 111, 105, 110, 103, 65, 114, 111,
|
||||
117, 110, 100, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
98, 108, 101, 110, 100, 102, 117, 108,
|
||||
76, 101, 102, 116, 80, 114, 111, 98,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
98, 108, 101, 110, 100, 102, 117, 108,
|
||||
82, 105, 103, 104, 116, 80, 114, 111,
|
||||
98, 0, 0, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
98, 108, 101, 110, 100, 102, 117, 108,
|
||||
76, 97, 110, 101, 87, 105, 100, 116,
|
||||
104, 0, 0, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
10, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, }
|
||||
};
|
||||
@@ -5275,11 +5111,11 @@ static const ::capnp::_::AlignedData<191> b_a1680744031fdb2d = {
|
||||
static const ::capnp::_::RawSchema* const d_a1680744031fdb2d[] = {
|
||||
&s_b73df234a23b0cc2,
|
||||
};
|
||||
static const uint16_t m_a1680744031fdb2d[] = {3, 6, 9, 7, 8, 4, 0, 1, 5, 2};
|
||||
static const uint16_t i_a1680744031fdb2d[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
|
||||
static const uint16_t m_a1680744031fdb2d[] = {0, 1, 2};
|
||||
static const uint16_t i_a1680744031fdb2d[] = {0, 1, 2};
|
||||
const ::capnp::_::RawSchema s_a1680744031fdb2d = {
|
||||
0xa1680744031fdb2d, b_a1680744031fdb2d.words, 191, d_a1680744031fdb2d, m_a1680744031fdb2d,
|
||||
1, 10, i_a1680744031fdb2d, nullptr, nullptr, { &s_a1680744031fdb2d, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
0xa1680744031fdb2d, b_a1680744031fdb2d.words, 74, d_a1680744031fdb2d, m_a1680744031fdb2d,
|
||||
1, 3, i_a1680744031fdb2d, nullptr, nullptr, { &s_a1680744031fdb2d, nullptr, nullptr, 0, 0, nullptr }, false
|
||||
};
|
||||
#endif // !CAPNP_LITE
|
||||
static const ::capnp::_::AlignedData<34> b_b73df234a23b0cc2 = {
|
||||
|
||||
@@ -855,7 +855,7 @@ struct ModelDataV2SP {
|
||||
|
||||
|
||||
struct _capnpPrivate {
|
||||
CAPNP_DECLARE_STRUCT_HEADER(a1680744031fdb2d, 4, 0)
|
||||
CAPNP_DECLARE_STRUCT_HEADER(a1680744031fdb2d, 1, 0)
|
||||
#if !CAPNP_LITE
|
||||
static constexpr ::capnp::_::RawBrandedSchema const* brand() { return &schema->defaultBrand; }
|
||||
#endif // !CAPNP_LITE
|
||||
@@ -1662,12 +1662,6 @@ public:
|
||||
|
||||
inline ::uint32_t getEta() const;
|
||||
|
||||
inline float getSpeed() const;
|
||||
|
||||
inline bool getCanceling() const;
|
||||
|
||||
inline bool getResuming() const;
|
||||
|
||||
private:
|
||||
::capnp::_::StructReader _reader;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -1705,15 +1699,6 @@ public:
|
||||
inline ::uint32_t getEta();
|
||||
inline void setEta( ::uint32_t value);
|
||||
|
||||
inline float getSpeed();
|
||||
inline void setSpeed(float value);
|
||||
|
||||
inline bool getCanceling();
|
||||
inline void setCanceling(bool value);
|
||||
|
||||
inline bool getResuming();
|
||||
inline void setResuming(bool value);
|
||||
|
||||
private:
|
||||
::capnp::_::StructBuilder _builder;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -4673,20 +4658,6 @@ public:
|
||||
|
||||
inline bool getRightLaneChangeEdgeBlock() const;
|
||||
|
||||
inline float getBlendfulCurvature() const;
|
||||
|
||||
inline float getBlendfulTrust() const;
|
||||
|
||||
inline float getModelCurvature() const;
|
||||
|
||||
inline bool getBlendfulGoingAround() const;
|
||||
|
||||
inline float getBlendfulLeftProb() const;
|
||||
|
||||
inline float getBlendfulRightProb() const;
|
||||
|
||||
inline float getBlendfulLaneWidth() const;
|
||||
|
||||
private:
|
||||
::capnp::_::StructReader _reader;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -4724,27 +4695,6 @@ public:
|
||||
inline bool getRightLaneChangeEdgeBlock();
|
||||
inline void setRightLaneChangeEdgeBlock(bool value);
|
||||
|
||||
inline float getBlendfulCurvature();
|
||||
inline void setBlendfulCurvature(float value);
|
||||
|
||||
inline float getBlendfulTrust();
|
||||
inline void setBlendfulTrust(float value);
|
||||
|
||||
inline float getModelCurvature();
|
||||
inline void setModelCurvature(float value);
|
||||
|
||||
inline bool getBlendfulGoingAround();
|
||||
inline void setBlendfulGoingAround(bool value);
|
||||
|
||||
inline float getBlendfulLeftProb();
|
||||
inline void setBlendfulLeftProb(float value);
|
||||
|
||||
inline float getBlendfulRightProb();
|
||||
inline void setBlendfulRightProb(float value);
|
||||
|
||||
inline float getBlendfulLaneWidth();
|
||||
inline void setBlendfulLaneWidth(float value);
|
||||
|
||||
private:
|
||||
::capnp::_::StructBuilder _builder;
|
||||
template <typename, ::capnp::Kind>
|
||||
@@ -6133,48 +6083,6 @@ inline void ModelManagerSP::DownloadProgress::Builder::setEta( ::uint32_t value)
|
||||
::capnp::bounded<2>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline float ModelManagerSP::DownloadProgress::Reader::getSpeed() const {
|
||||
return _reader.getDataField<float>(
|
||||
::capnp::bounded<3>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline float ModelManagerSP::DownloadProgress::Builder::getSpeed() {
|
||||
return _builder.getDataField<float>(
|
||||
::capnp::bounded<3>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void ModelManagerSP::DownloadProgress::Builder::setSpeed(float value) {
|
||||
_builder.setDataField<float>(
|
||||
::capnp::bounded<3>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline bool ModelManagerSP::DownloadProgress::Reader::getCanceling() const {
|
||||
return _reader.getDataField<bool>(
|
||||
::capnp::bounded<16>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline bool ModelManagerSP::DownloadProgress::Builder::getCanceling() {
|
||||
return _builder.getDataField<bool>(
|
||||
::capnp::bounded<16>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void ModelManagerSP::DownloadProgress::Builder::setCanceling(bool value) {
|
||||
_builder.setDataField<bool>(
|
||||
::capnp::bounded<16>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline bool ModelManagerSP::DownloadProgress::Reader::getResuming() const {
|
||||
return _reader.getDataField<bool>(
|
||||
::capnp::bounded<17>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline bool ModelManagerSP::DownloadProgress::Builder::getResuming() {
|
||||
return _builder.getDataField<bool>(
|
||||
::capnp::bounded<17>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void ModelManagerSP::DownloadProgress::Builder::setResuming(bool value) {
|
||||
_builder.setDataField<bool>(
|
||||
::capnp::bounded<17>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline bool ModelManagerSP::Chunk::Reader::hasFileName() const {
|
||||
return !_reader.getPointerField(
|
||||
::capnp::bounded<0>() * ::capnp::POINTERS).isNull();
|
||||
@@ -9154,104 +9062,6 @@ inline void ModelDataV2SP::Builder::setRightLaneChangeEdgeBlock(bool value) {
|
||||
::capnp::bounded<17>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline float ModelDataV2SP::Reader::getBlendfulCurvature() const {
|
||||
return _reader.getDataField<float>(
|
||||
::capnp::bounded<1>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline float ModelDataV2SP::Builder::getBlendfulCurvature() {
|
||||
return _builder.getDataField<float>(
|
||||
::capnp::bounded<1>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void ModelDataV2SP::Builder::setBlendfulCurvature(float value) {
|
||||
_builder.setDataField<float>(
|
||||
::capnp::bounded<1>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline float ModelDataV2SP::Reader::getBlendfulTrust() const {
|
||||
return _reader.getDataField<float>(
|
||||
::capnp::bounded<2>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline float ModelDataV2SP::Builder::getBlendfulTrust() {
|
||||
return _builder.getDataField<float>(
|
||||
::capnp::bounded<2>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void ModelDataV2SP::Builder::setBlendfulTrust(float value) {
|
||||
_builder.setDataField<float>(
|
||||
::capnp::bounded<2>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline float ModelDataV2SP::Reader::getModelCurvature() const {
|
||||
return _reader.getDataField<float>(
|
||||
::capnp::bounded<3>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline float ModelDataV2SP::Builder::getModelCurvature() {
|
||||
return _builder.getDataField<float>(
|
||||
::capnp::bounded<3>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void ModelDataV2SP::Builder::setModelCurvature(float value) {
|
||||
_builder.setDataField<float>(
|
||||
::capnp::bounded<3>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline bool ModelDataV2SP::Reader::getBlendfulGoingAround() const {
|
||||
return _reader.getDataField<bool>(
|
||||
::capnp::bounded<18>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline bool ModelDataV2SP::Builder::getBlendfulGoingAround() {
|
||||
return _builder.getDataField<bool>(
|
||||
::capnp::bounded<18>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void ModelDataV2SP::Builder::setBlendfulGoingAround(bool value) {
|
||||
_builder.setDataField<bool>(
|
||||
::capnp::bounded<18>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline float ModelDataV2SP::Reader::getBlendfulLeftProb() const {
|
||||
return _reader.getDataField<float>(
|
||||
::capnp::bounded<4>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline float ModelDataV2SP::Builder::getBlendfulLeftProb() {
|
||||
return _builder.getDataField<float>(
|
||||
::capnp::bounded<4>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void ModelDataV2SP::Builder::setBlendfulLeftProb(float value) {
|
||||
_builder.setDataField<float>(
|
||||
::capnp::bounded<4>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline float ModelDataV2SP::Reader::getBlendfulRightProb() const {
|
||||
return _reader.getDataField<float>(
|
||||
::capnp::bounded<5>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline float ModelDataV2SP::Builder::getBlendfulRightProb() {
|
||||
return _builder.getDataField<float>(
|
||||
::capnp::bounded<5>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void ModelDataV2SP::Builder::setBlendfulRightProb(float value) {
|
||||
_builder.setDataField<float>(
|
||||
::capnp::bounded<5>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
inline float ModelDataV2SP::Reader::getBlendfulLaneWidth() const {
|
||||
return _reader.getDataField<float>(
|
||||
::capnp::bounded<6>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
|
||||
inline float ModelDataV2SP::Builder::getBlendfulLaneWidth() {
|
||||
return _builder.getDataField<float>(
|
||||
::capnp::bounded<6>() * ::capnp::ELEMENTS);
|
||||
}
|
||||
inline void ModelDataV2SP::Builder::setBlendfulLaneWidth(float value) {
|
||||
_builder.setDataField<float>(
|
||||
::capnp::bounded<6>() * ::capnp::ELEMENTS, value);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CAPNP_END_HEADER
|
||||
|
||||
Binary file not shown.
@@ -76,7 +76,7 @@ static std::map<std::string, service> services = {
|
||||
{ "carControlSP", {"carControlSP", true, 100.000000, 10, 256000}},
|
||||
{ "carStateSP", {"carStateSP", true, 100.000000, 10, 256000}},
|
||||
{ "liveMapDataSP", {"liveMapDataSP", true, 1.000000, 1, 256000}},
|
||||
{ "modelDataV2SP", {"modelDataV2SP", true, 20.000000, 10, 10485760}},
|
||||
{ "modelDataV2SP", {"modelDataV2SP", true, 20.000000, -1, 10485760}},
|
||||
{ "liveLocationKalman", {"liveLocationKalman", true, 20.000000, -1, 256000}},
|
||||
{ "uiDebug", {"uiDebug", true, 0.000000, 1, 256000}},
|
||||
{ "testJoystick", {"testJoystick", true, 0.000000, -1, 256000}},
|
||||
|
||||
@@ -94,7 +94,7 @@ _services: dict[str, tuple] = {
|
||||
"carControlSP": (True, 100., 10),
|
||||
"carStateSP": (True, 100., 10),
|
||||
"liveMapDataSP": (True, 1., 1),
|
||||
"modelDataV2SP": (True, 20., 10, QueueSize.BIG),
|
||||
"modelDataV2SP": (True, 20., None, QueueSize.BIG),
|
||||
"liveLocationKalman": (True, 20.),
|
||||
|
||||
# debug
|
||||
|
||||
Binary file not shown.
@@ -162,18 +162,12 @@ ParamsBuffer params_key_at(ParamsHandle *handle, size_t index) noexcept {
|
||||
|
||||
size_t params_keys_by_flag(ParamsHandle *handle, uint32_t flag, ParamsBuffer *out, size_t out_size) noexcept {
|
||||
return translate_exceptions(size_t{0}, [&]() {
|
||||
size_t count = 0;
|
||||
// Keep each returned buffer stable and distinct until Python copies it.
|
||||
// return_string() would alias one thread-local buffer for every entry.
|
||||
for (const auto &key : handle->keys) {
|
||||
if (flag == ALL || (handle->params.getKeyFlag(key) & flag)) {
|
||||
if (count < out_size) {
|
||||
out[count] = {key.data(), key.size()};
|
||||
}
|
||||
++count;
|
||||
}
|
||||
auto filtered = handle->params.allKeys(static_cast<ParamKeyFlag>(flag));
|
||||
size_t count = std::min(filtered.size(), out_size);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
out[i] = return_string(filtered[i]);
|
||||
}
|
||||
return count;
|
||||
return filtered.size();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -246,7 +246,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaTSS2Smooth", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
@@ -257,13 +256,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}},
|
||||
{"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
|
||||
{"MapboxNextTurnEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"MapboxTurnLeadDistance", {PERSISTENT | BACKUP, FLOAT, "20.0"}},
|
||||
{"MapboxTurnIntent", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"MapboxTurnContext", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"MapboxTurnCue", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"NavModelState", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON}},
|
||||
{"Blendful", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"PlanplusControl", {PERSISTENT | BACKUP, FLOAT, "1.0"}},
|
||||
|
||||
// mapd
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
"""Filtered registry names must remain distinct across the C/Python boundary."""
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from openpilot.common import params as native
|
||||
|
||||
|
||||
class TestParamsFlagBuffers(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.directory = tempfile.TemporaryDirectory()
|
||||
self.params = native.Params(self.directory.name)
|
||||
|
||||
def tearDown(self):
|
||||
self.params._finalizer()
|
||||
self.directory.cleanup()
|
||||
|
||||
def test_filtered_names_are_unique_registered_keys(self):
|
||||
all_keys = set(self.params.all_keys())
|
||||
for flag in (native.ParamKeyFlag.BACKUP, native.ParamKeyFlag.CLEAR_ON_MANAGER_START, native.ParamKeyFlag.CLEAR_ON_ONROAD_TRANSITION):
|
||||
names = self.params.all_keys(flag)
|
||||
self.assertEqual(len(names), len(set(names)))
|
||||
self.assertTrue(set(names) <= all_keys)
|
||||
backup = set(self.params.all_keys(native.ParamKeyFlag.BACKUP))
|
||||
self.assertTrue({b'MapboxNextTurnEnabled', b'MapboxTurnLeadDistance'} <= backup)
|
||||
self.assertTrue({b'MapboxTurnIntent', b'MapboxTurnContext', b'MapboxTurnCue'}.isdisjoint(backup))
|
||||
|
||||
def test_each_output_buffer_survives_another_c_api_string_result(self):
|
||||
expected = self.params.all_keys(native.ParamKeyFlag.BACKUP)
|
||||
output = (native.ParamsBuffer*1024)()
|
||||
count = native.params_keys_by_flag(self.params.p, int(native.ParamKeyFlag.BACKUP), output, len(output))
|
||||
native.params_get_default(self.params.p, b'LongitudinalPersonality')
|
||||
self.assertEqual([native._copy_string(output[i]) for i in range(count)], expected)
|
||||
|
||||
def test_small_output_array_still_reports_total_matching_key_count(self):
|
||||
expected = self.params.all_keys(native.ParamKeyFlag.BACKUP)
|
||||
output = (native.ParamsBuffer*2)()
|
||||
count = native.params_keys_by_flag(self.params.p, int(native.ParamKeyFlag.BACKUP), output, len(output))
|
||||
self.assertEqual(count, len(expected))
|
||||
self.assertEqual([native._copy_string(output[i]) for i in range(len(output))], expected[:len(output)])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -2,7 +2,7 @@ from openpilot.cereal import log, custom
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, model_turn_probs
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController
|
||||
|
||||
LaneChangeState = log.LaneChangeState
|
||||
LaneChangeDirection = log.LaneChangeDirection
|
||||
@@ -33,28 +33,19 @@ class DesireHelper:
|
||||
def get_lane_change_direction(CS):
|
||||
return LaneChangeDirection.left if CS.leftBlinker else LaneChangeDirection.right
|
||||
|
||||
def update(self, carstate, lateral_active, lane_change_prob, left_edge_detected=False, right_edge_detected=False, desire_prediction=None,
|
||||
model_position=None, map_turn_supported=False):
|
||||
def update(self, carstate, lateral_active, lane_change_prob, left_edge_detected=False, right_edge_detected=False):
|
||||
self.alc.update_params()
|
||||
self.lane_turn_controller.update_params()
|
||||
v_ego = carstate.vEgo
|
||||
one_blinker = carstate.leftBlinker != carstate.rightBlinker
|
||||
below_lane_change_speed = v_ego < LANE_CHANGE_SPEED_MIN
|
||||
lane_change_completed = False
|
||||
|
||||
# Lane turn controller update
|
||||
self.lane_turn_controller.update_lane_turn(blindspot_left=carstate.leftBlindspot, blindspot_right=carstate.rightBlindspot,
|
||||
left_blinker=carstate.leftBlinker, right_blinker=carstate.rightBlinker, v_ego=v_ego,
|
||||
steering_pressed=carstate.steeringPressed, steering_torque=carstate.steeringTorque,
|
||||
turn_probs=model_turn_probs(desire_prediction), lateral_active=lateral_active,
|
||||
model_position=model_position, map_turn_supported=map_turn_supported)
|
||||
left_blinker=carstate.leftBlinker, right_blinker=carstate.rightBlinker, v_ego=v_ego)
|
||||
self.lane_turn_direction = self.lane_turn_controller.get_turn_direction()
|
||||
|
||||
# Only an already committed turn owns the signal. Pending map discovery
|
||||
# leaves the existing lane-change eligibility and timing unchanged.
|
||||
navigation_signal = self.lane_turn_controller.navigation_owns_signal
|
||||
if (navigation_signal or not lateral_active or self.lane_change_timer > LANE_CHANGE_TIME_MAX or
|
||||
self.alc.lane_change_set_timer == AutoLaneChangeMode.OFF):
|
||||
if not lateral_active or self.lane_change_timer > LANE_CHANGE_TIME_MAX or self.alc.lane_change_set_timer == AutoLaneChangeMode.OFF:
|
||||
self.lane_change_state = LaneChangeState.off
|
||||
self.lane_change_direction = LaneChangeDirection.none
|
||||
self.lane_change_timer = 0.0
|
||||
@@ -73,19 +64,16 @@ class DesireHelper:
|
||||
((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or
|
||||
(carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right))
|
||||
|
||||
blindspot_detected = ((carstate.leftBlindspot and self.lane_change_direction == LaneChangeDirection.left) or
|
||||
(carstate.rightBlindspot and self.lane_change_direction == LaneChangeDirection.right))
|
||||
blindspot_detected = (((carstate.leftBlindspot or left_edge_detected) and self.lane_change_direction == LaneChangeDirection.left) or
|
||||
((carstate.rightBlindspot or right_edge_detected) and self.lane_change_direction == LaneChangeDirection.right))
|
||||
|
||||
edge_detected = ((left_edge_detected and self.lane_change_direction == LaneChangeDirection.left) or
|
||||
(right_edge_detected and self.lane_change_direction == LaneChangeDirection.right))
|
||||
|
||||
self.alc.update_lane_change(blindspot_detected, edge_detected, carstate.brakePressed, torque_applied)
|
||||
self.alc.update_lane_change(blindspot_detected, carstate.brakePressed)
|
||||
|
||||
if not one_blinker or below_lane_change_speed:
|
||||
self.lane_change_state = LaneChangeState.off
|
||||
self.lane_change_direction = LaneChangeDirection.none
|
||||
self.lane_change_timer = 0.0
|
||||
elif (torque_applied or self.alc.auto_lane_change_allowed) and not self.alc.lane_change_blocked:
|
||||
elif (torque_applied or self.alc.auto_lane_change_allowed) and not blindspot_detected:
|
||||
self.lane_change_state = LaneChangeState.laneChangeStarting
|
||||
self.lane_change_timer = 0.0
|
||||
|
||||
@@ -93,7 +81,6 @@ class DesireHelper:
|
||||
self.lane_change_timer += DT_MDL
|
||||
|
||||
if lane_change_prob < 0.02 and self.lane_change_timer >= LANE_CHANGE_START_TIME:
|
||||
lane_change_completed = True
|
||||
self.lane_change_timer = 0.0
|
||||
if one_blinker:
|
||||
self.lane_change_state = LaneChangeState.preLaneChange
|
||||
@@ -114,9 +101,4 @@ class DesireHelper:
|
||||
elif self.lane_change_direction == LaneChangeDirection.right:
|
||||
self.desire = log.Desire.laneChangeRight
|
||||
|
||||
# LC wins while active. Only its normal completion releases its ownership;
|
||||
# speed changes, timeout and disengagement do not indicate completion.
|
||||
self.lane_turn_controller.finalize_navigation_intent(
|
||||
self.desire in (log.Desire.laneChangeLeft, log.Desire.laneChangeRight), v_ego, lateral_active, map_turn_supported,
|
||||
lane_change_completed=lane_change_completed)
|
||||
self.alc.update_state()
|
||||
|
||||
@@ -41,326 +41,326 @@ const static double MAHA_THRESH_31 = 3.8414588206941227;
|
||||
* *
|
||||
* This file is part of 'ekf' *
|
||||
******************************************************************************/
|
||||
void err_fun(double *nom_x, double *delta_x, double *out_3585265450871354510) {
|
||||
out_3585265450871354510[0] = delta_x[0] + nom_x[0];
|
||||
out_3585265450871354510[1] = delta_x[1] + nom_x[1];
|
||||
out_3585265450871354510[2] = delta_x[2] + nom_x[2];
|
||||
out_3585265450871354510[3] = delta_x[3] + nom_x[3];
|
||||
out_3585265450871354510[4] = delta_x[4] + nom_x[4];
|
||||
out_3585265450871354510[5] = delta_x[5] + nom_x[5];
|
||||
out_3585265450871354510[6] = delta_x[6] + nom_x[6];
|
||||
out_3585265450871354510[7] = delta_x[7] + nom_x[7];
|
||||
out_3585265450871354510[8] = delta_x[8] + nom_x[8];
|
||||
void err_fun(double *nom_x, double *delta_x, double *out_7360625163823445495) {
|
||||
out_7360625163823445495[0] = delta_x[0] + nom_x[0];
|
||||
out_7360625163823445495[1] = delta_x[1] + nom_x[1];
|
||||
out_7360625163823445495[2] = delta_x[2] + nom_x[2];
|
||||
out_7360625163823445495[3] = delta_x[3] + nom_x[3];
|
||||
out_7360625163823445495[4] = delta_x[4] + nom_x[4];
|
||||
out_7360625163823445495[5] = delta_x[5] + nom_x[5];
|
||||
out_7360625163823445495[6] = delta_x[6] + nom_x[6];
|
||||
out_7360625163823445495[7] = delta_x[7] + nom_x[7];
|
||||
out_7360625163823445495[8] = delta_x[8] + nom_x[8];
|
||||
}
|
||||
void inv_err_fun(double *nom_x, double *true_x, double *out_4534440969468340130) {
|
||||
out_4534440969468340130[0] = -nom_x[0] + true_x[0];
|
||||
out_4534440969468340130[1] = -nom_x[1] + true_x[1];
|
||||
out_4534440969468340130[2] = -nom_x[2] + true_x[2];
|
||||
out_4534440969468340130[3] = -nom_x[3] + true_x[3];
|
||||
out_4534440969468340130[4] = -nom_x[4] + true_x[4];
|
||||
out_4534440969468340130[5] = -nom_x[5] + true_x[5];
|
||||
out_4534440969468340130[6] = -nom_x[6] + true_x[6];
|
||||
out_4534440969468340130[7] = -nom_x[7] + true_x[7];
|
||||
out_4534440969468340130[8] = -nom_x[8] + true_x[8];
|
||||
void inv_err_fun(double *nom_x, double *true_x, double *out_8413491231423970122) {
|
||||
out_8413491231423970122[0] = -nom_x[0] + true_x[0];
|
||||
out_8413491231423970122[1] = -nom_x[1] + true_x[1];
|
||||
out_8413491231423970122[2] = -nom_x[2] + true_x[2];
|
||||
out_8413491231423970122[3] = -nom_x[3] + true_x[3];
|
||||
out_8413491231423970122[4] = -nom_x[4] + true_x[4];
|
||||
out_8413491231423970122[5] = -nom_x[5] + true_x[5];
|
||||
out_8413491231423970122[6] = -nom_x[6] + true_x[6];
|
||||
out_8413491231423970122[7] = -nom_x[7] + true_x[7];
|
||||
out_8413491231423970122[8] = -nom_x[8] + true_x[8];
|
||||
}
|
||||
void H_mod_fun(double *state, double *out_7815971298073658297) {
|
||||
out_7815971298073658297[0] = 1.0;
|
||||
out_7815971298073658297[1] = 0.0;
|
||||
out_7815971298073658297[2] = 0.0;
|
||||
out_7815971298073658297[3] = 0.0;
|
||||
out_7815971298073658297[4] = 0.0;
|
||||
out_7815971298073658297[5] = 0.0;
|
||||
out_7815971298073658297[6] = 0.0;
|
||||
out_7815971298073658297[7] = 0.0;
|
||||
out_7815971298073658297[8] = 0.0;
|
||||
out_7815971298073658297[9] = 0.0;
|
||||
out_7815971298073658297[10] = 1.0;
|
||||
out_7815971298073658297[11] = 0.0;
|
||||
out_7815971298073658297[12] = 0.0;
|
||||
out_7815971298073658297[13] = 0.0;
|
||||
out_7815971298073658297[14] = 0.0;
|
||||
out_7815971298073658297[15] = 0.0;
|
||||
out_7815971298073658297[16] = 0.0;
|
||||
out_7815971298073658297[17] = 0.0;
|
||||
out_7815971298073658297[18] = 0.0;
|
||||
out_7815971298073658297[19] = 0.0;
|
||||
out_7815971298073658297[20] = 1.0;
|
||||
out_7815971298073658297[21] = 0.0;
|
||||
out_7815971298073658297[22] = 0.0;
|
||||
out_7815971298073658297[23] = 0.0;
|
||||
out_7815971298073658297[24] = 0.0;
|
||||
out_7815971298073658297[25] = 0.0;
|
||||
out_7815971298073658297[26] = 0.0;
|
||||
out_7815971298073658297[27] = 0.0;
|
||||
out_7815971298073658297[28] = 0.0;
|
||||
out_7815971298073658297[29] = 0.0;
|
||||
out_7815971298073658297[30] = 1.0;
|
||||
out_7815971298073658297[31] = 0.0;
|
||||
out_7815971298073658297[32] = 0.0;
|
||||
out_7815971298073658297[33] = 0.0;
|
||||
out_7815971298073658297[34] = 0.0;
|
||||
out_7815971298073658297[35] = 0.0;
|
||||
out_7815971298073658297[36] = 0.0;
|
||||
out_7815971298073658297[37] = 0.0;
|
||||
out_7815971298073658297[38] = 0.0;
|
||||
out_7815971298073658297[39] = 0.0;
|
||||
out_7815971298073658297[40] = 1.0;
|
||||
out_7815971298073658297[41] = 0.0;
|
||||
out_7815971298073658297[42] = 0.0;
|
||||
out_7815971298073658297[43] = 0.0;
|
||||
out_7815971298073658297[44] = 0.0;
|
||||
out_7815971298073658297[45] = 0.0;
|
||||
out_7815971298073658297[46] = 0.0;
|
||||
out_7815971298073658297[47] = 0.0;
|
||||
out_7815971298073658297[48] = 0.0;
|
||||
out_7815971298073658297[49] = 0.0;
|
||||
out_7815971298073658297[50] = 1.0;
|
||||
out_7815971298073658297[51] = 0.0;
|
||||
out_7815971298073658297[52] = 0.0;
|
||||
out_7815971298073658297[53] = 0.0;
|
||||
out_7815971298073658297[54] = 0.0;
|
||||
out_7815971298073658297[55] = 0.0;
|
||||
out_7815971298073658297[56] = 0.0;
|
||||
out_7815971298073658297[57] = 0.0;
|
||||
out_7815971298073658297[58] = 0.0;
|
||||
out_7815971298073658297[59] = 0.0;
|
||||
out_7815971298073658297[60] = 1.0;
|
||||
out_7815971298073658297[61] = 0.0;
|
||||
out_7815971298073658297[62] = 0.0;
|
||||
out_7815971298073658297[63] = 0.0;
|
||||
out_7815971298073658297[64] = 0.0;
|
||||
out_7815971298073658297[65] = 0.0;
|
||||
out_7815971298073658297[66] = 0.0;
|
||||
out_7815971298073658297[67] = 0.0;
|
||||
out_7815971298073658297[68] = 0.0;
|
||||
out_7815971298073658297[69] = 0.0;
|
||||
out_7815971298073658297[70] = 1.0;
|
||||
out_7815971298073658297[71] = 0.0;
|
||||
out_7815971298073658297[72] = 0.0;
|
||||
out_7815971298073658297[73] = 0.0;
|
||||
out_7815971298073658297[74] = 0.0;
|
||||
out_7815971298073658297[75] = 0.0;
|
||||
out_7815971298073658297[76] = 0.0;
|
||||
out_7815971298073658297[77] = 0.0;
|
||||
out_7815971298073658297[78] = 0.0;
|
||||
out_7815971298073658297[79] = 0.0;
|
||||
out_7815971298073658297[80] = 1.0;
|
||||
void H_mod_fun(double *state, double *out_2632326835772732722) {
|
||||
out_2632326835772732722[0] = 1.0;
|
||||
out_2632326835772732722[1] = 0.0;
|
||||
out_2632326835772732722[2] = 0.0;
|
||||
out_2632326835772732722[3] = 0.0;
|
||||
out_2632326835772732722[4] = 0.0;
|
||||
out_2632326835772732722[5] = 0.0;
|
||||
out_2632326835772732722[6] = 0.0;
|
||||
out_2632326835772732722[7] = 0.0;
|
||||
out_2632326835772732722[8] = 0.0;
|
||||
out_2632326835772732722[9] = 0.0;
|
||||
out_2632326835772732722[10] = 1.0;
|
||||
out_2632326835772732722[11] = 0.0;
|
||||
out_2632326835772732722[12] = 0.0;
|
||||
out_2632326835772732722[13] = 0.0;
|
||||
out_2632326835772732722[14] = 0.0;
|
||||
out_2632326835772732722[15] = 0.0;
|
||||
out_2632326835772732722[16] = 0.0;
|
||||
out_2632326835772732722[17] = 0.0;
|
||||
out_2632326835772732722[18] = 0.0;
|
||||
out_2632326835772732722[19] = 0.0;
|
||||
out_2632326835772732722[20] = 1.0;
|
||||
out_2632326835772732722[21] = 0.0;
|
||||
out_2632326835772732722[22] = 0.0;
|
||||
out_2632326835772732722[23] = 0.0;
|
||||
out_2632326835772732722[24] = 0.0;
|
||||
out_2632326835772732722[25] = 0.0;
|
||||
out_2632326835772732722[26] = 0.0;
|
||||
out_2632326835772732722[27] = 0.0;
|
||||
out_2632326835772732722[28] = 0.0;
|
||||
out_2632326835772732722[29] = 0.0;
|
||||
out_2632326835772732722[30] = 1.0;
|
||||
out_2632326835772732722[31] = 0.0;
|
||||
out_2632326835772732722[32] = 0.0;
|
||||
out_2632326835772732722[33] = 0.0;
|
||||
out_2632326835772732722[34] = 0.0;
|
||||
out_2632326835772732722[35] = 0.0;
|
||||
out_2632326835772732722[36] = 0.0;
|
||||
out_2632326835772732722[37] = 0.0;
|
||||
out_2632326835772732722[38] = 0.0;
|
||||
out_2632326835772732722[39] = 0.0;
|
||||
out_2632326835772732722[40] = 1.0;
|
||||
out_2632326835772732722[41] = 0.0;
|
||||
out_2632326835772732722[42] = 0.0;
|
||||
out_2632326835772732722[43] = 0.0;
|
||||
out_2632326835772732722[44] = 0.0;
|
||||
out_2632326835772732722[45] = 0.0;
|
||||
out_2632326835772732722[46] = 0.0;
|
||||
out_2632326835772732722[47] = 0.0;
|
||||
out_2632326835772732722[48] = 0.0;
|
||||
out_2632326835772732722[49] = 0.0;
|
||||
out_2632326835772732722[50] = 1.0;
|
||||
out_2632326835772732722[51] = 0.0;
|
||||
out_2632326835772732722[52] = 0.0;
|
||||
out_2632326835772732722[53] = 0.0;
|
||||
out_2632326835772732722[54] = 0.0;
|
||||
out_2632326835772732722[55] = 0.0;
|
||||
out_2632326835772732722[56] = 0.0;
|
||||
out_2632326835772732722[57] = 0.0;
|
||||
out_2632326835772732722[58] = 0.0;
|
||||
out_2632326835772732722[59] = 0.0;
|
||||
out_2632326835772732722[60] = 1.0;
|
||||
out_2632326835772732722[61] = 0.0;
|
||||
out_2632326835772732722[62] = 0.0;
|
||||
out_2632326835772732722[63] = 0.0;
|
||||
out_2632326835772732722[64] = 0.0;
|
||||
out_2632326835772732722[65] = 0.0;
|
||||
out_2632326835772732722[66] = 0.0;
|
||||
out_2632326835772732722[67] = 0.0;
|
||||
out_2632326835772732722[68] = 0.0;
|
||||
out_2632326835772732722[69] = 0.0;
|
||||
out_2632326835772732722[70] = 1.0;
|
||||
out_2632326835772732722[71] = 0.0;
|
||||
out_2632326835772732722[72] = 0.0;
|
||||
out_2632326835772732722[73] = 0.0;
|
||||
out_2632326835772732722[74] = 0.0;
|
||||
out_2632326835772732722[75] = 0.0;
|
||||
out_2632326835772732722[76] = 0.0;
|
||||
out_2632326835772732722[77] = 0.0;
|
||||
out_2632326835772732722[78] = 0.0;
|
||||
out_2632326835772732722[79] = 0.0;
|
||||
out_2632326835772732722[80] = 1.0;
|
||||
}
|
||||
void f_fun(double *state, double dt, double *out_3011916394083426809) {
|
||||
out_3011916394083426809[0] = state[0];
|
||||
out_3011916394083426809[1] = state[1];
|
||||
out_3011916394083426809[2] = state[2];
|
||||
out_3011916394083426809[3] = state[3];
|
||||
out_3011916394083426809[4] = state[4];
|
||||
out_3011916394083426809[5] = dt*(stiffness_front*(-state[2] - state[3] + state[7])*state[0]/state[1] + (-stiffness_front*state[0] - stiffness_rear*state[0])*state[5]/state[4] + ((-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/state[4] - state[4])*state[6] - 9.8100000000000005*state[8]) + state[5];
|
||||
out_3011916394083426809[6] = dt*(center_to_front*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(rotational_inertia*state[1]) + (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])*state[5]/(rotational_inertia*state[4]) + (-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])*state[6]/(rotational_inertia*state[4])) + state[6];
|
||||
out_3011916394083426809[7] = state[7];
|
||||
out_3011916394083426809[8] = state[8];
|
||||
void f_fun(double *state, double dt, double *out_8532698894052839577) {
|
||||
out_8532698894052839577[0] = state[0];
|
||||
out_8532698894052839577[1] = state[1];
|
||||
out_8532698894052839577[2] = state[2];
|
||||
out_8532698894052839577[3] = state[3];
|
||||
out_8532698894052839577[4] = state[4];
|
||||
out_8532698894052839577[5] = dt*(stiffness_front*(-state[2] - state[3] + state[7])*state[0]/state[1] + (-stiffness_front*state[0] - stiffness_rear*state[0])*state[5]/state[4] + ((-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/state[4] - state[4])*state[6] - 9.8100000000000005*state[8]) + state[5];
|
||||
out_8532698894052839577[6] = dt*(center_to_front*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(rotational_inertia*state[1]) + (-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])*state[5]/(rotational_inertia*state[4]) + (-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])*state[6]/(rotational_inertia*state[4])) + state[6];
|
||||
out_8532698894052839577[7] = state[7];
|
||||
out_8532698894052839577[8] = state[8];
|
||||
}
|
||||
void F_fun(double *state, double dt, double *out_4768916904116545459) {
|
||||
out_4768916904116545459[0] = 1;
|
||||
out_4768916904116545459[1] = 0;
|
||||
out_4768916904116545459[2] = 0;
|
||||
out_4768916904116545459[3] = 0;
|
||||
out_4768916904116545459[4] = 0;
|
||||
out_4768916904116545459[5] = 0;
|
||||
out_4768916904116545459[6] = 0;
|
||||
out_4768916904116545459[7] = 0;
|
||||
out_4768916904116545459[8] = 0;
|
||||
out_4768916904116545459[9] = 0;
|
||||
out_4768916904116545459[10] = 1;
|
||||
out_4768916904116545459[11] = 0;
|
||||
out_4768916904116545459[12] = 0;
|
||||
out_4768916904116545459[13] = 0;
|
||||
out_4768916904116545459[14] = 0;
|
||||
out_4768916904116545459[15] = 0;
|
||||
out_4768916904116545459[16] = 0;
|
||||
out_4768916904116545459[17] = 0;
|
||||
out_4768916904116545459[18] = 0;
|
||||
out_4768916904116545459[19] = 0;
|
||||
out_4768916904116545459[20] = 1;
|
||||
out_4768916904116545459[21] = 0;
|
||||
out_4768916904116545459[22] = 0;
|
||||
out_4768916904116545459[23] = 0;
|
||||
out_4768916904116545459[24] = 0;
|
||||
out_4768916904116545459[25] = 0;
|
||||
out_4768916904116545459[26] = 0;
|
||||
out_4768916904116545459[27] = 0;
|
||||
out_4768916904116545459[28] = 0;
|
||||
out_4768916904116545459[29] = 0;
|
||||
out_4768916904116545459[30] = 1;
|
||||
out_4768916904116545459[31] = 0;
|
||||
out_4768916904116545459[32] = 0;
|
||||
out_4768916904116545459[33] = 0;
|
||||
out_4768916904116545459[34] = 0;
|
||||
out_4768916904116545459[35] = 0;
|
||||
out_4768916904116545459[36] = 0;
|
||||
out_4768916904116545459[37] = 0;
|
||||
out_4768916904116545459[38] = 0;
|
||||
out_4768916904116545459[39] = 0;
|
||||
out_4768916904116545459[40] = 1;
|
||||
out_4768916904116545459[41] = 0;
|
||||
out_4768916904116545459[42] = 0;
|
||||
out_4768916904116545459[43] = 0;
|
||||
out_4768916904116545459[44] = 0;
|
||||
out_4768916904116545459[45] = dt*(stiffness_front*(-state[2] - state[3] + state[7])/state[1] + (-stiffness_front - stiffness_rear)*state[5]/state[4] + (-center_to_front*stiffness_front + center_to_rear*stiffness_rear)*state[6]/state[4]);
|
||||
out_4768916904116545459[46] = -dt*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/pow(state[1], 2);
|
||||
out_4768916904116545459[47] = -dt*stiffness_front*state[0]/state[1];
|
||||
out_4768916904116545459[48] = -dt*stiffness_front*state[0]/state[1];
|
||||
out_4768916904116545459[49] = dt*(-(-stiffness_front*state[0] - stiffness_rear*state[0])*state[5]/pow(state[4], 2) + (-(-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/pow(state[4], 2) - 1)*state[6]);
|
||||
out_4768916904116545459[50] = dt*(-stiffness_front*state[0] - stiffness_rear*state[0])/state[4] + 1;
|
||||
out_4768916904116545459[51] = dt*((-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/state[4] - state[4]);
|
||||
out_4768916904116545459[52] = dt*stiffness_front*state[0]/state[1];
|
||||
out_4768916904116545459[53] = -9.8100000000000005*dt;
|
||||
out_4768916904116545459[54] = dt*(center_to_front*stiffness_front*(-state[2] - state[3] + state[7])/(rotational_inertia*state[1]) + (-center_to_front*stiffness_front + center_to_rear*stiffness_rear)*state[5]/(rotational_inertia*state[4]) + (-pow(center_to_front, 2)*stiffness_front - pow(center_to_rear, 2)*stiffness_rear)*state[6]/(rotational_inertia*state[4]));
|
||||
out_4768916904116545459[55] = -center_to_front*dt*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(rotational_inertia*pow(state[1], 2));
|
||||
out_4768916904116545459[56] = -center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_4768916904116545459[57] = -center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_4768916904116545459[58] = dt*(-(-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])*state[5]/(rotational_inertia*pow(state[4], 2)) - (-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])*state[6]/(rotational_inertia*pow(state[4], 2)));
|
||||
out_4768916904116545459[59] = dt*(-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(rotational_inertia*state[4]);
|
||||
out_4768916904116545459[60] = dt*(-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])/(rotational_inertia*state[4]) + 1;
|
||||
out_4768916904116545459[61] = center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_4768916904116545459[62] = 0;
|
||||
out_4768916904116545459[63] = 0;
|
||||
out_4768916904116545459[64] = 0;
|
||||
out_4768916904116545459[65] = 0;
|
||||
out_4768916904116545459[66] = 0;
|
||||
out_4768916904116545459[67] = 0;
|
||||
out_4768916904116545459[68] = 0;
|
||||
out_4768916904116545459[69] = 0;
|
||||
out_4768916904116545459[70] = 1;
|
||||
out_4768916904116545459[71] = 0;
|
||||
out_4768916904116545459[72] = 0;
|
||||
out_4768916904116545459[73] = 0;
|
||||
out_4768916904116545459[74] = 0;
|
||||
out_4768916904116545459[75] = 0;
|
||||
out_4768916904116545459[76] = 0;
|
||||
out_4768916904116545459[77] = 0;
|
||||
out_4768916904116545459[78] = 0;
|
||||
out_4768916904116545459[79] = 0;
|
||||
out_4768916904116545459[80] = 1;
|
||||
void F_fun(double *state, double dt, double *out_5046006616921834778) {
|
||||
out_5046006616921834778[0] = 1;
|
||||
out_5046006616921834778[1] = 0;
|
||||
out_5046006616921834778[2] = 0;
|
||||
out_5046006616921834778[3] = 0;
|
||||
out_5046006616921834778[4] = 0;
|
||||
out_5046006616921834778[5] = 0;
|
||||
out_5046006616921834778[6] = 0;
|
||||
out_5046006616921834778[7] = 0;
|
||||
out_5046006616921834778[8] = 0;
|
||||
out_5046006616921834778[9] = 0;
|
||||
out_5046006616921834778[10] = 1;
|
||||
out_5046006616921834778[11] = 0;
|
||||
out_5046006616921834778[12] = 0;
|
||||
out_5046006616921834778[13] = 0;
|
||||
out_5046006616921834778[14] = 0;
|
||||
out_5046006616921834778[15] = 0;
|
||||
out_5046006616921834778[16] = 0;
|
||||
out_5046006616921834778[17] = 0;
|
||||
out_5046006616921834778[18] = 0;
|
||||
out_5046006616921834778[19] = 0;
|
||||
out_5046006616921834778[20] = 1;
|
||||
out_5046006616921834778[21] = 0;
|
||||
out_5046006616921834778[22] = 0;
|
||||
out_5046006616921834778[23] = 0;
|
||||
out_5046006616921834778[24] = 0;
|
||||
out_5046006616921834778[25] = 0;
|
||||
out_5046006616921834778[26] = 0;
|
||||
out_5046006616921834778[27] = 0;
|
||||
out_5046006616921834778[28] = 0;
|
||||
out_5046006616921834778[29] = 0;
|
||||
out_5046006616921834778[30] = 1;
|
||||
out_5046006616921834778[31] = 0;
|
||||
out_5046006616921834778[32] = 0;
|
||||
out_5046006616921834778[33] = 0;
|
||||
out_5046006616921834778[34] = 0;
|
||||
out_5046006616921834778[35] = 0;
|
||||
out_5046006616921834778[36] = 0;
|
||||
out_5046006616921834778[37] = 0;
|
||||
out_5046006616921834778[38] = 0;
|
||||
out_5046006616921834778[39] = 0;
|
||||
out_5046006616921834778[40] = 1;
|
||||
out_5046006616921834778[41] = 0;
|
||||
out_5046006616921834778[42] = 0;
|
||||
out_5046006616921834778[43] = 0;
|
||||
out_5046006616921834778[44] = 0;
|
||||
out_5046006616921834778[45] = dt*(stiffness_front*(-state[2] - state[3] + state[7])/state[1] + (-stiffness_front - stiffness_rear)*state[5]/state[4] + (-center_to_front*stiffness_front + center_to_rear*stiffness_rear)*state[6]/state[4]);
|
||||
out_5046006616921834778[46] = -dt*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/pow(state[1], 2);
|
||||
out_5046006616921834778[47] = -dt*stiffness_front*state[0]/state[1];
|
||||
out_5046006616921834778[48] = -dt*stiffness_front*state[0]/state[1];
|
||||
out_5046006616921834778[49] = dt*(-(-stiffness_front*state[0] - stiffness_rear*state[0])*state[5]/pow(state[4], 2) + (-(-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/pow(state[4], 2) - 1)*state[6]);
|
||||
out_5046006616921834778[50] = dt*(-stiffness_front*state[0] - stiffness_rear*state[0])/state[4] + 1;
|
||||
out_5046006616921834778[51] = dt*((-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/state[4] - state[4]);
|
||||
out_5046006616921834778[52] = dt*stiffness_front*state[0]/state[1];
|
||||
out_5046006616921834778[53] = -9.8100000000000005*dt;
|
||||
out_5046006616921834778[54] = dt*(center_to_front*stiffness_front*(-state[2] - state[3] + state[7])/(rotational_inertia*state[1]) + (-center_to_front*stiffness_front + center_to_rear*stiffness_rear)*state[5]/(rotational_inertia*state[4]) + (-pow(center_to_front, 2)*stiffness_front - pow(center_to_rear, 2)*stiffness_rear)*state[6]/(rotational_inertia*state[4]));
|
||||
out_5046006616921834778[55] = -center_to_front*dt*stiffness_front*(-state[2] - state[3] + state[7])*state[0]/(rotational_inertia*pow(state[1], 2));
|
||||
out_5046006616921834778[56] = -center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_5046006616921834778[57] = -center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_5046006616921834778[58] = dt*(-(-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])*state[5]/(rotational_inertia*pow(state[4], 2)) - (-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])*state[6]/(rotational_inertia*pow(state[4], 2)));
|
||||
out_5046006616921834778[59] = dt*(-center_to_front*stiffness_front*state[0] + center_to_rear*stiffness_rear*state[0])/(rotational_inertia*state[4]);
|
||||
out_5046006616921834778[60] = dt*(-pow(center_to_front, 2)*stiffness_front*state[0] - pow(center_to_rear, 2)*stiffness_rear*state[0])/(rotational_inertia*state[4]) + 1;
|
||||
out_5046006616921834778[61] = center_to_front*dt*stiffness_front*state[0]/(rotational_inertia*state[1]);
|
||||
out_5046006616921834778[62] = 0;
|
||||
out_5046006616921834778[63] = 0;
|
||||
out_5046006616921834778[64] = 0;
|
||||
out_5046006616921834778[65] = 0;
|
||||
out_5046006616921834778[66] = 0;
|
||||
out_5046006616921834778[67] = 0;
|
||||
out_5046006616921834778[68] = 0;
|
||||
out_5046006616921834778[69] = 0;
|
||||
out_5046006616921834778[70] = 1;
|
||||
out_5046006616921834778[71] = 0;
|
||||
out_5046006616921834778[72] = 0;
|
||||
out_5046006616921834778[73] = 0;
|
||||
out_5046006616921834778[74] = 0;
|
||||
out_5046006616921834778[75] = 0;
|
||||
out_5046006616921834778[76] = 0;
|
||||
out_5046006616921834778[77] = 0;
|
||||
out_5046006616921834778[78] = 0;
|
||||
out_5046006616921834778[79] = 0;
|
||||
out_5046006616921834778[80] = 1;
|
||||
}
|
||||
void h_25(double *state, double *unused, double *out_8627167620031889565) {
|
||||
out_8627167620031889565[0] = state[6];
|
||||
void h_25(double *state, double *unused, double *out_5546638875155758793) {
|
||||
out_5546638875155758793[0] = state[6];
|
||||
}
|
||||
void H_25(double *state, double *unused, double *out_1719062931467283681) {
|
||||
out_1719062931467283681[0] = 0;
|
||||
out_1719062931467283681[1] = 0;
|
||||
out_1719062931467283681[2] = 0;
|
||||
out_1719062931467283681[3] = 0;
|
||||
out_1719062931467283681[4] = 0;
|
||||
out_1719062931467283681[5] = 0;
|
||||
out_1719062931467283681[6] = 1;
|
||||
out_1719062931467283681[7] = 0;
|
||||
out_1719062931467283681[8] = 0;
|
||||
void H_25(double *state, double *unused, double *out_890653814216758955) {
|
||||
out_890653814216758955[0] = 0;
|
||||
out_890653814216758955[1] = 0;
|
||||
out_890653814216758955[2] = 0;
|
||||
out_890653814216758955[3] = 0;
|
||||
out_890653814216758955[4] = 0;
|
||||
out_890653814216758955[5] = 0;
|
||||
out_890653814216758955[6] = 1;
|
||||
out_890653814216758955[7] = 0;
|
||||
out_890653814216758955[8] = 0;
|
||||
}
|
||||
void h_24(double *state, double *unused, double *out_5179238137807796494) {
|
||||
out_5179238137807796494[0] = state[4];
|
||||
out_5179238137807796494[1] = state[5];
|
||||
void h_24(double *state, double *unused, double *out_4367318711229477977) {
|
||||
out_4367318711229477977[0] = state[4];
|
||||
out_4367318711229477977[1] = state[5];
|
||||
}
|
||||
void H_24(double *state, double *unused, double *out_453586667538215885) {
|
||||
out_453586667538215885[0] = 0;
|
||||
out_453586667538215885[1] = 0;
|
||||
out_453586667538215885[2] = 0;
|
||||
out_453586667538215885[3] = 0;
|
||||
out_453586667538215885[4] = 1;
|
||||
out_453586667538215885[5] = 0;
|
||||
out_453586667538215885[6] = 0;
|
||||
out_453586667538215885[7] = 0;
|
||||
out_453586667538215885[8] = 0;
|
||||
out_453586667538215885[9] = 0;
|
||||
out_453586667538215885[10] = 0;
|
||||
out_453586667538215885[11] = 0;
|
||||
out_453586667538215885[12] = 0;
|
||||
out_453586667538215885[13] = 0;
|
||||
out_453586667538215885[14] = 1;
|
||||
out_453586667538215885[15] = 0;
|
||||
out_453586667538215885[16] = 0;
|
||||
out_453586667538215885[17] = 0;
|
||||
void H_24(double *state, double *unused, double *out_4448579413019082091) {
|
||||
out_4448579413019082091[0] = 0;
|
||||
out_4448579413019082091[1] = 0;
|
||||
out_4448579413019082091[2] = 0;
|
||||
out_4448579413019082091[3] = 0;
|
||||
out_4448579413019082091[4] = 1;
|
||||
out_4448579413019082091[5] = 0;
|
||||
out_4448579413019082091[6] = 0;
|
||||
out_4448579413019082091[7] = 0;
|
||||
out_4448579413019082091[8] = 0;
|
||||
out_4448579413019082091[9] = 0;
|
||||
out_4448579413019082091[10] = 0;
|
||||
out_4448579413019082091[11] = 0;
|
||||
out_4448579413019082091[12] = 0;
|
||||
out_4448579413019082091[13] = 0;
|
||||
out_4448579413019082091[14] = 1;
|
||||
out_4448579413019082091[15] = 0;
|
||||
out_4448579413019082091[16] = 0;
|
||||
out_4448579413019082091[17] = 0;
|
||||
}
|
||||
void h_30(double *state, double *unused, double *out_4528864321752517701) {
|
||||
out_4528864321752517701[0] = state[4];
|
||||
void h_30(double *state, double *unused, double *out_1909189346901736443) {
|
||||
out_1909189346901736443[0] = state[4];
|
||||
}
|
||||
void H_30(double *state, double *unused, double *out_4237395889974532308) {
|
||||
out_4237395889974532308[0] = 0;
|
||||
out_4237395889974532308[1] = 0;
|
||||
out_4237395889974532308[2] = 0;
|
||||
out_4237395889974532308[3] = 0;
|
||||
out_4237395889974532308[4] = 1;
|
||||
out_4237395889974532308[5] = 0;
|
||||
out_4237395889974532308[6] = 0;
|
||||
out_4237395889974532308[7] = 0;
|
||||
out_4237395889974532308[8] = 0;
|
||||
void H_30(double *state, double *unused, double *out_7807344155708375710) {
|
||||
out_7807344155708375710[0] = 0;
|
||||
out_7807344155708375710[1] = 0;
|
||||
out_7807344155708375710[2] = 0;
|
||||
out_7807344155708375710[3] = 0;
|
||||
out_7807344155708375710[4] = 1;
|
||||
out_7807344155708375710[5] = 0;
|
||||
out_7807344155708375710[6] = 0;
|
||||
out_7807344155708375710[7] = 0;
|
||||
out_7807344155708375710[8] = 0;
|
||||
}
|
||||
void h_26(double *state, double *unused, double *out_5472374174654180136) {
|
||||
out_5472374174654180136[0] = state[7];
|
||||
void h_26(double *state, double *unused, double *out_6910862614562582088) {
|
||||
out_6910862614562582088[0] = state[7];
|
||||
}
|
||||
void H_26(double *state, double *unused, double *out_2022440387406772543) {
|
||||
out_2022440387406772543[0] = 0;
|
||||
out_2022440387406772543[1] = 0;
|
||||
out_2022440387406772543[2] = 0;
|
||||
out_2022440387406772543[3] = 0;
|
||||
out_2022440387406772543[4] = 0;
|
||||
out_2022440387406772543[5] = 0;
|
||||
out_2022440387406772543[6] = 0;
|
||||
out_2022440387406772543[7] = 1;
|
||||
out_2022440387406772543[8] = 0;
|
||||
void H_26(double *state, double *unused, double *out_2850849504657297269) {
|
||||
out_2850849504657297269[0] = 0;
|
||||
out_2850849504657297269[1] = 0;
|
||||
out_2850849504657297269[2] = 0;
|
||||
out_2850849504657297269[3] = 0;
|
||||
out_2850849504657297269[4] = 0;
|
||||
out_2850849504657297269[5] = 0;
|
||||
out_2850849504657297269[6] = 0;
|
||||
out_2850849504657297269[7] = 1;
|
||||
out_2850849504657297269[8] = 0;
|
||||
}
|
||||
void h_27(double *state, double *unused, double *out_2749092444325053967) {
|
||||
out_2749092444325053967[0] = state[3];
|
||||
void h_27(double *state, double *unused, double *out_3918136026546952058) {
|
||||
out_3918136026546952058[0] = state[3];
|
||||
}
|
||||
void H_27(double *state, double *unused, double *out_6460989961158475525) {
|
||||
out_6460989961158475525[0] = 0;
|
||||
out_6460989961158475525[1] = 0;
|
||||
out_6460989961158475525[2] = 0;
|
||||
out_6460989961158475525[3] = 1;
|
||||
out_6460989961158475525[4] = 0;
|
||||
out_6460989961158475525[5] = 0;
|
||||
out_6460989961158475525[6] = 0;
|
||||
out_6460989961158475525[7] = 0;
|
||||
out_6460989961158475525[8] = 0;
|
||||
void H_27(double *state, double *unused, double *out_5632580843907950799) {
|
||||
out_5632580843907950799[0] = 0;
|
||||
out_5632580843907950799[1] = 0;
|
||||
out_5632580843907950799[2] = 0;
|
||||
out_5632580843907950799[3] = 1;
|
||||
out_5632580843907950799[4] = 0;
|
||||
out_5632580843907950799[5] = 0;
|
||||
out_5632580843907950799[6] = 0;
|
||||
out_5632580843907950799[7] = 0;
|
||||
out_5632580843907950799[8] = 0;
|
||||
}
|
||||
void h_29(double *state, double *unused, double *out_5132180206428995218) {
|
||||
out_5132180206428995218[0] = state[1];
|
||||
void h_29(double *state, double *unused, double *out_3092483920759109202) {
|
||||
out_3092483920759109202[0] = state[1];
|
||||
}
|
||||
void H_29(double *state, double *unused, double *out_4747627234288924492) {
|
||||
out_4747627234288924492[0] = 0;
|
||||
out_4747627234288924492[1] = 1;
|
||||
out_4747627234288924492[2] = 0;
|
||||
out_4747627234288924492[3] = 0;
|
||||
out_4747627234288924492[4] = 0;
|
||||
out_4747627234288924492[5] = 0;
|
||||
out_4747627234288924492[6] = 0;
|
||||
out_4747627234288924492[7] = 0;
|
||||
out_4747627234288924492[8] = 0;
|
||||
void H_29(double *state, double *unused, double *out_8317575500022767894) {
|
||||
out_8317575500022767894[0] = 0;
|
||||
out_8317575500022767894[1] = 1;
|
||||
out_8317575500022767894[2] = 0;
|
||||
out_8317575500022767894[3] = 0;
|
||||
out_8317575500022767894[4] = 0;
|
||||
out_8317575500022767894[5] = 0;
|
||||
out_8317575500022767894[6] = 0;
|
||||
out_8317575500022767894[7] = 0;
|
||||
out_8317575500022767894[8] = 0;
|
||||
}
|
||||
void h_28(double *state, double *unused, double *out_9148133961738903113) {
|
||||
out_9148133961738903113[0] = state[0];
|
||||
void h_28(double *state, double *unused, double *out_4521451911223429575) {
|
||||
out_4521451911223429575[0] = state[0];
|
||||
}
|
||||
void H_28(double *state, double *unused, double *out_334771782780606082) {
|
||||
out_334771782780606082[0] = 1;
|
||||
out_334771782780606082[1] = 0;
|
||||
out_334771782780606082[2] = 0;
|
||||
out_334771782780606082[3] = 0;
|
||||
out_334771782780606082[4] = 0;
|
||||
out_334771782780606082[5] = 0;
|
||||
out_334771782780606082[6] = 0;
|
||||
out_334771782780606082[7] = 0;
|
||||
out_334771782780606082[8] = 0;
|
||||
void H_28(double *state, double *unused, double *out_1163180900031130808) {
|
||||
out_1163180900031130808[0] = 1;
|
||||
out_1163180900031130808[1] = 0;
|
||||
out_1163180900031130808[2] = 0;
|
||||
out_1163180900031130808[3] = 0;
|
||||
out_1163180900031130808[4] = 0;
|
||||
out_1163180900031130808[5] = 0;
|
||||
out_1163180900031130808[6] = 0;
|
||||
out_1163180900031130808[7] = 0;
|
||||
out_1163180900031130808[8] = 0;
|
||||
}
|
||||
void h_31(double *state, double *unused, double *out_745894969277840380) {
|
||||
out_745894969277840380[0] = state[8];
|
||||
void h_31(double *state, double *unused, double *out_5821832937440264682) {
|
||||
out_5821832937440264682[0] = state[8];
|
||||
}
|
||||
void H_31(double *state, double *unused, double *out_2648648489640124019) {
|
||||
out_2648648489640124019[0] = 0;
|
||||
out_2648648489640124019[1] = 0;
|
||||
out_2648648489640124019[2] = 0;
|
||||
out_2648648489640124019[3] = 0;
|
||||
out_2648648489640124019[4] = 0;
|
||||
out_2648648489640124019[5] = 0;
|
||||
out_2648648489640124019[6] = 0;
|
||||
out_2648648489640124019[7] = 0;
|
||||
out_2648648489640124019[8] = 1;
|
||||
void H_31(double *state, double *unused, double *out_921299776093719383) {
|
||||
out_921299776093719383[0] = 0;
|
||||
out_921299776093719383[1] = 0;
|
||||
out_921299776093719383[2] = 0;
|
||||
out_921299776093719383[3] = 0;
|
||||
out_921299776093719383[4] = 0;
|
||||
out_921299776093719383[5] = 0;
|
||||
out_921299776093719383[6] = 0;
|
||||
out_921299776093719383[7] = 0;
|
||||
out_921299776093719383[8] = 1;
|
||||
}
|
||||
#include <eigen3/Eigen/Dense>
|
||||
#include <iostream>
|
||||
@@ -514,68 +514,68 @@ void car_update_28(double *in_x, double *in_P, double *in_z, double *in_R, doubl
|
||||
void car_update_31(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea) {
|
||||
update<1, 3, 0>(in_x, in_P, h_31, H_31, NULL, in_z, in_R, in_ea, MAHA_THRESH_31);
|
||||
}
|
||||
void car_err_fun(double *nom_x, double *delta_x, double *out_3585265450871354510) {
|
||||
err_fun(nom_x, delta_x, out_3585265450871354510);
|
||||
void car_err_fun(double *nom_x, double *delta_x, double *out_7360625163823445495) {
|
||||
err_fun(nom_x, delta_x, out_7360625163823445495);
|
||||
}
|
||||
void car_inv_err_fun(double *nom_x, double *true_x, double *out_4534440969468340130) {
|
||||
inv_err_fun(nom_x, true_x, out_4534440969468340130);
|
||||
void car_inv_err_fun(double *nom_x, double *true_x, double *out_8413491231423970122) {
|
||||
inv_err_fun(nom_x, true_x, out_8413491231423970122);
|
||||
}
|
||||
void car_H_mod_fun(double *state, double *out_7815971298073658297) {
|
||||
H_mod_fun(state, out_7815971298073658297);
|
||||
void car_H_mod_fun(double *state, double *out_2632326835772732722) {
|
||||
H_mod_fun(state, out_2632326835772732722);
|
||||
}
|
||||
void car_f_fun(double *state, double dt, double *out_3011916394083426809) {
|
||||
f_fun(state, dt, out_3011916394083426809);
|
||||
void car_f_fun(double *state, double dt, double *out_8532698894052839577) {
|
||||
f_fun(state, dt, out_8532698894052839577);
|
||||
}
|
||||
void car_F_fun(double *state, double dt, double *out_4768916904116545459) {
|
||||
F_fun(state, dt, out_4768916904116545459);
|
||||
void car_F_fun(double *state, double dt, double *out_5046006616921834778) {
|
||||
F_fun(state, dt, out_5046006616921834778);
|
||||
}
|
||||
void car_h_25(double *state, double *unused, double *out_8627167620031889565) {
|
||||
h_25(state, unused, out_8627167620031889565);
|
||||
void car_h_25(double *state, double *unused, double *out_5546638875155758793) {
|
||||
h_25(state, unused, out_5546638875155758793);
|
||||
}
|
||||
void car_H_25(double *state, double *unused, double *out_1719062931467283681) {
|
||||
H_25(state, unused, out_1719062931467283681);
|
||||
void car_H_25(double *state, double *unused, double *out_890653814216758955) {
|
||||
H_25(state, unused, out_890653814216758955);
|
||||
}
|
||||
void car_h_24(double *state, double *unused, double *out_5179238137807796494) {
|
||||
h_24(state, unused, out_5179238137807796494);
|
||||
void car_h_24(double *state, double *unused, double *out_4367318711229477977) {
|
||||
h_24(state, unused, out_4367318711229477977);
|
||||
}
|
||||
void car_H_24(double *state, double *unused, double *out_453586667538215885) {
|
||||
H_24(state, unused, out_453586667538215885);
|
||||
void car_H_24(double *state, double *unused, double *out_4448579413019082091) {
|
||||
H_24(state, unused, out_4448579413019082091);
|
||||
}
|
||||
void car_h_30(double *state, double *unused, double *out_4528864321752517701) {
|
||||
h_30(state, unused, out_4528864321752517701);
|
||||
void car_h_30(double *state, double *unused, double *out_1909189346901736443) {
|
||||
h_30(state, unused, out_1909189346901736443);
|
||||
}
|
||||
void car_H_30(double *state, double *unused, double *out_4237395889974532308) {
|
||||
H_30(state, unused, out_4237395889974532308);
|
||||
void car_H_30(double *state, double *unused, double *out_7807344155708375710) {
|
||||
H_30(state, unused, out_7807344155708375710);
|
||||
}
|
||||
void car_h_26(double *state, double *unused, double *out_5472374174654180136) {
|
||||
h_26(state, unused, out_5472374174654180136);
|
||||
void car_h_26(double *state, double *unused, double *out_6910862614562582088) {
|
||||
h_26(state, unused, out_6910862614562582088);
|
||||
}
|
||||
void car_H_26(double *state, double *unused, double *out_2022440387406772543) {
|
||||
H_26(state, unused, out_2022440387406772543);
|
||||
void car_H_26(double *state, double *unused, double *out_2850849504657297269) {
|
||||
H_26(state, unused, out_2850849504657297269);
|
||||
}
|
||||
void car_h_27(double *state, double *unused, double *out_2749092444325053967) {
|
||||
h_27(state, unused, out_2749092444325053967);
|
||||
void car_h_27(double *state, double *unused, double *out_3918136026546952058) {
|
||||
h_27(state, unused, out_3918136026546952058);
|
||||
}
|
||||
void car_H_27(double *state, double *unused, double *out_6460989961158475525) {
|
||||
H_27(state, unused, out_6460989961158475525);
|
||||
void car_H_27(double *state, double *unused, double *out_5632580843907950799) {
|
||||
H_27(state, unused, out_5632580843907950799);
|
||||
}
|
||||
void car_h_29(double *state, double *unused, double *out_5132180206428995218) {
|
||||
h_29(state, unused, out_5132180206428995218);
|
||||
void car_h_29(double *state, double *unused, double *out_3092483920759109202) {
|
||||
h_29(state, unused, out_3092483920759109202);
|
||||
}
|
||||
void car_H_29(double *state, double *unused, double *out_4747627234288924492) {
|
||||
H_29(state, unused, out_4747627234288924492);
|
||||
void car_H_29(double *state, double *unused, double *out_8317575500022767894) {
|
||||
H_29(state, unused, out_8317575500022767894);
|
||||
}
|
||||
void car_h_28(double *state, double *unused, double *out_9148133961738903113) {
|
||||
h_28(state, unused, out_9148133961738903113);
|
||||
void car_h_28(double *state, double *unused, double *out_4521451911223429575) {
|
||||
h_28(state, unused, out_4521451911223429575);
|
||||
}
|
||||
void car_H_28(double *state, double *unused, double *out_334771782780606082) {
|
||||
H_28(state, unused, out_334771782780606082);
|
||||
void car_H_28(double *state, double *unused, double *out_1163180900031130808) {
|
||||
H_28(state, unused, out_1163180900031130808);
|
||||
}
|
||||
void car_h_31(double *state, double *unused, double *out_745894969277840380) {
|
||||
h_31(state, unused, out_745894969277840380);
|
||||
void car_h_31(double *state, double *unused, double *out_5821832937440264682) {
|
||||
h_31(state, unused, out_5821832937440264682);
|
||||
}
|
||||
void car_H_31(double *state, double *unused, double *out_2648648489640124019) {
|
||||
H_31(state, unused, out_2648648489640124019);
|
||||
void car_H_31(double *state, double *unused, double *out_921299776093719383) {
|
||||
H_31(state, unused, out_921299776093719383);
|
||||
}
|
||||
void car_predict(double *in_x, double *in_P, double *in_Q, double dt) {
|
||||
predict(in_x, in_P, in_Q, dt);
|
||||
|
||||
@@ -9,27 +9,27 @@ void car_update_27(double *in_x, double *in_P, double *in_z, double *in_R, doubl
|
||||
void car_update_29(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void car_update_28(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void car_update_31(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void car_err_fun(double *nom_x, double *delta_x, double *out_3585265450871354510);
|
||||
void car_inv_err_fun(double *nom_x, double *true_x, double *out_4534440969468340130);
|
||||
void car_H_mod_fun(double *state, double *out_7815971298073658297);
|
||||
void car_f_fun(double *state, double dt, double *out_3011916394083426809);
|
||||
void car_F_fun(double *state, double dt, double *out_4768916904116545459);
|
||||
void car_h_25(double *state, double *unused, double *out_8627167620031889565);
|
||||
void car_H_25(double *state, double *unused, double *out_1719062931467283681);
|
||||
void car_h_24(double *state, double *unused, double *out_5179238137807796494);
|
||||
void car_H_24(double *state, double *unused, double *out_453586667538215885);
|
||||
void car_h_30(double *state, double *unused, double *out_4528864321752517701);
|
||||
void car_H_30(double *state, double *unused, double *out_4237395889974532308);
|
||||
void car_h_26(double *state, double *unused, double *out_5472374174654180136);
|
||||
void car_H_26(double *state, double *unused, double *out_2022440387406772543);
|
||||
void car_h_27(double *state, double *unused, double *out_2749092444325053967);
|
||||
void car_H_27(double *state, double *unused, double *out_6460989961158475525);
|
||||
void car_h_29(double *state, double *unused, double *out_5132180206428995218);
|
||||
void car_H_29(double *state, double *unused, double *out_4747627234288924492);
|
||||
void car_h_28(double *state, double *unused, double *out_9148133961738903113);
|
||||
void car_H_28(double *state, double *unused, double *out_334771782780606082);
|
||||
void car_h_31(double *state, double *unused, double *out_745894969277840380);
|
||||
void car_H_31(double *state, double *unused, double *out_2648648489640124019);
|
||||
void car_err_fun(double *nom_x, double *delta_x, double *out_7360625163823445495);
|
||||
void car_inv_err_fun(double *nom_x, double *true_x, double *out_8413491231423970122);
|
||||
void car_H_mod_fun(double *state, double *out_2632326835772732722);
|
||||
void car_f_fun(double *state, double dt, double *out_8532698894052839577);
|
||||
void car_F_fun(double *state, double dt, double *out_5046006616921834778);
|
||||
void car_h_25(double *state, double *unused, double *out_5546638875155758793);
|
||||
void car_H_25(double *state, double *unused, double *out_890653814216758955);
|
||||
void car_h_24(double *state, double *unused, double *out_4367318711229477977);
|
||||
void car_H_24(double *state, double *unused, double *out_4448579413019082091);
|
||||
void car_h_30(double *state, double *unused, double *out_1909189346901736443);
|
||||
void car_H_30(double *state, double *unused, double *out_7807344155708375710);
|
||||
void car_h_26(double *state, double *unused, double *out_6910862614562582088);
|
||||
void car_H_26(double *state, double *unused, double *out_2850849504657297269);
|
||||
void car_h_27(double *state, double *unused, double *out_3918136026546952058);
|
||||
void car_H_27(double *state, double *unused, double *out_5632580843907950799);
|
||||
void car_h_29(double *state, double *unused, double *out_3092483920759109202);
|
||||
void car_H_29(double *state, double *unused, double *out_8317575500022767894);
|
||||
void car_h_28(double *state, double *unused, double *out_4521451911223429575);
|
||||
void car_H_28(double *state, double *unused, double *out_1163180900031130808);
|
||||
void car_h_31(double *state, double *unused, double *out_5821832937440264682);
|
||||
void car_H_31(double *state, double *unused, double *out_921299776093719383);
|
||||
void car_predict(double *in_x, double *in_P, double *in_Q, double dt);
|
||||
void car_set_rotational_inertia(double x);
|
||||
void car_set_center_to_front(double x);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -5,18 +5,18 @@ void pose_update_4(double *in_x, double *in_P, double *in_z, double *in_R, doubl
|
||||
void pose_update_10(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void pose_update_13(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void pose_update_14(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea);
|
||||
void pose_err_fun(double *nom_x, double *delta_x, double *out_1372154176746733435);
|
||||
void pose_inv_err_fun(double *nom_x, double *true_x, double *out_1523580565963320994);
|
||||
void pose_H_mod_fun(double *state, double *out_7104221342799173532);
|
||||
void pose_f_fun(double *state, double dt, double *out_7349931890006772642);
|
||||
void pose_F_fun(double *state, double dt, double *out_1064691538495726265);
|
||||
void pose_h_4(double *state, double *unused, double *out_5786601062018184200);
|
||||
void pose_H_4(double *state, double *unused, double *out_8923920068705470393);
|
||||
void pose_h_10(double *state, double *unused, double *out_8200446056116706374);
|
||||
void pose_H_10(double *state, double *unused, double *out_8156688557227160880);
|
||||
void pose_h_13(double *state, double *unused, double *out_9145870174400750931);
|
||||
void pose_H_13(double *state, double *unused, double *out_5711646243373137592);
|
||||
void pose_h_14(double *state, double *unused, double *out_5989511771500844071);
|
||||
void pose_H_14(double *state, double *unused, double *out_6440035572708708927);
|
||||
void pose_err_fun(double *nom_x, double *delta_x, double *out_5308991872147398563);
|
||||
void pose_inv_err_fun(double *nom_x, double *true_x, double *out_5440750294658648606);
|
||||
void pose_H_mod_fun(double *state, double *out_7655853576405724458);
|
||||
void pose_f_fun(double *state, double dt, double *out_4136808220434975141);
|
||||
void pose_F_fun(double *state, double dt, double *out_2537852680944861179);
|
||||
void pose_h_4(double *state, double *unused, double *out_7730454059225088569);
|
||||
void pose_H_4(double *state, double *unused, double *out_7019083287459919586);
|
||||
void pose_h_10(double *state, double *unused, double *out_1910166407148471034);
|
||||
void pose_H_10(double *state, double *unused, double *out_3315929574475692252);
|
||||
void pose_h_13(double *state, double *unused, double *out_2907941445058093810);
|
||||
void pose_H_13(double *state, double *unused, double *out_7593905322947108006);
|
||||
void pose_h_14(double *state, double *unused, double *out_5410884076226838020);
|
||||
void pose_H_14(double *state, double *unused, double *out_8344872353954259734);
|
||||
void pose_predict(double *in_x, double *in_P, double *in_Q, double dt);
|
||||
}
|
||||
@@ -7,7 +7,6 @@ from functools import cached_property
|
||||
import os
|
||||
os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom
|
||||
os.environ.setdefault('AM_POWER_LIMIT', '100')
|
||||
os.environ.setdefault('AM_PERF_LEVEL', 'high')
|
||||
from tinygrad.device import Buffer, Device
|
||||
from tinygrad.dtype import DType, dtypes
|
||||
from tinygrad.engine.realize import lower_and_compile
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d4c7a3fcfc95813826ca871a80bea18df833aacb2eb6f5333e25a01e65b1a422
|
||||
size 799941965
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4b1709b9f6b05f94b1165afe0d9763ab4bf1d59102417708e4b10aaa519bd771
|
||||
size 858796
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a17a2f2ae70decfca855c5626178aefe8dac0d0d303c72edd44dfa582e98d73e
|
||||
size 858531
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,98 +0,0 @@
|
||||
# Signal navigation experiment
|
||||
|
||||
The physical turn signal selects a short real route through the next supported
|
||||
street. The route and maneuver instructions condition a navigation-trained
|
||||
driving network continuously. This replaces the earlier map-timed turn-cue
|
||||
experiment when a compatible model is selected; it does not add steering angles.
|
||||
The automatic candidate begins with a pending signal and may prefetch a route
|
||||
at any speed. Pending discovery does not send navigation inputs or suppress the
|
||||
existing lane-change checks. An emitted host lane-change request holds
|
||||
lane-change mode while preserving the candidate route. Otherwise, a fresh validated
|
||||
street approach below the configured turn speed (19 mph by default) may commit
|
||||
turn mode. An early fast signal can therefore prepare a route and later become
|
||||
a turn. LC wins a same-frame tie. A lane-change request blocks navigation until
|
||||
the existing helper reports normal completion (its probability/timer condition),
|
||||
after which the held signal may become a turn at a fresh eligible approach.
|
||||
Slowing down, timeout, disengagement and disabling lane changes do not themselves
|
||||
release this block. A committed turn remains a turn until cancel/opposite.
|
||||
|
||||
Only a committed turn suppresses competing lane-change pulses. Map conditioning
|
||||
requires a validated connected street, matching position and heading, and a
|
||||
coherent fresh route. Pending and lane-change intent cannot feed navigation.
|
||||
This arbitration still cannot reliably identify every early fast turn signal or
|
||||
slow lane change. A nearby legal street does not establish the driver's intended street.
|
||||
Predicted missing lane lines must not be treated as proof that no adjacent lane
|
||||
exists. These ambiguity cases remain validation limitations, not solved intent.
|
||||
|
||||
A causal host audit using captured map contexts and recorded helper/model heads
|
||||
finds eventual navigation eligibility on 29 of 39 street-turn requests, compared
|
||||
with 23 under the earlier permanent lane-change lock. Only two of those six
|
||||
additional opportunities precede the recorded intervention cutoff; later
|
||||
eligibility must not be counted as recovering the original turn. All four
|
||||
recorded lane-change controls retain their lane-change request and none gains
|
||||
navigation eligibility. These are selection proxies, not new navigation-model
|
||||
inference or neural turn successes. Map-target identity is a separate check and
|
||||
can still select the wrong street. The two newly recovered Mapbox probe responses
|
||||
were verified separately and are not silently included in this frozen-context audit.
|
||||
An earlier speed-at-signal classifier was rejected because 18 street-turn
|
||||
requests began above 20 mph. The automatic selector is not dependable enough to
|
||||
claim road-test readiness from these results.
|
||||
|
||||
## Model requirement
|
||||
|
||||
CTV3 and the small fallback have no navigation input sockets. A route on the
|
||||
screen cannot make those models follow navigation. The runtime checks actual
|
||||
`nav_map` and `navigation` tensors, and publishes `NavModelState` separately from
|
||||
route availability. The UI distinguishes **Route ready** from **Navigation active**.
|
||||
|
||||
The basis is commaai/openpilot `nav-model-integration`, pinned at
|
||||
`afbf2e547e9319e2eb9f515e1e4330af63705cec`. The available BIG ONNX is the earlier
|
||||
RL navigation checkpoint `80eedcdd/12864`, SHA256
|
||||
`1d18c42ea4cd95c8bffab4635844b78b8e2d4e25b9de44a9c5ac2e0961371a2e`.
|
||||
The pinned commit's newer N16 compiled PKL is a different artifact; running the
|
||||
ONNX is not evidence of running those N16 weights. Official LFS downloads for
|
||||
the latest three compiled navigation artifacts returned object-not-found.
|
||||
|
||||
The ONNX was downloaded through a third-party mirror and independently matched
|
||||
against comma's exact LFS pointer. A Chestnut-compatible plain artifact still
|
||||
requires the target compilation and verification step. Use **Settings → Models →
|
||||
Prepare Navigation Model** while parked, then explicitly select the prepared
|
||||
model. See [model preparation](../../sunnypilot/models/NAVIGATION_MODEL.md).
|
||||
A Mac CPU ONNX smoke is not a verified device build. No model is automatically
|
||||
swapped when signaling.
|
||||
|
||||
## Route ownership and input coherence
|
||||
|
||||
Manual destinations take priority. Automatic routes do not overwrite
|
||||
`NavDestination`. Each physical signal has an identifier; a held indicator does
|
||||
not repeatedly select turns. Cancellation before entry retires the route;
|
||||
already-entered turns may retain guidance through the receiving road, after
|
||||
which it is cleared. A missed turn is retired rather than rerouted back to the
|
||||
short endpoint.
|
||||
|
||||
Map frames carry the exact rendered `navRoute.logMonoTime` as their stream EOF
|
||||
generation; SOF remains the GPS publication time. The driving process requires
|
||||
a matching route generation, fresh map/instructions and the current signal
|
||||
session (or explicitly retained exit guidance). Stale/opposite/canceled route
|
||||
inputs are zeroed. A route preview alone never establishes active model input.
|
||||
|
||||
## Scope and validation
|
||||
|
||||
The feature is opt-in under **Signal Navigation (Experimental)**. It needs
|
||||
online Directions requests, a Mapbox public token, and automatically fetched
|
||||
road-map tiles. Directions use the configured Mapbox token. The basemap is fetched
|
||||
from public Protomaps builds, without comma authentication or a Prime subscription.
|
||||
Only the required archive byte ranges are downloaded and cached; the entire
|
||||
planet archive is not downloaded. The map data is derived from
|
||||
[OpenStreetMap](https://www.openstreetmap.org/copyright); attribution is displayed
|
||||
over the map preview without changing the model raster. The public basemap is not
|
||||
claimed to be byte-identical to comma's training archive. See
|
||||
[Protomaps downloads](https://docs.protomaps.com/basemaps/downloads) for distribution
|
||||
and attribution information; a wider deployment should serve its own archive mirror.
|
||||
Existing steering, cooperative steering, action conversion and
|
||||
the restored 1.6 m/s low-speed handling are preserved.
|
||||
|
||||
Recorded-camera model replay can test whether real route inputs change the
|
||||
model's plan. It cannot establish physical curb clearance or a successful driven
|
||||
turn, because the camera views remain the recorded ones. Model runtime checks,
|
||||
software fixture checks and recorded-sequence results must be reported separately.
|
||||
@@ -1,452 +0,0 @@
|
||||
"""Discover a router-validated next street turn; never issue a driving command.
|
||||
|
||||
The ahead/exit probe points are API queries only. A target requires a separate
|
||||
route proving its actual first turn. Map data is not a curb, signal-phase or
|
||||
physical-clearance oracle. Callers own localization freshness and target expiry.
|
||||
"""
|
||||
from dataclasses import dataclass, fields
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import time
|
||||
from types import MappingProxyType
|
||||
|
||||
import requests
|
||||
|
||||
EARTH_RADIUS_M = 6371008.8
|
||||
PROBE_DISTANCE_M = 500.
|
||||
PROBE_ENDPOINT_RADIUS_M = 100.
|
||||
PROBE_FALLBACK_ENDPOINT_RADIUS_M = 300.
|
||||
MAX_TARGET_DISTANCE_M = 300.
|
||||
EXIT_QUERY_DISTANCE_M = 45.
|
||||
MIN_EXIT_DISTANCE_M = 20.
|
||||
SOURCE_RADIUS_M = 20.
|
||||
EXIT_RADIUS_M = 15.
|
||||
JUNCTION_TOLERANCE_M = 2.
|
||||
BEARING_TOLERANCE_DEG = 25.
|
||||
MIN_TURN_DEG = 35.
|
||||
MAX_TURN_DEG = 145.
|
||||
TIMEOUT = (2., 3.)
|
||||
FORBIDDEN_CLASSES = {'restricted', 'motorway', 'ferry'}
|
||||
STREET_CLASSES = {'trunk', 'primary', 'secondary', 'tertiary', 'street', 'residential', 'unclassified'}
|
||||
UNSUPPORTED_MANEUVERS = {'roundabout', 'rotary', 'roundabout turn', 'exit roundabout', 'exit rotary', 'on ramp', 'off ramp', 'merge', 'fork'}
|
||||
|
||||
|
||||
class NextTurnError(Exception):
|
||||
"""A fixed reason code, containing no request URL, token or response body."""
|
||||
def __init__(self, reason):
|
||||
self.reason = reason
|
||||
super().__init__(reason)
|
||||
|
||||
|
||||
def _freeze(value):
|
||||
if isinstance(value, dict):
|
||||
return MappingProxyType({key: _freeze(item) for key, item in value.items()})
|
||||
if isinstance(value, list):
|
||||
return tuple(_freeze(item) for item in value)
|
||||
return value
|
||||
|
||||
|
||||
def _thaw(value):
|
||||
if isinstance(value, (dict, MappingProxyType)):
|
||||
return {key: _thaw(item) for key, item in value.items()}
|
||||
if isinstance(value, tuple):
|
||||
return [_thaw(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NextTurnTarget:
|
||||
junction: tuple[float, float]
|
||||
distance_m: float
|
||||
incoming_bearing_deg: float
|
||||
exit_bearing_deg: float
|
||||
direction: str
|
||||
name: str
|
||||
road_class: str
|
||||
geometry: tuple
|
||||
steps: tuple
|
||||
approach_geometry: tuple
|
||||
approach_distance_m: float
|
||||
turn_step_index: int
|
||||
junction_geometry_index: int
|
||||
origin: tuple[float, float]
|
||||
discovered_at: float
|
||||
probe_uuid: str
|
||||
validation_uuid: str
|
||||
classification_uuid: str | None
|
||||
provenance: MappingProxyType
|
||||
|
||||
def to_dict(self):
|
||||
return {field.name: _thaw(getattr(self, field.name)) for field in fields(self)}
|
||||
|
||||
|
||||
def _point(value):
|
||||
if not isinstance(value, (list, tuple)) or len(value) != 2:
|
||||
raise NextTurnError('invalid_coordinates')
|
||||
if any(isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v) for v in value):
|
||||
raise NextTurnError('invalid_coordinates')
|
||||
lon, lat = map(float, value)
|
||||
if not (-180 <= lon <= 180 and -85 < lat < 85):
|
||||
raise NextTurnError('invalid_coordinates')
|
||||
return lon, lat
|
||||
|
||||
|
||||
def distance(a, b):
|
||||
lon0, lat0, lon1, lat1 = map(math.radians, (*a, *b))
|
||||
h = math.sin((lat1 - lat0) / 2)**2 + math.cos(lat0) * math.cos(lat1) * math.sin((lon1 - lon0) / 2)**2
|
||||
return 2 * EARTH_RADIUS_M * math.asin(math.sqrt(min(1., max(0., h))))
|
||||
|
||||
|
||||
def bearing(a, b):
|
||||
lon0, lat0, lon1, lat1 = map(math.radians, (*a, *b))
|
||||
return math.degrees(math.atan2(math.sin(lon1-lon0) * math.cos(lat1),
|
||||
math.cos(lat0) * math.sin(lat1) - math.sin(lat0) * math.cos(lat1) * math.cos(lon1-lon0))) % 360
|
||||
|
||||
|
||||
def offset(point, heading, meters):
|
||||
lon, lat = map(math.radians, point)
|
||||
heading, arc = math.radians(heading), meters / EARTH_RADIUS_M
|
||||
lat1 = math.asin(math.sin(lat)*math.cos(arc) + math.cos(lat)*math.sin(arc)*math.cos(heading))
|
||||
lon1 = lon + math.atan2(math.sin(heading)*math.sin(arc)*math.cos(lat), math.cos(arc)-math.sin(lat)*math.sin(lat1))
|
||||
return (math.degrees(lon1)+180) % 360 - 180, math.degrees(lat1)
|
||||
|
||||
|
||||
def _delta(before, after):
|
||||
return (after - before + 180) % 360 - 180
|
||||
|
||||
|
||||
def _length(points):
|
||||
return sum(distance(a, b) for a, b in zip(points[:-1], points[1:], strict=True))
|
||||
|
||||
|
||||
def _initial_bearing(points):
|
||||
for point in points[1:]:
|
||||
if distance(points[0], point) >= .5:
|
||||
return bearing(points[0], point)
|
||||
raise NextTurnError('insufficient_approach')
|
||||
|
||||
|
||||
def _number(value):
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
||||
raise NextTurnError('missing_metadata')
|
||||
return float(value)
|
||||
|
||||
|
||||
def _parse_route(body, position, heading, endpoint, endpoint_radius):
|
||||
if not isinstance(body, dict) or body.get('code') != 'Ok':
|
||||
raise NextTurnError('no_route')
|
||||
if not isinstance(body.get('uuid'), str) or not body['uuid']:
|
||||
raise NextTurnError('missing_request_id')
|
||||
routes = body.get('routes')
|
||||
if not isinstance(routes, list) or not routes or not isinstance(routes[0], dict):
|
||||
raise NextTurnError('invalid_response')
|
||||
route = routes[0]
|
||||
waypoints = body.get('waypoints', route.get('waypoints'))
|
||||
if not isinstance(waypoints, list) or len(waypoints) != 2:
|
||||
raise NextTurnError('missing_metadata')
|
||||
snapped_start, snapped_end = (_point(value['location']) for value in waypoints)
|
||||
if distance(position, snapped_start) > SOURCE_RADIUS_M or distance(endpoint, snapped_end) > endpoint_radius:
|
||||
raise NextTurnError('snapping_mismatch')
|
||||
geometry = route.get('geometry')
|
||||
if not isinstance(geometry, dict) or geometry.get('type') != 'LineString':
|
||||
raise NextTurnError('missing_metadata')
|
||||
points = tuple(_point(point) for point in geometry.get('coordinates', []))
|
||||
if not 2 <= len(points) <= 10000 or distance(points[0], snapped_start) > 3 or distance(points[-1], snapped_end) > 3:
|
||||
raise NextTurnError('invalid_geometry')
|
||||
if abs(_delta(heading, _initial_bearing(points))) > BEARING_TOLERANCE_DEG:
|
||||
raise NextTurnError('incoming_heading_mismatch')
|
||||
legs = route.get('legs')
|
||||
if not isinstance(legs, list) or len(legs) != 1 or not isinstance(legs[0], dict):
|
||||
raise NextTurnError('missing_metadata')
|
||||
leg = legs[0]
|
||||
if leg.get('annotation', {}).get('closure') or any(item.get('type') == 'closure' for item in leg.get('incidents', [])):
|
||||
raise NextTurnError('closed_route')
|
||||
if any(item.get('type') == 'violation' for item in route.get('notifications', [])):
|
||||
raise NextTurnError('route_violation')
|
||||
steps = leg.get('steps')
|
||||
if not isinstance(steps, list) or not steps:
|
||||
raise NextTurnError('missing_metadata')
|
||||
cumulative = [0.]
|
||||
for a, b in zip(points[:-1], points[1:], strict=True):
|
||||
cumulative.append(cumulative[-1] + distance(a, b))
|
||||
intersections = []
|
||||
for step_index, step in enumerate(steps):
|
||||
if not isinstance(step, dict) or not isinstance(step.get('maneuver'), dict) or not isinstance(step.get('intersections'), list):
|
||||
raise NextTurnError('missing_metadata')
|
||||
kind = step['maneuver'].get('type')
|
||||
if not isinstance(kind, str) or not step.get('geometry', {}).get('coordinates'):
|
||||
raise NextTurnError('missing_metadata')
|
||||
if step.get('mode') != 'driving':
|
||||
raise NextTurnError('unsupported_road')
|
||||
for intersection in step['intersections']:
|
||||
if not isinstance(intersection, dict):
|
||||
raise NextTurnError('missing_metadata')
|
||||
if 'in' not in intersection or 'out' not in intersection:
|
||||
location = _point(intersection.get('location'))
|
||||
if kind == 'depart' and 'in' not in intersection and distance(location, points[0]) <= 3:
|
||||
continue
|
||||
if kind == 'arrive' and 'out' not in intersection and distance(location, points[-1]) <= 3:
|
||||
continue
|
||||
raise NextTurnError('missing_metadata')
|
||||
bearings, entry = intersection.get('bearings'), intersection.get('entry')
|
||||
if not isinstance(bearings, list) or not isinstance(entry, list) or len(bearings) != len(entry) or not 2 <= len(entry) <= 16:
|
||||
raise NextTurnError('missing_metadata')
|
||||
if any(type(flag) is not bool for flag in entry):
|
||||
raise NextTurnError('missing_metadata')
|
||||
bearings = [_number(value) for value in bearings]
|
||||
if any(not 0 <= value <= 360 for value in bearings):
|
||||
raise NextTurnError('missing_metadata')
|
||||
# Actual Directions responses sometimes encode north as 360 rather than 0.
|
||||
bearings = [value % 360 for value in bearings]
|
||||
incoming, outgoing, index = intersection['in'], intersection['out'], intersection.get('geometry_index')
|
||||
if any(type(value) is not int for value in (incoming, outgoing, index)):
|
||||
raise NextTurnError('missing_metadata')
|
||||
if not (0 <= incoming < len(entry) and 0 <= outgoing < len(entry) and 0 <= index < len(points)):
|
||||
raise NextTurnError('missing_metadata')
|
||||
location = _point(intersection.get('location'))
|
||||
if distance(location, points[index]) > JUNCTION_TOLERANCE_M or not entry[outgoing]:
|
||||
raise NextTurnError('inconsistent_intersection')
|
||||
before = (bearings[incoming]+180) % 360
|
||||
# A fixed long chord can cross the preceding corner at close junctions.
|
||||
prior = next((p for p in reversed(points[:index]) if distance(p, location) >= .5), None)
|
||||
if prior is None or abs(_delta(bearing(prior, location), before)) > BEARING_TOLERANCE_DEG:
|
||||
raise NextTurnError('incoming_heading_mismatch')
|
||||
intersections.append({'raw': intersection, 'location': location, 'bearings': bearings, 'entry': entry,
|
||||
'incoming': before, 'outgoing': bearings[outgoing], 'out': outgoing,
|
||||
'index': index, 'distance': cumulative[index], 'step': step_index, 'kind': kind})
|
||||
return {'body': body, 'route': route, 'steps': steps, 'points': points, 'intersections': sorted(intersections, key=lambda i:i['index']),
|
||||
'uuid': body['uuid'], 'responseSha256': hashlib.sha256(json.dumps(body, sort_keys=True, allow_nan=False).encode()).hexdigest()}
|
||||
|
||||
|
||||
def _choose_branch(route, direction):
|
||||
for item in route['intersections']:
|
||||
if item['distance'] > MAX_TARGET_DISTANCE_M:
|
||||
break
|
||||
if item['kind'] in UNSUPPORTED_MANEUVERS:
|
||||
raise NextTurnError('unsupported_junction')
|
||||
maneuver = route['steps'][item['step']]['maneuver']
|
||||
explicit_turn = ((item['kind'] in ('turn', 'end of road') or maneuver.get('modifier') == 'uturn')
|
||||
and distance(_point(maneuver.get('location')), item['location']) <= JUNCTION_TOLERANCE_M)
|
||||
has_branch = len(item['bearings']) >= 3
|
||||
candidates = []
|
||||
for heading, allowed in zip(item['bearings'], item['entry'], strict=True):
|
||||
delta = _delta(item['incoming'], heading)
|
||||
side = 'right' if delta > 0 else 'left'
|
||||
continuing_bend = heading == item['outgoing'] and not explicit_turn
|
||||
if allowed and not continuing_bend and side == direction and MIN_TURN_DEG <= abs(delta) <= MAX_TURN_DEG:
|
||||
candidates.append(heading)
|
||||
if len(candidates) > 1:
|
||||
raise NextTurnError('ambiguous_branch')
|
||||
if candidates and item['distance'] >= 2:
|
||||
return {**item, 'exit': candidates[0]}
|
||||
if not has_branch and not explicit_turn:
|
||||
continue
|
||||
if explicit_turn:
|
||||
# Only a maneuver at this intersection proves the probe left the approach;
|
||||
# a continuing main road can bend even where a minor branch joins it.
|
||||
break
|
||||
raise NextTurnError('no_matching_turn')
|
||||
|
||||
|
||||
def _validate_turn(route, candidate, direction):
|
||||
matched = None
|
||||
truncate = False
|
||||
for item in route['intersections']:
|
||||
if matched is not None and item['distance'] >= matched['distance'] + MIN_EXIT_DISTANCE_M:
|
||||
# A query endpoint can require a further turn beyond the local exit. Its
|
||||
# later route is irrelevant only after this first turn is proven below.
|
||||
if abs(_delta(item['incoming'], item['outgoing'])) >= MIN_TURN_DEG or item['kind'] in UNSUPPORTED_MANEUVERS:
|
||||
truncate = True
|
||||
break
|
||||
near = distance(item['location'], candidate['location']) <= JUNCTION_TOLERANCE_M
|
||||
turn = abs(_delta(item['incoming'], item['outgoing'])) >= MIN_TURN_DEG
|
||||
if item['kind'] in UNSUPPORTED_MANEUVERS:
|
||||
raise NextTurnError('unsupported_junction')
|
||||
if near and turn:
|
||||
if (abs(_delta(candidate['incoming'], item['incoming'])) > BEARING_TOLERANCE_DEG
|
||||
or abs(_delta(candidate['exit'], item['outgoing'])) > BEARING_TOLERANCE_DEG
|
||||
or ('right' if _delta(item['incoming'], item['outgoing']) > 0 else 'left') != direction
|
||||
or abs(_delta(item['incoming'], item['outgoing'])) > MAX_TURN_DEG):
|
||||
raise NextTurnError('target_mismatch')
|
||||
if abs(item['distance'] - candidate['distance']) > max(12., candidate['distance'] * .15):
|
||||
raise NextTurnError('approach_detour')
|
||||
if matched is None:
|
||||
matched = item
|
||||
elif turn:
|
||||
raise NextTurnError('earlier_or_additional_turn')
|
||||
if matched is None:
|
||||
raise NextTurnError('target_not_reached')
|
||||
for index, step in enumerate(route['steps']):
|
||||
if index != matched['step'] and step['maneuver']['type'] not in ('depart', 'arrive', 'continue', 'new name'):
|
||||
location = _point(step['maneuver'].get('location'))
|
||||
point_index = min(range(len(route['points'])), key=lambda i: distance(route['points'][i], location))
|
||||
along = _length(route['points'][:point_index+1])
|
||||
if (index <= matched['step'] or distance(route['points'][point_index], location) > JUNCTION_TOLERANCE_M
|
||||
or along < matched['distance'] + MIN_EXIT_DISTANCE_M):
|
||||
raise NextTurnError('earlier_or_additional_turn')
|
||||
truncate = True
|
||||
if truncate:
|
||||
route = _local_exit_scope(route, matched)
|
||||
points = route['points']
|
||||
if (_length(points[matched['index']:]) < MIN_EXIT_DISTANCE_M - 1e-6
|
||||
or (not route.get('targetScopeTruncated') and distance(matched['location'], points[-1]) < MIN_EXIT_DISTANCE_M - 1e-6)
|
||||
or abs(_delta(candidate['exit'], bearing(matched['location'], points[-1]))) > BEARING_TOLERANCE_DEG):
|
||||
raise NextTurnError('insufficient_exit')
|
||||
chosen_class = matched['raw'].get('mapbox_streets_v8', {}).get('class')
|
||||
for item in route['intersections']:
|
||||
if item['index'] >= matched['index']:
|
||||
if set(item['raw'].get('classes', [])) & FORBIDDEN_CLASSES:
|
||||
raise NextTurnError('unsupported_road')
|
||||
known_class = item['raw'].get('mapbox_streets_v8', {}).get('class')
|
||||
if known_class is not None and known_class not in STREET_CLASSES:
|
||||
raise NextTurnError('unsupported_road')
|
||||
if chosen_class is not None and chosen_class not in STREET_CLASSES:
|
||||
raise NextTurnError('unsupported_road')
|
||||
return matched, chosen_class, route
|
||||
|
||||
|
||||
def _local_exit_scope(route, matched):
|
||||
"""Return an explicit prefix of real geometry, ending before any later turn."""
|
||||
points = list(route['points'][:matched['index']+1])
|
||||
remaining = MIN_EXIT_DISTANCE_M
|
||||
for point in route['points'][matched['index']+1:]:
|
||||
length = distance(points[-1], point)
|
||||
if length >= remaining:
|
||||
points.append(offset(points[-1], bearing(points[-1], point), remaining))
|
||||
break
|
||||
points.append(point)
|
||||
remaining -= length
|
||||
else:
|
||||
raise NextTurnError('insufficient_exit')
|
||||
cutoff = matched['distance'] + MIN_EXIT_DISTANCE_M
|
||||
steps = json.loads(json.dumps(route['steps'][:matched['step']+1]))
|
||||
steps[-1]['geometry'] = {'type': 'LineString', 'coordinates': points[matched['index']:]}
|
||||
steps[-1]['intersections'] = [item['raw'] for item in route['intersections']
|
||||
if item['step'] == matched['step'] and item['distance'] <= cutoff]
|
||||
return {**route, 'points': tuple(points), 'steps': steps,
|
||||
'intersections': [item for item in route['intersections'] if item['distance'] <= cutoff],
|
||||
'targetScopeTruncated': True}
|
||||
|
||||
|
||||
def coherent_route_steps(route):
|
||||
"""Require a complete actual Directions leg, including its arrival step.
|
||||
|
||||
A clipped discovery prefix is not a new route: its old distances/banners no
|
||||
longer describe its geometry. It must be rerouted to the real exit endpoint.
|
||||
"""
|
||||
if route.get('targetScopeTruncated'):
|
||||
raise NextTurnError('incomplete_navigation_route')
|
||||
steps = json.loads(json.dumps(route['steps']))
|
||||
if not steps or steps[-1]['maneuver']['type'] != 'arrive':
|
||||
raise NextTurnError('incomplete_navigation_route')
|
||||
for step in steps:
|
||||
points = [_point(point) for point in step['geometry']['coordinates']]
|
||||
distance_m, duration = _number(step.get('distance')), _number(step.get('duration'))
|
||||
if not points or distance_m < 0 or duration < 0:
|
||||
raise NextTurnError('incomplete_navigation_route')
|
||||
if abs(_length(points)-distance_m) > max(5., distance_m*.2):
|
||||
raise NextTurnError('inconsistent_navigation_distance')
|
||||
step.setdefault('bannerInstructions', [])
|
||||
for before, after in zip(steps[:-1], steps[1:], strict=True):
|
||||
if distance(_point(before['geometry']['coordinates'][-1]), _point(after['geometry']['coordinates'][0])) > 3.:
|
||||
raise NextTurnError('disconnected_navigation_route')
|
||||
return steps
|
||||
|
||||
|
||||
class MapboxNextTurnClient:
|
||||
def __init__(self, token):
|
||||
if not isinstance(token, str) or not re.fullmatch(r'[ps]k\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+', token):
|
||||
raise NextTurnError('invalid_token')
|
||||
self._token = token
|
||||
|
||||
def _request(self, position, heading, endpoint, *, profile, endpoint_radius):
|
||||
coords = ';'.join(f'{lon:.7f},{lat:.7f}' for lon,lat in (position,endpoint))
|
||||
params = {'access_token': self._token, 'geometries': 'geojson', 'overview': 'full', 'steps': 'true',
|
||||
'alternatives': 'false', 'banner_instructions': 'true', 'bearings': f'{heading:.0f},30;',
|
||||
'radiuses': f'{SOURCE_RADIUS_M:.0f};{endpoint_radius:.0f}', 'depart_at': 'now',
|
||||
'exclude': 'ferry,motorway'}
|
||||
if profile == 'driving-traffic':
|
||||
params.update(annotations='closure', snapping_include_closures='false;false', snapping_include_static_closures='false;false')
|
||||
try:
|
||||
response = requests.get(f'https://api.mapbox.com/directions/v5/mapbox/{profile}/{coords}', params=params, timeout=TIMEOUT)
|
||||
response.raise_for_status()
|
||||
if len(response.content) > 2 * 1024 * 1024:
|
||||
raise NextTurnError('oversized_response')
|
||||
body = response.json()
|
||||
except requests.RequestException:
|
||||
raise NextTurnError('request_failed') from None
|
||||
except (ValueError, TypeError):
|
||||
raise NextTurnError('invalid_response') from None
|
||||
return _parse_route(body, position, heading, endpoint, endpoint_radius)
|
||||
|
||||
def discover(self, position, bearing_deg, direction):
|
||||
started = time.monotonic()
|
||||
position = _point(position)
|
||||
heading = _number(bearing_deg) % 360
|
||||
if direction not in ('left','right'):
|
||||
raise NextTurnError('invalid_direction')
|
||||
try:
|
||||
probe_endpoint = offset(position, heading, PROBE_DISTANCE_M)
|
||||
probe_radius = PROBE_ENDPOINT_RADIUS_M
|
||||
probe_fallback_used = False
|
||||
try:
|
||||
probe = self._request(position, heading, probe_endpoint, profile='driving-traffic', endpoint_radius=probe_radius)
|
||||
except NextTurnError as error:
|
||||
if error.reason != 'no_route':
|
||||
raise
|
||||
# This point is only a discovery probe, not the turn destination. A
|
||||
# straight500m projection can land over100m from any road. Retry once
|
||||
# with the same origin, heading and endpoint; actual exit validation
|
||||
# below keeps its own strict radius and first-turn/legality checks.
|
||||
probe_radius = PROBE_FALLBACK_ENDPOINT_RADIUS_M
|
||||
probe_fallback_used = True
|
||||
probe = self._request(position, heading, probe_endpoint, profile='driving-traffic', endpoint_radius=probe_radius)
|
||||
candidate = _choose_branch(probe, direction)
|
||||
query_endpoint = offset(candidate['location'], candidate['exit'], EXIT_QUERY_DISTANCE_M)
|
||||
validated = self._request(position, heading, query_endpoint, profile='driving-traffic', endpoint_radius=EXIT_RADIUS_M)
|
||||
matched, road_class, validated = _validate_turn(validated, candidate, direction)
|
||||
request_count = 2+int(probe_fallback_used)
|
||||
if validated.get('targetScopeTruncated'):
|
||||
# Query an actual endpoint on the validated receiving road. Keep the
|
||||
# complete new leg/arrival rather than turning a clipped preview into
|
||||
# model navigation data with stale step distances and instructions.
|
||||
query_endpoint = validated['points'][-1]
|
||||
validated = self._request(position, heading, query_endpoint, profile='driving-traffic', endpoint_radius=3.)
|
||||
matched, road_class, validated = _validate_turn(validated, candidate, direction)
|
||||
request_count += 1
|
||||
classification = None
|
||||
if road_class is None:
|
||||
request_count += 1
|
||||
classification = self._request(position, heading, query_endpoint, profile='driving', endpoint_radius=EXIT_RADIUS_M)
|
||||
_, road_class, classification = _validate_turn(classification, candidate, direction)
|
||||
if road_class is None:
|
||||
raise NextTurnError('missing_road_class')
|
||||
complete_steps = coherent_route_steps(validated)
|
||||
approach = tuple(validated['points'][:matched['index']]) + (matched['location'],)
|
||||
turn_step = validated['steps'][matched['step']]
|
||||
provenance = {'method': 'router intersection discovery plus separately validated first-turn route',
|
||||
'requestedPosition': position, 'requestedBearingDeg': heading, 'requestedDirection': direction,
|
||||
'probeResponseSha256': probe['responseSha256'], 'validationResponseSha256': validated['responseSha256'],
|
||||
'classificationResponseSha256': classification['responseSha256'] if classification else None,
|
||||
'requestCount': request_count,
|
||||
'probeEndpointRadiusM': probe_radius, 'probeFallbackUsed': probe_fallback_used,
|
||||
'probeFallbackOriginalReason': 'no_route' if probe_fallback_used else None,
|
||||
'targetScopeTruncated': validated.get('targetScopeTruncated', False),
|
||||
'targetExitDistanceM': _length(validated['points'][matched['index']:]),
|
||||
'fullValidationResponsePreservedByHash': True,
|
||||
'requestStartedAt': started, 'requestCompletedAt': time.monotonic(),
|
||||
'probeEndpointIsTarget': False, 'steeringCommandIssued': False}
|
||||
provenance['coherentNavigationRoute'] = True
|
||||
return NextTurnTarget(junction=matched['location'], distance_m=matched['distance'],
|
||||
incoming_bearing_deg=matched['incoming'], exit_bearing_deg=matched['outgoing'], direction=direction,
|
||||
name=str(turn_step.get('name') or 'Unnamed road'), road_class=road_class,
|
||||
geometry=tuple(validated['points']), steps=_freeze(complete_steps), approach_geometry=approach,
|
||||
approach_distance_m=_length(approach), turn_step_index=matched['step'], junction_geometry_index=matched['index'],
|
||||
origin=position, discovered_at=started, probe_uuid=probe['uuid'], validation_uuid=validated['uuid'],
|
||||
classification_uuid=classification['uuid'] if classification else None, provenance=_freeze(provenance))
|
||||
except NextTurnError:
|
||||
raise
|
||||
except (KeyError, TypeError, ValueError, OverflowError, IndexError, AttributeError):
|
||||
raise NextTurnError('invalid_response') from None
|
||||
@@ -24,12 +24,10 @@ class MapsD:
|
||||
self.route = np.empty((0, 2))
|
||||
self.prefetched = False
|
||||
self.retry_at = 0.0
|
||||
self.route_generation = 0
|
||||
|
||||
def update(self, sm) -> tuple[np.ndarray, int]:
|
||||
"""The map frame for the latest messages: black with a zero timestamp without a usable map, never stale."""
|
||||
if sm.updated['navRoute']:
|
||||
self.route_generation = int(sm.logMonoTime['navRoute'])
|
||||
route = np.array([(c.longitude, c.latitude) for c in sm['navRoute'].coordinates], dtype=np.float64).reshape((-1, 2))
|
||||
if not np.array_equal(route, self.route):
|
||||
self.route = route
|
||||
@@ -76,10 +74,7 @@ def main():
|
||||
while True:
|
||||
sm.update(0)
|
||||
image, timestamp = mapsd.update(sm)
|
||||
# EOF is the exact route generation rendered, not completion time. An
|
||||
# old-route render finishing after a new route cannot impersonate it.
|
||||
generation = mapsd.route_generation if timestamp else 0
|
||||
server.send(VisionStreamType.VISION_STREAM_MAP, image.tobytes(), rk.frame, timestamp, generation)
|
||||
server.send(VisionStreamType.VISION_STREAM_MAP, image.tobytes(), rk.frame, timestamp, time.monotonic_ns())
|
||||
rk.keep_time()
|
||||
finally:
|
||||
mapsd.close()
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import copy
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
@@ -15,389 +10,13 @@ from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.navigation.helpers import latest_gps_source
|
||||
from openpilot.selfdrive.navigation.mapbox_next_turn import MapboxNextTurnClient, NextTurnError
|
||||
from openpilot.selfdrive.navigation.route import RouteTracker, along_path, cumulative_distance, local_meters, nearest_segment
|
||||
from openpilot.selfdrive.navigation.route import along_path, local_meters
|
||||
|
||||
REROUTE_DISTANCE = 25
|
||||
MANEUVER_TRANSITION_THRESHOLD = 10
|
||||
REROUTE_COUNTER_MIN = 3
|
||||
ROUTE_RESEND_PERIOD = 10 # updates (seconds); a restarted mapsd gets the route within this
|
||||
RECOMPUTE_HOLD = 10 # updates (seconds) between route requests
|
||||
MAPBOX_TOKEN_PATH = Path('/data/navigation/mapbox_token')
|
||||
TURN_TARGET_TTL = 300. # seconds; context and GPS freshness are checked independently
|
||||
TURN_GPS_MAX_AGE = .5
|
||||
TURN_GPS_MAX_ACCURACY = 3.
|
||||
TURN_MAX_CROSS_TRACK = 6.
|
||||
TURN_MAX_HEADING_ERROR = 35.
|
||||
TURN_MAX_HEADING_UNCERTAINTY = 15.
|
||||
TURN_RETRY_SECONDS = 5.
|
||||
TURN_PASSED_DISTANCE = 10.
|
||||
TURN_ENTRY_HEADING_DEG = 25.
|
||||
TURN_EXIT_COMPLETION_M = 12.
|
||||
|
||||
|
||||
def chosen_heading(now, llk, gps):
|
||||
"""Select a past, fresh calibrated heading; GPS course is a bounded fallback.
|
||||
|
||||
llk: valid/status/orientationValid/orientationNed/orientationStd, optional
|
||||
baseOrientationValid/baseOrientationStd, and monotonic receivedAt/publishedAt.
|
||||
gps: valid/bearingDeg/bearingAccuracyDeg/fixAgeSeconds and those timestamps.
|
||||
The f5d producer leaves calibrated std NaN; an explicit base-orientation std
|
||||
fallback describes filter uncertainty only, not unknown calibration error.
|
||||
"""
|
||||
invalid = {'headingDeg': None, 'headingValid': False, 'headingSource': None,
|
||||
'headingAgeSeconds': None, 'headingUncertaintyDeg': None,
|
||||
'headingUncertaintyKind': None, 'calibrationUncertaintyKnown': False}
|
||||
def age(sample):
|
||||
values = [now-sample['receivedAt'], now-sample['publishedAt']]
|
||||
return max(values) if np.isfinite(values).all() and all(0. <= value <= TURN_GPS_MAX_AGE for value in values) else None
|
||||
try:
|
||||
values, deviations = np.asarray(llk['orientationNed'], dtype=float), np.asarray(llk['orientationStd'], dtype=float)
|
||||
uncertainty_kind, calibration_known = 'calibrated_orientation', True
|
||||
if deviations.shape != (3,) or not np.isfinite(deviations).all() or not (deviations >= 0.).all():
|
||||
if llk.get('baseOrientationValid') is not True:
|
||||
raise ValueError('No available orientation uncertainty')
|
||||
deviations = np.asarray(llk['baseOrientationStd'], dtype=float)
|
||||
uncertainty_kind, calibration_known = 'base_orientation', False
|
||||
sample_age = age(llk)
|
||||
uncertainty = float(np.degrees(deviations[2]))
|
||||
if (llk['valid'] is True and llk['status'] == 'valid' and llk['orientationValid'] is True and
|
||||
values.shape == (3,) and deviations.shape == (3,) and np.isfinite(values).all() and
|
||||
np.isfinite(deviations).all() and (deviations >= 0.).all() and sample_age is not None and
|
||||
uncertainty <= TURN_MAX_HEADING_UNCERTAINTY):
|
||||
return {'headingDeg': float(np.degrees(values[2]) % 360.), 'headingValid': True,
|
||||
'headingSource': 'liveLocationKalman', 'headingAgeSeconds': sample_age, 'headingUncertaintyDeg': uncertainty,
|
||||
'headingUncertaintyKind': uncertainty_kind, 'calibrationUncertaintyKnown': calibration_known}
|
||||
except (KeyError, TypeError, ValueError, IndexError):
|
||||
pass
|
||||
try:
|
||||
sample_age = age(gps)
|
||||
course, uncertainty, fix_age = gps['bearingDeg'], gps['bearingAccuracyDeg'], gps['fixAgeSeconds']
|
||||
if (gps['valid'] is True and sample_age is not None and np.isfinite([course, uncertainty, fix_age]).all() and
|
||||
0. <= fix_age <= TURN_GPS_MAX_AGE and 0. < uncertainty <= TURN_MAX_HEADING_UNCERTAINTY and 0. <= course <= 360.):
|
||||
return {'headingDeg': float(course % 360.), 'headingValid': True, 'headingSource': 'gps',
|
||||
'headingAgeSeconds': max(sample_age, fix_age), 'headingUncertaintyDeg': float(uncertainty),
|
||||
'headingUncertaintyKind': 'gps_course', 'calibrationUncertaintyKnown': False}
|
||||
except (KeyError, TypeError, ValueError):
|
||||
pass
|
||||
return invalid
|
||||
|
||||
|
||||
def approach_context(target, position, bearing_deg, horizontal_accuracy):
|
||||
"""Project the raw GPS fix onto the incoming route only, without extrapolation.
|
||||
|
||||
Endpoint extensions preserve signed distance before/after this junction. The
|
||||
outgoing road is deliberately excluded, so a crossroad cannot capture the
|
||||
projection. This map/GPS check does not measure physical corner clearance.
|
||||
"""
|
||||
invalid = {'distanceM': None, 'crossTrackM': None, 'approachBearingDeg': None,
|
||||
'headingValid': False, 'locationValid': False, 'reason': 'invalid_target_geometry'}
|
||||
try:
|
||||
coordinates = np.asarray(target.approach_geometry, dtype=np.float64).reshape((-1, 2)).copy()
|
||||
position = np.asarray(position, dtype=np.float64).reshape(2).copy()
|
||||
if len(coordinates) < 2 or not np.isfinite(coordinates).all() or not np.isfinite(position).all():
|
||||
return invalid
|
||||
# Keep the local longitude difference short across the antimeridian.
|
||||
origin = coordinates[0].copy()
|
||||
coordinates[:, 0] = origin[0] + (coordinates[:, 0]-origin[0]+180.) % 360.-180.
|
||||
position[0] = origin[0] + (position[0]-origin[0]+180.) % 360.-180.
|
||||
points, point = local_meters(coordinates, origin), local_meters(position, origin)
|
||||
vectors = np.diff(points, axis=0)
|
||||
lengths = np.linalg.norm(vectors, axis=1)
|
||||
keep = lengths > .001
|
||||
starts, vectors, lengths = points[:-1][keep], vectors[keep], lengths[keep]
|
||||
if not len(lengths) or not np.isfinite([bearing_deg, horizontal_accuracy]).all():
|
||||
return invalid
|
||||
fractions = ((point-starts)*vectors).sum(axis=1)/lengths**2
|
||||
clipped = np.clip(fractions, 0., 1.)
|
||||
clipped[0] = min(clipped[0], fractions[0])
|
||||
clipped[-1] = max(clipped[-1], fractions[-1])
|
||||
distances = np.linalg.norm(point-starts-clipped[:, None]*vectors, axis=1)
|
||||
index = int(np.argmin(distances))
|
||||
cumulative = np.r_[0., np.cumsum(lengths)]
|
||||
progress = cumulative[:-1]+clipped*lengths
|
||||
heading = float(np.degrees(np.arctan2(vectors[index, 0], vectors[index, 1])) % 360.)
|
||||
heading_error = abs((bearing_deg-heading+180.) % 360.-180.)
|
||||
ambiguous = np.any((distances <= distances[index]+1.) & (np.abs(progress-progress[index]) > 15.))
|
||||
location_valid = 0. < horizontal_accuracy <= TURN_GPS_MAX_ACCURACY and distances[index] <= TURN_MAX_CROSS_TRACK and not ambiguous
|
||||
reason = ('ambiguous_approach' if ambiguous else 'approach_location_mismatch' if not location_valid else
|
||||
'approach_heading_mismatch' if heading_error > TURN_MAX_HEADING_ERROR else 'approach_match')
|
||||
return {'distanceM': float(cumulative[-1]-progress[index]), 'crossTrackM': float(distances[index]),
|
||||
'approachBearingDeg': heading, 'headingErrorDeg': float(heading_error),
|
||||
'headingValid': bool(heading_error <= TURN_MAX_HEADING_ERROR), 'locationValid': bool(location_valid),
|
||||
'reason': reason}
|
||||
except (AttributeError, TypeError, ValueError, IndexError):
|
||||
return invalid
|
||||
|
||||
|
||||
def turn_route_progress(target, position, heading, entered=False):
|
||||
"""Observed route progress only; no future pose or steering command."""
|
||||
points = np.asarray(target.geometry, dtype=float)
|
||||
local, point = local_meters(points, points[0]), local_meters(position, points[0])
|
||||
index, fraction, cross = nearest_segment(local[:-1], local[1:], point)
|
||||
distance = cumulative_distance(points)
|
||||
progress = float(distance[index]+fraction*(distance[index+1]-distance[index]))
|
||||
junction_distance = float(distance[target.junction_geometry_index])
|
||||
sign = 1. if target.direction == 'right' else -1.
|
||||
change = ((heading-target.incoming_bearing_deg+180.) % 360.-180.)*sign
|
||||
exit_error = abs((heading-target.exit_bearing_deg+180.) % 360.-180.)
|
||||
near = np.linalg.norm(point-local[target.junction_geometry_index]) <= 25.
|
||||
entered = bool(entered or near and change >= TURN_ENTRY_HEADING_DEG and exit_error <= 65. and cross <= TURN_MAX_CROSS_TRACK)
|
||||
exit_distance = min(TURN_EXIT_COMPLETION_M, .6*max(0., distance[-1]-junction_distance))
|
||||
completed = bool(entered and progress >= junction_distance+exit_distance and exit_error <= TURN_MAX_HEADING_ERROR and cross <= TURN_MAX_CROSS_TRACK)
|
||||
approach = approach_context(target, position, heading, 1.)
|
||||
missed = bool(not entered and approach['distanceM'] is not None and approach['distanceM'] < -TURN_PASSED_DISTANCE)
|
||||
return {'entered': entered, 'completed': completed, 'missed': missed,
|
||||
'routeProgressM': progress, 'crossTrackM': cross, 'exitHeadingErrorDeg': exit_error}
|
||||
|
||||
|
||||
class SignalTurnEngine:
|
||||
"""Session-tagged, token-only discovery with one off-thread request at a time."""
|
||||
def __init__(self, params, client_factory=None, executor=None, clock=None, wall_clock=None):
|
||||
self.params = params
|
||||
self.client_factory = client_factory if client_factory is not None else MapboxNextTurnClient
|
||||
self.executor = executor
|
||||
self.clock = clock if clock is not None else time.monotonic
|
||||
# GNSS fix time is UTC; only this comparison needs the wall clock.
|
||||
self.wall_clock = wall_clock if wall_clock is not None else time.time # noqa: TID251
|
||||
self.key = None
|
||||
self.generation = 0
|
||||
self.pending = None
|
||||
self.target = None
|
||||
self.context = None
|
||||
self.client = None
|
||||
self.token_loaded = False
|
||||
self.failure = None
|
||||
self.retry_at = 0.
|
||||
self.blocked = False
|
||||
self.passed = False
|
||||
self.signal_blocked = False
|
||||
self.entered = False
|
||||
self.retained = False
|
||||
self.intent_mode = None
|
||||
self.retired_key = None
|
||||
|
||||
def _invalidate(self):
|
||||
self.generation += 1
|
||||
if self.pending is not None:
|
||||
self.pending[0].cancel()
|
||||
self.target = None
|
||||
self.client = None
|
||||
self.token_loaded = False
|
||||
self.failure = None
|
||||
self.retry_at = 0.
|
||||
self.passed = False
|
||||
self.signal_blocked = False
|
||||
self.entered = False
|
||||
self.retained = False
|
||||
self.intent_mode = None
|
||||
|
||||
def _clear(self):
|
||||
if self.key is not None:
|
||||
self._invalidate()
|
||||
self.key = None
|
||||
self.blocked = False
|
||||
if self.context is not None:
|
||||
self.params.remove('MapboxTurnContext')
|
||||
self.context = None
|
||||
|
||||
def _publish(self, now, status, **values):
|
||||
self.context = {'session': self.key[0], 'direction': self.key[1], 'status': status, 'updatedAt': now,
|
||||
'distanceM': None, 'locationValid': False, 'headingValid': False, **values}
|
||||
# route_pending is an internal proposal. Exposing it between every ready
|
||||
# update would intermittently clear valid model inputs. The RouteEngine
|
||||
# publishes ready only after the route and instruction are coherent.
|
||||
if status != 'route_pending':
|
||||
# Preserve ordering with synchronous retirement/removal of this key.
|
||||
self.params.put('MapboxTurnContext', self.context, block=True)
|
||||
return self.context
|
||||
|
||||
def _consume(self):
|
||||
if self.pending is None or not self.pending[0].done():
|
||||
return
|
||||
future, generation, key = self.pending
|
||||
self.pending = None
|
||||
if generation != self.generation or key != self.key:
|
||||
return # A canceled/opposite signal, disable or destination change retires the result.
|
||||
try:
|
||||
self.target = future.result()
|
||||
if self.target.direction != self.key[1]:
|
||||
self.target = None
|
||||
self.failure = 'target_direction_mismatch'
|
||||
except NextTurnError as error:
|
||||
self.target = None
|
||||
self.failure = error.reason
|
||||
except Exception:
|
||||
# Never log exception text/tracebacks: request URLs can contain tokens.
|
||||
self.target = None
|
||||
self.failure = 'discovery_failed'
|
||||
self.retry_at = self.clock()+TURN_RETRY_SECONDS
|
||||
|
||||
def update(self, sm, source, destination_active=False):
|
||||
now = self.clock()
|
||||
if self.params.get_bool('MapboxNextTurnEnabled') is not True:
|
||||
self._clear()
|
||||
self._consume()
|
||||
return None
|
||||
intent = self.params.get('MapboxTurnIntent')
|
||||
valid_intent = (isinstance(intent, dict) and isinstance(intent.get('session'), str) and
|
||||
1 <= len(intent['session']) <= 128 and intent.get('direction') in ('left', 'right') and
|
||||
intent.get('mode') in ('pending', 'lane_change', 'turn'))
|
||||
model = self.params.get('NavModelState')
|
||||
supported = (isinstance(model, dict) and model.get('supported') is True and
|
||||
isinstance(model.get('updatedAt'), (int, float)) and 0. <= now-model['updatedAt'] <= 1.)
|
||||
try:
|
||||
control_age = now-sm.logMonoTime['carControl']*1e-9
|
||||
lateral = bool(sm.valid['carControl'] and sm['carControl'].latActive and 0. <= control_age <= TURN_GPS_MAX_AGE)
|
||||
car = sm['carState']
|
||||
canceled_after_entry = self.target is not None and self.key is not None and not car.leftBlinker and not car.rightBlinker
|
||||
except (AttributeError, KeyError, TypeError):
|
||||
lateral, canceled_after_entry = False, False
|
||||
retained = bool(canceled_after_entry and self.intent_mode == 'turn' and supported and lateral and
|
||||
(not valid_intent or (intent['session'], intent['direction']) == self.key))
|
||||
if not valid_intent and not retained:
|
||||
self._clear()
|
||||
self._consume()
|
||||
return None
|
||||
if retained:
|
||||
intent = {'session': self.key[0], 'direction': self.key[1], 'mode': 'turn', 'modelSupportsNavigation': True, 'lateralActive': True}
|
||||
key = intent['session'], intent['direction']
|
||||
if key != self.key:
|
||||
self._invalidate()
|
||||
self.key = key
|
||||
self.retired_key = None
|
||||
if destination_active != self.blocked:
|
||||
self._invalidate()
|
||||
self.blocked = destination_active
|
||||
self.intent_mode = intent['mode']
|
||||
self._consume()
|
||||
if destination_active:
|
||||
self.retired_key = key
|
||||
return self._publish(now, 'blocked', reason='explicit_destination')
|
||||
if self.retired_key == key:
|
||||
return self._publish(now, 'blocked', reason='manual_destination_preempted')
|
||||
if not supported or intent.get('modelSupportsNavigation') is not True:
|
||||
self.target = None
|
||||
return self._publish(now, 'unavailable', reason='navigation_model_required')
|
||||
if not lateral or intent.get('lateralActive') is not True:
|
||||
self.target = None
|
||||
return self._publish(now, 'unavailable', reason='lateral_inactive')
|
||||
self.retained = retained
|
||||
if self.passed:
|
||||
return self._publish(now, 'passed', reason='route_completed_or_missed')
|
||||
if source is None:
|
||||
return self._publish(now, 'unavailable', reason='gps_unavailable')
|
||||
try:
|
||||
gps, car = sm[source], sm['carState']
|
||||
receipt_age, publication_age = now-sm.recv_time[source], now-sm.logMonoTime[source]*1e-9
|
||||
message_age = max(receipt_age, publication_age)
|
||||
fix_age = self.wall_clock()-gps.unixTimestampMillis*.001
|
||||
gps_age = max(message_age, fix_age)
|
||||
accuracy = float(gps.horizontalAccuracy)
|
||||
bearing = float(gps.bearingDeg)
|
||||
position = float(gps.longitude), float(gps.latitude)
|
||||
car_receipt_age, car_publication_age = now-sm.recv_time['carState'], now-sm.logMonoTime['carState']*1e-9
|
||||
gps_valid = bool(sm.valid[source] and gps.hasFix and gps.unixTimestampMillis > 0 and
|
||||
0. <= receipt_age <= TURN_GPS_MAX_AGE and 0. <= publication_age <= TURN_GPS_MAX_AGE and
|
||||
0. <= fix_age <= TURN_GPS_MAX_AGE and
|
||||
0. < accuracy <= TURN_GPS_MAX_ACCURACY and
|
||||
np.isfinite([*position, accuracy, gps_age, car.vEgo]).all() and
|
||||
-180. <= position[0] <= 180. and -85. < position[1] < 85. and
|
||||
sm.valid['carState'] and 0. <= car_receipt_age <= TURN_GPS_MAX_AGE and
|
||||
0. <= car_publication_age <= TURN_GPS_MAX_AGE and str(car.gearShifter) != 'reverse')
|
||||
except (AttributeError, KeyError, TypeError, ValueError):
|
||||
return self._publish(now, 'unavailable', reason='gps_or_car_state_invalid')
|
||||
quality = {'gpsAgeSeconds': float(gps_age), 'gpsMessageAgeSeconds': float(message_age),
|
||||
'gpsFixAgeSeconds': float(fix_age), 'gpsHorizontalAccuracyM': accuracy,
|
||||
'gpsSource': source, 'gpsLogMonoTime': int(sm.logMonoTime[source]), 'speedMps': float(car.vEgo)}
|
||||
if not gps_valid:
|
||||
# Do not serialize NaN/inf into a shared JSON context.
|
||||
quality = {key: value for key, value in quality.items() if not isinstance(value, float) or np.isfinite(value)}
|
||||
return self._publish(now, 'unavailable', reason='gps_or_car_state_invalid', **quality)
|
||||
try:
|
||||
location = sm['liveLocationKalman']
|
||||
llk = {'valid': sm.valid['liveLocationKalman'], 'status': str(location.status),
|
||||
'orientationValid': location.calibratedOrientationNED.valid,
|
||||
'orientationNed': list(location.calibratedOrientationNED.value), 'orientationStd': list(location.calibratedOrientationNED.std),
|
||||
'baseOrientationValid': location.orientationNED.valid, 'baseOrientationStd': list(location.orientationNED.std),
|
||||
'receivedAt': sm.recv_time['liveLocationKalman'], 'publishedAt': sm.logMonoTime['liveLocationKalman']*1e-9}
|
||||
except (AttributeError, KeyError, TypeError):
|
||||
llk = None
|
||||
heading = chosen_heading(now, llk, {'valid': gps_valid, 'bearingDeg': bearing, 'bearingAccuracyDeg': float(gps.bearingAccuracyDeg),
|
||||
'receivedAt': sm.recv_time[source], 'publishedAt': sm.logMonoTime[source]*1e-9, 'fixAgeSeconds': fix_age})
|
||||
quality.update({key: value for key, value in heading.items() if key != 'headingValid'})
|
||||
if not heading['headingValid']:
|
||||
return self._publish(now, 'unavailable', reason='heading_unavailable', **quality)
|
||||
bearing = heading['headingDeg']
|
||||
signal = 'left' if car.leftBlinker and not car.rightBlinker else 'right' if car.rightBlinker and not car.leftBlinker else None
|
||||
if signal != key[1] and not retained:
|
||||
if not self.signal_blocked:
|
||||
self._invalidate()
|
||||
self.signal_blocked = True
|
||||
return self._publish(now, 'blocked', reason='physical_signal_mismatch', **quality)
|
||||
self.signal_blocked = False
|
||||
if self.target is not None:
|
||||
age = now-self.target.discovered_at
|
||||
if not 0. <= age <= TURN_TARGET_TTL:
|
||||
return self._publish(now, 'unavailable', reason='target_expired', **quality)
|
||||
match = approach_context(self.target, position, bearing, accuracy)
|
||||
progress = turn_route_progress(self.target, position, bearing, self.entered)
|
||||
if retained and not progress['entered']:
|
||||
self._clear()
|
||||
return None
|
||||
self.entered = progress['entered']
|
||||
if progress['completed'] or progress['missed']:
|
||||
self.passed = True
|
||||
if self.entered:
|
||||
match.update(locationValid=progress['crossTrackM'] <= TURN_MAX_CROSS_TRACK,
|
||||
headingValid=progress['exitHeadingErrorDeg'] <= 65.)
|
||||
status = 'passed' if self.passed else 'route_pending' if match['locationValid'] and match['headingValid'] else 'unavailable'
|
||||
return self._publish(now, status, **quality, **match, **{k:v for k,v in progress.items() if k != 'crossTrackM'},
|
||||
retainedAfterSignalCancel=retained, routeOwner='signal', junction=list(self.target.junction),
|
||||
incomingBearingDeg=self.target.incoming_bearing_deg, exitBearingDeg=self.target.exit_bearing_deg,
|
||||
requestId=self.target.validation_uuid, probeRequestId=self.target.probe_uuid,
|
||||
targetAgeSeconds=age, name=self.target.name)
|
||||
if not self.token_loaded:
|
||||
token = get_mapbox_token() # Once per session, never at 10 Hz.
|
||||
self.token_loaded = True
|
||||
if token is None:
|
||||
self.failure = 'missing_token'
|
||||
else:
|
||||
try:
|
||||
self.client = self.client_factory(token)
|
||||
except Exception:
|
||||
self.failure = 'invalid_token'
|
||||
if self.client is not None and self.pending is None and now >= self.retry_at:
|
||||
if self.executor is None:
|
||||
self.executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix='navd-signal-turn')
|
||||
self.pending = (self.executor.submit(self.client.discover, position=position, bearing_deg=bearing, direction=key[1]), self.generation, key)
|
||||
self.failure = None
|
||||
return self._publish(now, 'searching' if self.pending is not None else 'unavailable',
|
||||
reason='discovering' if self.pending is not None else self.failure or 'retry_wait', **quality)
|
||||
|
||||
def close(self):
|
||||
self._clear()
|
||||
if self.executor is not None:
|
||||
self.executor.shutdown(wait=False, cancel_futures=True)
|
||||
|
||||
|
||||
def get_mapbox_token():
|
||||
token = os.environ.get('MAPBOX_TOKEN')
|
||||
if token is not None:
|
||||
return token
|
||||
try:
|
||||
token = MAPBOX_TOKEN_PATH.read_text().strip()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except (OSError, UnicodeError) as error:
|
||||
cloudlog.event('navd.mapbox_token_unavailable', error_type=type(error).__name__, error=True)
|
||||
return None
|
||||
if not token:
|
||||
return None
|
||||
if re.fullmatch(r'pk\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+', token) is None:
|
||||
cloudlog.event('navd.mapbox_token_invalid', error=True)
|
||||
return None
|
||||
return token
|
||||
|
||||
|
||||
def set_banner(instruction, banners, distance_to_maneuver):
|
||||
@@ -415,21 +34,10 @@ def set_banner(instruction, banners, distance_to_maneuver):
|
||||
|
||||
|
||||
class RouteEngine:
|
||||
def __init__(self, sm, pm, params=None, update_frequency=1):
|
||||
def __init__(self, sm, pm, params=None):
|
||||
self.sm = sm
|
||||
self.pm = pm
|
||||
self.params = params if params is not None else Params()
|
||||
if type(update_frequency) is not int or update_frequency < 1:
|
||||
raise ValueError('Navigation update frequency must be a positive integer')
|
||||
self.update_frequency = update_frequency
|
||||
self.update_counter = 0
|
||||
self.signal_turn = SignalTurnEngine(self.params)
|
||||
self.signal_preview_key = None
|
||||
self.route_owner = None
|
||||
self.route_key = None
|
||||
self.route_generation = 0
|
||||
self.route_tracker = None
|
||||
self.maneuver_distances = None
|
||||
|
||||
self.last_position = None # (longitude, latitude)
|
||||
self.last_bearing = None
|
||||
@@ -442,98 +50,27 @@ class RouteEngine:
|
||||
self.recompute_hold = 0
|
||||
self.reroute_counter = 0
|
||||
|
||||
self.configure_routing(get_mapbox_token())
|
||||
|
||||
def configure_routing(self, token):
|
||||
self.mapbox_token = token
|
||||
self.mapbox_token = os.environ.get("MAPBOX_TOKEN")
|
||||
self.api = Api(self.params.get("DongleId")) if self.mapbox_token is None else None
|
||||
self.mapbox_host = "https://api.mapbox.com" if self.mapbox_token is not None else API_HOST.rstrip('/') + '/v1/nav'
|
||||
|
||||
def update(self):
|
||||
self.sm.update(0)
|
||||
try:
|
||||
self._update_route()
|
||||
except Exception as error:
|
||||
# API URLs can contain credentials: report only a fixed event and type.
|
||||
cloudlog.event('navd.route_processing_failed', error_type=type(error).__name__, error=True)
|
||||
self.clear_route()
|
||||
self.send_route()
|
||||
self.params.put('MapboxTurnContext', {'status': 'unavailable', 'updatedAt': time.monotonic(),
|
||||
'reason': 'route_processing_failed', 'routeGeneration': 0}, block=True)
|
||||
self.send_instruction()
|
||||
|
||||
def _update_route(self):
|
||||
source = latest_gps_source(self.sm)
|
||||
if source is not None:
|
||||
gps = self.sm[source]
|
||||
self.last_position, self.last_bearing = (gps.longitude, gps.latitude), gps.bearingDeg
|
||||
destination = self.params.get('NavDestination')
|
||||
destination_active = destination is not None
|
||||
context = self.signal_turn.update(self.sm, source, destination_active=destination_active)
|
||||
legacy_tick = self.update_counter % self.update_frequency == 0
|
||||
self.update_counter += 1
|
||||
previous = self.route
|
||||
if destination_active:
|
||||
requested_destination = destination['longitude'], destination['latitude']
|
||||
if self.route_owner == 'signal' or (self.route is not None and self.nav_destination != requested_destination):
|
||||
self.clear_route()
|
||||
self.send_route()
|
||||
if legacy_tick:
|
||||
self.recompute_route(gps_ok=source is not None)
|
||||
if self.route is not previous:
|
||||
self.send_route()
|
||||
if legacy_tick or self.route is not previous:
|
||||
self.send_instruction()
|
||||
if self.route is not None and self.nav_destination == requested_destination:
|
||||
self.params.put('MapboxTurnContext', {'routeOwner': 'destination', 'status': 'ready',
|
||||
'updatedAt': time.monotonic(), 'routeGeneration': self.route_generation,
|
||||
'routePublishedAt': self.route_generation*1e-9,
|
||||
'destination': {'longitude': self.nav_destination[0], 'latitude': self.nav_destination[1]},
|
||||
'automaticStatus': 'blocked' if context is not None else None}, block=True)
|
||||
else:
|
||||
self.params.put('MapboxTurnContext', {'routeOwner': 'destination', 'status': 'searching',
|
||||
'updatedAt': time.monotonic(), 'reason': 'destination_route_pending'}, block=True)
|
||||
elif context is not None and context['status'] == 'route_pending':
|
||||
target = self.signal_turn.target
|
||||
key = context['session'], target.validation_uuid
|
||||
if self.route_owner != 'signal' or self.route_key != key:
|
||||
self.install_route([copy.deepcopy(dict(step)) for step in target.to_dict()['steps']],
|
||||
tuple(target.geometry[-1]), owner='signal', key=key)
|
||||
self.send_route()
|
||||
route = self.route
|
||||
try:
|
||||
self.recompute_route(gps_ok=source is not None)
|
||||
self.send_instruction()
|
||||
self.signal_turn._publish(context['updatedAt'], 'ready', **{k:v for k,v in context.items()
|
||||
if k not in ('session', 'direction', 'status', 'updatedAt')}, routeGeneration=self.route_generation,
|
||||
routePublishedAt=self.route_generation*1e-9)
|
||||
else:
|
||||
target = self.signal_turn.target
|
||||
same_signal_route = (self.route_owner == 'signal' and context is not None and target is not None and
|
||||
self.route_key == (context['session'], target.validation_uuid))
|
||||
transient_unavailable = (same_signal_route and context['status'] == 'unavailable' and
|
||||
context.get('reason') not in ('target_expired', 'navigation_model_required', 'lateral_inactive', 'route_processing_failed'))
|
||||
# Keep the generation through a brief localization outage. Context and
|
||||
# instructions remain invalid, so no stale conditioning is authorized.
|
||||
if self.route_owner is not None and not transient_unavailable:
|
||||
self.clear_route()
|
||||
self.send_route()
|
||||
if context is not None:
|
||||
self.send_signal_instruction(context)
|
||||
else:
|
||||
self.params.remove('MapboxTurnContext')
|
||||
self.send_instruction()
|
||||
resend_period = self.update_frequency if self.route_owner == 'signal' else ROUTE_RESEND_PERIOD*self.update_frequency
|
||||
if self.sm.frame % resend_period == 0:
|
||||
except Exception:
|
||||
cloudlog.exception("navd.failed_to_compute")
|
||||
# mapsd dedupes repeats, so a periodic resend is how a restarted mapsd gets the route
|
||||
if self.route is not route or self.sm.frame % ROUTE_RESEND_PERIOD == 0:
|
||||
self.send_route()
|
||||
|
||||
def install_route(self, steps, destination, owner='destination', key=None):
|
||||
self.route = steps
|
||||
self.route_geometry = [np.asarray(step['geometry']['coordinates'], dtype=float).reshape((-1, 2)) for step in steps]
|
||||
self.route_tracker = RouteTracker(np.concatenate(self.route_geometry))
|
||||
starts = np.cumsum([0]+[len(path) for path in self.route_geometry[:-1]])
|
||||
self.maneuver_distances = self.route_tracker.distance[starts]
|
||||
self.step_idx, self.nav_destination = 0, destination
|
||||
self.route_owner, self.route_key = owner, key
|
||||
self.route_generation = 0
|
||||
|
||||
def recompute_route(self, gps_ok: bool):
|
||||
if self.last_position is None:
|
||||
return
|
||||
@@ -549,14 +86,8 @@ class RouteEngine:
|
||||
if not gps_ok and self.step_idx is not None:
|
||||
return
|
||||
|
||||
token = get_mapbox_token()
|
||||
routing_changed = token != self.mapbox_token
|
||||
if routing_changed:
|
||||
self.configure_routing(token)
|
||||
self.recompute_hold = 0
|
||||
|
||||
self.recompute_hold -= 1
|
||||
if (routing_changed or self.should_recompute() or new_destination != self.nav_destination) and self.recompute_hold <= 0:
|
||||
if (self.should_recompute() or new_destination != self.nav_destination) and self.recompute_hold <= 0:
|
||||
self.recompute_hold = RECOMPUTE_HOLD
|
||||
self.calculate_route(new_destination)
|
||||
self.reroute_counter = 0
|
||||
@@ -588,24 +119,23 @@ class RouteEngine:
|
||||
|
||||
coords_str = ';'.join(f'{lon},{lat}' for lon, lat in (self.last_position, destination))
|
||||
url = self.mapbox_host + '/directions/v5/mapbox/driving-traffic/' + coords_str
|
||||
status_code = None
|
||||
try:
|
||||
resp = requests.get(url, params=params, timeout=10)
|
||||
status_code = resp.status_code
|
||||
if resp.status_code != 200:
|
||||
cloudlog.event("API request failed", status_code=resp.status_code, text=resp.text, error=True)
|
||||
resp.raise_for_status()
|
||||
|
||||
r = resp.json()
|
||||
if len(r['routes']):
|
||||
self.install_route(r['routes'][0]['legs'][0]['steps'], destination)
|
||||
self.route = r['routes'][0]['legs'][0]['steps']
|
||||
self.route_geometry = [np.array(step['geometry']['coordinates'], dtype=np.float64).reshape((-1, 2)) for step in self.route]
|
||||
self.step_idx = 0
|
||||
else:
|
||||
cloudlog.warning("Got empty route response")
|
||||
self.clear_route()
|
||||
|
||||
except requests.exceptions.RequestException as error:
|
||||
# HTTP errors and response bodies can contain the URL, including access_token.
|
||||
if error.response is not None:
|
||||
status_code = error.response.status_code
|
||||
cloudlog.event('navd.route_request_failed', status_code=status_code, error_type=type(error).__name__, error=True)
|
||||
except requests.exceptions.RequestException:
|
||||
cloudlog.exception("failed to get route")
|
||||
self.clear_route()
|
||||
|
||||
def send_instruction(self):
|
||||
@@ -618,26 +148,17 @@ class RouteEngine:
|
||||
|
||||
assert self.route is not None and self.route_geometry is not None and self.last_position is not None
|
||||
step = self.route[self.step_idx]
|
||||
geometry = self.route_geometry[self.step_idx]
|
||||
along_geometry = (along_path(geometry, self.last_position)[0] if len(geometry) >= 2 else
|
||||
float(np.linalg.norm(local_meters(self.last_position, geometry[0]))))
|
||||
along_geometry, _ = along_path(self.route_geometry[self.step_idx], self.last_position)
|
||||
distance_to_maneuver_along_geometry = step['distance'] - along_geometry
|
||||
|
||||
# Banner instructions are for the following maneuver step, don't use empty last step
|
||||
banner_step = step
|
||||
if not len(banner_step.get('bannerInstructions', [])) and self.step_idx == len(self.route) - 1:
|
||||
if not len(banner_step['bannerInstructions']) and self.step_idx == len(self.route) - 1:
|
||||
banner_step = self.route[max(self.step_idx - 1, 0)]
|
||||
|
||||
# Current instruction
|
||||
msg.navInstruction.maneuverDistance = distance_to_maneuver_along_geometry
|
||||
set_banner(msg.navInstruction, banner_step.get('bannerInstructions', []), distance_to_maneuver_along_geometry)
|
||||
|
||||
# Exact afbf2e5 upstream allManeuvers semantics: each step starts at its
|
||||
# maneuver, relative to monotonic measured progress along the full route.
|
||||
progress = self.route_tracker.project(*self.last_position)
|
||||
msg.navInstruction.allManeuvers = [
|
||||
{'distance': float(distance-progress), 'type': item['maneuver']['type'], 'modifier': item['maneuver'].get('modifier', '')}
|
||||
for item, distance in zip(self.route, self.maneuver_distances, strict=True) if distance >= progress]
|
||||
set_banner(msg.navInstruction, banner_step['bannerInstructions'], distance_to_maneuver_along_geometry)
|
||||
|
||||
# Total remaining distance and time: the rest of this step, then every step after it
|
||||
remaining = 1.0 - along_geometry / max(step['distance'], 1)
|
||||
@@ -647,12 +168,6 @@ class RouteEngine:
|
||||
|
||||
self.pm.send('navInstruction', msg)
|
||||
|
||||
if self.route_owner == 'signal':
|
||||
# The signal engine owns bounded completion/miss; never chase its outlet
|
||||
# with destination rerouting or remove a user-owned NavDestination.
|
||||
self.step_idx = min(int(np.searchsorted(self.maneuver_distances, progress, side='right'))-1, len(self.route)-1)
|
||||
return
|
||||
|
||||
# Transition to next route segment
|
||||
if distance_to_maneuver_along_geometry < -MANEUVER_TRANSITION_THRESHOLD:
|
||||
if self.step_idx + 1 < len(self.route):
|
||||
@@ -669,48 +184,14 @@ class RouteEngine:
|
||||
def send_route(self):
|
||||
coords = np.concatenate(self.route_geometry) if self.route_geometry is not None else np.empty((0, 2))
|
||||
msg = messaging.new_message('navRoute', valid=True)
|
||||
if not self.route_generation:
|
||||
self.route_generation = int(msg.logMonoTime)
|
||||
msg.logMonoTime = self.route_generation
|
||||
msg.navRoute.coordinates = [{'longitude': lon, 'latitude': lat} for lon, lat in coords.tolist()]
|
||||
self.pm.send('navRoute', msg)
|
||||
|
||||
def send_signal_instruction(self, context):
|
||||
msg = messaging.new_message('navInstruction', valid=False)
|
||||
if context['status'] == 'ready':
|
||||
instruction = msg.navInstruction
|
||||
instruction.maneuverPrimaryText = 'Signal turn'
|
||||
name = context.get('name', '')
|
||||
instruction.maneuverSecondaryText = f"{context['direction'].capitalize()} onto {name}" if name else context['direction'].capitalize()
|
||||
instruction.maneuverType = 'turn'
|
||||
instruction.maneuverModifier = context['direction']
|
||||
instruction.maneuverDistance = max(0., context['distanceM'])
|
||||
instruction.distanceRemaining = max(0., context['distanceM'])
|
||||
instruction.timeRemaining = max(0., context['distanceM'])/max(context['speedMps'], 1.)
|
||||
instruction.showFull = True
|
||||
else:
|
||||
texts = {'searching': (f"Finding next {context['direction']}", 'Creating a short navigation route'),
|
||||
'unavailable': ('Navigation unavailable', 'Route conditioning withheld'),
|
||||
'blocked': ('Signal turn blocked', 'Destination navigation active'),
|
||||
'passed': ('Turn passed', 'Signal again for another turn')}
|
||||
primary, secondary = texts.get(context['status'], texts['unavailable'])
|
||||
msg.navInstruction.maneuverPrimaryText = primary
|
||||
msg.navInstruction.maneuverSecondaryText = secondary
|
||||
msg.navInstruction.maneuverType = 'notification'
|
||||
msg.navInstruction.maneuverModifier = ''
|
||||
self.pm.send('navInstruction', msg)
|
||||
|
||||
def close(self):
|
||||
self.signal_turn.close()
|
||||
|
||||
def clear_route(self):
|
||||
self.route = None
|
||||
self.route_geometry = None
|
||||
self.step_idx = None
|
||||
self.nav_destination = None
|
||||
self.route_owner, self.route_key = None, None
|
||||
self.route_generation = 0
|
||||
self.route_tracker, self.maneuver_distances = None, None
|
||||
|
||||
def should_recompute(self):
|
||||
if self.step_idx is None:
|
||||
@@ -731,16 +212,13 @@ class RouteEngine:
|
||||
|
||||
def main():
|
||||
pm = messaging.PubMaster(['navInstruction', 'navRoute'])
|
||||
sm = messaging.SubMaster(['gpsLocation', 'gpsLocationExternal', 'carState', 'carControl', 'liveLocationKalman'])
|
||||
sm = messaging.SubMaster(['gpsLocation', 'gpsLocationExternal'])
|
||||
|
||||
rk = Ratekeeper(10.)
|
||||
route_engine = RouteEngine(sm, pm, update_frequency=10)
|
||||
try:
|
||||
while True:
|
||||
route_engine.update()
|
||||
rk.keep_time()
|
||||
finally:
|
||||
route_engine.close()
|
||||
rk = Ratekeeper(1.0)
|
||||
route_engine = RouteEngine(sm, pm)
|
||||
while True:
|
||||
route_engine.update()
|
||||
rk.keep_time()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,447 +0,0 @@
|
||||
"""Standalone recorded-schema fixtures: no credentials, network or driving code."""
|
||||
from dataclasses import FrozenInstanceError
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import traceback
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
MODULE = Path(__file__).resolve().parents[1] / 'mapbox_next_turn.py'
|
||||
spec = importlib.util.spec_from_file_location('mapbox_next_turn_under_test', MODULE)
|
||||
next_turn = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = next_turn
|
||||
spec.loader.exec_module(next_turn)
|
||||
|
||||
ORIGIN = (0., 0.)
|
||||
JUNCTION = next_turn.offset(ORIGIN, 0., 100.)
|
||||
PROBE = next_turn.offset(ORIGIN, 0., next_turn.PROBE_DISTANCE_M)
|
||||
EXIT = next_turn.offset(JUNCTION, 90., next_turn.EXIT_QUERY_DISTANCE_M)
|
||||
TOKEN = 'pk.synthetic.signature'
|
||||
|
||||
|
||||
def junction(location=JUNCTION, index=1, out=0):
|
||||
return {'location': list(location), 'geometry_index': index, 'bearings': [0, 90, 180, 270],
|
||||
'entry': [True, True, False, True], 'in': 2, 'out': out}
|
||||
|
||||
|
||||
def body(turn=False, road_class='street'):
|
||||
endpoint = EXIT if turn else PROBE
|
||||
points = [list(ORIGIN), list(JUNCTION), list(endpoint)]
|
||||
source = {'location': list(ORIGIN), 'bearings': [0], 'entry': [True], 'out': 0, 'geometry_index': 0}
|
||||
arrival = {'location': list(endpoint), 'bearings': [270 if turn else 180], 'entry': [True], 'in': 0, 'geometry_index': 2}
|
||||
at = junction(out=1 if turn else 0)
|
||||
if road_class is not None:
|
||||
at['mapbox_streets_v8'] = {'class': road_class}
|
||||
steps = [
|
||||
{'mode': 'driving', 'name': 'Approach', 'maneuver': {'type': 'depart', 'location': list(ORIGIN)},
|
||||
'geometry': {'type': 'LineString', 'coordinates': points[:2] if turn else points},
|
||||
'intersections': [source] if turn else [source, at]},
|
||||
]
|
||||
if turn:
|
||||
steps.append({'mode': 'driving', 'name': 'Side Street', 'maneuver': {'type': 'turn', 'modifier': 'right', 'location': list(JUNCTION)},
|
||||
'geometry': {'type': 'LineString', 'coordinates': points[1:]}, 'intersections': [at], 'bannerInstructions': []})
|
||||
steps.append({'mode': 'driving', 'name': 'End', 'maneuver': {'type': 'arrive', 'location': list(endpoint)},
|
||||
'geometry': {'type': 'LineString', 'coordinates': [points[-1]]}, 'intersections': [arrival]})
|
||||
for step in steps:
|
||||
step['distance'] = next_turn._length(step['geometry']['coordinates'])
|
||||
step['duration'] = step['distance']/4.
|
||||
step.setdefault('bannerInstructions', [])
|
||||
return {'code': 'Ok', 'uuid': 'validation-id' if turn else 'probe-id',
|
||||
'waypoints': [{'location': list(ORIGIN)}, {'location': list(endpoint)}],
|
||||
'routes': [{'geometry': {'type': 'LineString', 'coordinates': points}, 'legs': [{'steps': steps}]}]}
|
||||
|
||||
|
||||
def response(value):
|
||||
return Mock(content=json.dumps(value).encode(), json=Mock(return_value=value), raise_for_status=Mock())
|
||||
|
||||
|
||||
class NextTurnClientTests(unittest.TestCase):
|
||||
def discover(self, probe=None, validated=None, third=None, direction='right', bearing=0.):
|
||||
values = [body() if probe is None else probe, body(True) if validated is None else validated]
|
||||
if third is not None:
|
||||
values.append(third)
|
||||
with patch.object(next_turn.requests, 'get', side_effect=[response(value) for value in values]) as request:
|
||||
target = next_turn.MapboxNextTurnClient(TOKEN).discover(ORIGIN, bearing, direction)
|
||||
return target, request
|
||||
|
||||
def test_validated_target_excludes_synthetic_ahead_probe(self):
|
||||
target, request = self.discover()
|
||||
self.assertEqual(request.call_count, 2)
|
||||
self.assertEqual(target.junction, JUNCTION)
|
||||
self.assertAlmostEqual(target.distance_m, 100.)
|
||||
self.assertEqual(target.incoming_bearing_deg, 0.)
|
||||
self.assertEqual(target.exit_bearing_deg, 90.)
|
||||
self.assertEqual(target.geometry[-1], EXIT)
|
||||
self.assertNotEqual(target.geometry[-1], PROBE)
|
||||
self.assertEqual(target.approach_geometry, (ORIGIN, JUNCTION))
|
||||
self.assertEqual(target.turn_step_index, 1)
|
||||
self.assertEqual(target.junction_geometry_index, 1)
|
||||
self.assertFalse(target.provenance['probeEndpointIsTarget'])
|
||||
self.assertFalse(target.provenance['steeringCommandIssued'])
|
||||
self.assertFalse(target.provenance['probeFallbackUsed'])
|
||||
self.assertEqual(target.provenance['probeEndpointRadiusM'], 100.)
|
||||
for call in request.call_args_list:
|
||||
self.assertEqual(call.kwargs['timeout'], (2., 3.))
|
||||
self.assertEqual(call.kwargs['params']['snapping_include_closures'], 'false;false')
|
||||
self.assertEqual(call.kwargs['params']['snapping_include_static_closures'], 'false;false')
|
||||
self.assertNotIn(TOKEN, json.dumps(target.to_dict()))
|
||||
|
||||
def test_target_and_recursive_steps_are_immutable(self):
|
||||
target, _ = self.discover()
|
||||
with self.assertRaises(FrozenInstanceError):
|
||||
target.name = 'changed'
|
||||
with self.assertRaises(TypeError):
|
||||
target.steps[1]['name'] = 'changed'
|
||||
mutable = target.to_dict()
|
||||
mutable['steps'][1]['name'] = 'changed'
|
||||
self.assertEqual(target.steps[1]['name'], 'Side Street')
|
||||
|
||||
def test_restricted_entry_is_not_a_candidate(self):
|
||||
probe = body()
|
||||
probe['routes'][0]['legs'][0]['steps'][0]['intersections'][1]['entry'][1] = False
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'no_matching_turn'):
|
||||
self.discover(probe=probe)
|
||||
|
||||
def test_incoming_heading_is_rotated_180_degrees(self):
|
||||
target, _ = self.discover()
|
||||
self.assertEqual(next_turn._delta(target.incoming_bearing_deg, target.exit_bearing_deg), 90.)
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'incoming_heading_mismatch'):
|
||||
next_turn._parse_route(body(), ORIGIN, 180., PROBE, 100.)
|
||||
|
||||
def test_service_exit_is_rejected_even_when_named(self):
|
||||
for road_class in ('service', 'street_limited'):
|
||||
with self.subTest(road_class=road_class), self.assertRaisesRegex(next_turn.NextTurnError, 'unsupported_road'):
|
||||
self.discover(validated=body(True, road_class))
|
||||
|
||||
def test_restricted_selected_exit_is_rejected(self):
|
||||
value = body(True)
|
||||
value['routes'][0]['legs'][0]['steps'][1]['intersections'][0]['classes'] = ['restricted']
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'unsupported_road'):
|
||||
self.discover(validated=value)
|
||||
|
||||
def test_city_trunk_street_is_allowed_but_motorway_and_ramp_are_not(self):
|
||||
target, _ = self.discover(validated=body(True, 'trunk'))
|
||||
self.assertEqual(target.road_class, 'trunk')
|
||||
for kind in ('motorway', 'restricted'):
|
||||
value = body(True, 'trunk')
|
||||
value['routes'][0]['legs'][0]['steps'][1]['intersections'][0]['classes'] = [kind]
|
||||
with self.subTest(kind=kind), self.assertRaisesRegex(next_turn.NextTurnError, 'unsupported_road'):
|
||||
self.discover(validated=value)
|
||||
value = body(True, 'trunk')
|
||||
value['routes'][0]['legs'][0]['steps'][1]['maneuver']['type'] = 'off ramp'
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'unsupported_junction'):
|
||||
self.discover(validated=value)
|
||||
|
||||
def test_native_north_bearing_360_is_normalized_without_accepting_out_of_range(self):
|
||||
# Real 5e-15 Directions intersections contain north encoded as 360.
|
||||
probe = body()
|
||||
at = probe['routes'][0]['legs'][0]['steps'][0]['intersections'][1]
|
||||
at['bearings'][0] = 360
|
||||
target, _ = self.discover(probe=probe)
|
||||
self.assertEqual(target.incoming_bearing_deg, 0.)
|
||||
at['bearings'][0] = 361
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'missing_metadata'):
|
||||
self.discover(probe=probe)
|
||||
|
||||
def test_missing_road_class_requires_separate_same_turn_validation(self):
|
||||
third = body(True)
|
||||
third['uuid'] = 'classification-id'
|
||||
target, request = self.discover(validated=body(True, None), third=third)
|
||||
self.assertEqual(request.call_count, 3)
|
||||
self.assertIn('/mapbox/driving/', request.call_args_list[2].args[0])
|
||||
self.assertEqual(target.classification_uuid, 'classification-id')
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'missing_road_class'):
|
||||
self.discover(validated=body(True, None), third=body(True, None))
|
||||
|
||||
def test_missing_intermediate_legality_metadata_fails_closed(self):
|
||||
probe = body()
|
||||
del probe['routes'][0]['legs'][0]['steps'][0]['intersections'][1]['in']
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'missing_metadata'):
|
||||
self.discover(probe=probe)
|
||||
|
||||
def test_non_boolean_entry_flags_are_not_treated_as_truth(self):
|
||||
probe = body()
|
||||
probe['routes'][0]['legs'][0]['steps'][0]['intersections'][1]['entry'][1] = 1
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'missing_metadata'):
|
||||
self.discover(probe=probe)
|
||||
|
||||
def test_validation_cannot_turn_at_an_earlier_junction(self):
|
||||
value = body(True)
|
||||
earlier = next_turn.offset(ORIGIN, 0, 50.)
|
||||
value['routes'][0]['geometry']['coordinates'].insert(1, list(earlier))
|
||||
turn = value['routes'][0]['legs'][0]['steps'][1]['intersections'][0]
|
||||
turn.update(location=list(earlier), geometry_index=1)
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'earlier_or_additional_turn'):
|
||||
self.discover(validated=value)
|
||||
|
||||
def test_nearby_different_junction_is_not_the_selected_target(self):
|
||||
value = body(True)
|
||||
different = next_turn.offset(JUNCTION, 0., 5.)
|
||||
value['routes'][0]['geometry']['coordinates'][1] = list(different)
|
||||
step = value['routes'][0]['legs'][0]['steps'][1]
|
||||
step['geometry']['coordinates'][0] = list(different)
|
||||
step['intersections'][0]['location'] = list(different)
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'earlier_or_additional_turn'):
|
||||
self.discover(validated=value)
|
||||
|
||||
def test_two_edge_road_bend_does_not_consume_turn_intent(self):
|
||||
item = {'location': JUNCTION, 'distance': 100., 'kind': 'depart', 'step': 0,
|
||||
'incoming': 0., 'outgoing': 90., 'bearings': [90., 180.], 'entry': [True, False]}
|
||||
route = {'intersections': [item], 'steps': [{'maneuver': {'type': 'depart', 'location': ORIGIN}}]}
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'no_matching_turn'):
|
||||
next_turn._choose_branch(route, 'right')
|
||||
# The same two edges are eligible if the router explicitly labels a turn here.
|
||||
item['kind'] = 'turn'
|
||||
route['steps'][0]['maneuver'] = {'type': 'turn', 'location': JUNCTION}
|
||||
self.assertEqual(next_turn._choose_branch(route, 'right')['exit'], 90.)
|
||||
|
||||
def test_continuing_main_road_bend_with_minor_branch_does_not_hide_next_turn(self):
|
||||
bend = {'location': JUNCTION, 'distance': 100., 'kind': 'continue', 'step': 0,
|
||||
'incoming': 0., 'outgoing': 90., 'out': 0, 'bearings': [90., 180., 270.], 'entry': [True, False, True]}
|
||||
following = {'location': next_turn.offset(JUNCTION, 90., 50.), 'distance': 150., 'kind': 'continue', 'step': 0,
|
||||
'incoming': 90., 'outgoing': 90., 'out': 0, 'bearings': [90., 180., 270.], 'entry': [True, True, False]}
|
||||
route = {'intersections': [bend, following], 'steps': [{'maneuver': {'type': 'continue', 'location': ORIGIN}}]}
|
||||
selected = next_turn._choose_branch(route, 'right')
|
||||
self.assertEqual(selected['location'], following['location'])
|
||||
self.assertEqual(selected['exit'], 180.)
|
||||
|
||||
def test_probe_uturn_can_expose_first_divided_road_branch_but_is_not_target_authority(self):
|
||||
# Native route60's ahead query U-turns at Thornbrook. Its first left edge
|
||||
# can be queried, but the second route must prove an ordinary left turn.
|
||||
item = {'location': JUNCTION, 'distance': 100., 'kind': 'continue', 'step': 0,
|
||||
'incoming': 123., 'outgoing': 37., 'out': 0,
|
||||
'bearings': [37., 122., 220., 303.], 'entry': [True, True, True, False]}
|
||||
route = {'intersections': [item],
|
||||
'steps': [{'maneuver': {'type': 'continue', 'modifier': 'uturn', 'location': JUNCTION}}]}
|
||||
selected = next_turn._choose_branch(route, 'left')
|
||||
self.assertEqual(selected['exit'], 37.)
|
||||
|
||||
def test_later_turn_beyond_proven_exit_is_excluded_from_local_target(self):
|
||||
value = body(True)
|
||||
later = next_turn.offset(JUNCTION, 90., 35.)
|
||||
endpoint = next_turn.offset(later, 0., 5.)
|
||||
value['waypoints'][-1]['location'] = list(endpoint)
|
||||
points = [list(ORIGIN), list(JUNCTION), list(later), list(endpoint)]
|
||||
route = value['routes'][0]
|
||||
route['geometry']['coordinates'] = points
|
||||
steps = route['legs'][0]['steps']
|
||||
steps[1]['geometry']['coordinates'] = points[1:3]
|
||||
extra = junction(location=later, index=2, out=0)
|
||||
extra['in'] = 3
|
||||
extra['mapbox_streets_v8'] = {'class': 'service'}
|
||||
steps.insert(2, {'mode': 'driving', 'name': 'Later road',
|
||||
'maneuver': {'type': 'turn', 'location': list(later)},
|
||||
'geometry': {'type': 'LineString', 'coordinates': points[2:]}, 'intersections': [extra]})
|
||||
steps[-1]['maneuver']['location'] = list(endpoint)
|
||||
steps[-1]['geometry']['coordinates'] = [list(endpoint)]
|
||||
steps[-1]['intersections'][0].update(location=list(endpoint), geometry_index=3)
|
||||
parsed = next_turn._parse_route(value, ORIGIN, 0., endpoint, 15.)
|
||||
probe = next_turn._parse_route(body(), ORIGIN, 0., PROBE, 100.)
|
||||
candidate = next_turn._choose_branch(probe, 'right')
|
||||
_, _, scoped = next_turn._validate_turn(parsed, candidate, 'right')
|
||||
self.assertTrue(scoped['targetScopeTruncated'])
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'incomplete_navigation_route'):
|
||||
next_turn.coherent_route_steps(scoped)
|
||||
|
||||
def test_additional_turn_within_first_20m_of_exit_is_rejected(self):
|
||||
value = body(True)
|
||||
near = next_turn.offset(JUNCTION, 90., 10.)
|
||||
value['routes'][0]['geometry']['coordinates'].insert(2, list(near))
|
||||
step = value['routes'][0]['legs'][0]['steps'][1]
|
||||
extra = junction(location=near, index=2, out=0)
|
||||
extra['in'] = 3
|
||||
step['intersections'].append(extra)
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'earlier_or_additional_turn'):
|
||||
self.discover(validated=value)
|
||||
|
||||
def test_scoped_exit_uses_along_road_length_for_gently_curved_street(self):
|
||||
value = body(True)
|
||||
first = next_turn.offset(JUNCTION, 90., 10.)
|
||||
later = next_turn.offset(first, 100., 25.)
|
||||
endpoint = next_turn.offset(later, 0., 5.)
|
||||
value['waypoints'][-1]['location'] = list(endpoint)
|
||||
points = [list(ORIGIN), list(JUNCTION), list(first), list(later), list(endpoint)]
|
||||
value['routes'][0]['geometry']['coordinates'] = points
|
||||
steps = value['routes'][0]['legs'][0]['steps']
|
||||
steps[1]['geometry']['coordinates'] = points[1:4]
|
||||
extra = {'location': list(later), 'geometry_index': 3, 'bearings': [0, 180, 280],
|
||||
'entry': [True, True, False], 'in': 2, 'out': 0, 'mapbox_streets_v8': {'class': 'street'}}
|
||||
steps.insert(2, {'mode': 'driving', 'name': 'Later street',
|
||||
'maneuver': {'type': 'turn', 'location': list(later)},
|
||||
'geometry': {'type': 'LineString', 'coordinates': points[3:]}, 'intersections': [extra]})
|
||||
steps[-1]['maneuver']['location'] = list(endpoint)
|
||||
steps[-1]['geometry']['coordinates'] = [list(endpoint)]
|
||||
steps[-1]['intersections'][0].update(location=list(endpoint), geometry_index=4)
|
||||
parsed = next_turn._parse_route(value, ORIGIN, 0., endpoint, 15.)
|
||||
probe = next_turn._parse_route(body(), ORIGIN, 0., PROBE, 100.)
|
||||
_, _, scoped = next_turn._validate_turn(parsed, next_turn._choose_branch(probe, 'right'), 'right')
|
||||
self.assertTrue(scoped['targetScopeTruncated'])
|
||||
self.assertAlmostEqual(next_turn._length(scoped['points'][1:]), 20., places=5)
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'incomplete_navigation_route'):
|
||||
next_turn.coherent_route_steps(scoped)
|
||||
|
||||
def test_validation_cannot_choose_opposite_exit(self):
|
||||
value = body(True)
|
||||
value['routes'][0]['legs'][0]['steps'][1]['intersections'][0]['out'] = 3
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'target_mismatch'):
|
||||
self.discover(validated=value)
|
||||
|
||||
def test_clipped_discovery_is_requeried_as_a_complete_actual_exit_route(self):
|
||||
value = body(True)
|
||||
later = next_turn.offset(JUNCTION, 90., 35.)
|
||||
endpoint = next_turn.offset(later, 0., 5.)
|
||||
value['waypoints'][-1]['location'] = list(endpoint)
|
||||
points = [list(ORIGIN), list(JUNCTION), list(later), list(endpoint)]
|
||||
route = value['routes'][0]
|
||||
route['geometry']['coordinates'] = points
|
||||
steps = route['legs'][0]['steps']
|
||||
steps[1]['geometry']['coordinates'] = points[1:3]
|
||||
extra = junction(location=later, index=2, out=0)
|
||||
extra['in'] = 3
|
||||
steps.insert(2, {'mode': 'driving', 'name': 'Later', 'maneuver': {'type': 'turn', 'location': list(later)},
|
||||
'geometry': {'coordinates': points[2:]}, 'intersections': [extra]})
|
||||
steps[-1]['maneuver']['location'] = list(endpoint)
|
||||
steps[-1]['geometry']['coordinates'] = [list(endpoint)]
|
||||
steps[-1]['intersections'][0].update(location=list(endpoint), geometry_index=3)
|
||||
|
||||
final = body(True)
|
||||
actual_endpoint = next_turn.offset(JUNCTION, 90., 20.)
|
||||
final['waypoints'][-1]['location'] = list(actual_endpoint)
|
||||
final_route = final['routes'][0]
|
||||
final_route['geometry']['coordinates'][-1] = list(actual_endpoint)
|
||||
final_steps = final_route['legs'][0]['steps']
|
||||
final_steps[1]['geometry']['coordinates'][-1] = list(actual_endpoint)
|
||||
final_steps[1]['distance'], final_steps[1]['duration'] = 20., 5.
|
||||
final_steps[-1]['geometry']['coordinates'] = [list(actual_endpoint)]
|
||||
final_steps[-1]['maneuver']['location'] = list(actual_endpoint)
|
||||
final_steps[-1]['intersections'][0]['location'] = list(actual_endpoint)
|
||||
target, request = self.discover(validated=value, third=final)
|
||||
self.assertEqual(request.call_count, 3)
|
||||
self.assertEqual(target.provenance['requestCount'], 3)
|
||||
self.assertTrue(target.provenance['coherentNavigationRoute'])
|
||||
self.assertFalse(target.provenance['targetScopeTruncated'])
|
||||
self.assertEqual(target.steps[-1]['maneuver']['type'], 'arrive')
|
||||
self.assertAlmostEqual(target.steps[1]['distance'], 20.)
|
||||
|
||||
def test_navigation_route_rejects_missing_arrival_or_disconnected_steps(self):
|
||||
route = body(True)['routes'][0]
|
||||
steps = route['legs'][0]['steps']
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'incomplete_navigation_route'):
|
||||
next_turn.coherent_route_steps({'steps': steps[:-1]})
|
||||
steps[1]['geometry']['coordinates'][0] = list(next_turn.offset(JUNCTION, 0., 10.))
|
||||
steps[1]['distance'] = next_turn._length(steps[1]['geometry']['coordinates'])
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'disconnected_navigation_route'):
|
||||
next_turn.coherent_route_steps({'steps': steps})
|
||||
|
||||
def test_close_intersection_after_corner_uses_its_local_incoming_segment(self):
|
||||
value = body(True)
|
||||
close = next_turn.offset(JUNCTION, 90., 3.)
|
||||
value['routes'][0]['geometry']['coordinates'].insert(2, list(close))
|
||||
step = value['routes'][0]['legs'][0]['steps'][1]
|
||||
step['geometry']['coordinates'].insert(1, list(close))
|
||||
step['intersections'].append({'location': list(close), 'geometry_index': 2,
|
||||
'bearings': [90, 270], 'entry': [True, False], 'in': 1, 'out': 0,
|
||||
'mapbox_streets_v8': {'class': 'street'}})
|
||||
target, _ = self.discover(validated=value)
|
||||
self.assertEqual(target.junction, JUNCTION)
|
||||
|
||||
def test_unrelated_small_turn_step_cannot_be_hidden_by_bearing_threshold(self):
|
||||
value = body(True)
|
||||
extra = {'mode': 'driving', 'name': 'Detour',
|
||||
'maneuver': {'type': 'turn', 'modifier': 'slight right', 'location': list(ORIGIN)},
|
||||
'geometry': {'type': 'LineString', 'coordinates': [list(ORIGIN), list(JUNCTION)]}, 'intersections': []}
|
||||
value['routes'][0]['legs'][0]['steps'].insert(1, extra)
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'earlier_or_additional_turn'):
|
||||
self.discover(validated=value)
|
||||
|
||||
def test_validation_rejects_uturn(self):
|
||||
value = body(True)
|
||||
at = value['routes'][0]['legs'][0]['steps'][1]['intersections'][0]
|
||||
at['out'], at['entry'][2] = 2, True
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'target_mismatch'):
|
||||
self.discover(validated=value)
|
||||
|
||||
def test_bad_origin_snap_is_rejected(self):
|
||||
value = body()
|
||||
value['waypoints'][0]['location'] = list(next_turn.offset(ORIGIN, 90, 30.))
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'snapping_mismatch'):
|
||||
self.discover(probe=value)
|
||||
|
||||
def test_missing_request_id_and_no_route_are_explicit(self):
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'missing_request_id'):
|
||||
self.discover(probe={**body(), 'uuid': ''})
|
||||
with patch.object(next_turn.requests, 'get', return_value=response({'code': 'NoRoute'})) as request:
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'no_route'):
|
||||
next_turn.MapboxNextTurnClient(TOKEN).discover(ORIGIN, 0., 'right')
|
||||
self.assertEqual(request.call_count, 2)
|
||||
|
||||
def test_failed_probe_retries_once_with_same_coordinates_heading_and_strict_exit(self):
|
||||
with patch.object(next_turn.requests, 'get', side_effect=[response({'code': 'NoSegment'}), response(body()), response(body(True))]) as request:
|
||||
target = next_turn.MapboxNextTurnClient(TOKEN).discover(ORIGIN, 0., 'right')
|
||||
calls = request.call_args_list
|
||||
self.assertEqual(len(calls), 3)
|
||||
self.assertEqual(calls[0].args[0], calls[1].args[0])
|
||||
self.assertEqual([call.kwargs['params']['radiuses'] for call in calls], ['20;100', '20;300', '20;15'])
|
||||
self.assertEqual([call.kwargs['params']['bearings'] for call in calls], ['0,30;']*3)
|
||||
self.assertEqual(target.provenance['requestCount'], 3)
|
||||
self.assertTrue(target.provenance['probeFallbackUsed'])
|
||||
self.assertEqual(target.provenance['probeFallbackOriginalReason'], 'no_route')
|
||||
self.assertTrue(target.provenance['coherentNavigationRoute'])
|
||||
self.assertEqual(target.steps[-1]['maneuver']['type'], 'arrive')
|
||||
|
||||
def test_fallback_does_not_relax_source_snap(self):
|
||||
invalid = body()
|
||||
invalid['waypoints'][0]['location'] = list(next_turn.offset(ORIGIN, 90., 30.))
|
||||
with patch.object(next_turn.requests, 'get', side_effect=[response({'code': 'NoSegment'}), response(invalid)]) as request:
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'snapping_mismatch'):
|
||||
next_turn.MapboxNextTurnClient(TOKEN).discover(ORIGIN, 0., 'right')
|
||||
self.assertEqual(request.call_count, 2)
|
||||
|
||||
def test_exit_failure_never_uses_probe_fallback(self):
|
||||
with patch.object(next_turn.requests, 'get', side_effect=[response(body()), response({'code': 'NoSegment'})]) as request:
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'no_route'):
|
||||
next_turn.MapboxNextTurnClient(TOKEN).discover(ORIGIN, 0., 'right')
|
||||
self.assertEqual(request.call_count, 2)
|
||||
self.assertEqual(request.call_args_list[-1].kwargs['params']['radiuses'], '20;15')
|
||||
|
||||
def test_heading_mismatch_never_uses_probe_fallback(self):
|
||||
value = body()
|
||||
value['routes'][0]['geometry']['coordinates'][1] = list(next_turn.offset(ORIGIN, 30., 100.))
|
||||
with patch.object(next_turn.requests, 'get', return_value=response(value)) as request:
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'incoming_heading_mismatch'):
|
||||
next_turn.MapboxNextTurnClient(TOKEN).discover(ORIGIN, 0., 'right')
|
||||
self.assertEqual(request.call_count, 1)
|
||||
|
||||
def test_closed_route_and_exclusion_violation_are_rejected(self):
|
||||
closed = body(True)
|
||||
closed['routes'][0]['legs'][0]['annotation'] = {'closure': [{'geometry_index_start': 1}]}
|
||||
violation = body(True)
|
||||
violation['routes'][0]['notifications'] = [{'type': 'violation'}]
|
||||
for value, reason in ((closed, 'closed_route'), (violation, 'route_violation')):
|
||||
with self.subTest(reason=reason), self.assertRaisesRegex(next_turn.NextTurnError, reason):
|
||||
self.discover(validated=value)
|
||||
|
||||
def test_network_exceptions_do_not_expose_token_or_urls(self):
|
||||
with patch.object(next_turn.requests, 'get', side_effect=next_turn.requests.Timeout('https://example/?access_token='+TOKEN)):
|
||||
try:
|
||||
next_turn.MapboxNextTurnClient(TOKEN).discover(ORIGIN, 0., 'right')
|
||||
except next_turn.NextTurnError as error:
|
||||
self.assertEqual(error.reason, 'request_failed')
|
||||
self.assertNotIn(TOKEN, traceback.format_exc())
|
||||
self.assertNotIn('https://example', traceback.format_exc())
|
||||
else:
|
||||
self.fail('Expected fixed redacted failure')
|
||||
|
||||
def test_invalid_direction_and_invalid_token_never_query(self):
|
||||
with patch.object(next_turn.requests, 'get') as request:
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'invalid_direction'):
|
||||
next_turn.MapboxNextTurnClient(TOKEN).discover(ORIGIN, 0., 'straight')
|
||||
with self.assertRaisesRegex(next_turn.NextTurnError, 'invalid_token'):
|
||||
next_turn.MapboxNextTurnClient('not a token')
|
||||
request.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,370 +0,0 @@
|
||||
"""Execute navd integration with mocked messaging/HTTP and controllable futures.
|
||||
|
||||
Openpilot imports are supplied as test doubles so these tests also run on a Mac
|
||||
without the vehicle messaging/Params native libraries. Production function and
|
||||
class bodies are executed unchanged; no Mapbox/network request is made.
|
||||
"""
|
||||
import ast
|
||||
from concurrent.futures import Future
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import numpy as np
|
||||
|
||||
NAVIGATION = Path(__file__).resolve().parents[1]
|
||||
WORKTREE = NAVIGATION.parents[2]
|
||||
|
||||
|
||||
def source(name):
|
||||
path = NAVIGATION/name
|
||||
if path.exists():
|
||||
return path.read_text()
|
||||
return subprocess.check_output(['git', 'show', f'HEAD:openpilot/selfdrive/navigation/{name}'], cwd=WORKTREE, text=True)
|
||||
|
||||
|
||||
class Params:
|
||||
def __init__(self, enabled=True):
|
||||
self.values = {'MapboxNextTurnEnabled': enabled, 'MapboxTurnIntent':
|
||||
{'session': 'one', 'direction': 'right', 'mode': 'turn', 'modelSupportsNavigation': True, 'lateralActive': True}}
|
||||
self.values['NavModelState'] = {'updatedAt': 100., 'supported': True}
|
||||
self.writes = []
|
||||
|
||||
def get(self, key, **kwargs):
|
||||
return self.values.get(key)
|
||||
|
||||
def get_bool(self, key):
|
||||
return self.values.get(key) is True
|
||||
|
||||
def put(self, key, value, block=False):
|
||||
self.values[key] = value
|
||||
self.writes.append((key, value, block))
|
||||
|
||||
def remove(self, key):
|
||||
self.values.pop(key, None)
|
||||
|
||||
|
||||
class NextTurnError(Exception):
|
||||
def __init__(self, reason):
|
||||
self.reason = reason
|
||||
|
||||
|
||||
class Executor:
|
||||
def __init__(self):
|
||||
self.jobs = []
|
||||
|
||||
def submit(self, function, **kwargs):
|
||||
future = Future()
|
||||
future.set_running_or_notify_cancel() # Simulate an HTTP request already in flight.
|
||||
self.jobs.append((future, function, kwargs))
|
||||
return future
|
||||
|
||||
def shutdown(self, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def message(service, valid=True):
|
||||
return SimpleNamespace(valid=valid, logMonoTime=100_000_000_001, **{service: SimpleNamespace(coordinates=[])})
|
||||
|
||||
|
||||
def load_navd():
|
||||
route = {'__name__': 'route_test_fixture'}
|
||||
exec(compile(source('route.py'), 'route.py', 'exec'), route)
|
||||
namespace = {'__name__': 'navd_test_fixture', 'messaging': SimpleNamespace(new_message=message),
|
||||
'API_HOST': 'https://api.example.test', 'Api': Mock(), 'Params': Params, 'Ratekeeper': Mock(),
|
||||
'cloudlog': Mock(), 'MapboxNextTurnClient': Mock(), 'NextTurnError': NextTurnError,
|
||||
'latest_gps_source': lambda sm: 'gpsLocation' if sm.valid['gpsLocation'] else None,
|
||||
'along_path': route['along_path'], 'local_meters': route['local_meters'],
|
||||
'RouteTracker': route['RouteTracker'], 'cumulative_distance': route['cumulative_distance'],
|
||||
'nearest_segment': route['nearest_segment']}
|
||||
tree = ast.parse(source('navd.py'))
|
||||
tree.body = [node for node in tree.body
|
||||
if not (isinstance(node, ast.ImportFrom) and node.module.startswith('openpilot.'))
|
||||
and not (isinstance(node, ast.Import) and any(item.name.startswith('openpilot.') for item in node.names))]
|
||||
exec(compile(tree, str(NAVIGATION/'navd.py'), 'exec'), namespace)
|
||||
return SimpleNamespace(**namespace)
|
||||
|
||||
|
||||
class SM:
|
||||
def __init__(self):
|
||||
self.frame = 1
|
||||
self.valid = dict.fromkeys(('gpsLocation', 'carState', 'carControl'), True)
|
||||
self.recv_time = dict.fromkeys(self.valid, 100.)
|
||||
self.logMonoTime = dict.fromkeys(self.valid, 100_000_000_000)
|
||||
self.gps = SimpleNamespace(hasFix=True, longitude=0., latitude=0., bearingDeg=0., bearingAccuracyDeg=3.,
|
||||
horizontalAccuracy=1., unixTimestampMillis=1_000_000_000)
|
||||
self.car = SimpleNamespace(vEgo=2., gearShifter='drive', leftBlinker=False, rightBlinker=True)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return SimpleNamespace(latActive=True) if key == 'carControl' else self.car if key == 'carState' else self.gps
|
||||
|
||||
def update(self, timeout):
|
||||
pass
|
||||
|
||||
|
||||
def target(direction='right', request='validation-one', discovered=100.):
|
||||
result = SimpleNamespace(direction=direction, discovered_at=discovered, approach_geometry=((0., 0.), (0., .001)),
|
||||
geometry=((0., 0.), (0., .001), (.0004, .001)), junction=(0., .001),
|
||||
incoming_bearing_deg=0., exit_bearing_deg=90. if direction == 'right' else 270.,
|
||||
validation_uuid=request, probe_uuid='probe-one', name='Example Street', junction_geometry_index=1)
|
||||
|
||||
points = list(result.geometry)
|
||||
result.steps = [
|
||||
{'geometry': {'coordinates': points[:2]}, 'distance': 111., 'duration': 20.,
|
||||
'maneuver': {'type': 'depart'}, 'bannerInstructions': [{'distanceAlongGeometry': 111.,
|
||||
'primary': {'text': 'Turn right', 'type': 'turn', 'modifier': direction}}]},
|
||||
{'geometry': {'coordinates': points[1:]}, 'distance': 44., 'duration': 8.,
|
||||
'maneuver': {'type': 'turn', 'modifier': direction}, 'bannerInstructions': []},
|
||||
{'geometry': {'coordinates': [points[-1]]}, 'distance': 0., 'duration': 0.,
|
||||
'maneuver': {'type': 'arrive'}, 'bannerInstructions': []}]
|
||||
result.to_dict = lambda: {'steps': result.steps}
|
||||
return result
|
||||
|
||||
|
||||
class TestSignalTurnEngine(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.navd = load_navd()
|
||||
self.params, self.sm, self.executor = Params(), SM(), Executor()
|
||||
self.now = 100.
|
||||
self.wall = 1_000_000.
|
||||
self.client_factory = Mock(return_value=SimpleNamespace(discover=Mock()))
|
||||
self.engine = self.navd.SignalTurnEngine(self.params, self.client_factory, self.executor,
|
||||
clock=lambda: self.now, wall_clock=lambda: self.wall)
|
||||
self.token = patch.dict(self.engine.update.__globals__, get_mapbox_token=Mock(return_value='pk.example.signature'))
|
||||
self.token.start()
|
||||
self.addCleanup(self.token.stop)
|
||||
|
||||
def update(self, destination=False):
|
||||
return self.engine.update(self.sm, 'gpsLocation', destination_active=destination)
|
||||
|
||||
def ready(self):
|
||||
self.assertEqual(self.update()['status'], 'searching')
|
||||
self.executor.jobs[-1][0].set_result(target())
|
||||
self.assertEqual(self.update()['status'], 'route_pending')
|
||||
|
||||
def test_disabled_makes_no_token_client_worker_or_account_request(self):
|
||||
self.params.values['MapboxNextTurnEnabled'] = False
|
||||
with patch.dict(self.engine.update.__globals__, get_mapbox_token=Mock(side_effect=AssertionError('unexpected credential read'))):
|
||||
self.assertIsNone(self.update())
|
||||
self.client_factory.assert_not_called()
|
||||
self.assertEqual(self.executor.jobs, [])
|
||||
self.assertNotIn('MapboxTurnContext', self.params.values)
|
||||
|
||||
def test_missing_token_is_unavailable_and_read_once_per_session(self):
|
||||
getter = Mock(return_value=None)
|
||||
with patch.dict(self.engine.update.__globals__, get_mapbox_token=getter):
|
||||
for _ in range(3):
|
||||
self.assertEqual(self.update()['reason'], 'missing_token')
|
||||
getter.assert_called_once_with()
|
||||
self.assertEqual(self.executor.jobs, [])
|
||||
|
||||
def test_raw_fix_distance_not_extrapolated_and_context_has_true_ages(self):
|
||||
self.ready()
|
||||
distance = self.engine.context['distanceM']
|
||||
self.now += .2
|
||||
self.wall += .2
|
||||
context = self.update()
|
||||
self.assertAlmostEqual(context['distanceM'], distance)
|
||||
self.assertAlmostEqual(context['gpsAgeSeconds'], .2)
|
||||
self.assertEqual(context['updatedAt'], self.now)
|
||||
self.assertTrue(context['headingValid'] and context['locationValid'])
|
||||
self.assertEqual(context['requestId'], 'validation-one')
|
||||
|
||||
def test_opposite_signal_waits_for_single_worker_and_discards_old_result(self):
|
||||
self.update()
|
||||
self.params.values['MapboxTurnIntent'] = {'session': 'two', 'direction': 'left', 'mode': 'turn', 'modelSupportsNavigation': True, 'lateralActive': True}
|
||||
self.sm.car.leftBlinker, self.sm.car.rightBlinker = True, False
|
||||
self.assertEqual(self.update()['status'], 'searching')
|
||||
self.assertEqual(len(self.executor.jobs), 1)
|
||||
self.executor.jobs[0][0].set_result(target())
|
||||
self.assertEqual(self.update()['session'], 'two')
|
||||
self.assertIsNone(self.engine.target)
|
||||
self.assertEqual(len(self.executor.jobs), 2)
|
||||
self.executor.jobs[1][0].set_result(target('left', 'validation-two'))
|
||||
context = self.update()
|
||||
self.assertEqual((context['session'], context['direction'], context['status']), ('two', 'left', 'route_pending'))
|
||||
|
||||
def test_cancel_or_disable_retires_inflight_result(self):
|
||||
for cancel in ('intent', 'disable'):
|
||||
with self.subTest(cancel=cancel):
|
||||
self.setUp()
|
||||
self.update()
|
||||
if cancel == 'intent':
|
||||
self.params.values.pop('MapboxTurnIntent')
|
||||
else:
|
||||
self.params.values['MapboxNextTurnEnabled'] = False
|
||||
self.assertIsNone(self.update())
|
||||
self.executor.jobs[0][0].set_result(target())
|
||||
self.assertIsNone(self.update())
|
||||
self.assertIsNone(self.engine.target)
|
||||
self.assertNotIn('MapboxTurnContext', self.params.values)
|
||||
|
||||
def test_explicit_destination_blocks_requests_and_retires_prior_work(self):
|
||||
self.assertEqual(self.update(destination=True)['status'], 'blocked')
|
||||
self.assertEqual(self.executor.jobs, [])
|
||||
self.client_factory.assert_not_called()
|
||||
self.assertEqual(self.update(destination=False)['reason'], 'manual_destination_preempted')
|
||||
self.assertEqual(self.executor.jobs, [])
|
||||
self.assertIsNone(self.engine.target)
|
||||
|
||||
def test_stale_fix_poor_accuracy_future_publication_and_reverse_block_discovery(self):
|
||||
changes = (lambda: setattr(self.sm.gps, 'unixTimestampMillis', self.sm.gps.unixTimestampMillis-1000),
|
||||
lambda: setattr(self.sm.gps, 'horizontalAccuracy', 4.),
|
||||
lambda: self.sm.logMonoTime.update(gpsLocation=101_000_000_000),
|
||||
lambda: setattr(self.sm.car, 'gearShifter', 'reverse'))
|
||||
for change in changes:
|
||||
with self.subTest(change=change):
|
||||
self.sm = SM()
|
||||
change()
|
||||
self.assertEqual(self.update()['status'], 'unavailable')
|
||||
self.assertEqual(self.executor.jobs, [])
|
||||
|
||||
def test_physical_signal_cancellation_blocks_stale_intent(self):
|
||||
self.ready()
|
||||
self.sm.car.rightBlinker = False
|
||||
context = self.update()
|
||||
self.assertIsNone(context)
|
||||
self.assertIsNone(self.engine.target)
|
||||
self.assertEqual(len(self.executor.jobs), 1)
|
||||
|
||||
def test_wrong_heading_or_crossroad_position_has_no_ready_context(self):
|
||||
self.ready()
|
||||
self.sm.gps.bearingDeg = 90.
|
||||
self.assertEqual(self.update()['status'], 'unavailable')
|
||||
self.sm.gps.bearingDeg = 0.
|
||||
self.sm.gps.longitude = .0002
|
||||
context = self.update()
|
||||
self.assertEqual(context['status'], 'unavailable')
|
||||
self.assertGreater(context['crossTrackM'], 20.)
|
||||
|
||||
def test_expired_target_and_passed_junction_do_not_retarget_held_signal(self):
|
||||
self.ready()
|
||||
self.engine.target = target(discovered=-201.)
|
||||
self.assertEqual(self.update()['reason'], 'target_expired')
|
||||
self.engine.target = target()
|
||||
self.sm.gps.latitude = .0012
|
||||
self.assertEqual(self.update()['status'], 'passed')
|
||||
self.sm.gps.latitude = 0.
|
||||
self.assertEqual(self.update()['status'], 'passed')
|
||||
self.assertEqual(len(self.executor.jobs), 1)
|
||||
|
||||
def test_discovery_exception_does_not_publish_or_log_url_or_token(self):
|
||||
self.update()
|
||||
secret = 'https://example.test/?access_token=pk.secret.signature'
|
||||
self.executor.jobs[0][0].set_exception(RuntimeError(secret))
|
||||
context = self.update()
|
||||
self.assertEqual(context['reason'], 'discovery_failed')
|
||||
self.assertNotIn(secret, str(self.params.writes))
|
||||
self.assertEqual(self.navd.cloudlog.mock_calls, [])
|
||||
|
||||
|
||||
class TestApproachProjection(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.navd = load_navd()
|
||||
|
||||
def test_signed_distance_uses_incoming_geometry_not_outgoing_leg(self):
|
||||
before = self.navd.approach_context(target(), (0., .0009), 0., 1.)
|
||||
after = self.navd.approach_context(target(), (0., .0011), 0., 1.)
|
||||
self.assertAlmostEqual(before['distanceM'], 11.1195, delta=.01)
|
||||
self.assertAlmostEqual(after['distanceM'], -11.1195, delta=.01)
|
||||
on_outgoing = self.navd.approach_context(target(), (.0002, .001), 90., 1.)
|
||||
self.assertFalse(on_outgoing['locationValid'])
|
||||
self.assertGreater(on_outgoing['crossTrackM'], 20.)
|
||||
|
||||
def test_invalid_geometry_and_zero_accuracy_are_not_ready_and_inputs_unchanged(self):
|
||||
pose = np.array([0., 0.])
|
||||
self.assertFalse(self.navd.approach_context(target(), pose, 0., 0.)['locationValid'])
|
||||
np.testing.assert_array_equal(pose, [0., 0.])
|
||||
malformed = target()
|
||||
malformed.approach_geometry = ((float('nan'), 0.), (0., .001))
|
||||
self.assertFalse(self.navd.approach_context(malformed, pose, 0., 1.)['locationValid'])
|
||||
|
||||
|
||||
class TestChosenHeading(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.navd = load_navd()
|
||||
self.llk = {'valid': True, 'status': 'valid', 'orientationValid': True,
|
||||
'orientationNed': [0., 0., np.pi/2], 'orientationStd': [.01, .01, .02],
|
||||
'receivedAt': 99.9, 'publishedAt': 99.8}
|
||||
self.gps = {'valid': True, 'bearingDeg': 0., 'bearingAccuracyDeg': 4.,
|
||||
'fixAgeSeconds': .1, 'receivedAt': 99.9, 'publishedAt': 99.9}
|
||||
|
||||
def test_fresh_calibrated_localizer_heading_precedes_gps_course(self):
|
||||
result = self.navd.chosen_heading(100., self.llk, self.gps)
|
||||
self.assertEqual((result['headingSource'], result['headingDeg']), ('liveLocationKalman', 90.))
|
||||
self.assertAlmostEqual(result['headingAgeSeconds'], .2)
|
||||
|
||||
def test_invalid_or_stale_localizer_falls_back_only_to_fresh_accurate_gps(self):
|
||||
for update in ({'status': 'uncalibrated'}, {'publishedAt': 99.}, {'publishedAt': 100.1},
|
||||
{'orientationStd': [.01, .01, np.radians(16.)]}):
|
||||
result = self.navd.chosen_heading(100., {**self.llk, **update}, self.gps)
|
||||
self.assertEqual(result['headingSource'], 'gps')
|
||||
self.gps['bearingAccuracyDeg'] = 16.
|
||||
self.assertFalse(self.navd.chosen_heading(100., None, self.gps)['headingValid'])
|
||||
self.gps['bearingAccuracyDeg'] = 0.
|
||||
self.assertFalse(self.navd.chosen_heading(100., None, self.gps)['headingValid'])
|
||||
|
||||
def test_future_nan_or_missing_samples_are_not_valid_headings(self):
|
||||
self.gps['publishedAt'] = 100.1
|
||||
for orientation in (None, [float('nan'), 0., 0.], [0., 0.], [0., 0., 0.]):
|
||||
invalid = {**self.llk, 'orientationNed': orientation, 'receivedAt': 100.1}
|
||||
self.assertFalse(self.navd.chosen_heading(100., invalid, self.gps)['headingValid'])
|
||||
|
||||
def test_producer_nan_calibrated_std_uses_explicit_base_covariance_without_claiming_calibration(self):
|
||||
self.llk.update(orientationStd=[float('nan')]*3, baseOrientationValid=True, baseOrientationStd=[.01, .01, .02])
|
||||
result = self.navd.chosen_heading(100., self.llk, self.gps)
|
||||
self.assertEqual((result['headingSource'], result['headingDeg']), ('liveLocationKalman', 90.))
|
||||
self.assertEqual(result['headingUncertaintyKind'], 'base_orientation')
|
||||
self.assertFalse(result['calibrationUncertaintyKnown'])
|
||||
self.llk['baseOrientationValid'] = False
|
||||
self.assertEqual(self.navd.chosen_heading(100., self.llk, self.gps)['headingSource'], 'gps')
|
||||
|
||||
def test_finite_high_calibrated_uncertainty_cannot_select_smaller_base_covariance(self):
|
||||
self.llk.update(orientationStd=[.01, .01, np.radians(16.)], baseOrientationValid=True, baseOrientationStd=[.01]*3)
|
||||
self.assertEqual(self.navd.chosen_heading(100., self.llk, self.gps)['headingSource'], 'gps')
|
||||
|
||||
|
||||
class TestNavigationIntegration(unittest.TestCase):
|
||||
def test_10hz_updates_keep_destination_recompute_and_instruction_at1hz(self):
|
||||
navd, sm, params, pm = load_navd(), SM(), Params(enabled=False), Mock()
|
||||
params.values['NavDestination'] = {'longitude': 0., 'latitude': .002}
|
||||
with patch.dict(navd.RouteEngine.__init__.__globals__, get_mapbox_token=Mock(return_value=None)):
|
||||
engine = navd.RouteEngine(sm, pm, params, update_frequency=10)
|
||||
engine.recompute_route, engine.send_instruction, engine.send_route = Mock(), Mock(), Mock()
|
||||
for frame in range(1, 201):
|
||||
sm.frame = frame
|
||||
engine.update()
|
||||
self.assertEqual(engine.recompute_route.call_count, 20)
|
||||
self.assertEqual(engine.send_instruction.call_count, 20)
|
||||
self.assertEqual(engine.send_route.call_count, 2)
|
||||
engine.api.get_token.assert_not_called()
|
||||
|
||||
def test_signal_preview_is_labeled_and_clears_immediately_without_destination_write(self):
|
||||
navd, sm, params, pm, executor = load_navd(), SM(), Params(), Mock(), Executor()
|
||||
with patch.dict(navd.RouteEngine.__init__.__globals__, get_mapbox_token=Mock(return_value='pk.example.signature')):
|
||||
engine = navd.RouteEngine(sm, pm, params, update_frequency=10)
|
||||
engine.signal_turn = navd.SignalTurnEngine(params, Mock(return_value=SimpleNamespace(discover=Mock())), executor,
|
||||
clock=lambda: 100., wall_clock=lambda: 1_000_000.)
|
||||
engine.update()
|
||||
executor.jobs[0][0].set_result(target())
|
||||
sm.frame += 1
|
||||
engine.update()
|
||||
instructions = [call.args[1] for call in pm.send.call_args_list if call.args[0] == 'navInstruction']
|
||||
self.assertEqual(instructions[-1].navInstruction.maneuverPrimaryText, 'Turn right')
|
||||
self.assertTrue(instructions[-1].navInstruction.allManeuvers)
|
||||
self.assertEqual(engine.route_owner, 'signal')
|
||||
self.assertTrue(instructions[-1].valid)
|
||||
self.assertNotIn('NavDestination', params.values)
|
||||
params.values.pop('MapboxTurnIntent')
|
||||
sm.frame += 1
|
||||
engine.update()
|
||||
instructions = [call.args[1] for call in pm.send.call_args_list if call.args[0] == 'navInstruction']
|
||||
routes = [call.args[1] for call in pm.send.call_args_list if call.args[0] == 'navRoute']
|
||||
self.assertFalse(instructions[-1].valid)
|
||||
self.assertEqual(routes[-1].navRoute.coordinates, [])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,6 +1,4 @@
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import tempfile
|
||||
@@ -19,7 +17,7 @@ import requests
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.selfdrive.navigation.mapsd import MapsD
|
||||
from openpilot.selfdrive.navigation.navd import RECOMPUTE_HOLD, REROUTE_COUNTER_MIN, ROUTE_RESEND_PERIOD, RouteEngine, set_banner
|
||||
from openpilot.selfdrive.navigation.navd import REROUTE_COUNTER_MIN, ROUTE_RESEND_PERIOD, RouteEngine, set_banner
|
||||
from openpilot.selfdrive.navigation.pmtiles import PMTiles, decode_directory, tile_id
|
||||
from openpilot.selfdrive.navigation.renderer import MAJOR, MEDIUM, MINOR, SIZE, STREET, TRACK, MapRenderer, draw_segments, mercator, project, stroke_widths
|
||||
from openpilot.selfdrive.navigation.route import RouteTracker
|
||||
@@ -102,16 +100,6 @@ def route_engine(sm, pm, values):
|
||||
|
||||
class RangeHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path == '/builds.json':
|
||||
self.server.index_requests += 1
|
||||
entries = {} if self.server.invalid_index else [
|
||||
{'key': '20261103.pmtiles', 'version': '4.15.2', 'size': len(self.server.archive)}]
|
||||
data = json.dumps(entries).encode()
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Length', str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
return
|
||||
self.server.requests.append(dict(self.headers))
|
||||
if self.headers.get('If-Match', self.server.etag) != self.server.etag:
|
||||
self.send_error(412)
|
||||
@@ -134,7 +122,7 @@ class RangeHandler(BaseHTTPRequestHandler):
|
||||
|
||||
|
||||
class TileServerCase(unittest.TestCase):
|
||||
"""Serves a real public-index/leaf-directory archive protocol over local HTTP."""
|
||||
"""Serves a leaf-directory archive over HTTP range requests, with the comma API and tile cache stubbed."""
|
||||
|
||||
def setUp(self):
|
||||
self.directory = tempfile.TemporaryDirectory()
|
||||
@@ -148,10 +136,11 @@ class TileServerCase(unittest.TestCase):
|
||||
threading.Thread(target=self.server.serve_forever, kwargs={'poll_interval': 0.01}, daemon=True).start()
|
||||
self.addCleanup(self.server.server_close)
|
||||
self.addCleanup(self.server.shutdown)
|
||||
self.server.index_requests = 0
|
||||
self.server.invalid_index = False
|
||||
self.url = f'http://127.0.0.1:{self.server.server_port}'
|
||||
for name, value in [('PC', True), ('BUILD_INDEX_URL', self.url + '/builds.json'), ('BUILD_BASE_URL', self.url + '/')]:
|
||||
self.api = MagicMock(dongle_id='test-dongle')
|
||||
self.api.get_token.return_value = 'device-token'
|
||||
self.url = f'http://127.0.0.1:{self.server.server_port}/planet.pmtiles?sig=secret'
|
||||
self.api.get.return_value.json.return_value = {'url': self.url, 'expires': time.time() + 86400} # noqa: TID251
|
||||
for name, value in [('Api', MagicMock(return_value=self.api)), ('Params', MagicMock()), ('PC', True)]:
|
||||
self.enterContext(patch(f'openpilot.selfdrive.navigation.tiles.{name}', value))
|
||||
self.enterContext(patch('openpilot.selfdrive.navigation.tiles.Paths.comma_home', return_value=self.directory.name))
|
||||
|
||||
@@ -178,15 +167,6 @@ class TestEndToEnd(TileServerCase):
|
||||
sm.update(0)
|
||||
return mapsd.update(sm)
|
||||
|
||||
def restored_route_frame():
|
||||
# Destination cancellation retains the existing recompute cooldown.
|
||||
# Advance its bounded update count before testing tile-cache reuse.
|
||||
for _ in range((RECOMPUTE_HOLD + 1) * navd.update_frequency):
|
||||
result = frame()
|
||||
if navd.route is not None:
|
||||
return result
|
||||
self.fail('Destination route was not restored within the existing recompute cooldown')
|
||||
|
||||
image, timestamp = frame()
|
||||
# The route is centered over the white road, with the ego arrow three quarters down.
|
||||
self.assertEqual((image[128, 128], image[128, 131], image[188, 128], image[195, 128]), (128, 255, 255, 0))
|
||||
@@ -199,39 +179,39 @@ class TestEndToEnd(TileServerCase):
|
||||
values.pop('NavDestination')
|
||||
self.assertFalse(frame()[0].any()) # black without a route
|
||||
values['NavDestination'] = dict(destination)
|
||||
self.assertEqual(restored_route_frame()[0][128, 128], 128)
|
||||
self.assertEqual(frame()[0][128, 128], 128)
|
||||
self.assertEqual(len(self.server.requests), fetched) # the second route is served from the cache
|
||||
image, timestamp = frame(fix=False)
|
||||
self.assertFalse(image.any() or timestamp) # black, not stale, without a fix
|
||||
self.assertEqual(self.server.index_requests, 1)
|
||||
self.api.get.assert_called_once()
|
||||
self.assertTrue(all('Range' in headers for headers in self.server.requests))
|
||||
self.assertTrue(all(headers['If-Match'] == self.server.etag for headers in self.server.requests[1:]))
|
||||
self.assertTrue(all(b'secret' not in p.read_bytes() for p in self.cache.rglob('*') if p.is_file()))
|
||||
self.assertTrue(all(b'secret' not in p.read_bytes() for p in self.cache.iterdir()))
|
||||
# The archive is replaced on the server and the cache evicted: the next fetch fails, the map goes black,
|
||||
# and after a delay a fresh reader starts over on the new version.
|
||||
self.server.etag = '"new-version"'
|
||||
for cached in self.cache.rglob('*'):
|
||||
if cached.is_file() and re.fullmatch(r'\d+-\d+', cached.name):
|
||||
cached.unlink()
|
||||
for cached in self.cache.iterdir():
|
||||
cached.unlink()
|
||||
values.pop('NavDestination')
|
||||
frame()
|
||||
values['NavDestination'] = dict(destination)
|
||||
image, timestamp = restored_route_frame()
|
||||
image, timestamp = frame()
|
||||
self.assertFalse(image.any() or timestamp)
|
||||
fetched = len(self.server.requests)
|
||||
self.assertFalse(frame()[1]) # not retried yet
|
||||
self.assertEqual(len(self.server.requests), fetched)
|
||||
mapsd.retry_at = 0
|
||||
self.assertEqual(frame()[0][128, 128], 128)
|
||||
self.assertEqual(json.loads((self.cache / 'archive.json').read_text())['etag'], '"new-version"')
|
||||
self.assertEqual((self.cache / 'version').read_text().split()[0], '"new-version"')
|
||||
|
||||
|
||||
class TestRemoteTiles(TileServerCase):
|
||||
def test_shared_cache_and_refresh(self):
|
||||
with closing(PMTiles(RemoteTiles())) as archive:
|
||||
archive.source.expires = 0 # the signed URL is refreshed before the next fetch
|
||||
self.assertEqual(archive.get(0, 0, 0), road_tile())
|
||||
self.assertEqual(archive.get(0, 0, 0), road_tile())
|
||||
self.assertEqual(self.server.index_requests, 1)
|
||||
self.assertEqual(self.api.get.call_count, 2)
|
||||
count = len(self.server.requests)
|
||||
with closing(PMTiles(RemoteTiles())) as archive:
|
||||
self.assertEqual(archive.get(0, 0, 0), road_tile())
|
||||
@@ -239,9 +219,7 @@ class TestRemoteTiles(TileServerCase):
|
||||
self.assertEqual(len(self.server.requests), count + 1)
|
||||
with patch('openpilot.selfdrive.navigation.tiles.CACHE_LIMIT', 1), closing(RemoteTiles()) as source:
|
||||
source.read(1, 2) # an uncached range is downloaded, then the cache is trimmed to the limit
|
||||
retained = [p.name for p in self.cache.rglob('*') if p.is_file() and re.fullmatch(r'\d+-\d+', p.name)]
|
||||
self.assertEqual(retained, ['0-127']) # current header is retained for offline startup
|
||||
self.assertTrue((self.cache / 'archive.json').exists())
|
||||
self.assertEqual([p.name for p in self.cache.iterdir()], ['version'])
|
||||
|
||||
def test_prefetch_merges_nearby_ranges(self):
|
||||
with patch('openpilot.selfdrive.navigation.tiles.MERGE_GAP', 16), closing(RemoteTiles()) as source:
|
||||
@@ -256,25 +234,20 @@ class TestRemoteTiles(TileServerCase):
|
||||
def test_failures(self):
|
||||
with closing(PMTiles(RemoteTiles())) as archive:
|
||||
self.server.etag = '"new-version"'
|
||||
with self.assertRaises(OSError): # the archive changed under an open reader
|
||||
with self.assertRaisesRegex(OSError, '412'): # the archive changed under an open reader
|
||||
archive.get(0, 0, 0)
|
||||
with closing(PMTiles(RemoteTiles())) as archive: # a new reader starts over on the new version
|
||||
self.assertEqual(archive.get(0, 0, 0), road_tile())
|
||||
self.server.ignore_range = True
|
||||
with self.assertRaises(OSError):
|
||||
with self.assertRaisesRegex(OSError, '200'):
|
||||
RemoteTiles()
|
||||
self.server.ignore_range = False
|
||||
self.server.invalid_index = True
|
||||
with self.assertRaisesRegex(OSError, '^Unable to load public Protomaps basemap$'):
|
||||
self.api.get.return_value.raise_for_status.side_effect = requests.HTTPError(self.url)
|
||||
with self.assertRaisesRegex(OSError, '^Unable to fetch map tiles$'): # the signed URL stays out of the error
|
||||
RemoteTiles()
|
||||
|
||||
|
||||
class TestRouteEngine(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.enterContext(patch.dict(os.environ))
|
||||
os.environ.pop('MAPBOX_TOKEN', None)
|
||||
self.token_path = Path(self.enterContext(tempfile.TemporaryDirectory())) / 'mapbox_token'
|
||||
self.enterContext(patch('openpilot.selfdrive.navigation.navd.MAPBOX_TOKEN_PATH', self.token_path))
|
||||
self.pm = MagicMock()
|
||||
self.values = {'NavDestination': {'latitude': 0, 'longitude': 0.002}, 'LanguageSetting': 'main_en'}
|
||||
self.sm = MagicMock()
|
||||
@@ -286,10 +259,9 @@ class TestRouteEngine(unittest.TestCase):
|
||||
self.engine.last_position, self.engine.last_bearing = (0, 0), 90
|
||||
distance = 111.195 # Mapbox's length of 0.001 degrees at the equator
|
||||
self.response = mapbox_response([
|
||||
{'maneuver': {'type': 'depart'}, 'geometry': {'coordinates': [[0, 0], [0.0005, 0], [0.001, 0]]}, 'distance': distance, 'duration': 10,
|
||||
{'geometry': {'coordinates': [[0, 0], [0.0005, 0], [0.001, 0]]}, 'distance': distance, 'duration': 10,
|
||||
'bannerInstructions': [{'distanceAlongGeometry': 100, 'primary': {'text': 'Turn right', 'type': 'turn', 'modifier': 'right'}}]},
|
||||
{'maneuver': {'type': 'continue', 'modifier': 'straight'}, 'geometry': {'coordinates': [[0.001, 0], [0.002, 0]]},
|
||||
'distance': distance, 'duration': 10, 'bannerInstructions': []},
|
||||
{'geometry': {'coordinates': [[0.001, 0], [0.002, 0]]}, 'distance': distance, 'duration': 10, 'bannerInstructions': []},
|
||||
])
|
||||
|
||||
def last_message(self, service):
|
||||
@@ -319,97 +291,6 @@ class TestRouteEngine(unittest.TestCase):
|
||||
self.engine.send_instruction()
|
||||
self.assertAlmostEqual(self.last_message('navInstruction').navInstruction.maneuverDistance, 0, delta=0.01)
|
||||
|
||||
def test_file_token_routes_directly_to_mapbox(self):
|
||||
token = 'pk.test_payload.test_signature'
|
||||
self.token_path.write_text(token + '\n')
|
||||
self.engine = route_engine(self.sm, self.pm, self.values)
|
||||
self.engine.last_position, self.engine.last_bearing = (0, 0), 90
|
||||
get = self.load_route()
|
||||
self.assertEqual(get.call_args.args[0], 'https://api.mapbox.com/directions/v5/mapbox/driving-traffic/0,0;0.002,0')
|
||||
self.assertEqual(get.call_args.kwargs['params']['access_token'], token)
|
||||
self.assertIsNone(self.engine.api)
|
||||
self.engine.send_instruction()
|
||||
self.assertTrue(self.last_message('navInstruction').valid)
|
||||
|
||||
def test_environment_token_does_not_read_file(self):
|
||||
token = 'pk.environment.signature'
|
||||
with patch.dict(os.environ, {'MAPBOX_TOKEN': token}), patch.object(Path, 'read_text') as read, \
|
||||
patch('requests.get', return_value=self.response):
|
||||
engine = route_engine(self.sm, self.pm, self.values)
|
||||
engine.last_position = (0, 0)
|
||||
engine.recompute_route(gps_ok=True)
|
||||
read.assert_not_called()
|
||||
self.assertEqual(engine.mapbox_token, token)
|
||||
self.assertEqual(engine.mapbox_host, 'https://api.mapbox.com')
|
||||
|
||||
def test_token_provisioning_reloads_without_restart(self):
|
||||
self.load_route()
|
||||
self.engine.recompute_hold = 10
|
||||
token = 'pk.provisioned.signature'
|
||||
self.token_path.write_text(token)
|
||||
with patch('requests.get', return_value=self.response) as get:
|
||||
self.engine.recompute_route(gps_ok=True)
|
||||
self.assertTrue(get.call_args.args[0].startswith('https://api.mapbox.com/directions/'))
|
||||
self.assertEqual(get.call_args.kwargs['params']['access_token'], token)
|
||||
self.assertIsNone(self.engine.api)
|
||||
|
||||
def test_token_rotation_recomputes_route_once(self):
|
||||
self.token_path.write_text('pk.first.signature')
|
||||
with patch('requests.get', return_value=self.response) as get:
|
||||
self.engine.recompute_route(gps_ok=True)
|
||||
self.assertEqual(get.call_args.kwargs['params']['access_token'], 'pk.first.signature')
|
||||
self.token_path.write_text('pk.renewed.signature')
|
||||
self.engine.recompute_route(gps_ok=True)
|
||||
self.assertEqual(get.call_args.kwargs['params']['access_token'], 'pk.renewed.signature')
|
||||
self.engine.recompute_route(gps_ok=True)
|
||||
self.assertEqual(get.call_count, 2)
|
||||
|
||||
def test_missing_or_empty_file_uses_comma(self):
|
||||
self.assertFalse(self.token_path.exists())
|
||||
self.assertIsNone(self.engine.mapbox_token)
|
||||
for contents in ('', ' \n\t'):
|
||||
with self.subTest(contents=contents):
|
||||
self.token_path.write_text(contents)
|
||||
engine = route_engine(self.sm, self.pm, self.values)
|
||||
self.assertIsNone(engine.mapbox_token)
|
||||
self.assertEqual(engine.mapbox_host, 'https://api.commadotai.com/v1/nav')
|
||||
|
||||
def test_invalid_token_file_is_redacted(self):
|
||||
for token in ('sk.secret.signature', 'pk.secret withspace.signature', 'pk.', 'pk.secret.\x00signature'):
|
||||
with self.subTest(token=token), patch('openpilot.selfdrive.navigation.navd.cloudlog') as logger:
|
||||
self.token_path.write_text(token)
|
||||
engine = route_engine(self.sm, self.pm, self.values)
|
||||
self.assertIsNone(engine.mapbox_token)
|
||||
self.assertEqual(logger.mock_calls[0].args, ('navd.mapbox_token_invalid',))
|
||||
self.assertEqual(logger.mock_calls[0].kwargs, {'error': True})
|
||||
self.assertNotIn(token, str(logger.mock_calls))
|
||||
|
||||
def test_unreadable_token_file_is_redacted(self):
|
||||
for error in (PermissionError('pk.secret.signature'), UnicodeError('pk.secret.signature')):
|
||||
with self.subTest(error_type=type(error).__name__), \
|
||||
patch.object(Path, 'read_text', side_effect=error), patch('openpilot.selfdrive.navigation.navd.cloudlog') as logger:
|
||||
engine = route_engine(self.sm, self.pm, self.values)
|
||||
self.assertIsNone(engine.mapbox_token)
|
||||
logger.event.assert_called_once_with('navd.mapbox_token_unavailable', error_type=type(error).__name__, error=True)
|
||||
self.assertNotIn('pk.secret.signature', str(logger.mock_calls))
|
||||
|
||||
def test_http_error_does_not_log_token_or_response(self):
|
||||
token = 'pk.secret.signature'
|
||||
self.engine.mapbox_token = token
|
||||
self.engine.mapbox_host = 'https://api.mapbox.com'
|
||||
self.load_route()
|
||||
response = requests.Response()
|
||||
response.status_code = 401
|
||||
response.url = 'https://api.mapbox.com/directions?access_token=' + token
|
||||
response._content = ('Invalid token: ' + token).encode()
|
||||
with patch('requests.get', return_value=response), patch('openpilot.selfdrive.navigation.navd.cloudlog') as logger:
|
||||
self.engine.calculate_route((0.002, 0))
|
||||
logger.event.assert_called_once_with('navd.route_request_failed', status_code=401, error_type='HTTPError', error=True)
|
||||
logger.exception.assert_not_called()
|
||||
self.assertNotIn(token, str(logger.mock_calls))
|
||||
self.assertNotIn(response.url, str(logger.mock_calls))
|
||||
self.assertIsNone(self.engine.route)
|
||||
|
||||
def test_banner_selection(self):
|
||||
banners = [{'distanceAlongGeometry': 100, 'primary': {'text': 'Turn right', 'type': 'turn', 'modifier': 'right'}},
|
||||
{'distanceAlongGeometry': 50, 'primary': {'text': 'Turn right'}, 'secondary': {'text': 'onto Main St'}}]
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
"""Actual public tile client requests and cache behavior, without accounts/network."""
|
||||
import ast
|
||||
import io
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace as NS
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import requests
|
||||
from requests.adapters import BaseAdapter
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
SOURCE = Path(__file__).resolve().parents[1]
|
||||
REAL_SESSION = requests.Session
|
||||
|
||||
|
||||
class PublicServer(BaseAdapter):
|
||||
def __init__(self):
|
||||
self.archive = b'PMTiles\x03' + bytes(range(256)) * 64
|
||||
self.entries = [{'key': '20261103.pmtiles', 'version': '4.15.2', 'size': len(self.archive)}]
|
||||
self.etag = '"public-build"'
|
||||
self.calls = []
|
||||
self.offline = False
|
||||
self.index_bytes = None
|
||||
self.index_headers = {}
|
||||
self.range_headers = {}
|
||||
self.range_status = 206
|
||||
self.range_body = None
|
||||
|
||||
def send(self, request, **_kwargs):
|
||||
self.calls.append(request)
|
||||
if self.offline:
|
||||
raise requests.ConnectionError('fixture network unavailable')
|
||||
response = requests.Response()
|
||||
response.request, response.url = request, request.url
|
||||
if request.url == 'https://build-metadata.protomaps.dev/builds.json':
|
||||
body = self.index_bytes if self.index_bytes is not None else json.dumps(self.entries).encode()
|
||||
response.status_code = 200
|
||||
headers = {'Content-Length': str(len(body))} | self.index_headers
|
||||
else:
|
||||
if not re.fullmatch(r'https://build\.protomaps\.com/\d{8}\.pmtiles', request.url):
|
||||
raise AssertionError('Client must never contact an account or arbitrary endpoint')
|
||||
match = re.fullmatch(r'bytes=(\d+)-(\d+)', request.headers['Range'])
|
||||
start, end = map(int, match.groups())
|
||||
body = self.archive[start:end + 1] if self.range_body is None else self.range_body
|
||||
response.status_code = self.range_status
|
||||
headers = {'Content-Range': f'bytes {start}-{end}/{len(self.archive)}', 'ETag': self.etag,
|
||||
'Content-Length': str(len(body))} | self.range_headers
|
||||
response.headers = CaseInsensitiveDict(headers)
|
||||
response.raw = io.BytesIO(body)
|
||||
return response
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
class PublicTilesTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.home = Path(self.temp.name)
|
||||
dependencies = {'openpilot.common.hardware': NS(PC=True),
|
||||
'openpilot.common.hardware.hw': NS(Paths=NS(comma_home=lambda: self.temp.name)),
|
||||
'openpilot.selfdrive.navigation.pmtiles': NS(HEADER_LENGTH=127)}
|
||||
spec = importlib.util.spec_from_file_location('actual_public_tiles', SOURCE / 'tiles.py')
|
||||
self.tiles = importlib.util.module_from_spec(spec)
|
||||
with patch.dict(sys.modules, dependencies):
|
||||
spec.loader.exec_module(self.tiles)
|
||||
self.server = PublicServer()
|
||||
def session():
|
||||
value = REAL_SESSION()
|
||||
value.trust_env = False
|
||||
value.mount('https://', self.server)
|
||||
return value
|
||||
self.enterContext(patch.object(self.tiles.requests, 'Session', side_effect=session))
|
||||
|
||||
def client(self):
|
||||
value = self.tiles.RemoteTiles()
|
||||
self.addCleanup(value.close)
|
||||
return value
|
||||
|
||||
def ranges(self):
|
||||
return [call for call in self.server.calls if 'Range' in call.headers]
|
||||
|
||||
def test_latest_compatible_public_build_has_no_account_or_token_dependency(self):
|
||||
self.server.entries += [{'key': '20261104.pmtiles', 'version': '5.0.0', 'size': len(self.server.archive)},
|
||||
{'key': '20261102.pmtiles', 'version': '4.99.1', 'size': len(self.server.archive)}]
|
||||
client = self.client()
|
||||
self.assertEqual(client.url, 'https://build.protomaps.com/20261103.pmtiles')
|
||||
self.assertEqual(client.read(300, 100), self.server.archive[300:400])
|
||||
self.assertEqual(self.ranges()[-1].headers['If-Match'], self.server.etag)
|
||||
for call in self.server.calls:
|
||||
self.assertNotIn('Authorization', call.headers)
|
||||
self.assertNotIn('access_token', call.url)
|
||||
self.assertNotIn('comma', call.url)
|
||||
imported = [ast.unparse(node) for node in ast.parse((SOURCE / 'tiles.py').read_text()).body if isinstance(node, (ast.Import, ast.ImportFrom))]
|
||||
self.assertFalse(any('api' in value.lower() or 'params' in value.lower() for value in imported))
|
||||
|
||||
def test_invalid_index_or_unsupported_schema_fails_before_archive_request(self):
|
||||
invalid = ({'url': 'https://untrusted.invalid/archive'}, [],
|
||||
[{'key': '../20261103.pmtiles', 'version': '4.15.2', 'size': 200}],
|
||||
[{'key': '20260230.pmtiles', 'version': '4.15.2', 'size': 200}],
|
||||
[{'key': '20261103.pmtiles', 'version': '5.0.0', 'size': 200}],
|
||||
[{'key': '20261103.pmtiles', 'version': '4.15.2', 'size': True}])
|
||||
for value in invalid:
|
||||
with self.subTest(value=value):
|
||||
self.server.entries = value
|
||||
with self.assertRaises(OSError):
|
||||
self.client()
|
||||
self.assertEqual(self.ranges(), [])
|
||||
self.server.index_bytes = b'not JSON'
|
||||
with self.assertRaises(OSError):
|
||||
self.client()
|
||||
|
||||
def test_index_body_and_declared_length_are_bounded(self):
|
||||
self.server.index_headers = {'Content-Length': str(self.tiles.INDEX_LIMIT + 1)}
|
||||
with self.assertRaises(OSError):
|
||||
self.client()
|
||||
self.server.index_headers = {}
|
||||
self.server.index_bytes = b' ' * (self.tiles.INDEX_LIMIT + 1)
|
||||
with self.assertRaises(OSError):
|
||||
self.client()
|
||||
self.assertEqual(self.ranges(), [])
|
||||
|
||||
def test_range_status_identity_length_and_content_range_fail_closed(self):
|
||||
cases = [({'status': 200}), ({'headers': {'Content-Range': 'bytes 1-127/999'}}),
|
||||
({'headers': {'ETag': ''}}), ({'headers': {'ETag': 'W/"weak"'}}),
|
||||
({'headers': {'Content-Length': '99999999999999'}}),
|
||||
({'headers': {'Content-Encoding': 'gzip'}}),
|
||||
({'body': b'x' * 126, 'headers': {'Content-Length': '127'}}),
|
||||
({'body': b'x' * 128, 'headers': {'Content-Length': '127'}})]
|
||||
for case in cases:
|
||||
with self.subTest(case=case):
|
||||
self.server.range_status = case.get('status', 206)
|
||||
self.server.range_headers = case.get('headers', {})
|
||||
self.server.range_body = case.get('body')
|
||||
with self.assertRaises(OSError):
|
||||
self.client()
|
||||
|
||||
def test_archive_cannot_change_etag_or_total_size_mid_reader(self):
|
||||
client = self.client()
|
||||
self.server.etag = '"changed"'
|
||||
with self.assertRaisesRegex(OSError, 'changed'):
|
||||
client.read(200, 100)
|
||||
self.assertFalse((client.cache / '200-100').exists())
|
||||
self.server.etag = client.etag
|
||||
self.server.range_headers = {'Content-Range': f'bytes 200-299/{client.size + 1}'}
|
||||
with self.assertRaises(OSError):
|
||||
client.read(200, 100)
|
||||
|
||||
def test_restart_revalidates_header_and_reuses_only_same_archive_ranges(self):
|
||||
first = self.client()
|
||||
first.read(200, 100)
|
||||
count = len(self.server.calls)
|
||||
second = self.client()
|
||||
self.assertEqual(second.cache, first.cache)
|
||||
self.assertEqual(second.read(200, 100), self.server.archive[200:300])
|
||||
self.assertEqual(len(self.server.calls), count + 2) # build index and pinned header only
|
||||
self.server.entries[0]['key'] = '20261104.pmtiles'
|
||||
third = self.client()
|
||||
self.assertNotEqual(third.cache, first.cache)
|
||||
count = len(self.server.calls)
|
||||
third.read(200, 100)
|
||||
self.assertEqual(len(self.server.calls), count + 1)
|
||||
|
||||
def test_offline_restart_reuses_pinned_header_and_cached_ranges_only(self):
|
||||
first = self.client()
|
||||
first.read(200, 100)
|
||||
self.server.offline = True
|
||||
second = self.client()
|
||||
self.assertEqual(second.url, first.url)
|
||||
self.assertEqual(second.etag, first.etag)
|
||||
self.assertEqual(second.read(200, 100), self.server.archive[200:300])
|
||||
with self.assertRaises(OSError):
|
||||
second.read(500, 100)
|
||||
|
||||
def test_malformed_fresh_index_does_not_fall_back_or_relabel_old_bytes(self):
|
||||
first = self.client()
|
||||
first.read(200, 100)
|
||||
self.server.entries = []
|
||||
with self.assertRaises(OSError):
|
||||
self.client()
|
||||
self.assertEqual(first.read(200, 100), self.server.archive[200:300])
|
||||
|
||||
def test_prefetch_merges_nearby_ranges_but_caps_every_request(self):
|
||||
client = self.client()
|
||||
with patch.object(self.tiles, 'MAX_RANGE', 64):
|
||||
count = len(self.ranges())
|
||||
client.prefetch([(200, 32), (244, 32), (400, 16), (400, 16)])
|
||||
added = self.ranges()[count:]
|
||||
self.assertEqual(len(added), 3)
|
||||
for call in added:
|
||||
start, end = map(int, call.headers['Range'][6:].split('-'))
|
||||
self.assertLessEqual(end - start + 1, 64)
|
||||
count = len(self.server.calls)
|
||||
self.assertEqual(client.read(244, 32), self.server.archive[244:276])
|
||||
self.assertEqual(len(self.server.calls), count)
|
||||
count = len(self.ranges())
|
||||
client.prefetch([(600, 10), (620, 10)])
|
||||
self.assertEqual(len(self.ranges()), count + 1)
|
||||
|
||||
def test_cache_limit_counts_ranges_from_all_archive_identities(self):
|
||||
client = self.client()
|
||||
with patch.object(self.tiles, 'CACHE_LIMIT', 160):
|
||||
client.read(200, 100)
|
||||
total = sum(path.stat().st_size for path in client.base.rglob('*') if path.is_file() and re.fullmatch(r'\d+-\d+', path.name))
|
||||
self.assertLessEqual(total, 160)
|
||||
|
||||
def test_cache_trim_retains_current_header_for_offline_restart(self):
|
||||
first = self.client()
|
||||
with patch.object(self.tiles, 'CACHE_LIMIT', 228):
|
||||
first.read(200, 100)
|
||||
first.read(400, 100)
|
||||
self.assertTrue((first.cache / '0-127').exists())
|
||||
self.assertFalse((first.cache / '200-100').exists())
|
||||
self.server.offline = True
|
||||
second = self.client()
|
||||
self.assertEqual(second.read(400, 100), self.server.archive[400:500])
|
||||
|
||||
def test_invalid_or_excessive_ranges_do_not_make_requests(self):
|
||||
client = self.client()
|
||||
count = len(self.server.calls)
|
||||
for offset, length in ((-1, 1), (0, 0), (0, self.tiles.MAX_RANGE + 1), (client.size, 1), (True, 1)):
|
||||
with self.subTest(offset=offset, length=length), self.assertRaises(ValueError):
|
||||
client.read(offset, length)
|
||||
self.assertEqual(len(self.server.calls), count)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,250 +0,0 @@
|
||||
"""Real route ownership/generation lifecycle; no HTTP or vehicle execution."""
|
||||
from pathlib import Path
|
||||
import ast
|
||||
import importlib.util
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import numpy as np
|
||||
|
||||
from test_navd_next_turn import Executor, Params, SM, load_navd, target
|
||||
|
||||
|
||||
class SignalMiniRouteTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.navd, self.sm, self.params, self.pm, self.executor = load_navd(), SM(), Params(), Mock(), Executor()
|
||||
self.now, self.wall = 100., 1_000_000.
|
||||
self.token = patch.dict(self.navd.RouteEngine.__init__.__globals__, get_mapbox_token=Mock(return_value='pk.fixture.signature'))
|
||||
self.token.start()
|
||||
self.addCleanup(self.token.stop)
|
||||
self.engine = self.navd.RouteEngine(self.sm, self.pm, self.params, update_frequency=10)
|
||||
self.engine.signal_turn = self.navd.SignalTurnEngine(self.params, Mock(return_value=SimpleNamespace(discover=Mock())), self.executor,
|
||||
clock=lambda: self.now, wall_clock=lambda: self.wall)
|
||||
|
||||
def fresh(self, longitude=0., latitude=0., heading=0.):
|
||||
self.sm.gps.longitude, self.sm.gps.latitude, self.sm.gps.bearingDeg = longitude, latitude, heading
|
||||
self.sm.gps.unixTimestampMillis = (self.wall-.001)*1000.
|
||||
self.sm.logMonoTime.update({name: int(self.now*1e9)-1000 for name in self.sm.logMonoTime})
|
||||
self.sm.recv_time.update(dict.fromkeys(self.sm.recv_time, self.now))
|
||||
self.params.values['NavModelState'] = {'updatedAt': self.now, 'supported': True}
|
||||
|
||||
def ready(self):
|
||||
self.engine.update()
|
||||
self.executor.jobs[-1][0].set_result(target())
|
||||
self.engine.update()
|
||||
self.assertEqual(self.engine.route_owner, 'signal')
|
||||
self.assertEqual(self.params.values['MapboxTurnContext']['status'], 'ready')
|
||||
|
||||
def enter_and_cancel(self):
|
||||
self.params.values.pop('MapboxTurnIntent', None)
|
||||
self.sm.car.leftBlinker = self.sm.car.rightBlinker = False
|
||||
self.fresh(longitude=.000025, latitude=.001, heading=90.)
|
||||
self.engine.update()
|
||||
|
||||
def test_unsupported_model_never_discovers_or_installs(self):
|
||||
self.params.values['NavModelState']['supported'] = False
|
||||
self.engine.update()
|
||||
self.assertEqual(self.params.values['MapboxTurnContext']['reason'], 'navigation_model_required')
|
||||
self.assertEqual(self.executor.jobs, [])
|
||||
self.assertIsNone(self.engine.route)
|
||||
|
||||
def test_delayed_context_write_cannot_resurrect_retired_route(self):
|
||||
pending, persist = [], self.params.put
|
||||
|
||||
def deferred(key, value, block=False):
|
||||
if block:
|
||||
persist(key, value, block=True)
|
||||
else:
|
||||
pending.append((key, value))
|
||||
|
||||
self.params.put = deferred
|
||||
engine = self.engine.signal_turn
|
||||
engine.key = ('one', 'right')
|
||||
engine._publish(self.now, 'ready', routeOwner='signal', routeGeneration=123, retainedAfterSignalCancel=True)
|
||||
engine._clear()
|
||||
for key, value in pending:
|
||||
self.params.values[key] = value
|
||||
self.assertNotIn('MapboxTurnContext', self.params.values)
|
||||
|
||||
def test_coherent_route_arrival_all_maneuvers_and_publish_order(self):
|
||||
self.ready()
|
||||
self.assertEqual(self.engine.route[-1]['maneuver']['type'], 'arrive')
|
||||
self.assertNotIn('NavDestination', self.params.values)
|
||||
calls = self.pm.send.call_args_list
|
||||
route_index = max(i for i, call in enumerate(calls) if call.args[0] == 'navRoute')
|
||||
instruction_index = max(i for i, call in enumerate(calls) if call.args[0] == 'navInstruction')
|
||||
self.assertLess(route_index, instruction_index)
|
||||
instruction = calls[instruction_index].args[1].navInstruction
|
||||
self.assertTrue(any(item['type'] == 'turn' and item['modifier'] == 'right' for item in instruction.allManeuvers))
|
||||
self.assertEqual(self.params.values['MapboxTurnContext']['routeGeneration'], self.engine.route_generation)
|
||||
self.assertFalse(any(key == 'MapboxTurnContext' and value.get('status') == 'route_pending'
|
||||
for key, value, _block in self.params.writes))
|
||||
|
||||
def test_pending_route_can_prepare_without_authorizing_navigation_inputs(self):
|
||||
self.params.values['MapboxTurnIntent']['mode'] = 'pending'
|
||||
self.ready()
|
||||
path = Path(__file__).resolve().parents[3]/'sunnypilot/modeld_v2/navigation_input.py'
|
||||
spec = importlib.util.spec_from_file_location('mini_route_model_guard', path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
guard = module.SignalRouteGuard()
|
||||
arguments = {'now': self.now, 'enabled': True, 'lateral_active': True, 'left_blinker': False,
|
||||
'right_blinker': True, 'intent': self.params.values['MapboxTurnIntent'],
|
||||
'context': self.params.values['MapboxTurnContext']}
|
||||
self.assertFalse(guard.check(**arguments)['ready'])
|
||||
self.params.values['MapboxTurnIntent']['mode'] = 'turn'
|
||||
self.assertTrue(guard.check(**arguments)['ready'])
|
||||
|
||||
def test_lane_change_keeps_prefetch_generation_for_later_turn(self):
|
||||
self.params.values['MapboxTurnIntent']['mode'] = 'lane_change'
|
||||
self.ready()
|
||||
generation = self.engine.route_generation
|
||||
session = self.params.values['MapboxTurnIntent']['session']
|
||||
self.assertEqual(len(self.executor.jobs), 1)
|
||||
self.params.values['MapboxTurnIntent']['mode'] = 'pending'
|
||||
self.engine.update()
|
||||
self.params.values['MapboxTurnIntent']['mode'] = 'turn'
|
||||
self.engine.update()
|
||||
self.assertEqual(self.engine.route_generation, generation)
|
||||
self.assertEqual(self.params.values['MapboxTurnContext']['session'], session)
|
||||
self.assertEqual(len(self.executor.jobs), 1, 'Completing LC must not force another routing request')
|
||||
|
||||
def test_lane_change_only_route_is_cleared_on_cancel_even_at_junction(self):
|
||||
self.params.values['MapboxTurnIntent']['mode'] = 'lane_change'
|
||||
self.ready()
|
||||
self.enter_and_cancel()
|
||||
self.assertIsNone(self.engine.route)
|
||||
self.assertNotIn('MapboxTurnContext', self.params.values)
|
||||
|
||||
def test_cancel_before_entry_clears_route_and_context(self):
|
||||
self.ready()
|
||||
self.params.values.pop('MapboxTurnIntent')
|
||||
self.sm.car.rightBlinker = False
|
||||
self.engine.update()
|
||||
self.assertIsNone(self.engine.route)
|
||||
self.assertNotIn('MapboxTurnContext', self.params.values)
|
||||
|
||||
def test_same_tick_first_entry_and_auto_cancel_retains_exact_generation(self):
|
||||
self.ready()
|
||||
generation = self.engine.route_generation
|
||||
self.assertFalse(self.engine.signal_turn.entered)
|
||||
self.enter_and_cancel()
|
||||
context = self.params.values['MapboxTurnContext']
|
||||
self.assertEqual(context['status'], 'ready')
|
||||
self.assertTrue(context['retainedAfterSignalCancel'])
|
||||
self.assertEqual(context['routeGeneration'], generation)
|
||||
self.assertEqual(len(self.executor.jobs), 1)
|
||||
|
||||
def test_receiving_road_completion_retires_without_retarget(self):
|
||||
self.ready()
|
||||
self.enter_and_cancel()
|
||||
self.fresh(longitude=.00015, latitude=.001, heading=90.)
|
||||
self.engine.update()
|
||||
self.assertEqual(self.params.values['MapboxTurnContext']['status'], 'passed')
|
||||
self.assertIsNone(self.engine.route)
|
||||
self.engine.update()
|
||||
self.assertEqual(len(self.executor.jobs), 1)
|
||||
|
||||
def test_missed_junction_retires_instead_of_rerouting_to_endpoint(self):
|
||||
self.ready()
|
||||
self.fresh(latitude=.00115, heading=0.)
|
||||
self.engine.update()
|
||||
self.assertEqual(self.params.values['MapboxTurnContext']['status'], 'passed')
|
||||
self.assertIsNone(self.engine.route)
|
||||
self.engine.update()
|
||||
self.assertEqual(len(self.executor.jobs), 1)
|
||||
|
||||
def test_transient_gps_loss_retains_geometry_generation_but_invalidates_inputs(self):
|
||||
self.ready()
|
||||
self.enter_and_cancel()
|
||||
generation = self.engine.route_generation
|
||||
self.now += .6
|
||||
self.wall += .6
|
||||
self.params.values['NavModelState']['updatedAt'] = self.now
|
||||
self.sm.logMonoTime['carControl'] = int(self.now*1e9)-1000
|
||||
self.engine.update()
|
||||
self.assertEqual(self.params.values['MapboxTurnContext']['status'], 'unavailable')
|
||||
self.assertEqual(self.engine.route_generation, generation)
|
||||
self.assertIsNotNone(self.engine.route)
|
||||
self.fresh(longitude=.00003, latitude=.001, heading=90.)
|
||||
self.engine.update()
|
||||
self.assertEqual(self.params.values['MapboxTurnContext']['status'], 'ready')
|
||||
self.assertEqual(self.engine.route_generation, generation)
|
||||
|
||||
def test_manual_destination_switch_cannot_label_old_route_as_new_destination(self):
|
||||
a, b = {'longitude': .002, 'latitude': 0.}, {'longitude': .003, 'latitude': 0.}
|
||||
self.params.values['NavDestination'] = a
|
||||
self.engine.install_route(target().steps, (.002, 0.))
|
||||
self.engine.send_route()
|
||||
self.engine.recompute_route = Mock()
|
||||
self.engine.update()
|
||||
self.assertEqual(self.params.values['MapboxTurnContext']['destination'], a)
|
||||
self.params.values['NavDestination'] = b
|
||||
self.engine.update() # not a1Hz legacy recompute tick
|
||||
context = self.params.values['MapboxTurnContext']
|
||||
self.assertEqual(context['status'], 'searching')
|
||||
self.assertIsNone(self.engine.route)
|
||||
self.assertEqual(self.params.values['NavDestination'], b)
|
||||
|
||||
def test_signal_route_resends_at_one_hz_without_changing_generation(self):
|
||||
self.ready()
|
||||
generation = self.engine.route_generation
|
||||
before = sum(call.args[0] == 'navRoute' for call in self.pm.send.call_args_list)
|
||||
self.sm.frame = 10
|
||||
self.engine.update()
|
||||
routes = [call.args[1] for call in self.pm.send.call_args_list if call.args[0] == 'navRoute']
|
||||
self.assertEqual(len(routes), before+1)
|
||||
self.assertEqual(routes[-1].logMonoTime, generation)
|
||||
|
||||
def test_manual_destination_cancel_removes_its_ready_generation_context(self):
|
||||
self.params.values['NavDestination'] = {'longitude': .002, 'latitude': 0.}
|
||||
self.engine.install_route(target().steps, (.002, 0.))
|
||||
self.engine.send_route()
|
||||
self.engine.recompute_route = Mock()
|
||||
self.engine.update()
|
||||
self.params.values.pop('NavDestination')
|
||||
self.params.values.pop('MapboxTurnIntent')
|
||||
self.sm.car.rightBlinker = False
|
||||
self.engine.update()
|
||||
self.assertIsNone(self.engine.route)
|
||||
self.assertNotIn('MapboxTurnContext', self.params.values)
|
||||
|
||||
def test_malformed_route_is_contained_without_exception_text_logging(self):
|
||||
self.engine._update_route = Mock(side_effect=ValueError('secret-url-must-not-be-logged'))
|
||||
self.engine.update()
|
||||
self.assertEqual(self.params.values['MapboxTurnContext']['reason'], 'route_processing_failed')
|
||||
text = str(self.navd.cloudlog.event.call_args_list)
|
||||
self.assertNotIn('secret-url', text)
|
||||
|
||||
|
||||
class MapGenerationTests(unittest.TestCase):
|
||||
def test_map_eof_source_is_route_generation_not_render_completion(self):
|
||||
path = Path(__file__).resolve().parents[1]/'mapsd.py'
|
||||
tree = ast.parse(path.read_text())
|
||||
server_send = [node for node in ast.walk(tree) if isinstance(node, ast.Call) and
|
||||
isinstance(node.func, ast.Attribute) and node.func.attr == 'send'][0]
|
||||
self.assertEqual(ast.unparse(server_send.args[-1]), 'generation')
|
||||
# Also execute the actual map class: an unchanging route resend may not
|
||||
# relabel a render with wall-clock completion time.
|
||||
namespace = {'np': np, 'time': SimpleNamespace(monotonic=lambda: 100.), 'RETRY_SECONDS': 10,
|
||||
'RouteTracker': Mock(), 'latest_gps_source': lambda sm: 'gps', 'MapRenderer': Mock(), 'PMTiles': Mock(),
|
||||
'RemoteTiles': Mock(), 'SIZE': 256, 'cloudlog': Mock()}
|
||||
cls = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == 'MapsD')
|
||||
exec(compile(ast.Module(body=[cls], type_ignores=[]), str(path), 'exec'), namespace)
|
||||
maps = namespace['MapsD']()
|
||||
maps.renderer = Mock()
|
||||
maps.renderer.render.return_value = np.zeros((256, 256), np.uint8), True
|
||||
sm = SimpleNamespace(updated={'navRoute': True}, logMonoTime={'navRoute': 123, 'gps': 99_900_000_000})
|
||||
values = {'navRoute': SimpleNamespace(coordinates=[SimpleNamespace(longitude=0., latitude=0.),
|
||||
SimpleNamespace(longitude=0., latitude=.001)]), 'gps': SimpleNamespace(longitude=0., latitude=0.)}
|
||||
class Messages:
|
||||
updated, logMonoTime = sm.updated, sm.logMonoTime
|
||||
def __getitem__(self, key):
|
||||
return values[key]
|
||||
maps.update(Messages())
|
||||
self.assertEqual(maps.route_generation, 123)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,216 +0,0 @@
|
||||
"""Display-path tests without a window/GPU; execute the actual widget methods."""
|
||||
import ast
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
import numpy as np
|
||||
|
||||
WORKTREE = Path(__file__).resolve().parents[4]
|
||||
UI = WORKTREE/'openpilot/selfdrive/ui/mici/onroad'
|
||||
|
||||
|
||||
class Label:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.text = ''
|
||||
|
||||
def set_text(self, text):
|
||||
self.text = text
|
||||
|
||||
def render(self, rect):
|
||||
pass
|
||||
|
||||
|
||||
def load_widget(state):
|
||||
def rectangle(x, y, width, height):
|
||||
return SimpleNamespace(x=x, y=y, width=width, height=height)
|
||||
rl = SimpleNamespace(Color=lambda *args: args, WHITE='white', BLACK='black', Rectangle=rectangle,
|
||||
Vector2=lambda x, y: (x, y), draw_rectangle_rec=Mock(), draw_line_ex=Mock(), draw_circle_v=Mock())
|
||||
namespace = {'__name__': 'navigation_ui_test', 'np': np, 'rl': rl, 'time': SimpleNamespace(monotonic=lambda: 100.),
|
||||
'Widget': object, 'UnifiedLabel': Label, 'gui_label': Mock(), 'ui_state': state,
|
||||
'VisionIpcClient': Mock(), 'VisionStreamType': SimpleNamespace(VISION_STREAM_MAP=1),
|
||||
'FontWeight': SimpleNamespace(BOLD=1, MEDIUM=2), 'TextAlignmentVertical': SimpleNamespace(TOP=1),
|
||||
'ROUTE_INTENSITY': 128, 'SIZE': 256, 'latest_gps_source': lambda sm: 'gps',
|
||||
'CONNECTION_RETRY_INTERVAL': 1., 'EGO': [128, 192], 'CV': SimpleNamespace()}
|
||||
tree = ast.parse((UI/'navigation.py').read_text())
|
||||
tree.body = [node for node in tree.body if not isinstance(node, (ast.Import, ast.ImportFrom))]
|
||||
exec(compile(tree, str(UI/'navigation.py'), 'exec'), namespace)
|
||||
return SimpleNamespace(**namespace)
|
||||
|
||||
|
||||
class SM:
|
||||
def __init__(self):
|
||||
self.valid, self.alive = {'navInstruction': True}, {'navInstruction': True}
|
||||
self.car = SimpleNamespace(leftBlinker=False, rightBlinker=True)
|
||||
self.instruction = SimpleNamespace(maneuverModifier='right', maneuverDistance=8., timeRemaining=20., distanceRemaining=100.)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.car if key == 'carState' else self.instruction
|
||||
|
||||
|
||||
def state(status='unavailable', destination=False):
|
||||
values = {'MapboxNextTurnEnabled': True, 'MapboxTurnIntent': {'session': 'one', 'direction': 'right'},
|
||||
'MapboxTurnContext': {'session': 'one', 'direction': 'right', 'status': status, 'updatedAt': 100.,
|
||||
'reason': 'explicit_destination' if status == 'blocked' else 'gps_unavailable',
|
||||
'locationValid': status == 'ready', 'headingValid': status == 'ready',
|
||||
'distanceM': 8., 'name': 'Example Street', 'routeGeneration': 10}}
|
||||
params = SimpleNamespace(get=values.get, get_bool=lambda key: values.get(key) is True)
|
||||
return SimpleNamespace(sm=SM(), params=params, nav_destination_set=destination, navigation_enabled=True, is_metric=True), values
|
||||
|
||||
|
||||
class TestSignalTurnStatus(unittest.TestCase):
|
||||
def test_all_pending_states_are_visible_and_ready_is_separate(self):
|
||||
for status, text in [('searching', 'Finding next right'), ('unavailable', 'Map turn unavailable'),
|
||||
('passed', 'Turn passed'), ('blocked', 'Signal turn blocked')]:
|
||||
ui_state, _ = state(status)
|
||||
module = load_widget(ui_state)
|
||||
widget = module.NavigationPanel()
|
||||
self.assertTrue(widget.signal_turn_active)
|
||||
self.assertEqual(widget.signal_status['primary'], text)
|
||||
widget._render(module.rl.Rectangle(0, 0, 261, 400))
|
||||
module.rl.draw_line_ex.assert_not_called()
|
||||
self.assertTrue(any(call.args[1] == text for call in module.gui_label.call_args_list))
|
||||
|
||||
def test_ready_has_direction_arrow_and_explicit_signal_label(self):
|
||||
ui_state, _ = state('ready')
|
||||
module = load_widget(ui_state)
|
||||
widget = module.NavigationPanel()
|
||||
widget._render(module.rl.Rectangle(0, 0, 261, 400))
|
||||
self.assertGreater(module.rl.draw_line_ex.call_count, 0)
|
||||
self.assertIn('Route ready', widget._eta_label.text)
|
||||
|
||||
def test_route_preview_is_not_navigation_active_without_fresh_model_consumption(self):
|
||||
ui_state, values = state('ready')
|
||||
for value in (None, {'updatedAt': 99., 'supported': True, 'active': True, 'session': 'one'},
|
||||
{'updatedAt': 100., 'supported': True, 'active': True, 'session': 'old'},
|
||||
{'updatedAt': 100., 'supported': False, 'active': False, 'session': 'one'}):
|
||||
values['NavModelState'] = value
|
||||
widget = load_widget(ui_state).NavigationPanel()
|
||||
self.assertEqual(widget.signal_status['primary'], 'Route ready')
|
||||
values['NavModelState'] = {'updatedAt': 100., 'supported': True, 'active': True, 'session': 'one', 'routeGeneration': 10}
|
||||
widget = load_widget(ui_state).NavigationPanel()
|
||||
self.assertEqual(widget.signal_status['primary'], 'Navigation active')
|
||||
|
||||
def test_ready_route_explains_fresh_matched_model_map_wait(self):
|
||||
ui_state, values = state('ready')
|
||||
values['NavModelState'] = {'updatedAt': 100., 'supported': True, 'active': False,
|
||||
'session': 'one', 'routeGeneration': 10, 'reason': 'map_missing'}
|
||||
module = load_widget(ui_state)
|
||||
widget = module.NavigationPanel()
|
||||
self.assertEqual(widget.signal_status['primary'], 'Route ready')
|
||||
self.assertEqual(widget.signal_status['secondary'], 'Waiting for model map')
|
||||
widget._render(module.rl.Rectangle(0, 0, 261, 400))
|
||||
self.assertIn('Waiting for model map', widget._eta_label.text)
|
||||
|
||||
def test_fresh_matched_wait_reasons_are_specific_without_claiming_active(self):
|
||||
expected = {'map_stale': 'Model map is stale', 'map_invalid': 'Model map is invalid',
|
||||
'instruction_stale': 'Route instructions are stale', 'route_generation_mismatch': 'Waiting for current route map',
|
||||
'disabled': 'Navigation inputs disabled', 'invalid_time_or_speed': 'Vehicle data unavailable'}
|
||||
for reason, text in expected.items():
|
||||
with self.subTest(reason=reason):
|
||||
ui_state, values = state('ready')
|
||||
values['NavModelState'] = {'updatedAt': 100., 'supported': True, 'active': False,
|
||||
'session': 'one', 'routeGeneration': 10, 'reason': reason}
|
||||
result = load_widget(ui_state).NavigationPanel().signal_status
|
||||
self.assertEqual(result['primary'], 'Route ready')
|
||||
self.assertEqual(result['secondary'], text)
|
||||
|
||||
def test_stale_wrong_route_or_unsupported_model_reason_is_not_reused(self):
|
||||
for change in ({'updatedAt': 99.}, {'updatedAt': 101.}, {'updatedAt': float('nan')}, {'updatedAt': True},
|
||||
{'session': 'other'}, {'routeGeneration': 11}, {'supported': False}, {'active': 0}):
|
||||
with self.subTest(change=change):
|
||||
ui_state, values = state('ready')
|
||||
values['NavModelState'] = {'updatedAt': 100., 'supported': True, 'active': False,
|
||||
'session': 'one', 'routeGeneration': 10, 'reason': 'map_invalid', **change}
|
||||
result = load_widget(ui_state).NavigationPanel().signal_status
|
||||
self.assertEqual(result['primary'], 'Route ready')
|
||||
self.assertEqual(result['secondary'], 'Waiting for map and model')
|
||||
|
||||
def test_active_and_pending_route_labels_ignore_old_wait_reason(self):
|
||||
for mode, active, primary in (('turn', True, 'Navigation active'), ('pending', False, 'Turn candidate')):
|
||||
ui_state, values = state('ready')
|
||||
values['MapboxTurnIntent']['mode'] = mode
|
||||
values['NavModelState'] = {'updatedAt': 100., 'supported': True, 'active': active,
|
||||
'session': 'one', 'routeGeneration': 10, 'reason': 'map_missing'}
|
||||
result = load_widget(ui_state).NavigationPanel().signal_status
|
||||
self.assertEqual(result['primary'], primary)
|
||||
self.assertEqual(result['secondary'], 'Right onto Example Street')
|
||||
|
||||
def test_unknown_or_malformed_wait_reason_keeps_generic_message(self):
|
||||
for reason in (None, 'unknown_future_reason', [], {}):
|
||||
ui_state, values = state('ready')
|
||||
values['NavModelState'] = {'updatedAt': 100., 'supported': True, 'active': False,
|
||||
'session': 'one', 'routeGeneration': 10, 'reason': reason}
|
||||
self.assertEqual(load_widget(ui_state).NavigationPanel().signal_status['secondary'], 'Waiting for map and model')
|
||||
|
||||
def test_explicit_destination_keeps_normal_arrow_and_shows_blocked_footer(self):
|
||||
ui_state, _ = state('blocked', destination=True)
|
||||
module = load_widget(ui_state)
|
||||
widget = module.NavigationPanel()
|
||||
widget._render(module.rl.Rectangle(0, 0, 261, 400))
|
||||
self.assertGreater(module.rl.draw_line_ex.call_count, 0)
|
||||
self.assertIn('Destination navigation active', widget._eta_label.text)
|
||||
|
||||
def test_pending_candidate_does_not_claim_navigation_is_active(self):
|
||||
ui_state, values = state('ready')
|
||||
values['MapboxTurnIntent']['mode'] = 'pending'
|
||||
self.assertEqual(load_widget(ui_state).NavigationPanel().signal_status['primary'], 'Turn candidate')
|
||||
|
||||
def test_lane_change_holds_prepared_route_without_claiming_navigation(self):
|
||||
ui_state, values = state('ready')
|
||||
values['MapboxTurnIntent']['mode'] = 'lane_change'
|
||||
status = load_widget(ui_state).NavigationPanel().signal_status
|
||||
self.assertEqual(status['primary'], 'Lane change')
|
||||
self.assertEqual(status['state'], 'lane_change')
|
||||
|
||||
def test_actual_destination_owned_context_has_priority_without_a_signal_session(self):
|
||||
ui_state, values = state('ready', destination=True)
|
||||
values['MapboxTurnContext'] = {'status': 'ready', 'updatedAt': 100., 'routeOwner': 'destination', 'routeGeneration': 20}
|
||||
widget = load_widget(ui_state).NavigationPanel()
|
||||
self.assertEqual(widget.signal_status['secondary'], 'Destination navigation active')
|
||||
|
||||
def test_retained_exit_guidance_remains_visible_after_auto_cancel(self):
|
||||
ui_state, values = state('ready')
|
||||
ui_state.sm.car.rightBlinker = False
|
||||
values['MapboxTurnIntent'] = None
|
||||
values['MapboxTurnContext']['retainedAfterSignalCancel'] = True
|
||||
widget = load_widget(ui_state).NavigationPanel()
|
||||
self.assertTrue(widget.signal_turn_active)
|
||||
ui_state.sm.car.leftBlinker = True
|
||||
self.assertFalse(load_widget(ui_state).NavigationPanel().signal_turn_active)
|
||||
|
||||
def test_cancel_disable_hazards_stale_or_wrong_session_does_not_claim_ready(self):
|
||||
for update in ('off', 'cancel', 'hazards', 'stale', 'different_session'):
|
||||
ui_state, values = state('ready')
|
||||
if update == 'off':
|
||||
values['MapboxNextTurnEnabled'] = False
|
||||
elif update == 'cancel':
|
||||
ui_state.sm.car.rightBlinker = False
|
||||
elif update == 'hazards':
|
||||
ui_state.sm.car.leftBlinker = True
|
||||
elif update == 'stale':
|
||||
values['MapboxTurnContext']['updatedAt'] = 99.
|
||||
else:
|
||||
values['MapboxTurnContext']['session'] = 'old'
|
||||
widget = load_widget(ui_state).NavigationPanel()
|
||||
if update in ('off', 'cancel', 'hazards'):
|
||||
self.assertFalse(widget.signal_turn_active)
|
||||
else:
|
||||
self.assertEqual(widget.signal_status['state'], 'unavailable')
|
||||
|
||||
def test_augmented_view_opens_for_signal_without_destination_and_yields_to_alert(self):
|
||||
tree = ast.parse((UI/'augmented_road_view.py').read_text())
|
||||
assignment = next(node for node in ast.walk(tree) if isinstance(node, ast.Assign) and
|
||||
any(isinstance(target, ast.Name) and target.id == 'show_navigation' for target in node.targets))
|
||||
code = compile(ast.Expression(assignment.value), 'actual_navigation_visibility', 'eval')
|
||||
for enabled, destination, signal, alert, expected in [(True, False, True, None, True),
|
||||
(True, False, False, None, False), (False, False, True, None, False),
|
||||
(True, True, False, None, True), (True, False, True, object(), False)]:
|
||||
result = eval(code, {'ui_state': SimpleNamespace(navigation_enabled=enabled, nav_destination_set=destination),
|
||||
'self': SimpleNamespace(_navigation=SimpleNamespace(signal_turn_active=signal)), 'alert_to_render': alert})
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,215 +1,114 @@
|
||||
"""Bounded byte ranges of a public Protomaps v4 basemap; no device account.
|
||||
|
||||
Experimental single-device transport. Public daily-build hosting is not a fleet
|
||||
backend: deployments should mirror the archive while preserving its identity.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
"""Byte ranges of the hosted PMTiles basemap, cached on disk."""
|
||||
import re
|
||||
from datetime import date
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
from openpilot.common.api import Api
|
||||
from openpilot.common.hardware import PC
|
||||
from openpilot.common.hardware.hw import Paths
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.navigation.pmtiles import HEADER_LENGTH
|
||||
|
||||
BUILD_INDEX_URL = 'https://build-metadata.protomaps.dev/builds.json'
|
||||
BUILD_BASE_URL = 'https://build.protomaps.com/'
|
||||
INDEX_LIMIT = 512 * 1024
|
||||
MAX_RANGE = 4 * 1024 * 1024
|
||||
CACHE_LIMIT = 64 * 1024 * 1024
|
||||
MERGE_GAP = 256 * 1024
|
||||
TIMEOUT = (5, 10)
|
||||
|
||||
|
||||
def _build(entry):
|
||||
"""Only supported official dated archives, never a URL supplied by metadata."""
|
||||
if not isinstance(entry, dict):
|
||||
return None
|
||||
key, size, version = (entry.get(name) for name in ('key', 'size', 'version'))
|
||||
if (not isinstance(key, str) or not re.fullmatch(r'\d{8}\.pmtiles', key) or
|
||||
isinstance(size, bool) or not isinstance(size, int) or not HEADER_LENGTH <= size <= 1024 ** 4 or
|
||||
not isinstance(version, str) or not re.fullmatch(r'4\.\d+\.\d+', version)):
|
||||
return None
|
||||
try:
|
||||
date(int(key[:4]), int(key[4:6]), int(key[6:8]))
|
||||
except ValueError:
|
||||
return None
|
||||
return {'key': key, 'size': size, 'version': version, 'url': BUILD_BASE_URL + key}
|
||||
|
||||
|
||||
def _strong_etag(value):
|
||||
return isinstance(value, str) and re.fullmatch(r'"[\x21\x23-\x7e]{1,510}"', value) is not None
|
||||
MERGE_GAP = 256 * 1024 # tiles this close together in the archive are fetched in one range request
|
||||
|
||||
|
||||
class RemoteTiles:
|
||||
def __init__(self):
|
||||
self.base = Path(Paths.comma_home() if PC else '/data') / 'navigation' / 'tiles'
|
||||
self.base.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
self.cache = Path(Paths.comma_home() if PC else '/data') / 'navigation' / 'tiles'
|
||||
self.cache.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
self.session = requests.Session()
|
||||
self.url, self.etag, self.size = '', None, 0
|
||||
self.cache = None
|
||||
saved = self._saved_identity()
|
||||
self.api = Api(Params().get('DongleId'))
|
||||
self.url = ''
|
||||
self.expires = 0
|
||||
self.etag = None
|
||||
self.size = 0
|
||||
version = self.cache / 'version'
|
||||
try:
|
||||
try:
|
||||
build = self._latest_build()
|
||||
self.url, self.size = build['url'], build['size']
|
||||
header = self._fetch(0, HEADER_LENGTH)
|
||||
identity = build | {'etag': self.etag}
|
||||
except requests.RequestException:
|
||||
# An offline reader may reuse only a previously pinned archive with its
|
||||
# exact cached header. Missing ranges still fail; no route-only raster.
|
||||
if saved is None:
|
||||
raise
|
||||
identity = saved
|
||||
self.url, self.etag, self.size = identity['url'], identity['etag'], identity['size']
|
||||
self.cache = self._cache_for(identity)
|
||||
header = (self.cache / f'0-{HEADER_LENGTH}').read_bytes()
|
||||
if len(header) != HEADER_LENGTH or header[:8] != b'PMTiles\x03':
|
||||
raise ValueError('Invalid public PMTiles header')
|
||||
self.cache = self._cache_for(identity)
|
||||
self.cache.mkdir(parents=True, exist_ok=True)
|
||||
header = self._fetch(0, HEADER_LENGTH) # learns the archive's ETag and size
|
||||
if not version.exists() or version.read_text() != f'{self.etag} {self.size}':
|
||||
for cached in self.cache.iterdir(): # a new archive has new offsets
|
||||
cached.unlink()
|
||||
version.write_text(f'{self.etag} {self.size}')
|
||||
self._store(0, header)
|
||||
temporary = self.base / 'archive.tmp'
|
||||
temporary.write_text(json.dumps(identity))
|
||||
temporary.replace(self.base / 'archive.json')
|
||||
self._trim()
|
||||
except (requests.RequestException, OSError, ValueError, TypeError, KeyError):
|
||||
except Exception:
|
||||
self.close()
|
||||
raise OSError('Unable to load public Protomaps basemap') from None
|
||||
|
||||
def _cache_for(self, identity):
|
||||
key = hashlib.sha256(json.dumps(identity, sort_keys=True).encode()).hexdigest()[:32]
|
||||
return self.base / 'archives' / key
|
||||
|
||||
def _saved_identity(self):
|
||||
try:
|
||||
path = self.base / 'archive.json'
|
||||
if path.stat().st_size > 4096:
|
||||
return None
|
||||
saved = json.loads(path.read_text())
|
||||
build = _build(saved)
|
||||
if build is not None and saved.get('url') == build['url'] and _strong_etag(saved.get('etag')):
|
||||
return build | {'etag': saved['etag']}
|
||||
except (OSError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
def _latest_build(self):
|
||||
with self.session.get(BUILD_INDEX_URL, headers={'Accept-Encoding': 'identity'}, timeout=TIMEOUT, stream=True,
|
||||
allow_redirects=False) as response:
|
||||
response.raise_for_status()
|
||||
if response.status_code != 200:
|
||||
raise ValueError('Unexpected public build index response')
|
||||
declared = response.headers.get('Content-Length')
|
||||
if declared is not None and not 0 <= int(declared) <= INDEX_LIMIT:
|
||||
raise ValueError('Oversized public build index')
|
||||
data = bytearray()
|
||||
for chunk in response.iter_content(chunk_size=65536):
|
||||
if len(data) + len(chunk) > INDEX_LIMIT:
|
||||
raise ValueError('Oversized public build index')
|
||||
data.extend(chunk)
|
||||
entries = json.loads(data)
|
||||
if not isinstance(entries, list) or len(entries) > 4096:
|
||||
raise ValueError('Invalid public build index')
|
||||
builds = [build for entry in entries if (build := _build(entry)) is not None]
|
||||
if not builds:
|
||||
raise ValueError('No compatible public Protomaps v4 build')
|
||||
return max(builds, key=lambda build: build['key'])
|
||||
|
||||
def _range(self, offset, length):
|
||||
if (isinstance(offset, bool) or isinstance(length, bool) or not isinstance(offset, int) or not isinstance(length, int) or
|
||||
offset < 0 or not 0 < length <= MAX_RANGE or offset + length > self.size):
|
||||
raise ValueError('Invalid or oversized public map range')
|
||||
raise
|
||||
|
||||
def _fetch(self, offset: int, length: int) -> bytes:
|
||||
self._range(offset, length)
|
||||
headers = {'Range': f'bytes={offset}-{offset + length - 1}', 'Accept-Encoding': 'identity'}
|
||||
if self.etag is not None:
|
||||
headers['If-Match'] = self.etag
|
||||
with self.session.get(self.url, headers=headers, timeout=TIMEOUT, stream=True, allow_redirects=False) as response:
|
||||
response.raise_for_status()
|
||||
if response.status_code != 206:
|
||||
raise OSError(f'Public map range request failed ({response.status_code})')
|
||||
match = re.fullmatch(r'bytes (\d+)-(\d+)/(\d+)', response.headers.get('Content-Range', ''))
|
||||
if match is None or tuple(map(int, match.groups())) != (offset, offset + length - 1, self.size):
|
||||
raise OSError('Invalid public map range response')
|
||||
etag = response.headers.get('ETag')
|
||||
if not _strong_etag(etag) or (self.etag is not None and etag != self.etag):
|
||||
raise OSError('Public map archive changed during read')
|
||||
declared = response.headers.get('Content-Length')
|
||||
if declared is not None and int(declared) != length:
|
||||
raise OSError('Invalid public map range length')
|
||||
if response.headers.get('Content-Encoding', 'identity') != 'identity':
|
||||
raise OSError('Encoded public map range response')
|
||||
data = bytearray()
|
||||
for chunk in response.iter_content(chunk_size=min(length + 1, 65536)):
|
||||
if len(data) + len(chunk) > length:
|
||||
raise OSError('Oversized public map range response')
|
||||
data.extend(chunk)
|
||||
if len(data) != length:
|
||||
raise OSError('Truncated public map range response')
|
||||
self.etag = etag
|
||||
return bytes(data)
|
||||
try:
|
||||
if time.time() >= self.expires - 60: # noqa: TID251 - SAS expiry is a Unix timestamp
|
||||
response = self.api.get(f'v1/{self.api.dongle_id}/nav/tiles', access_token=self.api.get_token(), timeout=10)
|
||||
response.raise_for_status()
|
||||
metadata = response.json()
|
||||
self.url, self.expires = metadata['url'], metadata['expires']
|
||||
headers = {'Range': f'bytes={offset}-{offset + length - 1}', 'Accept-Encoding': 'identity'}
|
||||
if self.etag is not None:
|
||||
headers['If-Match'] = self.etag
|
||||
with self.session.get(self.url, headers=headers, timeout=10, stream=True) as response:
|
||||
if response.status_code != 206:
|
||||
raise OSError(f'Map tile range request failed ({response.status_code})')
|
||||
match = re.fullmatch(r'bytes (\d+)-(\d+)/(\d+)', response.headers.get('Content-Range', ''))
|
||||
if match is None or tuple(map(int, match.groups()[:2])) != (offset, offset + length - 1):
|
||||
raise OSError('Invalid map tile range response')
|
||||
size = int(match[3])
|
||||
etag = response.headers.get('ETag')
|
||||
if not etag or (self.etag is not None and (etag != self.etag or size != self.size)):
|
||||
raise OSError('Map archive changed during read')
|
||||
data = bytearray()
|
||||
for chunk in response.iter_content(chunk_size=min(length + 1, 65536)):
|
||||
data.extend(chunk)
|
||||
if len(data) > length:
|
||||
raise OSError('Oversized map tile range response')
|
||||
if len(data) != length:
|
||||
raise OSError('Truncated map tile range response')
|
||||
self.size, self.etag = size, etag
|
||||
return bytes(data)
|
||||
except (requests.RequestException, ValueError, KeyError, TypeError, RuntimeError):
|
||||
# requests exceptions contain the signed URL; keep it out of logs.
|
||||
raise OSError('Unable to fetch map tiles') from None
|
||||
|
||||
def _store(self, offset: int, data: bytes):
|
||||
path = self.cache / f'{offset}-{len(data)}'
|
||||
temporary = path.with_suffix('.tmp')
|
||||
temporary.write_bytes(data)
|
||||
temporary.replace(path)
|
||||
(self.cache / f'{offset}-{len(data)}').write_bytes(data)
|
||||
|
||||
def _trim(self):
|
||||
files = [(cached.stat().st_mtime, cached.stat().st_size, cached) for cached in self.base.rglob('*')
|
||||
if cached.is_file() and re.fullmatch(r'\d+-\d+', cached.name)]
|
||||
files = [(cached.stat().st_mtime, cached.stat().st_size, cached) for cached in self.cache.iterdir() if re.fullmatch(r'\d+-\d+', cached.name)]
|
||||
size = sum(cached_size for _, cached_size, _ in files)
|
||||
for _, cached_size, cached in sorted(files):
|
||||
if size <= CACHE_LIMIT:
|
||||
break
|
||||
if cached == self.cache / f'0-{HEADER_LENGTH}':
|
||||
continue # Required for an offline restart of the current pinned archive.
|
||||
cached.unlink(missing_ok=True)
|
||||
size -= cached_size
|
||||
|
||||
def read(self, offset: int, length: int) -> bytes:
|
||||
self._range(offset, length)
|
||||
try:
|
||||
data = (self.cache / f'{offset}-{length}').read_bytes()
|
||||
if len(data) == length:
|
||||
return data
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
try:
|
||||
data = self._fetch(offset, length)
|
||||
except (requests.RequestException, ValueError):
|
||||
raise OSError('Unable to fetch public map tiles') from None
|
||||
data = self._fetch(offset, length)
|
||||
self._store(offset, data)
|
||||
self._trim()
|
||||
return data
|
||||
|
||||
def prefetch(self, ranges):
|
||||
missing = []
|
||||
for offset, length in sorted(set(ranges)):
|
||||
self._range(offset, length)
|
||||
if not (self.cache / f'{offset}-{length}').exists():
|
||||
missing.append((offset, length))
|
||||
"""Cache (offset, length) ranges ahead of reads, fetching neighboring ones in a single request."""
|
||||
missing = sorted((offset, length) for offset, length in set(ranges) if not (self.cache / f'{offset}-{length}').exists())
|
||||
if not missing:
|
||||
return
|
||||
groups = [[missing[0]]]
|
||||
for offset, length in missing[1:]:
|
||||
start = groups[-1][0][0]
|
||||
last_offset, last_length = groups[-1][-1]
|
||||
if offset - (last_offset + last_length) <= MERGE_GAP and offset + length - start <= MAX_RANGE:
|
||||
if offset - (last_offset + last_length) <= MERGE_GAP:
|
||||
groups[-1].append((offset, length))
|
||||
else:
|
||||
groups.append([(offset, length)])
|
||||
for group in groups:
|
||||
start, end = group[0][0], max(offset + length for offset, length in group)
|
||||
try:
|
||||
data = self._fetch(start, end - start)
|
||||
except (requests.RequestException, ValueError):
|
||||
raise OSError('Unable to prefetch public map tiles') from None
|
||||
start, end = group[0][0], group[-1][0] + group[-1][1]
|
||||
data = self._fetch(start, end - start)
|
||||
for offset, length in group:
|
||||
self._store(offset, data[offset - start:offset - start + length])
|
||||
self._trim()
|
||||
|
||||
Binary file not shown.
@@ -221,8 +221,7 @@ class AugmentedRoadView(CameraView):
|
||||
self._update_calibration()
|
||||
|
||||
alert_to_render, not_animating_out = self._alert_renderer.will_render()
|
||||
show_navigation = (ui_state.navigation_enabled and (ui_state.nav_destination_set or self._navigation.signal_turn_active)
|
||||
and alert_to_render is None)
|
||||
show_navigation = ui_state.navigation_enabled and ui_state.nav_destination_set and alert_to_render is None
|
||||
|
||||
# Create inner content area with border padding
|
||||
self._content_rect = rl.Rectangle(
|
||||
|
||||
@@ -141,7 +141,6 @@ class ModelRenderer(Widget, ModelRendererSP):
|
||||
if ui_state.status != UIStatus.DISENGAGED:
|
||||
self._draw_lane_lines()
|
||||
self._draw_path()
|
||||
self.blendful.draw(self._rect)
|
||||
|
||||
if render_lead_indicator:
|
||||
self._update_leads(sm)
|
||||
@@ -220,7 +219,6 @@ class ModelRenderer(Widget, ModelRendererSP):
|
||||
# Update road edges using raw points
|
||||
for road_edge in self._road_edges:
|
||||
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, line_width_factor, 0.0, max_idx)
|
||||
self.blendful.update(self, max_idx)
|
||||
|
||||
# Update path using raw points
|
||||
if lead and lead.present:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
import time
|
||||
|
||||
from msgq.visionipc import VisionIpcClient
|
||||
from openpilot.cereal.visionipc import VisionStreamType
|
||||
@@ -23,75 +22,6 @@ MAP_COLORS = np.repeat(np.round(np.arange(256) * 0.45).astype(np.uint8)[:, None]
|
||||
MAP_COLORS[ROUTE_INTENSITY - 1:ROUTE_INTENSITY + 2] = (72, 235, 132)
|
||||
|
||||
|
||||
def signal_turn_status(enabled, intent, context, now, left_blinker, right_blinker, model_state=None):
|
||||
"""Distinguish a route lookup from actual navigation inputs consumed by the model."""
|
||||
direction = 'left' if left_blinker and not right_blinker else 'right' if right_blinker and not left_blinker else None
|
||||
if (direction is None and not left_blinker and not right_blinker and isinstance(context, dict) and
|
||||
context.get('retainedAfterSignalCancel') is True and context.get('status') == 'ready'):
|
||||
# Auto-cancel during an entered turn does not end its short exit guidance.
|
||||
direction = context.get('direction')
|
||||
intent = {'session': context.get('session'), 'direction': direction}
|
||||
if (enabled is not True or not isinstance(intent, dict) or not intent.get('session') or
|
||||
direction not in ('left', 'right') or intent.get('direction') != direction):
|
||||
return None
|
||||
status, reason = 'unavailable', 'navigation_unavailable'
|
||||
if isinstance(context, dict) and context.get('routeOwner') == 'destination':
|
||||
return {'state': 'blocked', 'primary': 'Signal turn blocked', 'secondary': 'Destination navigation active'}
|
||||
if intent.get('mode') == 'lane_change':
|
||||
return {'state': 'lane_change', 'primary': 'Lane change', 'secondary': 'Street route on hold'}
|
||||
if isinstance(context, dict) and context.get('session') == intent['session'] and context.get('direction') == direction:
|
||||
updated = context.get('updatedAt')
|
||||
if isinstance(updated, (int, float)) and not isinstance(updated, bool) and np.isfinite(updated) and 0. <= now-updated <= .35:
|
||||
status, reason = context.get('status'), context.get('reason')
|
||||
if status == 'ready' and context.get('locationValid') is True and context.get('headingValid') is True:
|
||||
distance = context.get('distanceM')
|
||||
if isinstance(distance, (int, float)) and not isinstance(distance, bool) and np.isfinite(distance):
|
||||
model_updated = model_state.get('updatedAt') if isinstance(model_state, dict) else None
|
||||
active = (isinstance(model_updated, (int, float)) and not isinstance(model_updated, bool) and
|
||||
np.isfinite(model_updated) and 0. <= now-model_updated <= .5 and
|
||||
model_state.get('supported') is True and model_state.get('active') is True and
|
||||
model_state.get('session') == intent['session'] and
|
||||
isinstance(context.get('routeGeneration'), int) and context['routeGeneration'] > 0 and
|
||||
model_state.get('routeGeneration') == context['routeGeneration'])
|
||||
waiting = 'Waiting for map and model'
|
||||
matched_wait = (isinstance(model_updated, (int, float)) and not isinstance(model_updated, bool) and
|
||||
np.isfinite(model_updated) and 0. <= now-model_updated <= .5 and
|
||||
model_state.get('supported') is True and model_state.get('active') is False and
|
||||
model_state.get('session') == intent['session'] and
|
||||
isinstance(context.get('routeGeneration'), int) and not isinstance(context['routeGeneration'], bool) and
|
||||
context['routeGeneration'] > 0 and model_state.get('routeGeneration') == context['routeGeneration'])
|
||||
model_reason = model_state.get('reason') if matched_wait else None
|
||||
if isinstance(model_reason, str):
|
||||
waiting = {
|
||||
'map_missing': 'Waiting for model map', 'map_stale': 'Model map is stale', 'map_invalid': 'Model map is invalid',
|
||||
'instruction_stale': 'Route instructions are stale', 'route_generation_mismatch': 'Waiting for current route map',
|
||||
'disabled': 'Navigation inputs disabled', 'invalid_time_or_speed': 'Vehicle data unavailable',
|
||||
'signal_navigation_disabled': 'Signal navigation is inactive', 'route_context_missing': 'Waiting for route context',
|
||||
'route_context_unavailable': 'Route context unavailable', 'signal_session_mismatch': 'Waiting for current signal',
|
||||
'destination_route_unavailable': 'Waiting for destination route', 'waiting_for_navigation': 'Waiting for navigation inputs',
|
||||
'ready': 'Waiting for model response',
|
||||
}.get(model_reason, waiting)
|
||||
pending = intent.get('mode') == 'pending'
|
||||
return {'state': 'ready', 'primary': 'Turn candidate' if pending else 'Navigation active' if active else 'Route ready',
|
||||
'direction': direction, 'distanceM': distance,
|
||||
'secondary': (f"{direction.capitalize()} onto {context.get('name', '')}".strip() if active or pending else waiting)}
|
||||
if status == 'searching':
|
||||
return {'state': status, 'primary': f'Finding next {direction}', 'secondary': 'Checking mapped streets'}
|
||||
if status == 'passed':
|
||||
return {'state': status, 'primary': 'Turn passed', 'secondary': 'Signal again for another turn'}
|
||||
if status == 'blocked':
|
||||
return {'state': status, 'primary': 'Signal turn blocked',
|
||||
'secondary': 'Destination navigation active' if reason == 'explicit_destination' else 'Signal changed'}
|
||||
explanations = {'missing_token': 'Map token missing', 'invalid_token': 'Map token unavailable',
|
||||
'navigation_model_required': 'Select a navigation-capable model',
|
||||
'gps_unavailable': 'GPS position unavailable', 'gps_or_car_state_invalid': 'GPS position unavailable',
|
||||
'heading_unavailable': 'Vehicle heading unavailable', 'navigation_unavailable': 'Waiting for navigation',
|
||||
'target_expired': 'Signal again to refresh target', 'no_matching_turn': 'No supported street turn ahead',
|
||||
'ambiguous_approach': 'Approach is uncertain', 'approach_location_mismatch': 'Position does not match approach',
|
||||
'approach_heading_mismatch': 'Heading does not match approach'}
|
||||
return {'state': 'unavailable', 'primary': 'Map turn unavailable', 'secondary': explanations.get(reason, 'Map lookup unavailable')}
|
||||
|
||||
|
||||
def format_distance(meters: float) -> str:
|
||||
"""m / ft under 1000 m / 300 m, km / mi beyond."""
|
||||
if ui_state.is_metric:
|
||||
@@ -111,24 +41,6 @@ class NavigationPanel(Widget):
|
||||
self._eta_label = UnifiedLabel('', 34, FontWeight.BOLD, rl.WHITE, scroll=True)
|
||||
self._accent = rl.Color(155, 230, 185, 255)
|
||||
self._muted = rl.Color(162, 175, 185, 255)
|
||||
self._signal_status = None
|
||||
self._last_signal_poll = -np.inf
|
||||
|
||||
@property
|
||||
def signal_status(self):
|
||||
now = time.monotonic()
|
||||
if now-self._last_signal_poll >= .1:
|
||||
self._last_signal_poll = now
|
||||
car = ui_state.sm['carState']
|
||||
self._signal_status = signal_turn_status(
|
||||
ui_state.params.get_bool('MapboxNextTurnEnabled'), ui_state.params.get('MapboxTurnIntent'),
|
||||
ui_state.params.get('MapboxTurnContext'), now, car.leftBlinker, car.rightBlinker,
|
||||
ui_state.params.get('NavModelState'))
|
||||
return self._signal_status
|
||||
|
||||
@property
|
||||
def signal_turn_active(self):
|
||||
return self.signal_status is not None
|
||||
|
||||
def _update_state(self):
|
||||
if not self._client.is_connected():
|
||||
@@ -169,14 +81,8 @@ class NavigationPanel(Widget):
|
||||
x, y = rect.x, rect.y
|
||||
rl.draw_rectangle_rec(rect, rl.Color(7, 13, 18, 255))
|
||||
instruction = ui_state.sm['navInstruction']
|
||||
signal = self.signal_status
|
||||
signal_ready = signal is not None and signal['state'] == 'ready' and not ui_state.nav_destination_set
|
||||
notification = signal is not None and signal['state'] != 'ready' and not ui_state.nav_destination_set
|
||||
if notification:
|
||||
gui_label(rl.Rectangle(x+12, y+18, rect.width-24, 30), signal['primary'], 22, rl.WHITE, FontWeight.BOLD,
|
||||
alignment_vertical=TextAlignmentVertical.TOP)
|
||||
elif self.routing or signal_ready:
|
||||
direction = signal['direction'] if signal_ready else instruction.maneuverModifier
|
||||
if self.routing:
|
||||
direction = instruction.maneuverModifier
|
||||
flip = -1 if 'left' in direction else 1
|
||||
# The maneuver arrow on a 1.4x grid, centered in the header's icon box, with round joins for the thick stroke.
|
||||
if direction in ('straight', ''):
|
||||
@@ -188,38 +94,25 @@ class NavigationPanel(Widget):
|
||||
rl.draw_line_ex(a, b, 7, self._accent)
|
||||
for point in points:
|
||||
rl.draw_circle_v(point, 3.5, self._accent)
|
||||
distance = signal['distanceM'] if signal_ready else instruction.maneuverDistance
|
||||
gui_label(rl.Rectangle(x+64, y+11, rect.width-72, 40), format_distance(max(0, distance)), 34, rl.WHITE, FontWeight.BOLD,
|
||||
gui_label(rl.Rectangle(x+64, y+11, rect.width-72, 40), format_distance(max(0, instruction.maneuverDistance)), 34, rl.WHITE, FontWeight.BOLD,
|
||||
alignment_vertical=TextAlignmentVertical.TOP)
|
||||
else:
|
||||
gui_label(rl.Rectangle(x+12, y+18, rect.width-24, 30), 'Waiting for route', 26, rl.WHITE, FontWeight.BOLD,
|
||||
alignment_vertical=TextAlignmentVertical.TOP)
|
||||
map_rect = rl.Rectangle(x+8, y+61, rect.width-16, rect.height-111)
|
||||
rl.draw_rectangle_rec(map_rect, rl.BLACK)
|
||||
if notification:
|
||||
gui_label(rl.Rectangle(map_rect.x+10, map_rect.y+16, map_rect.width-20, 80), signal['secondary'], 24, self._muted,
|
||||
FontWeight.MEDIUM, alignment_vertical=TextAlignmentVertical.TOP)
|
||||
elif self.map_ready:
|
||||
if self.map_ready:
|
||||
# A 1:1 crop of the model's frame, centered horizontally with the ego near the bottom.
|
||||
width, height = min(map_rect.width, SIZE), min(map_rect.height, SIZE)
|
||||
source = rl.Rectangle((SIZE - width) // 2, min(SIZE - height, max(0, EGO[1] + MARKER_MARGIN - height)), width, height)
|
||||
target = rl.Rectangle(map_rect.x + (map_rect.width - width) / 2, map_rect.y + map_rect.height - height, width, height)
|
||||
rl.draw_texture_pro(self._texture, source, target, rl.Vector2(0, 0), 0, rl.WHITE)
|
||||
# Attribution is a display overlay; the model receives the original raster.
|
||||
credit = rl.Rectangle(map_rect.x+2, map_rect.y+map_rect.height-17, map_rect.width-4, 16)
|
||||
rl.draw_rectangle_rec(credit, rl.Color(7, 13, 18, 220))
|
||||
gui_label(credit, '© OpenStreetMap contributors', 12, self._muted, FontWeight.MEDIUM,
|
||||
alignment_vertical=TextAlignmentVertical.TOP)
|
||||
else:
|
||||
self._draw_status(map_rect)
|
||||
if signal is not None and (notification or ui_state.nav_destination_set):
|
||||
self._eta_label.set_text(f"{signal['primary']}: {signal['secondary']}")
|
||||
self._eta_label.render(rl.Rectangle(x+9, y+rect.height-45, rect.width-18, 40))
|
||||
elif self.routing or signal_ready:
|
||||
if self.routing:
|
||||
seconds = max(0, instruction.timeRemaining)
|
||||
remaining = max(0, instruction.distanceRemaining)
|
||||
self._eta_label.set_text(f"{signal['primary']}: {signal['secondary']}" if signal is not None else
|
||||
f'{max(1, round(seconds / 60))} min / {format_distance(remaining)}')
|
||||
self._eta_label.set_text(f'{max(1, round(seconds / 60))} min / {format_distance(remaining)}')
|
||||
self._eta_label.render(rl.Rectangle(x+9, y+rect.height-45, rect.width-18, 40))
|
||||
|
||||
def _unload_texture(self):
|
||||
|
||||
@@ -208,7 +208,7 @@ class TorqueBar(Widget):
|
||||
# draw curved line polygon torque bar
|
||||
torque_line_radius = 1200 * self._scale
|
||||
top_angle = -90
|
||||
torque_bg_angle_span = self._torque_line_alpha_filter.x * TORQUE_ANGLE_SPAN * min(1.0, rect.width / (476 * self._scale))
|
||||
torque_bg_angle_span = self._torque_line_alpha_filter.x * TORQUE_ANGLE_SPAN * rect.width / 476
|
||||
torque_start_angle = top_angle - torque_bg_angle_span / 2
|
||||
torque_end_angle = top_angle + torque_bg_angle_span / 2
|
||||
# centerline radius & center (you already have these values)
|
||||
|
||||
@@ -123,7 +123,6 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
|
||||
# Draw elements
|
||||
self._draw_lane_lines()
|
||||
self._draw_path()
|
||||
self.blendful.draw(self._rect)
|
||||
|
||||
if render_lead_indicator and radar_state:
|
||||
self._draw_lead_indicator()
|
||||
@@ -173,7 +172,6 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
|
||||
# Update road edges using raw points
|
||||
for road_edge in self._road_edges:
|
||||
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, 0.025, 0.0, max_idx, max_distance)
|
||||
self.blendful.update(self, max_idx, max_distance)
|
||||
|
||||
# Update path using raw points
|
||||
if lead and lead.present:
|
||||
|
||||
@@ -8,6 +8,7 @@ from enum import IntEnum
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise_sub_layouts.speed_limit_settings import SpeedLimitSettingsLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.mapd import MAPD_AVAILABLE
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp, simple_button_item_sp
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -29,6 +30,7 @@ ACC_ENABLED_DESCRIPTION = tr_noop("Enable custom Short & Long press increments f
|
||||
ACC_NOLONG_DESCRIPTION = tr_noop("This feature can only be used with sunnypilot longitudinal control enabled.")
|
||||
ACC_PCMCRUISE_DISABLED_DESCRIPTION = tr_noop("This feature is not supported on this platform due to vehicle limitations.")
|
||||
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
|
||||
MAP_UNAVAILABLE_DESCRIPTION = tr_noop("Map data is temporarily unavailable.")
|
||||
|
||||
|
||||
class CruiseLayout(Widget):
|
||||
@@ -54,7 +56,8 @@ class CruiseLayout(Widget):
|
||||
|
||||
self.scc_m_toggle = toggle_item_sp(
|
||||
title=tr("Smart Cruise Control - Map"),
|
||||
description=tr("Use map data to estimate the appropriate speed to drive through turns ahead."),
|
||||
description=(f"<b>{tr(MAP_UNAVAILABLE_DESCRIPTION)}</b>\n\n" if not MAPD_AVAILABLE else "") +
|
||||
tr("Use map data to estimate the appropriate speed to drive through turns ahead."),
|
||||
param="SmartCruiseControlMap")
|
||||
|
||||
self.custom_acc_toggle = toggle_item_sp(
|
||||
@@ -139,7 +142,7 @@ class CruiseLayout(Widget):
|
||||
if has_long or has_icbm:
|
||||
self.custom_acc_toggle.action_item.set_enabled(((has_long and not ui_state.CP.pcmCruise) or has_icbm) and ui_state.is_offroad())
|
||||
self.scc_v_toggle.action_item.set_enabled(True)
|
||||
self.scc_m_toggle.action_item.set_enabled(True)
|
||||
self.scc_m_toggle.action_item.set_enabled(MAPD_AVAILABLE)
|
||||
else:
|
||||
ui_state.params.remove("CustomAccIncrementsEnabled")
|
||||
ui_state.params.remove("SmartCruiseControlVision")
|
||||
|
||||
+6
@@ -8,6 +8,8 @@ from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.mapd import MAPD_AVAILABLE
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Policy
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp
|
||||
from openpilot.system.ui.widgets.network import NavButton
|
||||
@@ -43,6 +45,8 @@ class SpeedLimitPolicyLayout(Widget):
|
||||
param="SpeedLimitPolicy",
|
||||
button_width=250,
|
||||
)
|
||||
if not MAPD_AVAILABLE:
|
||||
self._speed_limit_policy.action_item.set_enabled_buttons({Policy.car_state_only})
|
||||
|
||||
items = [
|
||||
self._speed_limit_policy
|
||||
@@ -51,6 +55,8 @@ class SpeedLimitPolicyLayout(Widget):
|
||||
|
||||
@staticmethod
|
||||
def _get_policy_description():
|
||||
if not MAPD_AVAILABLE:
|
||||
return f"<b>{tr('Map data is temporarily unavailable. Using Speed Limit data only from Car.')}</b>"
|
||||
return get_highlighted_description(ui_state.params, "SpeedLimitPolicy", SPEED_LIMIT_POLICY_DESCRIPTIONS)
|
||||
|
||||
def _render(self, rect):
|
||||
|
||||
@@ -4,14 +4,12 @@ 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.
|
||||
"""
|
||||
from html import escape
|
||||
import re
|
||||
import time
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.cereal import custom
|
||||
from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS, get_selected_bundle, resolve_bundle_by_ref
|
||||
from openpilot.sunnypilot.models.navigation_model import BUILD_REF
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||
from openpilot.selfdrive.ui.sunnypilot.model_info import (big_model_state, bundles_for_source, carrying_model, default_model_name,
|
||||
@@ -69,12 +67,6 @@ class ModelsLayout(Widget):
|
||||
callback=lambda: self._open_source_dialog("chestnut")
|
||||
)
|
||||
|
||||
self._navigation_preparation_description = tr(
|
||||
"Preparing the experimental navigation model requires a large download and an on-device build. Keep the device parked. " +
|
||||
"When ready, select it from Big Model; preparation does not change the active model.")
|
||||
self.prepare_navigation_item = button_item(tr("Prepare Navigation Model"), tr("PREPARE"),
|
||||
self._navigation_preparation_description, self._prepare_navigation_model)
|
||||
|
||||
self.download_item = download_status_item(lambda: tr("Download") if self._downloading else tr("Model Status"))
|
||||
|
||||
self.refresh_item = button_item(tr("Refresh Model List"),
|
||||
@@ -104,69 +96,19 @@ class ModelsLayout(Widget):
|
||||
"This prevents situations (like at red lights) where the car might plan the wrong turn direction."),
|
||||
param="LaneTurnDesire")
|
||||
|
||||
self.map_turn_toggle = toggle_item_sp(tr("Signal Navigation (Experimental)"),
|
||||
tr("Signals can prepare a short street route while normal lane-change checks remain available. " +
|
||||
"Navigation waits for an active lane change to complete. " +
|
||||
"Requires a navigation-capable model, internet and a Mapbox token. " +
|
||||
"Early fast turn signals and slow lane changes can be ambiguous; this is experimental."),
|
||||
callback=self._enable_map_turns, enabled=lambda: ui_state.is_offroad() and ui_state.active_runner_tinygrad,
|
||||
param="MapboxNextTurnEnabled")
|
||||
|
||||
self.delay_control = option_item_sp(tr("Adjust Software Delay"), "LagdToggleDelay", 5, 50,
|
||||
tr("Adjust the software delay when Live Learning Steer Delay is toggled off. The default software delay value is 0.2"),
|
||||
1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True, lambda v: f"{v / 100:.2f}s")
|
||||
|
||||
self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle")
|
||||
|
||||
self._blendful_desc = tr("Adds a gentle nudge toward lane center on top of the model's own steering, stronger when lane lines are clear." +
|
||||
" Yields around obstacles. Not available with the Default model.")
|
||||
self.blendful_toggle = toggle_item_sp(tr("Blendful (Alpha)"), self._blendful_desc, param="Blendful")
|
||||
|
||||
self.camera_offset = option_item_sp(tr("Adjust Camera Offset"), "CameraOffset", -35, 35,
|
||||
tr("Virtually shift camera's perspective to move model's center to Left(+ values) or Right (- values)"),
|
||||
1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True,
|
||||
lambda v: f"{v / 100:.2f} m")
|
||||
|
||||
self.items = [self.small_model_item, self.big_model_item, self.prepare_navigation_item, self.cancel_download_item, self.download_item,
|
||||
self.refresh_item, self.clear_cache_item,
|
||||
self.blendful_toggle, self.lane_turn_desire_toggle, self.map_turn_toggle, self.lane_turn_value_control, self.lagd_toggle, self.delay_control,
|
||||
self.camera_offset]
|
||||
|
||||
def _prepare_navigation_model(self):
|
||||
if (ui_state.is_offroad() and ui_state.params.get("ModelManager_DownloadRef") is None and
|
||||
not ui_state.params.get_bool("ModelManager_ClearCache")):
|
||||
ui_state.params.put("ModelManager_DownloadRef", BUILD_REF)
|
||||
|
||||
def _handle_navigation_preparation(self, bundle, progresses):
|
||||
if bundle is None or bundle.ref != BUILD_REF:
|
||||
if getattr(self, '_navigation_preparation_message', None) is not None:
|
||||
self._navigation_preparation_message = None
|
||||
self.prepare_navigation_item.set_description(self._navigation_preparation_description)
|
||||
self.prepare_navigation_item.show_description(False)
|
||||
return False
|
||||
if getattr(self, '_navigation_preparation_message', None) != bundle.displayName:
|
||||
self._navigation_preparation_message = bundle.displayName
|
||||
self._set_item_note(self.prepare_navigation_item, escape(bundle.displayName))
|
||||
ds = custom.ModelManagerSP.DownloadStatus
|
||||
self._downloading = bundle.status == ds.downloading
|
||||
stage = bundle.internalName
|
||||
self._verifying = stage in ('building', 'verifying')
|
||||
self.cancel_download_item.set_visible(ui_state.params.get("ModelManager_DownloadRef") is not None)
|
||||
state = self._download_row_state(progresses, bundle.displayName) if progresses else {"name": bundle.displayName}
|
||||
state["status_text"] = {
|
||||
'checking': tr("checking requirements"), 'downloading': tr("downloading"), 'building': tr("building"),
|
||||
'verifying': tr("verifying"), 'canceling': tr("canceling..."),
|
||||
}.get(stage, tr("preparing"))
|
||||
if bundle.status in (ds.failed, ds.downloaded):
|
||||
state["status_text"] = ""
|
||||
self.download_item.action_item.update(**state)
|
||||
if self._downloading:
|
||||
device._reset_interactive_timeout()
|
||||
return True
|
||||
|
||||
def _enable_map_turns(self, enabled):
|
||||
if enabled:
|
||||
ui_state.params.put_bool("NavigationEnabled", True)
|
||||
self.items = [self.small_model_item, self.big_model_item, self.cancel_download_item, self.download_item, self.refresh_item, self.clear_cache_item,
|
||||
self.lane_turn_desire_toggle, self.lane_turn_value_control, self.lagd_toggle, self.delay_control, self.camera_offset]
|
||||
|
||||
def _update_lagd_description(self, lagd_toggle: bool):
|
||||
desc = tr("Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. " +
|
||||
@@ -211,8 +153,6 @@ class ModelsLayout(Widget):
|
||||
|
||||
bundle = self.model_manager.selectedBundle if self.model_manager else None
|
||||
progresses = [model.artifact.downloadProgress for model in bundle.models if model.artifact.fileName] if bundle else []
|
||||
if self._handle_navigation_preparation(bundle, progresses):
|
||||
return
|
||||
if not progresses or bundle.status not in (custom.ModelManagerSP.DownloadStatus.downloading,
|
||||
custom.ModelManagerSP.DownloadStatus.failed):
|
||||
self.download_item.action_item.update(name="", segments=self._slot_segments())
|
||||
@@ -295,17 +235,13 @@ class ModelsLayout(Widget):
|
||||
progress = sum(p.progress for p in progresses) / len(progresses)
|
||||
ds = custom.ModelManagerSP.DownloadStatus
|
||||
|
||||
if any(p.canceling for p in progresses):
|
||||
return {"name": name, "downloading": True, "progress": progress, "status_text": tr("canceling...")}
|
||||
if ds.failed in statuses:
|
||||
# close.png is authored black and a tint cannot lift it, hence close2
|
||||
return {"name": name, "status_text": tr("download failed"), "text_color": rl.RED, "icon": "icons/close2.png"}
|
||||
if ds.verifying in statuses:
|
||||
return {"name": name, "downloading": True, "progress": progress, "status_text": tr("verifying")}
|
||||
if ds.downloading in statuses:
|
||||
speed = max((p.speed for p in progresses), default=0.0)
|
||||
status_text = f"{speed / 1024 ** 2:.1f} MB/s" if speed > 0 else "--.- MB/s"
|
||||
return {"name": name, "downloading": True, "progress": progress, "status_text": status_text}
|
||||
return {"name": name, "downloading": True, "progress": progress}
|
||||
if statuses <= {ds.downloaded, ds.cached}:
|
||||
return {"name": name, "text_color": ON_COLOR, "icon": "icons/checkmark.png"}
|
||||
# circled_slash is authored grey; tinting it again only darkens it
|
||||
@@ -382,21 +318,6 @@ class ModelsLayout(Widget):
|
||||
self.lane_turn_desire_toggle.action_item.set_state(turn_desire)
|
||||
self.lane_turn_value_control.set_visible(turn_desire and advanced_controls)
|
||||
self.lagd_toggle.action_item.set_state(live_delay)
|
||||
|
||||
blendful_allowed = ui_state.active_runner_tinygrad
|
||||
if blendful_allowed:
|
||||
self.blendful_toggle.action_item.set_state(ui_state.params.get_bool("Blendful"))
|
||||
self.blendful_toggle.action_item.set_enabled(True)
|
||||
else:
|
||||
self.blendful_toggle.action_item.set_state(False)
|
||||
self.blendful_toggle.action_item.set_enabled(False)
|
||||
|
||||
blendful_desc = self._blendful_desc if blendful_allowed else \
|
||||
f"<b>{tr('Select a non-Default model to use Blendful.')}</b><br><br>{self._blendful_desc}"
|
||||
if self.blendful_toggle.description != blendful_desc:
|
||||
self.blendful_toggle.set_description(blendful_desc)
|
||||
self.blendful_toggle.show_description(not blendful_allowed)
|
||||
|
||||
self.delay_control.set_visible(not live_delay and advanced_controls)
|
||||
new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100
|
||||
if self.lane_turn_value_control.action_item is not None and self.lane_turn_value_control.action_item.value_change_step != new_step:
|
||||
@@ -422,8 +343,6 @@ class ModelsLayout(Widget):
|
||||
offroad = ui_state.is_offroad()
|
||||
self.small_model_item.action_item.set_enabled(offroad)
|
||||
self.big_model_item.action_item.set_enabled(offroad)
|
||||
self.prepare_navigation_item.action_item.set_enabled(
|
||||
offroad and not self._downloading and not self._clearing and ui_state.params.get("ModelManager_DownloadRef") is None)
|
||||
self.small_model_item.set_description("" if offroad else tr("Only available when vehicle is off, or always offroad mode is on"))
|
||||
|
||||
# manager is offroad-only, so an onroad clear would never be serviced
|
||||
|
||||
@@ -11,12 +11,10 @@ from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
from opendbc.car.toyota.values import ToyotaFlags
|
||||
|
||||
|
||||
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
|
||||
SNG_HACK_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control must be available and enabled for your vehicle to use this feature.")
|
||||
PLATFORM_UNAVAILABLE = tr_noop("This feature is currently not available on this platform.")
|
||||
|
||||
DESCRIPTIONS = {
|
||||
'enforce_stock_longitudinal': tr_noop(
|
||||
@@ -25,11 +23,7 @@ DESCRIPTIONS = {
|
||||
'stop_and_go_hack': tr_noop(
|
||||
'sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. ' +
|
||||
'This feature is only applicable to certain models that are able to use longitudinal control. This is an alpha feature. Use at your own risk.'
|
||||
),
|
||||
'tss2_smooth': tr_noop(
|
||||
'Enable smoother longitudinal control for TSS2 vehicles. ' +
|
||||
'Uses a custom speed-dependent integral gain curve for smoother traffic driving.'
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,18 +47,9 @@ class ToyotaSettings(BrandSettings):
|
||||
enabled=lambda: not ui_state.engaged,
|
||||
)
|
||||
|
||||
self.tss2_smooth = toggle_item_sp(
|
||||
lambda: tr("TSS2 Smooth Longitudinal (Alpha)"),
|
||||
description=lambda: tr(DESCRIPTIONS["tss2_smooth"]),
|
||||
initial_state=ui_state.params.get_bool("ToyotaTSS2Smooth"),
|
||||
callback=self._on_enable_tss2_smooth,
|
||||
enabled=lambda: not ui_state.engaged,
|
||||
)
|
||||
|
||||
self.items = [
|
||||
self.enforce_stock_longitudinal,
|
||||
self.stop_and_go_hack,
|
||||
self.tss2_smooth,
|
||||
]
|
||||
|
||||
def _on_enable_enforce_stock_longitudinal(self, state: bool):
|
||||
@@ -76,8 +61,6 @@ class ToyotaSettings(BrandSettings):
|
||||
ui_state.params.put_bool("AlphaLongitudinalEnabled", False)
|
||||
ui_state.params.put_bool("ToyotaStopAndGoHack", False)
|
||||
self.stop_and_go_hack.action_item.set_state(False)
|
||||
ui_state.params.put_bool("ToyotaTSS2Smooth", False)
|
||||
self.tss2_smooth.action_item.set_state(False)
|
||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||
else:
|
||||
self.enforce_stock_longitudinal.action_item.set_state(False)
|
||||
@@ -92,25 +75,6 @@ class ToyotaSettings(BrandSettings):
|
||||
ui_state.params.put_bool("ToyotaEnforceStockLongitudinal", False)
|
||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def _on_enable_tss2_smooth(self, state: bool):
|
||||
if state:
|
||||
def confirm_callback(result: int):
|
||||
if result == DialogResult.CONFIRM:
|
||||
ui_state.params.put_bool("ToyotaTSS2Smooth", True)
|
||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||
else:
|
||||
self.tss2_smooth.action_item.set_state(False)
|
||||
|
||||
content = (f"<h1>{self.tss2_smooth.title}</h1><br>" +
|
||||
f"<p>{self.tss2_smooth.description}</p>")
|
||||
|
||||
dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
else:
|
||||
ui_state.params.put_bool("ToyotaTSS2Smooth", False)
|
||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def _on_enable_stop_and_go_hack(self, state: bool):
|
||||
if state:
|
||||
def confirm_callback(result: int):
|
||||
@@ -149,32 +113,9 @@ class ToyotaSettings(BrandSettings):
|
||||
self.stop_and_go_hack.set_description(new_desc)
|
||||
if show_desc:
|
||||
self.stop_and_go_hack.show_description(True)
|
||||
|
||||
tss2 = bool(ui_state.CP.flags & ToyotaFlags.TSS2)
|
||||
if longitudinal and not enforce_stock and tss2:
|
||||
self.tss2_smooth.action_item.set_enabled(not ui_state.engaged)
|
||||
new_desc = tr(DESCRIPTIONS["tss2_smooth"])
|
||||
show_desc = False
|
||||
else:
|
||||
self.tss2_smooth.action_item.set_enabled(False)
|
||||
self.tss2_smooth.action_item.set_state(False)
|
||||
unavailable = SNG_HACK_UNAVAILABLE if tss2 else PLATFORM_UNAVAILABLE
|
||||
new_desc = "<b>" + tr(unavailable) + "</b>\n\n" + tr(DESCRIPTIONS["tss2_smooth"])
|
||||
show_desc = True
|
||||
|
||||
if self.tss2_smooth.description != new_desc:
|
||||
self.tss2_smooth.set_description(new_desc)
|
||||
if show_desc:
|
||||
self.tss2_smooth.show_description(True)
|
||||
else:
|
||||
self.stop_and_go_hack.action_item.set_enabled(False)
|
||||
new_desc = "<b>" + tr(ONROAD_ONLY_DESCRIPTION) + "</b>\n\n" + tr(DESCRIPTIONS["stop_and_go_hack"])
|
||||
if self.stop_and_go_hack.description != new_desc:
|
||||
self.stop_and_go_hack.set_description(new_desc)
|
||||
self.stop_and_go_hack.show_description(True)
|
||||
|
||||
self.tss2_smooth.action_item.set_enabled(False)
|
||||
new_desc = "<b>" + tr(ONROAD_ONLY_DESCRIPTION) + "</b>\n\n" + tr(DESCRIPTIONS["tss2_smooth"])
|
||||
if self.tss2_smooth.description != new_desc:
|
||||
self.tss2_smooth.set_description(new_desc)
|
||||
self.tss2_smooth.show_description(True)
|
||||
|
||||
@@ -9,10 +9,9 @@ import time
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.cereal import custom
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog, SettingDescriptionDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog
|
||||
from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS, get_selected_bundle
|
||||
from openpilot.sunnypilot.models.navigation_model import BUILD_REF
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigParamControl
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.selfdrive.ui.sunnypilot.model_info import (active_source, big_model_state, bundles_for_source, carrying_model,
|
||||
default_model_name, model_cache_size_mb, model_info, queued_name,
|
||||
@@ -85,15 +84,6 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.select_model_btn = BigButton(tr("select model"))
|
||||
self.select_model_btn.set_click_callback(self._show_folders)
|
||||
|
||||
self.prepare_navigation_btn = BigButton(tr("prepare navigation model"),
|
||||
description=tr("Preparing the experimental navigation model requires a large download and an on-device build. Keep the device parked. " +
|
||||
"When ready, select it from Big Model; preparation does not change the active model."))
|
||||
self.prepare_navigation_btn.set_click_callback(self._prepare_navigation_model)
|
||||
self.preparation_details_btn = BigButton(tr("preparation details"))
|
||||
self.preparation_details_btn.set_click_callback(self._show_navigation_preparation_details)
|
||||
self.preparation_details_btn.set_visible(False)
|
||||
self._navigation_preparation_message = ""
|
||||
|
||||
self.refresh_btn = BigButton(tr("refresh models"))
|
||||
self.refresh_btn.set_click_callback(self._refresh_models)
|
||||
self._refresh_start: float | None = None
|
||||
@@ -105,62 +95,9 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.clear_cache_btn.set_click_callback(self._confirm_clear_cache)
|
||||
self._cache_size_time = 0.0
|
||||
|
||||
self.blendful_toggle = BigParamControl(tr("blendful (alpha)"), "Blendful",
|
||||
description=tr("Adds a gentle nudge toward lane center on top of the model's own steering, stronger when " +
|
||||
"lane lines are clear. Yields around obstacles. Not available with the Default model."))
|
||||
|
||||
self.map_turn_toggle = BigParamControl(tr("signal navigation (experimental)"), "MapboxNextTurnEnabled",
|
||||
toggle_callback=self._enable_map_turns,
|
||||
description=tr("Signals can prepare a short street route while normal lane-change checks remain available. " +
|
||||
"Navigation waits for an active lane change to complete. " +
|
||||
"Requires a navigation-capable model, internet and a Mapbox token. " +
|
||||
"Early fast turn signals and slow lane changes can be ambiguous; this is experimental."))
|
||||
|
||||
self.main_items = [self.current_model_info, self.preparation_details_btn, self.select_model_btn, self.prepare_navigation_btn, self.cancel_download_btn,
|
||||
self.refresh_btn, self.clear_cache_btn,
|
||||
self.blendful_toggle, self.map_turn_toggle]
|
||||
self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn, self.refresh_btn, self.clear_cache_btn]
|
||||
self._scroller.add_widgets(self.main_items)
|
||||
|
||||
def _prepare_navigation_model(self):
|
||||
if (ui_state.is_offroad() and ui_state.params.get("ModelManager_DownloadRef") is None and
|
||||
not ui_state.params.get_bool("ModelManager_ClearCache")):
|
||||
ui_state.params.put("ModelManager_DownloadRef", BUILD_REF)
|
||||
|
||||
def _show_navigation_preparation_details(self):
|
||||
if self._navigation_preparation_message:
|
||||
gui_app.push_widget(SettingDescriptionDialog(tr("Navigation model preparation"), self._navigation_preparation_message))
|
||||
|
||||
def _handle_navigation_preparation(self, bundle):
|
||||
self.preparation_details_btn.set_visible(bundle is not None and bundle.ref == BUILD_REF)
|
||||
if bundle is None or bundle.ref != BUILD_REF:
|
||||
self._navigation_preparation_message = ""
|
||||
return False
|
||||
self._navigation_preparation_message = bundle.displayName
|
||||
ds = custom.ModelManagerSP.DownloadStatus
|
||||
downloading = bundle.status == ds.downloading
|
||||
progresses = [model.artifact.downloadProgress for model in bundle.models if model.artifact.fileName]
|
||||
stage = bundle.internalName
|
||||
canceling = any(p.canceling for p in progresses)
|
||||
self.cancel_download_btn.set_visible(downloading and ui_state.params.get("ModelManager_DownloadRef") is not None)
|
||||
self.cancel_download_btn.set_text(tr("cancel preparation"))
|
||||
self.current_model_info.current_model_header.set_text(tr("navigation model preparation"))
|
||||
self.current_model_info.current_model_text.set_text(bundle.displayName)
|
||||
self.current_model_info.current_model_header._shimmer = downloading
|
||||
self.current_model_info.info_header.set_text(tr("progress") if downloading else tr("navigation model"))
|
||||
progress = sum(p.progress for p in progresses) / len(progresses) if progresses else 0.
|
||||
detail = {
|
||||
'checking': tr("checking requirements"), 'downloading': f"{progress:.1f}%", 'building': tr("building; keep the device parked"),
|
||||
'verifying': tr("verifying"), 'canceling': tr("canceling..."),
|
||||
}.get(stage, tr("preparing"))
|
||||
if canceling:
|
||||
detail = tr("canceling...")
|
||||
self.current_model_info.info_text.set_text(detail if downloading else bundle.displayName)
|
||||
return True
|
||||
|
||||
def _enable_map_turns(self, enabled):
|
||||
if enabled:
|
||||
ui_state.params.put_bool("NavigationEnabled", True)
|
||||
|
||||
@property
|
||||
def model_manager(self):
|
||||
return ui_state.sm["modelManagerSP"]
|
||||
@@ -268,31 +205,14 @@ class ModelsLayoutMici(NavScroller):
|
||||
device.set_override_interactive_timeout(None)
|
||||
self._was_downloading = False
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self.blendful_toggle.refresh()
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
self.select_model_btn.set_enabled(ui_state.is_offroad())
|
||||
self.map_turn_toggle.set_enabled(ui_state.is_offroad() and ui_state.active_runner_tinygrad)
|
||||
self.map_turn_toggle.set_checked(ui_state.params.get_bool("MapboxNextTurnEnabled"))
|
||||
self.cancel_download_btn.set_visible(False)
|
||||
self.current_model_info.current_model_header._shimmer = False
|
||||
self.current_model_info.info_header._shimmer = False
|
||||
|
||||
blendful_allowed = ui_state.active_runner_tinygrad
|
||||
if blendful_allowed:
|
||||
self.blendful_toggle.set_checked(ui_state.params.get_bool("Blendful"))
|
||||
self.blendful_toggle.set_enabled(True)
|
||||
else:
|
||||
self.blendful_toggle.set_checked(False)
|
||||
self.blendful_toggle.set_enabled(False)
|
||||
blendful_value = "" if blendful_allowed else tr("custom model only")
|
||||
if self.blendful_toggle.value != blendful_value:
|
||||
self.blendful_toggle.set_value(blendful_value)
|
||||
|
||||
manager = self.model_manager
|
||||
self._download_frame += 1
|
||||
should_update = self._download_frame % (gui_app.target_fps / 2) == 0
|
||||
@@ -308,8 +228,6 @@ class ModelsLayoutMici(NavScroller):
|
||||
# manager is offroad-only, so an onroad clear would never be serviced
|
||||
clearing = ui_state.params.get_bool("ModelManager_ClearCache")
|
||||
self.clear_cache_btn.set_enabled(ui_state.is_offroad() and not is_downloading and not clearing)
|
||||
self.prepare_navigation_btn.set_enabled(
|
||||
ui_state.is_offroad() and not is_downloading and not clearing and ui_state.params.get("ModelManager_DownloadRef") is None)
|
||||
if clearing:
|
||||
self.clear_cache_btn.set_value(tr("clearing..."))
|
||||
self._cache_size_time = 0.0 # refresh the size as soon as clearing finishes
|
||||
@@ -328,21 +246,16 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.current_model_info.info_header.set_text(info_header)
|
||||
self.current_model_info.info_text.set_text(info_text)
|
||||
|
||||
if self._handle_navigation_preparation(manager.selectedBundle):
|
||||
return
|
||||
|
||||
if manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.failed:
|
||||
self.current_model_info.info_header.set_text(tr("error") + self._download_progress)
|
||||
self.current_model_info.info_text.set_text(tr("download failed"))
|
||||
|
||||
elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
self.cancel_download_btn.set_visible(ui_state.params.get("ModelManager_DownloadRef") is not None)
|
||||
self.cancel_download_btn.set_visible(True)
|
||||
device.set_override_interactive_timeout(5)
|
||||
progress = 0.0
|
||||
count = 0
|
||||
verifying = False
|
||||
canceling = False
|
||||
speed = 0.0
|
||||
for model in manager.selectedBundle.models:
|
||||
count += 1
|
||||
p = model.artifact.downloadProgress
|
||||
@@ -350,14 +263,11 @@ class ModelsLayoutMici(NavScroller):
|
||||
custom.ModelManagerSP.DownloadStatus.verifying):
|
||||
progress += p.progress
|
||||
verifying = verifying or p.status == custom.ModelManagerSP.DownloadStatus.verifying
|
||||
canceling = canceling or p.canceling
|
||||
speed = max(speed, p.speed) # only one file downloads at a time, so the non-zero speed is the live one
|
||||
elif p.status in (custom.ModelManagerSP.DownloadStatus.downloaded,
|
||||
custom.ModelManagerSP.DownloadStatus.cached):
|
||||
progress += 100.0
|
||||
|
||||
header = tr("canceling...") if canceling else tr("verifying") if verifying else tr("downloading")
|
||||
self.current_model_info.current_model_header.set_text(header)
|
||||
self.current_model_info.current_model_header.set_text(tr("verifying") if verifying else tr("downloading"))
|
||||
self.cancel_download_btn.set_text(tr("cancel verification") if verifying else tr("cancel download"))
|
||||
self.current_model_info.current_model_header._shimmer = True
|
||||
name_text = manager.selectedBundle.internalName.lower()
|
||||
@@ -366,10 +276,7 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.current_model_info.current_model_text.set_text(name_text)
|
||||
self.current_model_info.info_header.set_text(tr("progress") + self._download_progress)
|
||||
self.current_model_info.info_header._shimmer = True
|
||||
progress_text = f"{progress/count:.2f}%"
|
||||
if not verifying and not canceling:
|
||||
progress_text += f" | {speed / 1024 ** 2:.1f} MB/s" if speed > 0 else " | --.- MB/s"
|
||||
self.current_model_info.info_text.set_text(progress_text)
|
||||
self.current_model_info.info_text.set_text(f"{progress/count:.2f}%")
|
||||
|
||||
elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloaded:
|
||||
self.current_model_info.info_header.set_text(tr("downloaded"))
|
||||
|
||||
@@ -6,7 +6,6 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.ui_state import UIStatus
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.blendful import BlendfulRenderer
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath
|
||||
|
||||
LANE_LINE_COLORS_SP = {
|
||||
@@ -18,4 +17,3 @@ LANE_LINE_COLORS_SP = {
|
||||
class ModelRendererSP:
|
||||
def __init__(self):
|
||||
self.rainbow_path = RainbowPath()
|
||||
self.blendful = BlendfulRenderer(rect_relative=True)
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP
|
||||
from openpilot.sunnypilot.modeld_v2.blendful import LANE_WIDTH_CLIP, LOOKAHEAD_TIME, MIN_LOOKAHEAD
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
|
||||
BLENDFUL_COLOR = (0, 200, 255)
|
||||
FULL_BAR = 1.0
|
||||
|
||||
|
||||
class BlendfulRenderer:
|
||||
def __init__(self, readout: bool = True, rect_relative: bool = False):
|
||||
self._readout = readout
|
||||
self._rect_relative = rect_relative
|
||||
self._center_points = np.empty((0, 2), dtype=np.float32)
|
||||
self._trust = 0.0
|
||||
self._font = gui_app.font(FontWeight.BOLD)
|
||||
self._pull_display = FirstOrderFilter(0.0, 0.5, DT_MDL)
|
||||
self._trust_display = FirstOrderFilter(0.0, 0.5, DT_MDL)
|
||||
self._alpha = FirstOrderFilter(0.0, 0.3, DT_MDL)
|
||||
|
||||
def update(self, renderer, max_idx: int, *map_args) -> None:
|
||||
blendful = ui_state.sm['modelDataV2SP']
|
||||
if ui_state.sm.updated['modelDataV2SP']:
|
||||
self._alpha.update(float(blendful.blendfulTrust > 0.0))
|
||||
if blendful.blendfulTrust > 0.0:
|
||||
self._trust = blendful.blendfulTrust
|
||||
if self._readout:
|
||||
lookahead = max(ui_state.sm['carState'].vEgo * LOOKAHEAD_TIME, MIN_LOOKAHEAD)
|
||||
self._pull_display.update(blendful.blendfulCurvature * lookahead ** 2 / 2)
|
||||
self._trust_display.update(self._trust)
|
||||
|
||||
left, right = renderer._lane_lines[1].raw_points, renderer._lane_lines[2].raw_points
|
||||
stds = list(ui_state.sm['modelV2'].laneLineStds)
|
||||
if self._alpha.x < 0.01 or len(left) == 0 or len(right) != len(left) or len(stds) < 3:
|
||||
self._center_points = np.empty((0, 2), dtype=np.float32)
|
||||
return
|
||||
|
||||
l_prob, r_prob = blendful.blendfulLeftProb, blendful.blendfulRightProb
|
||||
if blendful.blendfulTrust > 0.0 and l_prob + r_prob > 0.0:
|
||||
half_width = np.clip(right[:, 1] - left[:, 1], *LANE_WIDTH_CLIP) / 2 if l_prob * r_prob > 0.5 else blendful.blendfulLaneWidth / 2
|
||||
l_var = stds[1] ** 2 / max(l_prob, 1e-4)
|
||||
r_var = stds[2] ** 2 / max(r_prob, 1e-4)
|
||||
center = left.copy()
|
||||
center[:, 1] = (r_var * (left[:, 1] + half_width) + l_var * (right[:, 1] - half_width)) / (l_var + r_var)
|
||||
self._center_points = renderer._map_line_to_polygon(center, 0.15, 0.0, max_idx, *map_args)
|
||||
|
||||
def _color(self, rgb, alpha: float) -> rl.Color:
|
||||
return rl.Color(*rgb, int(alpha * self._alpha.x))
|
||||
|
||||
def draw(self, rect: rl.Rectangle) -> None:
|
||||
if not UIStateSP.lateral_active(ui_state):
|
||||
return
|
||||
if self._center_points.size and self._trust > 0.05:
|
||||
points = self._center_points + (rect.x, rect.y) if self._rect_relative else self._center_points
|
||||
draw_polygon(rect, points, self._color(BLENDFUL_COLOR, np.clip(self._trust, 0.2, 1.0) * 255))
|
||||
if self._center_points.size and self._readout:
|
||||
self._draw_readout(rect)
|
||||
|
||||
def _draw_readout(self, rect: rl.Rectangle) -> None:
|
||||
pull, trust = self._pull_display.x, self._trust_display.x
|
||||
w, h = 190, 76
|
||||
x, y = rect.x + (rect.width - w) / 2, rect.y + 10
|
||||
cx, cy = x + w / 2, y + 30
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(x, y, w, h), 0.5, 12, self._color((0, 0, 0), 170))
|
||||
|
||||
text = f"{abs(pull) * 100:.0f}"
|
||||
text_w = measure_text_cached(self._font, text, 44, 0).x
|
||||
rl.draw_text_ex(self._font, text, rl.Vector2(cx - text_w / 2, cy - 24), 44, 0, self._color((255, 255, 255), 255))
|
||||
|
||||
active = self._color(BLENDFUL_COLOR, np.clip(abs(pull) / 0.1, 0.0, 1.0) * 255)
|
||||
idle = self._color((255, 255, 255), 40)
|
||||
lx, rx = x + 18, x + w - 18
|
||||
rl.draw_triangle(rl.Vector2(lx, cy), rl.Vector2(lx + 16, cy + 12), rl.Vector2(lx + 16, cy - 12), active if pull < 0 else idle)
|
||||
rl.draw_triangle(rl.Vector2(rx - 16, cy - 12), rl.Vector2(rx - 16, cy + 12), rl.Vector2(rx, cy), active if pull > 0 else idle)
|
||||
|
||||
bar = rl.Rectangle(x + 24, y + h - 16, w - 48, 6)
|
||||
rl.draw_rectangle_rounded(bar, 1.0, 6, self._color((255, 255, 255), 40))
|
||||
rl.draw_rectangle(int(cx - 1), int(bar.y - 2), 2, int(bar.height + 4), self._color((255, 255, 255), 120))
|
||||
fill = bar.width / 2 * float(np.sqrt(np.clip(abs(pull) / FULL_BAR, 0.0, 1.0)))
|
||||
if fill > 1:
|
||||
fill_x = cx if pull > 0 else cx - fill
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(fill_x, bar.y, fill, bar.height), 1.0, 6,
|
||||
self._color(BLENDFUL_COLOR, np.clip(0.3 + 0.7 * trust, 0.0, 1.0) * 255))
|
||||
@@ -5,11 +5,10 @@ This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.blendful import BlendfulRenderer
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.chevron_metrics import ChevronMetrics
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath
|
||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import MADSState
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
|
||||
|
||||
@@ -17,12 +16,16 @@ class ModelRendererSP:
|
||||
def __init__(self):
|
||||
self.rainbow_path = RainbowPath()
|
||||
self.chevron_metrics = ChevronMetrics()
|
||||
self.blendful = BlendfulRenderer(readout=False)
|
||||
self._width_filter = FirstOrderFilter(0.9, 0.1, 1 / gui_app.target_fps)
|
||||
|
||||
@property
|
||||
def _lateral_active(self) -> bool:
|
||||
return UIStateSP.lateral_active(ui_state)
|
||||
sm = ui_state.sm
|
||||
if sm.valid["selfdriveStateSP"]:
|
||||
mads = sm["selfdriveStateSP"].mads
|
||||
if mads.available:
|
||||
return mads.enabled and mads.state != MADSState.paused
|
||||
return ui_state.status in (UIStatus.ENGAGED, UIStatus.LAT_ONLY)
|
||||
|
||||
def _get_path_half_width(self) -> float:
|
||||
target = 0.9 if self._lateral_active else 0.40
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
"""Execute actual UI action/status methods headlessly; no renderer or download."""
|
||||
import ast
|
||||
from html import escape
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace as NS
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BUILD_REF = 'local-prepare-rl-navigation-80eedcdd'
|
||||
DS = NS(downloading=1, verifying=2, failed=3, downloaded=4, cached=5)
|
||||
SOURCES = ((ROOT/'layouts/settings/models.py', 'ModelsLayout'), (ROOT/'mici/layouts/models.py', 'ModelsLayoutMici'))
|
||||
|
||||
|
||||
def load_methods(path, name):
|
||||
tree = ast.parse(path.read_text())
|
||||
cls = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == name)
|
||||
names = {'_prepare_navigation_model', '_handle_navigation_preparation', '_download_row_state', '_set_item_note', '_show_navigation_preparation_details'}
|
||||
cls.body = [n for n in cls.body if isinstance(n, ast.FunctionDef) and n.name in names]
|
||||
cls.bases = []
|
||||
ui = NS(is_offroad=Mock(return_value=True), params=Mock())
|
||||
ui.params.get.return_value = None
|
||||
ui.params.get_bool.return_value = False
|
||||
env = {'BUILD_REF': BUILD_REF, 'ui_state': ui, 'custom': NS(ModelManagerSP=NS(DownloadStatus=DS)),
|
||||
'escape': escape, 'gui_app': Mock(), 'SettingDescriptionDialog': lambda title, message: NS(title=title, message=message),
|
||||
'device': Mock(), 'tr': lambda s: s, 'ON_COLOR': 'green', 'rl': NS(RED='red', GRAY='gray', WHITE='white')}
|
||||
exec(compile(ast.Module(body=[cls], type_ignores=[]), str(path), 'exec'), env)
|
||||
obj = env[name]()
|
||||
obj.prepare_navigation_item = Mock()
|
||||
obj.preparation_details_btn = Mock()
|
||||
return obj, ui
|
||||
|
||||
|
||||
def bundle(status, *, ref=BUILD_REF, text='Preparing Navigation Model: Downloading…', progress_status=None, stage='building'):
|
||||
p = NS(status=progress_status or status, progress=37., canceling=False, speed=2048.)
|
||||
return NS(ref=ref, status=status, displayName=text, internalName=stage,
|
||||
models=[NS(artifact=NS(fileName='navigation-model', downloadProgress=p))])
|
||||
|
||||
|
||||
class NavigationPreparationUiTests(unittest.TestCase):
|
||||
def test_action_queues_special_ref_without_selecting_model(self):
|
||||
for path, name in SOURCES:
|
||||
with self.subTest(layout=name):
|
||||
obj, ui = load_methods(path, name)
|
||||
obj._prepare_navigation_model()
|
||||
ui.params.put.assert_called_once_with('ModelManager_DownloadRef', BUILD_REF)
|
||||
ui.params.put_bool.assert_not_called()
|
||||
ui.params.remove.assert_not_called()
|
||||
|
||||
def test_action_rejects_onroad_busy_or_cache_clear(self):
|
||||
for path, name in SOURCES:
|
||||
for offroad, queued, clearing in ((False, None, False), (True, 'other', False), (True, None, True)):
|
||||
with self.subTest(layout=name, offroad=offroad, queued=queued, clearing=clearing):
|
||||
obj, ui = load_methods(path, name)
|
||||
ui.is_offroad.return_value = offroad
|
||||
ui.params.get.return_value = queued
|
||||
ui.params.get_bool.return_value = clearing
|
||||
obj._prepare_navigation_model()
|
||||
ui.params.put.assert_not_called()
|
||||
|
||||
def test_tici_keeps_full_failure_and_success_status(self):
|
||||
for status, text in ((DS.failed, 'Preparation failed: LLVM 18+ required'),
|
||||
(DS.downloaded, 'Navigation Model ready — select it below')):
|
||||
obj, _ = load_methods(*SOURCES[0])
|
||||
obj.cancel_download_item, obj.download_item = Mock(), Mock()
|
||||
b = bundle(status, text=text)
|
||||
self.assertTrue(obj._handle_navigation_preparation(b, [b.models[0].artifact.downloadProgress]))
|
||||
state = obj.download_item.action_item.update.call_args.kwargs
|
||||
self.assertEqual(state['name'], text)
|
||||
self.assertEqual(state['status_text'], '')
|
||||
self.assertFalse(obj._downloading)
|
||||
|
||||
def test_tici_compile_progress_and_cancel(self):
|
||||
obj, ui = load_methods(*SOURCES[0])
|
||||
ui.params.get.return_value = BUILD_REF
|
||||
obj.cancel_download_item, obj.download_item = Mock(), Mock()
|
||||
b = bundle(DS.downloading, progress_status=DS.verifying, text='Building Navigation Model…')
|
||||
obj._handle_navigation_preparation(b, [b.models[0].artifact.downloadProgress])
|
||||
state = obj.download_item.action_item.update.call_args.kwargs
|
||||
self.assertEqual(state['name'], b.displayName)
|
||||
self.assertEqual(state['status_text'], 'building')
|
||||
self.assertTrue(obj._downloading and obj._verifying)
|
||||
obj.cancel_download_item.set_visible.assert_called_with(True)
|
||||
|
||||
def test_mici_keeps_full_status_and_offers_cancel_during_build(self):
|
||||
for status in (DS.downloading, DS.failed, DS.downloaded):
|
||||
obj, ui = load_methods(*SOURCES[1])
|
||||
ui.params.get.return_value = BUILD_REF if status == DS.downloading else None
|
||||
obj.cancel_download_btn, obj.current_model_info = Mock(), Mock()
|
||||
b = bundle(status, text='Full preparation stage or error', progress_status=DS.verifying)
|
||||
self.assertTrue(obj._handle_navigation_preparation(b))
|
||||
obj.current_model_info.current_model_text.set_text.assert_called_with(b.displayName)
|
||||
obj.cancel_download_btn.set_visible.assert_called_with(status == DS.downloading)
|
||||
if status != DS.downloading:
|
||||
obj.current_model_info.info_text.set_text.assert_called_with(b.displayName)
|
||||
else:
|
||||
self.assertIn('keep the device parked', obj.current_model_info.info_text.set_text.call_args.args[0])
|
||||
|
||||
def test_checking_is_not_reported_as_building(self):
|
||||
for source in SOURCES:
|
||||
obj, _ = load_methods(*source)
|
||||
obj.cancel_download_item, obj.download_item = Mock(), Mock()
|
||||
obj.cancel_download_btn, obj.current_model_info = Mock(), Mock()
|
||||
b = bundle(DS.downloading, progress_status=DS.verifying, stage='checking',
|
||||
text='Checking navigation model preparation requirements')
|
||||
if source == SOURCES[0]:
|
||||
obj._handle_navigation_preparation(b, [b.models[0].artifact.downloadProgress])
|
||||
self.assertEqual(obj.download_item.action_item.update.call_args.kwargs['status_text'], 'checking requirements')
|
||||
self.assertFalse(obj._verifying)
|
||||
else:
|
||||
obj._handle_navigation_preparation(b)
|
||||
self.assertEqual(obj.current_model_info.info_text.set_text.call_args.args[0], 'checking requirements')
|
||||
|
||||
def test_full_long_error_remains_readable(self):
|
||||
message = 'Preparation failed: missing LLVM <18> dependency. ' + 'Detailed diagnostic text. ' * 35
|
||||
for source in SOURCES:
|
||||
obj, _ = load_methods(*source)
|
||||
obj.cancel_download_item, obj.download_item = Mock(), Mock()
|
||||
obj.cancel_download_btn, obj.current_model_info = Mock(), Mock()
|
||||
b = bundle(DS.failed, stage='failed', text=message)
|
||||
if source == SOURCES[0]:
|
||||
obj._handle_navigation_preparation(b, [b.models[0].artifact.downloadProgress])
|
||||
obj.prepare_navigation_item.set_description.assert_called_with(escape(message))
|
||||
obj.prepare_navigation_item.show_description.assert_called_with(True)
|
||||
else:
|
||||
obj._handle_navigation_preparation(b)
|
||||
obj.preparation_details_btn.set_visible.assert_called_with(True)
|
||||
obj._show_navigation_preparation_details()
|
||||
dialog = obj._show_navigation_preparation_details.__globals__['gui_app'].push_widget.call_args.args[0]
|
||||
self.assertEqual(dialog.message, message)
|
||||
|
||||
def test_verification_and_unknown_stage_never_claim_building(self):
|
||||
for stage, expected in (('verifying', 'verifying'), ('unexpected', 'preparing')):
|
||||
for source in SOURCES:
|
||||
obj, _ = load_methods(*source)
|
||||
obj.cancel_download_item, obj.download_item = Mock(), Mock()
|
||||
obj.cancel_download_btn, obj.current_model_info = Mock(), Mock()
|
||||
b = bundle(DS.downloading, progress_status=DS.verifying, stage=stage)
|
||||
if source == SOURCES[0]:
|
||||
obj._handle_navigation_preparation(b, [b.models[0].artifact.downloadProgress])
|
||||
self.assertEqual(obj.download_item.action_item.update.call_args.kwargs['status_text'], expected)
|
||||
else:
|
||||
obj._handle_navigation_preparation(b)
|
||||
self.assertEqual(obj.current_model_info.info_text.set_text.call_args.args[0], expected)
|
||||
|
||||
def test_unmeasured_eta_removed_and_details_clear_with_normal_bundle(self):
|
||||
for path, _ in SOURCES:
|
||||
self.assertNotIn('several minutes', path.read_text())
|
||||
obj, _ = load_methods(*SOURCES[1])
|
||||
obj.cancel_download_btn, obj.current_model_info = Mock(), Mock()
|
||||
obj._handle_navigation_preparation(bundle(DS.failed, stage='failed'))
|
||||
obj._handle_navigation_preparation(bundle(DS.downloading, ref='normal-model'))
|
||||
obj.preparation_details_btn.set_visible.assert_called_with(False)
|
||||
self.assertEqual(obj._navigation_preparation_message, '')
|
||||
|
||||
def test_normal_bundle_status_follows_original_path(self):
|
||||
for path, name in SOURCES:
|
||||
obj, _ = load_methods(path, name)
|
||||
args = (bundle(DS.downloading, ref='normal-model'), []) if name == 'ModelsLayout' else (bundle(DS.downloading, ref='normal-model'),)
|
||||
self.assertFalse(obj._handle_navigation_preparation(*args))
|
||||
|
||||
def test_both_visible_actions_use_exported_ref_and_onroad_disable(self):
|
||||
for path, _ in SOURCES:
|
||||
source = path.read_text()
|
||||
self.assertIn('from openpilot.sunnypilot.models.navigation_model import BUILD_REF', source)
|
||||
self.assertIn('self._prepare_navigation_model)', source)
|
||||
tree = ast.parse(source)
|
||||
attributes = {n.attr for n in ast.walk(tree) if isinstance(n, ast.Attribute)}
|
||||
self.assertTrue('prepare_navigation_item' in attributes or 'prepare_navigation_btn' in attributes)
|
||||
self.assertIn('offroad and not self._downloading', source) if 'settings/models' in str(path) else self.assertIn(
|
||||
'ui_state.is_offroad() and not is_downloading', source)
|
||||
|
||||
def test_cancel_buttons_remove_existing_request_key(self):
|
||||
for path, _ in SOURCES:
|
||||
source = path.read_text()
|
||||
self.assertIn('ui_state.params.remove("ModelManager_DownloadRef")', source)
|
||||
self.assertNotIn('params.put("ModelManager_ActiveBundle', source)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -36,7 +36,7 @@ class UIStateSP:
|
||||
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
|
||||
self.sm_services_ext = [
|
||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||
"lateralTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "lateralDelay", "modelDataV2SP"
|
||||
"lateralTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "lateralDelay"
|
||||
]
|
||||
|
||||
self.sunnylink_state = SunnylinkState()
|
||||
@@ -46,7 +46,6 @@ class UIStateSP:
|
||||
|
||||
self.active_bundle = None
|
||||
self.model_runner_tinygrad: bool = False
|
||||
self.active_runner_tinygrad: bool = False
|
||||
self.blindspot: bool = False
|
||||
self.chevron_metrics = None
|
||||
self.custom_interactive_timeout: int = 0
|
||||
@@ -110,14 +109,6 @@ class UIStateSP:
|
||||
def auto_onroad_brightness(self) -> bool:
|
||||
return self.onroad_brightness in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK)
|
||||
|
||||
@staticmethod
|
||||
def lateral_active(_ui_state) -> bool:
|
||||
if _ui_state.sm.valid["selfdriveStateSP"]:
|
||||
mads = _ui_state.sm["selfdriveStateSP"].mads
|
||||
if mads.available:
|
||||
return mads.enabled and mads.state != MADSState.paused
|
||||
return _ui_state.status.value in ("engaged", "lat_only")
|
||||
|
||||
@staticmethod
|
||||
def update_status(ss, ss_sp, onroad_evt) -> str:
|
||||
state = ss.state
|
||||
@@ -166,8 +157,6 @@ class UIStateSP:
|
||||
chestnut_loading=self.chestnut_loading, offroad=self.is_offroad())
|
||||
self.active_bundle = self.params.get(ACTIVE_BUNDLE_KEYS[source])
|
||||
self.model_runner_tinygrad = self.active_bundle is not None and self.active_bundle.get("runner") == "tinygrad"
|
||||
# runner the manager launches (process_config.is_tinygrad_model); with a chestnut the big slot decides
|
||||
self.active_runner_tinygrad = self.params.get("ModelRunnerTypeCache") == custom.ModelManagerSP.Runner.tinygrad
|
||||
# stock only counts the default big model's compiled pkl. a downloaded big bundle runs on the
|
||||
# chestnut just the same, so ChestnutState has to see it as available too.
|
||||
self.chestnut_compiled = self.chestnut_compiled or self.model_runner_tinygrad
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define SUNNYPILOT_VERSION "2026.11.01-4997"
|
||||
#define SUNNYPILOT_VERSION "2026.10.18-4943"
|
||||
|
||||
@@ -3,3 +3,7 @@ from openpilot.common.basedir import BASEDIR
|
||||
|
||||
MAPD_BIN_DIR = os.path.join(BASEDIR, 'openpilot', 'third_party/mapd_pfeiferj')
|
||||
MAPD_PATH = os.path.join(MAPD_BIN_DIR, 'mapd')
|
||||
|
||||
# mapd still runs (OSM downloads) but has no position source since ubloxd owns LastGPSPosition;
|
||||
# map-based features stay off until mapd is rebuilt
|
||||
MAPD_AVAILABLE = False
|
||||
|
||||
@@ -4,7 +4,6 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import platform
|
||||
|
||||
@@ -27,21 +26,6 @@ class OsmMapData(BaseMapData):
|
||||
self.last_bearing = math.degrees(location.calibratedOrientationNED.value[2])
|
||||
self.last_position = Coordinate(location.positionGeodetic.value[0], location.positionGeodetic.value[1])
|
||||
|
||||
if self.last_position is None:
|
||||
return
|
||||
|
||||
params = {
|
||||
"latitude": self.last_position.latitude,
|
||||
"longitude": self.last_position.longitude,
|
||||
}
|
||||
|
||||
if self.last_bearing is not None:
|
||||
params['bearing'] = self.last_bearing
|
||||
|
||||
# mapd reads its position from the memory params as JSON, so write it for the mapd binary.
|
||||
# LastGPSPosition is registered as BYTES (ubloxd owns the persistent copy for pigeond),
|
||||
# so the JSON payload has to be encoded here.
|
||||
self.mem_params.put("LastGPSPosition", json.dumps(params).encode(), block=True)
|
||||
|
||||
def get_current_speed_limit(self) -> float:
|
||||
return float(self.mem_params.get("MapSpeedLimit") or 0.0)
|
||||
|
||||
@@ -121,10 +121,6 @@ def update_osm_db() -> None:
|
||||
if not mem_params.get("OSMDownloadBounds"):
|
||||
mem_params.put("OSMDownloadBounds", "", block=True)
|
||||
|
||||
# mapd reads LastGPSPosition from the memory params as JSON, and it is registered as BYTES upstream
|
||||
if not mem_params.get("LastGPSPosition"):
|
||||
mem_params.put("LastGPSPosition", b"{}", block=True)
|
||||
|
||||
|
||||
def main_thread():
|
||||
update_installed_version(VERSION, params)
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import platform
|
||||
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.cereal import log
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.sunnypilot.mapd.live_map_data.osm_map_data import OsmMapData
|
||||
|
||||
|
||||
class TestOsmMapData(OpenpilotTestCase):
|
||||
"""The mapd binary reads its position from the memory params as JSON, so mapd_manager has to feed it."""
|
||||
|
||||
LATITUDE = 32.875134
|
||||
LONGITUDE = -117.209777
|
||||
ALTITUDE = 42.0
|
||||
BEARING = 90.0
|
||||
|
||||
def setup_method(self):
|
||||
self.mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else Params()
|
||||
self.mem_params.remove("LastGPSPosition")
|
||||
self.map_data = OsmMapData()
|
||||
self.pm = messaging.PubMaster(['liveLocationKalman'])
|
||||
|
||||
def location_message(self, status: log.LiveLocationKalman.Status, position_valid: bool):
|
||||
msg = messaging.new_message('liveLocationKalman', valid=True)
|
||||
location = msg.liveLocationKalman
|
||||
location.status = status
|
||||
location.positionGeodetic = {'value': [self.LATITUDE, self.LONGITUDE, self.ALTITUDE], 'valid': position_valid}
|
||||
location.calibratedOrientationNED = {'value': [0.0, 0.0, math.radians(self.BEARING)], 'valid': True}
|
||||
return msg
|
||||
|
||||
def send_location(self, status: log.LiveLocationKalman.Status, position_valid: bool) -> None:
|
||||
# the first exchange synchronizes the PUB/SUB connection, the second one is actually received
|
||||
self.pm.send('liveLocationKalman', self.location_message(status, position_valid))
|
||||
self.map_data.sm.update(1000)
|
||||
self.pm.send('liveLocationKalman', self.location_message(status, position_valid))
|
||||
self.map_data.sm.update(1000)
|
||||
|
||||
def read_position(self) -> dict:
|
||||
value = self.mem_params.get("LastGPSPosition")
|
||||
self.assertIsNotNone(value, "LastGPSPosition was not written for mapd")
|
||||
return json.loads(value)
|
||||
|
||||
def test_writes_position_for_mapd(self):
|
||||
self.send_location(log.LiveLocationKalman.Status.valid, True)
|
||||
self.map_data.tick()
|
||||
|
||||
position = self.read_position()
|
||||
self.assertAlmostEqual(position['latitude'], self.LATITUDE, delta=1e-6)
|
||||
self.assertAlmostEqual(position['longitude'], self.LONGITUDE, delta=1e-6)
|
||||
self.assertAlmostEqual(position['bearing'], self.BEARING, delta=1e-6)
|
||||
|
||||
def test_invalid_localizer_keeps_last_known_position(self):
|
||||
self.send_location(log.LiveLocationKalman.Status.valid, True)
|
||||
self.map_data.tick()
|
||||
|
||||
self.send_location(log.LiveLocationKalman.Status.uninitialized, False)
|
||||
self.map_data.tick()
|
||||
|
||||
position = self.read_position()
|
||||
self.assertAlmostEqual(position['latitude'], self.LATITUDE, delta=1e-6)
|
||||
self.assertAlmostEqual(position['longitude'], self.LONGITUDE, delta=1e-6)
|
||||
@@ -1,145 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
|
||||
|
||||
LANE_WIDTH_DEFAULT = 3.6 # m
|
||||
LANE_WIDTH_CLIP = (2.8, 4.5) # m
|
||||
LOOKAHEAD_TIME = 1.5 # s
|
||||
MIN_LOOKAHEAD = 10.0 # m
|
||||
MIN_SPEED = 5.0 # m/s
|
||||
LINE_STD = (0.2, 0.6)
|
||||
POINT_STD = 0.1
|
||||
YIELD = (0.2, 0.45)
|
||||
X_IDXS = np.array(ModelConstants.X_IDXS)
|
||||
|
||||
|
||||
class Blendful:
|
||||
def __init__(self):
|
||||
self.lane_width = FirstOrderFilter(LANE_WIDTH_DEFAULT, 10.0, DT_MDL, initialized=False)
|
||||
self.line_gone = FirstOrderFilter(np.zeros(2), 2.0, DT_MDL)
|
||||
self.line_probs_slow = FirstOrderFilter(np.zeros(2), 3.0, DT_MDL)
|
||||
self.line_probs = FirstOrderFilter(np.zeros(2), 0.5, DT_MDL)
|
||||
self.usual_offset = FirstOrderFilter(0.0, 10.0, DT_MDL, initialized=False)
|
||||
self.going_around = False
|
||||
self.since_turning = 0.0
|
||||
self.trust = 0.0
|
||||
self.line_trust = 0.0
|
||||
self.dk = 0.0 # delta kappa; difference between lane center and model plan
|
||||
self.l_prob = 0.0
|
||||
self.r_prob = 0.0
|
||||
self.model_curvature = 0.0
|
||||
self.smooth = 0.0 # smooths the correction target below .53 lat accel
|
||||
self.curvature = 0.0
|
||||
|
||||
def update(self, model_output: dict[str, np.ndarray], v_ego: float, active: bool, model_curvature: float, enabled: bool = True) -> float:
|
||||
self.l_prob = self.r_prob = 0.0
|
||||
self.model_curvature = model_curvature
|
||||
if not enabled:
|
||||
if self.curvature or self.trust:
|
||||
self.__init__()
|
||||
return 0.0
|
||||
fading = not active or v_ego < MIN_SPEED
|
||||
if fading:
|
||||
self.trust = self.line_trust = 0.0
|
||||
self.usual_offset.initialized = False
|
||||
self.lane_width.initialized = False
|
||||
self.going_around = False
|
||||
target = 0.0 if fading else self._target(model_output, v_ego, model_curvature)
|
||||
target = self.smooth = self.smooth + (target - self.smooth) * DT_MDL / (DT_MDL + np.interp(abs(model_curvature) * v_ego ** 2, [0.18, 0.53], [0.5, 0.0]))
|
||||
build = 0.5 / max(v_ego, 1.0) ** 2 * DT_MDL # jerk over 1 second
|
||||
release = (0.5 if fading else 2.0) / max(v_ego, 1.0) ** 2 * DT_MDL # lets go fast, but past zero it builds like any other pull
|
||||
k = self.curvature
|
||||
self.curvature = float(np.clip(target, max(k - release, -build) if k > 0 else k - build, min(k + release, build) if k < 0 else k + build))
|
||||
return self.curvature
|
||||
|
||||
def _target(self, model_output: dict[str, np.ndarray], v_ego: float, model_curvature: float) -> float:
|
||||
lookahead = max(v_ego * LOOKAHEAD_TIME, MIN_LOOKAHEAD)
|
||||
lines = model_output['lane_lines'][0, :, :, 0]
|
||||
stds = model_output['lane_lines_stds'][0, :, :, 0]
|
||||
|
||||
lone_weight = np.interp(abs(model_curvature) * v_ego ** 2, [0.18, 0.53], [0.5, 1.0]) # 0.5 on straights, 1.0 in curves
|
||||
probs = self.line_probs.update(model_output['lane_lines_prob'][0, 1::2][1:3])
|
||||
absent = (probs < 0.3) & (lone_weight > 0.5)
|
||||
gone = self.line_gone.update((model_output['lane_lines_prob'][0, 1::2][1:3] < 0.3).astype(float))
|
||||
missing = np.where(absent, 1.0, gone)
|
||||
dropping = bool(np.any((probs < 0.6 * self.line_probs_slow.update(model_output['lane_lines_prob'][0, 1::2][1:3])) & ~absent))
|
||||
l_prob = float(probs[0] * np.interp(np.interp(0.5 * lookahead, X_IDXS, stds[1]), LINE_STD, [1.0, 0.0]))
|
||||
r_prob = float(probs[1] * np.interp(np.interp(0.5 * lookahead, X_IDXS, stds[2]), LINE_STD, [1.0, 0.0]))
|
||||
self.l_prob, self.r_prob = l_prob, r_prob
|
||||
# one line steadily on its own still centers at lone_weight. a line that is disappearing gives control back to laneless
|
||||
if missing[1] > 0.8 and l_prob > 0.5:
|
||||
lane_trust = l_prob * lone_weight
|
||||
elif missing[0] > 0.8 and r_prob > 0.5:
|
||||
lane_trust = r_prob * lone_weight
|
||||
elif dropping:
|
||||
lane_trust = min(l_prob, r_prob)
|
||||
else:
|
||||
lane_trust = l_prob + r_prob - l_prob * r_prob
|
||||
if lone_weight > 0.5 and lane_trust < self.line_trust:
|
||||
lane_trust = self.line_trust + (lane_trust - self.line_trust) * DT_MDL / (lone_weight - 0.5 + DT_MDL)
|
||||
self.line_trust = lane_trust
|
||||
if lane_trust <= 0.0:
|
||||
self.trust = 0.0
|
||||
return 0.0
|
||||
if l_prob + r_prob <= 0.0: # keep the last fit while trust lets go
|
||||
self.trust = min(self.trust, lane_trust)
|
||||
return float(self.trust * self.dk)
|
||||
|
||||
win = (X_IDXS >= 0.5 * lookahead) & (X_IDXS <= 1.5 * lookahead)
|
||||
x = X_IDXS[win]
|
||||
half_width = self.lane_width.x / 2
|
||||
if l_prob * r_prob > 0.5:
|
||||
width = np.interp(lookahead, X_IDXS, lines[2]) - np.interp(lookahead, X_IDXS, lines[1])
|
||||
self.lane_width.update(float(np.clip(width, *LANE_WIDTH_CLIP)))
|
||||
half_width = np.clip(lines[2, win] - lines[1, win], *LANE_WIDTH_CLIP) / 2
|
||||
l_var = stds[1, win] ** 2 / max(l_prob, 1e-4)
|
||||
r_var = stds[2, win] ** 2 / max(r_prob, 1e-4)
|
||||
center = (r_var * (lines[1, win] + half_width) + l_var * (lines[2, win] - half_width)) / (l_var + r_var)
|
||||
weight = 1.0 / (l_var * r_var / (l_var + r_var) + POINT_STD ** 2)
|
||||
|
||||
# Fit curvature to place the plant in center: e ~ dk x^2 / 2
|
||||
plan_x = np.interp(x, model_output['plan'][0, :, 0], model_output['plan'][0, :, 1])
|
||||
plan_share = np.interp(abs(model_curvature) * lookahead ** 2 / 2, [0.5, 2.0], [0.3, 0.5]) # up to 50% plan y vs curvature parabola in sharp curves
|
||||
error = np.clip(center - ((1 - plan_share) * model_curvature * x ** 2 / 2 + plan_share * plan_x), -1.0, 1.0)
|
||||
dk = 2.0 * np.sum(weight * error * x ** 2) / np.sum(weight * x ** 4)
|
||||
|
||||
# A model planning to end up further from its usual spot in the lane than the plant is now is likely going around something
|
||||
hw_now = np.clip(lines[2, 0] - lines[1, 0], *LANE_WIDTH_CLIP) / 2 if l_prob * r_prob > 0.5 else self.lane_width.x / 2
|
||||
center_now = (l_prob * (lines[1, 0] + hw_now) + r_prob * (lines[2, 0] - hw_now)) / (l_prob + r_prob)
|
||||
plan_y = np.interp(lookahead, model_output['plan'][0, :, 0], model_output['plan'][0, :, 1])
|
||||
offset = plan_y - np.interp(lookahead, x, center)
|
||||
if min(gone) > 0.5: # both lines gone: turns, intersections, relearn ideal model position
|
||||
self.usual_offset.initialized = False
|
||||
usual = self.usual_offset.x if self.usual_offset.initialized else offset
|
||||
swerve = abs(offset - usual) - 0.25 * abs(model_curvature) * lookahead ** 2 / 2
|
||||
away = swerve - abs(-center_now - usual)
|
||||
lane_curvature = 2.0 * np.polyfit(x, center, 2)[0]
|
||||
inside_curve = abs(lane_curvature) * lookahead ** 2 / 2 > 0.1 and (offset - usual) * lane_curvature > 0
|
||||
self.since_turning = 0.0 if abs(model_curvature) * lookahead ** 2 / 2 > 0.5 else self.since_turning + DT_MDL
|
||||
if inside_curve:
|
||||
self.going_around = False
|
||||
elif away > YIELD[0] and self.since_turning > 2.0:
|
||||
self.going_around = True
|
||||
elif swerve < YIELD[0] / 2:
|
||||
self.going_around = False
|
||||
self.usual_offset.update_alpha(20.0 if self.going_around else 10.0)
|
||||
self.usual_offset.update(offset)
|
||||
|
||||
if inside_curve:
|
||||
lane_trust *= np.interp(abs(offset) - abs(center_now) - 0.25 * abs(model_curvature) * lookahead ** 2 / 2, (0.5, 1.0), [1.0, 0.0])
|
||||
else:
|
||||
lane_trust *= np.interp(swerve, (YIELD[0] / 2, YIELD[0]), [1.0, 0.0]) if self.going_around else np.interp(away, YIELD, [1.0, 0.0])
|
||||
|
||||
if lane_trust < self.trust:
|
||||
dk = float(np.clip(dk, min(self.dk, 0.0), max(self.dk, 0.0)))
|
||||
self.trust = lane_trust if lane_trust < self.trust else self.trust + (lane_trust - self.trust) * DT_MDL / (1.5 + DT_MDL)
|
||||
self.dk = dk
|
||||
return float(self.trust * dk)
|
||||
@@ -7,7 +7,6 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import argparse
|
||||
import dataclasses
|
||||
import inspect
|
||||
import itertools
|
||||
import mmap
|
||||
import re
|
||||
@@ -46,11 +45,6 @@ def _address(v):
|
||||
return _root(v.src[0]), add + start, size
|
||||
|
||||
|
||||
def _buffer(u):
|
||||
from tinygrad.uop.ops import Ops
|
||||
return u.buffer if u.op is Ops.BUFFER else getattr(u.arg, "buffer", None)
|
||||
|
||||
|
||||
def _patched(buf, topo) -> tuple[bytearray, dict]:
|
||||
from tinygrad.uop.ops import Ops
|
||||
blob, rows = bytearray(buf.max_numel() * buf.dtype.itemsize), {}
|
||||
@@ -78,7 +72,7 @@ def _evaluate(u, env):
|
||||
table, index = _root(u.src[0].src[0]), _evaluate(u.src[0].src[1], env)
|
||||
return struct.unpack_from(u.dtype.fmt, table.arg, index * u.dtype.itemsize)[0] if table.op is Ops.BINARY and index is not None else None
|
||||
if u.op in GroupOp.ALU and None not in (values := [_evaluate(x, env) for x in u.src]):
|
||||
return exec_alu(u.op, u.dtype, values, *([False] if "truncate_output" in inspect.signature(exec_alu).parameters else []))
|
||||
return exec_alu(u.op, u.dtype, values, False)
|
||||
return None
|
||||
|
||||
|
||||
@@ -118,7 +112,7 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
|
||||
from tinygrad.uop.ops import Ops
|
||||
submit = jit.captured._linear.src[0].without_after
|
||||
kernels, topo = submit.arg.aux.kernels, list(jit.captured._linear.toposort(enter_calls=True))
|
||||
allocs = {str(name): u for u in topo if u.op is Ops.ALLOC and (name := getattr(u, "tag", None) or u.arg.name) is not None}
|
||||
allocs = {str(u.tag): u for u in topo if u.op is Ops.ALLOC and u.tag is not None}
|
||||
(commands, patched), kernargs = _patched(allocs["cmdbuf_compute_0"], topo), allocs["kernargs_compute_0"]
|
||||
argument_words, pokes = _patched(kernargs, topo)[1], _input_pokes(submit, kernargs)
|
||||
sh = {name: getattr(amd_gpu, f"reg{name}") + amd_gpu.GC_BASE__INST0_SEG0 - pm4.PACKET3_SET_SH_REG_START
|
||||
@@ -164,7 +158,7 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
|
||||
args.append({"input": pokes[slot][0]})
|
||||
elif slot in argument_words:
|
||||
base, at, nbytes = _address(argument_words[slot])
|
||||
args.append({"buffer": buffers.setdefault(base, len(buffers)), "offset": at, "nbytes": nbytes or base.max_numel() * base.dtype.itemsize, "dtype": u8})
|
||||
args.append({"buffer": buffers.setdefault(base, len(buffers)), "offset": at, "nbytes": nbytes or base.arg.buffer.nbytes, "dtype": u8})
|
||||
outs, ins = kernel[6]
|
||||
calls.append({"kind": "program", "name": re.sub(r"\x1b\[[0-9;]*m", "", kernel[1]), "lib": programs[image_alloc], "target": dataclasses.asdict(target),
|
||||
"signature": [[f"data{i}", i, u8, [a.get("nbytes", 1)]] for i, a in enumerate(args)], "global_size": grid, "local_size": local,
|
||||
@@ -172,13 +166,12 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
|
||||
needs_init |= {args[i]["buffer"] for i in ins if "buffer" in args[i] and args[i]["buffer"] not in written}
|
||||
written |= {args[i]["buffer"] for i in outs if "buffer" in args[i]}
|
||||
|
||||
assert all(_buffer(b) is not None for b, i in buffers.items() if i in needs_init), "a kernel reads a scratch before any kernel writes it"
|
||||
names = list(jit.captured.expected_names)
|
||||
inputs = [{"slot": slot, "name": names[slot] if slot < len(names) else None, "dtype": repr(u.dtype), "numel": int(u.max_numel()), "device": u.device}
|
||||
for slot, u in enumerate(_prepare_jit_inputs((), kwargs)[0])]
|
||||
header = {"input_names": names, "calls": calls, "returns": {"kind": "NoneType", "tensors": []}, "inputs": inputs,
|
||||
"buffers": [{"nbytes": bb.nbytes if (bb := _buffer(b)) else b.max_numel() * b.dtype.itemsize, "device": bb.device if bb else b.device, "dtype": u8}
|
||||
| ({"init": blob.add(bb.ensure_allocated().as_memoryview())} if i in needs_init else {}) for b, i in buffers.items()],
|
||||
"buffers": [{"nbytes": b.arg.buffer.nbytes, "device": b.arg.buffer.device, "dtype": u8}
|
||||
| ({"init": blob.add(b.arg.buffer.ensure_allocated().as_memoryview())} if i in needs_init else {}) for b, i in buffers.items()],
|
||||
"check": {"inputs": [[path, store(array), device] for path, array, device in frame["inputs"]],
|
||||
"outputs": [store(array) for array in frame["outputs"]], "output_container": frame["output_container"]}}
|
||||
with sink:
|
||||
|
||||
@@ -250,12 +250,6 @@ def _allocate_buffers(header: dict, blob: memoryview, device_map: dict | None, s
|
||||
return buffers
|
||||
|
||||
|
||||
def _fields(what, /, **values): # a tinygrad dataclass built or replaced with the fields it has today: a dropped field is fine, a new required one is not
|
||||
known = {field.name for field in dataclasses.fields(what)}
|
||||
values = {name: value for name, value in values.items() if name in known}
|
||||
return what(**values) if isinstance(what, type) else dataclasses.replace(what, **values)
|
||||
|
||||
|
||||
def _program_uop(call: dict, lib: bytes):
|
||||
from tinygrad.helpers import Target
|
||||
from tinygrad.uop.ops import KernelInfo, Ops, ProgramInfo, UOp
|
||||
@@ -263,9 +257,9 @@ def _program_uop(call: dict, lib: bytes):
|
||||
for name, compact_slot, dtype, shape in call["signature"]:
|
||||
parameter = UOp.param(call["globals"][compact_slot], _dtype(dtype), math.prod(shape))
|
||||
image = (shape[0], shape[1]) if len(shape) == 3 and shape[2] == 4 else None
|
||||
parameters.append(parameter.replace(arg=_fields(parameter.arg, name=name, image=image)))
|
||||
info = _fields(ProgramInfo, global_size=tuple(call["global_size"]), local_size=tuple(call["local_size"]), globals=tuple(call["globals"]),
|
||||
outs=tuple(call["outs"]), ins=tuple(call["ins"]), target=_fields(Target, **call["target"]))
|
||||
parameters.append(parameter.replace(arg=dataclasses.replace(parameter.arg, name=name, image=image)))
|
||||
info = ProgramInfo(global_size=tuple(call["global_size"]), local_size=tuple(call["local_size"]), vars=(), globals=tuple(call["globals"]),
|
||||
outs=tuple(call["outs"]), ins=tuple(call["ins"]), target=Target(**call["target"]))
|
||||
return UOp(Ops.PROGRAM, src=(UOp(Ops.SINK, arg=KernelInfo(name=call["name"])), UOp(Ops.LINEAR, src=tuple(parameters)),
|
||||
UOp(Ops.SOURCE, arg=""), UOp(Ops.BINARY, arg=lib)), arg=info)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import gc
|
||||
import os
|
||||
os.environ['GMMU'] = '0'
|
||||
@@ -35,12 +34,11 @@ from openpilot.common.realtime import config_realtime_process, DT_MDL
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
from openpilot.common.transformations.model import get_warp_matrix
|
||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, get_curvature_from_plan, smooth_value
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value
|
||||
from openpilot.selfdrive.modeld.modeld import ChestnutGpuState
|
||||
|
||||
from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState, get_curvature_from_output
|
||||
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser
|
||||
from openpilot.sunnypilot.modeld_v2.blendful import Blendful
|
||||
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants, Plan
|
||||
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants
|
||||
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
|
||||
@@ -49,9 +47,6 @@ from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||
from openpilot.sunnypilot.modeld_v2.helpers import load_model
|
||||
from openpilot.sunnypilot.modeld_v2.frame_fusion import fused_with_warp
|
||||
from openpilot.sunnypilot.modeld_v2.model_adapters import get_model_adapter
|
||||
from openpilot.sunnypilot.modeld_v2.turn_plan_floor import apply_turn_plan_floor, turn_plan_floor_scale
|
||||
from openpilot.sunnypilot.modeld_v2.navigation_input import (NavigationInputBuffer, NavigationStatePublisher, SignalRouteGuard,
|
||||
destination_route_context, navigation_capability)
|
||||
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||
|
||||
@@ -101,7 +96,6 @@ class ModelState(ModelStateBase):
|
||||
else:
|
||||
model_bundle = get_active_bundle(chestnut=chestnut)
|
||||
self.generation = model_bundle.generation if model_bundle is not None else None
|
||||
self.model_name = model_bundle.displayName if model_bundle is not None else ''
|
||||
overrides = {override.key: override.value for override in model_bundle.overrides} if model_bundle else {}
|
||||
|
||||
self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".0"))
|
||||
@@ -129,7 +123,6 @@ class ModelState(ModelStateBase):
|
||||
self._combined_model_type = self.adapter._combined_model_type
|
||||
self._vision_input_names = self.adapter._vision_input_names
|
||||
self.numpy_inputs = self.adapter.numpy_inputs
|
||||
self.navigation_supported, self.navigation_capability_reason = navigation_capability(self.numpy_inputs, bundle, native=self.adapter.is_native)
|
||||
self._policy_keys = self.adapter._policy_keys
|
||||
self._has_on_policy = self.adapter._has_on_policy
|
||||
self._desire_key = self.adapter._desire_key
|
||||
@@ -179,9 +172,6 @@ class ModelState(ModelStateBase):
|
||||
for key in ('traffic_convention', 'lateral_control_params', 'action_t'):
|
||||
if key in self.numpy_inputs and key in inputs:
|
||||
self.numpy_inputs[key][:] = inputs[key]
|
||||
for key in ('nav_map', 'navigation'):
|
||||
if key in self.numpy_inputs:
|
||||
self.numpy_inputs[key][:] = inputs.get(key, 0)
|
||||
|
||||
if self.adapter.is_native:
|
||||
for i, key in enumerate(self._vision_input_names):
|
||||
@@ -238,8 +228,7 @@ class ModelState(ModelStateBase):
|
||||
return outputs
|
||||
|
||||
def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
|
||||
lat_action_t: float, long_action_t: float, v_ego: float,
|
||||
blendful: Callable[[float], float] | None = None, turn_sign: float = 0.) -> log.ModelDataV2.Action:
|
||||
lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action:
|
||||
if 'action' not in model_output:
|
||||
plan = model_output['plan'][0]
|
||||
desired_accel = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS,
|
||||
@@ -251,14 +240,6 @@ class ModelState(ModelStateBase):
|
||||
else:
|
||||
desired_accel = model_output['action'][0, 1]
|
||||
desired_curvature = model_output['action'][0, 0] / (max(1.0, v_ego))**2
|
||||
if turn_sign and (floor_scale := turn_plan_floor_scale(v_ego)) > 0.:
|
||||
# the action head under-commits in slow turns while the model's own plan has the turn; use the plan as a floor
|
||||
plan = model_output['plan'][0]
|
||||
plan_curvature = get_curvature_from_plan(plan[:, Plan.T_FROM_CURRENT_EULER][:, 2], plan[:, Plan.ORIENTATION_RATE][:, 2],
|
||||
self.constants.T_IDXS, v_ego, lat_action_t)
|
||||
desired_curvature = apply_turn_plan_floor(desired_curvature, plan_curvature, turn_sign, floor_scale)
|
||||
if blendful is not None:
|
||||
desired_curvature += blendful(desired_curvature)
|
||||
|
||||
stop = v_ego < 0.3 and desired_accel < 0.1
|
||||
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
|
||||
@@ -284,8 +265,6 @@ def main(demo=False):
|
||||
AMDDevice.wait_timeout_ms = 3000
|
||||
|
||||
params = Params()
|
||||
navigation_state = NavigationStatePublisher(params)
|
||||
navigation_state.publish(now=time.monotonic(), supported=False, active=False, reason='model_loading', force=True)
|
||||
params.put_bool("ChestnutLoading", CHESTNUT)
|
||||
params.remove("ChestnutActive")
|
||||
|
||||
@@ -347,9 +326,6 @@ def main(demo=False):
|
||||
params.put_bool("ChestnutLoading", False)
|
||||
assert model is not None
|
||||
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
|
||||
navigation_state.publish(now=time.monotonic(), supported=model.navigation_supported, active=False,
|
||||
reason='waiting_for_navigation' if model.navigation_supported else model.navigation_capability_reason,
|
||||
model_name=model.model_name, force=True)
|
||||
|
||||
config_realtime_process(7, 54)
|
||||
|
||||
@@ -357,11 +333,7 @@ def main(demo=False):
|
||||
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutGpuState"] if CHESTNUT else [])
|
||||
pm = PubMaster(pub_socks)
|
||||
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState",
|
||||
"carControl", "controlsState", "lateralDelay", "navInstruction"])
|
||||
|
||||
vipc_client_map = VisionIpcClient("mapsd", VisionStreamType.VISION_STREAM_MAP, True)
|
||||
navigation_buffer = NavigationInputBuffer()
|
||||
signal_route_guard = SignalRouteGuard()
|
||||
"carControl", "controlsState", "lateralDelay"])
|
||||
|
||||
publish_state = PublishState()
|
||||
chestnut_state = ChestnutGpuState(pm, model.chestnut) if CHESTNUT else None
|
||||
@@ -394,8 +366,6 @@ def main(demo=False):
|
||||
DH = DesireHelper()
|
||||
meta_constants = load_meta_constants()
|
||||
RELC = RoadEdgeLaneChangeController()
|
||||
blendful = Blendful()
|
||||
blendful_enabled = False
|
||||
|
||||
while True:
|
||||
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
|
||||
@@ -439,7 +409,6 @@ def main(demo=False):
|
||||
model.lat_delay = get_lat_delay(params, sm["lateralDelay"].lateralDelay)
|
||||
model.PLANPLUS_CONTROL = params.get("PlanplusControl", return_default=True)
|
||||
camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True))
|
||||
blendful_enabled = params.get_bool("Blendful")
|
||||
lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS
|
||||
if sm.updated["extrinsicsCalibration"] and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']:
|
||||
device_from_calib_euler = np.array(sm["extrinsicsCalibration"].rpyCalib, dtype=np.float32)
|
||||
@@ -486,33 +455,6 @@ def main(demo=False):
|
||||
if 'action_t' in model.numpy_inputs:
|
||||
inputs['action_t'] = np.array([lat_action_t, long_action_t], dtype=np.float32)
|
||||
|
||||
navigation_route = {}
|
||||
navigation_reason = model.navigation_capability_reason
|
||||
if model.navigation_supported:
|
||||
destination, context = params.get('NavDestination'), params.get('MapboxTurnContext')
|
||||
intent = params.get('MapboxTurnIntent')
|
||||
if vipc_client_map.is_connected() or vipc_client_map.connect(False):
|
||||
if (buf_map := vipc_client_map.recv(0)) is not None:
|
||||
navigation_buffer.observe_map(buf_map.data, buf_map.width, buf_map.height,
|
||||
timestamp_ns=vipc_client_map.timestamp_sof, received_at=time.monotonic(),
|
||||
route_generation=vipc_client_map.timestamp_eof)
|
||||
now = time.monotonic()
|
||||
if destination is not None:
|
||||
signal_route_guard.reset()
|
||||
navigation_route = destination_route_context(destination, context, now)
|
||||
else:
|
||||
navigation_route = signal_route_guard.check(
|
||||
now=now, enabled=params.get_bool('MapboxNextTurnEnabled'), lateral_active=sm['carControl'].latActive,
|
||||
left_blinker=sm['carState'].leftBlinker, right_blinker=sm['carState'].rightBlinker,
|
||||
intent=intent, context=context,
|
||||
)
|
||||
inputs.update(navigation_buffer.prepare(
|
||||
now=now, enabled=params.get_bool('NavigationEnabled') and navigation_route['ready'], instruction=sm['navInstruction'],
|
||||
instruction_valid=sm.valid['navInstruction'], instruction_received_at=sm.recv_time['navInstruction'], speed=v_ego,
|
||||
instruction_published_ns=sm.logMonoTime['navInstruction'], expected_route_generation=navigation_route.get('routeGeneration'),
|
||||
))
|
||||
navigation_reason = navigation_buffer.reason if navigation_route['ready'] else navigation_route['reason']
|
||||
|
||||
mt1 = time.perf_counter()
|
||||
try:
|
||||
send_chestnut = (chestnut_state is not None and
|
||||
@@ -525,9 +467,6 @@ def main(demo=False):
|
||||
params.put_bool("ChestnutActive", False)
|
||||
assert small_model is not None
|
||||
model = small_model
|
||||
signal_route_guard.reset()
|
||||
navigation_state.publish(now=time.monotonic(), supported=model.navigation_supported, active=False,
|
||||
reason=model.navigation_capability_reason, model_name=model.model_name, force=True)
|
||||
if chestnut_state is not None:
|
||||
chestnut_state.big = False
|
||||
run_count = 0
|
||||
@@ -535,14 +474,6 @@ def main(demo=False):
|
||||
mt2 = time.perf_counter()
|
||||
model_execution_time = mt2 - mt1
|
||||
|
||||
navigation_active = model.navigation_supported and model_output is not None and navigation_reason == 'ready'
|
||||
if navigation_active and navigation_route.get('session') is not None:
|
||||
signal_route_guard.consumed(navigation_route['session'], navigation_route['routeGeneration'])
|
||||
navigation_state.publish(now=time.monotonic(), supported=model.navigation_supported, active=navigation_active,
|
||||
reason=navigation_reason if model.navigation_supported else model.navigation_capability_reason,
|
||||
model_name=model.model_name, session=navigation_route.get('session'),
|
||||
route_generation=navigation_route.get('routeGeneration'))
|
||||
|
||||
if model_output is not None:
|
||||
modelv2_send = messaging.new_message('modelV2')
|
||||
drivingdata_send = messaging.new_message('drivingModelData')
|
||||
@@ -551,11 +482,7 @@ def main(demo=False):
|
||||
|
||||
if not sm['carControl'].latActive:
|
||||
prev_action.desiredCurvature = sm['controlsState'].curvature
|
||||
blendful_active = desire == log.Desire.none and DH.lane_change_state == log.LaneChangeState.off
|
||||
turn_sign = 1. if desire == log.Desire.turnRight else -1. if desire == log.Desire.turnLeft else 0. # curvature + is right
|
||||
action = model.get_action_from_model(model_output, prev_action, lat_action_t, long_action_t, v_ego,
|
||||
partial(blendful.update, model_output, v_ego, blendful_active, enabled=blendful_enabled),
|
||||
turn_sign=turn_sign)
|
||||
action = model.get_action_from_model(model_output, prev_action, lat_action_t, long_action_t, v_ego)
|
||||
prev_action = action
|
||||
fill_model_msg(drivingdata_send, modelv2_send, model_output, action,
|
||||
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
|
||||
@@ -567,20 +494,11 @@ def main(demo=False):
|
||||
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
|
||||
lane_change_prob = l_lane_change_prob + r_lane_change_prob
|
||||
left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge,
|
||||
desire_prediction=modelv2_send.modelV2.meta.desirePrediction, model_position=modelv2_send.modelV2.position,
|
||||
map_turn_supported=model.navigation_supported)
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge)
|
||||
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||
mdv2sp_send.valid = modelv2_send.valid
|
||||
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
||||
mdv2sp_send.modelDataV2SP.blendfulCurvature = blendful.curvature
|
||||
mdv2sp_send.modelDataV2SP.blendfulTrust = float(blendful.trust)
|
||||
mdv2sp_send.modelDataV2SP.modelCurvature = float(blendful.model_curvature)
|
||||
mdv2sp_send.modelDataV2SP.blendfulGoingAround = bool(blendful.going_around)
|
||||
mdv2sp_send.modelDataV2SP.blendfulLeftProb = blendful.l_prob
|
||||
mdv2sp_send.modelDataV2SP.blendfulRightProb = blendful.r_prob
|
||||
mdv2sp_send.modelDataV2SP.blendfulLaneWidth = float(blendful.lane_width.x)
|
||||
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
|
||||
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction
|
||||
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
"""Inputs for comma's pinned nav-model-integration driving model.
|
||||
|
||||
The map raster and maneuver encoding follow afbf2e547e9319e2eb9f515e1e4330af63705cec.
|
||||
They condition the trained driving network; they do not issue a steering command
|
||||
or fabricate a turn desire. Missing/stale context clears both conditioning inputs.
|
||||
"""
|
||||
|
||||
import math
|
||||
from collections.abc import Mapping
|
||||
|
||||
import numpy as np
|
||||
|
||||
NAV_MAP_SIZE = 256
|
||||
NAVIGATION_SIZE = 47
|
||||
MAP_MAX_AGE = 0.5
|
||||
INSTRUCTION_MAX_AGE = 2.0
|
||||
VALHALLA_SIDED_TYPES = {
|
||||
'depart': (1, 2, 3), 'arrive': (4, 5, 6),
|
||||
'on ramp': (17, 18, 19), 'off ramp': (20, 20, 21), 'fork': (22, 23, 24),
|
||||
'roundabout': (26, 26, 26), 'rotary': (26, 26, 26), 'roundabout turn': (26, 26, 26),
|
||||
'exit roundabout': (27, 27, 27), 'exit rotary': (27, 27, 27), 'notification': (28, 28, 28),
|
||||
}
|
||||
VALHALLA_TURN_TYPES = {'straight': 8, 'slight right': 9, 'right': 10, 'sharp right': 11, 'uturn': 13,
|
||||
'sharp left': 14, 'left': 15, 'slight left': 16}
|
||||
|
||||
|
||||
def _field(value, name, default=None):
|
||||
return value.get(name, default) if isinstance(value, Mapping) else getattr(value, name, default)
|
||||
|
||||
|
||||
def valhalla_type(maneuver_type: str, modifier: str) -> int | None:
|
||||
if maneuver_type == 'merge':
|
||||
return None
|
||||
if maneuver_type in VALHALLA_SIDED_TYPES:
|
||||
straight, right, left = VALHALLA_SIDED_TYPES[maneuver_type]
|
||||
return right if 'right' in modifier else left if 'left' in modifier else straight
|
||||
return VALHALLA_TURN_TYPES.get(modifier, 8)
|
||||
|
||||
|
||||
def navigation_input(maneuvers, traveled: float = 0.) -> np.ndarray:
|
||||
"""The next non-depart/arrive Valhalla command plus distance in100m units."""
|
||||
result = np.zeros(NAVIGATION_SIZE, dtype=np.float32)
|
||||
if not math.isfinite(traveled) or traveled < 0:
|
||||
return result
|
||||
for maneuver in maneuvers:
|
||||
maneuver_type, modifier = _field(maneuver, 'type', ''), _field(maneuver, 'modifier', '')
|
||||
distance = _field(maneuver, 'distance')
|
||||
if (not isinstance(maneuver_type, str) or not isinstance(modifier, str) or
|
||||
isinstance(distance, bool) or not isinstance(distance, (int, float, np.floating)) or not math.isfinite(distance)):
|
||||
continue
|
||||
command, remaining = valhalla_type(maneuver_type, modifier), distance - traveled
|
||||
if command is not None and command > 6 and remaining >= 0:
|
||||
result[command], result[-1] = 1, remaining / 100
|
||||
break
|
||||
return result
|
||||
|
||||
|
||||
def supports_navigation(numpy_inputs) -> bool:
|
||||
"""Fail closed on CTV3/small fallback or an incompatible nav tensor contract."""
|
||||
expected = {'nav_map': (1, 1, NAV_MAP_SIZE, NAV_MAP_SIZE), 'navigation': (1, NAVIGATION_SIZE)}
|
||||
return all(name in numpy_inputs and numpy_inputs[name].shape == shape and numpy_inputs[name].dtype == np.float32
|
||||
for name, shape in expected.items())
|
||||
|
||||
|
||||
def navigation_capability(numpy_inputs, bundle, *, native: bool):
|
||||
if not supports_navigation(numpy_inputs):
|
||||
return False, 'model_unsupported'
|
||||
if bundle is None:
|
||||
return False, 'model_bundle_required'
|
||||
if not native or bundle.generation != 12 or not bundle.is20hz:
|
||||
return False, 'model_bundle_incompatible'
|
||||
return True, 'supported'
|
||||
|
||||
|
||||
def destination_route_context(destination, context, now):
|
||||
"""An explicit destination must own the exact map generation, including at boot."""
|
||||
missing = {'ready': False, 'reason': 'destination_route_unavailable'}
|
||||
if not isinstance(destination, dict) or not isinstance(context, dict):
|
||||
return missing
|
||||
snapshot = context.get('destination')
|
||||
updated, generation = context.get('updatedAt'), context.get('routeGeneration')
|
||||
if (context.get('routeOwner') != 'destination' or context.get('status') != 'ready' or
|
||||
not isinstance(snapshot, dict) or isinstance(updated, bool) or not isinstance(updated, (int, float)) or
|
||||
not math.isfinite(updated) or not 0 <= now - updated <= .35 or
|
||||
isinstance(generation, bool) or not isinstance(generation, int) or generation <= 0):
|
||||
return missing
|
||||
for name in ('latitude', 'longitude'):
|
||||
value = destination.get(name)
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or snapshot.get(name) != value:
|
||||
return missing
|
||||
return {'ready': True, 'reason': 'ready', 'routeGeneration': generation}
|
||||
|
||||
|
||||
class NavigationInputBuffer:
|
||||
"""Latest real map frame with bounded capture and instruction ages."""
|
||||
|
||||
def __init__(self):
|
||||
self.map = None
|
||||
self.map_received_at = 0.
|
||||
self.map_captured_at = 0.
|
||||
self.route_generation = None
|
||||
self.reason = 'map_missing'
|
||||
|
||||
def observe_map(self, data, width: int, height: int, *, timestamp_ns: int, received_at: float,
|
||||
route_generation: int | None = None) -> bool:
|
||||
# A newly received invalid/black-with-zero-timestamp frame must invalidate
|
||||
# the preceding map rather than make its old geometry appear fresh.
|
||||
self.map = None
|
||||
self.route_generation = None
|
||||
self.reason = 'map_invalid'
|
||||
if (width != NAV_MAP_SIZE or height != NAV_MAP_SIZE or timestamp_ns <= 0 or
|
||||
not math.isfinite(received_at) or received_at < 0):
|
||||
return False
|
||||
captured_at = timestamp_ns / 1e9
|
||||
if not 0 <= received_at - captured_at <= MAP_MAX_AGE:
|
||||
return False
|
||||
try:
|
||||
pixels = np.frombuffer(data, dtype=np.uint8, count=NAV_MAP_SIZE * NAV_MAP_SIZE)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
# The verified nav ONNX expects float32 raw raster values0..255, not a
|
||||
# normalized camera image. Assignment in comma's runner performs this cast.
|
||||
self.map = pixels.astype(np.float32).reshape(1, 1, NAV_MAP_SIZE, NAV_MAP_SIZE)
|
||||
self.map_received_at, self.map_captured_at = received_at, captured_at
|
||||
self.route_generation = route_generation
|
||||
self.reason = 'ready'
|
||||
return True
|
||||
|
||||
def prepare(self, *, now: float, enabled: bool, instruction, instruction_valid: bool,
|
||||
instruction_received_at: float, speed: float, instruction_published_ns: int | None = None,
|
||||
expected_route_generation: int | None = None):
|
||||
zero = {'nav_map': np.zeros((1, 1, NAV_MAP_SIZE, NAV_MAP_SIZE), dtype=np.float32),
|
||||
'navigation': np.zeros((1, NAVIGATION_SIZE), dtype=np.float32)}
|
||||
if not enabled:
|
||||
self.reason = 'disabled'
|
||||
return zero
|
||||
if self.map is None:
|
||||
self.reason = 'map_missing'
|
||||
return zero
|
||||
if not all(math.isfinite(value) for value in (now, instruction_received_at, speed)):
|
||||
self.reason = 'invalid_time_or_speed'
|
||||
return zero
|
||||
if not (0 <= now - self.map_received_at < MAP_MAX_AGE and 0 <= now - self.map_captured_at < MAP_MAX_AGE):
|
||||
self.reason = 'map_stale'
|
||||
return zero
|
||||
if (not isinstance(self.route_generation, int) or self.route_generation <= 0 or
|
||||
not isinstance(instruction_published_ns, int) or instruction_published_ns < self.route_generation or
|
||||
(expected_route_generation is not None and self.route_generation != expected_route_generation)):
|
||||
self.reason = 'route_generation_mismatch'
|
||||
return zero
|
||||
elapsed = now - instruction_received_at
|
||||
if not instruction_valid or not 0 <= elapsed < INSTRUCTION_MAX_AGE:
|
||||
self.reason = 'instruction_stale'
|
||||
return zero
|
||||
vector = navigation_input(_field(instruction, 'allManeuvers', []), max(speed, 0.) * elapsed)
|
||||
self.reason = 'ready'
|
||||
return {'nav_map': self.map, 'navigation': vector.reshape(1, NAVIGATION_SIZE)}
|
||||
|
||||
|
||||
class SignalRouteGuard:
|
||||
"""Bind automatic inputs to the current signal and exact rendered route.
|
||||
|
||||
The sole remembered identifier is a route this model actually consumed. It
|
||||
permits the router's explicitly bounded outgoing continuation after normal
|
||||
signal auto-cancel; a first-time retained route or another session is rejected.
|
||||
No timing, steering, maneuver selection or route-completion policy lives here.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.last_consumed = None
|
||||
|
||||
def reset(self):
|
||||
self.last_consumed = None
|
||||
|
||||
def consumed(self, session, route_generation):
|
||||
self.last_consumed = (session, route_generation)
|
||||
|
||||
def check(self, *, now, enabled, lateral_active, left_blinker, right_blinker, intent, context):
|
||||
def unavailable(reason, *, retire=False):
|
||||
if retire:
|
||||
self.reset()
|
||||
return {'ready': False, 'reason': reason}
|
||||
|
||||
if not enabled or not lateral_active:
|
||||
return unavailable('signal_navigation_disabled', retire=True)
|
||||
if isinstance(intent, dict) and self.last_consumed is not None and intent.get('session') != self.last_consumed[0]:
|
||||
self.reset()
|
||||
if not isinstance(context, dict):
|
||||
return unavailable('route_context_missing')
|
||||
updated = context.get('updatedAt')
|
||||
generation, session = context.get('routeGeneration'), context.get('session')
|
||||
direction = context.get('direction')
|
||||
if context.get('status') in ('passed', 'completed', 'arrived', 'cancelled', 'idle', 'disabled'):
|
||||
return unavailable('route_context_unavailable', retire=True)
|
||||
if self.last_consumed is not None and ((session is not None and session != self.last_consumed[0]) or
|
||||
(generation is not None and generation != self.last_consumed[1])):
|
||||
self.reset()
|
||||
if (context.get('status') != 'ready' or context.get('routeOwner') != 'signal' or
|
||||
context.get('locationValid') is not True or context.get('headingValid') is not True or
|
||||
isinstance(updated, bool) or not isinstance(updated, (int, float)) or not math.isfinite(updated) or
|
||||
not 0 <= now - updated <= .35 or isinstance(generation, bool) or not isinstance(generation, int) or generation <= 0 or
|
||||
not isinstance(session, str) or not session or direction not in ('left', 'right')):
|
||||
return unavailable('route_context_unavailable')
|
||||
physical_direction = 'left' if left_blinker and not right_blinker else 'right' if right_blinker and not left_blinker else None
|
||||
matches = (isinstance(intent, dict) and intent.get('session') == session and intent.get('direction') == direction and
|
||||
intent.get('mode') == 'turn' and intent.get('modelSupportsNavigation') is True and intent.get('lateralActive') is True)
|
||||
held_signal = matches and physical_direction == direction
|
||||
retained = (not left_blinker and not right_blinker and intent is None and
|
||||
context.get('retainedAfterSignalCancel') is True and self.last_consumed == (session, generation))
|
||||
if not (held_signal or retained):
|
||||
# The camera loop can see auto-cancel before navd's next10Hz decision.
|
||||
# With neither indicator, hold only the identity (never the inputs) until
|
||||
# the router explicitly retains or cancels that same owned route.
|
||||
return unavailable('signal_session_mismatch', retire=bool(left_blinker or right_blinker))
|
||||
return {'ready': True, 'reason': 'ready', 'session': session, 'routeGeneration': generation}
|
||||
|
||||
|
||||
class NavigationStatePublisher:
|
||||
"""Runtime capability and consumed-input status, without a control claim."""
|
||||
|
||||
def __init__(self, params):
|
||||
self.params = params
|
||||
self.last_published = float('-inf')
|
||||
|
||||
def publish(self, *, now: float, supported: bool, active: bool, reason: str, model_name: str = '', force: bool = False,
|
||||
session=None, route_generation=None):
|
||||
if not force and now - self.last_published < .1:
|
||||
return False
|
||||
value = {'updatedAt': now, 'supported': bool(supported), 'active': bool(supported and active), 'reason': reason}
|
||||
if model_name:
|
||||
value['modelName'] = model_name
|
||||
if session is not None:
|
||||
value.update(session=session, routeGeneration=route_generation)
|
||||
self.params.put('NavModelState', value)
|
||||
self.last_published = now
|
||||
return True
|
||||
@@ -23,7 +23,7 @@ def record_kernel_linear():
|
||||
recorded, hcq_compile, compile_linear = [], realize.hcq_compile, jit.compile_linear
|
||||
|
||||
def recording_compile_linear(*args, **kwargs):
|
||||
with mock.patch.object(realize, "hcq_compile", lambda linear, *a, **k: (linear.src and recorded.append(linear), hcq_compile(linear, *a, **k))[1]):
|
||||
with mock.patch.object(realize, "hcq_compile", lambda linear, *a, **k: (recorded.append(linear), hcq_compile(linear, *a, **k))[1]):
|
||||
return compile_linear(*args, **kwargs)
|
||||
|
||||
with mock.patch.object(jit, "compile_linear", recording_compile_linear):
|
||||
@@ -38,18 +38,6 @@ def capture_frame(jit, kwargs: dict, output_container: str | None = None):
|
||||
return {"inputs": inputs, "outputs": [tensor.numpy().copy() for tensor in outputs], "output_container": output_container}, result
|
||||
|
||||
|
||||
def kernel_signature(ast, elf) -> list:
|
||||
from tinygrad.dtype import AddrSpace
|
||||
if hasattr(ast, "kernel_params"):
|
||||
if any(p.arg.addrspace is AddrSpace.ALU for p in ast.kernel_params):
|
||||
raise ValueError(f"{elf.name} takes a variable, which plain artifacts don't support")
|
||||
compact = {slot: i for i, slot in enumerate(ast.arg.globals)}
|
||||
return [[p.arg.name, compact[p.arg.slot], repr(p.dtype), [int(x) for x in p._shape]] for p in ast.kernel_params]
|
||||
if not all(isinstance(slot, int) for _, slot, _, _ in elf.signature):
|
||||
raise ValueError("TinyELF.signature lost its slots and this tinygrad has no UOp.kernel_params: update kernel_signature")
|
||||
return [[name, slot, repr(dtype), [int(x) for x in shape]] for name, slot, dtype, shape in elf.signature]
|
||||
|
||||
|
||||
def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict, memoryview]:
|
||||
from tinygrad.uop.ops import Ops
|
||||
sink = tempfile.TemporaryFile(dir=".")
|
||||
@@ -63,12 +51,6 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
|
||||
if uop.op is Ops.PARAM:
|
||||
input_specs.setdefault(uop.arg.slot, {"dtype": repr(uop.dtype), "numel": int(uop.max_numel()), "device": uop.device})
|
||||
return {"input": uop.arg.slot}
|
||||
root, offset = uop, 0
|
||||
while root.op in (Ops.AFTER, Ops.BITCAST, Ops.SHRINK):
|
||||
offset, root = offset + (root.src[1].arg * root.src[0].dtype.itemsize if root.op is Ops.SHRINK else 0), root.src[0]
|
||||
if root.op is Ops.ALLOC:
|
||||
base = bases.setdefault(id(root), (len(bases), root, root.dtype))
|
||||
return {"buffer": base[0], "offset": offset, "nbytes": int(uop.max_numel()) * uop.dtype.itemsize, "dtype": repr(uop.dtype)}
|
||||
buffer = uop.buffer
|
||||
root = buffer._base if buffer._base is not None else buffer
|
||||
base = bases.setdefault(id(root), (len(bases), root, uop.base.dtype if buffer._base is None else uop.dtype))
|
||||
@@ -81,7 +63,7 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
|
||||
elf, info = ast.to_elf(), ast.arg
|
||||
global_size, local_size = info.launch_dims({})
|
||||
calls.append({"kind": "program", "name": elf.name, "lib": blob.add(bytes(elf.lib)), "target": dataclasses.asdict(elf.target),
|
||||
"signature": kernel_signature(ast, elf),
|
||||
"signature": [[name, slot, repr(dtype), [int(x) for x in shape]] for name, slot, dtype, shape in elf.signature],
|
||||
"global_size": [int(x) for x in global_size], "local_size": [int(x) for x in local_size],
|
||||
"globals": list(info.globals), "outs": list(info.outs), "ins": list(info.ins), "args": arguments})
|
||||
outs, ins = info.outs, info.ins
|
||||
@@ -95,9 +77,7 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
|
||||
|
||||
ret = jit.captured.ret
|
||||
returns = {"kind": type(ret).__name__, "tensors": [describe(t.uop.base) | {"shape": [int(x) for x in t.shape]} for t in flatten(ret)]}
|
||||
from tinygrad.device import Buffer
|
||||
assert not [base for index, base, _ in bases.values() if index in needs_init and not isinstance(base, Buffer)], "a kernel reads a scratch before writing it"
|
||||
buffers = [{"nbytes": base.nbytes if isinstance(base, Buffer) else int(base.max_numel()) * dtype.itemsize, "device": base.device, "dtype": repr(dtype)}
|
||||
buffers = [{"nbytes": base.nbytes, "device": base.device, "dtype": repr(dtype)}
|
||||
| ({"init": blob.add(base.as_memoryview())} if index in needs_init else {}) for index, base, dtype in bases.values()]
|
||||
names = list(jit.captured.expected_names)
|
||||
if check is not None:
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.sunnypilot.modeld_v2.blendful import Blendful
|
||||
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
|
||||
|
||||
X = np.array(ModelConstants.X_IDXS)
|
||||
BUILD, RELEASE = 0.5, 2.0
|
||||
|
||||
|
||||
def frame(center=0.0, plan=0.0, k=0.0, probs=(0.9, 0.9), stds=(0.1, 0.1), width=3.5):
|
||||
road = k * X ** 2 / 2
|
||||
lines = np.zeros((1, 4, 33, 2))
|
||||
lane_stds = np.ones((1, 4, 33, 2))
|
||||
for i, off in enumerate((-1.5 * width, -width / 2, width / 2, 1.5 * width)):
|
||||
lines[0, i, :, 0] = road + center + off
|
||||
lane_stds[0, 1, :, 0], lane_stds[0, 2, :, 0] = stds
|
||||
lane_probs = np.zeros((1, 8))
|
||||
lane_probs[0, 3], lane_probs[0, 5] = probs
|
||||
path = np.zeros((1, 33, 3))
|
||||
path[0, :, 0] = X
|
||||
path[0, :, 1] = road + plan * np.clip(X / 10, 0, 1)
|
||||
return {'lane_lines': lines, 'lane_lines_stds': lane_stds, 'lane_lines_prob': lane_probs, 'plan': path}
|
||||
|
||||
|
||||
def drive(bf, seconds, v=20.0, k=0.0, **kw):
|
||||
out = [bf.update(frame(k=k, **kw), v, True, k) for _ in range(int(seconds / DT_MDL))]
|
||||
return np.array(out) * v ** 2 # m/s^2
|
||||
|
||||
|
||||
class TestBlendful(OpenpilotTestCase):
|
||||
def setup_method(self):
|
||||
self.bf = Blendful()
|
||||
|
||||
def test_centered_adds_nothing(self):
|
||||
push = drive(self.bf, 5.0)
|
||||
assert self.bf.trust > 0.9
|
||||
assert np.abs(push).max() < 1e-3
|
||||
|
||||
def test_pulls_toward_lane_center(self):
|
||||
push = drive(self.bf, 5.0, center=0.5)
|
||||
assert push[-1] > 0.02 # + y side, where the lane center is
|
||||
assert self.bf.trust > 0.9
|
||||
assert np.all(np.diff(push) <= BUILD * DT_MDL + 1e-9)
|
||||
|
||||
def test_flipping_sides_builds_slowly(self):
|
||||
k = 0.002 # 0.8 m/s^2 at 20 m/s
|
||||
drive(self.bf, 5.0, k=k, center=0.5)
|
||||
push = drive(self.bf, 5.0, k=k, center=-0.5)
|
||||
rate = np.diff(push) / DT_MDL
|
||||
assert push[-1] < 0
|
||||
assert np.all(rate >= -RELEASE - 1e-6)
|
||||
assert np.all(rate[push[:-1] <= 0] >= -BUILD - 1e-6) # once past zero it builds
|
||||
crossing = push[1:][(push[:-1] > 0) & (push[1:] <= 0)]
|
||||
assert len(crossing) and np.all(crossing >= -BUILD * DT_MDL - 1e-9)
|
||||
|
||||
def test_weak_lines_drive_laneless(self):
|
||||
clear = drive(Blendful(), 5.0, center=0.5)[-1]
|
||||
faint = Blendful()
|
||||
push = drive(faint, 5.0, center=0.5, probs=(0.1, 0.1))
|
||||
assert faint.trust < 0.25 and abs(push[-1]) < 0.25 * clear # faint lines = trust and push scale down
|
||||
blurry = Blendful()
|
||||
push = drive(blurry, 5.0, center=0.5, stds=(0.8, 0.8))
|
||||
assert blurry.trust == 0.0 and np.abs(push).max() < 1e-6 # lines too uncertain = laneless
|
||||
|
||||
def test_lone_line_holds_through_a_blink_in_a_curve(self):
|
||||
k, v = 1 / 20, 6.0
|
||||
drive(self.bf, 6.0, v=v, k=k, plan=0.4, probs=(0.9, 0.05))
|
||||
before = self.bf.trust
|
||||
drive(self.bf, 0.25, v=v, k=k, plan=0.4, probs=(0.1, 0.05), stds=(1.0, 0.1))
|
||||
bottom = self.bf.trust
|
||||
drive(self.bf, 2.0, v=v, k=k, plan=0.4, probs=(0.9, 0.05))
|
||||
assert before > 0.5
|
||||
assert bottom > 0.5 * before # keeps latched through flicker
|
||||
assert self.bf.trust > 0.85 * before
|
||||
|
||||
def test_yields_when_the_model_goes_around(self):
|
||||
drive(self.bf, 6.0)
|
||||
push = drive(self.bf, 3.0, plan=0.8)
|
||||
assert self.bf.going_around
|
||||
assert self.bf.trust < 0.05
|
||||
assert abs(push[-1]) < 0.01
|
||||
|
||||
def test_fit_does_not_grow_while_trust_falls(self):
|
||||
drive(self.bf, 5.0, center=0.3)
|
||||
for i in range(40):
|
||||
dk, trust = self.bf.dk, self.bf.trust
|
||||
p = 0.9 - 0.02 * i
|
||||
self.bf.update(frame(center=0.3, plan=-0.02 * i, probs=(p, p)), 20.0, True, 0.0)
|
||||
if self.bf.trust < trust:
|
||||
assert abs(self.bf.dk) <= abs(dk) + 1e-12
|
||||
|
||||
def test_toggle_off_returns_zero_and_resets(self):
|
||||
drive(self.bf, 5.0, center=0.5)
|
||||
assert self.bf.curvature != 0.0
|
||||
assert self.bf.update(frame(center=0.5), 20.0, True, 0.0, enabled=False) == 0.0
|
||||
assert self.bf.curvature == 0.0 and self.bf.trust == 0.0
|
||||
|
||||
def test_fades_below_min_speed(self):
|
||||
drive(self.bf, 5.0, v=10.0, center=0.5)
|
||||
start = self.bf.curvature
|
||||
out = np.array([self.bf.update(frame(center=0.5), 4.0, True, 0.0) for _ in range(100)])
|
||||
assert self.bf.trust == 0.0
|
||||
assert abs(out[0]) < abs(start)
|
||||
assert abs(out[-1]) < 1e-6
|
||||
@@ -1,121 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import numpy as np
|
||||
|
||||
os.environ.setdefault("CHECK_OOB", "0")
|
||||
os.environ.setdefault("GMMU", "0")
|
||||
if sys.platform == "darwin" and os.environ.get("DEV", "").startswith("MOCKUSB"): # the mock's VRAM is a memfd, which macOS lacks: an unlinked file is the same
|
||||
from tinygrad.runtime.autogen import libc
|
||||
|
||||
def _memfd_create(name, flags):
|
||||
fd, path = tempfile.mkstemp()
|
||||
os.unlink(path)
|
||||
return fd
|
||||
libc.memfd_create = _memfd_create
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.sunnypilot.modeld_v2 import convert_upstream_pkl
|
||||
from openpilot.sunnypilot.modeld_v2.convert_upstream_pkl import convert
|
||||
from openpilot.sunnypilot.modeld_v2.helpers import PlainJit, load_model
|
||||
from openpilot.sunnypilot.modeld_v2.plain_artifact_compiler import capture_frame
|
||||
|
||||
IMG, DESIRE, STATE, OUT = (6, 8, 8), (8,), (4, 16), (1, 32)
|
||||
|
||||
|
||||
class TestConvertUpstreamPkl(OpenpilotTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
if not os.environ.get("TINYGRAD_TESTS"):
|
||||
raise unittest.SkipTest("TINYGRAD_TESTS not set. skipping converter test")
|
||||
from tinygrad import Device, Tensor
|
||||
assert Device.DEFAULT == "AMD" and getattr(Device["AMD"], "is_usb", False), f"{Device.DEFAULT} is not an AMD device behind the USB bridge"
|
||||
assert (Tensor([1, 2, 3]) * 2).tolist() == [2, 4, 6]
|
||||
cls.tmp = tempfile.TemporaryDirectory()
|
||||
cls.rng = np.random.default_rng(0)
|
||||
cls.weights = Tensor(cls.rng.standard_normal((16, 32)).astype(np.float32)).realize()
|
||||
cls.bias = Tensor(cls.rng.standard_normal((1, 32)).astype(np.float32)).realize()
|
||||
cls.input_specs = {"input_imgs": (IMG, "uint8", Device.DEFAULT), "desire": (DESIRE, "float32", Device.DEFAULT),
|
||||
"features": (STATE, "float32", Device.DEFAULT)}
|
||||
cls.output_specs = {"outputs": (OUT, "float32", Device.DEFAULT), "next_features": (STATE, "float32", Device.DEFAULT)}
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.tmp.cleanup()
|
||||
super().tearDownClass()
|
||||
|
||||
def model(self, input_imgs, desire, features, output_buffers):
|
||||
from tinygrad import Tensor
|
||||
feat = features.mean(axis=0).reshape(1, 16) + input_imgs.float().mean() + desire.sum()
|
||||
Tensor.realize(output_buffers["outputs"].assign((feat @ self.weights + self.bias).relu()),
|
||||
output_buffers["next_features"].assign(features[1:].cat(feat * 2)))
|
||||
|
||||
def inputs(self, seed: int) -> dict:
|
||||
from examples.openpilot.helpers import allocate_inputs
|
||||
rng = np.random.default_rng(seed)
|
||||
|
||||
def initialize(arrays: dict):
|
||||
for value in arrays.values():
|
||||
value[...] = rng.standard_normal(value.shape) if value.dtype.kind == "f" else rng.integers(0, 256, value.shape)
|
||||
return allocate_inputs(self.input_specs, initialize) | {"output_buffers": allocate_inputs(self.output_specs)}
|
||||
|
||||
def jit(self):
|
||||
from tinygrad import TinyJit
|
||||
run = TinyJit(self.model, prune=True)
|
||||
kwargs = self.inputs(1)
|
||||
run(**kwargs)
|
||||
run(**kwargs)
|
||||
return run
|
||||
|
||||
def assert_matches_eager(self, plain, seed: int):
|
||||
fresh, expected = self.inputs(seed), self.inputs(seed)
|
||||
plain(**fresh)
|
||||
self.model(**expected)
|
||||
for name in self.output_specs:
|
||||
assert np.array_equal(fresh["output_buffers"][name].numpy(), expected["output_buffers"][name].numpy()), name
|
||||
|
||||
def convert(self, jit) -> tuple[dict, memoryview]:
|
||||
from tinygrad import Device
|
||||
kwargs = self.inputs(2)
|
||||
frame, _ = capture_frame(jit, kwargs, "output_buffers")
|
||||
header, blob = convert(jit, kwargs, frame, Device[Device.DEFAULT].renderer.target)
|
||||
kernels = jit.captured._linear.src[0].without_after.arg.aux.kernels
|
||||
assert [call["name"] for call in header["calls"]] == [re.sub(r"\x1b\[[0-9;]*m", "", kernel[1]) for kernel in kernels]
|
||||
assert all(re.fullmatch(r"\w+", call["name"]) for call in header["calls"]), "kernel names: failure point 1"
|
||||
assert [(tuple(call["outs"]), tuple(call["ins"])) for call in header["calls"]] == [tuple(kernel[6]) for kernel in kernels], "outs/ins: failure point 1"
|
||||
bound = {argument["input"] for call in header["calls"] for argument in call["args"] if "input" in argument}
|
||||
assert bound == {spec["slot"] for spec in header["inputs"]}, f"input slots bound by pokes {sorted(bound)}: failure point 4"
|
||||
assert [spec["name"] for spec in header["inputs"]][:len(self.input_specs)] == sorted(self.input_specs)
|
||||
return header, blob
|
||||
|
||||
def test_converted_jit_matches_eager(self): # the submit decoded into plain calls, replayed against its own frame
|
||||
jit = self.jit()
|
||||
plain = PlainJit(*self.convert(jit))
|
||||
self.assert_matches_eager(plain, 3)
|
||||
self.assert_matches_eager(plain, 4)
|
||||
|
||||
def test_pickle_round_trip_through_main(self):
|
||||
from examples.openpilot.helpers import dump_pickle, load_pickle
|
||||
from tinygrad.engine.realize import lower_and_compile
|
||||
pkl, out = os.path.join(self.tmp.name, "tiny_tinygrad.pkl"), os.path.join(self.tmp.name, "tiny_plain.pkl")
|
||||
dump_pickle({"run": self.jit(), "input_specs": self.input_specs, "output_specs": self.output_specs, "metadata": {"name": "tiny"}}, pkl)
|
||||
|
||||
artifact = load_pickle(pkl, out_of_band=True)
|
||||
artifact["run"].captured._linear = lower_and_compile(artifact["run"].captured._linear)
|
||||
self.assert_matches_eager(PlainJit(*self.convert(artifact["run"])), 5)
|
||||
|
||||
with mock.patch.object(sys, "argv", ["convert_upstream_pkl", pkl, out]):
|
||||
convert_upstream_pkl.main()
|
||||
bundle = load_model(out, cache=False, verify=True)
|
||||
assert bundle["metadata"] == {"name": "tiny"} and bundle["input_specs"] == self.input_specs and bundle["output_specs"] == self.output_specs
|
||||
self.assert_matches_eager(bundle["run"], 6)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user