435 Commits

Author SHA1 Message Date
mawei f40845c5fb 编辑selfdrived.py 2026-04-18 19:19:48 +08:00
1okko 47e8216d39 Merge branch 'StarPilot' of https://github.com/firestar5683/StarPilot into StarPilot 2026-04-12 09:07:18 +08:00
firestar5683 d2e5f06395 calibration 2026-04-11 19:06:07 -05:00
firestar5683 8c93f6b896 fixes 2026-04-11 19:01:10 -05:00
firestarsdog 7ceb47eb23 not1454 is a liar 2026-04-11 13:21:11 -04:00
firestarsdog 851666e244 BigUI WIP: Some Model Manager Cleanup 2026-04-11 05:06:00 -04:00
firestar5683 161e0e10c8 customaccel patch 2026-04-11 00:51:12 -05:00
firestar5683 d44c26f415 customaccel patch 2026-04-11 00:51:05 -05:00
firestar5683 ca530afab2 raybig 2026-04-10 23:17:30 -05:00
firestar5683 87aa15fc33 raybig 2026-04-10 23:00:14 -05:00
firestar5683 b511785e01 Revert "Bigui Launch Fix - Revert in case of emergency"
This reverts commit 87b4f8a287.
2026-04-10 22:22:54 -05:00
firestar5683 8236c530fb op10 2026-04-10 22:09:01 -05:00
firestar5683 692ab16651 build 2026-04-10 20:59:44 -05:00
firestar5683 e0cf4fd988 kp / 2018 lat / 2017 lat / 2022 lat / GM long / trailer mode / 2026-04-10 20:57:30 -05:00
firestarsdog 87b4f8a287 Bigui Launch Fix - Revert in case of emergency 2026-04-10 21:35:20 -04:00
firestar5683 2efb42bc43 Boot Logo 2026-04-10 16:34:06 -05:00
firestar5683 5968f7e6a0 ui 2026-04-10 16:16:45 -05:00
firestar5683 2712642d9d We're not in Kansas anymore 2026-04-10 14:49:48 -05:00
firestar5683 36bc3aa4af show commit 2026-04-10 14:49:48 -05:00
firestar5683 c9e4e6e36c 1e1 2026-04-10 14:49:47 -05:00
firestar5683 93e496d239 whoops 2026-04-10 14:49:47 -05:00
firestar5683 9eb203fb03 standard gas tuning 2026-04-10 14:49:47 -05:00
firestar5683 7ab1ec0ace 2017 2026-04-10 14:49:47 -05:00
firestar5683 b4e4fa373f 3d1 regression 2026-04-10 14:49:47 -05:00
firestar5683 e338ef2585 raylib 2026-04-10 14:49:46 -05:00
firestar5683 ed51b41970 ui stall 2026-04-10 14:49:46 -05:00
firestar5683 00abf5bc2c Agahnim 2026-04-10 14:49:46 -05:00
firestar5683 6f22a65224 Follow that trail; The trail that we blaze 2026-04-09 21:18:41 -05:00
firestar5683 01bf1d15c2 volt tune / 2018 bolt tune 2026-04-09 20:55:24 -05:00
firestarsdog e91d0bef87 BigUI WIP: Spacing 2026-04-09 21:44:09 -04:00
firestar5683 3cb5792926 SDGM 2026-04-09 15:04:11 -05:00
firestar5683 4b5ec507be Persist Exp State 2026-04-09 13:00:49 -05:00
firestar5683 08efd8f877 2017 2026-04-09 11:23:06 -05:00
firestar5683 5122444610 build 2026-04-08 23:12:42 -05:00
firestar5683 07a4bfc5f5 0x370 Leak 2026-04-08 23:11:55 -05:00
firestar5683 375df40919 ui back to 5 2026-04-08 23:11:55 -05:00
firestar5683 30778bf908 logg 2026-04-08 23:11:54 -05:00
firestar5683 5b3229576a more ui logging 2026-04-08 19:41:01 -05:00
firestar5683 04e199cf7b 2017 test 2026-04-08 19:10:29 -05:00
firestar5683 258eedb42c Prius 2026-04-08 18:28:20 -05:00
firestar5683 3e885e238c Updated Bolt Tunes 2026-04-08 18:28:20 -05:00
firestar5683 89d3a0ef23 2022 2026-04-08 15:55:51 -05:00
firestar5683 5b27f36a62 less creep and DM alert 2026-04-08 11:11:25 -05:00
firestar5683 a22e8c963a 2022 tune 2026-04-08 11:02:31 -05:00
firestar5683 84016d34e1 2017 tune 2026-04-08 10:56:35 -05:00
firestar5683 1d23f7d59f pedal plotjuggler 2026-04-08 01:43:04 -05:00
firestar5683 b94bad766a update 2018 bolt tune 2026-04-08 00:15:38 -05:00
firestar5683 2d7d7a1246 Simple Mode 2026-04-07 23:52:11 -05:00
firestar5683 c2ff6b573a volt checks 2026-04-07 23:44:58 -05:00
firestar5683 3f2dc76051 prius / bolt / volt tuning | UI Logging 2026-04-07 23:44:58 -05:00
firestar5683 c4c5a9a92b earlier galaxy boot 2026-04-07 14:57:06 -05:00
firestar5683 3fe5adc303 ./models --dm 2026-04-07 12:43:48 -05:00
firestar5683 b7d76c176b patch 2026-04-07 12:11:21 -05:00
firestar5683 ce5c0217c0 Change ./models for off policy rename 2026-04-07 11:59:47 -05:00
firestar5683 f6601873a2 fix model manager 2026-04-07 11:50:50 -05:00
firestar5683 d59aca4fb6 OPv8 2026-04-07 11:31:46 -05:00
firestarsdog 2d1376cd86 BigUI WIP: Cleanup 2026-04-07 02:15:45 -04:00
firestarsdog 0b0fb3957a BigUI WIP: More Tab Stuff
(cherry picked from commit 9ba41d1a4a46cb95c5ef07901593e72d3d00ad7a)
2026-04-07 00:45:18 -05:00
firestar5683 355fe2a273 build 2026-04-07 00:45:17 -05:00
firestar5683 4beb1f6e2e Lateral maneuvers 2026-04-07 00:45:17 -05:00
firestar5683 088e4a6e56 upstream 2026-04-07 00:45:17 -05:00
firestar5683 c7f262e700 Bolt lateral tests 2026-04-07 00:45:17 -05:00
firestarsdog 5f3c593640 BigUI WIP: We need tabs 2026-04-07 00:01:31 -04:00
firestar5683 3ca76720fc cruise speed and ui test 2026-04-06 22:47:53 -05:00
firestarsdog ad948aa93e BigUI WIP: Some Font Smoothing 2026-04-06 22:23:37 -04:00
firestar5683 49f2e2854a Update carcontroller.py 2026-04-06 20:50:22 -05:00
firestar5683 c93675d153 Update speed_limit_us_detector.onnx 2026-04-06 20:18:23 -05:00
firestar5683 7ca1d6caca Merry Crisis 2026-04-06 19:14:50 -05:00
firestar5683 eb04ec95bf vision speed scheduler 2026-04-06 18:57:38 -05:00
firestar5683 3aa6a8ce52 volt steer ratio 2026-04-06 18:39:43 -05:00
firestar5683 deff72c5e4 long manuevers / model manager 2026-04-06 18:34:30 -05:00
firestar5683 19068fc2cc Update longitudinal_maneuvers.js 2026-04-06 18:12:42 -05:00
firestar5683 bcce932117 pedal creep 2026-04-06 17:45:04 -05:00
firestar5683 f859b2636d paddle state machine 2026-04-06 17:38:32 -05:00
firestar5683 f1feaa2cb9 long manuevers 2026-04-06 17:24:24 -05:00
firestar5683 033d83252e sounds 2026-04-06 15:29:50 -05:00
firestar5683 d745f51b22 Update override.toml 2026-04-06 14:47:29 -05:00
firestar5683 99771693fd patches 2026-04-06 14:18:10 -05:00
firestar5683 da3a047bb0 volt test tune 2026-04-06 13:13:48 -05:00
firestar5683 cc653f3bad saveme 2026-04-06 02:41:21 -05:00
firestar5683 aa427a6ee1 build 2026-04-06 02:25:41 -05:00
firestar5683 db2d380a6d patch 2026-04-06 02:14:53 -05:00
firestar5683 5b8fb4c4f1 toyboy galaxy 2026-04-06 02:14:53 -05:00
firestar5683 570d026542 test2 2026-04-06 02:14:53 -05:00
firestar5683 cda677c4dc Stopped Timer / Driver Cam on reverse / Camera view 2026-04-06 02:14:53 -05:00
firestar5683 1d49528e37 test fixes 2026-04-06 02:14:53 -05:00
firestarsdog 447b894aae BigUI WIP: More Settings Porting 2026-04-06 02:52:34 -04:00
firestarsdog 3a4a3b723e Dev Tools : Update Scripts 2026-04-06 02:49:59 -04:00
firestar5683 9972bbfb88 Pet watchdogs 2026-04-06 00:57:51 -05:00
firestar5683 eaab1cec23 can filter 2026-04-06 00:31:28 -05:00
firestar5683 af0faa1beb suburban 2026-04-05 22:45:24 -05:00
firestar5683 b1cf0aef68 build 2026-04-05 18:01:23 -05:00
Dom 701080a672 Merge pull request #50 from FLYINORION123/honda-speedlimit-dev
Honda Dashboard Speed Limit Controller (SLC) Support
2026-04-05 18:00:38 -05:00
firestar5683 523c12257b Lancia Delta 2026-04-05 17:40:44 -05:00
firestar5683 bad1a01c27 PRs 2026-04-05 17:18:38 -05:00
firestar5683 817a71ab73 mapbox 2026-04-05 17:08:05 -05:00
firestar5683 b6fa173103 Timeout 2026-04-05 15:05:38 -05:00
firestarsdog ba4cca6221 SLC Alert Update 2026-04-05 03:15:52 -04:00
firestar5683 876003da36 patches 2026-04-04 19:50:14 -05:00
firestar5683 4ebe5a277a custom accel and screen timeout 2026-04-04 19:22:18 -05:00
firestar5683 5f1e0eeb3a Update hud_renderer.py 2026-04-04 16:57:41 -05:00
firestar5683 10cab1ceff SLC 2026-04-04 10:58:40 -05:00
firestar5683 4a4b918d28 Update starpilot_process.py 2026-04-04 10:30:40 -05:00
firestar5683 85a1e92c26 new model 2026-04-03 19:44:30 -05:00
firestarsdog 2df630c727 Small dev env add 2026-04-03 19:18:39 -04:00
firestar5683 f8a2afe4e1 changed my mind again 2026-04-03 16:06:39 -05:00
firestar5683 766cd1ed92 maps 2026-04-03 15:58:20 -05:00
firestar5683 19da449199 opv8 2026-04-03 13:59:04 -05:00
firestar5683 b381fd8f44 training data 2026-04-03 13:05:27 -05:00
firestar5683 a695eda0aa safety suite 2026-04-03 12:43:34 -05:00
firestar5683 bbec85587f 0xbe 2026-04-03 12:04:21 -05:00
firestarsdog 99d8bb6efc Stop the Gate 2026-04-03 12:01:29 -04:00
firestarsdog 65b81c5710 BigUI WIP: Adj Path Bugfixes 2026-04-03 09:57:52 -04:00
firestarsdog 44bd84448c BigUI WIP: Aethergrid Update 2026-04-02 21:46:04 -04:00
firestar5683 1c95f0ebd0 cruise interval 2026-04-02 20:43:30 -05:00
firestar5683 74e580c584 param 2026-04-02 15:26:01 -05:00
firestar5683 62ed22177c update stagger 2026-04-02 14:49:53 -05:00
firestar5683 6346e05c5b force standstill 2026-04-02 14:29:08 -05:00
firestar5683 f8c5e90e3d driving model / dm changes 2026-04-02 14:01:58 -05:00
firestarsdog 0558712c47 BigUI WIP: Move Panel/StarPilot Default 2026-04-02 14:58:19 -04:00
firestar5683 a2008160c6 long mpc 2026-04-02 13:36:12 -05:00
Dom 6099e10998 Merge pull request #47 from firestar5683/codex/find-and-patch-braking-issue-in-chill-mode
Restore minimum lead-tracking buffer to fix chill-mode lead stopping
2026-04-02 11:48:45 -05:00
Dom fbdf401d1c Fix chill lead tracking regression from stop-distance buffer change 2026-04-02 11:48:31 -05:00
firestar5683 f14fa204fd Revert "saveme"
This reverts commit 2f9cdc2beb.
2026-04-02 10:05:50 -05:00
firestarsdog 57b8f97edf Aethergrid2 2026-04-02 09:58:35 -04:00
firestarsdog 30cfcd12d1 Aethergrid 2026-04-02 09:57:35 -04:00
firestar5683 06429a52df build 2026-04-02 01:47:37 -05:00
firestar5683 91cc63d68f messaged 2026-04-02 01:43:58 -05:00
firestar5683 a16ec85c27 no two publishers 2026-04-02 01:43:58 -05:00
firestar5683 5a91f3f1ca build 2026-04-02 01:43:57 -05:00
firestar5683 b0817dacef remove lon slope 2026-04-02 01:43:57 -05:00
firestar5683 9b075320b4 malibu 2026-04-02 01:43:57 -05:00
firestar5683 b6fec96236 Update carcontroller.py 2026-04-02 01:43:57 -05:00
firestar5683 054f886551 SLC Default Off 2026-04-02 01:43:56 -05:00
firestar5683 831f4f77f5 big oopsie 2026-04-02 01:43:56 -05:00
firestar5683 e8e765cb02 build fixes 2026-04-02 01:43:56 -05:00
firestar5683 4ebed61c3e malibu 2026-04-01 23:41:08 -05:00
firestar5683 a1f24c91e4 Update carcontroller.py 2026-04-01 23:09:06 -05:00
firestar5683 04bc9d275d SLC Default Off 2026-04-01 19:09:56 -05:00
firestar5683 b2c7e73637 big oopsie 2026-04-01 19:00:44 -05:00
firestar5683 37c52d1839 build fixes 2026-04-01 15:42:32 -05:00
firestar5683 f91eed0586 memory leaks 2026-04-01 12:13:37 -05:00
firestar5683 f6377538e9 Update starpilot_planner.py 2026-04-01 10:08:48 -05:00
firestar5683 105e879ae3 build 2026-04-01 10:02:01 -05:00
firestar5683 1e79d37919 disable human likes 2026-04-01 10:01:04 -05:00
firestar5683 daccf4e4ed throttle 2026-04-01 09:04:39 -05:00
firestar5683 fbbfeec0a8 build 2026-04-01 08:57:11 -05:00
firestar5683 902e61831d build 2026-04-01 08:31:26 -05:00
firestar5683 2f9cdc2beb saveme 2026-04-01 08:07:46 -05:00
firestarsdog 12453f78e9 BigUI WIP: Curve Speed Border + SLC 2026-04-01 03:30:09 -04:00
firestar5683 73f5fc84aa patch 2026-03-31 21:09:33 -05:00
firestar5683 0d9c58ac51 malibu 2026-03-31 20:15:14 -05:00
firestar5683 1f9849d752 compile 2026-03-31 19:59:25 -05:00
firestar5683 fd2209b01c panda and stats 2026-03-31 19:54:42 -05:00
firestar5683 b37bc78810 Update speed vision 2026-03-31 17:08:08 -05:00
firestar5683 bc988cb02b build 2026-03-31 14:35:36 -05:00
firestar5683 863b628946 show speed 2026-03-31 13:49:14 -05:00
firestar5683 a4c5b6e934 build 2026-03-31 13:29:56 -05:00
firestar5683 95107573aa planner bounds 2026-03-31 13:27:24 -05:00
firestar5683 22669da61c try paddle 2026-03-31 13:27:24 -05:00
firestarsdog f9131e4f02 BigUI StarPilot Layer + AOL + Personality Buttons 2026-03-31 13:27:24 -05:00
firestarsdog 23a7fd7d81 Fix mici replay on tizi ui 2026-03-31 13:27:23 -05:00
firestarsdog ba7e1ad8a8 raybig might run? 2026-03-31 13:27:23 -05:00
firestar5683 28d22d7a26 compile 2026-03-31 13:27:23 -05:00
firestar5683 ad06ffb4ce goat 2026-03-31 13:27:23 -05:00
firestar5683 ac47a9b73b troubleshoot 2026-03-31 13:27:22 -05:00
firestar5683 6bbe1dc4a1 Update device_settings_layout.json 2026-03-31 13:27:22 -05:00
firestar5683 fe4f42a616 friar carl 2026-03-31 13:27:22 -05:00
firestar5683 675227a8eb pond 2026-03-30 01:36:05 -05:00
firestar5683 bccffecef3 fix 2026-03-30 01:18:04 -05:00
firestar5683 f6b41914b9 fix 2026-03-30 01:11:27 -05:00
firestar5683 a4444ce617 troubleshoot 2026-03-30 00:44:32 -05:00
firestar5683 80f48a3b85 stock theme 2026-03-30 00:40:52 -05:00
firestar5683 2c3fc143a3 plot grading 2026-03-29 23:43:16 -05:00
firestarsdog 28d1ef85e6 Dev temp file/cache cleanip 2026-03-29 23:43:16 -05:00
firestar5683 5a5de70303 Update carcontroller.py 2026-03-29 23:43:15 -05:00
firestar5683 032b00affb curve speed and paddle commands 2026-03-29 23:43:15 -05:00
firestar5683 e8cce63b7f fixes 2026-03-29 23:43:15 -05:00
firestar5683 a6721ca3af do not hide system critical alerts 2026-03-29 23:43:15 -05:00
firestar5683 da0be57e88 Revert "test ui crash"
This reverts commit 2dd436c40d.
2026-03-29 23:43:15 -05:00
firestar5683 9ed1fc74e8 delete duplicate 2026-03-29 23:43:14 -05:00
firestar5683 1bbee6c390 fix 2026-03-29 23:43:14 -05:00
firestar5683 0f52ad6a28 Update params.toml 2026-03-29 23:43:14 -05:00
firestar5683 b7d448e064 ./onroad 2026-03-29 23:43:14 -05:00
firestar5683 f5d0eee724 compile 2026-03-29 15:32:50 -05:00
firestar5683 ec152f73f2 fix sdgm volt 2026-03-29 14:48:44 -05:00
firestar5683 6640a403b7 fix sdgm sascm 2026-03-29 14:20:59 -05:00
firestar5683 9f7f893668 min 2026-03-29 13:56:03 -05:00
firestar5683 d2bb05b61c personality 2026-03-29 09:53:32 -05:00
firestar5683 d3316f29f4 coasting 2026-03-29 09:45:50 -05:00
firestar5683 d42dff9d93 high sped interp back 2026-03-29 09:36:28 -05:00
firestar5683 64a80a5d24 radar fix 2026-03-29 09:23:27 -05:00
firestarsdog 8efbee199f Ubuntu 25.10 Build Patch 2026-03-29 04:32:40 -04:00
firestar5683 c2572f50a4 galaxy 2026-03-28 23:03:51 -05:00
firestar5683 2f1bb550e9 Galaxy 2026-03-28 22:38:29 -05:00
firestar5683 2dd436c40d test ui crash 2026-03-28 19:10:57 -05:00
firestar5683 865fd4ce75 where is rockhopper 2026-03-28 18:54:17 -05:00
firestar5683 58504c5a0f My username was friar carl 2026-03-28 18:35:58 -05:00
firestar5683 0f70d061e8 Ya'll remember club penguin? 2026-03-28 18:35:57 -05:00
firestarsdog 82619bd7db openmeteo adapter 2026-03-28 12:50:33 -04:00
firestar5683 8fafa6e8d4 No Dev Blocking 2026-03-27 23:28:47 -05:00
firestar5683 d36b9402d9 Revert "test"
This reverts commit b00ffb9a2b.
2026-03-27 23:21:20 -05:00
firestar5683 b00ffb9a2b test 2026-03-27 23:10:56 -05:00
firestar5683 09929f2454 lateral tuning migration and dashcam mode 2026-03-27 22:00:10 -05:00
firestar5683 741131974c fix UI crash? 2026-03-27 19:29:01 -05:00
firestar5683 9bb2a6904e UI restart 2026-03-27 19:26:18 -05:00
firestar5683 f256e8ea02 galaxy lost labels 2026-03-27 19:15:48 -05:00
firestar5683 742517b619 fix dashcam footage in galaxy 2026-03-27 19:15:31 -05:00
firestar5683 1296483543 show all toggles 2026-03-27 19:03:01 -05:00
firestar5683 3d8af2361e Rename 2026-03-27 18:05:44 -05:00
firestar5683 ac4cec2e22 da crap 2026-03-27 16:32:13 -05:00
firestar5683 6f1abdfd54 ev tuning 2026-03-27 11:38:06 -05:00
firestar5683 40c0ae3c93 accel profiles 2026-03-27 11:29:46 -05:00
firestar5683 3fe8671e90 auto updates 2026-03-27 11:13:33 -05:00
firestar5683 ca92674263 torque bar color 2026-03-27 10:48:56 -05:00
firestar5683 60a3a81d7c lane changes 2026-03-27 10:43:35 -05:00
firestar5683 056dc6df4e patch 2026-03-27 02:35:39 -05:00
firestar5683 be38557680 no border when disabled 2026-03-27 01:41:28 -05:00
firestar5683 280e0d7e94 compile 2026-03-27 01:32:14 -05:00
firestar5683 0f4bae1c77 TOOLS 2026-03-27 01:26:49 -05:00
firestar5683 431d59d731 long 2026-03-27 01:26:49 -05:00
Michael Tawata 8f04ab44fe readme overhaul 2026-03-26 12:09:56 -05:00
firestar5683 cc9a0c643c Long? 2026-03-26 12:09:56 -05:00
firestar5683 1bcfb0518d Mici Model Switcher 2026-03-26 00:48:55 -05:00
firestar5683 c52280ea04 Revert "paddle"
This reverts commit 96b27b422c.
2026-03-25 22:36:43 -05:00
firestar5683 96b27b422c paddle 2026-03-25 22:24:13 -05:00
firestar5683 b47cad3bab Revert "paddle"
This reverts commit 2fb8526d01.
2026-03-25 22:18:05 -05:00
firestar5683 310d852cf3 no pedal long pitch 2026-03-25 22:11:40 -05:00
firestar5683 8b02280419 wheel themes 2026-03-25 21:53:36 -05:00
firestar5683 2fb8526d01 paddle 2026-03-25 21:50:09 -05:00
firestar5683 d3b1cc8e69 mici remember networks 2026-03-25 21:43:19 -05:00
firestar5683 4046360299 mici network manager / allow uploads / long pitch / cruise offset 2026-03-25 21:23:15 -05:00
firestar5683 6a639c8619 compile 2026-03-25 15:56:12 -05:00
firestar5683 e9100341fc a target limiter 2026-03-25 15:55:22 -05:00
firestar5683 efb9884039 Networking Pond 2026-03-25 15:55:14 -05:00
firestar5683 4b1dee2466 Paddle 2026-03-25 15:46:29 -05:00
firestar5683 3e31394aaa minsteerspeed 2026-03-25 14:06:05 -05:00
firestar5683 00b8eb2b20 Long changes 2026-03-25 13:04:41 -05:00
firestar5683 187eb34733 Revert "cruise set overshoot?"
This reverts commit c726bf2b98.
2026-03-25 09:13:28 -05:00
firestar5683 d070d6c395 cem 2026-03-25 09:13:07 -05:00
firestar5683 edbc7e9931 unscaled threshold? 2026-03-25 09:07:15 -05:00
firestar5683 38fc73efc7 compile 2026-03-25 08:58:15 -05:00
firestar5683 61a9a12753 gas scaling 2026-03-25 08:57:14 -05:00
firestar5683 c726bf2b98 cruise set overshoot? 2026-03-25 08:34:40 -05:00
firestar5683 78683b1228 paddle 2026-03-25 08:19:50 -05:00
firestar5683 22b4aabece try cancel 2026-03-24 22:52:57 -05:00
firestar5683 99c5294647 keep onboarding for mici 2026-03-24 22:32:33 -05:00
firestar5683 73d2c910eb compile 2026-03-24 22:32:33 -05:00
firestar5683 8e3753d55b Pedal tune 2026-03-24 22:32:33 -05:00
firestar5683 c82a1a3830 long planner 2026-03-24 22:32:33 -05:00
firestar5683 291f63391b 3d1 spoof 2026-03-24 22:32:32 -05:00
firestar5683 1a9e5cf364 raw params in model switcher 2026-03-24 19:57:49 -05:00
firestar5683 1b93b4f777 stale model files 2026-03-24 19:56:01 -05:00
firestar5683 623669c2b3 build 2026-03-24 18:03:25 -05:00
firestar5683 10948af7ac longitudinal parity: restore StarPilot lead tracking and accel behaviors 2026-03-24 17:55:06 -05:00
firestar5683 38a9eef286 mici wifi: fix connected status and saved SSID matching 2026-03-24 16:21:45 -05:00
firestar5683 8d83e0cdcd Backport Mici UI polish from SP-Dev-Dom (exclude wifi_manager) 2026-03-24 16:05:33 -05:00
firestar5683 948a4e1eb6 Compile 2026-03-24 15:53:15 -05:00
firestar5683 abe565deed ./models 2026-03-24 15:50:04 -05:00
firestar5683 2227ac8052 Bring back temporalPose path, mapd wrapper, and offroad-only backup behavior 2026-03-24 15:47:19 -05:00
firestar5683 076bda91a0 Don't go onroad til controls are allowed 2026-03-24 15:34:30 -05:00
firestar5683 68d1c3d362 Backport requested GM/monitoring/long/cruise fixes onto Dom 2026-03-24 15:27:59 -05:00
firestar5683 78aaa913e7 dm: sync new tinygrad driver monitoring stack from SP-Dev-Dom-fixed 2026-03-24 15:23:37 -05:00
firestar5683 7a499b5933 Revert "ui: port mici visual polish and Wi-Fi status updates"
This reverts commit d2819e5a04.
2026-03-24 14:40:34 -05:00
firestar5683 d2819e5a04 ui: port mici visual polish and Wi-Fi status updates
volt dbc

Don't go onroad til controls are allowed

Button Mici

Start of Day 3

./models

ui: keep mici toggle labels at a stable size

compile
2026-03-24 14:40:31 -05:00
firestar5683 77764bd345 opendbc: fix GM pedal RX checks and restore ASCM F1 safety coverage 2026-03-23 01:53:53 -05:00
firestar5683 4368dd368b opendbc: add Escalade ASCM support and drop GM dashcam gating 2026-03-23 01:53:27 -05:00
firestarsdog 003dfa8f8e Keeb 2026-03-23 01:44:29 -04:00
firestarsdog 4d5d48dbe8 Rays 2026-03-22 22:25:32 -05:00
firestar5683 8258b6db84 Monitoring left hand 2026-03-22 22:17:40 -05:00
firestarsdog e4a1c33f35 Sigh question marks... 2026-03-22 22:17:40 -05:00
firestar5683 0e57f230d7 day2 2026-03-22 22:17:40 -05:00
firestar5683 a35382505e Lock driver monitoring Side 2026-03-22 14:06:22 -05:00
firestar5683 7000063c41 stop cruise main drop? 2026-03-22 14:06:22 -05:00
firestar5683 d0e1db6766 StarPilot 2026-03-22 03:15:05 -05:00
James 0e9ef526f7 Fix? 2026-03-06 14:48:44 -07:00
James d242fd96d5 Move to FrogPilot API 2026-03-06 03:43:30 -07:00
James c5a332081d WMI model 2026-02-13 11:04:27 -07:00
James d358250853 Update mapd to v2 2026-02-13 11:04:26 -07:00
James 5e224f8c9a Fixes 2026-02-13 11:04:26 -07:00
James 88816b375a Disable the "DRIVING MODEL" panel 2026-02-13 00:21:39 -07:00
James bcab265c86 ZSS support
Co-Authored-By: Erich Moraga <33645296+erichmoraga@users.noreply.github.com>
2026-02-13 00:21:39 -07:00
James 8927c23a33 Weather condition offsets 2026-02-13 00:21:39 -07:00
James db9bcb2967 Vehicle fingerprint selection 2026-02-13 00:21:39 -07:00
James 59241611b8 Use Wheel Speed 2026-02-13 00:21:39 -07:00
James 562e79f348 Use "single_pedal_mode" with regen
Co-Authored-By: garrettpall <76917194+garrettpall@users.noreply.github.com>
2026-02-13 00:21:39 -07:00
James 8dd23a7aa2 Turn Desires 2026-02-13 00:21:39 -07:00
James 590649eaf2 Traffic Mode 2026-02-13 00:21:39 -07:00
James b9d0f979d2 "Stop-and-Go Hack" for Toyota/Lexus 2026-02-13 00:21:39 -07:00
James 495da78739 "Stop-and-Go Hack" for 2017 Chevy Volt
Co-Authored-By: Tim Wilson <7284371+twilsonco@users.noreply.github.com>
2026-02-13 00:21:39 -07:00
James 7f9430751c "Stop-and-Go" for Subaru
Co-Authored-By: martinl <148686+martinl@users.noreply.github.com>
2026-02-13 00:21:38 -07:00
James 912c5c2611 Standby Mode 2026-02-13 00:21:38 -07:00
James 67f3371b45 Speed Limit Filler 2026-02-13 00:21:38 -07:00
James 3721245e1d Speed Limit Controller
Co-Authored-By: eFini <16603033+efinilan@users.noreply.github.com>
Co-Authored-By: Jacob Pfeifer <jacob@pfeifer.dev>
2026-02-13 00:21:38 -07:00
James f0444110c9 Sidebar Metrics 2026-02-13 00:21:38 -07:00
James 7919e10c10 Show Driver Camera When In Reverse 2026-02-13 00:21:38 -07:00
James 2f31c511f0 SDSU support 2026-02-13 00:21:38 -07:00
James e5b985f122 Screen Timeout 2026-02-13 00:21:38 -07:00
James cbf54aae3e Screen Recorder 2026-02-13 00:21:38 -07:00
James d7cb6b0ca2 Screen Brightness 2026-02-13 00:21:38 -07:00
James b5ad90fa64 Rotate the steering wheel icon 2026-02-13 00:21:38 -07:00
James 7ca4a582d5 Reverse cruise increase for Toyota/Lexus 2026-02-13 00:21:38 -07:00
James 2e9c26316e Retain the sidebar's position 2026-02-13 00:21:38 -07:00
James 81fbf42cd1 Retain network tethering 2026-02-13 00:21:38 -07:00
James fab2b745ac Report a Bug or an Issue 2026-02-13 00:21:38 -07:00
James 065b2f6356 Remove "dashcamOnly" flag check 2026-02-13 00:21:38 -07:00
James c31148f7f1 Random Themes 2026-02-13 00:21:37 -07:00
James cfb0d01ebd Random Events 2026-02-13 00:21:37 -07:00
James fec050a3c1 Rainbow Road™ 2026-02-13 00:21:37 -07:00
James 45a36222a4 Pause steering with an active turn signal 2026-02-13 00:21:37 -07:00
James f087ceff51 Pause Steering 2026-02-13 00:21:37 -07:00
James 39611ad6dd Pause Acceleration/Braking 2026-02-13 00:21:37 -07:00
James ae081bfd56 Path Edges 2026-02-13 00:21:37 -07:00
James 3a44c31bb0 Only one lane change per signal 2026-02-13 00:21:37 -07:00
James 211fa76107 Only change lanes when a lane is detected 2026-02-13 00:21:37 -07:00
James ada1fe0817 Only change driving personality if the menu is open (GM only) 2026-02-13 00:21:37 -07:00
James 024e1a77e2 Offset Set Speed
Co-Authored-By: Tim Wilson <7284371+twilsonco@users.noreply.github.com>
2026-02-13 00:21:37 -07:00
James a8122c8f05 Offline maps
Co-Authored-By: Jacob Pfeifer <jacob@pfeifer.dev>
2026-02-13 00:21:37 -07:00
James 83d1156bb4 Nudgeless lane changes delay
Update desire_helper.py
2026-02-13 00:21:37 -07:00
James 6141b5f2c1 Nudgeless lane changes 2026-02-13 00:21:37 -07:00
James b0af0289e9 Neural Network FeedForward (NNFF)
Co-Authored-By: Tim Wilson <7284371+twilsonco@users.noreply.github.com>
2026-02-13 00:21:36 -07:00
James e1de171de0 Model Manager 2026-02-13 00:21:36 -07:00
James dc54347f93 Mazda has blind spot monitoring
Co-Authored-By: bigboigahoy <49489685+bigboigahoy@users.noreply.github.com>
2026-02-13 00:21:36 -07:00
James 6c4addf05d Maximum Acceleration 2026-02-13 00:21:36 -07:00
James 7ab5f0c436 Low-Voltage Cutoff 2026-02-13 00:21:36 -07:00
James 81a7d571e6 Loud "Car Detected in Blindspot" Alert 2026-02-13 00:21:36 -07:00
James e57691a9b5 Lock doors when shifting out of park for Toyota/Lexus
Co-Authored-By: Alexandre Nobuharu Sato <66435071+alexandresato@users.noreply.github.com>
2026-02-13 00:21:36 -07:00
James e56294cc94 Lock doors after ignition off for Toyota/Lexus 2026-02-13 00:21:36 -07:00
James 91ce962e7c Lead Info 2026-02-13 00:21:36 -07:00
James af4e8a5ede Lead detection sensitivity 2026-02-13 00:21:36 -07:00
James 40b5e24b1b Lead Departing Alert 2026-02-13 00:21:36 -07:00
James dd47d0f413 Lane Detection 2026-02-13 00:21:36 -07:00
James 518da86590 Konik.ai server support
Co-Authored-By: MoreTorque <104254025+moretore@users.noreply.github.com>
2026-02-13 00:21:36 -07:00
James cc73376f75 Increase Temperature Limits 2026-02-13 00:21:35 -07:00
James 3e32a6a910 Increase Stopped Distance 2026-02-13 00:21:35 -07:00
James a52821e983 Increase steering torque for Subaru Impreza 2026-02-13 00:21:35 -07:00
James 1e21fe4acc Increase steering torque for "Global Gen2" Subaru 2026-02-13 00:21:35 -07:00
James 90883515fb Human-Like Lane Changes 2026-02-13 00:21:35 -07:00
James 68cd686b55 Human-Like Following 2026-02-13 00:21:35 -07:00
James f930a2a3a5 Human-Like Acceleration 2026-02-13 00:21:35 -07:00
James e0c1f3ec06 Hyundai Ioniq 6 has blind spot monitoring 2026-02-13 00:21:35 -07:00
James 4baa067d45 Holiday Themes 2026-02-13 00:21:35 -07:00
James 4f832f618e High-Quality Recordings 2026-02-13 00:21:34 -07:00
James 328d370ffb Hide Max Speed 2026-02-13 00:21:34 -07:00
James 8864f86b02 Hide Lead Marker 2026-02-13 00:21:34 -07:00
James ade0308007 Hide Current Speed 2026-02-13 00:21:34 -07:00
James 9861e6c630 Hide Alerts 2026-02-13 00:21:34 -07:00
James 48b3e1ee35 Green Light Alert 2026-02-13 00:21:34 -07:00
James 40c4e6dfcd Gracefully catch "set_core_affinity" errors 2026-02-13 00:21:34 -07:00
James be969678d0 Goat Scream 2026-02-13 00:21:34 -07:00
James 564633d0f6 Generate a fake "DongleID" for unofficial devices 2026-02-13 00:21:34 -07:00
James 823bc19e36 Gas / Brake Pedal Indicators 2026-02-13 00:21:34 -07:00
James 3955b1f70f FrogsGoMoo's personal tweaks for Toyota/Lexus 2026-02-13 00:21:34 -07:00
James b8d3d1e9cc Force Torque Controller 2026-02-13 00:21:34 -07:00
James 6efda62e23 Force Stop at "Detected" Stop Lights/Signs 2026-02-13 00:21:34 -07:00
James ae2009c207 Force openpilot to Coast 2026-02-13 00:21:34 -07:00
James 59272ea85c Force Drive State 2026-02-13 00:21:34 -07:00
James a27b1a5e6a Force Auto-Tune On 2026-02-13 00:21:34 -07:00
James e1f0b77910 Force Auto-Tune Off 2026-02-13 00:21:34 -07:00
James 6fe2973c3e Flash Panda 2026-02-13 00:21:33 -07:00
James 99e013e233 Far/radar only lead detection 2026-02-13 00:21:33 -07:00
James 4b57104d3e Extend the model UI projection 2026-02-13 00:21:33 -07:00
James a05c7b8f80 Error logs 2026-02-13 00:21:33 -07:00
James ab487baf0e Dynamic Path Width 2026-02-13 00:21:33 -07:00
James e5b7832be6 Driving Personality Button 2026-02-13 00:21:33 -07:00
James 7ba746e286 Drive Summary 2026-02-13 00:21:33 -07:00
James 0ed15a14ce Download prompt when switching branches 2026-02-13 00:21:33 -07:00
James e78b2760ca Display the turn signal/blind spot status in the screen's border 2026-02-13 00:21:33 -07:00
James 5075da5ac4 Display the steering torque in the screen's border 2026-02-13 00:21:33 -07:00
James 3cf04eb9cc Display the radar's track points 2026-02-13 00:21:33 -07:00
James 6a28fb4ada Display the model's stopping point 2026-02-13 00:21:33 -07:00
James 18fe10322b Display the FPS in the screen's border 2026-02-13 00:21:33 -07:00
James 0c6b96d9cd Display the current standstill time 2026-02-13 00:21:33 -07:00
James 6ab4df3197 Display the current road's name 2026-02-13 00:21:33 -07:00
James 999af5d1fb Disable Uploads 2026-02-13 00:21:32 -07:00
James b7db0c72ec Disable openpilot longitudinal 2026-02-13 00:21:32 -07:00
James d2da2e8a07 Disable Logging 2026-02-13 00:21:32 -07:00
James 93e5714330 Disable lane changes 2026-02-13 00:21:32 -07:00
James 3c4d22f8e6 Disable internet connectivity checks 2026-02-13 00:21:32 -07:00
James aa921a3996 Device Shutdown Timer 2026-02-13 00:21:32 -07:00
James 05029c814c Developer Sidebar 2026-02-13 00:21:32 -07:00
James 828728848c Detect if the driver is still in the car 2026-02-13 00:21:32 -07:00
James 3da8c27cc9 "Delete Driving Data" button 2026-02-13 00:21:32 -07:00
James c08c17ce66 Decimate "Torque Estimator" if not previously calibrated 2026-02-13 00:21:32 -07:00
James 583680d871 Debug Mode 2026-02-13 00:21:32 -07:00
James 2eff30c2b2 Date and version in home screen 2026-02-13 00:21:32 -07:00
James 42daa20b82 Custom "wrong_car_mode_alert" message for CC only cars 2026-02-13 00:21:32 -07:00
James ebc5be3eb5 Custom "vEgoCluster" offset for Toyota/Lexus 2026-02-13 00:21:32 -07:00
James 04f59c55d8 Custom Themes 2026-02-13 00:21:32 -07:00
James 06df794db2 Custom steering wheel button functions 2026-02-13 00:21:32 -07:00
James 461de7d0c0 Custom Model UI 2026-02-13 00:21:32 -07:00
James 3c77fb947b Custom minimum lane change speed 2026-02-13 00:21:31 -07:00
James 70a7318a3a Curve Speed Controller
Co-Authored-By: Jacob Pfeifer <jacob@pfeifer.dev>
2026-02-13 00:21:31 -07:00
James 6797e70f7e Cruise Interval 2026-02-13 00:21:31 -07:00
James 45ddd10358 Configure "Nissan Leaf 2018-23 - Instrument Cluster" 2026-02-13 00:21:31 -07:00
James 68b60b3635 Conditional Experimental Mode 2026-02-13 00:21:31 -07:00
James 7388432fb9 Compass 2026-02-13 00:21:31 -07:00
Adeeb Shihadeh 186c1a17f3 comma's steering speed hack from their 2022 Taco Bell Run
https://github.com/commaai/openpilot/commit/1801a14f85c93b3e1c461ddc5c552523bc8baffa
2026-02-13 00:21:31 -07:00
James f25cb9bd7c Chevrolet TRAX 2024
Co-Authored-By: darkerthan-black <40709016+darkerthan-black@users.noreply.github.com>
2026-02-13 00:21:31 -07:00
James 2f86ec9b1e Change the steering wheel icon's background color 2026-02-13 00:21:31 -07:00
James 2d426739d7 Camera view selection 2026-02-13 00:21:31 -07:00
James b2d94a9f0d Block users from using development branches 2026-02-13 00:21:31 -07:00
James ffd7259193 Blind spot path 2026-02-13 00:21:31 -07:00
James a5bd0d082e "Back" button in settings menu 2026-02-13 00:21:31 -07:00
James 48e1f14229 Automatic updates 2026-02-13 00:21:31 -07:00
James 5cdc5b7991 Automatic translations 2026-02-13 00:21:31 -07:00
James 3832c73da9 Automatic backups 2026-02-13 00:21:31 -07:00
James e97b9aca53 Always On Lateral
Co-Authored-By: Jacob Pfeifer <jacob@pfeifer.dev>
2026-02-13 00:21:30 -07:00
James 19246a8de0 Always allow "openpilot Longitudinal Control (Alpha)" 2026-02-13 00:21:30 -07:00
James 0a1c363661 Always allow branch switching 2026-02-13 00:21:30 -07:00
James 4b5c59d32c Alert Volume Controller 2026-02-13 00:21:30 -07:00
James 65c768cff0 Advanced longitudinal tuning 2026-02-13 00:21:30 -07:00
James 031e3fa609 Advanced lateral tuning 2026-02-13 00:21:30 -07:00
James 26f33e546e Adjacent leads tracking 2026-02-13 00:21:30 -07:00
James 94e4cc5c7c Add community DBC imports 2026-02-13 00:21:30 -07:00
James 74138472b7 Add back "set_timezone" 2026-02-13 00:21:29 -07:00
James 3ca3cb3312 Acceleration Path 2026-02-13 00:21:29 -07:00
James cc612ebd03 Acceleration/Deceleration Profiles 2026-02-13 00:21:29 -07:00
James ab8c4dea61 FrogPilot workflows 2026-02-13 00:21:29 -07:00
James f549233611 FrogPilot variables 2026-02-13 00:21:29 -07:00
James 2f00fc348d FrogPilot utilities 2026-02-13 00:21:28 -07:00
James 95583fb304 FrogPilot tracking 2026-02-13 00:21:26 -07:00
James b0c62707ad FrogPilot structs 2026-02-13 00:21:26 -07:00
James c2406d9c29 FrogPilot Sentry reporting 2026-02-13 00:21:26 -07:00
James 7218d74ec5 FrogPilot release/tested branches 2026-02-13 00:21:26 -07:00
James 87d5574e7a FrogPilot README 2026-02-13 00:21:26 -07:00
James e2cd1568e6 FrogPilot param functions 2026-02-13 00:21:26 -07:00
James b9f00dd14c FrogPilot panel 2026-02-13 00:20:57 -07:00
James ef16012768 FrogPilot languages
Co-Authored-By: Chechulin Serhii <78239416+keefeere@users.noreply.github.com>
2026-02-13 00:20:57 -07:00
James 17a81ac769 FrogPilot conversions 2026-02-13 00:20:57 -07:00
James e49978beff FrogPilot branding 2026-02-13 00:20:57 -07:00
James 1b590b15d6 FrogPilot base 2026-02-13 00:20:56 -07:00
Vincent Wright ff862a5f77 RealFast v0.10.3 release
https://github.com/realfast/openpilot
2026-01-12 12:33:47 -07:00
Eric Brown fb428dfcbf OPGM v0.10.3 release
https://github.com/opgm/openpilot
2026-01-12 12:33:47 -07:00
Vehicle Researcher 33d5cfc393 openpilot v0.10.3 release
date: 2025-12-18T23:23:16
master commit: 3cdee7b54718ee14bd85befd6c5bad3d699c5479
2025-12-18 23:23:21 -08:00
3714 changed files with 755683 additions and 354702 deletions
+58
View File
@@ -0,0 +1,58 @@
name: 'automatically cache based on current runner'
inputs:
path:
description: 'path to cache'
required: true
key:
description: 'key'
required: true
restore-keys:
description: 'restore-keys'
required: true
save:
description: 'whether to save the cache'
default: 'true'
required: false
outputs:
cache-hit:
description: 'cache hit occurred'
value: ${{ (contains(runner.name, 'nsc') && steps.ns-cache.outputs.cache-hit) ||
(!contains(runner.name, 'nsc') && inputs.save != 'false' && steps.gha-cache.outputs.cache-hit) ||
(!contains(runner.name, 'nsc') && inputs.save == 'false' && steps.gha-cache-ro.outputs.cache-hit) }}
runs:
using: "composite"
steps:
- name: setup namespace cache
id: ns-cache
if: ${{ contains(runner.name, 'nsc') }}
uses: namespacelabs/nscloud-cache-action@v1
with:
path: ${{ inputs.path }}
- name: setup github cache
id: gha-cache
if: ${{ !contains(runner.name, 'nsc') && inputs.save != 'false' }}
uses: 'actions/cache@v4'
with:
path: ${{ inputs.path }}
key: ${{ inputs.key }}
restore-keys: ${{ inputs.restore-keys }}
- name: setup github cache
id: gha-cache-ro
if: ${{ !contains(runner.name, 'nsc') && inputs.save == 'false' }}
uses: 'actions/cache/restore@v4'
with:
path: ${{ inputs.path }}
key: ${{ inputs.key }}
restore-keys: ${{ inputs.restore-keys }}
# make the directory manually in case we didn't get a hit, so it doesn't fail on future steps
- id: scons-cache-setup
shell: bash
run: |
mkdir -p ${{ inputs.path }}
sudo chmod -R 777 ${{ inputs.path }}
sudo chown -R $USER ${{ inputs.path }}
+52
View File
@@ -0,0 +1,52 @@
name: "PR review"
on:
pull_request_target:
types: [opened, reopened, synchronize, edited]
jobs:
labeler:
name: review
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: false
# Label PRs
- uses: actions/labeler@v5.0.0
with:
dot: true
configuration-path: .github/labeler.yaml
# Check PR target branch
- name: check branch
uses: Vankka/pr-target-branch-action@def32ec9d93514138d6ac0132ee62e120a72aed5
if: github.repository == 'commaai/openpilot'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
target: /^(?!master$).*/
exclude: /commaai:.*/
change-to: ${{ github.base_ref }}
already-exists-action: close_this
already-exists-comment: "Your PR should be made against the `master` branch"
# Welcome comment
- name: "First timers PR"
uses: actions/first-interaction@v1
if: github.event.pull_request.head.repo.full_name != 'commaai/openpilot'
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-message: |
<!-- _(run_id **${{ github.run_id }}**)_ -->
Thanks for contributing to openpilot! In order for us to review your PR as quickly as possible, check the following:
* Convert your PR to a draft unless it's ready to review
* Read the [contributing docs](https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md)
* Before marking as "ready for review", ensure:
* the goal is clearly stated in the description
* all the tests are passing
* the change is [something we merge](https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md#what-gets-merged)
* include a route or your device' dongle ID if relevant
+37
View File
@@ -0,0 +1,37 @@
name: badges
on:
schedule:
- cron: '0 * * * *'
workflow_dispatch:
env:
BASE_IMAGE: openpilot-base
DOCKER_REGISTRY: ghcr.io/commaai
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $DOCKER_REGISTRY/$BASE_IMAGE:latest /bin/bash -c
jobs:
badges:
name: create badges
runs-on: ubuntu-latest
if: github.repository == 'commaai/openpilot'
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
- name: Push badges
run: |
${{ env.RUN }} "python3 selfdrive/ui/translations/create_badges.py"
rm .gitattributes
git checkout --orphan badges
git rm -rf --cached .
git config user.email "badge-researcher@comma.ai"
git config user.name "Badge Researcher"
git add translation_badge.svg
git commit -m "Add/Update badges"
git push -f origin HEAD
+101
View File
@@ -0,0 +1,101 @@
name: weekly CI test report
on:
schedule:
- cron: '37 9 * * 1' # 9:37AM UTC -> 2:37AM PST every monday
workflow_dispatch:
inputs:
ci_runs:
description: 'The amount of runs to trigger in CI test report'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CI_RUNS: ${{ github.event.inputs.ci_runs || '50' }}
jobs:
setup:
if: github.repository == 'commaai/openpilot'
runs-on: ubuntu-latest
outputs:
ci_runs: ${{ steps.ci_runs_setup.outputs.matrix }}
steps:
- id: ci_runs_setup
name: CI_RUNS=${{ env.CI_RUNS }}
run: |
matrix=$(python3 -c "import json; print(json.dumps({ 'run_number' : list(range(${{ env.CI_RUNS }})) }))")
echo "matrix=$matrix" >> $GITHUB_OUTPUT
ci_matrix_run:
needs: [ setup ]
strategy:
fail-fast: false
matrix: ${{fromJSON(needs.setup.outputs.ci_runs)}}
uses: commaai/openpilot/.github/workflows/ci_weekly_run.yaml@master
with:
run_number: ${{ matrix.run_number }}
report:
needs: [ci_matrix_run]
runs-on: ubuntu-latest
if: always() && github.repository == 'commaai/openpilot'
steps:
- name: Get job results
uses: actions/github-script@v7
id: get-job-results
with:
script: |
const jobs = await github
.paginate("GET /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt}/jobs", {
owner: "commaai",
repo: "${{ github.event.repository.name }}",
run_id: "${{ github.run_id }}",
attempt: "${{ github.run_attempt }}",
})
var report = {}
jobs.slice(1, jobs.length-1).forEach(job => {
if (job.conclusion === "skipped") return;
const jobName = job.name.split(" / ")[2];
const runRegex = /\((.*?)\)/;
const run = job.name.match(runRegex)[1];
report[jobName] = report[jobName] || { successes: [], failures: [], canceled: [] };
switch (job.conclusion) {
case "success":
report[jobName].successes.push({ "run_number": run, "link": job.html_url}); break;
case "failure":
report[jobName].failures.push({ "run_number": run, "link": job.html_url }); break;
case "canceled":
report[jobName].canceled.push({ "run_number": run, "link": job.html_url }); break;
}
});
return JSON.stringify({"jobs": report});
- name: Add job results to summary
env:
JOB_RESULTS: ${{ fromJSON(steps.get-job-results.outputs.result) }}
run: |
cat <<EOF >> template.html
<table>
<thead>
<tr>
<th></th>
<th>Job</th>
<th>✅ Passing</th>
<th>❌ Failure Details</th>
</tr>
</thead>
<tbody>
{% for key in jobs.keys() %}<tr>
<td>{% for i in range(5) %}{% if i+1 <= (5 * jobs[key]["successes"]|length // ${{ env.CI_RUNS }}) %}🟩{% else %}🟥{% endif %}{% endfor%}</td>
<td>{{ key }}</td>
<td>{{ 100 * jobs[key]["successes"]|length // ${{ env.CI_RUNS }} }}%</td>
<td>{% if jobs[key]["failures"]|length > 0 %}<details>{% for failure in jobs[key]["failures"] %}<a href="{{ failure['link'] }}">Log for run #{{ failure['run_number'] }}</a><br>{% endfor %}</details>{% else %}{% endif %}</td>
</td>
</tr>{% endfor %}
</table>
EOF
pip install jinja2-cli
echo $JOB_RESULTS | jinja2 template.html > report.html
echo "# CI Test Report - ${{ env.CI_RUNS }} Runs" >> $GITHUB_STEP_SUMMARY
cat report.html >> $GITHUB_STEP_SUMMARY
+17
View File
@@ -0,0 +1,17 @@
name: weekly CI test run
on:
workflow_call:
inputs:
run_number:
required: true
type: string
concurrency:
group: ci-run-${{ inputs.run_number }}-${{ github.ref }}
cancel-in-progress: true
jobs:
tests:
uses: commaai/openpilot/.github/workflows/tests.yaml@master
with:
run_number: ${{ inputs.run_number }}
@@ -0,0 +1,21 @@
name: 'compile openpilot'
runs:
using: "composite"
steps:
- shell: bash
name: Build openpilot with all flags
run: |
${{ env.RUN }} "scons -j$(nproc)"
${{ env.RUN }} "release/check-dirty.sh"
- shell: bash
name: Cleanup scons cache and rebuild
run: |
${{ env.RUN }} "rm -rf /tmp/scons_cache/* && \
scons -j$(nproc) --cache-populate"
- name: Save scons cache
uses: actions/cache/save@v4
if: github.ref == 'refs/heads/master'
with:
path: .ci_cache/scons_cache
key: scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
+239
View File
@@ -0,0 +1,239 @@
name: Compile StarPilot
on:
workflow_dispatch:
inputs:
not_vetted:
description: "This branch is not vetted"
type: boolean
default: false
required: false
publish_custom_branch:
description: "Push to custom branch:"
type: string
default: ""
required: false
publish_starpilot:
description: "Push to StarPilot"
type: boolean
default: false
required: false
publish_staging:
description: "Push to StarPilot-Staging"
type: boolean
default: false
required: false
publish_testing:
description: "Push to StarPilot-Testing"
type: boolean
default: false
required: false
runner:
description: "Select runner"
type: choice
options:
- c3
- c3x
default: "c3"
required: true
update_translations:
description: "Update missing/outdated translations"
type: boolean
default: false
required: false
vet_existing_translations:
description: "Vet existing translations"
type: boolean
default: false
required: false
env:
BASE_DIR: ${{ github.workspace }}
BUILD_DIR: "/data/openpilot"
CUSTOM_BRANCH: ${{ inputs.publish_custom_branch }}
GIT_EMAIL: "91348155+FrogAi@users.noreply.github.com"
GIT_NAME: "James"
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
jobs:
get_branch:
runs-on: [self-hosted, "${{ inputs.runner }}"]
outputs:
branch: ${{ steps.get_branch.outputs.branch }}
python_version: ${{ steps.get_python_version.outputs.python_version }}
steps:
- name: Get Current Branch
id: get_branch
run: |
cd "$BUILD_DIR"
echo "branch=$(git rev-parse --abbrev-ref HEAD)" >> "$GITHUB_OUTPUT"
- name: Get Python Version
id: get_python_version
run: |
echo "python_version=$(tr -d '[:space:]' < "$BUILD_DIR/.python-version")" >> $GITHUB_OUTPUT
translate:
needs: get_branch
if: inputs.update_translations
runs-on: ubuntu-latest
steps:
- name: Configure Git Identity
run: |
git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"
- name: Checkout Required Files
uses: actions/checkout@v4
with:
ref: ${{ needs.get_branch.outputs.branch }}
sparse-checkout: |
starpilot/ui/
selfdrive/controls/lib/alerts_offroad.json
selfdrive/ui/
selfdrive/ui/translations/
selfdrive/ui/translations/auto_translate.py
selfdrive/ui/update_translations.py
- name: Set Up Python
uses: actions/setup-python@v4
with:
cache: "pip"
python-version: ${{ needs.get_branch.outputs.python_version }}
- name: Install Dependencies
run: |
pip install requests
sudo apt-get update && sudo apt-get install -y --no-install-recommends qttools5-dev-tools
- name: Update Translations
run: |
python selfdrive/ui/update_translations.py --vanish
- name: Update Missing Translations
continue-on-error: true
timeout-minutes: 300
run: |
python selfdrive/ui/translations/auto_translate.py --all-files
- name: Vet Existing Translations
if: inputs.vet_existing_translations
continue-on-error: true
timeout-minutes: 300
run: |
python selfdrive/ui/translations/auto_translate.py --all-files --vet-translations
- name: Commit and Push Translations
run: |
if git diff --quiet selfdrive/ui/translations/*.ts; then
echo "No translation updates detected."
exit 0
fi
git fetch --unshallow origin "${{ needs.get_branch.outputs.branch }}"
git checkout "${{ needs.get_branch.outputs.branch }}"
git add selfdrive/ui/translations/*.ts
git commit --amend --no-edit
git push --force origin "${{ needs.get_branch.outputs.branch }}"
build_and_push:
needs: [get_branch, translate]
if: ${{ !failure() && !cancelled() && needs.get_branch.result == 'success' }}
runs-on: [self-hosted, "${{ inputs.runner }}"]
permissions:
contents: write
defaults:
run:
working-directory: ${{ env.BUILD_DIR }}
steps:
- name: Configure Git
run: |
git config http.postBuffer 104857600
git config user.name "$GIT_NAME"
git config user.email "$GIT_EMAIL"
git remote set-url origin "https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/FrogAi/StarPilot.git"
- name: Sync Translation Updates
if: inputs.update_translations
run: |
git fetch origin "${{ needs.get_branch.outputs.branch }}"
git reset --hard FETCH_HEAD
- name: Take Ownership of Build Directory
run: |
sudo chown -R $(whoami):$(whoami) .
- name: Clean Build Artifacts
run: |
find . -name "matlab.*.md" -delete
find . -type d \( -iname "debug" -o -iname "test" -o -iname "tests" -o -name '__pycache__' \) -exec rm -rf {} +
find . -type f \( \
-name '*.a' -o \
-name '*.o' -o \
-name '*.onnx' -o \
-name '*.os' -o \
-name '*.pyc' -o \
-name 'moc_*' \
\) -delete
find .github -mindepth 1 -maxdepth 1 ! -name 'workflows' -exec rm -rf {} +
find .github/workflows -mindepth 1 ! \( \
-type f \( \
-name 'compile_starpilot.yaml' -o \
-name 'review_pull_request.yaml' -o \
-name 'schedule_update.yaml' -o \
-name 'update_pr_branch.yaml' -o \
-name 'update_release_branch.yaml' -o \
-name 'update_tinygrad.yaml' \
\) \
\) -exec rm -rf {} +
find panda/board -type f \
! -name '__init__.py' \
! -name 'bootstub.panda.bin' \
! -name 'bootstub.panda_h7.bin' \
! -name 'panda.bin.signed' \
! -name 'panda_h7.bin.signed' \
-delete
find third_party/ -name '*Darwin*' -exec rm -rf {} +
find third_party/ -name '*x86*' -exec rm -rf {} +
rm -f .gitignore .gitmodules .gitattributes .lfsconfig .overlay_init
rm -rf .sconsign.dblite .vscode/ Jenkinsfile release/ scripts/ site_scons/ teleoprtc_repo/
find . -type d -empty ! -path "./.git*" -delete
touch prebuilt
[ "${{ inputs.not_vetted }}" = "true" ] && touch not_vetted || true
- name: Add Update Date File
if: inputs.publish_staging
continue-on-error: true
run: |
curl -fLsS https://raw.githubusercontent.com/FrogAi/StarPilot/StarPilot-Staging/.github/update_date -o .github/update_date || echo "No update_date found, skipping..."
- name: Commit and Push Build
run: |
git add -f .
git commit -m "Compile StarPilot"
git push --force origin HEAD
if [ "${{ inputs.publish_starpilot }}" = "true" ]; then
git push --force origin HEAD:StarPilot
fi
if [ "${{ inputs.publish_staging }}" = "true" ]; then
git push --force origin HEAD:StarPilot-Staging
fi
if [ "${{ inputs.publish_testing }}" = "true" ]; then
git push --force origin HEAD:StarPilot-Testing
fi
if [ -n "$CUSTOM_BRANCH" ]; then
git push --force origin HEAD:"$CUSTOM_BRANCH"
fi
+65
View File
@@ -0,0 +1,65 @@
name: docs
on:
push:
branches:
- master
pull_request:
workflow_call:
inputs:
run_number:
default: '1'
required: true
type: string
concurrency:
group: docs-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
docs:
name: build docs
runs-on: ubuntu-24.04
steps:
- uses: commaai/timeout@v1
- uses: actions/checkout@v4
with:
submodules: true
# Build
- name: Build docs
run: |
# TODO: can we install just the "docs" dependency group without the normal deps?
pip install mkdocs
mkdocs build
# Push to docs.comma.ai
- uses: actions/checkout@v4
if: github.ref == 'refs/heads/master' && github.repository == 'commaai/openpilot'
with:
path: openpilot-docs
ssh-key: ${{ secrets.OPENPILOT_DOCS_KEY }}
repository: commaai/openpilot-docs
- name: Push
if: github.ref == 'refs/heads/master' && github.repository == 'commaai/openpilot'
run: |
set -x
source release/identity.sh
cd openpilot-docs
git checkout --orphan tmp
git rm -rf .
# copy over docs
cp -r ../docs_site/ docs/
# GitHub pages config
touch docs/.nojekyll
echo -n docs.comma.ai > docs/CNAME
git add -f .
git commit -m "build docs"
# docs live in different repo to not bloat openpilot's full clone size
git push -f origin tmp:gh-pages
+59
View File
@@ -0,0 +1,59 @@
name: jenkins scan
on:
issue_comment:
types: [created, edited]
jobs:
# TODO: gc old branches in a separate job in this workflow
scan-comments:
runs-on: ubuntu-latest
if: ${{ github.event.issue.pull_request }}
permissions:
contents: write
issues: write
steps:
- name: Check for trigger phrase
id: check_comment
uses: actions/github-script@v7
with:
script: |
const triggerPhrase = "trigger-jenkins";
const comment = context.payload.comment.body;
const commenter = context.payload.comment.user.login;
const { data: permissions } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: commenter
});
const hasWriteAccess = permissions.permission === 'write' || permissions.permission === 'admin';
return (hasWriteAccess && comment.includes(triggerPhrase));
result-encoding: json
- name: Checkout repository
if: steps.check_comment.outputs.result == 'true'
uses: actions/checkout@v4
with:
ref: refs/pull/${{ github.event.issue.number }}/head
- name: Push to tmp-jenkins branch
if: steps.check_comment.outputs.result == 'true'
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b tmp-jenkins-${{ github.event.issue.number }}
GIT_LFS_SKIP_PUSH=1 git push -f origin tmp-jenkins-${{ github.event.issue.number }}
- name: Delete trigger comment
if: steps.check_comment.outputs.result == 'true' && always()
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
});
@@ -0,0 +1,151 @@
name: "mici raylib ui preview"
on:
push:
branches:
- master
pull_request_target:
types: [assigned, opened, synchronize, reopened, edited]
branches:
- 'master'
paths:
- 'selfdrive/assets/**'
- 'selfdrive/ui/**'
- 'system/ui/**'
workflow_dispatch:
env:
UI_JOB_NAME: "Create mici raylib UI Report"
REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }}
BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-mici-raylib-ui"
MASTER_BRANCH_NAME: "openpilot_master_ui_mici_raylib"
# All report files are pushed here
REPORT_FILES_BRANCH_NAME: "mici-raylib-ui-reports"
jobs:
preview:
if: github.repository == 'commaai/openpilot'
name: preview
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
pull-requests: write
actions: read
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Waiting for ui generation to end
uses: lewagon/wait-on-check-action@v1.3.4
with:
ref: ${{ env.SHA }}
check-name: ${{ env.UI_JOB_NAME }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
allowed-conclusions: success
wait-interval: 20
- name: Getting workflow run ID
id: get_run_id
run: |
echo "run_id=$(curl https://api.github.com/repos/${{ github.repository }}/commits/${{ env.SHA }}/check-runs | jq -r '.check_runs[] | select(.name == "${{ env.UI_JOB_NAME }}") | .html_url | capture("(?<number>[0-9]+)") | .number')" >> $GITHUB_OUTPUT
- name: Getting proposed ui # filename: pr_ui/mici_ui_replay.mp4
id: download-artifact
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
run_id: ${{ steps.get_run_id.outputs.run_id }}
search_artifacts: true
name: mici-raylib-report-1-${{ env.REPORT_NAME }}
path: ${{ github.workspace }}/pr_ui
- name: Getting master ui # filename: master_ui_raylib/mici_ui_replay.mp4
uses: actions/checkout@v4
with:
repository: commaai/ci-artifacts
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
path: ${{ github.workspace }}/master_ui_raylib
ref: ${{ env.MASTER_BRANCH_NAME }}
- name: Saving new master ui
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
working-directory: ${{ github.workspace }}/master_ui_raylib
run: |
git checkout --orphan=new_master_ui_mici_raylib
git rm -rf *
git branch -D ${{ env.MASTER_BRANCH_NAME }}
git branch -m ${{ env.MASTER_BRANCH_NAME }}
git config user.name "GitHub Actions Bot"
git config user.email "<>"
mv ${{ github.workspace }}/pr_ui/* .
git add .
git commit -m "mici raylib video for commit ${{ env.SHA }}"
git push origin ${{ env.MASTER_BRANCH_NAME }} --force
- name: Setup FFmpeg
uses: AnimMouse/setup-ffmpeg@ae28d57dabbb148eff63170b6bf7f2b60062cbae
- name: Finding diff
if: github.event_name == 'pull_request_target'
id: find_diff
run: |
# Find the video file from PR
pr_video="${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4"
mv "${{ github.workspace }}/pr_ui/mici_ui_replay.mp4" "$pr_video"
master_video="${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4"
mv "${{ github.workspace }}/master_ui_raylib/mici_ui_replay.mp4" "$master_video"
# Run report
export PYTHONPATH=${{ github.workspace }}
baseurl="https://github.com/commaai/ci-artifacts/raw/refs/heads/${{ env.BRANCH_NAME }}"
diff_exit_code=0
python3 ${{ github.workspace }}/selfdrive/ui/tests/diff/diff.py "${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4" "${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4" "diff.html" --basedir "$baseurl" --no-open || diff_exit_code=$?
# Copy diff report files
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html ${{ github.workspace }}/pr_ui/
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.mp4 ${{ github.workspace }}/pr_ui/
REPORT_URL="https://commaai.github.io/ci-artifacts/diff_pr_${{ github.event.number }}.html"
if [ $diff_exit_code -eq 0 ]; then
DIFF="✅ Videos are identical! [View Diff Report]($REPORT_URL)"
else
DIFF="❌ <strong>Videos differ!</strong> [View Diff Report]($REPORT_URL)"
fi
echo "DIFF=$DIFF" >> "$GITHUB_OUTPUT"
- name: Saving proposed ui
if: github.event_name == 'pull_request_target'
working-directory: ${{ github.workspace }}/master_ui_raylib
run: |
# Overwrite PR branch w/ proposed ui, and master ui at this point in time for future reference
git config user.name "GitHub Actions Bot"
git config user.email "<>"
git checkout --orphan=${{ env.BRANCH_NAME }}
git rm -rf *
mv ${{ github.workspace }}/pr_ui/* .
git add .
git commit -m "mici raylib video for PR #${{ github.event.number }}"
git push origin ${{ env.BRANCH_NAME }} --force
# Append diff report to report files branch
git fetch origin ${{ env.REPORT_FILES_BRANCH_NAME }}
git checkout ${{ env.REPORT_FILES_BRANCH_NAME }}
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html diff_pr_${{ github.event.number }}.html
git add diff_pr_${{ github.event.number }}.html
git commit -m "mici raylib ui diff report for PR #${{ github.event.number }}" || echo "No changes to commit"
git push origin ${{ env.REPORT_FILES_BRANCH_NAME }}
- name: Comment Video on PR
if: github.event_name == 'pull_request_target'
uses: thollander/actions-comment-pull-request@v2
with:
message: |
<!-- _(run_id_video_mici_raylib **${{ github.run_id }}**)_ -->
## mici raylib UI Preview
${{ steps.find_diff.outputs.DIFF }}
comment_tag: run_id_video_mici_raylib
pr_number: ${{ github.event.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+42
View File
@@ -0,0 +1,42 @@
name: "model review"
on:
pull_request:
types: [opened, reopened, synchronize]
paths:
- 'selfdrive/modeld/models/*.onnx'
workflow_dispatch:
jobs:
comment:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
if: github.repository == 'commaai/openpilot'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Checkout master
uses: actions/checkout@v4
with:
ref: master
path: base
- run: git lfs pull
- run: cd base && git lfs pull
- run: pip install onnx
- name: scripts/reporter.py
id: report
run: |
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "## Model Review" >> $GITHUB_OUTPUT
MASTER_PATH=${{ github.workspace }}/base python scripts/reporter.py >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Post model report comment
uses: marocchino/sticky-pull-request-comment@baa7203ed60924babbe5dcd0ac8eae3b66ec5e16
with:
header: model-review
message: ${{ steps.report.outputs.content }}
+39
View File
@@ -0,0 +1,39 @@
name: prebuilt
on:
schedule:
- cron: '0 * * * *'
workflow_dispatch:
env:
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
BUILD: selfdrive/test/docker_build.sh prebuilt
jobs:
build_prebuilt:
name: build prebuilt
runs-on: ubuntu-latest
if: github.repository == 'commaai/openpilot'
env:
PUSH_IMAGE: true
permissions:
checks: read
contents: read
packages: write
steps:
- name: Wait for green check mark
if: ${{ github.event_name != 'workflow_dispatch' }}
uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc
with:
ref: master
wait-interval: 30
running-workflow-name: 'build prebuilt'
repo-token: ${{ secrets.GITHUB_TOKEN }}
check-regexp: ^((?!.*(build master-ci).*).)*$
- uses: actions/checkout@v4
with:
submodules: true
- run: git lfs pull
- name: Build and Push docker image
run: |
$DOCKER_LOGIN
eval "$BUILD"
+175
View File
@@ -0,0 +1,175 @@
name: "raylib ui preview"
on:
push:
branches:
- master
pull_request_target:
types: [assigned, opened, synchronize, reopened, edited]
branches:
- 'master'
paths:
- 'selfdrive/assets/**'
- 'selfdrive/ui/**'
- 'system/ui/**'
workflow_dispatch:
env:
UI_JOB_NAME: "Create raylib UI Report"
REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }}
BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-raylib-ui"
jobs:
preview:
if: github.repository == 'commaai/openpilot'
name: preview
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
pull-requests: write
actions: read
steps:
- name: Waiting for ui generation to start
run: sleep 30
- name: Waiting for ui generation to end
uses: lewagon/wait-on-check-action@v1.3.4
with:
ref: ${{ env.SHA }}
check-name: ${{ env.UI_JOB_NAME }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
allowed-conclusions: success
wait-interval: 20
- name: Getting workflow run ID
id: get_run_id
run: |
echo "run_id=$(curl https://api.github.com/repos/${{ github.repository }}/commits/${{ env.SHA }}/check-runs | jq -r '.check_runs[] | select(.name == "${{ env.UI_JOB_NAME }}") | .html_url | capture("(?<number>[0-9]+)") | .number')" >> $GITHUB_OUTPUT
- name: Getting proposed ui
id: download-artifact
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
run_id: ${{ steps.get_run_id.outputs.run_id }}
search_artifacts: true
name: raylib-report-1-${{ env.REPORT_NAME }}
path: ${{ github.workspace }}/pr_ui
- name: Getting master ui
uses: actions/checkout@v4
with:
repository: commaai/ci-artifacts
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
path: ${{ github.workspace }}/master_ui_raylib
ref: openpilot_master_ui_raylib
- name: Saving new master ui
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
working-directory: ${{ github.workspace }}/master_ui_raylib
run: |
git checkout --orphan=new_master_ui_raylib
git rm -rf *
git branch -D openpilot_master_ui_raylib
git branch -m openpilot_master_ui_raylib
git config user.name "GitHub Actions Bot"
git config user.email "<>"
mv ${{ github.workspace }}/pr_ui/*.png .
git add .
git commit -m "raylib screenshots for commit ${{ env.SHA }}"
git push origin openpilot_master_ui_raylib --force
- name: Finding diff
if: github.event_name == 'pull_request_target'
id: find_diff
run: >-
sudo apt-get update && sudo apt-get install -y imagemagick
scenes=$(find ${{ github.workspace }}/pr_ui/*.png -type f -printf "%f\n" | cut -d '.' -f 1 | grep -v 'pair_device')
A=($scenes)
DIFF=""
TABLE="<details><summary>All Screenshots</summary>"
TABLE="${TABLE}<table>"
for ((i=0; i<${#A[*]}; i=i+1));
do
# Check if the master file exists
if [ ! -f "${{ github.workspace }}/master_ui_raylib/${A[$i]}.png" ]; then
# This is a new file in PR UI that doesn't exist in master
DIFF="${DIFF}<details open>"
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{cyan}\\text{NEW}}\$\$</summary>"
DIFF="${DIFF}<table>"
DIFF="${DIFF}<tr>"
DIFF="${DIFF} <td> <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}.png\"> </td>"
DIFF="${DIFF}</tr>"
DIFF="${DIFF}</table>"
DIFF="${DIFF}</details>"
elif ! compare -fuzz 2% -highlight-color DeepSkyBlue1 -lowlight-color Black -compose Src ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png; then
convert ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png -transparent black mask.png
composite mask.png ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png composite_diff.png
convert -delay 100 ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png composite_diff.png -loop 0 ${{ github.workspace }}/pr_ui/${A[$i]}_diff.gif
mv ${{ github.workspace }}/master_ui_raylib/${A[$i]}.png ${{ github.workspace }}/pr_ui/${A[$i]}_master_ref.png
DIFF="${DIFF}<details open>"
DIFF="${DIFF}<summary>${A[$i]} : \$\${\\color{red}\\text{DIFFERENT}}\$\$</summary>"
DIFF="${DIFF}<table>"
DIFF="${DIFF}<tr>"
DIFF="${DIFF} <td> master <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_master_ref.png\"> </td>"
DIFF="${DIFF} <td> proposed <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}.png\"> </td>"
DIFF="${DIFF}</tr>"
DIFF="${DIFF}<tr>"
DIFF="${DIFF} <td> diff <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_diff.png\"> </td>"
DIFF="${DIFF} <td> composite diff <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}_diff.gif\"> </td>"
DIFF="${DIFF}</tr>"
DIFF="${DIFF}</table>"
DIFF="${DIFF}</details>"
else
rm -f ${{ github.workspace }}/pr_ui/${A[$i]}_diff.png
fi
INDEX=$(($i % 2))
if [[ $INDEX -eq 0 ]]; then
TABLE="${TABLE}<tr>"
fi
TABLE="${TABLE} <td> <img src=\"https://raw.githubusercontent.com/commaai/ci-artifacts/${{ env.BRANCH_NAME }}/${A[$i]}.png\"> </td>"
if [[ $INDEX -eq 1 || $(($i + 1)) -eq ${#A[*]} ]]; then
TABLE="${TABLE}</tr>"
fi
done
TABLE="${TABLE}</table></details>"
echo "DIFF=$DIFF$TABLE" >> "$GITHUB_OUTPUT"
- name: Saving proposed ui
if: github.event_name == 'pull_request_target'
working-directory: ${{ github.workspace }}/master_ui_raylib
run: |
git config user.name "GitHub Actions Bot"
git config user.email "<>"
git checkout --orphan=${{ env.BRANCH_NAME }}
git rm -rf *
mv ${{ github.workspace }}/pr_ui/* .
git add .
git commit -m "raylib screenshots for PR #${{ github.event.number }}"
git push origin ${{ env.BRANCH_NAME }} --force
- name: Comment Screenshots on PR
if: github.event_name == 'pull_request_target'
uses: thollander/actions-comment-pull-request@v2
with:
message: |
<!-- _(run_id_screenshots_raylib **${{ github.run_id }}**)_ -->
## raylib UI Preview
${{ steps.find_diff.outputs.DIFF }}
comment_tag: run_id_screenshots_raylib
pr_number: ${{ github.event.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+42
View File
@@ -0,0 +1,42 @@
name: release
on:
schedule:
- cron: '0 9 * * *'
workflow_dispatch:
jobs:
build_masterci:
name: build master-ci
env:
ImageOS: ubuntu24
container:
image: ghcr.io/commaai/openpilot-base:latest
runs-on: ubuntu-latest
if: github.repository == 'commaai/openpilot'
permissions:
checks: read
contents: write
steps:
- name: Install wait-on-check-action dependencies
run: |
sudo apt-get update
sudo apt-get install -y libyaml-dev
- name: Wait for green check mark
if: ${{ github.event_name == 'schedule' }}
uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc
with:
ref: master
wait-interval: 30
running-workflow-name: 'build master-ci'
repo-token: ${{ secrets.GITHUB_TOKEN }}
check-regexp: ^((?!.*(build prebuilt).*).)*$
- uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
- name: Pull LFS
run: |
git config --global --add safe.directory '*'
git lfs pull
- name: Push master-ci
run: BRANCH=__nightly release/build_stripped.sh
+72
View File
@@ -0,0 +1,72 @@
name: repo maintenance
on:
schedule:
- cron: "0 14 * * 1" # every Monday at 2am UTC (6am PST)
workflow_dispatch:
env:
BASE_IMAGE: openpilot-base
BUILD: selfdrive/test/docker_build.sh base
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
jobs:
update_translations:
runs-on: ubuntu-latest
if: github.repository == 'commaai/openpilot'
steps:
- uses: actions/checkout@v4
- uses: ./.github/workflows/setup-with-retry
- name: Update translations
run: |
${{ env.RUN }} "python3 selfdrive/ui/update_translations.py --vanish"
- name: Create Pull Request
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
with:
author: Vehicle Researcher <user@comma.ai>
commit-message: "Update translations"
title: "[bot] Update translations"
body: "Automatic PR from repo-maintenance -> update_translations"
branch: "update-translations"
base: "master"
delete-branch: true
labels: bot
package_updates:
name: package_updates
runs-on: ubuntu-latest
container:
image: ghcr.io/commaai/openpilot-base:latest
if: github.repository == 'commaai/openpilot'
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: uv lock
run: |
python3 -m ensurepip --upgrade
pip3 install uv
uv lock --upgrade
- name: bump submodules
run: |
git config --global --add safe.directory '*'
git submodule update --remote
git add .
- name: update car docs
run: |
export PYTHONPATH="$PWD"
scons -j$(nproc) --minimal opendbc_repo
python selfdrive/car/docs.py
git add docs/CARS.md
- name: Create Pull Request
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
with:
author: Vehicle Researcher <user@comma.ai>
token: ${{ secrets.ACTIONS_CREATE_PR_PAT }}
commit-message: Update Python packages
title: '[bot] Update Python packages'
branch: auto-package-updates
base: master
delete-branch: true
body: 'Automatic PR from repo-maintenance -> package_updates'
labels: bot
+71
View File
@@ -0,0 +1,71 @@
name: Schedule StarPilot Update
on:
workflow_dispatch:
inputs:
scheduled_date:
description: "Enter the date to update the \"StarPilot\" branch (YYYY-MM-DD)"
required: true
env:
GIT_EMAIL: "91348155+FrogAi@users.noreply.github.com"
GIT_NAME: "James"
TARGET_BRANCH: "StarPilot-Staging"
UPDATE_FILE_PATH: ".github/update_date"
jobs:
schedule_update:
runs-on: ubuntu-latest
steps:
- name: Checkout Target Branch
uses: actions/checkout@v4
with:
ref: ${{ env.TARGET_BRANCH }}
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
fetch-depth: 3
- name: Configure Git Identity
run: |
git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"
- name: Write Schedule Date
env:
SCHEDULED_DATE: ${{ github.event.inputs.scheduled_date }}
run: |
echo "$SCHEDULED_DATE" > "$UPDATE_FILE_PATH"
git add "$UPDATE_FILE_PATH"
- name: Get Target Commit Data
id: get_target
run: |
TARGET_COMMIT=$(git rev-parse HEAD~1)
AUTHOR_DATE=$(git show -s --format=%aD "$TARGET_COMMIT")
COMMITTER_DATE=$(git show -s --format=%cD "$TARGET_COMMIT")
echo "AUTHOR_DATE=$AUTHOR_DATE" >> "$GITHUB_ENV"
echo "COMMITTER_DATE=$COMMITTER_DATE" >> "$GITHUB_ENV"
echo "TARGET_COMMIT=$TARGET_COMMIT" >> "$GITHUB_ENV"
- name: Create Fixup Commit
id: fixup_commit
run: |
if git diff --cached --quiet; then
echo "No changes detected."
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "Changes detected. Creating fixup commit."
git commit --fixup="$TARGET_COMMIT"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Autosquash and Restore Timestamps
if: steps.fixup_commit.outputs.has_changes == 'true'
run: |
GIT_SEQUENCE_EDITOR=: git rebase --autosquash -i HEAD~3
git rebase --exec "GIT_COMMITTER_DATE='$COMMITTER_DATE' git commit --amend --no-edit --date='$AUTHOR_DATE'" HEAD~2
- name: Push Changes
if: steps.fixup_commit.outputs.has_changes == 'true'
run: |
git push origin "$TARGET_BRANCH" --force-with-lease
@@ -0,0 +1,52 @@
name: 'openpilot env setup, with retry on failure'
inputs:
docker_hub_pat:
description: 'Auth token for Docker Hub, required for BuildJet jobs'
required: false
default: ''
sleep_time:
description: 'Time to sleep between retries'
required: false
default: 30
outputs:
duration:
description: 'Duration of the setup process in seconds'
value: ${{ steps.get_duration.outputs.duration }}
runs:
using: "composite"
steps:
- id: start_time
shell: bash
run: echo "START_TIME=$(date +%s)" >> $GITHUB_ENV
- id: setup1
uses: ./.github/workflows/setup
continue-on-error: true
with:
is_retried: true
- if: steps.setup1.outcome == 'failure'
shell: bash
run: sleep ${{ inputs.sleep_time }}
- id: setup2
if: steps.setup1.outcome == 'failure'
uses: ./.github/workflows/setup
continue-on-error: true
with:
is_retried: true
- if: steps.setup2.outcome == 'failure'
shell: bash
run: sleep ${{ inputs.sleep_time }}
- id: setup3
if: steps.setup2.outcome == 'failure'
uses: ./.github/workflows/setup
with:
is_retried: true
- id: get_duration
shell: bash
run: |
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo "Total duration: $DURATION seconds"
echo "duration=$DURATION" >> $GITHUB_OUTPUT
+56
View File
@@ -0,0 +1,56 @@
name: 'openpilot env setup'
inputs:
is_retried:
description: 'A mock param that asserts that we use the setup-with-retry instead of this action directly'
required: false
default: 'false'
runs:
using: "composite"
steps:
# assert that this action is retried using the setup-with-retry
- shell: bash
if: ${{ inputs.is_retried == 'false' }}
run: |
echo "You should not run this action directly. Use setup-with-retry instead"
exit 1
- shell: bash
name: No retries!
run: |
if [ "${{ github.run_attempt }}" -gt 1 ]; then
echo -e "\033[0;31m##################################################"
echo -e "\033[0;31m Retries not allowed! Fix the flaky test! "
echo -e "\033[0;31m##################################################\033[0m"
exit 1
fi
# do this after checkout to ensure our custom LFS config is used to pull from GitLab
- shell: bash
run: git lfs pull
# build cache
- id: date
shell: bash
run: echo "CACHE_COMMIT_DATE=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d-%H:%M')" >> $GITHUB_ENV
- shell: bash
run: echo "$CACHE_COMMIT_DATE"
- id: scons-cache
uses: ./.github/workflows/auto-cache
with:
path: .ci_cache/scons_cache
key: scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
restore-keys: |
scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}
scons-${{ runner.arch }}
# as suggested here: https://github.com/moby/moby/issues/32816#issuecomment-910030001
- id: normalize-file-permissions
shell: bash
name: Normalize file permissions to ensure a consistent docker build cache
run: |
find . -type f -executable -not -perm 755 -exec chmod 755 {} \;
find . -type f -not -executable -not -perm 644 -exec chmod 644 {} \;
# build our docker image
- shell: bash
run: eval ${{ env.BUILD }}
+52
View File
@@ -0,0 +1,52 @@
name: stale
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
env:
DAYS_BEFORE_PR_CLOSE: 7
DAYS_BEFORE_PR_STALE: 24
DAYS_BEFORE_PR_STALE_DRAFT: 30
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
exempt-all-milestones: true
# pull request config
stale-pr-message: 'This PR has had no activity for ${{ env.DAYS_BEFORE_PR_STALE }} days. It will be automatically closed in ${{ env.DAYS_BEFORE_PR_CLOSE }} days if there is no activity.'
close-pr-message: 'This PR has been automatically closed due to inactivity. Feel free to re-open once activity resumes.'
stale-pr-label: stale
delete-branch: ${{ github.event.pull_request.head.repo.full_name == 'commaai/openpilot' }} # only delete branches on the main repo
exempt-pr-labels: "ignore stale,needs testing" # if wip or it needs testing from the community, don't mark as stale
days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }}
days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }}
exempt-draft-pr: false
# issue config
days-before-issue-stale: -1 # ignore issues for now
# same as above, but give draft PRs more time
stale_drafts:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
exempt-all-milestones: true
# pull request config
stale-pr-message: 'This PR has had no activity for ${{ env.DAYS_BEFORE_PR_STALE_DRAFT }} days. It will be automatically closed in ${{ env.DAYS_BEFORE_PR_CLOSE }} days if there is no activity.'
close-pr-message: 'This PR has been automatically closed due to inactivity. Feel free to re-open once activity resumes.'
stale-pr-label: stale
delete-branch: ${{ github.event.pull_request.head.repo.full_name == 'commaai/openpilot' }} # only delete branches on the main repo
exempt-pr-labels: "ignore stale,needs testing" # if wip or it needs testing from the community, don't mark as stale
days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE_DRAFT }}
days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }}
exempt-draft-pr: true
# issue config
days-before-issue-stale: -1 # ignore issues for now
+294
View File
@@ -0,0 +1,294 @@
name: tests
on:
push:
branches:
- master
pull_request:
workflow_dispatch:
workflow_call:
inputs:
run_number:
default: '1'
required: true
type: string
concurrency:
group: tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
cancel-in-progress: true
env:
PYTHONWARNINGS: error
BASE_IMAGE: openpilot-base
AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }}
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
BUILD: selfdrive/test/docker_build.sh base
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical
jobs:
build_release:
name: build release
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
env:
STRIPPED_DIR: /tmp/releasepilot
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Getting LFS files
uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e
with:
timeout_minutes: 2
max_attempts: 3
command: git lfs pull
- name: Build devel
timeout-minutes: 1
run: TARGET_DIR=$STRIPPED_DIR release/build_stripped.sh
- uses: ./.github/workflows/setup-with-retry
- name: Build openpilot and run checks
timeout-minutes: ${{ ((steps.restore-scons-cache.outputs.cache-hit == 'true') && 10 || 30) }} # allow more time when we missed the scons cache
run: |
cd $STRIPPED_DIR
${{ env.RUN }} "python3 system/manager/build.py"
- name: Run tests
timeout-minutes: 1
run: |
cd $STRIPPED_DIR
${{ env.RUN }} "release/check-dirty.sh"
- name: Check submodules
if: github.repository == 'commaai/openpilot'
timeout-minutes: 3
run: release/check-submodules.sh
build:
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Setup docker push
if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot'
run: |
echo "PUSH_IMAGE=true" >> "$GITHUB_ENV"
$DOCKER_LOGIN
- uses: ./.github/workflows/setup-with-retry
- uses: ./.github/workflows/compile-openpilot
timeout-minutes: 30
build_mac:
name: build macOS
if: false # tmp disable due to brew install not working
runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- run: echo "CACHE_COMMIT_DATE=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d-%H:%M')" >> $GITHUB_ENV
- name: Homebrew cache
uses: ./.github/workflows/auto-cache
with:
path: ~/Library/Caches/Homebrew
key: brew-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
restore-keys: |
brew-macos-${{ env.CACHE_COMMIT_DATE }}
brew-macos
- name: Install dependencies
run: ./tools/mac_setup.sh
env:
PYTHONWARNINGS: default # package install has DeprecationWarnings
HOMEBREW_DISPLAY_INSTALL_TIMES: 1
- run: git lfs pull
- name: Getting scons cache
uses: ./.github/workflows/auto-cache
with:
path: /tmp/scons_cache
key: scons-${{ runner.arch }}-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
restore-keys: |
scons-${{ runner.arch }}-macos-${{ env.CACHE_COMMIT_DATE }}
scons-${{ runner.arch }}-macos
- name: Building openpilot
run: . .venv/bin/activate && scons -j$(nproc)
static_analysis:
name: static analysis
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
env:
PYTHONWARNINGS: default
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
- name: Static analysis
timeout-minutes: 1
run: ${{ env.RUN }} "scripts/lint/lint.sh"
unit_tests:
name: unit tests
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
id: setup-step
- name: Build openpilot
run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Run unit tests
timeout-minutes: ${{ contains(runner.name, 'nsc') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }}
run: |
${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \
# Pre-compile Python bytecode so each pytest worker doesn't need to
$PYTEST --collect-only -m 'not slow' -qq && \
MAX_EXAMPLES=1 $PYTEST -m 'not slow' && \
chmod -R 777 /tmp/comma_download_cache"
process_replay:
name: process replay
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
id: setup-step
- name: Cache test routes
id: dependency-cache
uses: actions/cache@v4
with:
path: .ci_cache/comma_download_cache
key: proc-replay-${{ hashFiles('selfdrive/test/process_replay/ref_commit', 'selfdrive/test/process_replay/test_processes.py') }}
- name: Build openpilot
run: |
${{ env.RUN }} "scons -j$(nproc)"
- name: Run replay
timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.dependency-cache.outputs.cache-hit == 'true') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }}
run: |
${{ env.RUN }} "selfdrive/test/process_replay/test_processes.py -j$(nproc) && \
chmod -R 777 /tmp/comma_download_cache"
- name: Print diff
id: print-diff
if: always()
run: cat selfdrive/test/process_replay/diff.txt
- uses: actions/upload-artifact@v4
if: always()
continue-on-error: true
with:
name: process_replay_diff.txt
path: selfdrive/test/process_replay/diff.txt
- name: Upload reference logs
if: false # TODO: move this to github instead of azure
run: |
${{ env.RUN }} "unset PYTHONWARNINGS && AZURE_TOKEN='$AZURE_TOKEN' python3 selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only"
- name: Run regen
if: false
timeout-minutes: 4
run: |
${{ env.RUN }} "ONNXCPU=1 $PYTEST selfdrive/test/process_replay/test_regen.py && \
chmod -R 777 /tmp/comma_download_cache"
simulator_driving:
name: simulator driving
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
if: false # FIXME: Started to timeout recently
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
id: setup-step
- name: Build openpilot
run: |
${{ env.RUN }} "scons -j$(nproc)"
- name: Driving test
timeout-minutes: ${{ (steps.setup-step.outputs.duration < 18) && 1 || 2 }}
run: |
${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \
source selfdrive/test/setup_vsound.sh && \
CI=1 pytest -s tools/sim/tests/test_metadrive_bridge.py"
create_raylib_ui_report:
name: Create raylib UI Report
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
- name: Build openpilot
run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Create raylib UI Report
run: >
${{ env.RUN }} "PYTHONWARNINGS=ignore &&
source selfdrive/test/setup_xvfb.sh &&
python3 selfdrive/ui/tests/test_ui/raylib_screenshots.py"
- name: Upload Raylib UI Report
uses: actions/upload-artifact@v4
with:
name: raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
path: selfdrive/ui/tests/test_ui/raylib_report/screenshots
create_mici_raylib_ui_report:
name: Create mici raylib UI Report
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|| fromJSON('["ubuntu-24.04"]') }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-with-retry
- name: Build openpilot
run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Create mici raylib UI Report
run: >
${{ env.RUN }} "PYTHONWARNINGS=ignore &&
source selfdrive/test/setup_xvfb.sh &&
WINDOWED=1 python3 selfdrive/ui/tests/diff/replay.py"
- name: Upload Raylib UI Report
uses: actions/upload-artifact@v4
with:
name: mici-raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
path: selfdrive/ui/tests/diff/report
@@ -0,0 +1,107 @@
name: Update StarPilot Branch
on:
schedule:
- cron: "0 18 * * 6"
env:
BRANCH_STARPILOT: StarPilot
BRANCH_PREVIOUS: StarPilot-Previous
BRANCH_STAGING: StarPilot-Staging
GIT_EMAIL: "91348155+FrogAi@users.noreply.github.com"
GIT_NAME: "James"
GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
TZ: America/Phoenix
UPDATE_FILE: .github/update_date
jobs:
check_update:
runs-on: ubuntu-latest
outputs:
update_due: ${{ steps.check_update.outputs.update_due }}
scheduled_date: ${{ steps.check_update.outputs.scheduled_date }}
steps:
- name: Check Update Status
id: check_update
env:
REPO_NAME: ${{ github.repository }}
run: |
URL="https://raw.githubusercontent.com/$REPO_NAME/$BRANCH_STAGING/$UPDATE_FILE"
STATUS=$(curl -o /dev/null -s -w "%{http_code}\n" "$URL")
if [ "$STATUS" != "200" ]; then
echo "update_due=false" >> "$GITHUB_OUTPUT"
exit 0
fi
SCHEDULED_DATE=$(curl -s "$URL")
CURRENT_DATE=$(TZ="$TZ" date +%F)
if [ "$SCHEDULED_DATE" == "$CURRENT_DATE" ]; then
echo "update_due=true" >> "$GITHUB_OUTPUT"
echo "scheduled_date=$SCHEDULED_DATE" >> "$GITHUB_OUTPUT"
else
echo "update_due=false" >> "$GITHUB_OUTPUT"
fi
update_branch:
needs: check_update
if: ${{ needs.check_update.outputs.update_due == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout Staging
uses: actions/checkout@v4
with:
ref: ${{ env.BRANCH_STAGING }}
fetch-depth: 0
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Configure Git Identity
run: |
git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"
- name: Update README and Cleanup
env:
SCHEDULED_DATE: ${{ needs.check_update.outputs.scheduled_date }}
run: |
DAY=$(TZ="$TZ" date +'%d' | sed 's/^0//')
case "$DAY" in
1|21|31) SUFFIX="st" ;;
2|22) SUFFIX="nd" ;;
3|23) SUFFIX="rd" ;;
*) SUFFIX="th" ;;
esac
MONTH=$(TZ="$TZ" date +'%B')
YEAR=$(TZ="$TZ" date +'%Y')
DATE_FMT="${MONTH} ${DAY}${SUFFIX}, ${YEAR}"
DATE_ESCAPED=$(printf '%s' "$DATE_FMT" | sed -E 's/ /%20/g; s/,/%2C/g')
sed -i -E "s|(Last%20Updated-)[^-)]*|\1${DATE_ESCAPED}|g" README.md
git rm -f "$UPDATE_FILE"
git add README.md
git commit -m "Updated README date to ${DATE_FMT}"
git reset --soft HEAD~2
ORIGINAL_MSG=$(git log -1 --pretty=%B HEAD)
COMMIT_PHX=$(TZ="$TZ" date -d "$SCHEDULED_DATE 12:00" +"%Y-%m-%dT%H:%M:%S %z")
GIT_COMMITTER_DATE="$COMMIT_PHX" GIT_AUTHOR_DATE="$COMMIT_PHX" git commit -m "$ORIGINAL_MSG"
- name: Wait Until Noon ${{ env.TZ }}
run: |
NOW=$(TZ="$TZ" date +%s)
TARGET=$(TZ="$TZ" date -d "12:00" +%s)
if [ "$NOW" -lt "$TARGET" ]; then
sleep $((TARGET - NOW))
fi
- name: Push and Sync Branches
run: |
git push origin "$BRANCH_STAGING" --force
git fetch origin "$BRANCH_STARPILOT:$BRANCH_STARPILOT"
git push origin "$BRANCH_STARPILOT:$BRANCH_PREVIOUS" --force
git push origin "$BRANCH_STAGING:$BRANCH_STARPILOT" --force
+95
View File
@@ -0,0 +1,95 @@
name: Update Tinygrad
on:
workflow_dispatch:
inputs:
runner:
description: "Select runner"
type: choice
options:
- c3
- c3x
default: "c3"
required: true
env:
GIT_EMAIL: "91348155+FrogAi@users.noreply.github.com"
GIT_NAME: "James"
GITLAB_REPO_DIR: "StarPilot-Resources"
GITLAB_URL: "gitlab.com/FrogAi/StarPilot-Resources.git"
OPENPILOT_DIR: "/data/openpilot"
jobs:
update_tinygrad:
runs-on: [self-hosted, "${{ inputs.runner }}"]
steps:
- name: Get Version
id: get_version
run: |
VERSION=$(grep -oP '^VERSION\s*=\s*"\K[^"]+' "$OPENPILOT_DIR/starpilot/assets/model_manager.py")
echo "VERSION=$VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Setup Workspace and Clone GitLab
id: setup
env:
GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
run: |
WORK_DIR="$RUNNER_TEMP/starpilot_tinygrad"
rm -rf "$WORK_DIR" && mkdir -p "$WORK_DIR"
echo "work_dir=$WORK_DIR" >> "$GITHUB_OUTPUT"
cd "$WORK_DIR"
git clone --depth 1 --branch Tinygrad "https://oauth2:${GITLAB_TOKEN}@$GITLAB_URL"
- name: Create Tinygrad Archive
working-directory: ${{ env.OPENPILOT_DIR }}
env:
WORK_DIR: ${{ steps.setup.outputs.work_dir }}
VERSION: ${{ steps.get_version.outputs.version }}
run: |
set -euo pipefail
ARCHIVE_DEST="$WORK_DIR/$GITLAB_REPO_DIR"
ARCHIVE_NAME="Tinygrad_$VERSION.tar.gz"
DUMMY_DIR=$(mktemp -d)
touch "$DUMMY_DIR/SConscript"
tar -czf "$ARCHIVE_DEST/$ARCHIVE_NAME" \
--exclude="*.a" \
--exclude="*.o" \
--exclude="*.onnx" \
--exclude="*__pycache__*" \
--exclude="*tests*" \
--exclude="selfdrive/modeld/SConscript" \
selfdrive/modeld tinygrad_repo \
-C "$DUMMY_DIR" \
--transform 's|^SConscript$|selfdrive/modeld/SConscript|' \
SConscript
rm -rf "$DUMMY_DIR"
- name: Push Updated Tinygrad
working-directory: ${{ steps.setup.outputs.work_dir }}/${{ env.GITLAB_REPO_DIR }}
env:
VERSION: ${{ steps.get_version.outputs.version }}
run: |
git config user.name "$GIT_NAME"
git config user.email "$GIT_EMAIL"
git add Tinygrad_*.tar.gz
if git diff --staged --quiet; then
echo "No changes to commit."
else
git commit -m "Updated Tinygrad: $VERSION"
git push origin Tinygrad
fi
- name: Cleanup Temporary Files
if: always()
env:
WORK_DIR: ${{ steps.setup.outputs.work_dir }}
run: |
rm -rf "$WORK_DIR"
+41 -5
View File
@@ -13,6 +13,10 @@ venv/
a.out a.out
.hypothesis .hypothesis
.cache/ .cache/
.host_runtime/
.comma_sysroot/
.venv-linux-arm64/
compiledmodels/
/docs_site/ /docs_site/
@@ -44,14 +48,13 @@ clcache
compile_commands.json compile_commands.json
compare_runtime*.html compare_runtime*.html
selfdrive/pandad/pandad
cereal/services.h cereal/services.h
cereal/gen cereal/gen
cereal/messaging/bridge
selfdrive/ui/translations/tmp selfdrive/ui/translations/tmp
selfdrive/car/tests/cars_dump selfdrive/car/tests/cars_dump
system/camerad/camerad
system/camerad/test/ae_gray_test system/camerad/test/ae_gray_test
selfdrive/ui/ui.macos
selfdrive/ui/ui.larch64
.coverage* .coverage*
coverage.xml coverage.xml
@@ -65,6 +68,16 @@ cppcheck_report.txt
comma*.sh comma*.sh
selfdrive/modeld/models/*.pkl selfdrive/modeld/models/*.pkl
!selfdrive/modeld/models/driving_vision_tinygrad.pkl
!selfdrive/modeld/models/driving_policy_tinygrad.pkl
!selfdrive/modeld/models/driving_vision_metadata.pkl
!selfdrive/modeld/models/driving_policy_metadata.pkl
!selfdrive/modeld/models/dmonitoring_model_tinygrad.pkl
!selfdrive/modeld/models/dmonitoring_model_metadata.pkl
!selfdrive/modeld/models/warp_1928x1208_tinygrad.pkl
!selfdrive/modeld/models/warp_1344x760_tinygrad.pkl
!selfdrive/modeld/models/dm_warp_1928x1208_tinygrad.pkl
!selfdrive/modeld/models/dm_warp_1344x760_tinygrad.pkl
# openpilot log files # openpilot log files
*.bz2 *.bz2
@@ -96,5 +109,28 @@ Pipfile
.history .history
.ionide .ionide
# rick - keep panda_tici standalone # Keep prebuilt runtime artifacts trackable
panda_tici/ !cereal/messaging/bridge
!system/camerad/camerad
!system/loggerd/loggerd
!system/loggerd/encoderd
!system/loggerd/bootlog
!selfdrive/pandad/pandad
!cereal/services.h
!cereal/libcereal.a
!cereal/libsocketmaster.a
!common/params_pyx.so
!common/params_pyx.cpp
!common/transformations/transformations.so
!selfdrive/modeld/models/commonmodel_pyx.so
!selfdrive/pandad/pandad_api_impl.so
!selfdrive/controls/lib/lateral_mpc_lib/c_generated_code/acados_ocp_solver_pyx.so
!selfdrive/controls/lib/lateral_mpc_lib/c_generated_code/libacados_ocp_solver_lat.so
!selfdrive/controls/lib/longitudinal_mpc_lib/c_generated_code/acados_ocp_solver_pyx.so
!selfdrive/controls/lib/longitudinal_mpc_lib/c_generated_code/libacados_ocp_solver_long.so
!common/libcommon.a
!msgq_repo/msgq/ipc_pyx.so
!msgq_repo/msgq/visionipc/visionipc_pyx.so
!rednose_repo/rednose/helpers/ekf_sym_pyx.so
!panda/board/obj/
!panda/board/obj/**
+1 -3
View File
@@ -4,8 +4,6 @@
"ms-vscode.cpptools", "ms-vscode.cpptools",
"elagil.pre-commit-helper", "elagil.pre-commit-helper",
"charliermarsh.ruff", "charliermarsh.ruff",
"JamiTech.simply-blame", "openai.chatgpt",
"k--kato.intellij-idea-keybindings",
"trinm1709.dracula-theme-from-intellij"
] ]
} }
+1 -15
View File
@@ -3,24 +3,10 @@
"editor.insertSpaces": true, "editor.insertSpaces": true,
"editor.renderWhitespace": "trailing", "editor.renderWhitespace": "trailing",
"files.trimTrailingWhitespace": true, "files.trimTrailingWhitespace": true,
"terminal.integrated.defaultProfile.linux": "dragonpilot",
"terminal.integrated.profiles.linux": {
"dragonpilot": {
"path": "bash",
"args": ["-c", "distrobox enter dp"]
}
},
"search.exclude": { "search.exclude": {
"**/.git": true, "**/.git": true,
"**/.venv": true, "**/.venv": true,
"**/__pycache__": true, "**/__pycache__": true
"msgq_repo/": true,
"rednose/": true,
"rednose_repo/": true,
"openpilot/": true,
"teleoprtc_repo/": true,
"tinygrad/": true,
"tinygrad_repo/": true
}, },
"files.exclude": { "files.exclude": {
"**/.git": true, "**/.git": true,
-140
View File
@@ -1,140 +0,0 @@
# ALKA (Always-on Lane Keeping Assist) Design v3
## Overview
ALKA enables lateral control (steering) when ACC Main is ON, without requiring cruise to be engaged. This allows lane keeping assist to function independently of longitudinal control.
**Simplified Behavior (v3):**
- All brands use direct tracking: `lkas_on = acc_main_on`
- No button/toggle tracking (removed TJA, LKAS button, LKAS HUD)
- ACC Main ON = ALKA enabled, ACC Main OFF = ALKA disabled
---
## Per-Brand Summary
| Brand | Status | ACC Main Source | Notes |
|-------|--------|-----------------|-------|
| Body | Disabled | - | No steering capability |
| Chrysler | Disabled | - | Needs special handling |
| Ford | Enabled | EngBrakeData (0x165) CcStat | |
| GM | Disabled | - | No ACC Main signal |
| Honda Nidec | Enabled | SCM_FEEDBACK (0x326) MAIN_ON | |
| Honda Bosch | Enabled | SCM_FEEDBACK (0x326) MAIN_ON | |
| Hyundai | Enabled | SCC11 (0x420) bit 0 | |
| Hyundai CAN-FD | Enabled | SCC_CONTROL (0x1A0) bit 66 | |
| Hyundai Legacy | Enabled | SCC11 (0x420) bit 0 | |
| Mazda | Enabled | CRZ_CTRL (0x21C) bit 17 | |
| Nissan | Enabled | CRUISE_THROTTLE (0x239) bit 17 | |
| PSA | Disabled | - | Not implemented |
| Rivian | Disabled | - | Different architecture |
| Subaru | Enabled | CruiseControl (0x240) bit 40 | |
| Subaru Preglobal | Enabled | CruiseControl (0x144) bit 48 | |
| Tesla | Disabled | - | Different architecture |
| Toyota | Enabled | PCM_CRUISE_2 (0x1D3) bit 15 | |
| Toyota (UNSUPPORTED_DSU) | Enabled | DSU_CRUISE (0x365) bit 0 | |
| VW MQB | Enabled | TSK_06 TSK_Status (>=2) | |
| VW PQ | Enabled | Motor_5 (0x480) bit 50 (long) | |
---
## Permission Model
Lateral control requires checks at both layers. Normal path uses `controls_allowed`, ALKA path uses additional checks.
| Check | Panda | openpilot | Notes |
|-------|:-----:|:---------:|-------|
| **Normal Path** |
| `controls_allowed` (cruise engaged) | ✓ | ✓ | Either this OR ALKA path |
| **ALKA Path** |
| `alka_allowed` (brand supports) | ✓ | ✓ | Set per brand in safety init |
| `ALT_EXP_ALKA` (user enabled) | ✓ | ✓ | alternativeExperience flag |
| `lkas_on` (ACC Main ON) | ✓ | ✓ | Tracked via CAN messages |
| `vehicle_moving` / `!standstill` | ✓ | ✓ | |
| **openpilot Additional** |
| `gear_ok` (not P/N/R) | ✗ | ✓ | Python layer only |
| `calibrated` | ✗ | ✓ | Python layer only |
| `seatbelt latched` | ✗ | ✓ | Python layer only |
| `doors closed` | ✗ | ✓ | Python layer only |
| `!steerFaultTemporary` | ✗ | ✓ | Python layer only |
| `!steerFaultPermanent` | ✗ | ✓ | Python layer only |
---
## Data Flow
```
┌─────────────────────────────────────────────────────────────────────┐
│ CAN Bus │
└─────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ Safety Layer (panda C code) │ │ Python Layer │
│ │ │ │
│ rx_hook: │ │ carstate.py: │
│ - Parse ACC Main signal │ │ - Parse cruiseState.available │
│ - Set lkas_on = acc_main_on │ │ - Set self.lkas_on │
│ │ │ │
│ lat_control_allowed(): │ └─────────────┬───────────────────┘
│ - Check lkas_on + other flags │ │
│ - Gate steering commands │ ▼
└─────────────────────────────────┘ ┌─────────────────────────────────┐
│ card.py: │
│ - Publish carStateExt.lkasOn │
└─────────────┬───────────────────┘
┌─────────────────────────────────┐
│ controlsd.py: │
│ - Read carStateExt.lkasOn │
│ - Check ALKA conditions │
│ - Set CC.latActive │
└─────────────────────────────────┘
```
### Key Files
| File | Purpose |
|------|---------|
| `custom.capnp` | Defines `CarStateExt` struct with `lkasOn` field |
| `log.capnp` | Includes `carStateExt` in event union |
| `interfaces.py` | Defines `self.lkas_on = False` default in `CarStateBase` |
| `carstate.py` (per brand) | Tracks `lkas_on` based on ACC Main |
| `card.py` | Publishes `carStateExt.lkasOn` from `CI.CS.lkas_on` |
| `controlsd.py` | Reads `carStateExt.lkasOn` to determine `alka_active` |
---
## ACC Main Tracking
All brands use simple direct tracking:
```c
// Panda (C code)
if (alka_allowed && (alternative_experience & ALT_EXP_ALKA)) {
lkas_on = acc_main_on; // or GET_BIT(msg, bit_position)
}
```
```python
# Python carstate.py
self.lkas_on = ret.cruiseState.available
```
This guard ensures:
1. Brand supports ALKA (`alka_allowed`)
2. User enabled ALKA (`ALT_EXP_ALKA`)
Without both conditions, no ACC Main tracking occurs, and ALKA remains disabled.
---
## Testing
Safety tests verify:
- `alka_allowed` flag set correctly per brand
- ACC Main tracking updates `lkas_on` directly
- `lat_control_allowed()` returns true only when all conditions met
- Steering TX blocked when ALKA conditions not met
- Bus routing variants (camera_scc, unsupported_dsu)
-91
View File
@@ -1,91 +0,0 @@
```mermaid
flowchart TD
B000["devel-staging"] ---> CORE["core"]
CORE ---> CORE_001["core-feat/params"]
CORE_001 ---> CORE_002["core-feat/panel"]
CORE_002 ---> CORE_003["core-feat/safety-ext"]
CORE_003 ---> MIN["min"]
MIN ---> MIN_001["min-feat/ui/display-mode"]
MIN ---> MIN_002["min-feat/dev/model-selector"]
MIN ---> MIN_003["min-feat/lat/lca"]
MIN ---> MIN_004["min-feat/dev/on-off-road"]
MIN ---> MIN_005["min-feat/ui/hide-hud"]
MIN ---> MIN_006["min-feat/lon/ext-radar"]
MIN ---> MIN_007["min-feat/lat/road-edge-detection"]
MIN ---> MIN_008["min-feat/ui/rainbow-path"]
MIN ---> MIN_009["min-feat/lon/acm"]
MIN ---> MIN_010["min-feat/lon/aem"]
MIN ---> MIN_011["min-feat/lon/dtsc"]
MIN ---> MIN_012["min-feat/lon/apm"]
MIN ---> MIN_013["min-feat/lon/dasr"]
MIN ---> MIN_014["min-feat/dev/alert-mode"]
MIN ---> MIN_015["min-feat/dev/auto-shutdown"]
MIN ---> MIN_016["min-feat/ui/lead-stats"]
MIN ---> MIN_017["min-feat/ui/border-indicator"]
MIN ---> MIN_018["min-feat/dev/delay-loggerd"]
MIN ---> MIN_019["min-feat/dev/disable-connect"]
MIN ---> MIN_020["min-feat/dev/tether-on-boot"]
MIN ---> MIN_021["min-feat/ui/torque-bar"]
MIN ---> MIN_022["min-feat/ui/mici-ui-mode"]
MIN ---> MIN_023["min-feat/lat/lat-offset"]
MIN_001 ---> FULL["full"]
MIN_002 ---> FULL
MIN_003 ---> FULL
MIN_004 ---> FULL
MIN_005 ---> FULL
MIN_006 ---> FULL
MIN_007 ---> FULL
MIN_008 ---> FULL
MIN_009 ---> FULL
MIN_010 ---> FULL
MIN_011 ---> FULL
MIN_012 ---> FULL
MIN_013 ---> FULL
MIN_014 ---> FULL
MIN_015 ---> FULL
MIN_016 ---> FULL
MIN_017 ---> FULL
MIN_018 ---> FULL
MIN_019 ---> FULL
MIN_020 ---> FULL
MIN_021 ---> FULL
FULL ---> TOYOTA_001[brand/toyota/safety-common]
FULL ---> TOYOTA_002[brand/toyota/door-auto-lock-unlock]
FULL ---> TOYOTA_003[brand/toyota/tss1-sng]
FULL ---> TOYOTA_004[brand/toyota/radar-filter]
FULL ---> TOYOTA_005[brand/toyota/sdsu]
FULL ---> TOYOTA_006[brand/toyota/dsu-bypass]
FULL ---> TOYOTA_007[brand/toyota/zss]
FULL ---> TOYOTA_008[brand/toyota/stock-lon]
FULL ---> VAG_001[brand/vag/a0-sng]
FULL ---> VAG_002[brand/vag/pq-steering-patch]
FULL ---> VAG_003[brand/vag/pq-no-dashcam]
FULL ---> VAG_004[brand/vag/avoid-eps-lockout]
FULL ---> HKG_001[brand/hkg/smdps]
FULL ---> HONDA_001[brand/honda/eps-mod]
FULL ---> HONDA_002[brand/honda/nidec-stock-long]
FULL ---> SUBARU_001[brand/subaru/torque-3071]
TOYOTA_001 ---> TOYOTA[pre-toyota]
TOYOTA_002 ---> TOYOTA
TOYOTA_003 ---> TOYOTA
TOYOTA_004 ---> TOYOTA
TOYOTA_005 ---> TOYOTA
TOYOTA_006 ---> TOYOTA
TOYOTA_007 ---> TOYOTA
TOYOTA_008 ---> TOYOTA
VAG_001 ---> VAG[pre-vag]
VAG_002 ---> VAG
VAG_003 ---> VAG
VAG_004 ---> VAG
HKG_001 ---> HKG[pre-hkg]
HONDA_001 ---> HONDA[pre-honda]
SUBARU_001 ---> SUBARU[pre-subaru]
TOYOTA ---> PRE[pre]
VAG ---> PRE
HKG ---> PRE
HONDA ---> PRE
SUBARU ---> PRE
PRE ---> PRE_PATCH[pre-patch]
PRE_PATCH ---> PREBUILD[pre-build]
PREBUILD ---> VERSION[x.x.x]
```
-1877
View File
File diff suppressed because it is too large Load Diff
-30
View File
@@ -1,30 +0,0 @@
Copyright (c) 2019, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
Copyright (c) 2018, Comma.ai, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+60 -59
View File
@@ -1,74 +1,75 @@
![](dragonpilot/selfdrive/assets/dragonpilot.png) # StarPilot
[Read this in English](README_EN.md) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/firestar5683/StarPilot)
[![Discord](https://img.shields.io/discord/1137853399715549214?label=Discord)](https://firestar.link/discord)
[![Last Updated](https://img.shields.io/github/last-commit/firestar5683/StarPilot/StarPilot)](https://github.com/firestar5683/StarPilot)
[![Wiki](https://img.shields.io/badge/Wiki-StarPilot-blue?logo=wiki)](https://wiki.firestar.link)
# **🐲 dragonpilot - 賦予您的愛車「龍」之魂** **StarPilot** is a custom fork of [comma.ai's openpilot](https://comma.ai/openpilot),
an open source driver assistance system.
**我們與您一同翱翔於更智慧、更貼心的駕駛旅程。** Openpilot provides
* Automated Lane Centering
* Adaptive Cruise Control
* Lane Change Assist
* Driver Monitoring *without wheel nags*
## **👋 嘿, 朋友,歡迎您的到來!** StarPilot adds support for many GM vehicles along with improved tuning,
especially for radar-less (camera only) vehicles.
`dragonpilot` 誕生於 2019 年,由三位早期的 openpilot 華人玩家共同創立。初衷很簡單:為廣大的華人用戶、玩家們提供一個友善的交流環境、更簡便的設定協助,並加入更多適合在地使用的貼心功能。 StarPilot is built off of [StarPilot](https://github.com/FrogAi/StarPilot)
and supports the major features StarPilot offers.
我們深知在地化的重要性,特別是語言的親切感。因此,我們率先導入了完整的中文介面,讓 `dragonpilot` 迅速在華語地區累積了口碑,也讓華人的使用者數量在全球名列前茅。這份來自在地的支持,是我們持續前進的最大動力。 StarPilot has a vibrant, welcoming community [discord](https://firestar.link/discord).
Stop by to chat or ask questions!
我們以功能強大的 [openpilot](https://github.com/commaai/openpilot) 為基礎——這套據美國消費者報告評測優於市售車方案的開源輔助駕駛系統——融入了更多在地化的巧思與客製化的溫度,希望能打造出最符合您需求的駕駛夥伴。(您也可以參考我們 repo 中保留的 [openpilot 原始說明檔案](README_OPENPILOT.md)) ## Documentation
取名 `dragonpilot`,是因為我們希望它能像神話中的「龍」一樣,既強大又充滿智慧,為您的行車安全保駕護航。龍,在我們華人文化中,更是吉祥與力量的象徵,也代表著我們的根源與驕傲。 Please see [https://wiki.firestar.link](https://wiki.firestar.link) for hardware lists,
installation guides, and software configuration.
## **✨ dragonpilot 的里程碑** ## Features
我們不僅保留了 openpilot 的核心優勢,更達成了許多從社群回饋中誕生的里程碑,這些是我們引以為傲的足跡: * Full support for Comma C3, C3X, and C4
* C4 is currently in release testing. Join our fleet of C4 testers!
* Model switcher with all of comma's tinygrad driving models
* Special longitudinal planner tuning for VoACC (visual only, radar-less) vehicles
* Galaxy: StarPilot's portal to configure your comma device using your phone from anywhere.
Download models, change settings, update software, visualize live model outputs for tuning.
* Always On Lateral (full time steering assist)*
* Speed Limit Controller*
* Learning Curve Speed Controller*
* Conditional Experimental Mode (CEM)*
* Driving Profiles*
* Custom themes*
* Alert Volume Controller*
* Comma Pedal Interceptor support*
* Toyota SDSU support*
* ZSS support*
* High quality dashcam recordings*
* Enhanced tuning for CEM (dynamic experimental mode switching)
* **🚘 全時置中車道維持 (ALKA)** \* [Inherited from StarPilot](https://github.com/FrogAi/StarPilot#openpilot-vs-starpilot)
這不只是一個功能,更是 `dragonpilot` 的哲學。我們最早於 [0.6.2 版本](https://github.com/dragonpilot-community/dragonpilot/blob/2861467183d62151024320447ba04d18fc3fe1e6/selfdrive/car/toyota/carstate.py#L199) 時便實現了這個功能,其開發歷程始於 2017 Lexus IS300h,接著擴展至 Toyota 全車系,並逐步延伸到其他支援的品牌。它能溫柔地輔助您,讓車輛始終穩定地保持在車道中央,提供一份額外的安心與從容。 ## GM-only Features
* **🌐 率先導入多國語言介面** * Increased LKAS fault resiliency
* ASCM_INT and SASCM support
* Custom lateral torque controller, with special tuning for Bolts
* 50% extra torque on 2017 Chevy Bolt
* Improved lateral and longitudinal tuning
* Dashboard cruise control display speed spoofing for vehicles with pedal interceptor
* Extra steering wheel button functionality for vehicles with pedal interceptor
* Optional toggle to boot comma when remote starting your vehicle
在官方 openpilot 還未支援前,我們便已將多國語言介面實現。`dragonpilot` 完整支援繁體中文、簡體中文與英文,讓操作毫無隔閡。 ## Developer Features
* **💻 唯一同時支援多硬體平台** * Native and cross compilation for Windows, Mac, and Ubuntu
* Custom AGNOS to support C3, C3X, and C4
我們是唯一曾致力於讓專案同時兼容 EON、comma two、comma 3 與 Jetson 平台的社群分支,這份努力是為了服務最廣大的玩家社群。 * To run UI on PC:
此外,在 comma.ai 團隊於 0.10.0 版本宣布停止支持 comma 3 後,我們仍是唯一一個完整同時支援 comma 3、comma 3X 以及 O3、O3L、O3XL(O3 系列為副廠硬體)的社群分支。 * `./c3` for large UI
* `./c4` for small UI
* **📜 曾榮獲官方認證第一大分支** * `./build` to produce cross compiled binaries for comma devices.
Uses your comma's sysroot/toolchain
基於活躍的社群與功能創新,`dragonpilot` 曾一度成長為 comma ai 官方認證的第一大 openpilot 分支,這份榮耀屬於每一位參與者。 * Toggle: "Use Precompiled Binaries" to allow switching between fast boot / editable builds
* Custom long maneuver tests, specifically designed for regen-only vehicles
## **🧑‍💻 設計理念 - 少即是多 (Less is More)**
隨著 openpilot 的 AI 模型日益強大,許多過去需要手動微調的功能,現在都已能透過更先進的模型來實現。因此,我們現在的開發重心回歸到 **「最小化修改」(minimal changes)** 的核心原則上。
我們的目標是為您提供最純粹、最接近官方的 openpilot 駕駛感受,同時保留 `dragonpilot` 那些經過時間考驗、最受社群喜愛的經典功能。我們相信,在強大的 AI 基礎上,簡潔即是力量。
## **🛠️ 硬件的足跡 - 一路走來的夥伴們**
從最早的 **EON**,到官方的 **comma two / three (C2/C3/C3X)**,再到社群中各式各樣充滿智慧的**副廠機 (如 C1.5, O2, O3, O3L, O3XL 等)**,甚至我們也曾探索過在 [**Jetson Xavier NX**](https://github.com/eFiniLan/xnxpilot) 上的可能性。
目前最新版本主要支援: comma3 / 3X 以及 O3 / O3L / O3XL 等社群硬體。
針對 EON / C1.5 / C2 等舊款硬體,最後支援的版本位於 [d2 分支](https://github.com/dragonpilot-community/dragonpilot/tree/d2)。
無論您手上是哪一款設備,都代表著您對開源駕駛輔助的一份熱情。
## **🫂 加入我們,成為「尋龍者」的一份子**
`dragonpilot` 的成長,離不開每一位使用者的貢獻與回饋。我們是一個以**公開、透明**為原則的溫暖社群,希望在這裡能與所有對 openpilot / dragonpilot 有興趣的用戶分享、交流開發與使用上的經驗。
[**歡迎加入我們的 Facebook 社團進行交流!**](https://www.facebook.com/groups/930190251238639)
## **❤️ 特別感謝**
`dragonpilot` 從創立至今,從未打算透過 Patreon 等平台進行任何形式的募資。我們的初衷是建立一個讓大家能一起學習、一起成長的社群。It's all about fun, not money.
然而,我們仍要對那些自發性支持本專案的朋友們,致上最誠摯的感謝。正是因為有您們的鼓勵,我們才有更大的動力持續前進。
[**我們的贊助者名單**](SPONSORS.md)
### **安全聲明**
`dragonpilot` 是一種駕駛**輔助**系統,並非全自動駕駛。它旨在減輕您的駕駛疲勞,提升行車安全,但駕駛人仍需時刻保持專注,並隨時準備接管車輛。請務必遵守您所在地區的交通法規。
**最後,再次感謝您的到來。**
**期待與您一同在智慧駕駛的道路上,乘「龍」而行!**
-74
View File
@@ -1,74 +0,0 @@
![](dragonpilot/selfdrive/assets/dragonpilot.png)
[Read this in Chinese](README.md)
# **🐲 dragonpilot - Bringing the Spirit of the Dragon to Your Car**
**Join us on a smarter, more thoughtful driving journey.**
## **👋 Welcome, friend!**
`dragonpilot` was launched in 2019 by three early openpilot enthusiasts from the Chinese community. Our mission was simple: create a friendly space for users to share experiences, provide easier setup help, and add features tailored for local needs.
Localization has always been at the heart of what we do—starting with a fully Chinese interface. This made `dragonpilot` quickly popular in Chinese-speaking regions and helped our user base grow into one of the largest worldwide. That community support is what keeps us moving forward.
Built on top of the powerful [openpilot](https://github.com/commaai/openpilot)—an open-source driver assistance system rated by Consumer Reports as outperforming commercial offerings—we add localized refinements and user-focused features to create a driving companion that truly fits your needs. (You can also see the [original openpilot README](README_OPENPILOT.md) preserved in our repo.)
The name `dragonpilot` reflects our vision: like the dragon of mythology, it is strong and wise, guarding your safety on the road. In Chinese culture, the dragon is also a symbol of luck and strength, representing our roots and pride.
## **✨ Milestones**
Beyond carrying forward openpilot's core strengths, we've reached several milestones inspired by community feedback:
* **🚘 Always Lane Keep Assist (ALKA)**
More than a feature—it's part of the `dragonpilot` philosophy. Introduced as early as [version 0.6.2](https://github.com/dragonpilot-community/dragonpilot/blob/2861467183d62151024320447ba04d18fc3fe1e6/selfdrive/car/toyota/carstate.py#L199), first tested on a 2017 Lexus IS300h, then expanded to Toyota's lineup and beyond. ALKA helps keep your vehicle steadily centered, giving you extra confidence on the road.
* **🌐 First to add multilingual support**
Before openpilot officially supported it, we had already introduced multiple languages. `dragonpilot` fully supports Traditional Chinese, Simplified Chinese, and English.
* **💻 Only community fork to support multiple hardware platforms at once**
We uniquely worked to make the project run on EON, comma two, comma 3, and Jetson—serving the widest range of users possible.
Additionally, after the comma.ai team deprecated the comma 3 in version 0.10.0, we remain the only community fork to offer full, simultaneous support for the comma 3, comma 3X, and the O3, O3L, and O3XL (the O3 series being third-party hardware).
* **📜 Once recognized as the #1 openpilot fork**
Thanks to an active community and continuous innovation, `dragonpilot` was once the largest openpilot fork officially recognized by comma ai. This honor belongs to everyone who contributed.
## **🧑‍💻 Design Philosophy - Less is More**
As openpilot's AI grows stronger, many features that once required manual tuning are now handled by advanced models. That's why our focus has returned to **“minimal changes.”**
We aim to give you the purest, most official-like openpilot driving experience—while preserving `dragonpilot`'s classic, community-loved features. With a solid AI foundation, simplicity is strength.
## **🛠️ Hardware Journey**
From the early **EON**, to official devices like **comma two / three (C2/C3/C3X)**, to creative community builds (**C1.5, O2, O3, O3L, O3XL, etc.**), and even experiments with [**Jetson Xavier NX**](https://github.com/eFiniLan/xnxpilot).
Currently, the latest versions support: **comma3 / 3X** and community hardware like **O3 / O3L / O3XL**.
Older devices such as **EON / C1.5 / C2** are supported in the [d2 branch](https://github.com/dragonpilot-community/dragonpilot/tree/d2).
Whatever device you're on, it represents your passion for open-source driver assistance.
## **🫂 Join Us Become a “Dragon Seeker”**
`dragonpilot` thrives thanks to every user's contributions and feedback. We're an open, transparent, and welcoming community where enthusiasts can share experiences with openpilot and `dragonpilot`.
[**Join our Facebook group here!**](https://www.facebook.com/groups/930190251238639)
## **❤️ Special Thanks**
Since day one, `dragonpilot` has never asked for funding through Patreon or similar platforms. Our vision is a community where everyone learns and grows together. It's about fun, not money.
That said, we're deeply grateful to those who voluntarily supported the project. Your encouragement keeps us motivated to keep building.
[**See our sponsors**](SPONSORS.md)
### **Safety Notice**
`dragonpilot` is a driver **assistance** system, not full self-driving. It reduces fatigue and improves safety, but you must remain alert and ready to take control at all times. Always follow your local traffic laws.
**Thanks again for being here.**
**We look forward to riding the “dragon” with you on the road to smarter driving!**
-111
View File
@@ -1,111 +0,0 @@
<div align="center" style="text-align: center;">
<h1>openpilot</h1>
<p>
<b>openpilot is an operating system for robotics.</b>
<br>
Currently, it upgrades the driver assistance system in 300+ supported cars.
</p>
<h3>
<a href="https://docs.comma.ai">Docs</a>
<span> · </span>
<a href="https://docs.comma.ai/contributing/roadmap/">Roadmap</a>
<span> · </span>
<a href="https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md">Contribute</a>
<span> · </span>
<a href="https://discord.comma.ai">Community</a>
<span> · </span>
<a href="https://comma.ai/shop">Try it on a comma 3X</a>
</h3>
Quick start: `bash <(curl -fsSL openpilot.comma.ai)`
[![openpilot tests](https://github.com/commaai/openpilot/actions/workflows/tests.yaml/badge.svg)](https://github.com/commaai/openpilot/actions/workflows/tests.yaml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![X Follow](https://img.shields.io/twitter/follow/comma_ai)](https://x.com/comma_ai)
[![Discord](https://img.shields.io/discord/469524606043160576)](https://discord.comma.ai)
</div>
<table>
<tr>
<td><a href="https://youtu.be/NmBfgOanCyk" title="Video By Greer Viau"><img src="https://github.com/commaai/openpilot/assets/8762862/2f7112ae-f748-4f39-b617-fabd689c3772"></a></td>
<td><a href="https://youtu.be/VHKyqZ7t8Gw" title="Video By Logan LeGrand"><img src="https://github.com/commaai/openpilot/assets/8762862/92351544-2833-40d7-9e0b-7ef7ae37ec4c"></a></td>
<td><a href="https://youtu.be/SUIZYzxtMQs" title="A drive to Taco Bell"><img src="https://github.com/commaai/openpilot/assets/8762862/05ceefc5-2628-439c-a9b2-89ce77dc6f63"></a></td>
</tr>
</table>
Using openpilot in a car
------
To use openpilot in a car, you need four things:
1. **Supported Device:** a comma 3X, available at [comma.ai/shop](https://comma.ai/shop/comma-3x).
2. **Software:** The setup procedure for the comma 3X allows users to enter a URL for custom software. Use the URL `openpilot.comma.ai` to install the release version.
3. **Supported Car:** Ensure that you have one of [the 275+ supported cars](docs/CARS.md).
4. **Car Harness:** You will also need a [car harness](https://comma.ai/shop/car-harness) to connect your comma 3X to your car.
We have detailed instructions for [how to install the harness and device in a car](https://comma.ai/setup). Note that it's possible to run openpilot on [other hardware](https://blog.comma.ai/self-driving-car-for-free/), although it's not plug-and-play.
### Branches
Running `master` and other branches directly is supported, but it's recommended to run one of the following prebuilt branches:
| comma four branch | comma 3X branch | URL | description |
|------------------------|------------------------|----------------------------------------|-------------------------------------------------------------------------------------|
| `release-mici` | `release-tizi` | openpilot.comma.ai | This is openpilot's release branch. |
| `release-mici-staging` | `release-tizi-staging` | openpilot-test.comma.ai | This is the staging branch for releases. Use it to get new releases slightly early. |
| `nightly` | `nightly` | openpilot-nightly.comma.ai | This is the bleeding edge development branch. Do not expect this to be stable. |
| `nightly-dev` | `nightly-dev` | installer.comma.ai/commaai/nightly-dev | Same as nightly, but includes experimental development features for some cars. |
To start developing openpilot
------
openpilot is developed by [comma](https://comma.ai/) and by users like you. We welcome both pull requests and issues on [GitHub](http://github.com/commaai/openpilot).
* Join the [community Discord](https://discord.comma.ai)
* Check out [the contributing docs](docs/CONTRIBUTING.md)
* Check out the [openpilot tools](tools/)
* Code documentation lives at https://docs.comma.ai
* Information about running openpilot lives on the [community wiki](https://github.com/commaai/openpilot/wiki)
Want to get paid to work on openpilot? [comma is hiring](https://comma.ai/jobs#open-positions) and offers lots of [bounties](https://comma.ai/bounties) for external contributors.
Safety and Testing
----
* openpilot observes [ISO26262](https://en.wikipedia.org/wiki/ISO_26262) guidelines, see [SAFETY.md](docs/SAFETY.md) for more details.
* openpilot has software-in-the-loop [tests](.github/workflows/tests.yaml) that run on every commit.
* The code enforcing the safety model lives in panda and is written in C, see [code rigor](https://github.com/commaai/panda#code-rigor) for more details.
* panda has software-in-the-loop [safety tests](https://github.com/commaai/panda/tree/master/tests/safety).
* Internally, we have a hardware-in-the-loop Jenkins test suite that builds and unit tests the various processes.
* panda has additional hardware-in-the-loop [tests](https://github.com/commaai/panda/blob/master/Jenkinsfile).
* We run the latest openpilot in a testing closet containing 10 comma devices continuously replaying routes.
<details>
<summary>MIT Licensed</summary>
openpilot is released under the MIT license. Some parts of the software are released under other licenses as specified.
Any user of this software shall indemnify and hold harmless Comma.ai, Inc. and its directors, officers, employees, agents, stockholders, affiliates, subcontractors and customers from and against all allegations, claims, actions, suits, demands, damages, liabilities, obligations, losses, settlements, judgments, costs and expenses (including without limitation attorneys fees and costs) which arise out of, relate to or result from any use of this software by user.
**THIS IS ALPHA QUALITY SOFTWARE FOR RESEARCH PURPOSES ONLY. THIS IS NOT A PRODUCT.
YOU ARE RESPONSIBLE FOR COMPLYING WITH LOCAL LAWS AND REGULATIONS.
NO WARRANTY EXPRESSED OR IMPLIED.**
</details>
<details>
<summary>User Data and comma Account</summary>
By default, openpilot uploads the driving data to our servers. You can also access your data through [comma connect](https://connect.comma.ai/). We use your data to train better models and improve openpilot for everyone.
openpilot is open source software: the user is free to disable data collection if they wish to do so.
openpilot logs the road-facing cameras, CAN, GPS, IMU, magnetometer, thermal sensors, crashes, and operating system logs.
The driver-facing camera and microphone are only logged if you explicitly opt-in in settings.
By using openpilot, you agree to [our Privacy Policy](https://comma.ai/privacy). You understand that use of this software or its related services will generate certain types of user data, which may be logged and stored at the sole discretion of comma. By accepting this agreement, you grant an irrevocable, perpetual, worldwide right to comma for the use of this data.
</details>
+381 -95
View File
@@ -1,56 +1,266 @@
import os import os
import shutil
import subprocess import subprocess
import sys import sys
import sysconfig import sysconfig
import platform import platform
import shlex
import numpy as np import numpy as np
import SCons.Errors import SCons.Errors
SCons.Warnings.warningAsException(True) SCons.Warnings.warningAsException(True)
# pending upstream fix - https://github.com/SCons/scons/issues/4461
#SetOption('warn', 'all')
force_tici = os.environ.get("SP_FORCE_TICI", "").lower() in {"1", "true", "yes", "on"}
TICI = os.path.isfile('/TICI') or force_tici
AGNOS = TICI
Decider('MD5-timestamp') Decider('MD5-timestamp')
SetOption('num_jobs', max(1, int(os.cpu_count()/2))) SetOption('num_jobs', max(1, int(os.cpu_count()/2)))
AddOption('--kaitai', action='store_true', help='Regenerate kaitai struct parsers') AddOption('--kaitai',
AddOption('--asan', action='store_true', help='turn on ASAN') action='store_true',
AddOption('--ubsan', action='store_true', help='turn on UBSan') help='Regenerate kaitai struct parsers')
AddOption('--mutation', action='store_true', help='generate mutation-ready code')
AddOption('--ccflags', action='store', type='string', default='', help='pass arbitrary flags over the command line') AddOption('--asan',
action='store_true',
help='turn on ASAN')
AddOption('--ubsan',
action='store_true',
help='turn on UBSan')
AddOption('--coverage',
action='store_true',
help='build with test coverage options')
AddOption('--clazy',
action='store_true',
help='build with clazy')
AddOption('--ccflags',
action='store',
type='string',
default='',
help='pass arbitrary flags over the command line')
AddOption('--external-sconscript',
action='store',
metavar='FILE',
dest='external_sconscript',
help='add an external SConscript to the build')
AddOption('--mutation',
action='store_true',
help='generate mutation-ready code')
AddOption('--minimal', AddOption('--minimal',
action='store_false', action='store_false',
dest='extras', dest='extras',
default=os.path.exists(File('#.gitattributes').abspath), # minimal by default on release branch (where there's no LFS) default=os.path.exists(File('#.lfsconfig').abspath), # minimal by default on release branch (where there's no LFS)
help='the minimum build to run openpilot. no tests, tools, etc.') help='the minimum build to run openpilot. no tests, tools, etc.')
# Detect platform AddOption('--extras',
arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip() action='store_true',
if platform.system() == "Darwin": dest='extras',
default=os.path.exists(File('#.lfsconfig').abspath),
help='build optional tools/tests even when minimal is the default')
def maybe_delegate_to_laptop_device_builder() -> None:
if platform.system() != "Darwin":
return
if os.environ.get("SP_FORCE_ARCH"):
return
if os.environ.get("SP_SKIP_CONTAINER_REEXEC"):
return
if os.environ.get("SP_DISABLE_AUTO_DEVICE_SCONS", "").lower() in {"1", "true", "yes", "on"}:
return
basedir = Dir("#").abspath
sysroot_dir = os.environ.get("COMMA_SYSROOT_DIR", os.path.join(basedir, ".comma_sysroot"))
required_sysroot_dirs = (
"usr/local/lib",
"lib/aarch64-linux-gnu",
"usr/lib/aarch64-linux-gnu",
"system/vendor/lib64",
)
if not all(os.path.isdir(os.path.join(sysroot_dir, p)) for p in required_sysroot_dirs):
return
docker_bin = shutil.which("docker")
if docker_bin is None:
mac_docker = "/Applications/Docker.app/Contents/Resources/bin/docker"
if os.path.isfile(mac_docker):
docker_bin = mac_docker
if docker_bin is None and shutil.which("podman") is None:
return
builder = os.path.join(basedir, "scripts", "laptop_device_build.sh")
if not os.path.isfile(builder):
return
print(f"Auto-routing scons to laptop device build (sysroot: {sysroot_dir})", flush=True)
env = os.environ.copy()
env["SP_SKIP_CONTAINER_REEXEC"] = "1"
env.setdefault("COMMA_SYSROOT_DIR", sysroot_dir)
if docker_bin is not None and shutil.which("docker") is None:
docker_dir = os.path.dirname(docker_bin)
env["PATH"] = f"{docker_dir}:{env.get('PATH', '')}"
cmd = [builder, "build", *sys.argv[1:]]
raise SystemExit(subprocess.call(cmd, cwd=basedir, env=env))
maybe_delegate_to_laptop_device_builder()
## Architecture name breakdown (arch)
## - larch64: linux tici aarch64
## - aarch64: linux pc aarch64
## - x86_64: linux pc x64
## - Darwin: mac x64 or arm64
real_arch = arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()
forced_arch = os.environ.get("SP_FORCE_ARCH")
if forced_arch:
arch = forced_arch
elif platform.system() == "Darwin":
arch = "Darwin" arch = "Darwin"
brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip() brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip()
elif arch == "aarch64" and os.path.isfile('/TICI'): elif arch == "aarch64" and AGNOS:
arch = "larch64" arch = "larch64"
assert arch in [ assert arch in ["larch64", "aarch64", "x86_64", "Darwin"]
"larch64", # linux tici arm64
"aarch64", # linux pc arm64
"x86_64", # linux pc x64
"Darwin", # macOS arm64 (x86 not supported)
]
env = Environment( # Homebrew llvm can shadow Apple clang and break macOS SDK header resolution.
ENV={ # Use the system toolchain explicitly on macOS for reliable local builds.
cc = '/usr/bin/clang' if arch == "Darwin" else 'clang'
cxx = '/usr/bin/clang++' if arch == "Darwin" else 'clang++'
ar = '/usr/bin/ar' if arch == "Darwin" else 'ar'
ranlib = '/usr/bin/ranlib' if arch == "Darwin" else 'ranlib'
lenv = {
"PATH": os.environ['PATH'], "PATH": os.environ['PATH'],
"PYTHONPATH": Dir("#").abspath + ':' + Dir(f"#third_party/acados").abspath, "PYTHONPATH": ":".join([
Dir("#").abspath,
Dir("#third_party/acados").abspath,
Dir("#opendbc_repo").abspath,
]),
"ACADOS_SOURCE_DIR": Dir("#third_party/acados").abspath, "ACADOS_SOURCE_DIR": Dir("#third_party/acados").abspath,
"ACADOS_PYTHON_INTERFACE_PATH": Dir("#third_party/acados/acados_template").abspath, "ACADOS_PYTHON_INTERFACE_PATH": Dir("#third_party/acados/acados_template").abspath,
"TERA_PATH": Dir("#").abspath + f"/third_party/acados/{arch}/t_renderer" "TERA_PATH": Dir("#").abspath + f"/third_party/acados/{arch}/t_renderer"
}, }
CC='clang',
CXX='clang++', # Allow callers to override cache/temp dirs used by subprocesses (e.g. tinygrad model compilation).
for key in ("HOME", "TMPDIR", "XDG_CACHE_HOME", "CACHEDB"):
if key in os.environ:
lenv[key] = os.environ[key]
rpath = []
arch_ldflags = []
def tici_libpath(path: str) -> str:
tici_sysroot = os.environ.get("SP_TICI_SYSROOT", "").strip().rstrip("/")
if arch != "larch64" or not tici_sysroot or not path.startswith("/"):
return path
return os.path.join(tici_sysroot, path.lstrip("/"))
if arch == "larch64":
cpppath = [
"#third_party/opencl/include",
tici_libpath("/usr/local/include"),
tici_libpath("/usr/include"),
tici_libpath("/usr/include/aarch64-linux-gnu"),
]
libpath = [
tici_libpath("/usr/local/lib"),
tici_libpath("/system/vendor/lib64"),
f"#third_party/acados/{arch}/lib",
]
libpath += [
"#third_party/libyuv/larch64/lib",
tici_libpath("/lib/aarch64-linux-gnu"),
tici_libpath("/usr/lib/aarch64-linux-gnu")
]
cflags = ["-D__TICI__", "-DQCOM2", "-mcpu=cortex-a57"]
cxxflags = ["-D__TICI__", "-DQCOM2", "-mcpu=cortex-a57"]
arch_ldflags += [
f"-Wl,-rpath-link,{tici_libpath('/usr/local/lib')}",
f"-Wl,-rpath-link,{tici_libpath('/lib/aarch64-linux-gnu')}",
f"-Wl,-rpath-link,{tici_libpath('/usr/lib/aarch64-linux-gnu')}",
f"-Wl,-rpath-link,{tici_libpath('/system/vendor/lib64')}",
f"-Wl,-rpath-link,{tici_libpath('/vendor/lib64')}",
]
# On non-aarch64 hosts (e.g. Docker on macOS), force clang cross-targeting.
if platform.machine() not in ("aarch64", "arm64"):
cross_target = os.environ.get("SP_CROSS_TARGET", "aarch64-linux-gnu")
cflags += [f"--target={cross_target}"]
cxxflags += [f"--target={cross_target}"]
arch_ldflags += [f"--target={cross_target}"]
rpath += ["/usr/local/lib"]
else:
cflags = []
cxxflags = []
cpppath = []
rpath += []
# MacOS
if arch == "Darwin":
libpath = [
f"#third_party/libyuv/{arch}/lib",
f"#third_party/acados/{arch}/lib",
f"{brew_prefix}/lib",
f"{brew_prefix}/opt/openssl@3.0/lib",
"/System/Library/Frameworks/OpenGL.framework/Libraries",
]
cflags += ["-DGL_SILENCE_DEPRECATION"]
cxxflags += ["-DGL_SILENCE_DEPRECATION"]
cpppath += [
f"{brew_prefix}/include",
f"{brew_prefix}/opt/openssl@3.0/include",
]
# Linux
else:
libpath = [
f"#third_party/acados/{arch}/lib",
f"#third_party/libyuv/{arch}/lib",
"/usr/lib",
"/usr/local/lib",
]
if GetOption('asan'):
ccflags = ["-fsanitize=address", "-fno-omit-frame-pointer"]
ldflags = ["-fsanitize=address"]
elif GetOption('ubsan'):
ccflags = ["-fsanitize=undefined"]
ldflags = ["-fsanitize=undefined"]
else:
ccflags = []
ldflags = []
ldflags += arch_ldflags
# AGNOS devices are memory-constrained during on-device C++ compiles.
# Building without debug symbols dramatically reduces peak clang memory and
# prevents lowmemorykiller SIGKILLs (Error -9) on large translation units.
use_debug_symbols = os.environ.get("SP_FORCE_DEBUG_SYMBOLS", "").lower() in {"1", "true", "yes", "on"}
debug_flag = "-g" if (not AGNOS or use_debug_symbols) else "-g0"
# no --as-needed on mac linker
if arch != "Darwin":
ldflags += ["-Wl,--as-needed", "-Wl,--no-undefined"]
ccflags_option = GetOption('ccflags')
if ccflags_option:
ccflags += ccflags_option.split(' ')
env = Environment(
ENV=lenv,
CCFLAGS=[ CCFLAGS=[
"-g", debug_flag,
"-fPIC", "-fPIC",
"-O2", "-O2",
"-Wunused", "-Wunused",
@@ -61,31 +271,38 @@ env = Environment(
"-Wno-c99-designator", "-Wno-c99-designator",
"-Wno-reorder-init-list", "-Wno-reorder-init-list",
"-Wno-vla-cxx-extension", "-Wno-vla-cxx-extension",
], ] + cflags + ccflags,
CFLAGS=["-std=gnu11"],
CXXFLAGS=["-std=c++1z"], CPPPATH=cpppath + [
CPPPATH=[
"#", "#",
"#msgq",
"#third_party",
"#third_party/json11",
"#third_party/linux/include",
"#third_party/acados/include", "#third_party/acados/include",
"#third_party/acados/include/blasfeo/include", "#third_party/acados/include/blasfeo/include",
"#third_party/acados/include/hpipm/include", "#third_party/acados/include/hpipm/include",
"#third_party/catch2/include", "#third_party/catch2/include",
"#third_party/libyuv/include", "#third_party/libyuv/include",
"#third_party/json11",
"#third_party/linux/include",
"#third_party",
"#msgq",
], ],
LIBPATH=[
"#common", CC=cc,
CXX=cxx,
AR=ar,
RANLIB=ranlib,
LINKFLAGS=ldflags,
RPATH=rpath,
CFLAGS=["-std=gnu11"] + cflags,
CXXFLAGS=["-std=c++1z"] + cxxflags,
LIBPATH=libpath + [
"#msgq_repo", "#msgq_repo",
"#third_party", "#third_party",
"#selfdrive/pandad", "#selfdrive/pandad",
"#common",
"#rednose/helpers", "#rednose/helpers",
f"#third_party/libyuv/{arch}/lib",
f"#third_party/acados/{arch}/lib",
], ],
RPATH=[],
CYTHONCFILESUFFIX=".cpp", CYTHONCFILESUFFIX=".cpp",
COMPILATIONDB_USE_ABSPATH=True, COMPILATIONDB_USE_ABSPATH=True,
REDNOSE_ROOT="#", REDNOSE_ROOT="#",
@@ -93,64 +310,36 @@ env = Environment(
toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"], toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
) )
# Arch-specific flags and paths if arch == "Darwin":
if arch == "larch64": # RPATH is not supported on macOS, instead use the linker flags
env.Append(CPPPATH=["#third_party/opencl/include"]) darwin_rpath_link_flags = [f"-Wl,-rpath,{path}" for path in env["RPATH"]]
env.Append(LIBPATH=[ env["LINKFLAGS"] += darwin_rpath_link_flags
"/usr/local/lib",
"/system/vendor/lib64",
"/usr/lib/aarch64-linux-gnu",
])
arch_flags = ["-D__TICI__", "-mcpu=cortex-a57"]
env.Append(CCFLAGS=arch_flags)
env.Append(CXXFLAGS=arch_flags)
elif arch == "Darwin":
env.Append(LIBPATH=[
f"{brew_prefix}/lib",
f"{brew_prefix}/opt/openssl@3.0/lib",
f"{brew_prefix}/opt/llvm/lib/c++",
"/System/Library/Frameworks/OpenGL.framework/Libraries",
])
env.Append(CCFLAGS=["-DGL_SILENCE_DEPRECATION"])
env.Append(CXXFLAGS=["-DGL_SILENCE_DEPRECATION"])
env.Append(CPPPATH=[
f"{brew_prefix}/include",
f"{brew_prefix}/opt/openssl@3.0/include",
])
else:
env.Append(LIBPATH=[
"/usr/lib",
"/usr/local/lib",
])
# Sanitizers and extra CCFLAGS from CLI env.CompilationDatabase('compile_commands.json')
if GetOption('asan'):
env.Append(CCFLAGS=["-fsanitize=address", "-fno-omit-frame-pointer"])
env.Append(LINKFLAGS=["-fsanitize=address"])
elif GetOption('ubsan'):
env.Append(CCFLAGS=["-fsanitize=undefined"])
env.Append(LINKFLAGS=["-fsanitize=undefined"])
_extra_cc = shlex.split(GetOption('ccflags') or '') # Setup cache dir
if _extra_cc: cache_dir = os.environ.get("SP_SCONS_CACHE_DIR", "").strip()
env.Append(CCFLAGS=_extra_cc) if not cache_dir:
cache_dir = '/data/scons_cache' if AGNOS else '/tmp/scons_cache'
CacheDir(cache_dir)
Clean(["."], cache_dir)
# no --as-needed on mac linker
if arch != "Darwin":
env.Append(LINKFLAGS=["-Wl,--as-needed", "-Wl,--no-undefined"])
# progress output
node_interval = 5 node_interval = 5
node_count = 0 node_count = 0
def progress_function(node): def progress_function(node):
global node_count global node_count
node_count += node_interval node_count += node_interval
sys.stderr.write("progress: %d\n" % node_count) sys.stderr.write("progress: %d\n" % node_count)
if os.environ.get('SCONS_PROGRESS'): if os.environ.get('SCONS_PROGRESS'):
Progress(progress_function, interval=node_interval) Progress(progress_function, interval=node_interval)
# ********** Cython build environment ********** # Cython build environment
py_include = sysconfig.get_paths()['include'] py_include = sysconfig.get_paths()['include']
if arch == "larch64" and platform.machine() not in ("aarch64", "arm64"):
tici_py_include = tici_libpath(f"/usr/include/python{sys.version_info.major}.{sys.version_info.minor}")
if os.path.isdir(tici_py_include):
py_include = tici_py_include
envCython = env.Clone() envCython = env.Clone()
envCython["CPPPATH"] += [py_include, np.get_include()] envCython["CPPPATH"] += [py_include, np.get_include()]
envCython["CCFLAGS"] += ["-Wno-#warnings", "-Wno-shadow", "-Wno-deprecated-declarations"] envCython["CCFLAGS"] += ["-Wno-#warnings", "-Wno-shadow", "-Wno-deprecated-declarations"]
@@ -158,27 +347,124 @@ envCython["CCFLAGS"].remove("-Werror")
envCython["LIBS"] = [] envCython["LIBS"] = []
if arch == "Darwin": if arch == "Darwin":
envCython["LINKFLAGS"] = env["LINKFLAGS"] + ["-bundle", "-undefined", "dynamic_lookup"] envCython["LINKFLAGS"] = ["-bundle", "-undefined", "dynamic_lookup"] + darwin_rpath_link_flags
else: else:
envCython["LINKFLAGS"] = ["-pthread", "-shared"] envCython["LINKFLAGS"] = arch_ldflags + ["-pthread", "-shared"]
np_version = SCons.Script.Value(np.__version__) np_version = SCons.Script.Value(np.__version__)
Export('envCython', 'np_version') Export('envCython', 'np_version')
Export('env', 'arch') # Qt build environment
qt_env = env.Clone()
qt_modules = ["Widgets", "Gui", "Core", "Network", "Concurrent", "DBus", "Xml"]
# Setup cache dir qt_libs = []
cache_dir = '/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache' if arch == "Darwin":
CacheDir(cache_dir) qt_env['QTDIR'] = f"{brew_prefix}/opt/qt@5"
Clean(["."], cache_dir) qt_dirs = [
os.path.join(qt_env['QTDIR'], "include"),
]
qt_dirs += [f"{qt_env['QTDIR']}/include/Qt{m}" for m in qt_modules]
qt_env["LINKFLAGS"] += ["-F" + os.path.join(qt_env['QTDIR'], "lib")]
qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules] + ["OpenGL"]
qt_env.AppendENVPath('PATH', os.path.join(qt_env['QTDIR'], "bin"))
else:
if arch == "larch64":
qt_env.PrependENVPath('PATH', Dir("#third_party/qt5/larch64/bin/").abspath)
# For laptop/device builds that mount an AGNOS sysroot, always prefer Qt
# headers from that sysroot to keep headers/libs ABI-matched (Qt 5.12.x).
if arch == "larch64" and os.environ.get("SP_TICI_SYSROOT"):
qt_install_prefix = tici_libpath("/usr")
qt_install_headers = tici_libpath("/usr/include/aarch64-linux-gnu/qt5")
else:
qmake = os.environ.get("SP_QMAKE", "qmake")
qt_install_prefix = subprocess.check_output([qmake, '-query', 'QT_INSTALL_PREFIX'], encoding='utf8').strip()
qt_install_headers = subprocess.check_output([qmake, '-query', 'QT_INSTALL_HEADERS'], encoding='utf8').strip()
# ********** start building stuff ********** qt_env['QTDIR'] = qt_install_prefix
qt_dirs = [
f"{qt_install_headers}",
]
qt_gui_path = os.path.join(qt_install_headers, "QtGui")
qt_gui_dirs = [d for d in os.listdir(qt_gui_path) if os.path.isdir(os.path.join(qt_gui_path, d))]
qt_dirs += [f"{qt_install_headers}/QtGui/{qt_gui_dirs[0]}/QtGui", ] if qt_gui_dirs else []
qt_dirs += [f"{qt_install_headers}/Qt{m}" for m in qt_modules]
qt_libs = [f"Qt5{m}" for m in qt_modules]
if arch == "larch64":
qt_libs += ["GLESv2", "wayland-client"]
elif arch != "Darwin":
qt_libs += ["GL"]
qt_env['QT3DIR'] = qt_env['QTDIR']
qt_env.Tool('qt3')
if arch == "larch64" and os.environ.get("SP_TICI_SYSROOT"):
qt_tool_bin = tici_libpath("/usr/lib/qt5/bin")
qt_tool_root = tici_libpath("/")
qt_arm_moc = os.path.join(qt_tool_bin, "moc")
qt_arm_uic = os.path.join(qt_tool_bin, "uic")
qt_arm_rcc = os.path.join(qt_tool_bin, "rcc")
if platform.machine() in ("aarch64", "arm64"):
if os.path.isfile(qt_arm_moc):
qt_env['QT3_MOC'] = qt_arm_moc
if os.path.isfile(qt_arm_uic):
qt_env['QT3_UIC'] = qt_arm_uic
if os.path.isfile(qt_arm_rcc):
qt_env['SP_QT_RCC'] = qt_arm_rcc
else:
qt_qemu = shutil.which("qemu-aarch64-static") or shutil.which("qemu-aarch64")
if qt_qemu and os.path.isfile(qt_arm_moc):
qt_env['QT3_MOC'] = f"{qt_qemu} -L {qt_tool_root} {qt_arm_moc}"
else:
qt_host_bin = os.environ.get("SP_QT_HOST_BIN", "/usr/lib/qt5/bin")
qt_env['QT3_MOC'] = os.environ.get("SP_QT_HOST_MOC", os.path.join(qt_host_bin, "moc"))
if qt_qemu and os.path.isfile(qt_arm_uic):
qt_env['QT3_UIC'] = f"{qt_qemu} -L {qt_tool_root} {qt_arm_uic}"
else:
qt_host_bin = os.environ.get("SP_QT_HOST_BIN", "/usr/lib/qt5/bin")
qt_env['QT3_UIC'] = os.environ.get("SP_QT_HOST_UIC", os.path.join(qt_host_bin, "uic"))
if qt_qemu and os.path.isfile(qt_arm_rcc):
qt_env['SP_QT_RCC'] = f"{qt_qemu} -L {qt_tool_root} {qt_arm_rcc}"
else:
qt_env['SP_QT_RCC'] = os.environ.get("SP_QT_HOST_RCC", "rcc")
qt_env['CPPPATH'] += qt_dirs + ["#third_party/qrcode"]
qt_flags = [
"-D_REENTRANT",
"-DQT_NO_DEBUG",
"-DQT_WIDGETS_LIB",
"-DQT_GUI_LIB",
"-DQT_CORE_LIB",
"-DQT_MESSAGELOGCONTEXT",
]
qt_env['CXXFLAGS'] += qt_flags
qt_env['LIBPATH'] += ['#selfdrive/ui', ]
qt_env['LIBS'] = qt_libs
if GetOption("clazy"):
checks = [
"level0",
"level1",
"no-range-loop",
"no-non-pod-global-static",
]
qt_env['CXX'] = 'clazy'
qt_env['ENV']['CLAZY_IGNORE_DIRS'] = qt_dirs[0]
qt_env['ENV']['CLAZY_CHECKS'] = ','.join(checks)
Export('env', 'qt_env', 'arch', 'real_arch')
# Build common module # Build common module
SConscript(['common/SConscript']) SConscript(['common/SConscript'])
Import('_common') Import('_common', '_gpucommon')
common = [_common, 'json11', 'zmq'] common = [_common, 'json11', 'zmq']
Export('common') gpucommon = [_gpucommon]
Export('common', 'gpucommon')
# Build messaging (cereal + msgq + socketmaster + their dependencies) # Build messaging (cereal + msgq + socketmaster + their dependencies)
# Enable swaglog include in submodules # Enable swaglog include in submodules
@@ -196,7 +482,6 @@ Export('messaging')
# Build other submodules # Build other submodules
SConscript(['panda/SConscript']) SConscript(['panda/SConscript'])
SConscript(['panda_tici/SConscript'])
# Build rednose library # Build rednose library
SConscript(['rednose/SConscript']) SConscript(['rednose/SConscript'])
@@ -220,5 +505,6 @@ if Dir('#tools/cabana/').exists() and GetOption('extras'):
if arch != "larch64": if arch != "larch64":
SConscript(['tools/cabana/SConscript']) SConscript(['tools/cabana/SConscript'])
external_sconscript = GetOption('external_sconscript')
env.CompilationDatabase('compile_commands.json') if external_sconscript:
SConscript([external_sconscript])
-26
View File
@@ -1,26 +0,0 @@
# Sponsors 贊助者
我們誠摯感謝以下贊助者提供的硬體資源,讓專案能夠在多種平台上進行測試與驗證。
We sincerely thank the following sponsors for providing hardware resources, which enable the project to be tested and validated across multiple platforms.
---
## 贊助者列表 Sponsors
| 贊助者 Sponsor | 設備 Deices | 備註 Notes |
| -------------- | ----------- | ---------- |
| BlueGood | <ul><li>Radar Filter x 2</li><li>sDSU x1</li></ul> | - |
| Chia Chun Lee | <ul><li>C3 Quick Mount x1</li></ul> | - |
| CloudJ | <ul><li>C3X x1</li></ul> | - |
| FareWay | <ul><li>EON Quick Mount x1</li><li>C2/C3 Quick Mount x1</li></ul> | Special thanks for fixing my good old EON |
| Fred Wang | <ul><li>Oneplus 3t x1</li></ul> | - |
| Saber Huang | <ul><li>O3L x1</li></ul> | - |
| [馬威 Mr. One](https://shop61532546.taobao.com/) | <ul><li>O3 x 1</li><li>O3 (Dev) x1</li><li>O3XL x1</li><li>Red Panda x1</li><li>Panda Jungle v1 x1</li></ul> | - |
| 門文梁 | <ul><li>C1.5 x1</li></ul> | - |
---
🙏 沒有你們的支持,我們無法讓專案在這麼多硬體平台上持續成長與驗證。
Without your support, this project could not continue to grow and be validated across so many hardware platforms.
+2972
View File
File diff suppressed because it is too large Load Diff
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "${ROOT_DIR}/scripts/laptop_device_build.sh" build "$@"
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "${ROOT_DIR}/scripts/host_tool_runner.sh" c3 "$@"
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "${ROOT_DIR}/scripts/host_tool_runner.sh" c4 "$@"
+329 -84
View File
@@ -1,6 +1,8 @@
using Cxx = import "./include/c++.capnp"; using Cxx = import "./include/c++.capnp";
$Cxx.namespace("cereal"); $Cxx.namespace("cereal");
using Car = import "car.capnp";
@0xb526ba661d550a59; @0xb526ba661d550a59;
# custom.capnp: a home for empty structs reserved for custom forks # custom.capnp: a home for empty structs reserved for custom forks
@@ -10,110 +12,239 @@ $Cxx.namespace("cereal");
# DO rename the structs # DO rename the structs
# DON'T change the identifier (e.g. @0x81c2f05a394cf4af) # DON'T change the identifier (e.g. @0x81c2f05a394cf4af)
struct ControlsStateExt @0x81c2f05a394cf4af { struct StarPilotCarControl @0x81c2f05a394cf4af {
alkaActive @0 :Bool; hudControl @0 :HUDControl;
}
struct CarStateExt @0xaedffd8f31e7b55d { struct HUDControl {
# dp - ALKA: lkasOn state from carstate (mirrors panda's lkas_on) audibleAlert @0 :AudibleAlert;
lkasOn @0 :Bool;
}
struct ModelExt @0xf35cc4560bbf6ec2 { enum AudibleAlert {
leftEdgeDetected @0 :Bool; none @0;
rightEdgeDetected @1 :Bool;
}
struct LiveGPS @0xda96579883444c35 { engage @1;
# Position disengage @2;
latitude @0 :Float64; # degrees refuse @3;
longitude @1 :Float64; # degrees
altitude @2 :Float64; # meters (WGS84)
# Motion warningSoft @4;
speed @3 :Float32; # m/s (horizontal speed) warningImmediate @5;
bearingDeg @4 :Float32; # degrees (heading)
# Accuracy prompt @6;
horizontalAccuracy @5 :Float32; # meters promptRepeat @7;
verticalAccuracy @6 :Float32; # meters promptDistracted @8;
# Status # Random Events
gpsOK @7 :Bool; # livePose valid + GPS fresh angry @9;
status @8 :Status; continued @10;
dejaVu @11;
enum Status { doc @12;
uninitialized @0; # no GPS data yet fart @13;
uncalibrated @1; # has GPS but fusion not ready (raw passthrough) firefox @14;
valid @2; # fusion active with calibrated bearing goat @15;
hal9000 @16;
mail @17;
nessie @18;
noice @19;
startup @20;
thisIsFine @21;
uwu @22;
}
} }
# Metadata
unixTimestampMillis @9 :Int64;
lastGpsTimestamp @10 :UInt64; # logMonoTime of last GPS
# livePose health (for debugging fusion issues)
livePoseOk @11 :Bool; # livePose valid and providing orientation/velocity
} }
struct MaaControl @0x80ae746ee2596b11 { struct StarPilotCarParams @0xaedffd8f31e7b55d {
# Map-Aware Assist control signals alternativeExperience @0 :Int16;
canUsePedal @1 :Bool;
canUseSDSU @2 :Bool;
flags @3 :UInt32;
isHDA2 @4 :Bool;
openpilotLongitudinalControlDisabled @5 :Bool;
safetyConfigs @6 :List(SafetyConfig);
# Curvature data (for lateral control) struct SafetyConfig {
curvature @0 :Float32; # current nav curvature (1/m) safetyParam @0 :UInt16;
curvatureValid @1 :Bool; # curvature data is valid }
}
# Turn speed data (for longitudinal control) struct StarPilotCarState @0xf35cc4560bbf6ec2 {
turnSpeedLimit @2 :Float32; # target speed for turn (m/s) accelPressed @0 :Bool;
turnDistance @3 :Float32; # distance to turn (m) alwaysOnLateralEnabled @1 :Bool;
turnDirection @4 :TurnDirection; brakeLights @2 :Bool;
turnValid @5 :Bool; # turn data is valid dashboardSpeedLimit @3 :Float32;
maneuverType @6 :ManeuverType; # type of maneuver (turn vs lane change) decelPressed @4 :Bool;
turnAngle @7 :Float32; # expected turn angle in degrees (positive=left, negative=right) distancePressed @5 :Bool;
turnCurvature @8 :Float32; # curvature at turn point (1/m), used for speed limit calc distanceLongPressed @6 :Bool;
distanceVeryLongPressed @7 :Bool;
ecoGear @8 :Bool;
forceCoast @9 :Bool;
isParked @10 :Bool;
pauseLateral @11 :Bool;
pauseLongitudinal @12 :Bool;
sportGear @13 :Bool;
trafficModeEnabled @14 :Bool;
gasStack @15 :Bool; # Compatibility with older StarPilot payloads
}
# Turn execution (heading-based tracking) struct StarPilotDeviceState @0xda96579883444c35 {
desireActive @9 :Bool; # true when turn desire should be sent to model freeSpace @0 :Int16;
turnState @10 :UInt8; # TurnState enum: 0=none, 1=approaching, 2=executing, 3=complete usedSpace @1 :Int16;
turnProgress @11 :Float32; # accumulated heading change during turn (degrees) }
# Driver acknowledgment (blinker = commit to turn) struct StarPilotModelDataV2 @0x80ae746ee2596b11 {
driverAcknowledged @12 :Bool; # driver turned on blinker matching turn direction turnDirection @0 :TurnDirection;
speedLimitActive @13 :Bool; # turn speed limit should be enforced (blinker on)
blockLaneChange @14 :Bool; # within commit distance, block lane change desire
enum TurnDirection { enum TurnDirection {
none @0; none @0;
left @1; turnLeft @1;
right @2; turnRight @2;
}
}
struct StarPilotOnroadEvent @0xa5cd762cd951a455 {
name @0 :EventName;
enable @1 :Bool;
noEntry @2 :Bool;
warning @3 :Bool;
userDisable @4 :Bool;
softDisable @5 :Bool;
immediateDisable @6 :Bool;
preEnable @7 :Bool;
permanent @8 :Bool;
overrideLateral @9 :Bool;
overrideLongitudinal @10 :Bool;
enum EventName {
blockUser @0;
customStartupAlert @1;
forcingStop @2;
goatSteerSaturated @3;
greenLight @4;
holidayActive @5;
laneChangeBlockedLoud @6;
leadDeparting @7;
noLaneAvailable @8;
nnffLoaded @9;
openpilotCrashed @10;
pedalInterceptorNoBrake @11;
speedLimitChanged @12;
trafficModeActive @13;
trafficModeInactive @14;
turningLeft @15;
turningRight @16;
# Random Events
accel30 @17;
accel35 @18;
accel40 @19;
dejaVuCurve @20;
firefoxSteerSaturated @21;
hal9000 @22;
openpilotCrashedRandomEvent @23;
thisIsFineSteerSaturated @24;
toBeContinued @25;
vCruise69 @26;
yourFrogTriedToKillMe @27;
youveGotMail @28;
switchbackModeActive @29;
switchbackModeInactive @30;
}
}
struct StarPilotPlan @0xf98d843bfd7004a3 {
accelerationJerk @0 :Float32;
cscControllingSpeed @1 :Bool;
cscSpeed @2 :Float32;
cscTraining @3 :Bool;
dangerFactor @4 :Float32;
dangerJerk @5 :Float32;
desiredFollowDistance @6 :Int64;
experimentalMode @7 :Bool;
forcingStop @8 :Bool;
forcingStopLength @9 :Float32;
starpilotEvents @10 :List(StarPilotOnroadEvent);
starpilotToggles @11 :Text;
increasedStoppedDistance @12 :Float32;
lateralCheck @13 :Bool;
laneWidthLeft @14 :Float32;
laneWidthRight @15 :Float32;
maxAcceleration @16 :Float32;
minAcceleration @17 :Float32;
redLight @18 :Bool;
roadCurvature @19 :Float32;
slcMapSpeedLimit @20 :Float32;
slcMapboxSpeedLimit @21 :Float32;
slcNextSpeedLimit @22 :Float32;
slcOverriddenSpeed @23 :Float32;
slcSpeedLimit @24 :Float32;
slcSpeedLimitOffset @25 :Float32;
slcSpeedLimitSource @26 :Text;
speedJerk @27 :Float32;
speedLimitChanged @28 :Bool;
tFollow @29 :Float32;
themeUpdated @30 :Bool;
unconfirmedSlcSpeedLimit @31 :Float32;
vCruise @32 :Float32;
weatherDaytime @33 :Bool;
weatherId @34 :Int16;
disableThrottle @35 :Bool;
trackingLead @36 :Bool;
}
struct StarPilotRadarState @0xb86e6369214c01c8 {
leadLeft @0 :LeadData;
leadRight @1 :LeadData;
struct LeadData {
dRel @0 :Float32;
yRel @1 :Float32;
vRel @2 :Float32;
aRel @3 :Float32;
vLead @4 :Float32;
dPath @6 :Float32;
vLat @7 :Float32;
vLeadK @8 :Float32;
aLeadK @9 :Float32;
fcw @10 :Bool;
status @11 :Bool;
aLeadTau @12 :Float32;
modelProb @13 :Float32;
radar @14 :Bool;
radarTrackId @15 :Int32 = -1;
aLeadDEPRECATED @5 :Float32;
}
}
struct StarPilotSelfdriveState @0xf416ec09499d9d19 {
alertText1 @0 :Text;
alertText2 @1 :Text;
alertStatus @2 :AlertStatus;
alertSize @3 :AlertSize;
alertType @4 :Text;
alertSound @5 :Car.CarControl.HUDControl.AudibleAlert;
enum AlertStatus {
normal @0;
userPrompt @1;
critical @2;
starpilot @3;
} }
enum ManeuverType { enum AlertSize {
none @0; none @0;
turn @1; # intersection turn - use turnLeft/Right desire small @1;
laneChange @2; # highway exit/fork - use laneChangeLeft/Right desire mid @2;
full @3;
} }
} }
struct DashyState @0xa5cd762cd951a455 {
# Pre-serialized JSON bytes for dashy UI
# Aggregates all topics needed by dashy into single message
json @0 :Data;
}
struct NavInstructionExt @0xf98d843bfd7004a3 {
# Extension fields for NavInstruction (not in upstream)
turnAngle @0 :Float32; # degrees, positive=left, negative=right
turnCurvature @1 :Float32; # 1/m, positive=left, negative=right
}
struct CustomReserved7 @0xb86e6369214c01c8 {
}
struct CustomReserved8 @0xf416ec09499d9d19 {
}
struct CustomReserved9 @0xa1680744031fdb2d { struct CustomReserved9 @0xa1680744031fdb2d {
slotId @0 :Text;
slotName @1 :Text;
variant @2 :Text;
variantLabel @3 :Text;
reason @4 :Text;
wallTimeNanos @5 :UInt64;
} }
struct CustomReserved10 @0xcb9fd56c7057593a { struct CustomReserved10 @0xcb9fd56c7057593a {
@@ -137,11 +268,125 @@ struct CustomReserved15 @0xbd443b539493bc68 {
struct CustomReserved16 @0xfc6241ed8877b611 { struct CustomReserved16 @0xfc6241ed8877b611 {
} }
struct CustomReserved17 @0xa30662f84033036c { struct MapdDownloadLocationDetails @0xff889853e7b0987f {
location @0 :Text;
totalFiles @1 :UInt32;
downloadedFiles @2 :UInt32;
} }
struct CustomReserved18 @0xc86a3d38d13eb3ef { struct MapdDownloadProgress @0xfaa35dcac85073a2 {
active @0 :Bool;
cancelled @1 :Bool;
totalFiles @2 :UInt32;
downloadedFiles @3 :UInt32;
locations @4 :List(Text);
locationDetails @5 :List(MapdDownloadLocationDetails);
} }
struct CustomReserved19 @0xa4f1eb3323f5f582 { struct MapdPathPoint @0xd6f78acca1bc3939 {
latitude @0 :Float64;
longitude @1 :Float64;
curvature @2 :Float32;
targetVelocity @3 :Float32;
}
struct MapdExtendedOut @0xa30662f84033036c {
downloadProgress @0 :MapdDownloadProgress;
settings @1 :Text;
path @2 :List(MapdPathPoint);
}
enum MapdInputType {
download @0;
setTargetLateralAccel @1;
setSpeedLimitOffset @2;
setSpeedLimitControl @3;
setMapCurveSpeedControl @4;
setVisionCurveSpeedControl @5;
setLogLevel @6;
setVisionCurveTargetLatA @7;
setVisionCurveMinTargetV @8;
reloadSettings @9;
saveSettings @10;
setEnableSpeed @11;
setVisionCurveUseEnableSpeed @12;
setMapCurveUseEnableSpeed @13;
setSpeedLimitUseEnableSpeed @14;
setHoldLastSeenSpeedLimit @15;
setTargetSpeedJerk @16;
setTargetSpeedAccel @17;
setTargetSpeedTimeOffset @18;
setDefaultLaneWidth @19;
setMapCurveTargetLatA @20;
loadDefaultSettings @21;
loadRecommendedSettings @22;
setSlowDownForNextSpeedLimit @23;
setSpeedUpForNextSpeedLimit @24;
setHoldSpeedLimitWhileChangingSetSpeed @25;
loadPersistentSettings @26;
cancelDownload @27;
setLogJson @28;
setLogSource @29;
setExternalSpeedLimitControl @30;
setExternalSpeedLimit @31;
setSpeedLimitPriority @32;
setSpeedLimitChangeRequiresAccept @33;
acceptSpeedLimit @34;
setPressGasToAcceptSpeedLimit @35;
setAdjustSetSpeedToAcceptSpeedLimit @36;
setAcceptSpeedLimitTimeout @37;
setPressGasToOverrideSpeedLimit @38;
}
enum WaySelectionType {
current @0;
predicted @1;
possible @2;
extended @3;
fail @4;
}
enum SpeedLimitOffsetType {
static @0;
percent @1;
}
struct MapdIn @0xc86a3d38d13eb3ef {
type @0 :MapdInputType;
float @1 :Float32;
str @2 :Text;
bool @3 :Bool;
}
enum RoadContext {
freeway @0;
city @1;
unknown @2;
}
struct MapdOut @0xa4f1eb3323f5f582 {
wayName @0 :Text;
wayRef @1 :Text;
roadName @2 :Text;
speedLimit @3 :Float32;
nextSpeedLimit @4 :Float32;
nextSpeedLimitDistance @5 :Float32;
hazard @6 :Text;
nextHazard @7 :Text;
nextHazardDistance @8 :Float32;
advisorySpeed @9 :Float32;
nextAdvisorySpeed @10 :Float32;
nextAdvisorySpeedDistance @11 :Float32;
oneWay @12 :Bool;
lanes @13 :UInt8;
tileLoaded @14 :Bool;
speedLimitSuggestedSpeed @15 :Float32;
suggestedSpeed @16 :Float32;
estimatedRoadWidth @17 :Float32;
roadContext @18 :RoadContext;
distanceFromWayCenter @19 :Float32;
visionCurveSpeed @20 :Float32;
mapCurveSpeed @21 :Float32;
waySelectionType @22 :WaySelectionType;
speedLimitAccepted @23 :Bool;
} }
Binary file not shown.
Binary file not shown.
+26 -18
View File
@@ -130,6 +130,7 @@ struct OnroadEvent @0xc4fa6047f024e718 {
userBookmark @95; userBookmark @95;
excessiveActuation @96; excessiveActuation @96;
audioFeedback @97; audioFeedback @97;
lateralManeuver @98;
soundsUnavailableDEPRECATED @47; soundsUnavailableDEPRECATED @47;
} }
@@ -1087,7 +1088,7 @@ struct ModelDataV2 {
confidence @23: ConfidenceClass; confidence @23: ConfidenceClass;
# Model perceived motion # Model perceived motion
temporalPoseDEPRECATED @21 :Pose; temporalPose @21 :Pose;
# e2e lateral planner # e2e lateral planner
action @26: Action; action @26: Action;
@@ -1238,6 +1239,10 @@ struct DriverAssistance {
# FCW, AEB, etc. will go here # FCW, AEB, etc. will go here
} }
struct LateralManeuverPlan {
desiredCurvature @0 :Float32; # 1/m
}
struct LongitudinalPlan @0xe00b5b3eba12876c { struct LongitudinalPlan @0xe00b5b3eba12876c {
modelMonoTime @9 :UInt64; modelMonoTime @9 :UInt64;
hasLead @7 :Bool; hasLead @7 :Bool;
@@ -2161,12 +2166,14 @@ struct DriverStateV2 {
facePosition @2 :List(Float32); facePosition @2 :List(Float32);
facePositionStd @3 :List(Float32); facePositionStd @3 :List(Float32);
faceProb @4 :Float32; faceProb @4 :Float32;
leftEyeProb @5 :Float32; eyesVisibleProb @14 :Float32;
rightEyeProb @6 :Float32; eyesClosedProb @15 :Float32;
leftBlinkProb @7 :Float32;
rightBlinkProb @8 :Float32;
sunglassesProb @9 :Float32;
phoneProb @13 :Float32; phoneProb @13 :Float32;
leftEyeProbDEPRECATED @5 :Float32;
rightEyeProbDEPRECATED @6 :Float32;
leftBlinkProbDEPRECATED @7 :Float32;
rightBlinkProbDEPRECATED @8 :Float32;
sunglassesProbDEPRECATED @9 :Float32;
notReadyProbDEPRECATED @12 :List(Float32); notReadyProbDEPRECATED @12 :List(Float32);
occludedProbDEPRECATED @10 :Float32; occludedProbDEPRECATED @10 :Float32;
readyProbDEPRECATED @11 :List(Float32); readyProbDEPRECATED @11 :List(Float32);
@@ -2601,6 +2608,7 @@ struct Event {
userBookmark @93 :UserBookmark; userBookmark @93 :UserBookmark;
bookmarkButton @148 :UserBookmark; bookmarkButton @148 :UserBookmark;
audioFeedback @149 :AudioFeedback; audioFeedback @149 :AudioFeedback;
lateralManeuverPlan @150 :LateralManeuverPlan;
# *********** debug *********** # *********** debug ***********
testJoystick @52 :Joystick; testJoystick @52 :Joystick;
@@ -2625,15 +2633,15 @@ struct Event {
# DO change the name of the field and struct # DO change the name of the field and struct
# DON'T change the ID (e.g. @107) # DON'T change the ID (e.g. @107)
# DON'T change which struct it points to # DON'T change which struct it points to
controlsStateExt @107 :Custom.ControlsStateExt; starpilotCarControl @107 :Custom.StarPilotCarControl;
carStateExt @108 :Custom.CarStateExt; starpilotCarParams @108 :Custom.StarPilotCarParams;
modelExt @109 :Custom.ModelExt; starpilotCarState @109 :Custom.StarPilotCarState;
liveGPS @110 :Custom.LiveGPS; starpilotDeviceState @110 :Custom.StarPilotDeviceState;
maaControl @111 :Custom.MaaControl; starpilotModelV2 @111 :Custom.StarPilotModelDataV2;
dashyState @112 :Custom.DashyState; starpilotOnroadEvents @112 :List(Custom.StarPilotOnroadEvent);
navInstructionExt @113 :Custom.NavInstructionExt; starpilotPlan @113 :Custom.StarPilotPlan;
customReserved7 @114 :Custom.CustomReserved7; starpilotRadarState @114 :Custom.StarPilotRadarState;
customReserved8 @115 :Custom.CustomReserved8; starpilotSelfdriveState @115 :Custom.StarPilotSelfdriveState;
customReserved9 @116 :Custom.CustomReserved9; customReserved9 @116 :Custom.CustomReserved9;
customReserved10 @136 :Custom.CustomReserved10; customReserved10 @136 :Custom.CustomReserved10;
customReserved11 @137 :Custom.CustomReserved11; customReserved11 @137 :Custom.CustomReserved11;
@@ -2642,9 +2650,9 @@ struct Event {
customReserved14 @140 :Custom.CustomReserved14; customReserved14 @140 :Custom.CustomReserved14;
customReserved15 @141 :Custom.CustomReserved15; customReserved15 @141 :Custom.CustomReserved15;
customReserved16 @142 :Custom.CustomReserved16; customReserved16 @142 :Custom.CustomReserved16;
customReserved17 @143 :Custom.CustomReserved17; mapdExtendedOut @143 :Custom.MapdExtendedOut;
customReserved18 @144 :Custom.CustomReserved18; mapdIn @144 :Custom.MapdIn;
customReserved19 @145 :Custom.CustomReserved19; mapdOut @145 :Custom.MapdOut;
# *********** legacy + deprecated *********** # *********** legacy + deprecated ***********
model @9 :Legacy.ModelData; # TODO: rename modelV2 and mark this as deprecated model @9 :Legacy.ModelData; # TODO: rename modelV2 and mark this as deprecated
+23
View File
@@ -197,6 +197,10 @@ class SubMaster:
self.data[s] = getattr(data.as_reader(), s) self.data[s] = getattr(data.as_reader(), s)
self.freq_tracker[s] = FrequencyTracker(SERVICE_LIST[s].frequency, self.update_freq, s == poll) self.freq_tracker[s] = FrequencyTracker(SERVICE_LIST[s].frequency, self.update_freq, s == poll)
# StarPilot variables
self.addr = addr
self.poll = poll
def __getitem__(self, s: str) -> capnp.lib.capnp._DynamicStructReader: def __getitem__(self, s: str) -> capnp.lib.capnp._DynamicStructReader:
return self.data[s] return self.data[s]
@@ -248,6 +252,18 @@ class SubMaster:
def all_checks(self, service_list: Optional[List[str]] = None) -> bool: def all_checks(self, service_list: Optional[List[str]] = None) -> bool:
return self.all_alive(service_list) and self.all_freq_ok(service_list) and self.all_valid(service_list) return self.all_alive(service_list) and self.all_freq_ok(service_list) and self.all_valid(service_list)
# StarPilot variables
def extend(self, new_services: List[str]):
return SubMaster(
self.services + new_services,
poll=self.poll,
ignore_alive=self.ignore_alive,
ignore_avg_freq=self.ignore_average_freq,
ignore_valid=self.ignore_valid,
addr=self.addr,
frequency=None if self.poll is not None else self.update_freq,
)
class PubMaster: class PubMaster:
def __init__(self, services: List[str]): def __init__(self, services: List[str]):
@@ -269,3 +285,10 @@ class PubMaster:
def all_readers_updated(self, s: str) -> bool: def all_readers_updated(self, s: str) -> bool:
return self.sock[s].all_readers_updated() # type: ignore return self.sock[s].all_readers_updated() # type: ignore
# StarPilot variables
def extend(self, new_services: List[str]):
for service in new_services:
if service not in self.sock:
self.sock[service] = pub_sock(service)
return self
BIN
View File
Binary file not shown.
+102
View File
@@ -0,0 +1,102 @@
/* THIS IS AN AUTOGENERATED FILE, PLEASE EDIT services.py */
#ifndef __SERVICES_H
#define __SERVICES_H
#include <map>
#include <string>
struct service { std::string name; bool should_log; float frequency; int decimation; size_t queue_size; };
static std::map<std::string, service> services = {
{ "gyroscope", {"gyroscope", true, 104.000000, 104, 256000}},
{ "accelerometer", {"accelerometer", true, 104.000000, 104, 256000}},
{ "magnetometer", {"magnetometer", true, 25.000000, -1, 256000}},
{ "lightSensor", {"lightSensor", true, 100.000000, 100, 256000}},
{ "temperatureSensor", {"temperatureSensor", true, 2.000000, 200, 256000}},
{ "gpsNMEA", {"gpsNMEA", true, 9.000000, -1, 256000}},
{ "deviceState", {"deviceState", true, 2.000000, 1, 256000}},
{ "touch", {"touch", true, 20.000000, 1, 256000}},
{ "can", {"can", true, 100.000000, 2053, 10485760}},
{ "controlsState", {"controlsState", true, 100.000000, 10, 2097152}},
{ "selfdriveState", {"selfdriveState", true, 100.000000, 10, 256000}},
{ "pandaStates", {"pandaStates", true, 10.000000, 1, 256000}},
{ "peripheralState", {"peripheralState", true, 2.000000, 1, 256000}},
{ "radarState", {"radarState", true, 20.000000, 5, 256000}},
{ "roadEncodeIdx", {"roadEncodeIdx", false, 20.000000, 1, 256000}},
{ "liveTracks", {"liveTracks", true, 20.000000, -1, 256000}},
{ "sendcan", {"sendcan", true, 100.000000, 139, 2097152}},
{ "logMessage", {"logMessage", true, 0.000000, -1, 256000}},
{ "errorLogMessage", {"errorLogMessage", true, 0.000000, 1, 256000}},
{ "liveCalibration", {"liveCalibration", true, 4.000000, 4, 256000}},
{ "liveTorqueParameters", {"liveTorqueParameters", true, 4.000000, 1, 256000}},
{ "liveDelay", {"liveDelay", true, 4.000000, 1, 256000}},
{ "androidLog", {"androidLog", true, 0.000000, -1, 256000}},
{ "carState", {"carState", true, 100.000000, 10, 256000}},
{ "carControl", {"carControl", true, 100.000000, 10, 256000}},
{ "carOutput", {"carOutput", true, 100.000000, 10, 256000}},
{ "longitudinalPlan", {"longitudinalPlan", true, 20.000000, 10, 256000}},
{ "lateralManeuverPlan", {"lateralManeuverPlan", true, 20.000000, -1, 256000}},
{ "driverAssistance", {"driverAssistance", true, 20.000000, 20, 256000}},
{ "procLog", {"procLog", true, 0.500000, 15, 10485760}},
{ "gpsLocationExternal", {"gpsLocationExternal", true, 10.000000, 10, 256000}},
{ "gpsLocation", {"gpsLocation", true, 1.000000, 1, 256000}},
{ "ubloxGnss", {"ubloxGnss", true, 10.000000, -1, 256000}},
{ "qcomGnss", {"qcomGnss", true, 2.000000, -1, 256000}},
{ "gnssMeasurements", {"gnssMeasurements", true, 10.000000, 10, 256000}},
{ "clocks", {"clocks", true, 0.100000, 1, 256000}},
{ "ubloxRaw", {"ubloxRaw", true, 20.000000, -1, 256000}},
{ "livePose", {"livePose", true, 20.000000, 4, 256000}},
{ "liveParameters", {"liveParameters", true, 20.000000, 5, 256000}},
{ "cameraOdometry", {"cameraOdometry", true, 20.000000, 10, 256000}},
{ "thumbnail", {"thumbnail", true, 0.016667, 1, 256000}},
{ "onroadEvents", {"onroadEvents", true, 1.000000, 1, 256000}},
{ "carParams", {"carParams", true, 0.020000, 1, 256000}},
{ "roadCameraState", {"roadCameraState", true, 20.000000, 20, 256000}},
{ "driverCameraState", {"driverCameraState", true, 20.000000, 20, 256000}},
{ "driverEncodeIdx", {"driverEncodeIdx", false, 20.000000, 1, 256000}},
{ "driverStateV2", {"driverStateV2", true, 20.000000, 10, 256000}},
{ "driverMonitoringState", {"driverMonitoringState", true, 20.000000, 10, 256000}},
{ "wideRoadEncodeIdx", {"wideRoadEncodeIdx", false, 20.000000, 1, 256000}},
{ "wideRoadCameraState", {"wideRoadCameraState", true, 20.000000, 20, 256000}},
{ "drivingModelData", {"drivingModelData", true, 20.000000, 10, 256000}},
{ "modelV2", {"modelV2", true, 20.000000, -1, 10485760}},
{ "managerState", {"managerState", true, 2.000000, 1, 256000}},
{ "uploaderState", {"uploaderState", true, 0.000000, 1, 256000}},
{ "navInstruction", {"navInstruction", true, 1.000000, 10, 256000}},
{ "navRoute", {"navRoute", true, 0.000000, -1, 256000}},
{ "navThumbnail", {"navThumbnail", true, 0.000000, -1, 256000}},
{ "qRoadEncodeIdx", {"qRoadEncodeIdx", false, 20.000000, -1, 256000}},
{ "userBookmark", {"userBookmark", true, 0.000000, 1, 256000}},
{ "soundPressure", {"soundPressure", true, 10.000000, 10, 256000}},
{ "rawAudioData", {"rawAudioData", false, 20.000000, -1, 256000}},
{ "bookmarkButton", {"bookmarkButton", true, 0.000000, 1, 256000}},
{ "audioFeedback", {"audioFeedback", true, 0.000000, 1, 256000}},
{ "roadEncodeData", {"roadEncodeData", false, 20.000000, -1, 10485760}},
{ "driverEncodeData", {"driverEncodeData", false, 20.000000, -1, 10485760}},
{ "wideRoadEncodeData", {"wideRoadEncodeData", false, 20.000000, -1, 10485760}},
{ "qRoadEncodeData", {"qRoadEncodeData", false, 20.000000, -1, 10485760}},
{ "uiDebug", {"uiDebug", true, 0.000000, 1, 256000}},
{ "testJoystick", {"testJoystick", true, 0.000000, -1, 256000}},
{ "alertDebug", {"alertDebug", true, 20.000000, 5, 256000}},
{ "livestreamWideRoadEncodeIdx", {"livestreamWideRoadEncodeIdx", false, 20.000000, -1, 256000}},
{ "livestreamRoadEncodeIdx", {"livestreamRoadEncodeIdx", false, 20.000000, -1, 256000}},
{ "livestreamDriverEncodeIdx", {"livestreamDriverEncodeIdx", false, 20.000000, -1, 256000}},
{ "livestreamWideRoadEncodeData", {"livestreamWideRoadEncodeData", false, 20.000000, -1, 2097152}},
{ "livestreamRoadEncodeData", {"livestreamRoadEncodeData", false, 20.000000, -1, 2097152}},
{ "livestreamDriverEncodeData", {"livestreamDriverEncodeData", false, 20.000000, -1, 2097152}},
{ "customReserved9", {"customReserved9", true, 0.000000, 1, 256000}},
{ "customReservedRawData0", {"customReservedRawData0", true, 0.000000, -1, 256000}},
{ "customReservedRawData1", {"customReservedRawData1", true, 0.000000, -1, 256000}},
{ "customReservedRawData2", {"customReservedRawData2", true, 0.000000, -1, 256000}},
{ "starpilotCarControl", {"starpilotCarControl", true, 100.000000, 10, 256000}},
{ "starpilotCarParams", {"starpilotCarParams", true, 0.020000, 1, 256000}},
{ "starpilotCarState", {"starpilotCarState", true, 100.000000, 10, 256000}},
{ "starpilotDeviceState", {"starpilotDeviceState", true, 2.000000, 1, 256000}},
{ "starpilotModelV2", {"starpilotModelV2", true, 20.000000, -1, 256000}},
{ "starpilotOnroadEvents", {"starpilotOnroadEvents", true, 1.000000, 1, 256000}},
{ "starpilotPlan", {"starpilotPlan", true, 20.000000, 10, 256000}},
{ "starpilotRadarState", {"starpilotRadarState", true, 20.000000, 5, 256000}},
{ "starpilotSelfdriveState", {"starpilotSelfdriveState", true, 100.000000, 10, 256000}},
{ "mapdExtendedOut", {"mapdExtendedOut", true, 1.000000, 1, 2097152}},
{ "mapdIn", {"mapdIn", true, 1.000000, 1, 2097152}},
{ "mapdOut", {"mapdOut", true, 20.000000, 20, 2097152}},
};
#endif
+17 -10
View File
@@ -49,6 +49,7 @@ _services: dict[str, tuple] = {
"carControl": (True, 100., 10), "carControl": (True, 100., 10),
"carOutput": (True, 100., 10), "carOutput": (True, 100., 10),
"longitudinalPlan": (True, 20., 10), "longitudinalPlan": (True, 20., 10),
"lateralManeuverPlan": (True, 20.),
"driverAssistance": (True, 20., 20), "driverAssistance": (True, 20., 20),
"procLog": (True, 0.5, 15, QueueSize.BIG), "procLog": (True, 0.5, 15, QueueSize.BIG),
"gpsLocationExternal": (True, 10., 10), "gpsLocationExternal": (True, 10., 10),
@@ -75,7 +76,7 @@ _services: dict[str, tuple] = {
"modelV2": (True, 20., None, QueueSize.BIG), "modelV2": (True, 20., None, QueueSize.BIG),
"managerState": (True, 2., 1), "managerState": (True, 2., 1),
"uploaderState": (True, 0., 1), "uploaderState": (True, 0., 1),
# "navInstruction": (True, 1., 10), # dp - make it 0 hz "navInstruction": (True, 1., 10),
"navRoute": (True, 0.), "navRoute": (True, 0.),
"navThumbnail": (True, 0.), "navThumbnail": (True, 0.),
"qRoadEncodeIdx": (False, 20.), "qRoadEncodeIdx": (False, 20.),
@@ -99,18 +100,24 @@ _services: dict[str, tuple] = {
"livestreamWideRoadEncodeData": (False, 20., None, QueueSize.MEDIUM), "livestreamWideRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
"livestreamRoadEncodeData": (False, 20., None, QueueSize.MEDIUM), "livestreamRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
"livestreamDriverEncodeData": (False, 20., None, QueueSize.MEDIUM), "livestreamDriverEncodeData": (False, 20., None, QueueSize.MEDIUM),
"customReserved9": (True, 0., 1),
"customReservedRawData0": (True, 0.), "customReservedRawData0": (True, 0.),
"customReservedRawData1": (True, 0.), "customReservedRawData1": (True, 0.),
"customReservedRawData2": (True, 0.), "customReservedRawData2": (True, 0.),
"controlsStateExt": (True, 100.),
"carStateExt": (True, 100.), # StarPilot variables
"modelExt": (True, 20.), "starpilotCarControl": (True, 100., 10),
# dashy "starpilotCarParams": (True, 0.02, 1),
"navInstruction": (True, 0.), "starpilotCarState": (True, 100., 10),
"navInstructionExt": (True, 0.), "starpilotDeviceState": (True, 2., 1),
"liveGPS": (True, 0.), # GPS fusion from gpsd (optional) "starpilotModelV2": (True, 20.),
"maaControl": (True, 0.), # Map-Aware Assist control signals (optional) "starpilotOnroadEvents": (True, 1., 1),
"dashyState": (True, 0.), # Aggregated dashy UI state (optional) "starpilotPlan": (True, 20., 10),
"starpilotRadarState": (True, 20., 5),
"starpilotSelfdriveState": (True, 100., 10),
"mapdExtendedOut": (True, 1., 1, QueueSize.MEDIUM),
"mapdIn": (True, 1., 1, QueueSize.MEDIUM),
"mapdOut": (True, 20., 20, QueueSize.MEDIUM),
} }
SERVICE_LIST = {name: Service(*vals) for SERVICE_LIST = {name: Service(*vals) for
idx, (name, vals) in enumerate(_services.items())} idx, (name, vals) in enumerate(_services.items())}
+1
View File
@@ -1 +1,2 @@
*.cpp *.cpp
!params_pyx.cpp
+9 -3
View File
@@ -4,12 +4,18 @@ common_libs = [
'params.cc', 'params.cc',
'swaglog.cc', 'swaglog.cc',
'util.cc', 'util.cc',
'ratekeeper.cc', 'watchdog.cc',
'clutil.cc', 'ratekeeper.cc'
] ]
_common = env.Library('common', common_libs, LIBS="json11") _common = env.Library('common', common_libs, LIBS="json11")
Export('_common')
files = [
'clutil.cc',
]
_gpucommon = env.Library('gpucommon', files)
Export('_common', '_gpucommon')
if GetOption('extras'): if GetOption('extras'):
env.Program('tests/test_common', env.Program('tests/test_common',
+3 -1
View File
@@ -5,7 +5,9 @@ from datetime import datetime, timedelta, UTC
from openpilot.system.hardware.hw import Paths from openpilot.system.hardware.hw import Paths
from openpilot.system.version import get_version from openpilot.system.version import get_version
API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com') from openpilot.starpilot.common.starpilot_utilities import use_konik_server
API_HOST = os.getenv('API_HOST', f"https://api.{'konik.ai' if use_konik_server() else 'commadotai.com'}")
# name: jwt signature algorithm # name: jwt signature algorithm
KEYS = {"id_rsa": "RS256", KEYS = {"id_rsa": "RS256",
+32 -1
View File
@@ -53,6 +53,37 @@ void cl_print_build_errors(cl_program program, cl_device_id device) {
LOGE("build failed; status=%d, log: %s", status, log.c_str()); LOGE("build failed; status=%d, log: %s", status, log.c_str());
} }
std::string resolve_program_path(const char *path) {
std::string resolved_path(path);
if (util::file_exists(resolved_path)) {
return resolved_path;
}
const std::string basedir = util::getenv("BASEDIR", "");
if (!basedir.empty()) {
// Handle repository-relative paths, e.g. "selfdrive/modeld/transforms/transform.cl".
if (!resolved_path.empty() && resolved_path.front() != '/') {
const std::string candidate = basedir + "/" + resolved_path;
if (util::file_exists(candidate)) {
return candidate;
}
}
// Recover from build-path-embedded absolute paths like "/work/selfdrive/...".
for (const char *marker : {"/selfdrive/", "/starpilot/", "/common/"}) {
if (const size_t idx = resolved_path.find(marker); idx != std::string::npos) {
const std::string candidate = basedir + "/" + resolved_path.substr(idx + 1);
if (util::file_exists(candidate)) {
LOGW("OpenCL source path remapped: %s -> %s", resolved_path.c_str(), candidate.c_str());
return candidate;
}
}
}
}
return resolved_path;
}
} // namespace } // namespace
cl_device_id cl_get_device_id(cl_device_type device_type) { cl_device_id cl_get_device_id(cl_device_type device_type) {
@@ -84,7 +115,7 @@ void cl_release_context(cl_context context) {
} }
cl_program cl_program_from_file(cl_context ctx, cl_device_id device_id, const char* path, const char* args) { cl_program cl_program_from_file(cl_context ctx, cl_device_id device_id, const char* path, const char* args) {
return cl_program_from_source(ctx, device_id, util::read_file(path), args); return cl_program_from_source(ctx, device_id, util::read_file(resolve_program_path(path)), args);
} }
cl_program cl_program_from_source(cl_context ctx, cl_device_id device_id, const std::string& src, const char* args) { cl_program cl_program_from_source(cl_context ctx, cl_device_id device_id, const std::string& src, const char* args) {
+6
View File
@@ -19,5 +19,11 @@ class CV:
# Mass # Mass
LB_TO_KG = 0.453592 LB_TO_KG = 0.453592
# StarPilot variables
METER_TO_FOOT = 3.28084
FOOT_TO_METER = 1. / METER_TO_FOOT
CM_TO_INCH = 1. / 2.54
INCH_TO_CM = 1. / CM_TO_INCH
ACCELERATION_DUE_TO_GRAVITY = 9.81 # m/s^2 ACCELERATION_DUE_TO_GRAVITY = 9.81 # m/s^2
+41
View File
@@ -0,0 +1,41 @@
import math
import os
from pathlib import Path
CHUNK_SIZE = 45 * 1024 * 1024 # 45MB, under GitHub's 50MB limit
def get_chunk_name(name, idx, num_chunks):
return f"{name}.chunk{idx + 1:02d}of{num_chunks:02d}"
def get_manifest_path(name):
return f"{name}.chunkmanifest"
def get_chunk_paths(path, file_size):
num_chunks = math.ceil(file_size / CHUNK_SIZE)
return [get_manifest_path(path)] + [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)]
def chunk_file(path, targets):
manifest_path, *chunk_paths = targets
with open(path, 'rb') as f:
data = f.read()
actual_num_chunks = max(1, math.ceil(len(data) / CHUNK_SIZE))
assert len(chunk_paths) >= actual_num_chunks, f"Allowed {len(chunk_paths)} chunks but needs at least {actual_num_chunks}, for path {path}"
for i, chunk_path in enumerate(chunk_paths):
with open(chunk_path, 'wb') as f:
f.write(data[i * CHUNK_SIZE:(i + 1) * CHUNK_SIZE])
Path(manifest_path).write_text(str(len(chunk_paths)))
os.remove(path)
def read_file_chunked(path):
manifest_path = get_manifest_path(path)
if os.path.isfile(manifest_path):
num_chunks = int(Path(manifest_path).read_text().strip())
return b''.join(Path(get_chunk_name(path, i, num_chunks)).read_bytes() for i in range(num_chunks))
if os.path.isfile(path):
return Path(path).read_bytes()
raise FileNotFoundError(path)
Binary file not shown.
+85 -9
View File
@@ -91,16 +91,28 @@ private:
} // namespace } // namespace
Params::Params(const std::string &path) { Params::Params(const std::string &path, bool memory) {
params_prefix = "/" + util::getenv("OPENPILOT_PREFIX", "d"); params_prefix = "/" + util::getenv("OPENPILOT_PREFIX", "d");
params_path = ensure_params_path(params_prefix, path);
// StarPilot variables
std::string params_folder;
if (memory) {
params_folder = Path::shm_path() + "/params";
} else {
cache_path = "/cache/params" + params_prefix + "/";
params_folder = path;
}
params_path = ensure_params_path(params_prefix, params_folder);
} }
Params::~Params() { Params::~Params() {
if (future.valid()) { if (future.valid()) {
future.wait(); future.wait();
} }
std::scoped_lock lk(pending_writes_lock);
assert(queue.empty()); assert(queue.empty());
assert(pending_writes.empty());
assert(!writer_running);
} }
std::vector<std::string> Params::allKeys() const { std::vector<std::string> Params::allKeys() const {
@@ -169,6 +181,12 @@ int Params::put(const char* key, const char* value, size_t value_size) {
int Params::remove(const std::string &key) { int Params::remove(const std::string &key) {
FileLock file_lock(params_path + "/.lock"); FileLock file_lock(params_path + "/.lock");
int result = unlink(getParamPath(key).c_str()); int result = unlink(getParamPath(key).c_str());
// StarPilot variables
if (!cache_path.empty()) {
unlink((cache_path + key).c_str());
}
if (result != 0) { if (result != 0) {
return result; return result;
} }
@@ -215,6 +233,11 @@ void Params::clearAll(ParamKeyFlag key_flag) {
auto it = keys.find(de->d_name); auto it = keys.find(de->d_name);
if (it == keys.end() || (it->second.flags & key_flag)) { if (it == keys.end() || (it->second.flags & key_flag)) {
unlink(getParamPath(de->d_name).c_str()); unlink(getParamPath(de->d_name).c_str());
// StarPilot variables
if (!cache_path.empty()) {
unlink((cache_path + de->d_name).c_str());
}
} }
} }
} }
@@ -225,18 +248,71 @@ void Params::clearAll(ParamKeyFlag key_flag) {
} }
void Params::putNonBlocking(const std::string &key, const std::string &val) { void Params::putNonBlocking(const std::string &key, const std::string &val) {
queue.push(std::make_pair(key, val)); bool should_enqueue = false;
// start thread on demand bool should_start_thread = false;
if (!future.valid() || future.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready) {
{
std::scoped_lock lk(pending_writes_lock);
auto it = pending_writes.find(key);
if (it == pending_writes.end()) {
pending_writes.emplace(key, val);
should_enqueue = true;
} else {
it->second = val;
}
if (!writer_running) {
writer_running = true;
should_start_thread = true;
}
}
if (should_enqueue) {
queue.push(key);
}
if (should_start_thread) {
future = std::async(std::launch::async, &Params::asyncWriteThread, this); future = std::async(std::launch::async, &Params::asyncWriteThread, this);
} }
} }
void Params::asyncWriteThread() { void Params::asyncWriteThread() {
// TODO: write the latest one if a key has multiple values in the queue. std::string key;
std::pair<std::string, std::string> p; while (true) {
while (queue.try_pop(p, 0)) { if (!queue.try_pop(key, 0)) {
std::scoped_lock lk(pending_writes_lock);
if (queue.empty() && pending_writes.empty()) {
writer_running = false;
return;
}
continue;
}
std::string val;
{
std::scoped_lock lk(pending_writes_lock);
auto it = pending_writes.find(key);
if (it == pending_writes.end()) {
continue;
}
val = std::move(it->second);
pending_writes.erase(it);
}
// Params::put is Thread-Safe // Params::put is Thread-Safe
put(p.first, p.second); put(key, val);
} }
} }
// StarPilot variables
int Params::getTuningLevel(const std::string &key) {
return keys[key].tuning_level;
}
std::optional<std::string> Params::getStockValue(const std::string &key) {
ParamKeyAttributes &attributes = keys[key];
if (attributes.stock_value) {
return attributes.stock_value;
}
return attributes.default_value;
}
+44 -2
View File
@@ -2,9 +2,11 @@
#include <future> #include <future>
#include <map> #include <map>
#include <mutex>
#include <optional> #include <optional>
#include <string> #include <string>
#include <tuple> #include <tuple>
#include <unordered_map>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -35,11 +37,16 @@ struct ParamKeyAttributes {
uint32_t flags; uint32_t flags;
ParamKeyType type; ParamKeyType type;
std::optional<std::string> default_value = std::nullopt; std::optional<std::string> default_value = std::nullopt;
// StarPilot variables
std::optional<std::string> stock_value = std::nullopt;
int tuning_level = 0;
}; };
class Params { class Params {
public: public:
explicit Params(const std::string &path = {}); explicit Params(const std::string &path = {}, bool memory = false);
~Params(); ~Params();
// Not copyable. // Not copyable.
Params(const Params&) = delete; Params(const Params&) = delete;
@@ -78,6 +85,35 @@ public:
putNonBlocking(key, val ? "1" : "0"); putNonBlocking(key, val ? "1" : "0");
} }
// StarPilot variables
int getInt(const std::string &key, bool block = false) {
std::string value = get(key, block);
return value.empty() ? 0 : std::stoi(value);
}
float getFloat(const std::string &key, bool block = false) {
std::string value = get(key, block);
return value.empty() ? 0.0f : std::stof(value);
}
int putInt(const std::string &key, int val) {
std::string str = std::to_string(val);
return put(key.c_str(), str.c_str(), str.size());
}
int putFloat(const std::string &key, float val) {
std::string str = std::to_string(val);
return put(key.c_str(), str.c_str(), str.size());
}
void putIntNonBlocking(const std::string &key, int val) {
putNonBlocking(key, std::to_string(val));
}
void putFloatNonBlocking(const std::string &key, float val) {
putNonBlocking(key, std::to_string(val));
}
int getTuningLevel(const std::string &key);
std::optional<std::string> getStockValue(const std::string &key);
private: private:
void asyncWriteThread(); void asyncWriteThread();
@@ -86,5 +122,11 @@ private:
// for nonblocking write // for nonblocking write
std::future<void> future; std::future<void> future;
SafeQueue<std::pair<std::string, std::string>> queue; SafeQueue<std::string> queue;
std::mutex pending_writes_lock;
std::unordered_map<std::string, std::string> pending_writes;
bool writer_running = false;
// StarPilot variables
std::string cache_path;
}; };
+64 -2
View File
@@ -1,9 +1,71 @@
from openpilot.common.params_pyx import Params, ParamKeyFlag, ParamKeyType, UnknownKeyName from openpilot.common.params_pyx import Params as _Params, ParamKeyFlag, ParamKeyType, UnknownKeyName
assert Params assert _Params
assert ParamKeyFlag assert ParamKeyFlag
assert ParamKeyType assert ParamKeyType
assert UnknownKeyName assert UnknownKeyName
class Params(_Params):
def get(self, key, block=False, return_default=False, encoding=None, default=None):
try:
value = super().get(key, block=block, return_default=return_default)
except UnknownKeyName:
return default
if value is None:
return default
if encoding is not None and isinstance(value, bytes):
try:
return value.decode(encoding)
except Exception:
return value.decode("utf-8", errors="replace")
return value
def get_bool(self, key, block=False, default=False):
try:
return super().get_bool(key, block=block)
except UnknownKeyName:
return bool(default)
def get_int(self, key, block=False, return_default=False, default=0):
val = self.get(key, block=block, return_default=return_default, encoding="utf-8")
if val is None or val == "":
return default
try:
return int(float(val))
except ValueError:
return default
def get_float(self, key, block=False, return_default=False, default=0.0):
val = self.get(key, block=block, return_default=return_default, encoding="utf-8")
if val is None or val == "":
return default
try:
return float(val)
except ValueError:
return default
def put_int(self, key, val):
t = self.get_type(key)
if t == ParamKeyType.FLOAT:
self.put(key, float(val))
elif t == ParamKeyType.INT:
self.put(key, int(val))
elif t == ParamKeyType.BOOL:
self.put(key, bool(val))
else:
self.put(key, str(int(val)))
def put_float(self, key, val):
t = self.get_type(key)
if t == ParamKeyType.FLOAT:
self.put(key, float(val))
elif t == ParamKeyType.INT:
self.put(key, int(val))
elif t == ParamKeyType.BOOL:
self.put(key, bool(val))
else:
self.put(key, str(float(val)))
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
+431 -43
View File
@@ -8,6 +8,7 @@
inline static std::unordered_map<std::string, ParamKeyAttributes> keys = { inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"AccessToken", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}}, {"AccessToken", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}},
{"AdbEnabled", {PERSISTENT, BOOL}}, {"AdbEnabled", {PERSISTENT, BOOL}},
{"AlwaysAllowUploads", {PERSISTENT, BOOL, "0"}},
{"AlwaysOnDM", {PERSISTENT, BOOL}}, {"AlwaysOnDM", {PERSISTENT, BOOL}},
{"ApiCache_Device", {PERSISTENT, STRING}}, {"ApiCache_Device", {PERSISTENT, STRING}},
{"ApiCache_FirehoseStats", {PERSISTENT, JSON}}, {"ApiCache_FirehoseStats", {PERSISTENT, JSON}},
@@ -28,7 +29,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ControlsReady", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"ControlsReady", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"CurrentBootlog", {PERSISTENT, STRING}}, {"CurrentBootlog", {PERSISTENT, STRING}},
{"CurrentRoute", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}}, {"CurrentRoute", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}},
{"DisableLogging", {PERSISTENT, BOOL, "0"}}, {"DisableLogging", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"DisablePowerDown", {PERSISTENT, BOOL}}, {"DisablePowerDown", {PERSISTENT, BOOL}},
{"DisableUpdates", {PERSISTENT, BOOL}}, {"DisableUpdates", {PERSISTENT, BOOL}},
{"DisengageOnAccelerator", {PERSISTENT, BOOL, "0"}}, {"DisengageOnAccelerator", {PERSISTENT, BOOL, "0"}},
@@ -37,9 +38,12 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"DoShutdown", {CLEAR_ON_MANAGER_START, BOOL}}, {"DoShutdown", {CLEAR_ON_MANAGER_START, BOOL}},
{"DoUninstall", {CLEAR_ON_MANAGER_START, BOOL}}, {"DoUninstall", {CLEAR_ON_MANAGER_START, BOOL}},
{"DriverTooDistracted", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}}, {"DriverTooDistracted", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}},
{"AlphaLongitudinalEnabled", {PERSISTENT | DEVELOPMENT_ONLY, BOOL}}, {"AlphaLongitudinalEnabled", {PERSISTENT, BOOL}},
{"ExperimentalLongitudinalEnabled", {PERSISTENT, BOOL}},
{"ExperimentalMode", {PERSISTENT, BOOL}}, {"ExperimentalMode", {PERSISTENT, BOOL}},
{"ExperimentalModeConfirmed", {PERSISTENT, BOOL}}, {"ExperimentalModeConfirmed", {PERSISTENT, BOOL}},
{"PersistExperimentalState", {PERSISTENT, BOOL, "0", "0", 1}},
{"PersistedCEStatus", {PERSISTENT, INT, "0", "0"}},
{"FirmwareQueryDone", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"FirmwareQueryDone", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"ForcePowerDown", {PERSISTENT, BOOL}}, {"ForcePowerDown", {PERSISTENT, BOOL}},
{"GitBranch", {PERSISTENT, STRING}}, {"GitBranch", {PERSISTENT, STRING}},
@@ -66,7 +70,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"IsTakingSnapshot", {CLEAR_ON_MANAGER_START, BOOL}}, {"IsTakingSnapshot", {CLEAR_ON_MANAGER_START, BOOL}},
{"IsTestedBranch", {CLEAR_ON_MANAGER_START, BOOL}}, {"IsTestedBranch", {CLEAR_ON_MANAGER_START, BOOL}},
{"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, {"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"LanguageSetting", {PERSISTENT, STRING, "en"}}, {"LanguageSetting", {PERSISTENT, STRING, "main_en"}},
{"LastAthenaPingTime", {CLEAR_ON_MANAGER_START, INT}}, {"LastAthenaPingTime", {CLEAR_ON_MANAGER_START, INT}},
{"LastGPSPosition", {PERSISTENT, STRING}}, {"LastGPSPosition", {PERSISTENT, STRING}},
{"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}}, {"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}},
@@ -81,6 +85,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"LiveParameters", {PERSISTENT, JSON}}, {"LiveParameters", {PERSISTENT, JSON}},
{"LiveParametersV2", {PERSISTENT, BYTES}}, {"LiveParametersV2", {PERSISTENT, BYTES}},
{"LiveTorqueParameters", {PERSISTENT | DONT_LOG, BYTES}}, {"LiveTorqueParameters", {PERSISTENT | DONT_LOG, BYTES}},
{"LateralManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"LocationFilterInitialState", {PERSISTENT, BYTES}}, {"LocationFilterInitialState", {PERSISTENT, BYTES}},
{"LongitudinalManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, {"LongitudinalManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"LongitudinalPersonality", {PERSISTENT, INT, std::to_string(static_cast<int>(cereal::LongitudinalPersonality::STANDARD))}}, {"LongitudinalPersonality", {PERSISTENT, INT, std::to_string(static_cast<int>(cereal::LongitudinalPersonality::STANDARD))}},
@@ -111,9 +116,13 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet {"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet
{"SecOCKey", {PERSISTENT | DONT_LOG, STRING}}, {"SecOCKey", {PERSISTENT | DONT_LOG, STRING}},
{"ShowDebugInfo", {PERSISTENT, BOOL}}, {"ShowDebugInfo", {PERSISTENT, BOOL}},
{"ShowAllToggles", {PERSISTENT, BOOL, "0", "0", 3}},
{"UsePrebuilt", {PERSISTENT, BOOL, "1"}},
{"RouteCount", {PERSISTENT, INT, "0"}}, {"RouteCount", {PERSISTENT, INT, "0"}},
{"SnoozeUpdate", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, {"SnoozeUpdate", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"SshEnabled", {PERSISTENT, BOOL}}, {"SshEnabled", {PERSISTENT, BOOL}},
{"TermsVersion", {PERSISTENT, STRING}},
{"TrainingVersion", {PERSISTENT, STRING}},
{"UbloxAvailable", {PERSISTENT, BOOL}}, {"UbloxAvailable", {PERSISTENT, BOOL}},
{"UpdateAvailable", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"UpdateAvailable", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"UpdateFailedCount", {CLEAR_ON_MANAGER_START, INT}}, {"UpdateFailedCount", {CLEAR_ON_MANAGER_START, INT}},
@@ -129,44 +138,423 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}},
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
{"Version", {PERSISTENT, STRING}}, {"Version", {PERSISTENT, STRING}},
{"dp_dev_last_log", {CLEAR_ON_ONROAD_TRANSITION, STRING}},
{"dp_dev_reset_conf", {CLEAR_ON_MANAGER_START, BOOL, "0"}}, // StarPilot variables
{"dp_dev_beep", {PERSISTENT, BOOL, "0"}}, {"AccelerationPath", {PERSISTENT, BOOL, "1", "0", 2}},
{"dp_dev_is_rhd", {PERSISTENT, BOOL, "0"}}, {"AccelerationProfile", {PERSISTENT, INT, "2", "0", 0}},
{"dp_lat_alka", {PERSISTENT, BOOL, "0"}}, {"AdjacentLeadsUI", {PERSISTENT, BOOL, "1", "0", 3}},
{"dp_ui_display_mode", {PERSISTENT, INT, "0"}}, {"AdjacentPath", {PERSISTENT, BOOL, "0", "0", 3}},
{"dp_dev_model_selected", {PERSISTENT, STRING}}, {"AdjacentPathMetrics", {PERSISTENT, BOOL, "0", "0", 3}},
{"dp_dev_model_list", {PERSISTENT, STRING}}, {"AdvancedCustomUI", {PERSISTENT, BOOL, "0", "0", 2}},
{"dp_lat_lca_speed", {PERSISTENT, INT, "20"}}, {"AdvancedLateralTune", {PERSISTENT, BOOL, "1", "0", 2}},
{"dp_lat_lca_auto_sec", {PERSISTENT, FLOAT, "0.0"}}, {"AdvancedLongitudinalTune", {PERSISTENT, BOOL, "1", "0", 3}},
{"dp_dev_go_off_road", {CLEAR_ON_MANAGER_START, BOOL}}, {"AggressiveFollow", {PERSISTENT, FLOAT, "1.25", "1.25", 2}},
{"dp_ui_hide_hud_speed_kph", {PERSISTENT, INT, "0"}}, {"AggressiveFollowHigh", {PERSISTENT, FLOAT, "1.25", "1.25", 2}},
{"dp_lon_ext_radar", {PERSISTENT, BOOL, "0"}}, {"AggressiveJerkAcceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"dp_lat_road_edge_detection", {PERSISTENT, BOOL, "0"}}, {"AggressiveJerkDanger", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
{"dp_ui_rainbow", {PERSISTENT, BOOL, "0"}}, {"AggressiveJerkDeceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"dp_lon_acm", {PERSISTENT, BOOL, "0"}}, {"AggressiveJerkSpeed", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"dp_lon_aem", {PERSISTENT, BOOL, "0"}}, {"AggressiveJerkSpeedDecrease", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"dp_lon_dtsc", {PERSISTENT, BOOL, "0"}}, {"AlertVolumeControl", {PERSISTENT, BOOL, "0", "0", 2}},
{"dp_lon_apm", {PERSISTENT, BOOL, "0"}}, {"AlwaysOnLateral", {PERSISTENT, BOOL, "1", "0", 0}},
{"dp_lon_dasr", {PERSISTENT, BOOL, "0"}}, {"AlwaysOnLateralLKAS", {PERSISTENT, BOOL, "1", "0", 2}},
{"dp_dev_audible_alert_mode", {PERSISTENT, INT, "0"}}, {"ApiCache_DriveStats", {PERSISTENT, JSON, "{}", "{}"}},
{"dp_dev_auto_shutdown_in", {PERSISTENT, INT, "-5"}}, {"AutomaticallyDownloadModels", {PERSISTENT, BOOL, "1", "0", 1}},
{"dp_ui_lead", {PERSISTENT, INT, "0"}}, {"AutomaticUpdates", {PERSISTENT, BOOL, "1", "1", 0}},
{"dp_dev_opview", {PERSISTENT, BOOL, "0"}}, {"AvailableModelNames", {PERSISTENT, STRING, "", "", 1}},
{"dp_dev_dashy", {PERSISTENT, BOOL, "0"}}, {"AvailableModelSeries", {PERSISTENT, STRING, "", "", 1}},
{"dp_maa_route", {CLEAR_ON_MANAGER_START, JSON}}, {"AvailableModels", {PERSISTENT, STRING, "", "", 1}},
{"dp_maa_destination", {PERSISTENT, JSON}}, {"BlacklistedModels", {PERSISTENT, STRING, "", "", 2}},
{"dp_maa_places", {PERSISTENT, JSON}}, {"BootLogo", {PERSISTENT, STRING, "starpilot", "stock", 0}},
{"dp_dev_delay_loggerd", {PERSISTENT, INT, "0"}}, {"BuildMetadata", {PERSISTENT, STRING, "", "", 0}},
{"dp_dev_disable_connect", {PERSISTENT, BOOL, "0"}}, {"BlindSpotMetrics", {PERSISTENT, BOOL, "1", "0", 3}},
{"dp_dev_tethering", {PERSISTENT, BOOL, "0"}}, {"BlindSpotPath", {PERSISTENT, BOOL, "1", "0", 1}},
{"dp_ui_mici", {PERSISTENT, BOOL, "0"}}, {"BelowSteerSpeedVolume", {PERSISTENT, INT, "101", "101", 2}},
{"dp_lat_offset_cm", {PERSISTENT, INT, "0"}}, {"BorderMetrics", {PERSISTENT, BOOL, "0", "0", 3}},
{"dp_toyota_door_auto_lock_unlock", {PERSISTENT, BOOL, "0"}}, {"CalibratedLateralAcceleration", {PERSISTENT, FLOAT, "2.0", "2.0", 2}},
{"dp_toyota_tss1_sng", {PERSISTENT, BOOL, "0"}}, {"CalibrationProgress", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"dp_toyota_stock_lon", {PERSISTENT, BOOL, "0"}}, {"CameraView", {PERSISTENT, INT, "3", "0", 2}},
{"dp_vag_a0_sng", {PERSISTENT, BOOL, "0"}}, {"CancelDownloadMaps", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"dp_vag_pq_steering_patch", {PERSISTENT, BOOL, "0"}}, {"CancelModelDownload", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"dp_vag_avoid_eps_lockout", {PERSISTENT, BOOL, "0"}}, {"CancelThemeDownload", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"dp_honda_nidec_stock_long", {PERSISTENT, BOOL, "0"}}, {"CarMake", {PERSISTENT, STRING, "mock", "mock", 0}},
{"CarModel", {PERSISTENT, STRING, "MOCK", "MOCK", 0}},
{"CarModelName", {PERSISTENT, STRING, "", "", 0}},
{"CECurves", {PERSISTENT, BOOL, "0", "0", 1}},
{"CECurvesLead", {PERSISTENT, BOOL, "0", "0", 1}},
{"CELead", {PERSISTENT, BOOL, "1", "0", 1}},
{"CEModelStopTime", {PERSISTENT, FLOAT, "7.0", "0.0", 2}},
{"CESignalLaneDetection", {PERSISTENT, BOOL, "1", "0", 2}},
{"CESignalSpeed", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"CESlowerLead", {PERSISTENT, BOOL, "1", "0", 1}},
{"CESpeed", {PERSISTENT, FLOAT, "0.0", "0.0", 1}},
{"CESpeedLead", {PERSISTENT, FLOAT, "0.0", "0.0", 1}},
{"CEStatus", {CLEAR_ON_OFFROAD_TRANSITION, INT, "0", "0"}},
{"CEStopLights", {PERSISTENT, BOOL, "1", "0", 1}},
{"CEStoppedLead", {PERSISTENT, BOOL, "1", "0", 1}},
{"ClusterOffset", {PERSISTENT, FLOAT, "1.015", "1.015", 2}},
{"ColorScheme", {PERSISTENT, STRING, "frog", "stock", 0}},
{"ColorToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"BootLogoToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"Compass", {PERSISTENT, BOOL, "0", "0", 1}},
{"CommunityFavorites", {PERSISTENT, STRING, "", "", 1}},
{"ConditionalExperimental", {PERSISTENT, BOOL, "1", "0", 1}},
{"CurvatureData", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
{"CurveSpeedController", {PERSISTENT, BOOL, "1", "0", 1}},
{"CustomAlerts", {PERSISTENT, BOOL, "0", "0", 0}},
{"CustomAccelProfile", {PERSISTENT, BOOL, "0", "0", 3}},
{"CustomAccelProfileInitialized", {PERSISTENT, BOOL, "0", "0", 3}},
{"CustomAccelProfile0MPH", {PERSISTENT, FLOAT, "3.0", "3.0", 3}},
{"CustomAccelProfile11MPH", {PERSISTENT, FLOAT, "2.5", "2.5", 3}},
{"CustomAccelProfile22MPH", {PERSISTENT, FLOAT, "2.0", "2.0", 3}},
{"CustomAccelProfile34MPH", {PERSISTENT, FLOAT, "1.5", "1.5", 3}},
{"CustomAccelProfile45MPH", {PERSISTENT, FLOAT, "1.0", "1.0", 3}},
{"CustomAccelProfile56MPH", {PERSISTENT, FLOAT, "0.8", "0.8", 3}},
{"CustomAccelProfile89MPH", {PERSISTENT, FLOAT, "0.6", "0.6", 3}},
{"CustomCruise", {PERSISTENT, FLOAT, "1.0", "1.0", 2}},
{"CustomCruiseLong", {PERSISTENT, FLOAT, "5.0", "5.0", 2}},
{"CustomPersonalities", {PERSISTENT, BOOL, "0", "0", 2}},
{"TrafficPersonalityProfile", {PERSISTENT, BOOL, "1", "1", 2}},
{"AggressivePersonalityProfile", {PERSISTENT, BOOL, "1", "1", 2}},
{"StandardPersonalityProfile", {PERSISTENT, BOOL, "1", "1", 2}},
{"RelaxedPersonalityProfile", {PERSISTENT, BOOL, "1", "1", 2}},
{"CustomThemes", {PERSISTENT, BOOL, "1", "0", 0}},
{"CustomUI", {PERSISTENT, BOOL, "1", "0", 1}},
{"DebugMode", {CLEAR_ON_OFFROAD_TRANSITION, BOOL, "0", "0"}},
{"DecelerationProfile", {PERSISTENT, INT, "1", "0", 2}},
{"DeveloperMetrics", {PERSISTENT, BOOL, "1", "0", 3}},
{"DeveloperSidebar", {PERSISTENT, BOOL, "1", "0", 3}},
{"DeveloperSidebarMetric1", {PERSISTENT, INT, "1", "0", 3}},
{"DeveloperSidebarMetric2", {PERSISTENT, INT, "2", "0", 3}},
{"DeveloperSidebarMetric3", {PERSISTENT, INT, "3", "0", 3}},
{"DeveloperSidebarMetric4", {PERSISTENT, INT, "4", "0", 3}},
{"DeveloperSidebarMetric5", {PERSISTENT, INT, "5", "0", 3}},
{"DeveloperSidebarMetric6", {PERSISTENT, INT, "6", "0", 3}},
{"DeveloperSidebarMetric7", {PERSISTENT, INT, "7", "0", 3}},
{"DeveloperUI", {PERSISTENT, BOOL, "0", "0", 3}},
{"DeveloperWidgets", {PERSISTENT, BOOL, "1", "0", 3}},
{"DeviceManagement", {PERSISTENT, BOOL, "1", "0", 1}},
{"DeviceShutdown", {PERSISTENT, INT, "9", "33", 1}},
{"DisableOnroadUploads", {PERSISTENT, BOOL, "0", "0", 2}},
{"DisableOpenpilotLongitudinal", {PERSISTENT, BOOL, "0", "0", 0}},
{"DiscordUsername", {PERSISTENT, STRING, "", "", 0}},
{"DisengageVolume", {PERSISTENT, INT, "101", "101", 2}},
{"DistanceButtonControl", {PERSISTENT, INT, "1", "0", 2}},
{"DistanceIconPack", {PERSISTENT, STRING, "stock", "stock", 0}},
{"DistanceIconToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"DownloadableBootLogos", {PERSISTENT, STRING, "", ""}},
{"DownloadableColors", {PERSISTENT, STRING, "", ""}},
{"DownloadableDistanceIcons", {PERSISTENT, STRING, "", ""}},
{"DownloadableIcons", {PERSISTENT, STRING, "", ""}},
{"DownloadableSignals", {PERSISTENT, STRING, "", ""}},
{"DownloadableSounds", {PERSISTENT, STRING, "", ""}},
{"DownloadableWheels", {PERSISTENT, STRING, "", ""}},
{"DownloadAllModels", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"DownloadMaps", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"DriverCamera", {PERSISTENT, BOOL, "0", "0", 1}},
{"Model", {PERSISTENT, STRING, "sc2", "sc2", 1}},
{"ModelVersion", {PERSISTENT, STRING, "v11", "v11", 1}},
{"DrivingModel", {PERSISTENT, STRING, "sc2", "sc2", 1}},
{"DrivingModelName", {PERSISTENT, STRING, "South Carolina", "South Carolina", 1}},
{"DrivingModelVersion", {PERSISTENT, STRING, "v11", "v11", 1}},
{"DynamicPathWidth", {PERSISTENT, BOOL, "0", "0", 2}},
{"DynamicPedalsOnUI", {PERSISTENT, BOOL, "1", "0", 1}},
{"EngageVolume", {PERSISTENT, INT, "101", "101", 2}},
{"EVTuning", {PERSISTENT, BOOL, "0", "0", 3}},
{"Fahrenheit", {PERSISTENT, BOOL, "0", "0", 3}},
{"FlashPanda", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"GMPedalLongitudinal", {PERSISTENT, BOOL, "1", "1", 2}},
{"LongPitch", {PERSISTENT, BOOL, "1", "0", 2}},
{"RemoteStartBootsComma", {PERSISTENT, BOOL, "0", "0"}},
{"RemapCancelToDistance", {PERSISTENT, BOOL, "0", "0"}},
{"ForceAutoTune", {PERSISTENT, BOOL, "0", "0", 3}},
{"ForceAutoTuneOff", {PERSISTENT, BOOL, "1", "0", 2}},
{"ForceFingerprint", {PERSISTENT, BOOL, "0", "0", 2}},
{"ForceOffroad", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"ForceOnroad", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"ForceStops", {PERSISTENT, BOOL, "0", "0", 2}},
{"ForceStandstill", {PERSISTENT, BOOL, "0", "0", 2}},
{"ForceTorqueController", {PERSISTENT, BOOL, "0", "0", 3}},
{"FPSCounter", {PERSISTENT, BOOL, "1", "0", 3}},
{"StarPilotApiToken", {PERSISTENT | DONT_LOG, STRING, "", "", 0}},
{"StarPilotCarParams", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BYTES, "", ""}},
{"StarPilotCarParamsPersistent", {PERSISTENT, BYTES, "", ""}},
{"StarPilotDongleId", {PERSISTENT | DONT_LOG, STRING, "", "", 0}},
{"StarPilotStats", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
{"StarPilotTogglesUpdated", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"FrogsGoMoosTweak", {PERSISTENT, BOOL, "1", "0", 2}},
{"GoatScream", {PERSISTENT, BOOL, "0", "0", 1}},
{"GoatScreamCriticalAlerts", {PERSISTENT, BOOL, "0", "0", 1}},
{"GreenLightAlert", {PERSISTENT, BOOL, "0", "0", 0}},
{"HideAlerts", {PERSISTENT, BOOL, "0", "0", 2}},
{"HideLeadMarker", {PERSISTENT, BOOL, "0", "0", 2}},
{"HideMaxSpeed", {PERSISTENT, BOOL, "0", "0", 2}},
{"HideSpeed", {PERSISTENT, BOOL, "0", "0", 2}},
{"HideSpeedLimit", {PERSISTENT, BOOL, "0", "0", 2}},
{"HigherBitrate", {PERSISTENT, BOOL, "0", "0", 2}},
{"HolidayThemes", {PERSISTENT, BOOL, "1", "0", 0}},
{"HumanAcceleration", {PERSISTENT, BOOL, "0", "0", 2}},
{"HumanFollowing", {PERSISTENT, BOOL, "0", "0", 2}},
{"HumanLaneChanges", {PERSISTENT, BOOL, "0", "0", 2}},
{"IconPack", {PERSISTENT, STRING, "frog-animated", "stock", 0}},
{"IconToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"IncreasedStoppedDistance", {PERSISTENT, FLOAT, "0.0", "0.0", 1}},
{"IncreasedStoppedDistanceLowVisibility", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreasedStoppedDistanceRain", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreasedStoppedDistanceRainStorm", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreasedStoppedDistanceSnow", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreaseFollowingLowVisibility", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreaseFollowingRain", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreaseFollowingRainStorm", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreaseFollowingSnow", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"IncreaseThermalLimits", {PERSISTENT, BOOL, "0", "0", 2}},
{"IssueReported", {CLEAR_ON_MANAGER_START, JSON, "{}", "{}"}},
{"KonikDongleId", {PERSISTENT, STRING, "", "", 0}},
{"KonikMinutes", {PERSISTENT, INT, "0", "0", 0}},
{"LaneChanges", {PERSISTENT, BOOL, "1", "1", 0}},
{"LaneChangeTime", {PERSISTENT, FLOAT, "1.0", "0.0", 1}},
{"LaneDetectionWidth", {PERSISTENT, FLOAT, "0.0", "0.0", 1}},
{"LaneLinesWidth", {PERSISTENT, FLOAT, "4.0", "2.0", 2}},
{"LastMapsUpdate", {PERSISTENT, STRING, "", ""}},
{"LateralTune", {PERSISTENT, BOOL, "1", "0", 1}},
{"LeadDepartingAlert", {PERSISTENT, BOOL, "0", "0", 0}},
{"LeadDetectionThreshold", {PERSISTENT, INT, "35", "50", 3}},
{"LeadInfo", {PERSISTENT, BOOL, "1", "0", 3}},
{"LKASButtonControl", {PERSISTENT, INT, "5", "0", 2}},
{"LockDoors", {PERSISTENT, BOOL, "1", "0", 0}},
{"LockDoorsTimer", {PERSISTENT, INT, "0", "0", 0}},
{"LongDistanceButtonControl", {PERSISTENT, INT, "5", "0", 2}},
{"LongitudinalActuatorDelay", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"LongitudinalActuatorDelayStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"LateralManeuverStatus", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON, "{}", "{}"}},
{"LongitudinalManeuverPaddleMode", {PERSISTENT, STRING, "auto", "auto"}},
{"LongitudinalManeuverStatus", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON, "{}", "{}"}},
{"LongitudinalTune", {PERSISTENT, BOOL, "1", "0", 0}},
{"LoudBlindspotAlert", {PERSISTENT, BOOL, "0", "0", 0}},
{"LowVoltageShutdown", {PERSISTENT, FLOAT, "11.8", "11.8", 3}},
{"ManualUpdateInitiated", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"MapAcceleration", {PERSISTENT, BOOL, "0", "0", 1}},
{"MapboxPublicKey", {PERSISTENT | DONT_LOG, STRING, "", "", 0}},
{"MapBoxRequests", {PERSISTENT, JSON, "{}", "{}"}},
{"MapboxSecretKey", {PERSISTENT | DONT_LOG, STRING, "", "", 0}},
{"MapDeceleration", {PERSISTENT, BOOL, "0", "0", 1}},
{"MapdSettings", {PERSISTENT, JSON, "{}", "{}"}},
{"MapGears", {PERSISTENT, BOOL, "0", "0", 2}},
{"MapsSelected", {PERSISTENT, STRING, "", "", 0}},
{"MapSpeedLimit", {CLEAR_ON_MANAGER_START, FLOAT, "0.0", "0.0"}},
{"NextMapSpeedLimit", {CLEAR_ON_MANAGER_START, JSON, "{}", "{}"}},
{"VisionSpeedLimit", {CLEAR_ON_MANAGER_START, FLOAT, "0.0", "0.0"}},
{"VisionSpeedLimitConfidence", {CLEAR_ON_MANAGER_START, FLOAT, "0.0", "0.0"}},
{"VisionSpeedLimitBookmarkCount", {CLEAR_ON_MANAGER_START, INT, "0", "0"}},
{"VisionSpeedLimitDebugSession", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"VisionSpeedLimitLastEvent", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"VisionSpeedLimitStatus", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"VisionSpeedLimitStream", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"MaxDesiredAcceleration", {PERSISTENT, FLOAT, "4.0", "2.0", 2}},
{"MinimumBackupSize", {PERSISTENT, INT, "0", "0"}},
{"MinimumLaneChangeSpeed", {PERSISTENT, FLOAT, "20.0", "20.0", 2}},
{"ModelDownloadProgress", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"ModelDrivesAndScores", {PERSISTENT, JSON, "{}", "{}"}},
{"ModelReleasedDates", {PERSISTENT, STRING, "", "", 1}},
{"ModelRandomizer", {PERSISTENT, BOOL, "0", "0", 2}},
{"ModelSortMode", {PERSISTENT, STRING, "alphabetical", "alphabetical", 1}},
{"ModelToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"ModelUI", {PERSISTENT, BOOL, "1", "0", 2}},
{"ModelVersions", {PERSISTENT, STRING, "", "", 1}},
{"NavigationUI", {PERSISTENT, BOOL, "1", "0", 1}},
{"NNFF", {PERSISTENT, BOOL, "0", "0", 2}},
{"NNFFLite", {PERSISTENT, BOOL, "0", "0", 2}},
{"NNFFModelName", {CLEAR_ON_MANAGER_START, STRING, "", "", 0}},
{"NoLogging", {PERSISTENT, BOOL, "0", "0", 2}},
{"NoUploads", {PERSISTENT, BOOL, "0", "0", 2}},
{"NudgelessLaneChange", {PERSISTENT, BOOL, "1", "0", 0}},
{"NumericalTemp", {PERSISTENT, BOOL, "1", "0", 3}},
{"Offset1", {PERSISTENT, FLOAT, "5.0", "0.0", 0}},
{"Offset2", {PERSISTENT, FLOAT, "5.0", "0.0", 0}},
{"Offset3", {PERSISTENT, FLOAT, "5.0", "0.0", 0}},
{"Offset4", {PERSISTENT, FLOAT, "5.0", "0.0", 0}},
{"Offset5", {PERSISTENT, FLOAT, "10.0", "0.0", 0}},
{"Offset6", {PERSISTENT, FLOAT, "10.0", "0.0", 0}},
{"Offset7", {PERSISTENT, FLOAT, "10.0", "0.0", 0}},
{"OneLaneChange", {PERSISTENT, BOOL, "1", "0", 2}},
{"OnroadDistanceButton", {PERSISTENT, BOOL, "0", "0", 0}},
{"OnroadDistanceButtonPressed", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"openpilotMinutes", {PERSISTENT, INT, "0", "0", 0}},
{"OverpassRequests", {PERSISTENT, JSON, "{}", "{}"}},
{"PathEdgeWidth", {PERSISTENT, FLOAT, "20.0", "0.0", 2}},
{"PathWidth", {PERSISTENT, FLOAT, "6.1", "5.9", 2}},
{"PauseAOLOnBrake", {PERSISTENT, BOOL, "0", "0", 1}},
{"PauseLateralOnSignal", {PERSISTENT, BOOL, "0", "0", 1}},
{"PauseLateralSpeed", {PERSISTENT, FLOAT, "0.0", "0.0", 1}},
{"PedalsOnUI", {PERSISTENT, BOOL, "0", "0", 1}},
{"PondPaired", {PERSISTENT, BOOL, "0", "0", 0}},
{"PondUploadPending", {PERSISTENT, BOOL, "0", "0", 0}},
{"PreferredSchedule", {PERSISTENT, INT, "2", "0", 0}},
{"PreviousSpeedLimit", {PERSISTENT, FLOAT, "0.0", "0.0"}},
{"PromptDistractedVolume", {PERSISTENT, INT, "101", "101", 2}},
{"PromptVolume", {PERSISTENT, INT, "101", "101", 2}},
{"QOLLateral", {PERSISTENT, BOOL, "1", "0", 1}},
{"QOLLongitudinal", {PERSISTENT, BOOL, "1", "0", 1}},
{"QOLVisuals", {PERSISTENT, BOOL, "1", "0", 0}},
{"RadarTracksUI", {PERSISTENT, BOOL, "0", "0", 3}},
{"RainbowPath", {PERSISTENT, BOOL, "0", "0", 1}},
{"RandomEvents", {PERSISTENT, BOOL, "0", "0", 1}},
{"RandomThemes", {PERSISTENT, BOOL, "0", "0", 1}},
{"RandomThemesHolidays", {PERSISTENT, BOOL, "0", "0", 1}},
{"ReduceAccelerationLowVisibility", {PERSISTENT, INT, "0", "0", 2}},
{"ReduceAccelerationRain", {PERSISTENT, INT, "0", "0", 2}},
{"ReduceAccelerationRainStorm", {PERSISTENT, INT, "0", "0", 2}},
{"ReduceAccelerationSnow", {PERSISTENT, INT, "0", "0", 2}},
{"ReduceLateralAccelerationLowVisibility", {PERSISTENT, INT, "0", "0", 2}},
{"ReduceLateralAccelerationRain", {PERSISTENT, INT, "0", "0", 2}},
{"ReduceLateralAccelerationRainStorm", {PERSISTENT, INT, "0", "0", 2}},
{"ReduceLateralAccelerationSnow", {PERSISTENT, INT, "0", "0", 2}},
{"RefuseVolume", {PERSISTENT, INT, "101", "101", 2}},
{"RelaxedFollow", {PERSISTENT, FLOAT, "1.75", "1.75", 2}},
{"RelaxedFollowHigh", {PERSISTENT, FLOAT, "1.75", "1.75", 2}},
{"RelaxedJerkAcceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"RelaxedJerkDanger", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
{"RelaxedJerkDeceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"RelaxedJerkSpeed", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"RelaxedJerkSpeedDecrease", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"ReverseCruise", {PERSISTENT, BOOL, "0", "0", 1}},
{"RecoveryPower", {PERSISTENT, FLOAT, "1.0", "1.0", 2}},
{"RoadEdgesWidth", {PERSISTENT, FLOAT, "2.0", "2.0", 2}},
{"RoadNameUI", {PERSISTENT, BOOL, "1", "0", 1}},
{"RotatingWheel", {PERSISTENT, BOOL, "1", "0", 1}},
{"ScreenBrightness", {PERSISTENT, INT, "101", "101", 2}},
{"ScreenBrightnessOnroad", {PERSISTENT, INT, "101", "101", 2}},
{"ScreenManagement", {PERSISTENT, BOOL, "1", "0", 1}},
{"ScreenRecorder", {PERSISTENT, BOOL, "1", "0", 2}},
{"ScreenTimeout", {PERSISTENT, INT, "30", "30", 2}},
{"ScreenTimeoutOnroad", {PERSISTENT, INT, "30", "10", 2}},
{"SecOCKeys", {PERSISTENT | DONT_LOG, STRING, "", "", 0}},
{"SafeMode", {PERSISTENT, BOOL, "0", "0", 0}},
{"SafeModeBackup", {PERSISTENT, JSON, "{}", "{}"}},
{"SetSpeedLimit", {PERSISTENT, BOOL, "0", "0", 1}},
{"SetSpeedOffset", {PERSISTENT, FLOAT, "0.0", "0.0", 2}},
{"ShowCEMStatus", {PERSISTENT, BOOL, "1", "0", 2}},
{"ShowCPU", {PERSISTENT, BOOL, "1", "0", 3}},
{"ShowCSCStatus", {PERSISTENT, BOOL, "1", "0", 2}},
{"ShowGPU", {PERSISTENT, BOOL, "0", "0", 3}},
{"ShowIP", {PERSISTENT, BOOL, "0", "0", 3}},
{"ShowMemoryUsage", {PERSISTENT, BOOL, "1", "0", 3}},
{"ShownToggleDescriptions", {PERSISTENT, JSON, "{}", "{}"}},
{"ShowSLCOffset", {PERSISTENT, BOOL, "1", "0", 0}},
{"ShowSpeedLimits", {PERSISTENT, BOOL, "1", "0", 1}},
{"ShowSteering", {PERSISTENT, BOOL, "0", "0", 3}},
{"ShowStoppingPoint", {PERSISTENT, BOOL, "1", "0", 3}},
{"ShowStoppingPointMetrics", {PERSISTENT, BOOL, "1", "0", 3}},
{"ShowStorageLeft", {PERSISTENT, BOOL, "0", "0", 3}},
{"ShowStorageUsed", {PERSISTENT, BOOL, "0", "0", 3}},
{"SidebarMetrics", {PERSISTENT, BOOL, "1", "0", 3}},
{"SidebarOpen", {PERSISTENT, BOOL, "0", "0", 0}},
{"SignalAnimation", {PERSISTENT, STRING, "frog", "stock", 0}},
{"SignalMetrics", {PERSISTENT, BOOL, "0", "0", 3}},
{"SignalToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"SimpleMode", {PERSISTENT, BOOL, "0", "0", 0}},
{"SLCConfirmation", {PERSISTENT, BOOL, "0", "0", 0}},
{"SLCConfirmationHigher", {PERSISTENT, BOOL, "0", "0", 0}},
{"SLCConfirmationLower", {PERSISTENT, BOOL, "0", "0", 0}},
{"SLCFallback", {PERSISTENT, INT, "2", "0", 1}},
{"SLCLookaheadHigher", {PERSISTENT, INT, "0", "0", 2}},
{"SLCLookaheadLower", {PERSISTENT, INT, "0", "0", 2}},
{"SLCMapboxFiller", {PERSISTENT, BOOL, "1", "0", 1}},
{"SLCOverride", {PERSISTENT, INT, "1", "0", 1}},
{"SLCPriority", {PERSISTENT, STRING, "", "", 2}},
{"SLCPriority1", {PERSISTENT, STRING, "Map Data", "Map Data", 2}},
{"SLCPriority2", {PERSISTENT, STRING, "Dashboard", "Dashboard", 2}},
{"SNGHack", {PERSISTENT, BOOL, "1", "0", 2}},
{"SoundPack", {PERSISTENT, STRING, "frog", "stock", 0}},
{"SoundToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"SpeedLimitAccepted", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"SpeedLimitChangedAlert", {PERSISTENT, BOOL, "0", "0", 0}},
{"SpeedLimitController", {PERSISTENT, BOOL, "0", "0", 0}},
{"SpeedLimitFiller", {PERSISTENT, BOOL, "0", "0", 0}},
{"SpeedLimits", {PERSISTENT | DONT_LOG, JSON, "[]", "[]"}},
{"SpeedLimitsFiltered", {PERSISTENT | DONT_LOG, JSON, "[]", "[]"}},
{"SpeedLimitSources", {PERSISTENT, BOOL, "0", "0", 3}},
{"VisionSpeedLimitAutoBookmark", {PERSISTENT, BOOL, "0", "0", 0}},
{"VisionSpeedLimitAutoPreserveSegment", {PERSISTENT, BOOL, "0", "0", 0}},
{"VisionSpeedLimitDetection", {PERSISTENT, BOOL, "0", "0", 0}},
{"VisionSpeedLimitTrainingCollector", {PERSISTENT, BOOL, "1", "1", 0}},
{"StandardFollow", {PERSISTENT, FLOAT, "1.45", "1.45", 2}},
{"StandardFollowHigh", {PERSISTENT, FLOAT, "1.45", "1.45", 2}},
{"StandardJerkAcceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"StandardJerkDanger", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
{"StandardJerkDeceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"StandardJerkSpeed", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"StandardJerkSpeedDecrease", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"StandbyMode", {PERSISTENT, BOOL, "0", "0", 1}},
{"StartAccel", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"StartAccelStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"StartupMessageBottom", {PERSISTENT, STRING, "Human-tested, frog-approved 🐸", "Always keep hands on wheel and eyes on road", 0}},
{"StartupMessageTop", {PERSISTENT, STRING, "Hop in and buckle up!", "Be ready to take over at any time", 0}},
{"StaticPedalsOnUI", {PERSISTENT, BOOL, "0", "0", 1}},
{"SteerDelay", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerDelayStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerFriction", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerFrictionStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerKP", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerKPStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerLatAccel", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerLatAccelStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerOffset", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerOffsetStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerRatio", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerRatioStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"StockDongleId", {PERSISTENT, STRING, "", ""}},
{"StopAccel", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"StopAccelStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"StoppedTimer", {PERSISTENT, BOOL, "0", "0", 1}},
{"StopDistance", {PERSISTENT, FLOAT, "6.0", "6.0", 2}},
{"StoppingDecelRate", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"StoppingDecelRateStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SwitchbackModeCooldown", {PERSISTENT, INT, "5", "0", 2}},
{"SwitchbackModeEnabled", {CLEAR_ON_OFFROAD_TRANSITION, BOOL, "0", "0"}},
{"SubaruSNG", {PERSISTENT, BOOL, "1", "0", 2}},
{"TacoTune", {PERSISTENT, BOOL, "0", "0", 2}},
{"TacoTuneHacks", {PERSISTENT, BOOL, "0", "0", 2}},
{"TestAlert", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"TetheringEnabled", {PERSISTENT, INT, "0", "0", 0}},
{"ThemeDownloadProgress", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"ThemesDownloaded", {PERSISTENT, JSON, "{}", "{}"}},
{"Timezone", {PERSISTENT, STRING, "", ""}},
{"TinygradUpdateAvailable", {PERSISTENT, BOOL, "0", "0", 1}},
{"ToyotaDoors", {PERSISTENT, BOOL, "1", "0", 0}},
{"TrafficFollow", {PERSISTENT, FLOAT, "0.5", "0.5", 2}},
{"TrafficJerkAcceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"TrafficJerkDanger", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
{"TrafficJerkDeceleration", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"TrafficJerkSpeed", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"TrafficJerkSpeedDecrease", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
{"TruckTuning", {PERSISTENT, BOOL, "0", "0", 3}},
{"TuningLevel", {PERSISTENT, INT, "0", "0", 0}},
{"TuningLevelConfirmed", {PERSISTENT, BOOL, "0", "0", 0}},
{"TurnDesires", {PERSISTENT, BOOL, "0", "0", 2}},
{"UnlockDoors", {PERSISTENT, BOOL, "1", "0", 0}},
{"Updated", {PERSISTENT, STRING, "0", "0"}},
{"UpdateSpeedLimits", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"UpdateSpeedLimitsStatus", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
{"UpdateTinygrad", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"UpdateWheelImage", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"UseActiveTheme", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}},
{"UseKonikServer", {PERSISTENT, BOOL, "0", "0", 2}},
{"UseSI", {PERSISTENT, BOOL, "1", "1", 3}},
{"UserFavorites", {PERSISTENT, STRING, "", "", 1}},
{"UseVienna", {PERSISTENT, BOOL, "0", "0", 1}},
{"VEgoStarting", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"VEgoStartingStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"VEgoStopping", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"VEgoStoppingStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"VeryLongDistanceButtonControl", {PERSISTENT, INT, "6", "0", 2}},
{"VoltSNG", {PERSISTENT, BOOL, "0", "0", 2}},
{"WarningImmediateVolume", {PERSISTENT, INT, "101", "101", 2}},
{"WarningSoftVolume", {PERSISTENT, INT, "101", "101", 2}},
{"WeatherPresets", {PERSISTENT, BOOL, "0", "0", 2}},
{"WeatherToken", {PERSISTENT | DONT_LOG, STRING, "", "", 2}},
{"WheelControls", {PERSISTENT, STRING, "", "", 2}},
{"WheelIcon", {PERSISTENT, STRING, "frog", "stock", 0}},
{"WheelSpeed", {PERSISTENT, BOOL, "0", "0", 2}},
{"WheelToDownload", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
}; };
+19413
View File
File diff suppressed because it is too large Load Diff
+68 -7
View File
@@ -20,6 +20,9 @@ cdef extern from "common/params.h":
CLEAR_ON_IGNITION_ON CLEAR_ON_IGNITION_ON
ALL ALL
# StarPilot variables
DONT_LOG
cpdef enum ParamKeyType: cpdef enum ParamKeyType:
STRING STRING
BOOL BOOL
@@ -30,7 +33,7 @@ cdef extern from "common/params.h":
BYTES BYTES
cdef cppclass c_Params "Params": cdef cppclass c_Params "Params":
c_Params(string) except + nogil c_Params(string, bool) except + nogil
string get(string, bool) nogil string get(string, bool) nogil
bool getBool(string, bool) nogil bool getBool(string, bool) nogil
int remove(string) nogil int remove(string) nogil
@@ -45,6 +48,13 @@ cdef extern from "common/params.h":
void clearAll(ParamKeyFlag) void clearAll(ParamKeyFlag)
vector[string] allKeys() vector[string] allKeys()
# StarPilot variables
ParamKeyFlag getKeyFlag(string) nogil
optional[string] getStockValue(string) nogil
int getTuningLevel(string) nogil
PYTHON_2_CPP = { PYTHON_2_CPP = {
(str, STRING): lambda v: v, (str, STRING): lambda v: v,
(builtins.bool, BOOL): lambda v: "1" if v else "0", (builtins.bool, BOOL): lambda v: "1" if v else "0",
@@ -55,12 +65,35 @@ PYTHON_2_CPP = {
(list, JSON): json.dumps, (list, JSON): json.dumps,
(bytes, BYTES): lambda v: v, (bytes, BYTES): lambda v: v,
} }
def _decode_int(v):
decoded = v.decode("utf-8")
try:
return int(decoded)
except ValueError:
return int(float(decoded))
def _decode_time(v):
decoded = v.decode("utf-8")
try:
return datetime.datetime.fromisoformat(decoded)
except ValueError:
for fmt in ("%B %d, %Y - %I:%M%p", "%B %d, %Y - %I:%M %p"):
try:
return datetime.datetime.strptime(decoded, fmt)
except ValueError:
pass
raise
CPP_2_PYTHON = { CPP_2_PYTHON = {
STRING: lambda v: v.decode("utf-8"), STRING: lambda v: v.decode("utf-8"),
BOOL: lambda v: v == b"1", BOOL: lambda v: v == b"1",
INT: int, INT: _decode_int,
FLOAT: float, FLOAT: float,
TIME: lambda v: datetime.datetime.fromisoformat(v.decode("utf-8")), TIME: _decode_time,
JSON: json.loads, JSON: json.loads,
BYTES: lambda v: v, BYTES: lambda v: v,
} }
@@ -75,14 +108,27 @@ cdef class Params:
cdef c_Params* p cdef c_Params* p
cdef str d cdef str d
def __cinit__(self, d=""): # StarPilot variables
cdef bool m
cdef bool return_defaults
def __cinit__(self, d="", *, memory=False, return_defaults=False):
cdef string path = <string>d.encode() cdef string path = <string>d.encode()
# StarPilot variables
cdef bool c_memory = memory
with nogil: with nogil:
self.p = new c_Params(path) self.p = new c_Params(path, c_memory)
self.d = d self.d = d
# StarPilot variables
self.m = memory
self.return_defaults = return_defaults or memory
def __reduce__(self): def __reduce__(self):
return (type(self), (self.d,)) return (type(self), (self.d, self.m, self.return_defaults))
def __dealloc__(self): def __dealloc__(self):
del self.p del self.p
@@ -119,7 +165,7 @@ cdef class Params:
with nogil: with nogil:
val = self.p.get(k, block) val = self.p.get(k, block)
default_val = (default.value() if default.has_value() else None) if return_default else None default_val = (default.value() if default.has_value() else None) if (return_default or self.return_defaults and not block) else None
if val == b"": if val == b"":
if block: if block:
# If we got no value while running in blocked mode # If we got no value while running in blocked mode
@@ -194,3 +240,18 @@ cdef class Params:
cdef string k = self.check_key(key) cdef string k = self.check_key(key)
cdef ParamKeyType t = self.p.getKeyType(k) cdef ParamKeyType t = self.p.getKeyType(k)
return self._cpp2python(t, value, None, key) return self._cpp2python(t, value, None, key)
# StarPilot variables
def get_key_flag(self, key):
return self.p.getKeyFlag(self.check_key(key))
def get_stock_value(self, key):
cdef string k = self.check_key(key)
cdef ParamKeyType t = self.p.getKeyType(k)
cdef optional[string] stock = self.p.getStockValue(k)
return self._cpp2python(t, stock.value(), None, key) if stock.has_value() else None
def get_tuning_level(self, key):
cdef string k = self.check_key(key)
cdef optional[int] level = self.p.getTuningLevel(k)
return level.value() if level.has_value() else 0
BIN
View File
Binary file not shown.
+3
View File
@@ -30,7 +30,10 @@ class Priority:
def set_core_affinity(cores: list[int]) -> None: def set_core_affinity(cores: list[int]) -> None:
if sys.platform == 'linux' and not PC: if sys.platform == 'linux' and not PC:
try:
os.sched_setaffinity(0, cores) os.sched_setaffinity(0, cores)
except OSError:
pass
def config_realtime_process(cores: int | list[int], priority: int) -> None: def config_realtime_process(cores: int | list[int], priority: int) -> None:
+20 -2
View File
@@ -5,10 +5,28 @@ from openpilot.common.basedir import BASEDIR
class Spinner: class Spinner:
def __init__(self): def __init__(self):
self.spinner_proc = None
# Prefer the legacy compiled spinner on device.
legacy_spinner = os.path.join(BASEDIR, "selfdrive", "ui", "spinner")
if os.path.isfile("/TICI") and os.path.isfile(legacy_spinner) and os.access(legacy_spinner, os.X_OK):
try: try:
self.spinner_proc = subprocess.Popen(["./spinner.py"], self.spinner_proc = subprocess.Popen([legacy_spinner],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
cwd=os.path.join(BASEDIR, "system", "ui"), cwd=os.path.join(BASEDIR, "selfdrive", "ui"),
close_fds=True)
return
except OSError:
self.spinner_proc = None
# Raylib spinner requires Python deps from the repo virtualenv.
spinner_cwd = os.path.join(BASEDIR, "system", "ui")
venv_python = os.path.join(BASEDIR, ".venv", "bin", "python")
python_exec = venv_python if os.path.isfile(venv_python) else "python3"
try:
self.spinner_proc = subprocess.Popen([python_exec, "./spinner.py"],
stdin=subprocess.PIPE,
cwd=spinner_cwd,
close_fds=True) close_fds=True)
except OSError: except OSError:
self.spinner_proc = None self.spinner_proc = None
+10
View File
@@ -101,6 +101,16 @@ class TestParams:
assert q.get("CarParams") is None assert q.get("CarParams") is None
assert q.get("CarParams", True) == b"1" assert q.get("CarParams", True) == b"1"
def test_put_nonblocking_latest_value_wins(self, tmp_path):
q = Params(str(tmp_path))
for i in range(100):
q.put_nonblocking("CarParams", f"value-{i}".encode())
del q
r = Params(str(tmp_path))
assert r.get("CarParams") == b"value-99"
def test_params_all_keys(self): def test_params_all_keys(self):
keys = Params().all_keys() keys = Params().all_keys()
+19 -2
View File
@@ -7,10 +7,27 @@ from openpilot.common.basedir import BASEDIR
class TextWindow: class TextWindow:
def __init__(self, text): def __init__(self, text):
self.text_proc = None
# Prefer the legacy compiled text window on device.
legacy_text = os.path.join(BASEDIR, "selfdrive", "ui", "text")
if os.path.isfile("/TICI") and os.path.isfile(legacy_text) and os.access(legacy_text, os.X_OK):
try: try:
self.text_proc = subprocess.Popen(["./text.py", text], self.text_proc = subprocess.Popen([legacy_text, text],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
cwd=os.path.join(BASEDIR, "system", "ui"), cwd=os.path.join(BASEDIR, "selfdrive", "ui"),
close_fds=True)
return
except OSError:
self.text_proc = None
text_cwd = os.path.join(BASEDIR, "system", "ui")
venv_python = os.path.join(BASEDIR, ".venv", "bin", "python")
python_exec = venv_python if os.path.isfile(venv_python) else "python3"
try:
self.text_proc = subprocess.Popen([python_exec, "./text.py", text],
stdin=subprocess.PIPE,
cwd=text_cwd,
close_fds=True) close_fds=True)
except OSError: except OSError:
self.text_proc = None self.text_proc = None
Binary file not shown.
+5
View File
@@ -37,6 +37,11 @@ const double MS_TO_MPH = MS_TO_KPH * KM_TO_MILE;
const double METER_TO_MILE = KM_TO_MILE / 1000.0; const double METER_TO_MILE = KM_TO_MILE / 1000.0;
const double METER_TO_FOOT = 3.28084; const double METER_TO_FOOT = 3.28084;
// StarPilot variables
const double FOOT_TO_METER = 1. / METER_TO_FOOT;
const double CM_TO_INCH = 1. / 2.54;
const double INCH_TO_CM = 1. / CM_TO_INCH;
#define ALIGNED_SIZE(x, align) (((x) + (align)-1) & ~((align)-1)) #define ALIGNED_SIZE(x, align) (((x) + (align)-1) & ~((align)-1))
namespace util { namespace util {
+12
View File
@@ -0,0 +1,12 @@
#include <string>
#include "common/watchdog.h"
#include "common/util.h"
#include "system/hardware/hw.h"
const std::string watchdog_fn_prefix = Path::shm_path() + "/wd_"; // + <pid>
bool watchdog_kick(uint64_t ts) {
static std::string fn = watchdog_fn_prefix + std::to_string(getpid());
return util::write_file(fn.c_str(), &ts, sizeof(ts), O_WRONLY | O_CREAT) == 0;
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include <cstdint>
bool watchdog_kick(uint64_t ts);
+28
View File
@@ -0,0 +1,28 @@
import os
import time
import struct
from openpilot.system.hardware.hw import Paths
from openpilot.common.swaglog import cloudlog
WATCHDOG_FN = f"{Paths.shm_path()}/wd_"
_LAST_KICK = 0.0
_LAST_ERROR_LOG = 0.0
def kick_watchdog():
global _LAST_KICK, _LAST_ERROR_LOG
current_time = time.monotonic()
if current_time - _LAST_KICK < 1.0:
return True
try:
with open(f"{WATCHDOG_FN}{os.getpid()}", 'wb') as f:
f.write(struct.pack('<Q', int(current_time * 1e9)))
f.flush()
_LAST_KICK = current_time
return True
except OSError as e:
if current_time - _LAST_ERROR_LOG >= 5.0:
cloudlog.error(f"watchdog kick failed for pid {os.getpid()}: {e}")
_LAST_ERROR_LOG = current_time
return False
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "${ROOT_DIR}/scripts/host_tool_runner.sh" "$@"
+129
View File
@@ -0,0 +1,129 @@
# Laptop Device Build (Mac/Linux)
This flow builds **device-target (`larch64`) binaries on your laptop** using a Linux/aarch64 container and a synced comma sysroot.
For the full StarPilot branch workflow, including host-native shorthand tools such as `./dev`, `./c3`, `./c4`, and `./raybig`, see [tools/STARPILOT_DEVELOPMENT.md](../../tools/STARPILOT_DEVELOPMENT.md).
## Prerequisites
- Docker Desktop (or Podman) with Linux/aarch64 support.
- `rsync` and `ssh` on your laptop.
- Either:
- access to a comma device over SSH, or
- internet access to download AGNOS system image for sysroot extraction.
## One-time setup
Fast path (no physical comma):
```bash
cd /path/to/starpilot
scripts/laptop_device_build.sh setup
```
Equivalent wrapper:
```bash
scripts/starpilot_build_flow.sh laptop-setup
```
### Option A: no physical comma (AGNOS-based)
```bash
cd /path/to/starpilot
scripts/laptop_device_build.sh build-image
scripts/laptop_device_build.sh setup-sysroot-agnos
```
### Option B: copy sysroot from a comma device
```bash
cd /path/to/starpilot
scripts/laptop_device_build.sh setup-sysroot <device-ip> comma 22
scripts/laptop_device_build.sh build-image
```
Optional all-in-one with physical device sysroot:
```bash
scripts/laptop_device_build.sh setup <device-ip> comma 22
```
## Build device-compatible artifacts
```bash
cd /path/to/starpilot
scripts/laptop_device_build.sh build
```
Equivalent wrapper:
```bash
scripts/starpilot_build_flow.sh laptop-device
```
This runs:
- `uv sync` into `/work/.venv-linux-arm64` (inside container)
- full `scons` with:
- `SP_FORCE_ARCH=larch64`
- `SP_FORCE_TICI=1`
- `SP_TICI_SYSROOT=/opt/tici-sysroot`
- `touch prebuilt`
To run `scons` targets explicitly in the same device-compatible environment:
```bash
scripts/laptop_device_build.sh scons selfdrive/ui/ui
```
On macOS, once `.comma_sysroot` is present, plain `scons ...` auto-routes to this containerized device build.
Set `SP_DISABLE_AUTO_DEVICE_SCONS=1` to force native host `scons`.
## Quick checks
```bash
scripts/laptop_device_build.sh doctor
```
If `doctor` fails, fix the missing runtime/sysroot step before running `build`.
## One-command manager launch on desktop
`./launch_openpilot.sh` now auto-routes desktop/mac launches to the containerized larch64 manager path:
- runs `scripts/laptop_device_build.sh doctor`
- runs `setup` automatically if prerequisites are missing
- runs container manager launch, auto-building missing runtime artifacts first
Useful overrides:
- `SP_SKIP_DOCKER_AUTO_BUILD=1 ./launch_openpilot.sh` to skip auto-build.
- `SP_DOCKER_BUILD_JOBS=12 ./launch_openpilot.sh` to control build parallelism.
- `SP_FORCE_DEVICE_LAUNCH=1 ./launch_openpilot.sh` to force legacy device launch path.
Note:
- `manager` mode requires the container runtime machine to be `aarch64`.
- If your built image runs as `x86_64`, build mode still works for artifact generation, but manager launch in-container is not supported.
Preferred host-side shorthand commands on this branch:
```bash
./c3
./c4
./raybig
./dev replay
./dev cabana
./dev plotjuggler
```
These commands use the isolated `.host_runtime` cache so host-native artifacts do not churn the main tree.
Legacy direct script entrypoints still exist if needed:
```bash
scripts/launch_ui_desktop.sh
scripts/launch_ui_c4_desktop.sh
scripts/launch_ui_raybig_desktop.sh
```
+304
View File
@@ -0,0 +1,304 @@
# Train Speed-Limit Vision
This flow is for replacing the current imported `ayoubsa_best` checkpoint with a U.S.-focused detector plus a separate posted-speed classifier.
The intended recipe is:
1. bootstrap from public traffic-sign data
2. fine-tune on comma-specific bookmarked drives
3. export ONNX models
4. copy them onto the device for runtime testing
## Why Two Models
The detector and the value reader solve different problems.
- detector: find the sign and decide whether it is a regulatory speed-limit sign
- classifier: read the posted value from the cropped sign
This is a better fit for U.S. roads than a single detector with baked-in classes like `Speed Limit 10`, `Speed Limit 20`, `Speed Limit 30`, and so on.
## Suggested Public Data
- `LISA` for U.S. roadside sign geometry and sign style
- `ARTS` for U.S. MUTCD-style sign annotations and values
- `GLARE` for U.S. glare/lighting failures
Use public data to get the model into the right regime, then fine-tune on comma bookmarks and replay clips.
## Install Training Deps
From the repo root:
```bash
uv sync --extra speedvision
```
The runtime device does not need these packages. Only the training machine does.
## Initialize a Workspace
```bash
.venv/bin/python scripts/speed_limit_vision/init_workspace.py
```
This creates `.tmp/speed_limit_training` with:
- detector image/label folders
- classifier crop folders
- review/bookmark manifests
- raw-source manifests
- export and run directories
To keep the raw datasets off the internal disk, point `--workspace` at the SSD-backed workspace, for example:
```bash
.venv/bin/python scripts/speed_limit_vision/init_workspace.py \
--workspace /Volumes/T5/starpilot_speed_limit/workspace/speed_limit_training_clean
```
The workspace now also tracks:
- `manifests/raw_sources.csv`
- `manifests/public_detector_samples.csv`
- `manifests/public_classifier_samples.csv`
Those manifests are the provenance record for every imported public sample.
## Import Public Datasets
ARTS challenging subset:
```bash
.venv/bin/python scripts/speed_limit_vision/import_arts_challenging.py \
--workspace /Volumes/T5/starpilot_speed_limit/workspace/speed_limit_training_clean
```
This imports mapped speed-limit signs from the raw `challenging-dev.tar.gz` archive and appends detector/classifier provenance rows into the manifest CSVs.
GLARE and LISA should be downloaded into the SSD raw tree first:
- `/Volumes/T5/starpilot_speed_limit/raw/glare_official`
- `/Volumes/T5/starpilot_speed_limit/raw/lisa_official`
Then import them into the same workspace so the detector/classifier datasets stay source-traceable.
For GLARE, do not pull the whole Drive tree blindly. Use the filtered raw downloader so only the `Images/` and optional `Tracks/` files land on disk:
```bash
.venv/bin/python scripts/speed_limit_vision/download_glare_raw.py \
--workspace /Volumes/T5/starpilot_speed_limit/workspace/speed_limit_training_clean \
--output-root /Volumes/T5/starpilot_speed_limit/raw/glare_raw \
--prefix Images/ \
--resume
```
Then import the completed image tree:
```bash
.venv/bin/python scripts/speed_limit_vision/import_glare_images.py \
--workspace /Volumes/T5/starpilot_speed_limit/workspace/speed_limit_training_clean \
--overwrite
```
## Import Bookmarked Debug Sessions
After a drive, copy or mount the debug session directory locally, then import it:
```bash
.venv/bin/python scripts/speed_limit_vision/import_debug_sessions.py
```
Or point at specific sessions:
```bash
.venv/bin/python scripts/speed_limit_vision/import_debug_sessions.py 20260330_220102 20260330_223355
```
This writes:
- `review/bookmarks.csv`
- snapshot images under `review/images`
That manifest is the shortlist for labeling.
If the route only lives on comma connect, fetch it directly into the same clip layout the bookmark tools expect:
```bash
.venv/bin/python scripts/speed_limit_vision/download_connect_routes.py \
<dongle_id>/<route_log_id> \
--streams fcamera,qlog
```
This reads the JWT from `~/.comma/auth.json`, downloads the requested route files from comma connect, and updates:
- `/Volumes/T5/starpilot_speed_limit/live_route_clips/bookmark_windows/data/media/0/realdata/...` when the SSD is mounted, otherwise the same `.tmp/...` paths under the repo
- `/Volumes/T5/starpilot_speed_limit/live_routes_meta/qlog_mtimes.txt` when the SSD is mounted, otherwise `.tmp/live_routes_meta/qlog_mtimes.txt`
- `/Volumes/T5/starpilot_speed_limit/live_routes_meta/files.txt` when the SSD is mounted, otherwise `.tmp/live_routes_meta/files.txt`
For multiple routes, either pass several ids on the command line or use `--routes-file`.
## Evaluate Real Sign Lead-Ins
Bookmark stills are often too late. To score what matters, replay the real `fcamera.hevc` footage from the `5` seconds before each bookmark:
```bash
.venv/bin/python scripts/speed_limit_vision/evaluate_bookmark_leadins.py \
--json-out .tmp/live_route_clips/bookmark_windows_report.json
```
This produces a per-bookmark report of whether the current runtime saw anything in the usable sign approach window.
The evaluator now reads a local session-to-route map from `session_route_map.json` under the same `live_routes_meta` root. Keep that file local or on the SSD so personal route ids never need to live in the repo.
## Import Missed Lead-Ins for Labeling
Turn those lead-in misses into review frames and contact sheets:
```bash
.venv/bin/python scripts/speed_limit_vision/import_bookmark_leadins.py \
--mode misses
```
This writes:
- `review/bookmark_leadins.csv`
- sampled frames under `review/leadins/frames`
- contact sheets under `review/leadins/contact_sheets`
That review set is the right source for labeling missed `55 mph`, night, and town-sequence failures.
The bookmark/lead-in importers also accept source metadata fields such as region, device, and driver. Use those when importing debug sessions from multiple users so the comma-specific fine-tune can be sliced by contributor or geography instead of becoming one opaque pool.
To shrink that review set to the most promising frames per missed sign window:
```bash
.venv/bin/python scripts/speed_limit_vision/rank_bookmark_leadin_frames.py
```
This writes `review/bookmark_leadin_shortlist.csv` with the top-ranked sampled frames per bookmark.
## Build the Detector Dataset
Take the imported review images and move or copy the ones you want into:
- `detector/images/train`
- `detector/images/val`
Label them in YOLO detect format into:
- `detector/labels/train`
- `detector/labels/val`
Recommended classes:
- `regulatory_speed_limit`
- `advisory_speed_limit`
- `school_zone_speed_limit`
The dataset YAML is already generated at:
- `detector/dataset.yaml`
## Build the Value Classifier Dataset
Fill out:
- `classifier/value_labels.csv`
Columns:
- `image_path`: source image file
- `split`: `train` or `val`
- `speed_limit_mph`: posted value such as `25`, `35`, or `55`
- `bbox_index`: which YOLO box to crop if an image has multiple labeled signs
- `padding`: optional crop padding ratio
- `label_path`: optional explicit path to the YOLO label file
Then generate the classifier crop folders:
```bash
.venv/bin/python scripts/speed_limit_vision/build_value_dataset.py
```
This writes cropped sign images into:
- `classifier/train/<value>/...`
- `classifier/val/<value>/...`
## Train
Detector:
```bash
.venv/bin/python scripts/speed_limit_vision/train_detector.py --device mps
```
Classifier:
```bash
.venv/bin/python scripts/speed_limit_vision/train_value_classifier.py --device mps
```
Use `--device cpu`, `--device mps`, or a CUDA device string depending on the training host.
## Rebalance Toward Real Comma Data
If the detector starts overfitting to synthetic/public data, build a lighter rebalanced dataset that keeps all `real_*` detector images and samples the rest:
```bash
.venv/bin/python scripts/speed_limit_vision/rebalance_detector_dataset.py \
--workspace .tmp/speed_limit_training \
--max-other-train 3200
```
Then point the detector trainer at the generated YAML:
```bash
.venv/bin/python scripts/speed_limit_vision/train_detector.py \
--workspace .tmp/speed_limit_training \
--data .tmp/speed_limit_training/detector_rebalanced/dataset.yaml \
--device mps
```
This keeps validation unchanged while making retrains faster and more comma-biased.
## Export ONNX
```bash
.venv/bin/python scripts/speed_limit_vision/export_models.py \
--detector-weights .tmp/speed_limit_training/runs/detector/yolo11n-speed-limit-us/weights/best.pt \
--classifier-weights .tmp/speed_limit_training/runs/classifier/yolo11n-cls-speed-limit-us/weights/best.pt \
--install-repo-assets
```
That writes:
- `.tmp/speed_limit_training/exports/speed_limit_us_detector.onnx`
- `.tmp/speed_limit_training/exports/speed_limit_us_value_classifier.onnx`
And optionally copies them into:
- `starpilot/assets/vision_models`
## Copy to the Device
```bash
.venv/bin/python scripts/speed_limit_vision/install_models.py --host comma@192.168.3.110
```
The runtime already prefers `speed_limit_us_detector.onnx` plus `speed_limit_us_value_classifier.onnx` when both files exist in `starpilot/assets/vision_models`.
## Evaluate the Runtime Path
Run the real StarPilot runtime path, using the installed ONNX pair, against the known saved-frame cases:
```bash
.venv/bin/python scripts/speed_limit_vision/evaluate_runtime_cases.py --strict
```
For temporal behavior on a saved frame directory or route extract, replay the runtime directly:
```bash
.venv/bin/python scripts/replay_speed_limit_vision.py .tmp/vision_iter/seg10_5fps --frames-fps 5
```
-598
View File
@@ -1,598 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Dashy State Aggregation Daemon
Aggregates all cereal topics needed by dashy UI into a single dashyState message.
This reduces CPU overhead in webrtcd by doing JSON serialization once here
instead of serializing 14+ topics separately.
All display formatting (units, distances, times) is done here so the frontend
can be a pure display layer with no conversion logic.
Publishes: dashyState (pre-serialized JSON at 15Hz)
"""
import json
import cereal.messaging as messaging
from openpilot.common.params import Params
from openpilot.common.realtime import Ratekeeper
from openpilot.common.swaglog import cloudlog
from opendbc.car.common.conversions import Conversions
# Main loop rate
LOOP_RATE = 15 # Hz
# Downsample factor for modelV2 arrays (33 points -> 17 points)
DOWNSAMPLE_FACTOR = 2
# Unit conversion constants
M_TO_KM = 0.001
M_TO_MI = 0.000621371
M_TO_FT = 3.28084
# Global state (refreshed periodically)
_is_metric = True
_params = None
_car_params_cache = None
def _ensure_params():
"""Ensure Params instance exists."""
global _params
if _params is None:
_params = Params()
return _params
def refresh_metric_preference():
"""Refresh metric preference from params (called periodically)."""
global _is_metric
try:
_is_metric = _ensure_params().get_bool("IsMetric")
except Exception:
_is_metric = True
def get_car_params_from_params():
"""Read carParams from Params storage (for immediate availability at startup)."""
global _car_params_cache
if _car_params_cache is not None:
return _car_params_cache
try:
from cereal import car
cp_bytes = _ensure_params().get("CarParams")
if cp_bytes:
with car.CarParams.from_bytes(cp_bytes) as cp:
_car_params_cache = {
'openpilotLongitudinalControl': bool(cp.openpilotLongitudinalControl),
}
return _car_params_cache
except Exception:
pass
return {'openpilotLongitudinalControl': False}
def format_speed(speed_ms: float) -> str:
"""Format speed for display (m/s -> km/h or mph)."""
if _is_metric:
return f"{max(0, speed_ms * Conversions.MS_TO_KPH):.0f}"
return f"{max(0, speed_ms * Conversions.MS_TO_MPH):.0f}"
def format_speed_value(speed_ms: float) -> float:
"""Convert speed to display units (m/s -> km/h or mph)."""
if _is_metric:
return max(0, speed_ms * Conversions.MS_TO_KPH)
return max(0, speed_ms * Conversions.MS_TO_MPH)
def format_distance(meters: float) -> str:
"""Format distance for display."""
if meters <= 0:
return ""
if _is_metric:
if meters >= 1000:
return f"{meters * M_TO_KM:.1f} km"
return f"{meters:.0f} m"
else:
miles = meters * M_TO_MI
if miles >= 0.1:
return f"{miles:.1f} mi"
return f"{meters * M_TO_FT:.0f} ft"
def format_time(seconds: float) -> str:
"""Format time duration for display."""
if seconds <= 0:
return ""
minutes = int(seconds / 60)
if minutes < 60:
return f"{minutes} min"
hours = minutes // 60
mins = minutes % 60
if mins == 0:
return f"{hours} hr"
return f"{hours} hr {mins} min"
def get_speed_unit() -> str:
"""Get current speed unit string."""
return "km/h" if _is_metric else "mph"
def get_distance_unit() -> str:
"""Get current distance unit string."""
return "km" if _is_metric else "mi"
SET_SPEED_NA = 255
def get_cruise_speed(v_cruise_cluster: float) -> int:
"""Get cruise speed value for display.
Returns the set speed in display units (km/h or mph), or 255 if not set.
"""
if not (0 < v_cruise_cluster < SET_SPEED_NA):
return SET_SPEED_NA
set_speed = v_cruise_cluster
if not _is_metric:
set_speed *= Conversions.KPH_TO_MPH
return round(set_speed)
def downsample(arr):
"""Downsample list by factor."""
if not arr:
return []
return list(arr[::DOWNSAMPLE_FACTOR])
def safe_get(obj, attr, default=None):
"""Safely get attribute from object."""
try:
return getattr(obj, attr, default)
except Exception:
return default
def extract_car_state(sm):
"""Extract carState fields used by dashy."""
cs = sm['carState']
v_ego = float(cs.vEgo)
v_ego_cluster = float(cs.vEgoCluster)
# Get set speed from controlsState.vCruiseDEPRECATED (fallback to carState.vCruiseCluster)
v_cruise = float(cs.vCruiseCluster)
if 'controlsState' in sm.updated and sm.updated['controlsState']:
v_cruise = float(sm['controlsState'].vCruiseDEPRECATED)
set_speed = get_cruise_speed(v_cruise)
return {
'vEgo': v_ego,
'vEgoCluster': v_ego_cluster,
'gearShifter': str(cs.gearShifter),
'aEgo': float(cs.aEgo),
'steeringAngleDeg': float(cs.steeringAngleDeg),
'leftBlinker': bool(cs.leftBlinker),
'rightBlinker': bool(cs.rightBlinker),
'leftBlindspot': bool(cs.leftBlindspot),
'rightBlindspot': bool(cs.rightBlindspot),
# Pre-formatted display values
'speedDisplay': format_speed(v_ego),
'speedClusterDisplay': format_speed(v_ego_cluster) if v_ego_cluster > 0 else format_speed(v_ego),
'setSpeed': set_speed, # 255 = not set, otherwise display value
'speedUnit': get_speed_unit(),
}
def extract_selfdrive_state(sm):
"""Extract selfdriveState fields used by dashy."""
ss = sm['selfdriveState']
return {
'enabled': bool(ss.enabled),
'activeOverride': int(safe_get(ss, 'activeOverride', 0)),
'experimentalMode': bool(ss.experimentalMode),
'alertText1': str(ss.alertText1),
'alertText2': str(ss.alertText2),
'alertSize': str(ss.alertSize),
'alertStatus': str(ss.alertStatus),
}
def extract_device_state(sm):
"""Extract deviceState fields used by dashy."""
ds = sm['deviceState']
temp_c = float(safe_get(ds, 'maxTempC', 0))
# Pre-format temperature for display
if _is_metric:
temp_display = f"{temp_c:.0f}°" if temp_c > 0 else "--"
else:
temp_f = temp_c * 9 / 5 + 32
temp_display = f"{temp_f:.0f}°" if temp_c > 0 else "--"
return {
'cpuUsagePercent': list(ds.cpuUsagePercent) if ds.cpuUsagePercent else [],
'memoryUsagePercent': int(ds.memoryUsagePercent),
'maxTempC': temp_c,
'deviceType': str(ds.deviceType),
'tempDisplay': temp_display,
}
def extract_lead(lead, sm):
"""Extract lead vehicle data."""
d_rel = float(lead.dRel)
v_rel = float(lead.vRel)
y_rel = float(lead.yRel)
has_lead = bool(lead.status)
# Pre-format lead display values
if has_lead:
v_ego = float(sm['carState'].vEgo) if sm.valid['carState'] else 0
lead_speed_ms = max(0, v_ego + v_rel)
speed_display = format_speed(lead_speed_ms)
distance_display = f"{d_rel:.1f}m" if _is_metric else f"{d_rel * M_TO_FT:.0f}ft"
else:
speed_display = "--"
distance_display = "--"
return {
'status': has_lead,
'dRel': d_rel,
'yRel': y_rel,
'vRel': v_rel,
'speedDisplay': speed_display,
'distanceDisplay': distance_display,
}
def extract_radar_state(sm):
"""Extract radarState fields used by dashy."""
rs = sm['radarState']
return {
'leadOne': extract_lead(rs.leadOne, sm),
'leadTwo': extract_lead(rs.leadTwo, sm),
}
def extract_live_tracks(sm):
"""Extract liveTracks radar points for bird's eye view.
Filters out tracks that are already shown as leadOne or leadTwo.
Uses radarTrackId matching: when radarState matches a liveTrack to a lead,
radarTrackId changes from -1 (vision-only) to the track's ID.
"""
try:
lt = sm['liveTracks']
points = []
# Get lead vehicle radar track IDs to filter them out
# radarTrackId = -1 means vision-only (no radar match)
# radarTrackId >= 0 means matched to a radar track
lead_track_ids = set()
if sm.valid.get('radarState', False):
rs = sm['radarState']
if rs.leadOne.status and rs.leadOne.radarTrackId >= 0:
lead_track_ids.add(rs.leadOne.radarTrackId)
if rs.leadTwo.status and rs.leadTwo.radarTrackId >= 0:
lead_track_ids.add(rs.leadTwo.radarTrackId)
if hasattr(lt, 'points'):
for pt in lt.points:
# Skip if this track is already shown as a lead vehicle
if pt.trackId in lead_track_ids:
continue
points.append({
'd': float(pt.dRel),
'y': float(pt.yRel),
'v': float(pt.vRel),
'm': bool(pt.measured),
})
return {'points': points}
except Exception as e:
cloudlog.warning(f"extract_live_tracks error: {e}")
return {'points': []}
def extract_live_gps(sm):
"""Extract liveGPS fields used by dashy."""
gps = sm['liveGPS']
# Skip if no coordinates yet
if gps.latitude == 0 and gps.longitude == 0:
return None
return {
'latitude': float(gps.latitude),
'longitude': float(gps.longitude),
'bearingDeg': float(gps.bearingDeg),
'speed': float(gps.speed),
'gpsOK': bool(gps.gpsOK),
'horizontalAccuracy': float(gps.horizontalAccuracy),
'status': str(gps.status),
}
def extract_nav_instruction(sm):
"""Extract navInstruction fields used by dashy."""
nav = sm['navInstruction']
maneuver_dist = float(safe_get(nav, 'maneuverDistance', 0))
dist_remaining = float(safe_get(nav, 'distanceRemaining', 0))
time_remaining = float(safe_get(nav, 'timeRemaining', 0))
speed_limit_ms = float(safe_get(nav, 'speedLimit', 0))
return {
'valid': sm.valid['navInstruction'],
'maneuverDistance': maneuver_dist,
'maneuverPrimaryText': str(safe_get(nav, 'maneuverPrimaryText', '')),
'maneuverSecondaryText': str(safe_get(nav, 'maneuverSecondaryText', '')),
'maneuverType': str(safe_get(nav, 'maneuverType', '')),
'maneuverModifier': str(safe_get(nav, 'maneuverModifier', '')),
'distanceRemaining': dist_remaining,
'timeRemaining': time_remaining,
'timeRemainingTypical': float(safe_get(nav, 'timeRemainingTypical', 0)),
'speedLimit': speed_limit_ms,
'speedLimitSign': str(safe_get(nav, 'speedLimitSign', '')),
# Pre-formatted display values
'maneuverDistanceDisplay': format_distance(maneuver_dist),
'distanceRemainingDisplay': format_distance(dist_remaining),
'timeRemainingDisplay': format_time(time_remaining),
'speedLimitDisplay': format_speed(speed_limit_ms) if speed_limit_ms > 0 else '',
}
def extract_nav_instruction_ext(sm):
"""Extract navInstructionExt fields used by dashy (extended nav data)."""
nav_ext = sm['navInstructionExt']
# Extract allManeuvers list (with name field and formatted distance)
all_maneuvers = []
if hasattr(nav_ext, 'allManeuvers'):
for m in nav_ext.allManeuvers:
dist = float(safe_get(m, 'distance', 0))
all_maneuvers.append({
'distance': dist,
'distanceDisplay': format_distance(dist),
'type': str(safe_get(m, 'type', '')),
'modifier': str(safe_get(m, 'modifier', '')),
'name': str(safe_get(m, 'name', '')),
})
return {
'turnAngle': float(safe_get(nav_ext, 'turnAngle', 0)),
'turnCurvature': float(safe_get(nav_ext, 'turnCurvature', 0)),
'allManeuvers': all_maneuvers,
}
def extract_nav_route(sm):
"""Extract navRoute coordinates used by dashy."""
route = sm['navRoute']
coords = []
if hasattr(route, 'coordinates'):
for c in route.coordinates:
coords.append([float(c.longitude), float(c.latitude)])
return {
'coordinates': coords,
}
def extract_model_v2(sm):
"""Extract modelV2 fields used by dashy (downsampled)."""
model = sm['modelV2']
# Position
pos = model.position
position = {
'x': downsample(list(pos.x)),
'y': downsample(list(pos.y)),
'z': downsample(list(pos.z)),
}
# Lane lines (4 lines)
lane_lines = []
for line in model.laneLines:
lane_lines.append({
'x': downsample(list(line.x)),
'y': downsample(list(line.y)),
'z': downsample(list(line.z)),
})
# Road edges (2 edges)
road_edges = []
for edge in model.roadEdges:
road_edges.append({
'x': downsample(list(edge.x)),
'y': downsample(list(edge.y)),
'z': downsample(list(edge.z)),
})
return {
'position': position,
'laneLines': lane_lines,
'laneLineProbs': list(model.laneLineProbs) if hasattr(model, 'laneLineProbs') else [0, 0, 0, 0],
'roadEdges': road_edges,
'roadEdgeStds': list(model.roadEdgeStds) if hasattr(model, 'roadEdgeStds') else [1, 1],
}
def extract_live_calibration(sm):
"""Extract liveCalibration fields used by dashy."""
cal = sm['liveCalibration']
return {
'rpyCalib': list(cal.rpyCalib) if hasattr(cal, 'rpyCalib') and cal.rpyCalib else [],
'calStatus': str(cal.calStatus) if hasattr(cal, 'calStatus') else 'uncalibrated',
'height': list(cal.height) if hasattr(cal, 'height') else [],
}
def extract_longitudinal_plan(sm):
"""Extract longitudinalPlan fields used by dashy."""
lp = sm['longitudinalPlan']
return {
'allowThrottle': bool(safe_get(lp, 'allowThrottle', True)),
}
def extract_controls_state_ext(sm):
"""Extract controlsStateExt fields used by dashy."""
cse = sm['controlsStateExt']
return {
'alkaActive': bool(safe_get(cse, 'alkaActive', False)),
}
def extract_car_params(sm):
"""Extract carParams fields used by dashy."""
cp = sm['carParams']
return {
'openpilotLongitudinalControl': bool(safe_get(cp, 'openpilotLongitudinalControl', False)),
}
# =============================================================================
# TOPIC CONFIGURATION
# =============================================================================
# Single source of truth for all subscribed topics.
# Comment out a line to disable that topic entirely.
#
# Fields:
# extractor: function(sm) -> dict, extracts data from message
# rate: 'fast' = every frame when updated
# number = slow poll divider (e.g., LOOP_RATE = 1Hz)
# 'valid' = just track valid state, no extraction
# 'subscribe' = subscribed but extracted within other extractors
# default: initial cache value (None if not specified)
# =============================================================================
TOPICS = {
# Fast topics - extract every frame when updated
'carState': {'extractor': extract_car_state, 'rate': 'fast'},
'selfdriveState': {'extractor': extract_selfdrive_state, 'rate': 'fast'},
'radarState': {'extractor': extract_radar_state, 'rate': 'fast'},
'liveTracks': {'extractor': extract_live_tracks, 'rate': 'fast'},
'modelV2': {'extractor': extract_model_v2, 'rate': 'fast'},
'longitudinalPlan': {'extractor': extract_longitudinal_plan, 'rate': 'fast'},
'liveGPS': {'extractor': extract_live_gps, 'rate': 'fast'},
# Slow topics - poll at fixed intervals
'deviceState': {'extractor': extract_device_state, 'rate': LOOP_RATE // 2},
'liveCalibration': {'extractor': extract_live_calibration, 'rate': LOOP_RATE},
'navInstruction': {'extractor': extract_nav_instruction, 'rate': LOOP_RATE},
'navInstructionExt': {'extractor': extract_nav_instruction_ext, 'rate': LOOP_RATE},
'navRoute': {'extractor': extract_nav_route, 'rate': LOOP_RATE},
'carParams': {'extractor': extract_car_params, 'rate': LOOP_RATE * 2},
# Valid-only topics - just track valid state
'roadCameraState': {'rate': 'valid', 'default': False},
# Subscribe-only topics - subscribed but extracted within other extractors
'controlsState': {'rate': 'subscribe'},
# Optional/dragonpilot-specific topics - comment out to disable
'controlsStateExt': {'extractor': extract_controls_state_ext, 'rate': 'fast', 'default': {'alkaActive': False}},
}
def main():
cloudlog.info("dashyd: starting")
# Initialize metric preference
refresh_metric_preference()
# Derive services list from TOPICS config
services = list(TOPICS.keys())
sm = messaging.SubMaster(services)
pm = messaging.PubMaster(['dashyState'])
rk = Ratekeeper(LOOP_RATE)
# Initialize cache from TOPICS defaults (exclude subscribe-only topics)
cache = {t: cfg.get('default') for t, cfg in TOPICS.items() if cfg.get('rate') != 'subscribe'}
cache['carParams'] = get_car_params_from_params() # special: init from Params
# Build topic lists from TOPICS config
fast_topics = {t: cfg['extractor'] for t, cfg in TOPICS.items() if cfg.get('rate') == 'fast'}
slow_topics = {t: (cfg['extractor'], cfg['rate']) for t, cfg in TOPICS.items()
if isinstance(cfg.get('rate'), int)}
valid_topics = [t for t, cfg in TOPICS.items() if cfg.get('rate') == 'valid']
cache_dirty = True
frame_count = 0
while True:
sm.update(0)
frame_count += 1
# Refresh metric preference every ~2 seconds
if frame_count % (LOOP_RATE * 2) == 0:
refresh_metric_preference()
cache_dirty = True # Force re-format with new units
# Fast topics - extract when updated
for topic, extractor in fast_topics.items():
if sm.updated[topic]:
cache[topic] = extractor(sm)
cache_dirty = True
# Slow topics - extract at fixed intervals (ignore sm.updated)
for topic, (extractor, divider) in slow_topics.items():
if frame_count % divider == 0:
cache[topic] = extractor(sm)
cache_dirty = True
# Valid-only topics - just track valid state
for topic in valid_topics:
if sm.updated[topic]:
new_val = sm.valid[topic]
if cache[topic] != new_val:
cache[topic] = new_val
cache_dirty = True
# Only serialize and publish if something changed
if cache_dirty:
# Only publish when critical data exists (nav data can be null)
critical_ready = (
cache.get('carState') is not None and
cache.get('modelV2') is not None and
cache.get('selfdriveState') is not None
)
if critical_ready:
state = {
'ts': sm.logMonoTime['carState'],
'display': {
'isMetric': _is_metric,
'speedUnit': get_speed_unit(),
'distanceUnit': get_distance_unit(),
},
**cache, # include all cached topics
}
msg = messaging.new_message('dashyState')
msg.dashyState.json = json.dumps(state).encode()
pm.send('dashyState', msg)
cache_dirty = False
rk.keep_time()
if __name__ == "__main__":
main()
-423
View File
@@ -1,423 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
GPS Location Service - GPS + livePose fusion with Kalman filter.
- Position: 2D Kalman filter fusing GPS with livePose velocity
- Bearing: livePose yaw + GPS-calibrated offset (with slow drift correction)
"""
import numpy as np
from cereal import messaging, custom
from openpilot.common.params import Params
from openpilot.common.realtime import Ratekeeper
from openpilot.common.transformations.coordinates import LocalCoord
from openpilot.common.swaglog import cloudlog
from openpilot.common.gps import get_gps_location_service
def wrap_angle(x):
return np.arctan2(np.sin(x), np.cos(x))
class GPSKalman:
"""
3D Kalman filter for GPS fusion.
State: [north, east, yaw_offset] where yaw_offset calibrates livePose to true north.
Adapts automatically to GPS accuracy (ublox vs qcom).
"""
# Process noise
POS_NOISE = 0.5 # m²/s - position uncertainty growth
YAW_NOISE = 0.0001 # rad²/s - yaw offset drift (~0.6°/min)
def __init__(self):
self.x = np.zeros(3) # [north, east, yaw_offset]
self.P = np.diag([100.0, 100.0, 1.0]) # uncertainty (position high, yaw moderate)
def get_yaw(self, pose_yaw):
"""Get calibrated yaw from pose yaw + estimated offset."""
return wrap_angle(pose_yaw + self.x[2])
def predict(self, vel_ned, dt):
"""Predict state using velocity from livePose."""
# Position prediction
self.x[0] += vel_ned[0] * dt
self.x[1] += vel_ned[1] * dt
# yaw_offset: no change (constant), just add process noise
# Process noise - more yaw drift when stopped (gyro drift accumulates)
speed = np.linalg.norm(vel_ned[:2])
yaw_noise = self.YAW_NOISE if speed > 1.0 else self.YAW_NOISE * 10
Q = np.diag([self.POS_NOISE * dt, self.POS_NOISE * dt, yaw_noise * dt])
self.P += Q
def update_position(self, gps_ned, gps_accuracy):
"""Update position with GPS measurement."""
# Observation matrix: observe [north, east], not yaw_offset
H = np.array([[1, 0, 0],
[0, 1, 0]])
# Measurement noise from GPS accuracy
R = np.eye(2) * (gps_accuracy ** 2)
# Innovation
z = gps_ned[:2]
y = z - H @ self.x
# Kalman gain
S = H @ self.P @ H.T + R
det = S[0, 0] * S[1, 1] - S[0, 1] * S[1, 0]
if abs(det) < 1e-10:
return
S_inv = np.array([[S[1, 1], -S[0, 1]],
[-S[1, 0], S[0, 0]]]) / det
K = self.P @ H.T @ S_inv
# Update
self.x += K @ y
self.P = (np.eye(3) - K @ H) @ self.P
self._ensure_positive_definite()
def update_yaw(self, gps_bearing, pose_yaw, bearing_std):
"""Update yaw_offset with GPS bearing measurement."""
# Observation: yaw_offset = gps_bearing - pose_yaw
# H = [0, 0, 1] - we observe yaw_offset directly
H = np.array([[0, 0, 1]])
# Measurement noise from GPS bearing uncertainty
R = np.array([[bearing_std ** 2]])
# Expected yaw_offset from GPS
observed_offset = wrap_angle(gps_bearing - pose_yaw)
# Innovation (handle angle wrapping)
predicted_offset = self.x[2]
y = np.array([wrap_angle(observed_offset - predicted_offset)])
# Kalman gain
S = H @ self.P @ H.T + R
K = self.P @ H.T / S[0, 0]
# Update
self.x += (K @ y).flatten()
self.x[2] = wrap_angle(self.x[2]) # keep yaw_offset wrapped
self.P = (np.eye(3) - K @ H) @ self.P
self._ensure_positive_definite()
def _ensure_positive_definite(self):
"""Ensure covariance stays positive definite. Reinit if corrupted."""
self.P = (self.P + self.P.T) / 2
if np.any(np.diag(self.P) < 0):
cloudlog.warning("gpsd: negative covariance detected, reinitializing filter")
self.P = np.diag([100.0, 100.0, 1.0])
return
min_var = np.array([0.01, 0.01, 0.0001]) # minimum variances
for i in range(3):
self.P[i, i] = max(self.P[i, i], min_var[i])
def reset(self, pos, yaw_offset=None):
"""Reset to known position, optionally with yaw offset."""
self.x[0] = pos[0]
self.x[1] = pos[1]
if yaw_offset is not None:
self.x[2] = yaw_offset
self.P = np.diag([1.0, 1.0, 0.1]) # low uncertainty
else:
self.P = np.diag([1.0, 1.0, self.P[2, 2]]) # keep yaw uncertainty
@property
def pos(self):
"""Position [north, east]."""
return self.x[:2]
@property
def yaw_offset(self):
"""Yaw offset estimate."""
return self.x[2]
@property
def pos_uncertainty(self):
"""Position uncertainty (meters)."""
return np.sqrt(max(0.0, (self.P[0, 0] + self.P[1, 1]) / 2))
@property
def yaw_uncertainty(self):
"""Yaw offset uncertainty (radians)."""
return np.sqrt(max(0.0, self.P[2, 2]))
class LiveGPS:
"""
GPS + livePose fusion with 3D Kalman filter.
- Position: Kalman filter fusing GPS with livePose velocity
- Bearing: Kalman-estimated yaw_offset + livePose yaw
"""
GPS_MIN_SPEED = 5.0 # m/s (18 km/h) - need speed for reliable GPS bearing
GPS_MAX_ACCURACY = 50.0 # m - reject very bad GPS
BEARING_STD_BASE = 0.1 # rad (~6°) - base GPS bearing uncertainty
BEARING_STD_PER_ACC = 0.02 # rad per meter of GPS accuracy
def __init__(self):
# pose inputs
self.orientation_ned = np.zeros(3)
self.vel_device = np.zeros(3)
# gps inputs
self.gps = None
self.last_gps_t = 0.0
self.unix_timestamp_millis = 0
# Kalman filter: [north, east, yaw_offset]
self.origin = None # LocalCoord of first GPS fix
self.kf = GPSKalman() # 3D Kalman filter
self.altitude = 0.0 # altitude tracked separately (1D)
self.last_gps_update_t = 0.0 # track when we last updated Kalman with GPS
# timing
self.last_t = None
self.last_pose_yaw = None # for yaw rate calculation
self.live_pose_ok = False # for monitoring
# -----------------------------
# inputs
# -----------------------------
def handle_pose(self, pose):
if pose.orientationNED.valid:
self.orientation_ned[:] = [
pose.orientationNED.x,
pose.orientationNED.y,
pose.orientationNED.z,
]
if pose.velocityDevice.valid:
self.vel_device[:] = [
pose.velocityDevice.x,
pose.velocityDevice.y,
pose.velocityDevice.z,
]
# For monitoring
self.live_pose_ok = pose.orientationNED.valid and pose.velocityDevice.valid
def handle_gps(self, t, gps):
if gps.horizontalAccuracy > 0 and gps.horizontalAccuracy > self.GPS_MAX_ACCURACY:
return
if abs(gps.latitude) < 0.1 or abs(gps.longitude) < 0.1:
return
self.gps = gps
self.last_gps_t = t
self.unix_timestamp_millis = gps.unixTimestampMillis
# -----------------------------
# core update
# -----------------------------
def update(self, t):
dt = (t - self.last_t) if self.last_t else 0.05
self.last_t = t
if self.gps is None:
return
# initialize origin on first GPS
if self.origin is None:
self.origin = LocalCoord.from_geodetic([self.gps.latitude, self.gps.longitude, self.gps.altitude])
self.kf.reset(np.zeros(2))
self.altitude = self.gps.altitude
cloudlog.info(f"gpsd: origin set at {self.gps.latitude:.6f}, {self.gps.longitude:.6f}")
return
# get current yaw from Kalman (pose_yaw + estimated yaw_offset)
pose_yaw = self.orientation_ned[2]
yaw = self.kf.get_yaw(pose_yaw)
# transform velocity from device frame to NED
cos_yaw, sin_yaw = np.cos(yaw), np.sin(yaw)
vel_ned = np.array([
self.vel_device[0] * cos_yaw - self.vel_device[1] * sin_yaw,
self.vel_device[0] * sin_yaw + self.vel_device[1] * cos_yaw,
-self.vel_device[2]
])
# Kalman predict: propagate position using livePose velocity
# Skip prediction when stationary (GPS wanders, IMU drifts)
# Threshold 0.1 m/s - above noise (~0.04) but catches actual stops
speed = np.linalg.norm(self.vel_device[:2])
is_moving = speed > 0.1
if is_moving:
self.kf.predict(vel_ned, dt)
# Kalman update: only on NEW GPS data (not stale)
# Skip position update when stopped - prevents GPS wander from moving position
new_gps = self.last_gps_t > self.last_gps_update_t
if new_gps and is_moving:
self.last_gps_update_t = self.last_gps_t
gps_ned = self.origin.geodetic2ned([self.gps.latitude, self.gps.longitude, self.gps.altitude])
gps_accuracy = self.gps.horizontalAccuracy if self.gps.horizontalAccuracy > 0 else 15.0
# Check for hard reset (large error after tunnel/GPS loss)
error = np.linalg.norm(gps_ned[:2] - self.kf.pos)
if error > 100.0 and gps_accuracy < 20.0:
# GPS is good but we're far off - full reset (new origin, fresh Kalman)
yaw_err_deg = np.degrees(self.kf.yaw_uncertainty)
cloudlog.warning(f"gpsd: hard reset, error={error:.1f}m, yaw_unc={yaw_err_deg:.1f}°, gps_acc={gps_accuracy:.1f}m, livePoseOk={self.live_pose_ok}")
# Reset origin to current GPS - starts fresh
self.origin = LocalCoord.from_geodetic([self.gps.latitude, self.gps.longitude, self.gps.altitude])
self.kf = GPSKalman() # fresh Kalman filter
self.kf.reset(np.zeros(2)) # position (0,0) at new origin
self.altitude = self.gps.altitude
return # skip normal update this frame
else:
# Position update - adapts to GPS accuracy:
# - ublox (2-5m): high gain, trusts GPS
# - qcom (10-30m): low gain, trusts IMU more
self.kf.update_position(gps_ned, gps_accuracy)
# simple altitude tracking (no Kalman needed)
self.altitude = 0.9 * self.altitude + 0.1 * self.gps.altitude
# Yaw update (need speed for reliable GPS bearing)
if self.gps.speed > self.GPS_MIN_SPEED:
# compute yaw rate to check if driving straight
yaw_rate = 0.0
if self.last_pose_yaw is not None and dt > 0:
yaw_rate = abs(wrap_angle(pose_yaw - self.last_pose_yaw)) / dt
# GPS bearing is unreliable during turns - increase uncertainty
gps_bearing = np.radians(self.gps.bearingDeg)
bearing_std = self.BEARING_STD_BASE + self.BEARING_STD_PER_ACC * gps_accuracy
if yaw_rate > 0.1: # turning - GPS bearing lags
bearing_std *= 3.0
self.kf.update_yaw(gps_bearing, pose_yaw, bearing_std)
self.last_pose_yaw = pose_yaw
# -----------------------------
# output
# -----------------------------
def get_msg(self, log_mono_time):
msg = messaging.new_message("liveGPS", valid=True)
msg.logMonoTime = log_mono_time
out = msg.liveGPS
t = log_mono_time * 1e-9
gps_fresh = self.gps is not None and (t - self.last_gps_t) < 5.0
pos_initialized = self.origin is not None
# yaw is calibrated when uncertainty < 0.5 rad (~30°)
yaw_calibrated = self.kf.yaw_uncertainty < 0.5
if pos_initialized:
# position from Kalman filter (NED -> geodetic)
pos_ned = np.array([self.kf.pos[0], self.kf.pos[1], self.altitude])
geodetic = self.origin.ned2geodetic(pos_ned)
out.latitude = float(geodetic[0])
out.longitude = float(geodetic[1])
out.altitude = float(geodetic[2])
out.speed = float(np.linalg.norm(self.vel_device[:2]))
# horizontalAccuracy from Kalman uncertainty
out.horizontalAccuracy = float(self.kf.pos_uncertainty)
out.verticalAccuracy = float(self.gps.verticalAccuracy) if hasattr(self.gps, 'verticalAccuracy') and self.gps.verticalAccuracy > 0 else 15.0
# bearing from Kalman (pose_yaw + estimated yaw_offset)
has_livePose = np.any(self.orientation_ned != 0)
if yaw_calibrated and has_livePose and gps_fresh:
yaw = self.kf.get_yaw(self.orientation_ned[2])
out.bearingDeg = float(np.degrees(yaw) % 360)
out.status = custom.LiveGPS.Status.valid
else:
# fallback to raw GPS bearing
out.bearingDeg = float(self.gps.bearingDeg)
out.status = custom.LiveGPS.Status.uncalibrated
elif self.gps is not None:
# have GPS but not initialized yet - pass through raw
out.latitude = float(self.gps.latitude)
out.longitude = float(self.gps.longitude)
out.altitude = float(self.gps.altitude)
out.speed = float(self.gps.speed)
out.bearingDeg = float(self.gps.bearingDeg)
out.horizontalAccuracy = float(self.gps.horizontalAccuracy) if self.gps.horizontalAccuracy > 0 else 20.0
out.status = custom.LiveGPS.Status.uncalibrated
else:
out.status = custom.LiveGPS.Status.uninitialized
# gpsOK = position is usable (bearing calibration tracked separately via status)
out.gpsOK = gps_fresh and pos_initialized
out.unixTimestampMillis = self.unix_timestamp_millis
out.lastGpsTimestamp = int(self.last_gps_t * 1e9)
# livePose health - for monitoring
out.livePoseOk = self.live_pose_ok
return msg
def main():
import os
params = Params()
# EXT=1 forces gpsLocationExternal (ublox), EXT=0 forces gpsLocation (qcom)
ext_override = os.environ.get("EXT")
if ext_override == "1":
gps_service = "gpsLocationExternal"
cloudlog.info("gpsd: EXT=1, using gpsLocationExternal (ublox)")
elif ext_override == "0":
gps_service = "gpsLocation"
cloudlog.info("gpsd: EXT=0, using gpsLocation (qcom)")
else:
gps_service = get_gps_location_service(params)
pm = messaging.PubMaster(["liveGPS"])
sm = messaging.SubMaster([gps_service, "livePose"], ignore_alive=[gps_service])
gps = LiveGPS()
rk = Ratekeeper(20)
while True:
try:
sm.update(0)
if sm.logMonoTime["livePose"] > 0:
t = sm.logMonoTime["livePose"] * 1e-9
log_mono_time = sm.logMonoTime["livePose"]
else:
log_mono_time = int(rk.frame * 1e9 / 20)
t = log_mono_time * 1e-9
if sm.updated[gps_service]:
gps.handle_gps(t, sm[gps_service])
if sm.updated["livePose"] and sm.valid["livePose"]:
gps.handle_pose(sm["livePose"])
gps.update(t)
pm.send("liveGPS", gps.get_msg(log_mono_time))
except Exception:
cloudlog.exception("gpsd: error in main loop")
rk.keep_time()
if __name__ == "__main__":
main()
-30
View File
@@ -1,30 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
dragonpilot Map-Aware Assist (MAA)
Navigation-assisted driving module that provides:
- Route tracking and turn-by-turn navigation
- Turn speed limits based on road curvature
- Blinker-based turn desire for lane changes
- Physics-based acceleration limiting
"""
-657
View File
@@ -1,657 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Navigation helpers for dragonpilot MAA.
Coordinate math, route parsing, and curvature computation utilities.
"""
from __future__ import annotations
import json
import math
from typing import Any, cast
from opendbc.car.common.conversions import Conversions
from openpilot.common.params import Params
DIRECTIONS = ('left', 'right', 'straight')
MODIFIABLE_DIRECTIONS = ('left', 'right')
EARTH_MEAN_RADIUS = 6371007.2
# Speed unit conversions to m/s
SPEED_CONVERSIONS = {
'km/h': Conversions.KPH_TO_MS,
'mph': Conversions.MPH_TO_MS,
}
class Coordinate:
def __init__(self, latitude: float, longitude: float) -> None:
self.latitude = latitude
self.longitude = longitude
self.annotations: dict[str, float] = {}
@classmethod
def from_mapbox_tuple(cls, t: tuple[float, float]) -> Coordinate:
return cls(t[1], t[0])
@classmethod
def from_osrm_tuple(cls, t: list[float]) -> Coordinate:
"""OSRM uses [lon, lat] order."""
return cls(t[1], t[0])
def as_dict(self) -> dict[str, float]:
return {'latitude': self.latitude, 'longitude': self.longitude}
def __str__(self) -> str:
return f'Coordinate({self.latitude}, {self.longitude})'
def __repr__(self) -> str:
return self.__str__()
def __eq__(self, other) -> bool:
if not isinstance(other, Coordinate):
return False
return self.latitude == other.latitude and self.longitude == other.longitude
def __hash__(self) -> int:
return hash((self.latitude, self.longitude))
def __sub__(self, other: Coordinate) -> Coordinate:
return Coordinate(self.latitude - other.latitude, self.longitude - other.longitude)
def __add__(self, other: Coordinate) -> Coordinate:
return Coordinate(self.latitude + other.latitude, self.longitude + other.longitude)
def __mul__(self, c: float) -> Coordinate:
return Coordinate(self.latitude * c, self.longitude * c)
def dot(self, other: Coordinate) -> float:
return self.latitude * other.latitude + self.longitude * other.longitude
def distance_to(self, other: Coordinate) -> float:
"""Haversine distance in meters."""
dlat = math.radians(other.latitude - self.latitude)
dlon = math.radians(other.longitude - self.longitude)
haversine_dlat = math.sin(dlat / 2.0)
haversine_dlat *= haversine_dlat
haversine_dlon = math.sin(dlon / 2.0)
haversine_dlon *= haversine_dlon
y = haversine_dlat \
+ math.cos(math.radians(self.latitude)) \
* math.cos(math.radians(other.latitude)) \
* haversine_dlon
x = 2 * math.asin(math.sqrt(y))
return x * EARTH_MEAN_RADIUS
def bearing_to(self, other: Coordinate) -> float:
"""Bearing to other coordinate in degrees (0-360)."""
lat1, lat2 = math.radians(self.latitude), math.radians(other.latitude)
dlon = math.radians(other.longitude - self.longitude)
x = math.sin(dlon) * math.cos(lat2)
y = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dlon)
bearing = math.degrees(math.atan2(x, y))
return (bearing + 360) % 360
def minimum_distance(a: Coordinate, b: Coordinate, p: Coordinate) -> float:
"""Minimum distance from point p to line segment ab."""
if a.distance_to(b) < 0.01:
return a.distance_to(p)
ap = p - a
ab = b - a
t = max(0.0, min(1.0, ap.dot(ab) / ab.dot(ab)))
projection = a + ab * t
return projection.distance_to(p)
def project_onto_segment(a: Coordinate, b: Coordinate, p: Coordinate) -> tuple[Coordinate, float, float]:
"""Project point p onto line segment ab (snap to road).
Returns:
(projected_point, t, distance) where:
- projected_point: the closest point on segment ab to p
- t: parameter 0-1 indicating position along segment (0=a, 1=b)
- distance: distance from p to the projected point
"""
seg_len = a.distance_to(b)
if seg_len < 0.01:
return a, 0.0, a.distance_to(p)
ap = p - a
ab = b - a
t = max(0.0, min(1.0, ap.dot(ab) / ab.dot(ab)))
projection = a + ab * t
return projection, t, projection.distance_to(p)
def distance_along_geometry(geometry: list[Coordinate], pos: Coordinate) -> float:
"""Calculate distance traveled along geometry from start to closest point to pos."""
if len(geometry) <= 2:
return geometry[0].distance_to(pos)
total_distance = 0.0
total_distance_closest = 0.0
closest_distance = 1e9
for i in range(len(geometry) - 1):
d = minimum_distance(geometry[i], geometry[i + 1], pos)
if d < closest_distance:
closest_distance = d
total_distance_closest = total_distance + geometry[i].distance_to(pos)
total_distance += geometry[i].distance_to(geometry[i + 1])
return total_distance_closest
def normalize_angle(angle: float) -> float:
"""Normalize angle to -180 to 180 degrees."""
while angle > 180:
angle -= 360
while angle < -180:
angle += 360
return angle
def calculate_turn_angle(
current_geometry: list[Coordinate],
next_geometry: list[Coordinate],
samples: int = 3
) -> float:
"""
Calculate turn angle between two road segments.
Uses the bearing of the end of current geometry vs the bearing
of the start of next geometry to determine turn angle.
Args:
current_geometry: Coordinates of current road segment (before turn)
next_geometry: Coordinates of next road segment (after turn)
samples: Number of points to use for bearing calculation (for stability)
Returns:
Turn angle in degrees. Positive = left turn, negative = right turn.
Example: 90 = 90° left turn, -90 = 90° right turn
"""
if len(current_geometry) < 2 or len(next_geometry) < 2:
return 0.0
# Get bearing of current road (last segment)
# Use last few points for stability
start_idx = max(0, len(current_geometry) - samples)
current_bearing = current_geometry[start_idx].bearing_to(current_geometry[-1])
# Get bearing of next road (first segment)
# Use first few points for stability
end_idx = min(samples, len(next_geometry) - 1)
next_bearing = next_geometry[0].bearing_to(next_geometry[end_idx])
# Calculate turn angle (positive = left, negative = right)
# This matches openpilot convention where left is positive curvature
angle = normalize_angle(current_bearing - next_bearing)
return angle
def coordinate_from_param(key: str, params: Params = None) -> Coordinate | None:
"""Read coordinate from params.
Handles both JSON type params (returns dict) and STRING type params (returns string).
"""
if params is None:
params = Params()
try:
value = params.get(key)
if value is None:
return None
# JSON type params return dict directly, STRING type needs json.loads()
if isinstance(value, str):
pos = json.loads(value)
else:
pos = value
if 'latitude' not in pos or 'longitude' not in pos:
return None
return Coordinate(pos['latitude'], pos['longitude'])
except (json.JSONDecodeError, KeyError, TypeError):
return None
def find_closest_point_on_route(pos: Coordinate, route_coords: list[Coordinate]) -> tuple[int, float]:
"""Find closest point index and distance on route."""
if not route_coords:
return 0, float('inf')
min_dist = float('inf')
closest_idx = 0
for i in range(len(route_coords) - 1):
# Check distance to segment, not just point
d = minimum_distance(route_coords[i], route_coords[i + 1], pos)
if d < min_dist:
min_dist = d
closest_idx = i
return closest_idx, min_dist
def calculate_remaining_distance(route_coords: list[Coordinate], from_idx: int) -> float:
"""Calculate remaining distance along route from index."""
if from_idx >= len(route_coords) - 1:
return 0.0
total = 0.0
for i in range(from_idx, len(route_coords) - 1):
total += route_coords[i].distance_to(route_coords[i + 1])
return total
# --- Instruction Parsing ---
def string_to_direction(direction: str) -> str:
"""Convert direction string to standard format."""
for d in DIRECTIONS:
if d in direction:
if 'slight' in direction and d in MODIFIABLE_DIRECTIONS:
return 'slight' + d.capitalize()
return d
return 'none'
def maxspeed_to_ms(maxspeed: dict[str, str | float]) -> float:
"""Convert speed limit dict to m/s."""
unit = cast(str, maxspeed['unit'])
speed = cast(float, maxspeed['speed'])
return SPEED_CONVERSIONS.get(unit, 1.0) * speed
def field_valid(dat: dict, field: str) -> bool:
"""Check if field exists and is not None."""
return field in dat and dat[field] is not None
def parse_banner_instructions(banners: Any, distance_to_maneuver: float = 0.0) -> dict[str, Any] | None:
"""Parse Mapbox/OSRM banner instructions."""
if not banners or not len(banners):
return None
instruction = {}
# A segment can contain multiple banners, find one that we need to show now
current_banner = banners[0]
for banner in banners:
if distance_to_maneuver < banner.get('distanceAlongGeometry', 0):
current_banner = banner
# Only show banner when close enough to maneuver
instruction['showFull'] = distance_to_maneuver < current_banner.get('distanceAlongGeometry', 0)
# Primary
p = current_banner.get('primary', {})
if field_valid(p, 'text'):
instruction['maneuverPrimaryText'] = p['text']
if field_valid(p, 'type'):
instruction['maneuverType'] = p['type']
if field_valid(p, 'modifier'):
instruction['maneuverModifier'] = p['modifier']
# Secondary
if field_valid(current_banner, 'secondary'):
instruction['maneuverSecondaryText'] = current_banner['secondary']['text']
# Lane lines
if field_valid(current_banner, 'sub'):
lanes = []
for component in current_banner['sub'].get('components', []):
if component.get('type') != 'lane':
continue
lane = {
'active': component.get('active', False),
'directions': [string_to_direction(d) for d in component.get('directions', [])],
}
if field_valid(component, 'active_direction'):
lane['activeDirection'] = string_to_direction(component['active_direction'])
lanes.append(lane)
instruction['lanes'] = lanes
return instruction
def parse_osrm_step(step: dict) -> dict[str, Any]:
"""Parse OSRM route step into instruction format."""
maneuver = step.get('maneuver', {})
instruction = {
'distance': step.get('distance', 0),
'duration': step.get('duration', 0),
'name': step.get('name', ''),
'maneuverType': maneuver.get('type', ''),
'maneuverModifier': maneuver.get('modifier', ''),
'location': maneuver.get('location', []), # [lon, lat]
}
return instruction
def classify_maneuver(maneuver_type: str, maneuver_modifier: str) -> str:
"""
Classify OSRM maneuver as 'turn' or 'laneChange'.
Highway exits/forks use laneChange desires (smoother).
Intersection turns use turn desires (sharper).
OSRM maneuver types:
- turn: regular intersection turn
- fork: highway split/junction
- off ramp: highway exit
- on ramp: highway entrance
- merge: merging lanes
- roundabout turn: roundabout exit
- exit roundabout: leaving roundabout
- continue: straight (no maneuver)
- depart/arrive: start/end
Returns:
'turn' for intersection turns
'laneChange' for highway exits/forks
'none' for straight/no maneuver
"""
maneuver_type = maneuver_type.lower()
maneuver_modifier = maneuver_modifier.lower()
# Highway exits and forks -> lane change desire
LANE_CHANGE_TYPES = {
'fork', # highway fork/split
'off ramp', # highway exit
'on ramp', # highway entrance
'merge', # merging
'exit rotary', # leaving rotary
'exit roundabout',# leaving roundabout
}
# Intersection turns -> turn desire
TURN_TYPES = {
'turn', # regular turn
'end of road', # forced turn at end of road
'rotary', # entering rotary
'roundabout', # entering roundabout
'roundabout turn',# turn within roundabout
}
# No maneuver
NO_MANEUVER_TYPES = {
'continue',
'depart',
'arrive',
'new name',
'notification',
}
if maneuver_type in LANE_CHANGE_TYPES:
return 'laneChange'
if maneuver_type in TURN_TYPES:
# For turns, check modifier - slight turns at highway speeds might be lane changes
if 'slight' in maneuver_modifier:
# Slight turns could be either - default to turn but could be lane change
# CarrotPilot uses additional context like road speed limit
return 'turn'
return 'turn'
if maneuver_type in NO_MANEUVER_TYPES:
return 'none'
# Unknown type - default to turn
return 'turn'
def get_turn_direction(maneuver_modifier: str) -> str:
"""
Get turn direction from OSRM maneuver modifier.
Returns:
'left', 'right', or 'none'
"""
modifier = maneuver_modifier.lower()
if 'left' in modifier:
return 'left'
if 'right' in modifier:
return 'right'
return 'none'
# --- Curvature Computation ---
def compute_path_curvature(pos: Coordinate, bearing: float, route_coords: list[Coordinate],
closest_idx: int, v_ego: float, lookahead_time: float = 2.5) -> float:
"""
Compute desired curvature from route geometry using pure pursuit.
Args:
pos: Current position
bearing: Current heading in degrees
route_coords: List of route coordinates
closest_idx: Index of closest point on route
v_ego: Current vehicle speed m/s
lookahead_time: How far ahead to look in seconds
Returns:
Desired curvature in 1/m (positive = left turn, negative = right turn)
"""
if not route_coords or closest_idx >= len(route_coords) - 1:
return 0.0
# Calculate lookahead distance (min 30m, based on speed)
lookahead_dist = max(v_ego * lookahead_time, 30.0)
# Find lookahead point along route
dist_traveled = 0.0
lookahead_idx = closest_idx
for i in range(closest_idx, len(route_coords) - 1):
segment_dist = route_coords[i].distance_to(route_coords[i + 1])
if dist_traveled + segment_dist >= lookahead_dist:
# Interpolate within segment
remaining = lookahead_dist - dist_traveled
ratio = remaining / segment_dist if segment_dist > 0 else 0
lookahead_idx = i
# Could interpolate here, but using next point is simpler
if ratio > 0.5:
lookahead_idx = i + 1
break
dist_traveled += segment_dist
lookahead_idx = i + 1
if lookahead_idx >= len(route_coords):
lookahead_idx = len(route_coords) - 1
lookahead_point = route_coords[lookahead_idx]
# Calculate desired heading to lookahead point
desired_bearing = pos.bearing_to(lookahead_point)
# Calculate heading error (normalized to -180 to 180)
heading_error = desired_bearing - bearing
if heading_error > 180:
heading_error -= 360
elif heading_error < -180:
heading_error += 360
# Convert heading error to yaw (radians)
yaw_error = math.radians(heading_error)
# Distance to lookahead point
dist_to_lookahead = pos.distance_to(lookahead_point)
if dist_to_lookahead < 1.0:
return 0.0
# Pure pursuit curvature: 2 * sin(yaw_error) / lookahead_distance
# Note: Negative because heading_error > 0 means target is to the RIGHT (clockwise),
# and right turn should produce negative curvature
curvature = -2.0 * math.sin(yaw_error) / dist_to_lookahead
# Clamp to reasonable values (max ~7m radius turn)
MAX_CURVATURE = 0.15
return max(-MAX_CURVATURE, min(MAX_CURVATURE, curvature))
def smooth_curvature(new_curv: float, prev_curv: float, alpha: float = 0.3) -> float:
"""Exponential smoothing for curvature."""
return alpha * new_curv + (1 - alpha) * prev_curv
def curvature_to_radius(curvature: float) -> float:
"""Convert curvature to turn radius in meters."""
if abs(curvature) < 0.001:
return float('inf')
return 1.0 / abs(curvature)
def compute_turn_angle_at_index(route_coords: list[Coordinate], turn_idx: int,
sample_dist: float = 20.0) -> float:
"""
Compute turn angle at a specific point on the route.
Uses points sample_dist meters before and after for stability.
Returns angle in degrees (positive = left, negative = right).
"""
if turn_idx < 1 or turn_idx >= len(route_coords) - 1:
return 0.0
# Find points ~sample_dist before and after the turn for stable bearing
before_idx = turn_idx
after_idx = turn_idx
# Walk backwards to find before point
dist = 0.0
for i in range(turn_idx, 0, -1):
dist += route_coords[i].distance_to(route_coords[i - 1])
if dist >= sample_dist:
before_idx = i - 1
break
else:
before_idx = 0
# Walk forwards to find after point
dist = 0.0
for i in range(turn_idx, len(route_coords) - 1):
dist += route_coords[i].distance_to(route_coords[i + 1])
if dist >= sample_dist:
after_idx = i + 1
break
else:
after_idx = len(route_coords) - 1
if before_idx == turn_idx or after_idx == turn_idx:
return 0.0
# Calculate bearings using the sampled points
p1 = route_coords[before_idx]
p2 = route_coords[turn_idx]
p3 = route_coords[after_idx]
bearing1 = p1.bearing_to(p2)
bearing2 = p2.bearing_to(p3)
# Angle difference (positive = left, negative = right)
angle = bearing1 - bearing2
# Normalize to -180 to 180
while angle > 180:
angle -= 360
while angle < -180:
angle += 360
return angle
def compute_turn_curvature_at_index(route_coords: list[Coordinate], turn_idx: int,
sample_dist: float = 15.0) -> float:
"""
Compute curvature at turn point using three-point circle fitting.
Returns curvature in 1/m (positive = left, negative = right).
"""
if turn_idx < 1 or turn_idx >= len(route_coords) - 1:
return 0.0
# Find points sample_dist before and after
before_idx = turn_idx
after_idx = turn_idx
dist = 0.0
for i in range(turn_idx, 0, -1):
dist += route_coords[i].distance_to(route_coords[i - 1])
if dist >= sample_dist:
before_idx = i - 1
break
dist = 0.0
for i in range(turn_idx, len(route_coords) - 1):
dist += route_coords[i].distance_to(route_coords[i + 1])
if dist >= sample_dist:
after_idx = i + 1
break
if before_idx == turn_idx or after_idx == turn_idx:
return 0.0
# Three points for circle fitting
p1 = route_coords[before_idx]
p2 = route_coords[turn_idx]
p3 = route_coords[after_idx]
# Convert to local meters (approximate)
lat_center = p2.latitude
lon_scale = math.cos(math.radians(lat_center))
m_per_deg = 111319.5 # meters per degree latitude
x1 = (p1.longitude - p2.longitude) * lon_scale * m_per_deg
y1 = (p1.latitude - p2.latitude) * m_per_deg
x2 = 0.0
y2 = 0.0
x3 = (p3.longitude - p2.longitude) * lon_scale * m_per_deg
y3 = (p3.latitude - p2.latitude) * m_per_deg
# Calculate curvature using cross product / (product of distances)
d12 = math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
d13 = math.sqrt((x3 - x1)**2 + (y3 - y1)**2)
d23 = math.sqrt((x3 - x2)**2 + (y3 - y2)**2)
if d12 < 0.1 or d13 < 0.1 or d23 < 0.1:
return 0.0
# Cross product (p2-p1) x (p3-p1) - z component only
cross = (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1)
curvature = 2.0 * cross / (d12 * d13 * d23)
# Clamp to reasonable values
MAX_CURVATURE = 0.2 # ~5m radius
return max(-MAX_CURVATURE, min(MAX_CURVATURE, curvature))
-47
View File
@@ -1,47 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
dragonpilot Map-Aware Assist Library
Core modules:
- maa_desire: Simple turn desire logic (blinker confirmation, lane change blocking, RHD/LHD)
- model_helper: Helper class for modeld integration
- longitudinal_helper: Planner integration for speed/accel limiting
"""
from dragonpilot.dashy.maa.lib.maa_desire import (
should_block_lane_change,
get_turn_desire,
is_crossing_turn,
get_turn_trigger_distance,
)
from dragonpilot.dashy.maa.lib.model_helper import ModelHelper
from dragonpilot.dashy.maa.lib.longitudinal_helper import LongitudinalHelper
__all__ = [
'should_block_lane_change',
'get_turn_desire',
'is_crossing_turn',
'get_turn_trigger_distance',
'ModelHelper',
'LongitudinalHelper',
]
-238
View File
@@ -1,238 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Acceleration Limiter
Physics-based acceleration limiting for turns.
Algorithm adapted from CarrotPilot (https://github.com/ajouatom/openpilot)
Credit: carrotpilot team for the physics-based lateral acceleration approach.
The key insight is that total acceleration is limited by tire grip:
a_total² = a_x² + a_y² a_max²
Where:
a_x = longitudinal acceleration (throttle/brake)
a_y = lateral acceleration (from turning) = × curvature
This means during turns, we must reduce longitudinal acceleration
to stay within the grip circle.
"""
import math
from dataclasses import dataclass
from typing import Tuple, Optional
@dataclass
class AccelLimits:
"""Acceleration limits."""
min_accel: float # m/s² (negative = braking)
max_accel: float # m/s² (positive = acceleration)
@dataclass
class AccelLimiterState:
"""Current state of acceleration limiter."""
v_ego: float = 0.0
curvature: float = 0.0
lateral_accel: float = 0.0
available_long_accel: float = 0.0
is_limiting: bool = False
class AccelLimiter:
"""
Physics-based acceleration limiter for turns.
Uses the friction circle concept: total acceleration magnitude
is limited by tire grip. During turns, lateral acceleration
consumes part of this budget, leaving less for longitudinal
acceleration.
Adapted from CarrotPilot's limit_accel_in_turns function.
"""
# Default lateral acceleration limit (m/s²)
# Comfortable limit is ~2-3 m/s², sporty is ~4 m/s²
DEFAULT_LAT_ACCEL_MAX = 2.5
# Lookup table for max total acceleration vs speed
# Higher speeds = lower max lateral accel for comfort
A_TOTAL_MAX_BP = [0., 10., 20., 30., 40.] # m/s
A_TOTAL_MAX_V = [3.0, 2.8, 2.5, 2.2, 2.0] # m/s²
def __init__(
self,
lat_accel_max: float = DEFAULT_LAT_ACCEL_MAX,
comfort_mode: bool = True
):
"""
Initialize acceleration limiter.
Args:
lat_accel_max: Maximum allowed lateral acceleration (m/)
comfort_mode: If True, use speed-dependent limits
"""
self.lat_accel_max = lat_accel_max
self.comfort_mode = comfort_mode
self.state = AccelLimiterState()
def get_max_total_accel(self, v_ego: float) -> float:
"""
Get maximum total acceleration for current speed.
In comfort mode, reduces limit at higher speeds.
"""
if not self.comfort_mode:
return self.lat_accel_max
# Interpolate from lookup table
import numpy as np
return np.interp(v_ego, self.A_TOTAL_MAX_BP, self.A_TOTAL_MAX_V)
def compute_lateral_accel(self, v_ego: float, curvature: float) -> float:
"""
Compute lateral acceleration from speed and curvature.
a_y = × κ
Args:
v_ego: Vehicle speed in m/s
curvature: Road curvature in 1/m
Returns:
Lateral acceleration in m/
"""
return v_ego * v_ego * abs(curvature)
def compute_available_long_accel(
self,
v_ego: float,
curvature: float,
a_max: Optional[float] = None
) -> float:
"""
Compute available longitudinal acceleration given current turn.
Uses friction circle: a_x² + a_y² a_max²
Solving for a_x: a_x = sqrt(a_max² - a_y²)
Adapted from CarrotPilot's limit_accel_in_turns.
Args:
v_ego: Vehicle speed in m/s
curvature: Road curvature in 1/m
a_max: Override for max total acceleration
Returns:
Maximum available longitudinal acceleration in m/
"""
if a_max is None:
a_max = self.get_max_total_accel(v_ego)
# Compute lateral acceleration
a_y = self.compute_lateral_accel(v_ego, curvature)
a_y_abs = abs(a_y)
# Update state
self.state.v_ego = v_ego
self.state.curvature = curvature
self.state.lateral_accel = a_y
# Check if lateral accel exceeds limit
if a_y_abs >= a_max:
# Already at or over limit - no longitudinal accel available
self.state.available_long_accel = 0.0
self.state.is_limiting = True
return 0.0
# Compute remaining budget for longitudinal acceleration
a_x_available = math.sqrt(a_max * a_max - a_y_abs * a_y_abs)
self.state.available_long_accel = a_x_available
self.state.is_limiting = a_x_available < a_max * 0.9 # Limiting if < 90% available
return a_x_available
def limit_accel(
self,
v_ego: float,
curvature: float,
accel_limits: AccelLimits,
a_max: Optional[float] = None
) -> AccelLimits:
"""
Apply turn-based acceleration limiting.
Args:
v_ego: Vehicle speed in m/s
curvature: Road curvature in 1/m
accel_limits: Current acceleration limits
a_max: Override for max total acceleration
Returns:
New AccelLimits with turn limiting applied
"""
a_x_available = self.compute_available_long_accel(v_ego, curvature, a_max)
# Clamp max acceleration to available budget
new_max = min(accel_limits.max_accel, a_x_available)
# Don't limit braking as much - we may need to slow down
# But still apply some limit for comfort
new_min = max(accel_limits.min_accel, -a_x_available * 1.5)
return AccelLimits(
min_accel=new_min,
max_accel=new_max
)
def limit_accel_tuple(
self,
v_ego: float,
curvature: float,
accel_limits: Tuple[float, float],
a_max: Optional[float] = None
) -> Tuple[float, float]:
"""
Apply turn-based acceleration limiting (tuple interface).
For compatibility with existing planner code.
Args:
v_ego: Vehicle speed in m/s
curvature: Road curvature in 1/m
accel_limits: (min_accel, max_accel) tuple
a_max: Override for max total acceleration
Returns:
(min_accel, max_accel) tuple with turn limiting applied
"""
limits = self.limit_accel(
v_ego,
curvature,
AccelLimits(accel_limits[0], accel_limits[1]),
a_max
)
return (limits.min_accel, limits.max_accel)
@@ -1,360 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Map-Aware Assist Longitudinal Helper
Provides navigation-based speed and acceleration limiting for the longitudinal planner.
This module keeps all nav-related planner logic isolated from the core planner code.
Features:
- Turn speed limiting based on maaControl cereal message
- Physics-based acceleration limiting using friction circle
- Smooth slowdown/resume transitions
- Driver acknowledgment (blinker) required to activate speed limiting
Adapted from CarrotPilot (https://github.com/ajouatom/openpilot)
Credit: carrotpilot team for curvature-based speed and physics-based accel limiting.
Usage in longitudinal_planner.py:
from dragonpilot.dashy.maa.lib.longitudinal_helper import LongitudinalHelper
# Add 'maaControl' to SubMaster
# In update:
maa = sm['maaControl']
if sm.valid['maaControl'] and maa.turnValid and maa.speedLimitActive:
# Only apply speed limit when driver acknowledged (blinker on)
turn_speed = maa.turnSpeedLimit
turn_distance = maa.turnDistance
else:
turn_speed, turn_distance = None, None
v_cruise = self.maa_helper.apply_nav_speed_limit(v_cruise, turn_speed, turn_distance)
"""
from dataclasses import dataclass
from typing import Optional, List
# Import nav modules
try:
from dragonpilot.dashy.maa.lib.accel_limiter import AccelLimiter
ACCEL_LIMITER_AVAILABLE = True
except ImportError:
ACCEL_LIMITER_AVAILABLE = False
@dataclass
class NavPlannerState:
"""Current state of nav planner integration."""
nav_limited_speed: Optional[float] = None
is_limiting_speed: bool = False
is_limiting_accel: bool = False
@dataclass
class VirtualLead:
"""Virtual lead car for turn deceleration."""
status: bool = False
dRel: float = 100.0 # distance to virtual lead
vLead: float = 0.0 # speed of virtual lead
aLeadK: float = 0.0 # acceleration
aLeadTau: float = 0.3
class RadarStateWrapper:
"""
Wrapper to inject virtual lead into radarState for MPC.
This allows the MPC to "see" a fake slow car at the turn point,
triggering natural deceleration using the well-tuned lead following logic.
"""
def __init__(self, radar_state, virtual_lead=None):
self._radar_state = radar_state
self._virtual_lead = virtual_lead
@property
def leadOne(self):
real_lead = self._radar_state.leadOne
# If no virtual lead, use real lead
if self._virtual_lead is None or not self._virtual_lead.status:
return real_lead
# If real lead exists and is closer, use real lead
if real_lead.status and real_lead.dRel < self._virtual_lead.dRel:
return real_lead
# Use virtual lead (turn point)
return self._virtual_lead
@property
def leadTwo(self):
return self._radar_state.leadTwo
def __getattr__(self, name):
return getattr(self._radar_state, name)
class LongitudinalHelper:
"""
Navigation helper for longitudinal planner.
Provides turn speed limiting and physics-based acceleration limiting
based on navigation data. Keeps all nav logic isolated from core planner.
Design rationale (based on automotive research):
- Stopping distance at 50 km/h is ~48m (25m braking + 15m reaction)
- Intersection approach speeds typically 30-50 km/h
- Comfortable deceleration: 1.5-2.0 m/ for normal driving
"""
# Configuration - Speed limiting
# Physics-based: calculate braking distance from speed difference
# More natural driving: maintain speed, brake closer to turn
SLOWDOWN_END_DIST = 10.0 # meters - be at turn speed by this distance (buffer before turn)
SLOWDOWN_BUFFER = 15.0 # meters - extra buffer added to braking distance
# Physics-based accel limiting
ACCEL_LIMIT_ENABLED = True
LAT_ACCEL_MAX = 2.5 # m/s² - max comfortable lateral acceleration
# Comfortable deceleration target (m/s²)
# Research shows 1.5-2.0 m/s² is comfortable for passengers
# Using 2.0 for more natural, later braking
COMFORT_DECEL = 2.0
def __init__(self):
"""Initialize nav planner helper."""
self.state = NavPlannerState()
# Physics-based acceleration limiter
if ACCEL_LIMITER_AVAILABLE and self.ACCEL_LIMIT_ENABLED:
self.accel_limiter = AccelLimiter(
lat_accel_max=self.LAT_ACCEL_MAX,
comfort_mode=True
)
else:
self.accel_limiter = None
def apply_nav_speed_limit(
self,
v_cruise: float,
turn_speed: Optional[float] = None,
distance: Optional[float] = None
) -> float:
"""
Apply navigation-based speed limiting with physics-based braking.
Uses kinematic equation: d = ( - v_target²) / (2 * decel)
Starts braking at calculated distance + buffer for natural driving feel.
Args:
v_cruise: Current cruise speed in m/s
turn_speed: Turn speed limit from maaControl (None if invalid)
distance: Distance to turn from maaControl (None if invalid)
Returns:
Limited cruise speed
"""
if turn_speed is None or distance is None:
self.state.nav_limited_speed = None
self.state.is_limiting_speed = False
return v_cruise
# No need to slow if already at or below turn speed
if v_cruise <= turn_speed:
self.state.nav_limited_speed = None
self.state.is_limiting_speed = False
return v_cruise
# Calculate required braking distance using kinematics
# d = (v² - v_target²) / (2 * decel)
speed_diff_sq = v_cruise ** 2 - turn_speed ** 2
braking_distance = speed_diff_sq / (2 * self.COMFORT_DECEL)
# Start braking at: braking_distance + buffer + end_distance
slowdown_start = braking_distance + self.SLOWDOWN_BUFFER + self.SLOWDOWN_END_DIST
# Outside slowdown zone - no limit
if distance > slowdown_start:
self.state.nav_limited_speed = None
self.state.is_limiting_speed = False
return v_cruise
# In slowdown zone - calculate target speed at this distance
# v² = v_target² + 2 * decel * (distance - end_dist)
if distance > self.SLOWDOWN_END_DIST:
remaining = distance - self.SLOWDOWN_END_DIST
# Target speed that allows comfortable braking to turn_speed
target_speed_sq = turn_speed ** 2 + 2 * self.COMFORT_DECEL * remaining
limited_speed = min(v_cruise, target_speed_sq ** 0.5)
else:
# Very close to turn - calculate achievable speed with comfort decel
# This handles late blinker: never brake harder than COMFORT_DECEL
# If we can't reach turn_speed in time, accept entering turn faster
achievable_speed_sq = turn_speed ** 2 + 2 * self.COMFORT_DECEL * max(0, distance)
achievable_speed = achievable_speed_sq ** 0.5
# Never target lower than achievable (prevents harsh braking)
limited_speed = min(v_cruise, max(turn_speed, achievable_speed))
self.state.nav_limited_speed = limited_speed
self.state.is_limiting_speed = True
return limited_speed
# Minimum distance for virtual lead - below this, don't use virtual lead
# to avoid MPC braking to stop
VIRTUAL_LEAD_MIN_DIST = 15.0 # meters
def get_virtual_lead(
self,
v_ego: float,
turn_speed: Optional[float] = None,
distance: Optional[float] = None
) -> Optional[VirtualLead]:
"""
Create a virtual lead car at the turn point for natural deceleration.
The virtual lead "drives" at turn_speed, positioned at the turn point.
This makes the MPC decelerate naturally as if following a slow car.
Args:
v_ego: Current vehicle speed in m/s
turn_speed: Turn speed limit from maaControl (None if invalid)
distance: Distance to turn from maaControl (None if invalid)
Returns:
VirtualLead object if turn is approaching, None otherwise
"""
if turn_speed is None or distance is None:
return None
# Only create virtual lead when we need to slow down
if v_ego <= turn_speed:
return None
# Don't use virtual lead when very close to turn - avoids brake-to-stop
# At this point, we should already be at turn speed from earlier braking
if distance < self.VIRTUAL_LEAD_MIN_DIST:
return None
# Calculate when to start showing virtual lead
# Use same physics: braking distance + buffer
speed_diff_sq = v_ego ** 2 - turn_speed ** 2
braking_distance = speed_diff_sq / (2 * self.COMFORT_DECEL)
activation_distance = braking_distance + self.SLOWDOWN_BUFFER + self.SLOWDOWN_END_DIST
if distance > activation_distance:
return None
# Create virtual lead at turn point, moving at turn speed
# The MPC will naturally decelerate to follow it
return VirtualLead(
status=True,
dRel=distance, # distance to virtual lead
vLead=turn_speed, # virtual lead moves at turn speed
aLeadK=0.0, # no acceleration (constant speed)
aLeadTau=0.3 # response time constant
)
def apply_nav_accel_limit(
self,
v_ego: float,
curvature: float,
accel_clip: List[float]
) -> List[float]:
"""
Apply physics-based acceleration limiting for turns.
Uses friction circle: a_x² + a_y² a_max²
Args:
v_ego: Current vehicle speed in m/s
curvature: Current road curvature (from nav or model)
accel_clip: Current [min_accel, max_accel] limits
Returns:
Updated [min_accel, max_accel] with turn limiting applied
"""
if self.accel_limiter is None:
return accel_clip
if abs(curvature) < 0.001: # Essentially straight
self.state.is_limiting_accel = False
return accel_clip
limited = list(self.accel_limiter.limit_accel_tuple(
v_ego, curvature, tuple(accel_clip)
))
self.state.is_limiting_accel = self.accel_limiter.state.is_limiting
return limited
# Staleness threshold for maaControl message (nanoseconds)
STALE_THRESHOLD_NS = 5e8 # 0.5 seconds
def process(
self,
sm,
v_ego: float,
v_cruise: float,
accel_clip: List[float]
) -> tuple:
"""
Process maaControl and return updated planner values.
Encapsulates all MAA logic:
- Validity and staleness checking
- Speed limiting (when driver acknowledged via blinker)
- Virtual lead creation for natural deceleration
- Curvature-based acceleration limiting
Args:
sm: SubMaster with 'maaControl' and 'carState'
v_ego: Current vehicle speed in m/s
v_cruise: Current cruise speed in m/s
accel_clip: Current [min_accel, max_accel] limits
Returns:
tuple: (v_cruise, accel_clip, virtual_lead)
"""
virtual_lead = None
maa = sm['maaControl']
# Check valid and not stale
maa_valid = sm.valid['maaControl'] and maa.turnValid
if maa_valid and (sm.logMonoTime['carState'] - sm.logMonoTime['maaControl']) > self.STALE_THRESHOLD_NS:
maa_valid = False
if not maa_valid:
self.state.nav_limited_speed = None
self.state.is_limiting_speed = False
self.state.is_limiting_accel = False
return v_cruise, accel_clip, virtual_lead
# Speed limiting only when driver acknowledged (blinker on)
# Without blinker: informational only, no speed reduction
if maa.speedLimitActive:
virtual_lead = self.get_virtual_lead(v_ego, maa.turnSpeedLimit, maa.turnDistance)
v_cruise = self.apply_nav_speed_limit(v_cruise, maa.turnSpeedLimit, maa.turnDistance)
# Curvature-based acceleration limiting (always active when valid)
if maa.curvatureValid and abs(maa.curvature) > 0.001:
accel_clip = self.apply_nav_accel_limit(v_ego, maa.curvature, accel_clip)
return v_cruise, accel_clip, virtual_lead
-119
View File
@@ -1,119 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
MAA Desire Helper
Turn execution flow:
1. APPROACHING (200m): Start dead reckoning, show turn suggestion
2. Driver turns on blinker: acknowledged
3. At 100m with blinker: COMMIT - block lane change, slow down, send desire
4. EXECUTING (30m): Track heading change
5. COMPLETE: When heading change expected turn angle
Turn desire is sent when:
- desireActive is true (from maa_controld)
- This requires: driver acknowledged (blinker) + committed (<100m) + EXECUTING state
Without blinker:
- System shows turn info but doesn't intervene
- No speed reduction, no desire sent
- Driver maintains full control
"""
from cereal import log, custom
TurnDirection = custom.MaaControl.TurnDirection
ManeuverType = custom.MaaControl.ManeuverType
TurnState = {
'NONE': 0,
'APPROACHING': 1,
'EXECUTING': 2,
'COMPLETE': 3,
'MISSED': 4,
}
# Configuration
MAX_TURN_SPEED = 50.0 / 3.6 # m/s (50 km/h) - no turn assist above this
SHARP_TURN_ANGLE = 45.0 # degrees - above: turnLeft/Right, below: laneChangeLeft/Right
def should_block_lane_change(maa_control, v_ego: float) -> bool:
"""
Check if lane change should be blocked due to approaching turn.
Uses blockLaneChange from maa_controld which is set when:
- Driver acknowledged (blinker on matching direction)
- Within commit distance (100m)
"""
if maa_control is None:
return False
if v_ego > MAX_TURN_SPEED:
return False
# Use the pre-computed blockLaneChange from maa_controld
return maa_control.blockLaneChange
def get_turn_desire(maa_control, carstate, is_rhd: bool = False) -> log.Desire:
"""
Get turn desire based on maaControl.
desireActive from maa_controld is true when:
- Driver acknowledged (blinker matches turn direction)
- Committed (<100m) OR in EXECUTING state
This function adds additional checks:
- Speed < 50 km/h
- maneuverType == turn
Args:
maa_control: MaaControl message
carstate: CarState message
is_rhd: True if right-hand drive (UK/Japan), False for left-hand drive (US/Taiwan)
Returns:
- turnLeft/Right if angle >= 45°
- laneChangeLeft/Right if angle < 45°
- none if conditions not met
"""
if maa_control is None or not maa_control.turnValid:
return log.Desire.none
if maa_control.maneuverType != ManeuverType.turn:
return log.Desire.none
if carstate.vEgo > MAX_TURN_SPEED:
return log.Desire.none
# desireActive encapsulates: acknowledged + (committed OR executing)
if not maa_control.desireActive:
return log.Desire.none
# Sharp turn (>= 45°) uses turnLeft/Right, gentle uses laneChange
is_sharp = abs(maa_control.turnAngle) >= SHARP_TURN_ANGLE
if maa_control.turnDirection == TurnDirection.left:
return log.Desire.turnLeft if is_sharp else log.Desire.laneChangeLeft
elif maa_control.turnDirection == TurnDirection.right:
return log.Desire.turnRight if is_sharp else log.Desire.laneChangeRight
return log.Desire.none
-118
View File
@@ -1,118 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Model Helper for modeld integration.
Provides turn desire logic for modeld.py.
"""
from cereal import log
from dragonpilot.dashy.maa.lib.maa_desire import (
should_block_lane_change,
get_turn_desire,
)
class ModelHelper:
"""
Helper class for MAA integration with modeld.
Usage in modeld.py:
from dragonpilot.dashy.maa.lib import ModelHelper
model_helper = ModelHelper()
# In the loop:
is_rhd = sm["driverMonitoringState"].isRHD
desire = model_helper.get_desire(sm, lane_change_desire, is_rhd)
"""
def __init__(self):
self.active = False
self.last_desire = log.Desire.none
def get_desire(self, sm, lane_change_desire: int, is_rhd: bool = False) -> int:
"""
Get combined desire from MAA turn logic and lane change.
Priority:
1. Active lane change (driver initiated) - don't interrupt
2. MAA turn desire (navigation turn)
3. Lane change desire (pre-lane-change, keep, etc.)
Args:
sm: SubMaster with maaControl and carState
lane_change_desire: Desire from DesireHelper (lane change logic)
is_rhd: True if right-hand drive
Returns:
Final desire value
"""
# Check if MAA data is available
if not sm.valid.get('maaControl', False) or not sm.valid.get('carState', False):
return lane_change_desire
# Check for stale maaControl (wait for 0.5s)
if (sm.logMonoTime['carState'] - sm.logMonoTime['maaControl']) > 5e8:
return lane_change_desire
maa_control = sm['maaControl']
carstate = sm['carState']
# Don't interrupt active lane change
if lane_change_desire in (log.Desire.laneChangeLeft, log.Desire.laneChangeRight):
self.active = False
return lane_change_desire
# Check if lane change should be blocked due to approaching turn
if should_block_lane_change(maa_control, carstate.vEgo):
# Block lane change desires, but allow none/keep
if lane_change_desire in (log.Desire.laneChangeLeft, log.Desire.laneChangeRight):
lane_change_desire = log.Desire.none
# Get MAA turn desire
maa_desire = get_turn_desire(maa_control, carstate, is_rhd)
# MAA turn desire takes priority over none/keep
if maa_desire != log.Desire.none:
self.active = True
self.last_desire = maa_desire
return maa_desire
# If MAA was active but now returns none, we completed the turn
if self.active and maa_desire == log.Desire.none:
self.active = False
return lane_change_desire
def update(self, modelv2, desire_state):
"""
Update helper with model output (for future use).
Can be used to detect turn completion via desireState probabilities.
Args:
modelv2: ModelV2 message
desire_state: Model's desire state output
"""
# Reserved for future use - detecting turn completion via model output
pass
-919
View File
@@ -1,919 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
MAA Control Daemon - Turn Assist with Dead Reckoning
================================================================================
OVERVIEW
================================================================================
This daemon provides high-frequency (20Hz) turn assist signals for navigation.
The key insight is that navInstruction updates at only 1Hz (too slow for smooth
turn tracking), so we use "snapshot and coast" - capture turn info once, then
dead reckon using the car's own sensors.
Subscribes to:
- navInstruction: turn info from maad (1Hz) - used as TRIGGER only
- liveGPS: position, bearing (1Hz)
- navRoute: route geometry - reset on route change
- carState: vEgo, blinker, yawRate (100Hz) - for dead reckoning
Publishes:
- maaControl: turnDistance, turnDirection, desireActive, turnState, etc.
================================================================================
DEAD RECKONING APPROACH
================================================================================
Problem:
Navigation updates come in slow (1Hz). At 60 km/h, that's 17m between updates.
This causes jerky distance countdown and imprecise turn detection.
Solution: "Snapshot and Coast"
1. TRIGGER at ~200m: capture turn params (angle, distance, direction)
2. IGNORE subsequent navInstruction updates for this turn
3. DEAD RECKON distance: integrate vEgo from carState (100Hz)
4. TRACK HEADING: integrate yawRate from carState during turn execution
Distance remaining = initial_distance - (vEgo × dt)
Heading change = (yawRate × dt)
================================================================================
STATE MACHINE
================================================================================
NONE
navInstruction shows turn within 200m
(significant angle 20°, has direction)
APPROACHING
Show turn suggestion at <150m
Wait for driver blinker (acknowledgment)
Dead reckon distance using vEgo
NOT tracking yaw yet (allows overtaking)
At <100m WITH blinker: COMMIT
blockLaneChange = true
speedLimitActive = true
desireActive = true
Dead reckoned distance < 30m
EXECUTING
Now tracking yaw (accumulated heading change)
Compare accumulated vs expected turn angle
desireActive = true (only if acknowledged)
|accumulated_yaw| >= |expected_angle| - tolerance
COMPLETE (2s cooldown) NONE
MISSED (any abort condition)
Wait for route change (navRoute coords change)
NONE
================================================================================
DRIVER ACKNOWLEDGMENT (Two-Step, Like Lane Change)
================================================================================
Two confirmation steps:
1. Blinker matching turn direction = approach confirmed (speed limit, block lane change)
2. Steering torque in turn direction = turn execution confirmed (desire sent)
Blinker is the main confirmation for slowing down. Steering is required to
actually send the turn desire to the model (like lane change).
Distance | Without Blinker | With Blinker | + Steering
-----------|--------------------------|----------------------------|------------------
200m-150m | Informational only | Informational only | (same)
150m-100m | Show suggestion | speedLimitActive = true | (same)
<100m | Show suggestion | slow down, block LC | + desireActive
<30m | Enter EXECUTING | slow down, block LC | + desireActive
Key behaviors:
- speedLimitActive: When blinker on, enforce turn speed limit
- blockLaneChange: At <100m with blinker, block lane change desire
- desireActive: Only sent when blinker AND steering confirmed (at turn)
If driver has blinker but doesn't steer:
- System slows down for the turn
- But doesn't send turn desire (driver steers manually)
- Once driver steers in turn direction, desire activates
================================================================================
ABORT/MISS DETECTION
================================================================================
Driver can override at any time. We detect "missed turn" via:
1. DROVE TOO FAR (no turn)
- Condition: distance_traveled > 2× initial_distance
- Meaning: drove way past expected turn point without turning
- Example: triggered at 200m, drove 400m, barely any yaw change
2. WRONG DIRECTION
- Condition: accumulated_yaw > 20° in opposite direction
- Meaning: driver turned the wrong way
- Example: expected right turn, driver turned 25° left
3. INSUFFICIENT TURN
- Condition: drove 2× total distance but <30% of expected yaw
- Meaning: drove through intersection without completing turn
- Example: expected 90° turn, only turned 20° after driving 500m
4. TIMEOUT
- Condition: 30 seconds of MOVING time (v_ego > 1 m/s)
- Meaning: something went wrong, taking too long
- Note: stopped time (traffic light) doesn't count
5. PASSED TURN (nav jumped to next)
- Condition: dead_reckon < 50m but nav says > 150m
- Meaning: nav is now showing NEXT turn, we passed this one
- Example: we think 30m to turn, nav says 400m (next turn)
6. DIRECTION CHANGED
- Condition: turnAngle sign flipped (left right)
- Meaning: route recalculated or nav corrected itself
- Example: was +90° (left), now -45° (right)
7. TURN DISAPPEARED
- Condition: turnAngle dropped below 20° threshold
- Meaning: no longer a significant turn (route changed or passed)
================================================================================
DRIVER OVERRIDE SCENARIOS
================================================================================
Overtaking another car:
- During APPROACHING: No problem! We only track distance, not yaw.
Driver can swerve left/right to overtake, doesn't affect tracking.
- During EXECUTING: Brief swerves (<20°) won't trigger wrong direction.
Only sustained opposite turn triggers abort.
Stopping at traffic light:
- moving_time only increments when v_ego > 1 m/s
- Can wait indefinitely at red light without timeout
- Distance tracking pauses when stopped (vEgo 0)
Taking a different route intentionally:
- System correctly detects as MISSED
- Waits for navRoute to change (reroute)
- Then ready to track new turn
================================================================================
DATA FLOW
================================================================================
navInstruction (1Hz) carState (100Hz)
turnAngle, maneuverDistance vEgo, yawRate, blinker
maneuverType, modifier
TurnTracker
trigger() at 200m, capture params
update(vEgo, yawRate) every 50ms (20Hz)
distance_traveled += vEgo × dt
accumulated_yaw += yawRate × dt (if EXECUTING)
check abort conditions
maaControl (20Hz)
turnDistance (dead reckoned)
turnState (NONE/APPROACHING/EXECUTING/COMPLETE/MISSED)
desireActive (for blinker/lane change desire)
turnProgress (accumulated yaw in degrees)
================================================================================
CONFIGURATION
================================================================================
TURN_TRIGGER_DISTANCE = 200m # Start dead reckoning
TURN_DESIRE_DISTANCE = 150m # Show turn suggestion, wait for blinker
TURN_COMMIT_DISTANCE = 100m # With blinker: commit (block lane change, slow)
TURN_EXECUTE_DISTANCE = 30m # Start tracking yaw
TURN_ANGLE_TOLERANCE = 15° # Turn complete within this
TURN_MIN_ANGLE = 20° # Minimum to be "significant"
TURN_TIMEOUT = 30s # Max moving time
================================================================================
OUTPUT FIELDS (maaControl)
================================================================================
turnDistance - Dead reckoned distance to turn (m)
turnDirection - left/right/none
turnAngle - Expected turn angle (deg, + = left)
turnState - 0=none, 1=approaching, 2=executing, 3=complete, 4=missed
turnProgress - Accumulated yaw during turn (deg)
desireActive - Send turn desire to model (blinker + steering confirmed)
driverAcknowledged - Driver turned on matching blinker
speedLimitActive - Enforce turn speed limit (blinker on)
blockLaneChange - Block lane change desire (blinker + committed)
turnSpeedLimit - Target speed for turn (m/s)
================================================================================
"""
import json
import math
import time
from enum import IntEnum
import numpy as np
from cereal import messaging, log, custom
from openpilot.common.params import Params
from openpilot.common.realtime import Ratekeeper
from openpilot.common.swaglog import cloudlog
from dragonpilot.dashy.maa.helpers import (
Coordinate,
compute_path_curvature,
find_closest_point_on_route,
)
# CarrotPilot curvature-to-speed lookup table
# Adapted from https://github.com/ajouatom/openpilot
# Maps curvature (1/m) to recommended speed (km/h)
# Based on physics: v = sqrt(a_lat / κ) where a_lat ≈ 2.5 m/s² for comfort
V_CURVE_LOOKUP_BP = [0., 1./800., 1./670., 1./560., 1./440., 1./360., 1./265., 1./190., 1./135., 1./85., 1./55., 1./30., 1./25.]
V_CURVE_LOOKUP_VALS = [300., 150., 120., 110., 100., 90., 80., 70., 60., 50., 40., 15., 5.] # km/h
# Configuration
CURVATURE_ASSIST_ENABLED = False # Disable continuous curvature steering assist
CURVATURE_LOOKAHEAD = 2.5 # seconds ahead for curvature calculation
TURN_VALID_DISTANCE = 500.0 # meters - turn is valid if within this distance
MIN_SPEED_FOR_CURVATURE = 1.0 # m/s - minimum speed to use curvature
# Turn execution - dead reckoning based
TURN_TRIGGER_DISTANCE = 200.0 # meters - capture turn info and start dead reckoning
TURN_DESIRE_DISTANCE = 150.0 # meters - show turn suggestion, wait for blinker
TURN_COMMIT_DISTANCE = 100.0 # meters - if blinker on, commit (block lane change, slow down)
TURN_EXECUTE_DISTANCE = 30.0 # meters - start tracking heading (entering intersection)
TURN_ANGLE_TOLERANCE = 15.0 # degrees - turn complete when within this of target
TURN_MIN_ANGLE = 20.0 # degrees - minimum angle to consider a "turn"
# Abort detection thresholds
TURN_MISS_DISTANCE_FACTOR = 2.0 # drove 2x expected distance without turning = missed
TURN_MISS_YAW_THRESHOLD = 0.3 # must achieve at least 30% of turn angle
TURN_WRONG_DIRECTION_ANGLE = 20.0 # degrees in wrong direction = missed
TURN_TIMEOUT = 30.0 # seconds - max time in APPROACHING/EXECUTING
class TurnState(IntEnum):
NONE = 0 # no turn pending
APPROACHING = 1 # turn ahead, dead reckoning distance
EXECUTING = 2 # in intersection, tracking heading change
COMPLETE = 3 # turn done, cooldown before next
MISSED = 4 # turn missed/aborted, wait for reroute
class TurnTracker:
"""
Tracks turn execution using dead reckoning.
Once triggered at ~200m, ignores navInstruction updates and purely
uses vEgo (distance) and yawRate (heading) from carState.
Driver acknowledgment flow (two-step, like lane change):
- System shows turn suggestion at 150m
- Driver turns on blinker = acknowledged (speed limit, block lane change)
- Driver steers in turn direction = steering confirmed (desire sent)
- Blinker gates the approach, steering gates the turn execution
"""
def __init__(self):
self.state = TurnState.NONE
# Captured at trigger
self.expected_angle = 0.0 # degrees (positive=left, negative=right)
self.initial_distance = 0.0 # distance to turn when triggered
self.direction = 'none' # 'left' or 'right'
self.maneuver_type = 0 # MaaControl.ManeuverType
self.turn_speed_limit = 0.0 # m/s
# Dead reckoning state
self.distance_traveled = 0.0 # meters since trigger
self.accumulated_yaw = 0.0 # degrees turned since execute start
self.moving_time = 0.0 # accumulated time while moving (for timeout)
self.execute_start_time = 0.0 # when EXECUTING started
self.complete_time = 0.0 # when turn completed
self.last_update_time = 0.0 # for dt calculation
# Driver acknowledgment (two-step like lane change)
self.driver_acknowledged = False # blinker matches turn direction
self.steering_confirmed = False # steering torque matches turn direction
def reset(self):
self.state = TurnState.NONE
self.expected_angle = 0.0
self.initial_distance = 0.0
self.direction = 'none'
self.maneuver_type = 0
self.turn_speed_limit = 0.0
self.distance_traveled = 0.0
self.accumulated_yaw = 0.0
self.moving_time = 0.0
self.execute_start_time = 0.0
self.complete_time = 0.0
self.last_update_time = 0.0
self.driver_acknowledged = False
self.steering_confirmed = False
def get_estimated_distance(self) -> float:
"""Get estimated distance to turn based on dead reckoning."""
if self.state in (TurnState.APPROACHING, TurnState.EXECUTING):
return max(0.0, self.initial_distance - self.distance_traveled)
return 9999.0
def trigger(self, t: float, turn_distance: float, turn_angle: float,
maneuver_type: int, turn_direction: int, turn_speed_limit: float):
"""Capture turn parameters and start dead reckoning."""
self.state = TurnState.APPROACHING
self.expected_angle = turn_angle
self.initial_distance = turn_distance
self.maneuver_type = maneuver_type
self.turn_speed_limit = turn_speed_limit
self.direction = 'left' if turn_direction == custom.MaaControl.TurnDirection.left else 'right'
self.distance_traveled = 0.0
self.accumulated_yaw = 0.0
self.moving_time = 0.0
self.last_update_time = t
cloudlog.info(f"maa: turn triggered, angle={turn_angle:.1f}°, dist={turn_distance:.0f}m, dir={self.direction}")
def check_blinker(self, left_blinker: bool, right_blinker: bool):
"""Check if driver turned on blinker matching turn direction."""
if self.direction == 'left' and left_blinker:
self.driver_acknowledged = True
elif self.direction == 'right' and right_blinker:
self.driver_acknowledged = True
# Note: we don't reset acknowledged if blinker turns off
# (driver may have tapped blinker briefly to acknowledge)
def check_steering(self, steering_pressed: bool, steering_torque: float):
"""Check if driver is steering in the turn direction (like lane change confirmation)."""
if not steering_pressed:
return
# Same logic as lane change: positive torque = left, negative = right
if self.direction == 'left' and steering_torque > 0:
self.steering_confirmed = True
elif self.direction == 'right' and steering_torque < 0:
self.steering_confirmed = True
# Once confirmed, stays confirmed (one-time check)
def update(self, t: float, v_ego: float, yaw_rate: float) -> bool:
"""
Update turn state with dead reckoning.
Args:
t: current time (monotonic)
v_ego: vehicle speed (m/s) from carState
yaw_rate: yaw rate (rad/s) from carState
Returns True if desire should be sent (acknowledged + within commit distance).
"""
if self.state == TurnState.NONE:
return False
# Calculate dt
dt = t - self.last_update_time if self.last_update_time > 0 else 0.05
dt = min(dt, 0.2) # Cap dt to handle timing glitches
self.last_update_time = t
# Integrate distance traveled (and moving time for timeout)
self.distance_traveled += v_ego * dt
if v_ego > 1.0: # Only count time when actually moving
self.moving_time += dt
estimated_dist = self.get_estimated_distance()
# Timeout check - based on moving time (allows waiting at traffic lights)
if self.moving_time > TURN_TIMEOUT:
cloudlog.warning(f"maa: turn timeout after {self.moving_time:.0f}s moving")
self.state = TurnState.MISSED
return False
if self.state == TurnState.APPROACHING:
# Check if we've reached the intersection
if estimated_dist <= TURN_EXECUTE_DISTANCE:
self.state = TurnState.EXECUTING
self.execute_start_time = t
self.accumulated_yaw = 0.0
cloudlog.info(f"maa: turn executing, traveled={self.distance_traveled:.0f}m")
# Miss detection: drove way past without entering execute
if self.distance_traveled > self.initial_distance * TURN_MISS_DISTANCE_FACTOR:
cloudlog.warning(f"maa: turn missed (drove past), traveled={self.distance_traveled:.0f}m")
self.state = TurnState.MISSED
return False
elif self.state == TurnState.EXECUTING:
# Integrate yaw rate (convert rad/s to deg)
yaw_change_deg = math.degrees(yaw_rate) * dt
self.accumulated_yaw += yaw_change_deg
# Fix 3: Early abort if going straight through the "turn" (map error)
# If we've traveled 20m past the turn point with no significant yaw change,
# the turn doesn't exist - abort quickly instead of extended false braking
distance_past_turn = self.distance_traveled - self.initial_distance
if distance_past_turn > 20.0 and abs(self.accumulated_yaw) < 5.0:
cloudlog.warning(f"maa: no turn detected (past {distance_past_turn:.0f}m, yaw={self.accumulated_yaw:.1f}°), aborting")
self.state = TurnState.MISSED
return False
# Check completion: achieved target heading
# For left turn: expected_angle > 0, accumulated should go positive
# For right turn: expected_angle < 0, accumulated should go negative
progress = abs(self.accumulated_yaw) / abs(self.expected_angle) if self.expected_angle != 0 else 0
angle_remaining = abs(self.expected_angle) - abs(self.accumulated_yaw)
if angle_remaining <= TURN_ANGLE_TOLERANCE:
self.state = TurnState.COMPLETE
self.complete_time = t
cloudlog.info(f"maa: turn complete, yaw={self.accumulated_yaw:.1f}°, expected={self.expected_angle:.1f}°")
return False
# Wrong direction detection: significant yaw in opposite direction
if self.expected_angle > 0 and self.accumulated_yaw < -TURN_WRONG_DIRECTION_ANGLE:
cloudlog.warning(f"maa: turn wrong direction (expected left, went right)")
self.state = TurnState.MISSED
return False
if self.expected_angle < 0 and self.accumulated_yaw > TURN_WRONG_DIRECTION_ANGLE:
cloudlog.warning(f"maa: turn wrong direction (expected right, went left)")
self.state = TurnState.MISSED
return False
# Miss detection: drove 2x expected total distance without sufficient turn
total_expected = self.initial_distance + 50.0 # Add some buffer for turn itself
if self.distance_traveled > total_expected * TURN_MISS_DISTANCE_FACTOR:
if progress < TURN_MISS_YAW_THRESHOLD:
cloudlog.warning(f"maa: turn missed (insufficient yaw), progress={progress:.1%}")
self.state = TurnState.MISSED
return False
elif self.state == TurnState.COMPLETE:
# Short cooldown then reset
if t - self.complete_time > 2.0:
self.reset()
return False
elif self.state == TurnState.MISSED:
# Stay in missed state until route changes (reset called externally)
return False
# Desire active when:
# Driver acknowledged (blinker on) AND steering confirmed AND either:
# 1. APPROACHING and within commit distance (<100m), OR
# 2. EXECUTING (in the turn)
# Without both confirmations, NO desire is sent (driver maintains control)
if not self.driver_acknowledged or not self.steering_confirmed:
return False
if self.state == TurnState.APPROACHING:
return estimated_dist <= TURN_COMMIT_DISTANCE
if self.state == TurnState.EXECUTING:
return True
return False
def get_turn_speed_from_curvature(curvature: float) -> float:
"""
Compute recommended speed for turn using CarrotPilot's curvature lookup table.
This is physics-based: v = sqrt(a_lat / κ) where a_lat 2.5 m/ for comfort.
Args:
curvature: Road curvature in 1/m (positive or negative)
Returns:
Recommended speed in m/s
"""
abs_curv = abs(curvature)
speed_kph = np.interp(abs_curv, V_CURVE_LOOKUP_BP, V_CURVE_LOOKUP_VALS)
return speed_kph / 3.6 # Convert to m/s
def get_turn_speed_limit(modifier: str, turn_angle: float, nav_type: str = '', curvature: float = 0.0) -> float:
"""
Compute recommended speed for turn based on curvature (preferred) or heuristics.
Uses CarrotPilot's curvature lookup table when curvature is available.
Falls back to heuristic-based calculation when curvature is not available.
Args:
modifier: OSRM maneuver modifier (e.g., 'left', 'right', 'sharp left')
turn_angle: Turn angle in degrees (positive=left, negative=right)
nav_type: OSRM maneuver type (e.g., 'turn', 'off ramp')
curvature: Road curvature in 1/m (from geometry calculation)
Returns:
Recommended speed in m/s
"""
# Use curvature-based calculation when curvature is available
# This is more accurate than heuristics as it's physics-based
if abs(curvature) > 0.001: # Meaningful curvature
speed_from_curv = get_turn_speed_from_curvature(curvature)
# Clamp to reasonable range: 5 km/h to 100 km/h for turns
return max(5.0 / 3.6, min(100.0 / 3.6, speed_from_curv))
# Fallback: Heuristic-based calculation when curvature not available
# Highway maneuvers (exits, ramps, merges) - much higher speeds
if nav_type in ('depart', 'off ramp', 'on ramp', 'merge', 'fork'):
if 'sharp' in modifier:
base_speed = 50.0 # km/h - sharp highway exit
elif 'slight' in modifier:
base_speed = 80.0 # km/h - gentle curve
else:
base_speed = 60.0 # km/h - normal exit ramp
return base_speed / 3.6 # Don't apply angle adjustments to highway maneuvers
# Intersection turns - lower speeds
if 'sharp' in modifier:
base_speed = 15.0 # km/h
elif 'slight' in modifier:
base_speed = 45.0 # km/h
elif modifier in ('left', 'right'):
base_speed = 25.0 # km/h
elif 'uturn' in modifier:
base_speed = 10.0 # km/h
else:
base_speed = 50.0 # km/h default
# Adjust based on actual angle if available (only for intersection turns)
abs_angle = abs(turn_angle)
if abs_angle > 90:
base_speed = min(base_speed, 15.0)
elif abs_angle > 60:
base_speed = min(base_speed, 25.0)
elif abs_angle > 30:
base_speed = min(base_speed, 35.0)
return base_speed / 3.6 # Convert to m/s
def get_maneuver_type(nav_type: str, turn_angle: float = 0.0) -> int:
"""Determine maneuver type primarily from geometry, with OSRM hints.
Geometry is the source of truth - OSRM labels can be wrong.
"""
# Geometry-first: significant turn angle = it's a turn
if abs(turn_angle) >= TURN_MIN_ANGLE:
return custom.MaaControl.ManeuverType.turn
# Highway maneuvers (even with small angle) - use laneChange desire
if nav_type in ('off ramp', 'on ramp', 'merge', 'fork'):
return custom.MaaControl.ManeuverType.laneChange
# OSRM says turn but geometry is minor - still treat as turn
if nav_type in ('turn', 'end of road'):
return custom.MaaControl.ManeuverType.turn
# Everything else: no action needed
return custom.MaaControl.ManeuverType.none
def get_last_gps_position(params: Params) -> tuple:
"""Get last known GPS position from params. Returns (lat, lon, bearing) or None."""
try:
data = params.get("LastGPSPosition")
if data:
pos = json.loads(data)
return pos.get('latitude'), pos.get('longitude'), pos.get('bearing', 0.0)
except Exception:
pass
return None
def main():
cloudlog.info("maa_controld: starting")
params = Params()
sm = messaging.SubMaster(
['navRoute', 'navInstruction', 'liveGPS', 'carState'],
ignore_alive=['navRoute', 'navInstruction', 'liveGPS', 'carState']
)
pm = messaging.PubMaster(['maaControl'])
rk = Ratekeeper(20)
# State
route_coords: list[Coordinate] = []
last_route_len = 0
last_gps_time = 0.0
closest_idx = 0
# Turn tracker (dead reckoning based)
turn_tracker = TurnTracker()
# Fallback position from params (used before liveGPS is ready)
fallback_pos = get_last_gps_position(params)
if fallback_pos:
cloudlog.info(f"maa_controld: fallback position {fallback_pos[0]:.6f}, {fallback_pos[1]:.6f}")
while True:
sm.update(0)
t = time.monotonic()
# Get current position - prefer liveGPS, fall back to LastGPSPosition
if sm.updated['liveGPS']:
last_gps_time = time.monotonic()
gps = sm['liveGPS']
gps_stale = (time.monotonic() - last_gps_time) > 2.0
gps_valid = gps.status == custom.LiveGPS.Status.valid and not gps_stale
# Use fallback if liveGPS not ready
use_fallback = not gps_valid and fallback_pos is not None
if gps_valid:
current_lat = gps.latitude
current_lon = gps.longitude
current_bearing = gps.bearingDeg
elif use_fallback:
current_lat, current_lon, current_bearing = fallback_pos
else:
current_lat = current_lon = current_bearing = None
# Get turn info from navInstruction
nav = sm['navInstruction']
nav_valid = sm.valid['navInstruction']
# Always update route coordinates (needed for turn angle calculation)
nav_route = sm['navRoute']
if sm.valid['navRoute'] and nav_route.coordinates:
new_len = len(nav_route.coordinates)
if new_len != last_route_len:
route_coords = [
Coordinate(c.latitude, c.longitude)
for c in nav_route.coordinates
]
last_route_len = new_len
closest_idx = 0
turn_tracker.reset() # Reset turn state when route changes
cloudlog.debug(f"maa_controld: route updated, {new_len} points")
# Find current position on route
has_position = current_lat is not None
if route_coords and has_position:
current_pos = Coordinate(current_lat, current_lon)
try:
# Optimization: Search locally around last known position
search_start = max(0, closest_idx - 10)
search_end = min(len(route_coords), closest_idx + 50)
if closest_idx == 0:
search_start = 0
search_end = len(route_coords)
subset = route_coords[search_start:search_end]
local_idx, _ = find_closest_point_on_route(current_pos, subset)
closest_idx = search_start + local_idx
except Exception as e:
cloudlog.warning(f"maa_controld: position error: {e}")
# Get speed, blinker, and yawRate from carState
cs = sm['carState']
cs_valid = sm.valid['carState']
v_ego = cs.vEgo if cs_valid else 0.0
left_blinker = cs.leftBlinker if cs_valid else False
right_blinker = cs.rightBlinker if cs_valid else False
yaw_rate = cs.yawRate if cs_valid else 0.0
# Continuous curvature assist (optional - for steering)
curvature = 0.0
curvature_valid = False
if CURVATURE_ASSIST_ENABLED and route_coords and has_position and v_ego > MIN_SPEED_FOR_CURVATURE:
try:
curvature = compute_path_curvature(
current_pos, current_bearing, route_coords, closest_idx, v_ego, CURVATURE_LOOKAHEAD
)
curvature_valid = True
except Exception as e:
cloudlog.warning(f"maa_controld: curvature error: {e}")
# Build maaControl message
msg = messaging.new_message('maaControl', valid=True)
maa = msg.maaControl
maa.curvature = float(curvature)
maa.curvatureValid = curvature_valid
# Get maneuver info from navInstruction (1Hz, used only for trigger)
maneuver_dist = getattr(nav, 'maneuverDistance', None) if nav_valid else None
# If turn tracker is active (including MISSED/COMPLETE), handle accordingly
if turn_tracker.state in (TurnState.APPROACHING, TurnState.EXECUTING):
# Safety checks: detect if turn info changed significantly
if nav_valid and maneuver_dist is not None:
# Use turnAngle from navInstructionExt - this is geometry-based (reliable)
turn_angle = getattr(nav, 'turnAngle', 0.0) or 0.0
estimated_dist = turn_tracker.get_estimated_distance()
# Check 1: Did we pass the turn? (nav distance jumped up = now showing NEXT turn)
# If we think we're close (<50m) but nav says far (>150m), we probably passed it
if estimated_dist < 50.0 and maneuver_dist > 150.0:
cloudlog.warning(f"maa: likely passed turn (est={estimated_dist:.0f}m, nav={maneuver_dist:.0f}m), resetting")
turn_tracker.reset()
# Check 2: Did direction flip? (route recalculated or now showing different turn)
else:
current_nav_dir = None
if turn_angle > TURN_MIN_ANGLE:
current_nav_dir = 'left'
elif turn_angle < -TURN_MIN_ANGLE:
current_nav_dir = 'right'
if current_nav_dir and current_nav_dir != turn_tracker.direction:
cloudlog.warning(f"maa: turn direction changed ({turn_tracker.direction}{current_nav_dir}), angle={turn_angle:.1f}°, resetting")
turn_tracker.reset()
# Check 3: Turn disappeared (angle now below threshold)
elif abs(turn_angle) < TURN_MIN_ANGLE:
nav_type = getattr(nav, 'maneuverType', '') or ''
if get_maneuver_type(nav_type, turn_angle) == custom.MaaControl.ManeuverType.none:
cloudlog.warning(f"maa: turn no longer valid (angle={turn_angle:.1f}°), resetting")
turn_tracker.reset()
if turn_tracker.state in (TurnState.APPROACHING, TurnState.EXECUTING):
# Check blinker and steering for driver acknowledgment (like lane change)
turn_tracker.check_blinker(left_blinker, right_blinker)
turn_tracker.check_steering(cs.steeringPressed, cs.steeringTorque)
# Dead reckon distance - ignore navInstruction updates
estimated_dist = turn_tracker.get_estimated_distance()
# Fix 2: Allow abort if blinker turns off before commitment (>100m)
# User can change their mind if not yet committed to the turn
if turn_tracker.state == TurnState.APPROACHING and turn_tracker.driver_acknowledged:
blinker_matches = (turn_tracker.direction == 'left' and left_blinker) or \
(turn_tracker.direction == 'right' and right_blinker)
if not blinker_matches and estimated_dist > TURN_COMMIT_DISTANCE:
turn_tracker.driver_acknowledged = False
turn_tracker.steering_confirmed = False
cloudlog.info("maa: blinker canceled before commit, aborting turn assist")
maa.turnDistance = float(estimated_dist)
maa.turnValid = True
maa.turnAngle = float(turn_tracker.expected_angle)
maa.maneuverType = turn_tracker.maneuver_type
maa.turnSpeedLimit = float(turn_tracker.turn_speed_limit)
# Direction from captured state
if turn_tracker.direction == 'left':
maa.turnDirection = custom.MaaControl.TurnDirection.left
elif turn_tracker.direction == 'right':
maa.turnDirection = custom.MaaControl.TurnDirection.right
else:
maa.turnDirection = custom.MaaControl.TurnDirection.none
# Update tracker with dead reckoning
desire_active = turn_tracker.update(t, v_ego, yaw_rate)
maa.turnState = int(turn_tracker.state)
maa.turnProgress = float(turn_tracker.accumulated_yaw)
# Driver acknowledgment status
maa.driverAcknowledged = turn_tracker.driver_acknowledged
# Speed limit active when blinker on (driver acknowledged)
maa.speedLimitActive = turn_tracker.driver_acknowledged
# Block lane change when committed (blinker + within commit distance)
maa.blockLaneChange = turn_tracker.driver_acknowledged and estimated_dist <= TURN_COMMIT_DISTANCE
# desireActive: send turn desire to model (requires blinker)
maa.desireActive = desire_active
# Curvature from captured state (not recalculated)
maa.curvature = 0.0
maa.curvatureValid = False
maa.turnCurvature = 0.0
elif nav_valid and maneuver_dist is not None:
# No active turn tracking - check if we should trigger
maa.turnDistance = float(maneuver_dist)
maa.turnValid = maneuver_dist < TURN_VALID_DISTANCE
nav_type = getattr(nav, 'maneuverType', '') or ''
modifier = getattr(nav, 'maneuverModifier', '') or ''
# Get pre-computed turn geometry from navInstruction
turn_angle = getattr(nav, 'turnAngle', 0.0) or 0.0
turn_curvature = getattr(nav, 'turnCurvature', 0.0) or 0.0
maa.turnAngle = float(turn_angle)
maa.turnCurvature = float(turn_curvature)
# Compute maneuver type
maa.maneuverType = get_maneuver_type(nav_type, turn_angle)
# Set turn direction
if maa.maneuverType != custom.MaaControl.ManeuverType.none:
if 'left' in modifier.lower():
maa.turnDirection = custom.MaaControl.TurnDirection.left
elif 'right' in modifier.lower():
maa.turnDirection = custom.MaaControl.TurnDirection.right
elif turn_angle > TURN_MIN_ANGLE:
maa.turnDirection = custom.MaaControl.TurnDirection.left
elif turn_angle < -TURN_MIN_ANGLE:
maa.turnDirection = custom.MaaControl.TurnDirection.right
else:
maa.turnDirection = custom.MaaControl.TurnDirection.none
else:
maa.turnDirection = custom.MaaControl.TurnDirection.none
# Compute turn speed limit using CarrotPilot curvature lookup table
# Curvature-based is more accurate than heuristics when available
turn_speed_limit = get_turn_speed_limit(modifier, turn_angle, nav_type, turn_curvature)
maa.turnSpeedLimit = float(turn_speed_limit)
# Check if we should trigger dead reckoning
# Don't trigger if already in MISSED/COMPLETE state (wait for route change)
can_trigger = turn_tracker.state == TurnState.NONE
is_actionable = maa.maneuverType != custom.MaaControl.ManeuverType.none
has_direction = maa.turnDirection != custom.MaaControl.TurnDirection.none
is_significant = abs(turn_angle) >= TURN_MIN_ANGLE
in_trigger_range = maneuver_dist <= TURN_TRIGGER_DISTANCE
if can_trigger and is_actionable and has_direction and is_significant and in_trigger_range:
# Trigger! Capture turn params and start dead reckoning
turn_tracker.trigger(
t, maneuver_dist, turn_angle,
maa.maneuverType, maa.turnDirection, turn_speed_limit
)
# Check blinker and steering immediately after trigger
turn_tracker.check_blinker(left_blinker, right_blinker)
turn_tracker.check_steering(cs.steeringPressed, cs.steeringTorque)
# Blinker = approach confirmation, steering = turn execution confirmation
maa.desireActive = turn_tracker.driver_acknowledged and turn_tracker.steering_confirmed and maneuver_dist <= TURN_COMMIT_DISTANCE
maa.turnState = int(turn_tracker.state)
maa.turnProgress = 0.0
maa.driverAcknowledged = turn_tracker.driver_acknowledged
maa.speedLimitActive = turn_tracker.driver_acknowledged
maa.blockLaneChange = turn_tracker.driver_acknowledged and maneuver_dist <= TURN_COMMIT_DISTANCE
elif turn_tracker.state in (TurnState.COMPLETE, TurnState.MISSED):
# In cooldown - call update to handle state transitions
turn_tracker.update(t, v_ego, yaw_rate)
maa.desireActive = False
maa.turnState = int(turn_tracker.state)
maa.turnProgress = float(turn_tracker.accumulated_yaw)
maa.driverAcknowledged = False
maa.speedLimitActive = False
maa.blockLaneChange = False
else:
# Not triggered yet (turn too far away)
maa.desireActive = False
maa.turnState = int(TurnState.NONE)
maa.turnProgress = 0.0
maa.driverAcknowledged = False
maa.speedLimitActive = False
maa.blockLaneChange = False
else:
# No valid nav instruction
maa.turnValid = False
maa.turnDirection = custom.MaaControl.TurnDirection.none
maa.turnSpeedLimit = 50.0 / 3.6
maa.maneuverType = custom.MaaControl.ManeuverType.none
maa.turnAngle = 0.0
maa.turnCurvature = 0.0
maa.turnDistance = 9999.0
maa.desireActive = False
maa.turnState = int(TurnState.NONE)
maa.turnProgress = 0.0
maa.driverAcknowledged = False
maa.speedLimitActive = False
maa.blockLaneChange = False
# Only reset if not in MISSED state (wait for route change)
if turn_tracker.state != TurnState.MISSED:
turn_tracker.reset()
pm.send('maaControl', msg)
rk.keep_time()
if __name__ == "__main__":
main()
-566
View File
@@ -1,566 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
dragonpilot Map-Aware Assist Daemon (maad)
Handles route calculation and navigation instructions using OSRM routing.
Similar to openpilot's navd but uses free OSRM API instead of Mapbox.
Control signals (maaControl) are published by maa_controld.py separately
for low-latency, deterministic control.
Flow:
1. Reads dp_maa_destination from params (set by dashy)
2. Fetches route from OSRM (free routing API) - async to avoid blocking
3. Subscribes to liveGPS for position updates
4. Publishes navInstruction (for UI) and navRoute (for map display)
"""
import json
import time
import threading
import queue
from dataclasses import dataclass
from typing import Optional
import cereal.messaging as messaging
from cereal import custom, log
from openpilot.common.params import Params
from openpilot.common.realtime import Ratekeeper
from openpilot.common.swaglog import cloudlog
from dragonpilot.dashy.maa.helpers import (
Coordinate,
coordinate_from_param,
find_closest_point_on_route,
distance_along_geometry,
compute_turn_angle_at_index,
compute_turn_curvature_at_index,
)
from dragonpilot.dashy.maa.providers import MapService
from dragonpilot.dashy.maa.providers.models import Coordinate as ProviderCoordinate
from dragonpilot.dashy.maa.route_tracker import RouteTracker, REROUTE_DISTANCE_BASE, REROUTE_DEBOUNCE_TIME
MANEUVER_TRANSITION_THRESHOLD = 10 # meters past maneuver to transition
NAV_RATE = 1.0 # Hz - navigation update rate
@dataclass
class Step:
"""Represents a navigation step/segment."""
distance: float # total distance of step in meters
duration: float # duration in seconds
duration_typical: Optional[float]
name: str
maneuver_type: str
maneuver_modifier: str
geometry: list[Coordinate] # coordinates for this step
speed_limit: Optional[float] # m/s
speed_limit_sign: str # 'mutcd' or 'vienna'
# Pre-computed turn geometry (computed once when route is fetched)
turn_angle: float = 0.0 # degrees, positive=left, negative=right
turn_curvature: float = 0.0 # 1/m, positive=left, negative=right
# Explicit maneuver point from OSRM (for fork/turn detection)
maneuver_point: Optional[Coordinate] = None
class RouteEngine:
def __init__(self, sm: messaging.SubMaster, pm: messaging.PubMaster):
self.sm = sm
self.pm = pm
self.params = Params()
self.map_service = MapService(self.params)
# Get last GPS position from params
self.last_position = coordinate_from_param("LastGPSPosition", self.params)
if self.last_position is None:
# Default to Taipei 101 for bench testing
self.last_position = Coordinate(25.033976, 121.564472)
self.last_bearing: Optional[float] = None
self.gps_ok = False
self.gps_speed = 0.0
self.gps_accuracy = 0.0 # horizontal accuracy in meters
self.last_gps_time = 0.0
# Route state
self.nav_destination: Optional[Coordinate] = None
self.route: Optional[list[Step]] = None
self.tracker = RouteTracker() # OsmAnd-style route tracking
# Recompute state
self.recompute_backoff = 0
self.recompute_countdown = 0
# Async route calculation
self._route_queue: queue.Queue = queue.Queue()
self._route_thread: Optional[threading.Thread] = None
self._route_calculating = False
# Timing diagnostics
self._frame_count = 0
self._last_timing_log = time.monotonic()
self._gps_save_counter = 0
self._route_send_counter = 0
# Params cache - avoid reading params every frame
self._cached_destination: Optional[Coordinate] = None
self._last_destination_check = 0.0
self._destination_check_interval = 3.0 # seconds
# First valid GPS flag - triggers immediate route calculation
self._had_first_valid_gps = False
def update(self):
t0 = time.monotonic()
self.sm.update(0)
t1 = time.monotonic()
self._update_location()
self._check_route_result() # Non-blocking check for async route result
t2 = time.monotonic()
try:
self._recompute_route()
t3 = time.monotonic()
self._send_instruction()
# Resend route periodically (every 1s) so new UI clients can see it
if self.route is not None:
self.send_route()
t4 = time.monotonic()
except Exception:
cloudlog.exception("maad.failed_to_compute")
t3 = t4 = time.monotonic()
# Log timing every 10 seconds
self._frame_count += 1
total_time = t4 - t0
if total_time > 0.05: # Log if frame took > 50ms
cloudlog.warning(f"maad slow frame: total={total_time*1000:.0f}ms "
f"(sm={1000*(t1-t0):.0f}, loc={1000*(t2-t1):.0f}, "
f"route={1000*(t3-t2):.0f}, instr={1000*(t4-t3):.0f})")
if t4 - self._last_timing_log > 10.0:
actual_rate = self._frame_count / (t4 - self._last_timing_log)
cloudlog.info(f"maad rate: {actual_rate:.1f} Hz (target {NAV_RATE} Hz), frames={self._frame_count}")
self._frame_count = 0
self._last_timing_log = t4
def _check_route_result(self):
"""Check for async route calculation result (non-blocking)."""
try:
result = self._route_queue.get_nowait()
self._route_calculating = False
if result is None:
# Route calculation failed
self._clear_route(clear_destination=False)
return
# Apply the calculated route
self.route, route_data = result
self.nav_curvature_valid = True
# Start at first step (simple, like navd.py)
self.tracker.set_step(0)
self._reset_recompute_limits() # Reset backoff on successful route
cloudlog.warning(f"maad: route calculated - {route_data['distance']/1000:.1f}km, "
f"{route_data['duration']/60:.0f}min, {len(self.route)} steps")
# Debug: log each step's maneuver info
for i, s in enumerate(self.route):
mp_str = f"({s.maneuver_point.latitude:.6f},{s.maneuver_point.longitude:.6f})" if s.maneuver_point else "None"
cloudlog.info(f"maad step {i}: {s.maneuver_type} {s.maneuver_modifier} -> '{s.name}' ({s.distance:.0f}m) mp={mp_str}")
self.send_route()
except queue.Empty:
pass # No result yet
def _update_location(self):
"""Update position from GPS."""
# Debug: log GPS reception status every 50 frames
self._gps_debug_counter = getattr(self, '_gps_debug_counter', 0) + 1
if self._gps_debug_counter >= 50:
self._gps_debug_counter = 0
gps = self.sm['liveGPS']
cloudlog.info(f"maad GPS: updated={self.sm.updated['liveGPS']} valid={self.sm.valid['liveGPS']} "
f"gpsOK={gps.gpsOK} pos=({gps.latitude:.6f},{gps.longitude:.6f})")
if self.sm.updated['liveGPS']:
gps = self.sm['liveGPS']
# Always update position and speed from GPS (needed for route calculation)
if gps.gpsOK:
self.last_position = Coordinate(gps.latitude, gps.longitude)
self.gps_speed = gps.speed
self.gps_accuracy = gps.horizontalAccuracy # Track for OsmAnd-style tolerance
self.last_gps_time = time.monotonic()
# Save last position every ~60 frames (12 seconds at 5Hz) to reduce I/O
self._gps_save_counter += 1
if self._gps_save_counter >= 60:
self._gps_save_counter = 0
self.params.put("LastGPSPosition", json.dumps({
'latitude': gps.latitude,
'longitude': gps.longitude
}))
# GPS is valid when OK, good accuracy, AND fully calibrated
was_gps_ok = self.gps_ok
is_calibrated = gps.status == custom.LiveGPS.Status.valid
self.gps_ok = gps.gpsOK and gps.horizontalAccuracy <= 20.0 and is_calibrated
# Only use bearing when fusion is fully calibrated
if is_calibrated:
self.last_bearing = gps.bearingDeg
else:
self.last_bearing = None # clear stale bearing
# Detect first valid GPS - triggers immediate route check
if self.gps_ok and not was_gps_ok and not self._had_first_valid_gps:
self._had_first_valid_gps = True
cloudlog.info("maad: first valid GPS fix (calibrated), checking for destination")
# Staleness check
if time.monotonic() - self.last_gps_time > 2.0:
self.gps_ok = False
def _recompute_route(self):
"""Check if we need to recompute route."""
# Don't start route until GPS is valid (OK + calibrated)
if not self.gps_ok:
return
# Skip if route calculation is already in progress
if self._route_calculating:
return
# Check params - immediately on first valid GPS, otherwise every 3 seconds
now = time.monotonic()
first_gps_check = self._had_first_valid_gps and self.route is None and self._cached_destination is None
if first_gps_check or now - self._last_destination_check >= self._destination_check_interval:
self._last_destination_check = now
self._cached_destination = coordinate_from_param("dp_maa_destination", self.params)
new_destination = self._cached_destination
if new_destination is None:
if self.nav_destination is not None or self.route is not None:
self._clear_route()
self._reset_recompute_limits()
return
should_recompute = self._should_recompute()
if should_recompute and self.route is not None:
cloudlog.warning(f"maad: reroute triggered, countdown={self.recompute_countdown}, backoff={self.recompute_backoff}")
# New destination
if new_destination != self.nav_destination:
cloudlog.warning(f"Got new destination from dp_maa_destination param {new_destination}")
self.nav_destination = new_destination
should_recompute = True
# Don't recompute when GPS drifts in tunnels
if not self.gps_ok and self.tracker.step_idx is not None:
return
# First route calculation (no existing route) - start immediately without backoff
is_first_route = self.route is None and should_recompute
if is_first_route or (self.recompute_countdown == 0 and should_recompute):
if not is_first_route:
self.recompute_countdown = 2 ** self.recompute_backoff
self.recompute_backoff = min(3, self.recompute_backoff + 1) # Max 8 second backoff
self._start_route_calculation(new_destination)
else:
self.recompute_countdown = max(0, self.recompute_countdown - 1)
def _start_route_calculation(self, destination: Coordinate):
"""Start async route calculation in a separate thread."""
start_pos = self.last_position
bearing = self.last_bearing
self._route_calculating = True
self.nav_destination = destination
cloudlog.info(f"maad: starting async route calculation {start_pos} -> {destination}")
def calculate():
try:
result = self._fetch_route(start_pos, destination, bearing)
self._route_queue.put(result)
except Exception as e:
cloudlog.exception(f"maad: route calculation failed: {e}")
self._route_queue.put(None)
self._route_thread = threading.Thread(target=calculate, daemon=True)
self._route_thread.start()
def _fetch_route(self, start: Coordinate, destination: Coordinate,
bearing: Optional[float]) -> Optional[tuple]:
"""Fetch route using MapService (runs in thread). Returns (route, route_data) or None."""
origin = ProviderCoordinate(start.latitude, start.longitude)
dest = ProviderCoordinate(destination.latitude, destination.longitude)
provider_route = self.map_service.route_provider.get_route_sync(
origin=origin,
destination=dest,
bearing=bearing
)
if provider_route is None:
cloudlog.warning("maad: route provider returned None")
return None
# Convert provider Route to local Step format
# Filter out depart/arrive - merge their geometry with adjacent steps
route = []
all_coords = [] # Full route geometry for turn angle computation
pending_geometry = [] # Geometry from depart to merge with first real step
for provider_step in provider_route.steps:
# Convert provider Coordinates to helper Coordinates
geometry = [
Coordinate(c.latitude, c.longitude)
for c in provider_step.geometry
]
all_coords.extend(geometry)
# Skip depart/arrive steps but keep their geometry
if provider_step.maneuver_type in ('depart', 'arrive'):
if provider_step.maneuver_type == 'depart':
pending_geometry = geometry # Save for merging with first real step
elif provider_step.maneuver_type == 'arrive' and route:
# Merge arrive geometry with last step
route[-1].geometry.extend(geometry)
route[-1].distance += provider_step.distance
route[-1].duration += provider_step.duration
continue
# Merge pending depart geometry with this step
if pending_geometry:
geometry = pending_geometry + geometry
pending_geometry = []
# Convert provider maneuver_point to helpers Coordinate
maneuver_pt = None
if provider_step.maneuver_point:
maneuver_pt = Coordinate(
provider_step.maneuver_point.latitude,
provider_step.maneuver_point.longitude
)
route_step = Step(
distance=provider_step.distance,
duration=provider_step.duration,
duration_typical=provider_step.duration_typical or provider_step.duration,
name=provider_step.name,
maneuver_type=provider_step.maneuver_type,
maneuver_modifier=provider_step.maneuver_modifier,
geometry=geometry,
speed_limit=provider_step.speed_limit,
speed_limit_sign=provider_step.speed_limit_sign,
maneuver_point=maneuver_pt,
)
route.append(route_step)
# Pre-compute turn geometry at each step's maneuver point (end of step)
coord_idx = 0
for step in route:
coord_idx += len(step.geometry)
# Turn point is at the end of this step (start of next)
turn_idx = min(coord_idx - 1, len(all_coords) - 2)
if turn_idx > 0:
step.turn_angle = compute_turn_angle_at_index(all_coords, turn_idx)
step.turn_curvature = compute_turn_curvature_at_index(all_coords, turn_idx)
# Build route_data dict for compatibility
route_data = {
'distance': provider_route.distance,
'duration': provider_route.duration,
}
cloudlog.info(f"maad: route from {provider_route.provider} - "
f"{provider_route.distance/1000:.1f}km, {len(route)} steps")
return (route, route_data)
def _send_instruction(self):
"""Send navInstruction message ."""
msg = messaging.new_message('navInstruction', valid=True)
if self.tracker.step_idx is None or self.route is None or self.last_position is None or not self.gps_ok:
# Debug: log why we're sending invalid
reasons = []
if self.tracker.step_idx is None:
reasons.append("step_idx=None")
if self.route is None:
reasons.append("route=None")
if self.last_position is None:
reasons.append("position=None")
if not self.gps_ok:
reasons.append(f"gps_ok=False")
cloudlog.info(f"maad: sending invalid navInstruction: {', '.join(reasons)}")
msg.valid = False
self.pm.send('navInstruction', msg)
return
# Sanity check: ensure step_idx is valid
if self.tracker.step_idx >= len(self.route):
cloudlog.error(f"maad: step_idx {self.tracker.step_idx} >= route length {len(self.route)}, resetting to 0")
self.tracker.set_step(0)
step = self.route[self.tracker.step_idx]
geometry = step.geometry
# Calculate distance along current step geometry
along_geometry = distance_along_geometry(geometry, self.last_position)
distance_to_maneuver = step.distance - along_geometry
# Current instruction (depart/arrive already filtered out during route build)
msg.navInstruction.maneuverDistance = distance_to_maneuver
msg.navInstruction.maneuverPrimaryText = step.name or step.maneuver_type
# Override maneuver type/modifier based on geometry
# Geometry-first: always use turn_angle for direction when significant
TURN_MIN_ANGLE = 20.0
if abs(step.turn_angle) >= TURN_MIN_ANGLE:
# Significant turn - use geometry for both type and direction
if step.maneuver_type in ('continue', 'new name'):
msg.navInstruction.maneuverType = 'turn'
else:
msg.navInstruction.maneuverType = step.maneuver_type
# Always use geometry-based direction for significant turns
msg.navInstruction.maneuverModifier = 'left' if step.turn_angle > 0 else 'right'
else:
msg.navInstruction.maneuverType = step.maneuver_type
msg.navInstruction.maneuverModifier = step.maneuver_modifier
# Next step's road name (the road to turn onto)
if self.tracker.step_idx + 1 < len(self.route):
next_step = self.route[self.tracker.step_idx + 1]
msg.navInstruction.maneuverSecondaryText = next_step.name or ""
# Compute total remaining time and distance
remaining_ratio = 1.0 - along_geometry / max(step.distance, 1)
total_distance = step.distance * remaining_ratio
total_time = step.duration * remaining_ratio
total_time_typical = (step.duration_typical or step.duration) * remaining_ratio
for i in range(self.tracker.step_idx + 1, len(self.route)):
total_distance += self.route[i].distance
total_time += self.route[i].duration
total_time_typical += self.route[i].duration_typical or self.route[i].duration
msg.navInstruction.distanceRemaining = total_distance
msg.navInstruction.timeRemaining = total_time
msg.navInstruction.timeRemainingTypical = total_time_typical
# Speed limit from closest coordinate
if geometry:
closest_idx, _ = find_closest_point_on_route(self.last_position, geometry)
if closest_idx < len(geometry):
closest = geometry[closest_idx]
if 'maxspeed' in closest.annotations and self.gps_ok:
msg.navInstruction.speedLimit = closest.annotations['maxspeed']
if step.speed_limit_sign == 'mutcd':
msg.navInstruction.speedLimitSign = log.NavInstruction.SpeedLimitSign.mutcd
else:
msg.navInstruction.speedLimitSign = log.NavInstruction.SpeedLimitSign.vienna
self.pm.send('navInstruction', msg)
# Send extended nav instruction (turn geometry)
msg_ext = messaging.new_message('navInstructionExt')
msg_ext.navInstructionExt.turnAngle = step.turn_angle
msg_ext.navInstructionExt.turnCurvature = step.turn_curvature
self.pm.send('navInstructionExt', msg_ext)
# Transition to next step
if self.tracker.update_step(self.route, self.last_position, self.last_bearing, self.gps_speed):
self._reset_recompute_limits()
# Check if arrived at destination
if self.nav_destination:
dist = self.nav_destination.distance_to(self.last_position)
if dist < 30: # Within 30m of destination
cloudlog.warning("maad: destination reached")
self.params.remove("dp_maa_destination")
self._clear_route()
def send_route(self):
"""Send navRoute message for dashy to display route on map."""
coords = []
if self.route is not None:
for step in self.route:
coords.extend([[c.longitude, c.latitude] for c in step.geometry])
msg = messaging.new_message('navRoute', valid=True)
msg.navRoute.coordinates = [{"longitude": c[0], "latitude": c[1]} for c in coords]
self.pm.send('navRoute', msg)
def _clear_route(self, clear_destination=True):
"""Clear navigation state."""
self.route = None
self.tracker.reset()
if clear_destination:
self.nav_destination = None
# Send empty navRoute to clear map display
msg = messaging.new_message('navRoute', valid=False)
msg.navRoute.coordinates = []
self.pm.send('navRoute', msg)
def _reset_recompute_limits(self):
"""Reset recompute backoff and deviation timer."""
self.recompute_backoff = 0
self.recompute_countdown = 0
self.tracker.deviation_start_time = None # Reset OsmAnd-style deviation timer
def _should_recompute(self) -> bool:
"""Check if route should be recomputed (delegates to RouteTracker)."""
if self.route is None:
return True
return self.tracker.should_reroute(self.route, self.last_position, self.gps_accuracy)
def main():
cloudlog.info("maad starting")
pm = messaging.PubMaster(['navInstruction', 'navInstructionExt', 'navRoute'])
sm = messaging.SubMaster(['liveGPS'], ignore_alive=['liveGPS'])
rk = Ratekeeper(NAV_RATE)
route_engine = RouteEngine(sm, pm)
while True:
try:
route_engine.update()
except Exception:
cloudlog.exception("maad: error in main loop")
rk.keep_time()
if __name__ == "__main__":
main()
@@ -1,54 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Map Provider Abstraction Layer
Uses api.dragonpilot.org for search and routing with device JWT authentication.
Falls back to free providers (Photon/OSRM) when not authenticated.
Usage:
from dragonpilot.dashy.maa.providers import MapService
map_service = MapService()
route = map_service.route_provider.get_route_sync(origin, dest)
results = await map_service.search_provider.search("Taipei 101")
"""
from .map_service import MapService
from .models import Coordinate, SearchResult, Route, Step, TileConfig
from .base import SearchProvider, RouteProvider, TileProvider
from .dragonpilot import DragonpilotSearchProvider, DragonpilotRouteProvider, DragonpilotApiClient
__all__ = [
'MapService',
'Coordinate',
'SearchResult',
'Route',
'Step',
'TileConfig',
'SearchProvider',
'RouteProvider',
'TileProvider',
'DragonpilotSearchProvider',
'DragonpilotRouteProvider',
'DragonpilotApiClient',
]
-177
View File
@@ -1,177 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Abstract base classes for map providers.
These interfaces define the contract that all provider implementations must follow.
"""
from abc import ABC, abstractmethod
from typing import Optional
import asyncio
from .models import Coordinate, SearchResult, Route, TileConfig
class SearchProvider(ABC):
"""
Abstract search/geocoding provider.
Implementations: PhotonSearchProvider, MapboxSearchProvider, etc.
"""
name: str = "base"
requires_api_key: bool = False
@abstractmethod
async def search(
self,
query: str,
proximity: Optional[Coordinate] = None,
limit: int = 10
) -> list[SearchResult]:
"""
Search for places by query string.
Args:
query: Search query (address, place name, etc.)
proximity: Optional coordinate to bias results toward
limit: Maximum number of results
Returns:
List of SearchResult objects sorted by relevance/distance
"""
pass
@abstractmethod
async def reverse_geocode(
self,
coord: Coordinate
) -> Optional[SearchResult]:
"""
Get address/place information from coordinates.
Args:
coord: Coordinate to reverse geocode
Returns:
SearchResult with address info, or None if not found
"""
pass
def search_sync(
self,
query: str,
proximity: Optional[Coordinate] = None,
limit: int = 10
) -> list[SearchResult]:
"""Synchronous wrapper for search()."""
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(self.search(query, proximity, limit))
finally:
loop.close()
class RouteProvider(ABC):
"""
Abstract routing provider.
Implementations: OSRMRouteProvider, MapboxRouteProvider, etc.
"""
name: str = "base"
requires_api_key: bool = False
supports_traffic: bool = False
@abstractmethod
async def get_route(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]] = None,
bearing: Optional[float] = None
) -> Optional[Route]:
"""
Calculate route between points.
Args:
origin: Starting coordinate
destination: Ending coordinate
waypoints: Optional intermediate waypoints
bearing: Optional current heading in degrees (for better route start)
Returns:
Route object with steps and geometry, or None if routing fails
"""
pass
def get_route_sync(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]] = None,
bearing: Optional[float] = None
) -> Optional[Route]:
"""
Synchronous wrapper for get_route().
Use this in maad.py where async is not available.
"""
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(
self.get_route(origin, destination, waypoints, bearing)
)
finally:
loop.close()
class TileProvider(ABC):
"""
Abstract map tile provider.
Implementations: OpenFreeMapTileProvider, MapboxTileProvider, etc.
"""
name: str = "base"
requires_api_key: bool = False
@abstractmethod
def get_tile_config(self) -> TileConfig:
"""
Get tile URL template and configuration.
Returns:
TileConfig with URL template and attribution
"""
pass
@abstractmethod
def get_style_json(self) -> dict:
"""
Get MapLibre GL style JSON for this provider.
Returns:
Style JSON dict for MapLibre GL JS
"""
pass
-45
View File
@@ -1,45 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Configuration for map providers.
Uses api.dragonpilot.org for search and routing with device serial authentication.
Falls back to free providers (Photon/OSRM) when not authenticated.
All providers use WGS-84 coordinates (standard GPS).
"""
from dataclasses import dataclass
@dataclass
class ProviderConfig:
"""
Provider configuration.
Uses Dragonpilot API with automatic fallback to free providers.
All providers use WGS-84 coordinates (standard GPS).
"""
@classmethod
def from_params(cls, params) -> 'ProviderConfig':
"""Load configuration from openpilot Params."""
return cls()
@@ -1,32 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Dragonpilot API Provider
Uses api.dragonpilot.org for search and routing with device JWT authentication.
"""
from .client import DragonpilotApiClient
from .search import DragonpilotSearchProvider
from .routing import DragonpilotRouteProvider
__all__ = ['DragonpilotApiClient', 'DragonpilotSearchProvider', 'DragonpilotRouteProvider']
@@ -1,123 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Dragonpilot API Client
Simple HTTP client using device serial for authentication.
"""
import os
from typing import Optional
import aiohttp
import requests
from openpilot.system.hardware import HARDWARE
API_HOST = os.getenv('DRAGONPILOT_API_HOST', 'https://api.dragonpilot.org')
# Module-level serial cache - queried once from HARDWARE
_serial: Optional[str] = None
def _get_serial() -> Optional[str]:
"""Get device serial (cached)."""
global _serial
if _serial is None:
try:
_serial = HARDWARE.get_serial()
except Exception:
pass
return _serial
class DragonpilotApiClient:
"""
API client for api.dragonpilot.org.
Uses device serial from HARDWARE for authentication.
"""
def __init__(self, serial: str = None):
self.api_host = API_HOST
self.serial = serial if serial is not None else _get_serial()
@property
def is_authenticated(self) -> bool:
return self.serial is not None
def _headers(self) -> dict:
headers = {'Content-Type': 'application/json'}
if self.serial:
headers['X-Device-Serial'] = self.serial
return headers
def get_sync(self, endpoint: str, params: dict = None, timeout: int = 10) -> Optional[dict]:
try:
resp = requests.get(f"{self.api_host}{endpoint}", params=params, headers=self._headers(), timeout=timeout)
return resp.json() if resp.status_code == 200 else None
except Exception:
return None
def post_sync(self, endpoint: str, data: dict = None, timeout: int = 10) -> Optional[dict]:
try:
resp = requests.post(f"{self.api_host}{endpoint}", json=data, headers=self._headers(), timeout=timeout)
return resp.json() if resp.status_code == 200 else None
except Exception:
return None
async def get(self, endpoint: str, params: dict = None, timeout: int = 10) -> Optional[dict]:
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{self.api_host}{endpoint}",
params=params,
headers=self._headers(),
timeout=aiohttp.ClientTimeout(total=timeout)
) as resp:
return await resp.json() if resp.status == 200 else None
except Exception:
return None
async def post(self, endpoint: str, data: dict = None, timeout: int = 10) -> Optional[dict]:
try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.api_host}{endpoint}",
json=data,
headers=self._headers(),
timeout=aiohttp.ClientTimeout(total=timeout)
) as resp:
return await resp.json() if resp.status == 200 else None
except Exception:
return None
# Singleton client instance
_client: Optional[DragonpilotApiClient] = None
def get_client() -> DragonpilotApiClient:
"""Get the shared API client instance."""
global _client
if _client is None:
_client = DragonpilotApiClient()
return _client
@@ -1,269 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Dragonpilot Routing Provider
Uses api.dragonpilot.org routing endpoint with device serial authentication.
Falls back to OSRM if not authenticated or on error.
"""
from typing import Optional
from openpilot.common.swaglog import cloudlog
from ..base import RouteProvider
from ..models import Coordinate, Route, Step
from .client import get_client
def decode_polyline(polyline: str) -> list[tuple[float, float]]:
"""
Decode a Google/HERE Encoded Polyline into (lat, lon) tuples.
Algorithm: https://developers.google.com/maps/documentation/utilities/polylinealgorithm
"""
coordinates = []
index = 0
lat = 0
lng = 0
while index < len(polyline):
# Decode latitude
result = 0
shift = 0
while True:
b = ord(polyline[index]) - 63
index += 1
result |= (b & 0x1f) << shift
shift += 5
if b < 0x20:
break
lat += (~(result >> 1) if result & 1 else result >> 1)
# Decode longitude
result = 0
shift = 0
while True:
b = ord(polyline[index]) - 63
index += 1
result |= (b & 0x1f) << shift
shift += 5
if b < 0x20:
break
lng += (~(result >> 1) if result & 1 else result >> 1)
coordinates.append((lat / 1e5, lng / 1e5))
return coordinates
# Map action to OSRM-style maneuver type/modifier
ACTION_MAP = {
'depart': ('depart', ''),
'arrive': ('arrive', ''),
'turn': ('turn', ''),
'turn-left': ('turn', 'left'),
'turn-right': ('turn', 'right'),
'turn-slight-left': ('turn', 'slight left'),
'turn-slight-right': ('turn', 'slight right'),
'turn-sharp-left': ('turn', 'sharp left'),
'turn-sharp-right': ('turn', 'sharp right'),
'continue': ('continue', 'straight'),
'keep': ('continue', ''),
'merge': ('merge', ''),
'roundabout': ('roundabout', ''),
'roundaboutExit': ('roundabout', 'exit'),
'ferry': ('ferry', ''),
'uturn': ('turn', 'uturn'),
}
class DragonpilotRouteProvider(RouteProvider):
"""
Dragonpilot API routing provider.
Uses device serial for authentication. Falls back to OSRM on auth failure.
API Response format:
{
"route": {
"route_id": "...",
"distance_m": 6837,
"duration_s": 759,
"duration_traffic_s": 1595,
"polyline": "...",
"maneuvers": [{
"instruction": "Turn left onto Main Street",
"distance_m": 500,
"duration_s": 60,
"position": {"lat": 25.03, "lon": 121.56},
"action": "turn"
}],
"provider": "here"
},
"cached": false,
"provider": "here"
}
"""
name = "dragonpilot"
requires_api_key = False
supports_traffic = True
def __init__(self):
self._client = get_client()
self._fallback = None
def _get_fallback(self) -> RouteProvider:
"""Get fallback OSRM provider."""
if self._fallback is None:
from ..routing.osrm import OSRMRouteProvider
self._fallback = OSRMRouteProvider()
return self._fallback
async def get_route(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]] = None,
bearing: Optional[float] = None
) -> Optional[Route]:
"""Calculate route using Dragonpilot API (async)."""
if not self._client.is_authenticated:
cloudlog.warning("dragonpilot routing: no serial, using osrm fallback")
return await self._get_fallback().get_route(origin, destination, waypoints, bearing)
data = self._build_request(origin, destination, waypoints)
response = await self._client.post('/v1/route', data=data, timeout=30)
if response is None:
cloudlog.warning("dragonpilot routing: API error, using osrm fallback")
return await self._get_fallback().get_route(origin, destination, waypoints, bearing)
return self._parse_response(response)
def get_route_sync(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]] = None,
bearing: Optional[float] = None
) -> Optional[Route]:
"""Calculate route using Dragonpilot API (synchronous)."""
if not self._client.is_authenticated:
cloudlog.warning("dragonpilot routing: no serial, using osrm fallback")
return self._get_fallback().get_route_sync(origin, destination, waypoints, bearing)
data = self._build_request(origin, destination, waypoints)
response = self._client.post_sync('/v1/route', data=data, timeout=30)
if response is None:
cloudlog.warning("dragonpilot routing: API error, using osrm fallback")
return self._get_fallback().get_route_sync(origin, destination, waypoints, bearing)
return self._parse_response(response)
def _build_request(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]]
) -> dict:
"""Build API request body."""
origin = origin.to_wgs84() if hasattr(origin, 'to_wgs84') else origin
destination = destination.to_wgs84() if hasattr(destination, 'to_wgs84') else destination
data = {
'origin': {'lat': origin.latitude, 'lon': origin.longitude},
'destination': {'lat': destination.latitude, 'lon': destination.longitude},
}
if waypoints:
data['waypoints'] = []
for wp in waypoints:
wp = wp.to_wgs84() if hasattr(wp, 'to_wgs84') else wp
data['waypoints'].append({'lat': wp.latitude, 'lon': wp.longitude})
return data
def _parse_response(self, data: dict) -> Optional[Route]:
"""Parse API response into Route object."""
if not data:
return None
# Unwrap route object
route_data = data.get('route', data)
# Decode full route geometry
full_geometry = []
polyline = route_data.get('polyline', '')
if polyline:
try:
for lat, lon in decode_polyline(polyline):
coord = Coordinate(lat, lon)
full_geometry.append(coord)
except Exception:
pass
# Parse maneuvers into steps
steps = []
maneuvers = route_data.get('maneuvers', [])
for maneuver in maneuvers:
pos = maneuver.get('position', {})
action = maneuver.get('action', 'continue')
# Map action to maneuver type/modifier
maneuver_type, maneuver_modifier = ACTION_MAP.get(action, ('continue', ''))
# Get maneuver point
maneuver_point = None
if pos.get('lat') is not None and pos.get('lon') is not None:
maneuver_point = Coordinate(pos['lat'], pos['lon'])
# Use full instruction text as the step name
instruction = maneuver.get('instruction', '')
steps.append(Step(
distance=maneuver.get('distance_m', 0),
duration=maneuver.get('duration_s', 0),
name=instruction,
maneuver_type=maneuver_type,
maneuver_modifier=maneuver_modifier,
geometry=[maneuver_point] if maneuver_point else [],
speed_limit=None,
speed_limit_sign='vienna',
maneuver_point=maneuver_point,
))
# Use traffic duration if available
duration = route_data.get('duration_traffic_s') or route_data.get('duration_s', 0)
return Route(
steps=steps,
distance=route_data.get('distance_m', 0),
duration=duration,
geometry=full_geometry,
provider=self.name,
has_traffic=route_data.get('duration_traffic_s') is not None,
raw=data,
)
@@ -1,191 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Dragonpilot Search Provider
Uses api.dragonpilot.org geocoding endpoint for address search.
Falls back to Photon if not authenticated or on error.
"""
from typing import Optional
from openpilot.common.swaglog import cloudlog
from ..base import SearchProvider
from ..models import Coordinate, SearchResult
from .client import get_client
class DragonpilotSearchProvider(SearchProvider):
"""
Dragonpilot API search provider.
Uses device serial for authentication. Falls back to Photon on auth failure.
API Response format:
{
"results": [{
"id": "here:...",
"title": "Taipei 101",
"address": "No. 7, Section 5, Xinyi Road",
"position": {"lat": 25.033, "lon": 121.565},
"category": "landmark",
"distance_m": 150
}],
"provider": "here"
}
"""
name = "dragonpilot"
requires_api_key = False
def __init__(self):
self._client = get_client()
self._fallback = None
def _get_fallback(self) -> SearchProvider:
"""Get fallback Photon provider."""
if self._fallback is None:
from ..search.photon import PhotonSearchProvider
self._fallback = PhotonSearchProvider()
return self._fallback
async def search(
self,
query: str,
proximity: Optional[Coordinate] = None,
limit: int = 10
) -> list[SearchResult]:
"""Search for places using Dragonpilot API (async)."""
if not query or len(query) < 1:
return []
# Fall back to Photon if not authenticated
if not self._client.is_authenticated:
cloudlog.debug("dragonpilot search: no serial, using photon fallback")
return await self._get_fallback().search(query, proximity, limit)
params = {'q': query, 'limit': min(limit, 10)}
if proximity:
prox = proximity.to_wgs84() if hasattr(proximity, 'to_wgs84') else proximity
params['lat'] = prox.latitude
params['lon'] = prox.longitude
data = await self._client.get('/v1/geocode/autocomplete', params=params)
if data is None:
cloudlog.debug("dragonpilot search: API error, using photon fallback")
return await self._get_fallback().search(query, proximity, limit)
return self._parse_results(data, proximity, limit)
def search_sync(
self,
query: str,
proximity: Optional[Coordinate] = None,
limit: int = 10
) -> list[SearchResult]:
"""Search for places using Dragonpilot API (synchronous)."""
if not query or len(query) < 1:
return []
# Fall back to Photon if not authenticated
if not self._client.is_authenticated:
cloudlog.debug("dragonpilot search: no serial, using photon fallback")
return self._get_fallback().search_sync(query, proximity, limit)
params = {'q': query, 'limit': min(limit, 10)}
if proximity:
prox = proximity.to_wgs84() if hasattr(proximity, 'to_wgs84') else proximity
params['lat'] = prox.latitude
params['lon'] = prox.longitude
data = self._client.get_sync('/v1/geocode/autocomplete', params=params)
if data is None:
cloudlog.debug("dragonpilot search: API error, using photon fallback")
return self._get_fallback().search_sync(query, proximity, limit)
return self._parse_results(data, proximity, limit)
async def reverse_geocode(self, coord: Coordinate) -> Optional[SearchResult]:
"""Get address from coordinates. Falls back to Photon."""
return await self._get_fallback().reverse_geocode(coord)
def _parse_results(
self,
data: dict,
proximity: Optional[Coordinate],
limit: int
) -> list[SearchResult]:
"""Parse API response into SearchResult list."""
results = []
# Handle both GeoJSON format (features) and simple format (results)
items = data.get('features', data.get('results', []))
for item in items:
try:
# GeoJSON format
if 'geometry' in item:
coords = item['geometry']['coordinates']
lon, lat = coords[0], coords[1] # GeoJSON is [lon, lat]
props = item.get('properties', {})
title = props.get('title', 'Unknown')
address = props.get('address', '')
place_id = props.get('id')
distance = props.get('distance_m')
# Simple format
else:
pos = item.get('position', {})
lat = pos.get('lat')
lon = pos.get('lon')
title = item.get('title', 'Unknown')
address = item.get('address', '')
place_id = item.get('id')
distance = item.get('distance_m')
if lat is None or lon is None:
continue
coord = Coordinate(lat, lon)
# Calculate distance if not provided
if distance is None and proximity:
prox = proximity.to_wgs84() if hasattr(proximity, 'to_wgs84') else proximity
distance = Coordinate(lat, lon).distance_to(prox)
results.append(SearchResult(
name=title,
address=address,
coordinate=coord,
distance=distance,
place_id=place_id,
provider=self.name,
raw=item,
))
except (KeyError, TypeError, IndexError):
continue
return results[:limit]
@@ -1,56 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Provider factory for creating map service instances.
Uses api.dragonpilot.org for search and routing.
"""
from .config import ProviderConfig
from .base import SearchProvider, RouteProvider, TileProvider
class ProviderFactory:
"""
Factory for creating map providers.
Uses Dragonpilot API providers which have built-in fallback
to free providers (Photon/OSRM) when not authenticated.
"""
@classmethod
def create_search_provider(cls, config: ProviderConfig) -> SearchProvider:
"""Create search provider."""
from .dragonpilot.search import DragonpilotSearchProvider
return DragonpilotSearchProvider()
@classmethod
def create_route_provider(cls, config: ProviderConfig) -> RouteProvider:
"""Create route provider."""
from .dragonpilot.routing import DragonpilotRouteProvider
return DragonpilotRouteProvider()
@classmethod
def create_tile_provider(cls, config: ProviderConfig) -> TileProvider:
"""Create tile provider."""
from .tiles.openfreemap import OpenFreeMapTileProvider
return OpenFreeMapTileProvider()
@@ -1,188 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Map Service - Main entry point for all map operations.
Usage:
from dragonpilot.dashy.maa.providers import MapService
# In maad.py (sync):
map_service = MapService()
route = map_service.route_provider.get_route_sync(origin, dest)
# In server.py (async):
results = await MapService.get_instance().search_provider.search("Taipei 101")
# Get tile config for frontend:
tile_config = map_service.get_tile_config_for_js()
"""
from typing import Optional
from .config import ProviderConfig
from .factory import ProviderFactory
from .base import SearchProvider, RouteProvider, TileProvider
class MapService:
"""
Main entry point for all map operations.
Provides lazy-loaded access to search, routing, and tile providers.
Configuration is read from openpilot Params.
"""
_instance: Optional['MapService'] = None
def __init__(self, params=None):
"""
Initialize MapService.
Args:
params: Optional openpilot Params instance.
If None, will be created when needed.
"""
self._params = params
self._config: Optional[ProviderConfig] = None
self._search_provider: Optional[SearchProvider] = None
self._route_provider: Optional[RouteProvider] = None
self._tile_provider: Optional[TileProvider] = None
@classmethod
def get_instance(cls, params=None) -> 'MapService':
"""
Get singleton instance of MapService.
Args:
params: Optional Params instance for first-time initialization
"""
if cls._instance is None:
cls._instance = cls(params)
return cls._instance
@classmethod
def reset_instance(cls):
"""Reset singleton instance. Useful for testing or config reload."""
cls._instance = None
def _get_params(self):
"""Get or create Params instance."""
if self._params is None:
from openpilot.common.params import Params
self._params = Params()
return self._params
def reload_config(self):
"""
Reload configuration from params and recreate providers.
Call this after changing provider settings in params.
"""
self._config = ProviderConfig.from_params(self._get_params())
self._search_provider = None
self._route_provider = None
self._tile_provider = None
@property
def config(self) -> ProviderConfig:
"""Get current configuration, loading from params if needed."""
if self._config is None:
self._config = ProviderConfig.from_params(self._get_params())
return self._config
@property
def search_provider(self) -> SearchProvider:
"""Get search provider, creating if needed."""
if self._search_provider is None:
# Import providers to trigger registration
self._ensure_providers_imported()
self._search_provider = ProviderFactory.create_search_provider(self.config)
return self._search_provider
@property
def route_provider(self) -> RouteProvider:
"""Get route provider, creating if needed."""
if self._route_provider is None:
# Import providers to trigger registration
self._ensure_providers_imported()
self._route_provider = ProviderFactory.create_route_provider(self.config)
return self._route_provider
@property
def tile_provider(self) -> TileProvider:
"""Get tile provider, creating if needed."""
if self._tile_provider is None:
# Import providers to trigger registration
self._ensure_providers_imported()
self._tile_provider = ProviderFactory.create_tile_provider(self.config)
return self._tile_provider
def _ensure_providers_imported(self):
"""Import provider modules to trigger registration."""
try:
from . import search
from . import routing
from . import tiles
except ImportError:
pass
def get_tile_config_for_js(self) -> dict:
"""
Get tile configuration as dict for JavaScript frontend.
Returns dict suitable for sending to frontend via API.
"""
tile_config = self.tile_provider.get_tile_config()
style = self.tile_provider.get_style_json()
return {
'provider': self.config.tile_provider.value,
'url_template': tile_config.url_template,
'style': style,
'attribution': tile_config.attribution,
'min_zoom': tile_config.min_zoom,
'max_zoom': tile_config.max_zoom,
}
def get_provider_info(self) -> dict:
"""
Get information about current providers.
Useful for debugging and UI display.
"""
return {
'search': {
'provider': self.config.search_provider.value,
'name': self.search_provider.name,
'requires_api_key': self.search_provider.requires_api_key,
},
'route': {
'provider': self.config.route_provider.value,
'name': self.route_provider.name,
'requires_api_key': self.route_provider.requires_api_key,
'supports_traffic': self.route_provider.supports_traffic,
},
'tile': {
'provider': self.config.tile_provider.value,
'name': self.tile_provider.name,
'requires_api_key': self.tile_provider.requires_api_key,
},
}
-180
View File
@@ -1,180 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Data models for map providers.
These models provide a common interface for search results, routes, and coordinates
across different providers (OSRM, Mapbox, Google, AMap).
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
from typing import Optional
from enum import Enum
EARTH_MEAN_RADIUS = 6371007.2 # meters
class CoordinateSystem(Enum):
"""Coordinate reference system."""
WGS84 = "wgs84" # Standard GPS coordinates (used globally)
GCJ02 = "gcj02" # China encrypted coordinates (required for China maps)
@dataclass
class Coordinate:
"""
Geographic coordinate with optional coordinate system awareness.
Supports WGS-84 (standard GPS) and GCJ-02 (China) coordinate systems.
Provides distance and bearing calculations.
"""
latitude: float
longitude: float
system: CoordinateSystem = CoordinateSystem.WGS84
annotations: dict = field(default_factory=dict)
def distance_to(self, other: Coordinate) -> float:
"""Calculate Haversine distance to another coordinate in meters."""
dlat = math.radians(other.latitude - self.latitude)
dlon = math.radians(other.longitude - self.longitude)
haversine_dlat = math.sin(dlat / 2.0)
haversine_dlat *= haversine_dlat
haversine_dlon = math.sin(dlon / 2.0)
haversine_dlon *= haversine_dlon
y = haversine_dlat + \
math.cos(math.radians(self.latitude)) * \
math.cos(math.radians(other.latitude)) * \
haversine_dlon
x = 2 * math.asin(math.sqrt(y))
return x * EARTH_MEAN_RADIUS
def bearing_to(self, other: Coordinate) -> float:
"""Calculate bearing to another coordinate in degrees (0-360)."""
lat1, lat2 = math.radians(self.latitude), math.radians(other.latitude)
dlon = math.radians(other.longitude - self.longitude)
x = math.sin(dlon) * math.cos(lat2)
y = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dlon)
bearing = math.degrees(math.atan2(x, y))
return (bearing + 360) % 360
def to_wgs84(self) -> Coordinate:
"""Convert to WGS-84 coordinate system."""
if self.system == CoordinateSystem.WGS84:
return self
from .utils.coordinates import gcj02_to_wgs84
lat, lon = gcj02_to_wgs84(self.latitude, self.longitude)
return Coordinate(lat, lon, CoordinateSystem.WGS84, self.annotations.copy())
def to_gcj02(self) -> Coordinate:
"""Convert to GCJ-02 coordinate system (for China maps)."""
if self.system == CoordinateSystem.GCJ02:
return self
from .utils.coordinates import wgs84_to_gcj02
lat, lon = wgs84_to_gcj02(self.latitude, self.longitude)
return Coordinate(lat, lon, CoordinateSystem.GCJ02, self.annotations.copy())
def as_dict(self) -> dict:
"""Convert to dictionary."""
return {'latitude': self.latitude, 'longitude': self.longitude}
def __str__(self) -> str:
return f'Coordinate({self.latitude:.6f}, {self.longitude:.6f})'
def __repr__(self) -> str:
return self.__str__()
def __eq__(self, other) -> bool:
if not isinstance(other, Coordinate):
return False
return self.latitude == other.latitude and self.longitude == other.longitude
def __hash__(self) -> int:
return hash((self.latitude, self.longitude))
@dataclass
class SearchResult:
"""
Normalized search/geocoding result.
All search providers return results in this format.
"""
name: str # Display name (e.g., "Taipei 101")
address: str # Full address string
coordinate: Coordinate # Location
distance: Optional[float] = None # Distance from search proximity (meters)
place_id: Optional[str] = None # Provider-specific place ID
provider: str = "" # Provider name (e.g., "photon", "mapbox")
raw: dict = field(default_factory=dict) # Raw provider response for debugging
@dataclass
class Step:
"""
Single navigation step/maneuver in a route.
Represents one turn or road segment with its geometry.
"""
distance: float # Step distance in meters
duration: float # Step duration in seconds
duration_typical: Optional[float] = None # Typical duration (with traffic)
name: str = "" # Road/street name
maneuver_type: str = "" # Type: turn, fork, off ramp, merge, etc.
maneuver_modifier: str = "" # Direction: left, right, slight left, etc.
geometry: list[Coordinate] = field(default_factory=list) # Path coordinates
speed_limit: Optional[float] = None # Speed limit in m/s
speed_limit_sign: str = "vienna" # Sign type: vienna or mutcd
maneuver_point: Optional[Coordinate] = None # Explicit maneuver location from OSRM
@dataclass
class Route:
"""
Complete navigation route.
Contains all steps from origin to destination with total distance/duration.
"""
steps: list[Step] # List of navigation steps
distance: float # Total distance in meters
duration: float # Total duration in seconds
duration_typical: Optional[float] = None # Typical duration (with traffic)
geometry: list[Coordinate] = field(default_factory=list) # Full route polyline
provider: str = "" # Provider name
has_traffic: bool = False # Whether duration includes traffic
raw: dict = field(default_factory=dict) # Raw provider response
@dataclass
class TileConfig:
"""
Map tile configuration for frontend display.
"""
url_template: str # URL template with {z}/{x}/{y}
style_url: Optional[str] = None # MapLibre style URL
attribution: str = "" # Map attribution text
min_zoom: int = 0
max_zoom: int = 22
@@ -1,5 +0,0 @@
"""Routing providers."""
from .osrm import OSRMRouteProvider
__all__ = ['OSRMRouteProvider']
@@ -1,190 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
OSRM Route Provider.
Uses the free OSRM (Open Source Routing Machine) API for routing.
No API key required.
OSRM API Docs: http://project-osrm.org/docs/v5.24.0/api/
"""
from typing import Optional
import aiohttp
import requests
from ..base import RouteProvider
from ..models import Coordinate, Route, Step
OSRM_URL = 'https://router.project-osrm.org'
class OSRMRouteProvider(RouteProvider):
"""
Free OSRM routing provider.
No API key required. Uses public OSRM demo server.
Does not support traffic data.
"""
name = "osrm"
requires_api_key = False
supports_traffic = False
def __init__(self):
"""Initialize OSRM provider."""
pass
async def get_route(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]] = None,
bearing: Optional[float] = None
) -> Optional[Route]:
"""Calculate route using OSRM API (async)."""
url, params = self._build_request(origin, destination, waypoints, bearing)
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as resp:
if resp.status != 200:
return None
data = await resp.json()
except Exception:
return None
return self._parse_response(data)
def get_route_sync(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]] = None,
bearing: Optional[float] = None
) -> Optional[Route]:
"""Calculate route using OSRM API (synchronous for maad.py)."""
url, params = self._build_request(origin, destination, waypoints, bearing)
try:
resp = requests.get(url, params=params, timeout=10)
if resp.status_code != 200:
return None
data = resp.json()
if data.get('code') != 'Ok':
return None
except Exception:
return None
return self._parse_response(data)
def _build_request(
self,
origin: Coordinate,
destination: Coordinate,
waypoints: Optional[list[Coordinate]],
bearing: Optional[float]
) -> tuple[str, dict]:
"""Build OSRM API request URL and params."""
# Convert to WGS-84 if needed (OSRM uses WGS-84)
origin = origin.to_wgs84() if hasattr(origin, 'to_wgs84') and origin.system.value != 'wgs84' else origin
destination = destination.to_wgs84() if hasattr(destination, 'to_wgs84') and destination.system.value != 'wgs84' else destination
# Build coordinate string: lon,lat;lon,lat;...
all_coords = [(origin.longitude, origin.latitude)]
if waypoints:
for wp in waypoints:
wp = wp.to_wgs84() if hasattr(wp, 'to_wgs84') and wp.system.value != 'wgs84' else wp
all_coords.append((wp.longitude, wp.latitude))
all_coords.append((destination.longitude, destination.latitude))
# Limit coordinate precision to 6 decimal places (about 0.1m accuracy)
coords_str = ';'.join([f'{lon:.6f},{lat:.6f}' for lon, lat in all_coords])
url = f"{OSRM_URL}/route/v1/driving/{coords_str}"
params = {
'overview': 'full',
'geometries': 'geojson',
'steps': 'true',
}
# Add bearing if provided (helps with route direction at start)
# OSRM bearings format: bearing,range for each coord, separated by ;
# Note: Disabled for now due to URL encoding issues with semicolons
# TODO: Re-enable once we properly handle the bearings parameter
# if bearing is not None:
# bearing_parts = [f"{int(bearing) % 360},90"] + [''] * (len(all_coords) - 1)
# url += f"?bearings={';'.join(bearing_parts)}"
# return url, params
return url, params
def _parse_response(self, data: dict) -> Optional[Route]:
"""Parse OSRM API response into Route object."""
if data.get('code') != 'Ok' or not data.get('routes'):
return None
route_data = data['routes'][0]
steps = []
full_geometry = []
for leg in route_data.get('legs', []):
for step in leg.get('steps', []):
maneuver = step.get('maneuver', {})
# Parse geometry coordinates
geometry = []
for coord in step.get('geometry', {}).get('coordinates', []):
# OSRM uses [lon, lat] order
c = Coordinate(coord[1], coord[0])
geometry.append(c)
full_geometry.append(c)
step_name = step.get('name', '')
# Extract explicit maneuver location from OSRM
maneuver_location = maneuver.get('location') # [lon, lat]
maneuver_point = None
if maneuver_location and len(maneuver_location) == 2:
maneuver_point = Coordinate(maneuver_location[1], maneuver_location[0])
steps.append(Step(
distance=step.get('distance', 0),
duration=step.get('duration', 0),
name=step_name,
maneuver_type=maneuver.get('type', ''),
maneuver_modifier=maneuver.get('modifier', ''),
geometry=geometry,
speed_limit=None,
speed_limit_sign='vienna',
maneuver_point=maneuver_point,
))
return Route(
steps=steps,
distance=route_data.get('distance', 0),
duration=route_data.get('duration', 0),
geometry=full_geometry,
provider=self.name,
has_traffic=False,
raw=route_data,
)
@@ -1,5 +0,0 @@
"""Search providers."""
from .photon import PhotonSearchProvider
__all__ = ['PhotonSearchProvider']
@@ -1,198 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Photon Search Provider.
Uses the free Photon (Komoot) geocoding API for address search.
No API key required, no rate limits.
Photon API: https://photon.komoot.io/
"""
from typing import Optional
import aiohttp
import requests
from ..base import SearchProvider
from ..models import Coordinate, SearchResult
PHOTON_URL = 'https://photon.komoot.io/api'
class PhotonSearchProvider(SearchProvider):
"""
Free Photon geocoding provider.
No API key required. Fast, reliable, uses OpenStreetMap data.
"""
name = "photon"
requires_api_key = False
def __init__(self):
"""Initialize Photon provider."""
pass
async def search(
self,
query: str,
proximity: Optional[Coordinate] = None,
limit: int = 10
) -> list[SearchResult]:
"""Search for places using Photon API (async)."""
if not query or len(query) < 2:
return []
params = {'q': query, 'limit': min(limit + 5, 15)} # Request extra for filtering
if proximity:
prox = proximity.to_wgs84() if hasattr(proximity, 'to_wgs84') else proximity
params['lat'] = prox.latitude
params['lon'] = prox.longitude
try:
async with aiohttp.ClientSession() as session:
async with session.get(PHOTON_URL, params=params, timeout=aiohttp.ClientTimeout(total=10)) as resp:
if resp.status != 200:
return []
data = await resp.json()
except Exception:
return []
return self._parse_results(data, proximity, limit)
def search_sync(
self,
query: str,
proximity: Optional[Coordinate] = None,
limit: int = 10
) -> list[SearchResult]:
"""Search for places using Photon API (synchronous)."""
if not query or len(query) < 2:
return []
params = {'q': query, 'limit': min(limit + 5, 15)}
if proximity:
prox = proximity.to_wgs84() if hasattr(proximity, 'to_wgs84') else proximity
params['lat'] = prox.latitude
params['lon'] = prox.longitude
try:
resp = requests.get(PHOTON_URL, params=params, timeout=10)
if resp.status_code != 200:
return []
data = resp.json()
except Exception:
return []
return self._parse_results(data, proximity, limit)
async def reverse_geocode(
self,
coord: Coordinate
) -> Optional[SearchResult]:
"""Get address from coordinates using Photon reverse API."""
wgs_coord = coord.to_wgs84() if hasattr(coord, 'to_wgs84') else coord
url = f"{PHOTON_URL}/reverse"
params = {'lat': wgs_coord.latitude, 'lon': wgs_coord.longitude}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as resp:
if resp.status != 200:
return None
data = await resp.json()
except Exception:
return None
features = data.get('features', [])
if not features:
return None
feature = features[0]
props = feature.get('properties', {})
parts = [props.get('name'), props.get('street'), props.get('city')]
address = ', '.join(filter(None, parts))
return SearchResult(
name=address.split(',')[0] if address else 'Unknown',
address=address or 'Unknown location',
coordinate=coord,
provider=self.name,
raw=feature,
)
def _parse_results(
self,
data: dict,
proximity: Optional[Coordinate],
limit: int
) -> list[SearchResult]:
"""Parse Photon GeoJSON response into SearchResult list."""
results = []
for feature in data.get('features', []):
try:
coords = feature['geometry']['coordinates']
props = feature.get('properties', {})
# Create coordinate (Photon uses [lon, lat] GeoJSON order)
coord = Coordinate(coords[1], coords[0])
# Build address from properties
parts = [
props.get('name'),
props.get('street'),
props.get('city'),
props.get('state'),
props.get('country')
]
address = ', '.join(filter(None, parts))
# Calculate distance if proximity provided
distance = None
if proximity:
prox = proximity.to_wgs84() if hasattr(proximity, 'to_wgs84') else proximity
search_coord = Coordinate(coords[1], coords[0]) # Use WGS84 for distance
distance = search_coord.distance_to(prox)
# Determine display name
name = props.get('name') or props.get('street') or (address.split(',')[0] if address else 'Unknown')
results.append(SearchResult(
name=name,
address=address or 'Unknown location',
coordinate=coord,
distance=distance,
place_id=props.get('osm_id'),
provider=self.name,
raw=feature,
))
except (KeyError, IndexError):
continue
# Sort by distance if proximity provided
if proximity:
results.sort(key=lambda r: r.distance if r.distance is not None else float('inf'))
return results[:limit]
@@ -1,6 +0,0 @@
"""Tile providers."""
# Import providers to register them with the factory
from .openfreemap import OpenFreeMapTileProvider
__all__ = ['OpenFreeMapTileProvider']
@@ -1,88 +0,0 @@
"""
Copyright (c) 2026, Rick Lan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, and/or sublicense,
for non-commercial purposes only, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
- Commercial use (e.g. use in a product, service, or activity intended to
generate revenue) is prohibited without explicit written permission from
the copyright holder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
OpenFreeMap Tile Provider.
Uses free OpenFreeMap tiles. No API key required.
OpenFreeMap: https://openfreemap.org/
"""
from ..base import TileProvider
from ..models import TileConfig
from ..factory import ProviderFactory
from ..config import TileProviderType
@ProviderFactory.register_tile(TileProviderType.OPENFREEMAP)
class OpenFreeMapTileProvider(TileProvider):
"""
Free OpenFreeMap tile provider.
No API key required. Uses OpenStreetMap data with nice styling.
"""
name = "openfreemap"
requires_api_key = False
def __init__(self, api_key: str = None):
"""Initialize OpenFreeMap provider."""
pass # No API key needed
def get_tile_config(self) -> TileConfig:
"""Get tile configuration."""
return TileConfig(
url_template="https://tiles.openfreemap.org/planet/{z}/{x}/{y}.pbf",
style_url="https://tiles.openfreemap.org/styles/liberty",
attribution='<a href="https://openfreemap.org">OpenFreeMap</a> | <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
min_zoom=0,
max_zoom=20,
)
def get_style_json(self) -> dict:
"""
Get MapLibre GL style JSON.
This style is optimized for navigation display with good contrast
and road visibility.
"""
return {
"version": 8,
"name": "OpenFreeMap Liberty",
"sources": {
"openmaptiles": {
"type": "vector",
"url": "https://tiles.openfreemap.org/planet"
}
},
"glyphs": "https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf",
"sprite": "https://tiles.openfreemap.org/styles/liberty/sprite",
"layers": [
# Simplified layer config - the actual style is loaded from style_url
# This is a fallback/reference
{
"id": "background",
"type": "background",
"paint": {"background-color": "#f8f4f0"}
}
]
}
@@ -1,5 +0,0 @@
"""Utility functions for map providers."""
from .coordinates import wgs84_to_gcj02, gcj02_to_wgs84
__all__ = ['wgs84_to_gcj02', 'gcj02_to_wgs84']

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