Compare commits

..

632 Commits

Author SHA1 Message Date
firestarsdog 4898152b1e Large UI Speed Display 2026-10-09 23:49:59 -04:00
firestar5683 cc181947b6 Build 2026-10-09 19:48:02 -05:00
firestar5683 8984fb8d26 Publication test fixtures 2026-10-09 19:47:47 -05:00
firestar5683 1fd07f1cd5 Intent projection recovery 2026-10-09 19:47:38 -05:00
firestar5683 34625050a9 Forced offroad display timeout 2026-10-09 18:49:02 -05:00
firestar5683 7267af7411 Build 2026-10-09 18:35:38 -05:00
firestar5683 df38093ff1 AOL transport recovery 2026-10-09 18:34:26 -05:00
firestar5683 5c337e297a Build 2026-10-09 16:41:24 -05:00
firestar5683 c0c1c0353d GM lateral recovery 2026-10-09 16:41:11 -05:00
firestar5683 ac1968a384 Build 2026-10-09 16:13:24 -05:00
firestar5683 89aa8a294e Volt steering recovery 2026-10-09 16:11:28 -05:00
firestar5683 26a3ec8fcd Build 2026-10-09 14:35:01 -05:00
firestar5683 969ce02810 AOL message pairing 2026-10-09 14:34:54 -05:00
firestar5683 9ff53d39c2 Build 2026-10-09 12:43:43 -05:00
firestar5683 65a5b48c30 CEM green release 2026-10-09 12:42:52 -05:00
firestar5683 0e76d0c6f6 Build 2026-10-09 12:35:58 -05:00
firestar5683 2764593fff Replay schema contract 2026-10-09 12:35:58 -05:00
firestar5683 3fc56ef7e4 Replay speed limit display 2026-10-09 12:35:57 -05:00
firestar5683 6dbfce2fc5 Build 2026-10-09 12:19:56 -05:00
firestar5683 0e30087e9a Volt steering authority 2026-10-09 12:13:42 -05:00
firestar5683 41bc6d6704 CAN FD replay reference 2026-10-09 12:13:42 -05:00
firestar5683 6086120108 Vehicle caller checks 2026-10-09 12:13:42 -05:00
firestarsdog 77b44257a0 Old Route -> New Replay shim
added retirement instructions
2026-10-09 12:58:07 -04:00
firestar5683 139a5302e9 I6 Domi Trial 2026-10-09 11:21:56 -05:00
firestarsdog 6ddeb91d16 Large UI Flicker Pass 2 2026-10-09 11:54:06 -04:00
firestar5683 931620ff00 I6 Domi Trial 2026-10-09 10:30:13 -05:00
firestarsdog b41f3df1fb Large UI Replay Flicker Be Gone 2026-10-09 11:19:26 -04:00
firestar5683 9d46558823 Build 2026-10-09 09:08:49 -05:00
firestar5683 5be29a9ad0 CAN FD steering 2026-10-09 09:05:37 -05:00
firestar5683 fc3290c1f1 Build 2026-10-09 08:42:06 -05:00
firestar5683 45de4e2929 Lateral authority 2026-10-09 08:36:42 -05:00
firestar5683 86e3002cb2 Build 2026-10-09 06:19:42 -05:00
firestar5683 c8ec1ae5ec GM recovery checks 2026-10-09 06:19:18 -05:00
firestar5683 531daa1f22 Build 2026-10-09 05:15:26 -05:00
firestar5683 24dc7b9112 Safety checks 2026-10-09 05:12:53 -05:00
firestar5683 ae7b168a86 Build 2026-10-09 03:35:05 -05:00
firestar5683 eb46cdf31a Galaxy capability tests 2026-10-09 03:07:24 -05:00
firestar5683 63fbaa6b24 Hyundai AOL button handling 2026-10-09 03:07:24 -05:00
firestarsdog c34b0f97de Revert "Fix replay jitter"
This reverts commit ef42c19924.
2026-10-09 03:44:36 -04:00
firestarsdog ef42c19924 Fix replay jitter 2026-10-09 03:12:10 -04:00
Prabhaav Pillai c6bc07a9a0 (Another sixth) round of Galaxy UI bug fixes
Improved busy state handling across sound packs, backup settings, and vehicle controls.
Prevented duplicate requests and user interactions during ongoing processes.
Added haptic feedback for smoother user interactions.
Introduced automated settings state tests to improve reliability.
2026-10-09 02:32:17 -04:00
firestar5683 d2ae98eb39 Build 2026-10-08 22:25:02 -05:00
firestar5683 670927368c Bolt cruise cancellation 2026-10-08 22:19:14 -05:00
firestar5683 71c7bd8411 Build 2026-10-08 21:49:19 -05:00
firestar5683 deb1702cc2 Automatic lane change activation 2026-10-08 21:45:44 -05:00
firestar5683 980f9fb081 AOL pause settings 2026-10-08 21:45:44 -05:00
firestar5683 65586189f7 Bolt regen command cadence 2026-10-08 21:45:43 -05:00
Prabhaav Pillai 21806fa504 (Another fifth) round of Galaxy UI bug fixes
Updated headers and descriptions across various pages to enhance user understanding.
Added navigation and interaction improvements in the logs, longitudinal curves, and model laboratory sections.
Enhanced error handling and user notifications in map operations and vehicle controls.
Improved accessibility features, including ARIA labels and roles for better screen reader support.
Consolidated and clarified messages related to software updates and branch selections.
Streamlined the layout and organization of settings and configuration pages for better usability.
Smoother Transitions
2026-10-08 21:57:58 -04:00
firestar5683 9443422926 Build 2026-10-08 19:19:46 -05:00
firestar5683 015c810b17 Camera CPU recovery 2026-10-08 19:17:11 -05:00
firestar5683 cfc1243a2f Build 2026-10-08 18:52:10 -05:00
firestar5683 a1f1e53e87 Personality lead colors 2026-10-08 18:48:48 -05:00
firestar5683 e18c328081 Build 2026-10-08 18:25:06 -05:00
firestar5683 c7088d98d6 Volt optional gas limits 2026-10-08 18:23:11 -05:00
firestar5683 31abbcfe2c Build 2026-10-08 17:57:03 -05:00
firestar5683 1e0cb24fd2 Controller learning help 2026-10-08 17:54:06 -05:00
firestar5683 831413dd9e Volt low-speed controls 2026-10-08 17:54:06 -05:00
firestar5683 e05f7e9925 Volt AOL recovery 2026-10-08 17:54:05 -05:00
firestar5683 8c79866e22 Build 2026-10-08 17:00:51 -05:00
firestar5683 02043d1276 Sensor process recovery 2026-10-08 16:58:33 -05:00
firestar5683 4022b9304c Build 2026-10-08 16:39:17 -05:00
firestar5683 6f7fe4068c Volt safety checks 2026-10-08 16:31:31 -05:00
firestar5683 d0bf0728b0 RAV4 torque tuning 2026-10-08 16:31:30 -05:00
firestar5683 873089ef7a Sonata Hybrid wheel defaults 2026-10-08 16:31:30 -05:00
firestar5683 41d11d08af Mach-E control tuning 2026-10-08 16:31:30 -05:00
firestar5683 f499290a35 Build 2026-10-08 15:24:26 -05:00
firestar5683 89f60950ed Audio underrun diagnostics 2026-10-08 15:21:28 -05:00
firestar5683 35186e67fd Replay clock sampling 2026-10-08 15:21:27 -05:00
firestar5683 99ef4b9646 Model catalog checks 2026-10-08 15:21:27 -05:00
firestar5683 efbee4155e Vehicle selector guards 2026-10-08 15:21:27 -05:00
firestar5683 fa7c0e155f Volt SDGM acceleration envelope 2026-10-08 15:21:27 -05:00
firestar5683 f71f19cf7a Build 2026-10-08 14:25:05 -05:00
firestar5683 136d2f168c Onroad lead icon and lane-change widgets 2026-10-08 14:22:47 -05:00
whoisdomi 0709a36b5f Update Manifest 2026-10-08 13:20:35 -05:00
firestar5683 9507f5f07d Build 2026-10-08 12:34:48 -05:00
firestar5683 befa15cdfd GM configuration lookup performance 2026-10-08 12:31:45 -05:00
firestar5683 7ea69c1c0b Volt startup handoff 2026-10-08 12:31:45 -05:00
firestar5683 c054e427c4 Bugsink error reporting 2026-10-08 12:31:44 -05:00
firestar5683 2a3e0420dd Conditional mode test typing 2026-10-08 09:40:10 -05:00
firestar5683 3ce0894783 Build 2026-10-08 09:33:34 -05:00
firestar5683 5fe62337aa Toyota manual brake hold 2026-10-08 09:31:10 -05:00
firestar5683 2d8e40060e Personality controls 2026-10-08 09:31:09 -05:00
firestar5683 6bb0f8b0c2 Experimental driving mode 2026-10-08 09:31:09 -05:00
firestar5683 0632ab9f28 Safety mutation equivalence 2026-10-08 09:31:09 -05:00
firestar5683 1fb7aa116b Honda transmission detection 2026-10-08 09:31:09 -05:00
firestar5683 a74a6f2e45 Volt hold heartbeat 2026-10-08 09:31:08 -05:00
firestar5683 bbad2d9e4b GM control loop performance 2026-10-08 09:31:08 -05:00
firestar5683 43efb4ce66 Build 2026-10-08 07:04:51 -05:00
firestar5683 3c0301bce2 CI mutation progress 2026-10-08 07:03:45 -05:00
firestar5683 5b6dcdd5e8 Hyundai longitudinal parity 2026-10-08 06:42:59 -05:00
firestar5683 532a3604d0 Build 2026-10-08 03:46:39 -05:00
firestar5683 1830f41a09 GM test serialization 2026-10-08 03:44:49 -05:00
firestar5683 0d34efe7c3 CI build guards 2026-10-08 03:44:48 -05:00
firestar5683 be5fb88423 Build 2026-10-08 03:06:47 -05:00
firestar5683 e29f60b188 Migration and UI fixtures 2026-10-08 03:02:19 -05:00
firestar5683 65a0ef7679 Galaxy HTTP responses 2026-10-08 03:02:19 -05:00
firestar5683 17d3f08641 Runtime type contracts 2026-10-08 03:02:19 -05:00
firestar5683 c2a47219cf Vehicle regression fixtures 2026-10-08 03:02:19 -05:00
firestar5683 38b86cb991 Safety regression coverage 2026-10-08 03:02:18 -05:00
firestar5683 b59074b296 Native test coverage 2026-10-08 02:21:34 -05:00
firestar5683 51e08a5eda Galaxy test fixtures 2026-10-08 02:21:34 -05:00
firestar5683 602932a104 Safety qualification harness 2026-10-08 02:21:34 -05:00
firestar5683 2fde98dab6 Display regression expectations 2026-10-08 02:21:34 -05:00
firestar5683 a5ef6002ec Mac test prerequisites 2026-10-08 02:21:33 -05:00
firestar5683 65a2b29b34 Regression test fixtures 2026-10-08 02:21:33 -05:00
firestar5683 f7c0090274 Replay expectations 2026-10-08 02:21:33 -05:00
firestar5683 745f87e735 Runtime type contracts 2026-10-08 02:21:33 -05:00
firestar5683 be163b12da Recording metadata portability 2026-10-08 02:21:33 -05:00
firestar5683 dd7314e7a7 Galaxy HTTP server 2026-10-08 02:21:32 -05:00
firestar5683 e33c74d639 Replay clock overflow 2026-10-08 02:21:32 -05:00
firestar5683 32ea41e2e7 Hyundai wheel capabilities 2026-10-08 02:21:32 -05:00
firestar5683 51dbc31513 Schema policy 2026-10-08 02:21:32 -05:00
firestar5683 71daa79f66 CI workflow reporting 2026-10-08 02:21:32 -05:00
prabhaavp 144b48e65f Merge pull request #210 from dirwin31:Stardust-PR-4
Merged! This adds an on-road map with offline map downloads, improves navigation search and shortcuts, and expands layout customization. Awesome work and thanks for the contribution!
2026-10-08 03:11:39 -04:00
Prabhaav Pillai d7e4c021b6 Updated RouteRuntime to read routing settings instead of general settings.
Modified test cases to ensure proper handling of route changes and storage failures.
Enhanced tests for temporary destination handling and suggestions.
Improved scaling logic in Android Auto UI to account for letterbox offsets.
Added CSS for offline road maps in the Android Auto overlay.
Refined touch handling in projection favorites to prevent interaction issues.
Ensured proper state management in map overlays and tile reading.
2026-10-08 03:10:10 -04:00
dirwin31 d5f7852c0d Rebase Tweaks 2026-10-07 22:34:05 -07:00
dirwin31 6afa673a3b Buttins Be whack 2026-10-07 22:34:05 -07:00
dirwin31 e7a5040267 NBigger ETA, Easier Nav, Map Tail, Video Lag and Gaurd Rail Comments 2026-10-07 22:34:05 -07:00
Danny fca44e169c Make Favorites Easier, Condense Mobile Nav Page. 2026-10-07 22:34:05 -07:00
dirwin31 f2f32b76a7 Fix Screen Smoothing Again, AAwireless v1, Lane Changes 2026-10-07 22:34:05 -07:00
Danny 1b112b898d Pretty Nav Page 2026-10-07 22:34:05 -07:00
Danny 17463be653 Add Favorites Widgets 2026-10-07 22:34:05 -07:00
Danny 236c794eff Optimize Maps, No Frogs 2026-10-07 22:34:04 -07:00
dirwin31 ddd6eeb7f0 Better Drag and drop 2026-10-07 22:34:04 -07:00
dirwin31 014c8618ae Screen Setup Page ReOrg 2026-10-07 22:34:04 -07:00
dirwin31 ce83d86818 Allow for dropped params keys 2026-10-07 22:34:04 -07:00
Danny 6fc37f8aae Map Tweaks 2026-10-07 22:34:04 -07:00
Danny 2ce246f6a4 Mappsss 2026-10-07 22:34:04 -07:00
firestar5683 b005656ed7 Build 2026-10-07 23:58:07 -05:00
firestar5683 1af52d8a7b Mach-E curvature preview 2026-10-07 23:55:41 -05:00
firestar5683 b7d6827450 MPC terminal constraints 2026-10-07 23:55:41 -05:00
firestar5683 3c406e4374 Build 2026-10-07 23:30:59 -05:00
firestar5683 57967e709c Calibration reset 2026-10-07 23:29:12 -05:00
firestar5683 4a0e2eb0ba Speed limit defaults 2026-10-07 23:29:12 -05:00
firestar5683 8429b06436 Big UI torque bar 2026-10-07 23:29:11 -05:00
firestar5683 b078f9fe6a Build 2026-10-07 22:51:17 -05:00
firestar5683 b2896e7180 Test fixtures 2026-10-07 22:49:59 -05:00
firestar5683 fb63d4442d Build 2026-10-07 22:47:10 -05:00
firestar5683 3c0458e646 Volt resume test clocks 2026-10-07 22:47:10 -05:00
firestar5683 6800730446 Volt dashboard messages 2026-10-07 22:47:10 -05:00
firestar5683 b9df324f19 Build 2026-10-07 22:47:09 -05:00
firestar5683 14c1349a5d Control loop overhead 2026-10-07 22:47:09 -05:00
firestar5683 acaee3b624 Build 2026-10-07 22:47:09 -05:00
firestar5683 ce09aec43d Recorded replay state 2026-10-07 22:47:09 -05:00
firestar5683 3fa904765c Build 2026-10-07 22:47:09 -05:00
firestar5683 ef33866889 C4 personality indicator 2026-10-07 22:47:08 -05:00
firestar5683 67b213a5aa Driving personality switching 2026-10-07 22:47:08 -05:00
firestarsdog 30477f3cef Large UI Gamma Effect Trial 2026-10-07 21:17:47 -04:00
firestarsdog 2564796b78 Set speed favorite fix 2026-10-07 20:30:27 -04:00
Prabhaav Pillai 856320d333 (Another fourth) round of Galaxy UI bug fixes
Improved UI consistency and responsiveness
added Playwright tests
implemented toggle preference backup/restore with validation and unit tests
moved navigation styling and state management.
2026-10-07 20:28:47 -04:00
firestar5683 e41ff41107 Build 2026-10-07 18:22:53 -05:00
firestar5683 38bca18f8b Volt startup keepalives 2026-10-07 18:20:27 -05:00
firestar5683 fc60ef16c6 GM profile lookup 2026-10-07 17:52:03 -05:00
Daniel Koepping dd3241e9f9 chestnut: show switch branch alert only on release (#39049)
chestnut: only show branch alert on release branches

Original-upstream-commit: 3f06ed3bc7a4cc9c320be1f7d03dcd8a885a8e6f
Upstream: https://github.com/commaai/openpilot/commit/3f06ed3bc7a4cc9c320be1f7d03dcd8a885a8e6f
2026-10-07 15:13:02 -07:00
Harald Schäfer 3d7509c4ca Enable Experimental Mode by default without confirmation (#39043)
* Enable Experimental Mode by default without confirmation

* Update toggles.py

* Fix formatting in End-to-End Longitudinal Control description

Original-upstream-commit: f21bfc36be5f423f31b2c2a948d1e2eff91b02e8
Upstream: https://github.com/commaai/openpilot/commit/f21bfc36be5f423f31b2c2a948d1e2eff91b02e8
2026-10-07 07:59:43 -07:00
Adeeb Shihadeh 5d16c4c9af Fix Athena test port collision (#39048)
Original-upstream-commit: 657d3a4f7c248fd3dd37d6520bf4ea91eeba57d7
Upstream: https://github.com/commaai/openpilot/commit/657d3a4f7c248fd3dd37d6520bf4ea91eeba57d7
2026-10-06 23:43:16 -07:00
Daniel Koepping cf9fb44a5b modeld: drop chestnut on fallback (#39044)
Original-upstream-commit: a0d47bcd08d9c359ccc7bd73145ed90ade2255ab
Upstream: https://github.com/commaai/openpilot/commit/a0d47bcd08d9c359ccc7bd73145ed90ade2255ab
2026-10-06 18:43:50 -07:00
Daniel Koepping 120cf0adb6 ui: longitudinal policy indicator (#39040)
* ui: mici longitudinal icons

* ui: rm orange state

* ui: lead policy indicator

* fix

* show on op long

* make own widget

* sync long indicator with dmoji

* fast fade in with dmoji

Original-upstream-commit: 035a447d1f0f0ef5abf2ed9724e33d9265399019
Upstream: https://github.com/commaai/openpilot/commit/035a447d1f0f0ef5abf2ed9724e33d9265399019
2026-10-06 15:51:25 -07:00
stef 1e891dd9f2 ui: bookmark fill (#38631)
* bookmark fill

* fade and sticky click

* fade out animation

* no funny animation

* clean

---------

Co-authored-by: Daniel Koepping <elkoled@gmail.com>

Original-upstream-commit: 5b2a59d288f9d1eb3123f7af81ffab4159600b1f
Upstream: https://github.com/commaai/openpilot/commit/5b2a59d288f9d1eb3123f7af81ffab4159600b1f
2026-10-06 15:42:39 -07:00
firestar5683 72f094753e Galaxy vehicle categories 2026-10-07 17:41:39 -05:00
firestar5683 181b8059f1 Build 2026-10-07 16:56:09 -05:00
firestar5683 5c14b29fc1 Volt radar recovery alert 2026-10-07 16:48:50 -05:00
firestar5683 8efe5bac09 Build 2026-10-07 16:12:40 -05:00
firestar5683 217500af2a GM torque learning 2026-10-07 16:12:40 -05:00
firestar5683 65a1e2bef9 GM longitudinal preferences 2026-10-07 16:12:40 -05:00
firestar5683 0a3b849c5d GM runtime settings 2026-10-07 16:12:40 -05:00
firestar5683 4a13f306d8 Sound callback diagnostics 2026-10-07 16:12:40 -05:00
firestar5683 7ef391311e Cruise settings 2026-10-07 16:12:40 -05:00
firestar5683 68e655fc9e Build 2026-10-07 12:46:33 -05:00
firestar5683 8215eeb362 Bolt automatic identity 2026-10-07 12:43:24 -05:00
firestar5683 3d7c83eb5f Sound callback timing 2026-10-07 12:25:01 -05:00
firestar5683 3a60707936 Build 2026-10-07 12:10:27 -05:00
firestar5683 9b03a5b233 Vision workload priority 2026-10-07 12:06:03 -05:00
firestar5683 45ab728330 Bolt AOL startup and recovery 2026-10-07 11:42:39 -05:00
firestar5683 1380d84574 Build 2026-10-07 10:19:16 -05:00
firestar5683 959f0e837d Vision inference scheduling 2026-10-07 10:13:24 -05:00
firestar5683 3236168a74 Vision sign lifecycle 2026-10-07 10:13:24 -05:00
firestar5683 f3903a1051 AOL intent recovery 2026-10-07 10:00:53 -05:00
firestar5683 a7d0248b62 CEM stop release 2026-10-07 09:56:59 -05:00
firestar5683 c01f815736 Shared UI performance 2026-10-07 09:10:24 -05:00
firestar5683 e91fb76713 C4 UI performance 2026-10-07 08:50:10 -05:00
firestar5683 6f026efb13 Build 2026-10-07 07:27:18 -05:00
firestar5683 e4232643fa Restore EV acceleration presets 2026-10-07 07:23:34 -05:00
firestar5683 bfdad768b5 Restore Silverado truck tuning 2026-10-07 07:10:31 -05:00
firestar5683 0da456b93d Build 2026-10-07 06:37:36 -05:00
firestar5683 4238b61cdd Guard Hybrid cruise status 2026-10-07 06:34:06 -05:00
firestar5683 e80648bd20 Fix GM cruise transitions 2026-10-07 06:33:54 -05:00
firestar5683 01e80cf4ce Build 2026-10-07 05:34:32 -05:00
firestar5683 1bd1328531 Respect Volt cruise button timing 2026-10-07 05:33:37 -05:00
firestar5683 d798d7882a Respect GM cruise button state 2026-10-07 05:11:48 -05:00
firestar5683 eb78c0696c Restore GM geometry adjustments 2026-10-07 05:11:48 -05:00
firestar5683 53e9e4ec28 Build 2026-10-07 03:59:46 -05:00
firestar5683 a3c32e4633 Restore GM manual steering settings 2026-10-07 03:54:15 -05:00
firestar5683 3d79a80cce Build 2026-10-07 02:41:16 -05:00
firestar5683 4a59a56ae3 Match expanded paging controls 2026-10-07 02:40:06 -05:00
firestar5683 6affbf97eb Restore Volt cruise engagement 2026-10-07 02:38:46 -05:00
firestarsdog e58a905e07 Previous/Next expanded tap zones 2026-10-07 03:20:48 -04:00
firestar5683 06dfbec562 Build 2026-10-07 01:44:52 -05:00
firestar5683 f809a5610f Restore Bolt stock cruise cancellation 2026-10-07 01:42:57 -05:00
firestarsdog c67ade21d8 Extract Selector Dialog and add to System 2026-10-07 02:30:54 -04:00
firestar5683 50cae15248 Build 2026-10-07 00:58:15 -05:00
firestar5683 1b7219806d Restore GM cruise status 2026-10-07 00:56:40 -05:00
firestarsdog acc2b6191c Remove unneeded header tap dialog 2026-10-07 01:37:00 -04:00
firestarsdog e7e26ee074 Fix Sounds Panel Page 2 2026-10-07 01:31:18 -04:00
prabhaavp 0b81733e4d Merge pull request #209 from dirwin31:Stardust-PR-3
Live Edits to Galaxy Dev
2026-10-07 01:30:56 -04:00
Prabhaav Pillai be468d902b Unload guard for unsaved drafts and enhance live reload handling 2026-10-07 01:29:21 -04:00
firestar5683 7e11b01b0e Build 2026-10-07 00:27:41 -05:00
firestar5683 8864990859 Restore GM cruise cancellation 2026-10-07 00:25:31 -05:00
firestar5683 fd91811394 Fix validation fixtures and formatting 2026-10-07 00:25:31 -05:00
firestarsdog 10b27d6613 Large UI Sound Selection Update 2026-10-07 01:05:01 -04:00
prabhaavp 7ce888ed93 Merge pull request #208 from dirwin31:Stardust-PR-2
More Flowz
2026-10-07 00:56:51 -04:00
Prabhaav Pillai 104f88da3b Merge branch 'SecretGoodStarPilot' into pr/dirwin31/208 2026-10-07 00:51:17 -04:00
Prabhaav Pillai 2521eec0c0 streamline setup, pairing, and dialogs 2026-10-07 00:45:46 -04:00
firestar5683 244ed62cd6 Build 2026-10-06 23:35:30 -05:00
firestar5683 cdee63dd8e Restore Honda AOL engagement 2026-10-06 23:33:59 -05:00
Danny 69fa026fef Allow Live Edits to Galaxy 2026-10-06 21:03:32 -07:00
firestar5683 4085afd697 Build 2026-10-06 23:01:59 -05:00
firestar5683 e81b0c8e5e Restore Forte steering parity 2026-10-06 23:00:21 -05:00
firestar5683 33a9f104d2 Build 2026-10-06 22:50:37 -05:00
firestar5683 56761cbe15 Fix C4 audio toggle responsiveness 2026-10-06 22:49:00 -05:00
dirwin31 6b670e16c4 Prebuilt Lock 2026-10-06 20:45:43 -07:00
dirwin31 d0a5e1c43a Merge remote-tracking branch 'upstream/SecretGoodStarPilot' into Stardust-PR-2
# Conflicts:
#	openpilot/starpilot/galaxy/web/css/galaxy.css
2026-10-06 20:43:43 -07:00
firestar5683 2a9b98e8e6 Build 2026-10-06 22:37:07 -05:00
firestar5683 fa6a7c5b85 Fix GM AOL button capability 2026-10-06 22:35:25 -05:00
firestar5683 1d803c6506 Fix AOL startup 2026-10-06 22:35:25 -05:00
firestar5683 1acb1b1c8c Build 2026-10-06 22:15:10 -05:00
dirwin31 08d4634370 Car with built-in wireless Android Auto Wording 2026-10-06 20:13:13 -07:00
firestar5683 769dd1d0f3 Preserve Volkswagen GPS fix freshness 2026-10-06 22:12:33 -05:00
firestar5683 778269b0a5 Check vehicle GPS before updates 2026-10-06 22:09:45 -05:00
firestar5683 2ff07b2beb Restore vehicle GPS fallback 2026-10-06 22:08:02 -05:00
firestar5683 3cd4dce42d Update vehicle tuning and K4 coverage 2026-10-06 22:05:08 -05:00
firestar5683 5fd6b202f3 Build 2026-10-06 21:32:49 -05:00
firestar5683 987bdf9b9c Add driving mode descriptions 2026-10-06 21:29:28 -05:00
firestar5683 851fd50b2b Build 2026-10-06 21:18:51 -05:00
firestar5683 a913d65aa8 Restore low-speed steering advisory 2026-10-06 21:16:24 -05:00
firestar5683 9c0083820d Restore 2018-21 Bolt cruise cancellation 2026-10-06 21:16:24 -05:00
firestar5683 ca8146e63c Build 2026-10-06 20:48:07 -05:00
firestar5683 88f9a8eaf8 Retain audio underrun diagnostics 2026-10-06 20:45:40 -05:00
firestar5683 71ebd701eb panda 2026-10-06 20:15:24 -05:00
firestar5683 64a68b388a Build 2026-10-06 19:49:49 -05:00
firestar5683 92dc796811 Restore Bolt ACC pedal dashboard 2026-10-06 19:47:58 -05:00
firestar5683 587d30d455 Build 2026-10-06 19:05:32 -05:00
firestar5683 e1c48fa90f Verify shared steering pause inputs 2026-10-06 18:59:46 -05:00
firestar5683 4cded622b1 Cover map package validation in build dispatch 2026-10-06 18:53:44 -05:00
firestar5683 51ffef7025 Check map packaging before local tests 2026-10-06 18:51:14 -05:00
firestar5683 445a8c74f3 Preserve steering pause during invalid settings reads 2026-10-06 18:51:03 -05:00
firestar5683 9f0c443ffc Recover Galaxy after interrupted sessions 2026-10-06 18:50:03 -05:00
firestar5683 4c5fdfe100 Build 2026-10-06 18:46:51 -05:00
firestar5683 1ea9ad62a9 Improve navigation setup and map loading 2026-10-06 18:45:32 -05:00
firestar5683 caf2682c36 Ship the offline map provider 2026-10-06 18:43:51 -05:00
firestar5683 315c6689c2 Build 2026-10-06 18:40:56 -05:00
firestar5683 d51a78915e Restore steering pause 2026-10-06 18:39:29 -05:00
firestar5683 f339867727 Build 2026-10-06 18:25:39 -05:00
firestar5683 2a6de7f69a Restore Toyota gap buttons 2026-10-06 18:24:00 -05:00
firestar5683 00547795aa Keep zero speed visible 2026-10-06 18:17:31 -05:00
firestar5683 91a45b034e Build 2026-10-06 18:13:29 -05:00
firestar5683 ae28f2c70a Discover models from the remote catalog 2026-10-06 18:05:09 -05:00
firestar5683 50536f6f72 Preserve Jetlink artifacts across loader updates 2026-10-06 18:05:09 -05:00
firestar5683 25de3efff5 Clarify update progress 2026-10-06 17:51:04 -05:00
Danny 133d754b77 Dont Write if Not Needed, FAQ v2, Wonky Words.
(cherry picked from commit 423bbf6a36c0146a281c9366b4fc55155a7b334d)
2026-10-06 15:47:23 -07:00
Danny 7ad314f587 PiP Smoothing, Cer Expiring Warning, FAQ v1
(cherry picked from commit 135f7dd62aa3f6a2bc1e8ce10097a4c6f6a323be)
2026-10-06 15:47:23 -07:00
dirwin31 793b8857d8 Blind-spot side camera: wait for first cabin frame and show bubbles on Android Auto
(cherry picked from commit 60e39faa3e7c9c52df3cd314ac4c0645887aef2b)
2026-10-06 15:47:23 -07:00
dirwin31 354443893f Allow for dropped params keys
(cherry picked from commit 5ae8fc237c95c49f871b1e4377d25daef199177f)
2026-10-06 15:47:22 -07:00
firestar5683 222c6669ea Build 2026-10-06 17:40:08 -05:00
firestar5683 b6845519d2 Bind empty-arena loader runtime provenance 2026-10-06 17:30:07 -05:00
firestar5683 9f347b313c Optimize stateful model transfers 2026-10-06 17:28:19 -05:00
firestar5683 db5a559e73 Support stateful GPU model compilation 2026-10-06 17:17:39 -05:00
firestar5683 93fe9f1652 Protect longitudinal companion wire prefix 2026-10-06 17:15:16 -05:00
firestar5683 3b9f353ad4 Merge remote-tracking branch 'origin/SecretGoodStarPilot' into SecretGoodStarPilot 2026-10-06 16:58:22 -05:00
firestar5683 adc67c199c Adapt StarPilot hold and Tesla alerts to custom schema companions
Preserve official core schema and bind hold publication to the source plan.
2026-10-06 17:54:49 -05:00
Harald Schäfer f29fb8f724 Gas override boost (#39015)
* Add gas override boost, mici boost bar, and Rivian maximum set speed

* Refactor acceleration boost and gate accumulation on model limit

* Fade acceleration boost with model acceleration

* Limit boost buildup per gas override

* Latch boost eligibility before gas override

* Move model limiting check into AccelBoost

* Use DT_MDL directly in AccelBoost

* Clarify acceleration boost state names

* Scale acceleration boost by speed instead of decaying it

* Enable Experimental Mode by default

* Scale boost buildup with the application speed ramp

* Fix boost serialization and model-limit eligibility

* Remove boost UI and Experimental Mode default changes

* Combine boost update and application

Original-upstream-commit: ab8c84c3989bab34969d83e78c5fad3ee5b95112
Upstream: https://github.com/commaai/openpilot/commit/ab8c84c3989bab34969d83e78c5fad3ee5b95112
2026-10-06 17:54:49 -05:00
firestar5683 1364266b5a Prepare official longitudinal schema ordinal for upstream boost 2026-10-06 17:54:49 -05:00
Adeeb Shihadeh 017bf476f2 jp: fix file resolving
Original-upstream-commit: c933446486e6923c8468a979b6fab890b46f5cdb
Upstream: https://github.com/commaai/openpilot/commit/c933446486e6923c8468a979b6fab890b46f5cdb
2026-10-06 17:54:16 -05:00
Adeeb Shihadeh 448da4d39f jp: show text fields (#39042)
Original-upstream-commit: 2b7c34a5c32f78f4892a07598cbe3827be880438
Upstream: https://github.com/commaai/openpilot/commit/2b7c34a5c32f78f4892a07598cbe3827be880438
2026-10-06 17:54:16 -05:00
Adeeb Shihadeh 49c960a3f0 jotpluggler: fix subset time bounds (#39041)
Original-upstream-commit: fc616407826c52abd1338a34e85ce9d4fcbd20e1
Upstream: https://github.com/commaai/openpilot/commit/fc616407826c52abd1338a34e85ce9d4fcbd20e1
2026-10-06 17:54:15 -05:00
Adeeb Shihadeh 6c0074def3 jp: add initData tab (#39039)
Original-upstream-commit: 84f9bdbc10df0184976c14457bddab008b141208
Upstream: https://github.com/commaai/openpilot/commit/84f9bdbc10df0184976c14457bddab008b141208
2026-10-06 17:54:15 -05:00
Adeeb Shihadeh 7ec39898a8 deprecate unused initData fields
Original-upstream-commit: cce54f20d52c381499ae16119cb83c710c1f1083
Upstream: https://github.com/commaai/openpilot/commit/cce54f20d52c381499ae16119cb83c710c1f1083
2026-10-06 17:54:15 -05:00
Daniel Koepping 7073765e83 Revert #38464 (#39038)
* Revert #38464

Reverts aa0ac919ba.

Original-upstream-commit: 910c08e0828c54ce7919ae14606166779a52bf94
Upstream: https://github.com/commaai/openpilot/commit/910c08e0828c54ce7919ae14606166779a52bf94
2026-10-06 17:54:15 -05:00
Shane Smiskol d5748b0a61 ui: fix keyboard text overflow gradient
Original-upstream-commit: 01a670d6b2fc41c0fc79300b200906e4058f8d33
Upstream: https://github.com/commaai/openpilot/commit/01a670d6b2fc41c0fc79300b200906e4058f8d33
2026-10-06 17:54:15 -05:00
firestarsdog dcc47f2838 LaneLines 6->4 2026-10-06 17:22:31 -04:00
firestar5683 5ed05015a4 Build 2026-10-06 16:18:14 -05:00
firestar5683 80336a4eb7 Palisade Always On Lateral 2026-10-06 16:16:48 -05:00
firestar5683 bfc5a16280 Build 2026-10-06 15:48:42 -05:00
firestar5683 48c8a06b2f Palisade Longitudinal Startup 2026-10-06 15:46:21 -05:00
firestar5683 0a34c129f1 Build 2026-10-06 15:28:16 -05:00
firestar5683 c8de87e680 Malibu Hybrid Cruise Control 2026-10-06 15:26:49 -05:00
dirwin31 3600e3a461 Logs Page Weirdness and Step 4 2026-10-06 13:20:43 -07:00
dirwin31 229c9e7beb Make Uploads calm down 2026-10-06 13:07:02 -07:00
dirwin31 8a7770a4e0 Move Buttons 2026-10-06 13:07:02 -07:00
dirwin31 5b52ff4bc8 Android Auto config v4: move sync-readback configs back to async
Version 3 configs that turned gpu_nv12/async_readback off did so while a
failed async readback still crashed the car view. Now that it only drops the
frame, older files get today's defaults (both on); from v4 on a saved value
is a real choice again.
2026-10-06 13:06:51 -07:00
dirwin31 da82092bc9 Car view: drop a frame on a failed async readback instead of crashing
A failed GPU wait (GL_WAIT_FAILED as openpilot went onroad) raised out of the
renderer, three crashes in a row exhausted its restart budget and the car
showed 'display unavailable'. Now the frame is dropped, the GL error is
logged with it, and after three consecutive failures the session switches to
synchronous readback.
2026-10-06 13:06:51 -07:00
dirwin31 5c09df9b61 Fix Android Auto start screen logo and font paths
BASEDIR is the repo root, but assets live under openpilot/ in this layout,
so the logo and both fonts were never found and the car showed tiny
default-font text only.
2026-10-06 13:06:51 -07:00
dirwin31 889ff8506b Restart soundd and android_autod after a crash
Both can crash at boot before their hardware is ready: android_autod when
bluetoothd is not up yet (NoSuchUnit / adapter unavailable while restoring
a phone-role journal left by a reboot mid-projection), soundd when the audio
stream is not active. The manager never restarted them, so Android Auto was
dead and selfdrived refused to engage for the rest of the drive.
2026-10-06 13:06:51 -07:00
dirwin31 6030a79f8c Better Wording for Step 1 2026-10-06 13:06:51 -07:00
dirwin31 93a3cfe2bd VASM Camera and Helper Arrows 2026-10-06 13:06:51 -07:00
dirwin31 d1def0f266 Smooth Install Flow V1 2026-10-06 13:06:50 -07:00
Danny 5a6e17a0c8 Fix the Boot Logo Screen 2026-10-06 13:06:50 -07:00
Danny 738c7a2dbc Add bounded restart logic and output logging for crashed or stalled car UI renderers 2026-10-06 13:06:50 -07:00
Danny a636fa9d51 Allow Route Delete and Account for Route Counter Reset After Migration to Stop Auto Deleting Newest Routes 2026-10-06 13:06:50 -07:00
firestar5683 fb4a28774e Build 2026-10-06 14:00:42 -05:00
firestar5683 2d0cd49eea Malibu Cruise Control 2026-10-06 13:58:51 -05:00
firestar5683 baf1f55111 Build 2026-10-06 13:30:28 -05:00
firestar5683 1b89180b83 Turn Assist 2026-10-06 13:28:42 -05:00
firestar5683 13260d36c9 Lane Centering Strength 2026-10-06 13:14:32 -05:00
firestar5683 db4769cdde Build 2026-10-06 13:02:45 -05:00
firestar5683 3acfd0505c Settings Upgrade Recovery 2026-10-06 12:55:44 -05:00
firestar5683 9aa464f8e2 Build 2026-10-06 12:44:31 -05:00
firestar5683 0fcceb8294 Silverado Stock Lateral 2026-10-06 12:42:08 -05:00
firestar5683 8f71076e25 Build 2026-10-06 11:14:00 -05:00
firestar5683 bc27e48c5a Sienna Longitudinal Control 2026-10-06 11:12:12 -05:00
firestar5683 90050d8a95 Build 2026-10-06 10:56:11 -05:00
firestar5683 8269de1077 Corolla Longitudinal Control 2026-10-06 10:54:24 -05:00
firestar5683 b2d422d43a Build 2026-10-06 10:35:24 -05:00
firestar5683 84bb2a4110 Prius Always On Lateral 2026-10-06 10:31:18 -05:00
firestar5683 f7de968fcb Build 2026-10-06 10:02:55 -05:00
firestar5683 e91ca11e1c Toyota Radar AOL 2026-10-06 10:01:04 -05:00
firestar5683 caadb75088 Build 2026-10-06 09:39:22 -05:00
firestar5683 36553441f6 Startup Test Fixtures 2026-10-06 09:37:45 -05:00
firestar5683 83ee23c836 UI Test Fixtures 2026-10-06 09:29:31 -05:00
firestar5683 93d78f9b8c Build 2026-10-06 09:25:05 -05:00
firestar5683 75a505d851 Toyota Stopping and Steering 2026-10-06 09:25:05 -05:00
firestar5683 4daa209c06 Build 2026-10-06 09:25:04 -05:00
firestar5683 bcab96d071 Toyota Always On Lateral 2026-10-06 09:25:04 -05:00
firestar5683 7ae1a6a417 Build 2026-10-06 09:25:04 -05:00
firestar5683 d2a06a7a0e Toyota Hybrid Brake Hold 2026-10-06 09:25:04 -05:00
firestar5683 56d90ef0a6 Build 2026-10-06 09:25:04 -05:00
firestar5683 3db047a9af Mach-E Steering Unwind 2026-10-06 09:25:03 -05:00
firestar5683 249ab7cfc6 Build 2026-10-06 09:25:03 -05:00
firestar5683 81af2a0b32 Forte AOL Startup 2026-10-06 09:25:03 -05:00
firestar5683 aa5557acf1 Build 2026-10-06 09:25:03 -05:00
firestar5683 e05d3d3e99 G70 Steering Damping 2026-10-06 09:25:02 -05:00
firestar5683 f2e4f6084e Build 2026-10-06 09:25:02 -05:00
firestar5683 873f7059ea GM Pedal Control 2026-10-06 09:25:02 -05:00
firestar5683 2e3621ca8f Build 2026-10-06 09:25:02 -05:00
firestar5683 c219110392 GM Longitudinal Control 2026-10-06 09:25:01 -05:00
firestar5683 60868dd8bd Android Auto Report Permissions 2026-10-06 09:24:36 -05:00
firestar5683 0b9643b1e4 Build 2026-10-06 09:24:35 -05:00
firestar5683 dffeb7aa03 Honda Hybrid AOL
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
2026-10-06 09:24:35 -05:00
dirwin31 acffe8f112 Android Auto Connection and Rendering 2026-10-06 09:24:35 -05:00
firestar5683 875eb6bc8c Build 2026-10-06 09:24:06 -05:00
firestar5683 d18f4968fe Automatic Usage Statistics 2026-10-06 09:24:05 -05:00
Prabhaav Pillai 5ffc32f2a5 (Another third) round of Galaxy UI bug fixes
Centered loading states and unified page layouts, margins, and shared controls.
Added horizontal tab transitions and a moving glass navigation indicator.
Fixed mobile button overflow and compacted model summaries.
Removed unused code and styles.
2026-10-06 03:23:02 -04:00
firestarsdog b53e9201f3 Back button behavior 2026-10-06 02:37:43 -04:00
firestarsdog ccdfa0367c Header Styling 2026-10-06 02:23:09 -04:00
firestarsdog a47b1d4188 Pagination QoL #2 2026-10-06 01:44:26 -04:00
firestar5683 40e6b5747c Build 2026-10-05 23:51:16 -05:00
firestar5683 bbdaa61b28 Prius EPS Retrofit 2026-10-05 23:49:31 -05:00
Prabhaav Pillai 9bf387e709 (Another second) round of Galaxy UI bug fixes
Fixed logo/search overlap by aligning responsive breakpoints and preventing logo shrink.
Mobile search expands across the bar; closing it restores controls and keyboard focus.
Parked uses a small icon on phones, with an accessible label; desktop retains the text.
Verified no overlaps or page overflow at nine widths, from 320px to 1440px.
2026-10-06 00:23:54 -04:00
firestar5683 6f08e10686 Build 2026-10-05 23:21:01 -05:00
firestar5683 df540edd42 Galaxy Interface Contracts 2026-10-05 23:19:12 -05:00
firestar5683 5f08653c24 Shared Safety Headers 2026-10-05 23:10:40 -05:00
firestar5683 26f4468314 Steering Replay Checks 2026-10-05 23:10:40 -05:00
firestar5683 c310dacc2d CI Test Fixtures 2026-10-05 23:10:40 -05:00
firestar5683 8ade5916f4 Galaxy Control Settings 2026-10-05 23:10:39 -05:00
firestar5683 5cbe643f40 Bolt Pedal Control 2026-10-05 23:10:39 -05:00
firestarsdog 026ac0b1ce Pagination Swipe QoL 2026-10-05 23:42:46 -04:00
Prabhaav Pillai 7b84b4e621 (Another one) round of Galaxy UI bug fixes
VASM/PiP previews fixed with shared camera lifecycle, timeouts and automatic recovery.
Theme Maker side cameras now work independently.
VASM handles warning borders only; PiP handles cameras and saved layouts.
Refresh/Retry controls removed in favor of automatic state updates.
Disconnect/loading states now persist correctly and recover automatically.
Settings sync and shared-write propagation restored.
Toggle/editor and Vehicle Default flicker fixed.
Default actions simplified and numeric Default restored.
Sidebar and Tools now share one catalog.
Notices and form controls standardized.
Recordings UI improved with search, sorting and preservation filters.
Playback supports full-resolution cameras and automatic segment advancement.
Recording downloads and management restored.
Recording status now reflects actual scan state.
Map partial-recovery errors fixed.
Regression coverage expanded across browser, camera, polling, recording and placement flows.
2026-10-05 23:28:46 -04:00
firestar5683 9d02cc4f61 Build 2026-10-05 21:35:10 -05:00
firestar5683 0017b1992c CEM Standstill Hold 2026-10-05 21:33:23 -05:00
firestar5683 417f4e09d9 Cruise Resume Memory 2026-10-05 21:33:23 -05:00
firestar5683 85a6f7c992 Optional Android Auto Health 2026-10-05 21:33:10 -05:00
firestar5683 6c1977ba9f Build 2026-10-05 20:26:49 -05:00
firestar5683 0820a009cc Volt Cruise Pedal Control 2026-10-05 20:24:44 -05:00
firestar5683 1dba933c25 Build 2026-10-05 19:40:13 -05:00
firestar5683 94471f853e Lane Centering Strength 2026-10-05 19:38:11 -05:00
firestar5683 7a0c8f721c Ioniq 6 Steering Messages 2026-10-05 19:38:11 -05:00
firestar5683 79322cfb1e GPU Status 2026-10-05 19:38:11 -05:00
firestar5683 e01e290109 Build 2026-10-05 18:59:50 -05:00
firestar5683 66a706b4b6 Volt 2019 Pedal Control 2026-10-05 18:59:49 -05:00
firestar5683 af1c132e56 Build 2026-10-05 18:59:49 -05:00
firestar5683 bf7966efb9 Volt ASCM Pedal Control 2026-10-05 18:59:49 -05:00
firestar5683 58eef91c2a Build 2026-10-05 18:59:49 -05:00
firestar5683 4ea1b49b67 Volt Gateway Pedal Control 2026-10-05 18:59:48 -05:00
firestarsdog 74e08d7e38 Baseline Swipe Pagination 2026-10-05 19:01:08 -04:00
firestar5683 f36dcfa4f3 Build 2026-10-05 17:23:04 -05:00
firestar5683 f9e70f2ee9 Offroad Settings 2026-10-05 17:21:39 -05:00
firestar5683 a1bdaf855f Highlander Lateral Control 2026-10-05 17:21:39 -05:00
firestar5683 b4a49b74cf Volt Camera Pedal Control 2026-10-05 17:21:39 -05:00
firestar5683 c6e826b0e4 AOL Seatbelt Behavior 2026-10-05 16:02:20 -05:00
firestar5683 8aea0c1ac0 GM Camera Pedal Control 2026-10-05 16:00:59 -05:00
firestar5683 ae3cbf9340 UI and Update Checks 2026-10-05 15:53:20 -05:00
firestar5683 3e39b1531c AOL Calibration Recovery
Co-authored-by: Intelli <contact@intelli.software>
2026-10-05 15:53:20 -05:00
firestar5683 5debd4cd8e Recent Update Versions
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
2026-10-05 15:53:20 -05:00
firestar5683 82daa5bbe9 Updater Process Cleanup
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
2026-10-05 15:53:20 -05:00
firestar5683 63ea964a56 Training Texture Cleanup
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
2026-10-05 15:53:20 -05:00
firestar5683 cecba8590a Label Layout Cache
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
2026-10-05 15:53:20 -05:00
firestar5683 d29613b7c5 Bounded Text Caches
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
2026-10-05 15:53:20 -05:00
firestar5683 6e48383568 EV9 Steering Targets
Co-authored-by: Intelli <contact@intelli.software>
2026-10-05 15:53:20 -05:00
firestar5683 570fc2d71c Hyundai Angle Validation
Co-authored-by: Intelli <contact@intelli.software>
2026-10-05 15:53:20 -05:00
firestar5683 0b3997491d ID4 Vehicle Detection
Co-authored-by: Ian <ianpang126@gmail.com>
2026-10-05 15:53:20 -05:00
firestar5683 c8badeddf7 AOL Steering Feedback
Co-authored-by: Intelli <contact@intelli.software>
2026-10-05 15:53:20 -05:00
firestar5683 763f9171ee Steering Direction
Co-authored-by: pharmacomaniac <blittle65@gmail.com>
2026-10-05 15:53:20 -05:00
firestar5683 6465fa5e37 Niro Follow Tuning 2026-10-05 15:53:20 -05:00
firestar5683 54260529a5 Tesla Screen Controls 2026-10-05 15:53:20 -05:00
firestarsdog a62ef8e3ea Menu clamp 2026-10-05 15:18:32 -04:00
firestar5683 9999b0686a Fast Update Compatibility 2026-10-05 13:58:34 -05:00
firestar5683 5fbb935a2c Usage Statistics 2026-10-05 13:18:55 -05:00
firestar5683 c6b73f3afa Jetlink Support 2026-10-05 12:55:41 -05:00
firestar5683 6c8966d044 Jetlink Client 2026-10-05 12:21:26 -05:00
firestar5683 674ece7430 Developer Test Command 2026-10-05 11:41:45 -05:00
firestar5683 2f8a0096c2 Build 2026-10-05 11:02:36 -05:00
firestar5683 758942fdbf Large Settings Types 2026-10-05 11:00:54 -05:00
firestar5683 3046431700 Build 2026-10-05 10:57:54 -05:00
firestar5683 896994d17b Pedal Sensor Inputs 2026-10-05 10:57:54 -05:00
firestar5683 ed6c19f4d3 Build 2026-10-05 10:57:54 -05:00
firestar5683 525d9ab26d Paddle Modes 2026-10-05 10:57:53 -05:00
firestar5683 55b0c8e12c Build 2026-10-05 10:57:53 -05:00
firestar5683 d8cc751cef Volt One-Pedal 2026-10-05 10:57:53 -05:00
firestar5683 ead4c0cf73 Build 2026-10-05 10:57:53 -05:00
firestar5683 efa7781dfe GM Gateway Steering 2026-10-05 10:57:52 -05:00
firestar5683 2709f1c642 Build 2026-10-05 10:57:52 -05:00
firestar5683 de589222a7 GM Brake Hold 2026-10-05 10:57:52 -05:00
firestar5683 c1d07152c1 Build 2026-10-05 10:57:52 -05:00
firestar5683 fa9341ec6f Volt 2019 Brake Hold 2026-10-05 10:57:51 -05:00
firestar5683 00207c7b6c Build 2026-10-05 10:57:51 -05:00
firestar5683 09f7de29a5 Volt Camera Auto Hold 2026-10-05 10:57:51 -05:00
firestar5683 3ab13b3434 Build 2026-10-05 10:57:51 -05:00
firestar5683 6a11b09031 Volt Stop Controls 2026-10-05 10:57:50 -05:00
firestar5683 ca305dda2e Build 2026-10-05 10:57:50 -05:00
firestar5683 16fb0eee3b Replay Validation 2026-10-05 10:57:50 -05:00
firestar5683 6327ec50e0 Firmware Build Stamp 2026-10-05 10:57:50 -05:00
firestar5683 0986e61b4e UI Snapshot Reuse 2026-10-05 10:57:50 -05:00
firestar5683 3f0d494764 Ford Engagement 2026-10-05 10:57:50 -05:00
firestarsdog 06dc2cdfbf Add Stars to Header, very important 2026-10-05 02:21:33 -04:00
firestarsdog 972d918f39 Swap Large UI to Horizontal Pagination and StarPilot Styling 2026-10-05 02:05:28 -04:00
firestar5683 003a3bfded CI Workflow Validation 2026-10-04 23:57:53 -05:00
firestarsdog ed2ee4b772 Device menu : Galaxy line top, power options bottom 2026-10-05 00:57:00 -04:00
firestarsdog 3268cd014c Large UI Software Panel Fixes 2026-10-05 00:45:02 -04:00
firestar5683 195333a76a Build 2026-10-04 23:33:59 -05:00
firestar5683 7df3b2aef4 Mach-E Steering 2026-10-04 23:30:46 -05:00
firestar5683 cf9366e5e1 Toyota Auto Hold 2026-10-04 23:29:20 -05:00
firestar5683 1cdb868434 Build 2026-10-04 22:39:31 -05:00
firestar5683 41b2def9c4 GPIO Test Contracts 2026-10-04 22:31:08 -05:00
firestar5683 418b1c8e43 GPU Load Diagnostics 2026-10-04 22:31:07 -05:00
firestar5683 5c8b676c03 GPU Status 2026-10-04 22:31:05 -05:00
firestar5683 942a562a0a Bolt Gear State 2026-10-04 22:31:03 -05:00
firestarsdog a71528c485 Batch AGNOS GPIO Permission Rules 2026-10-04 22:56:08 -04:00
firestar5683 f6a01d2126 Build 2026-10-04 21:32:23 -05:00
firestar5683 262ec0b938 C3 Test Types 2026-10-04 21:32:03 -05:00
firestar5683 3c578f310e Galaxy Settings Defaults and Availability 2026-10-04 21:28:43 -05:00
firestar5683 5a6fbb0e92 Restore Steering Settings 2026-10-04 21:28:43 -05:00
firestar5683 93fcddaa9f Sidebar Telemetry Timing 2026-10-04 21:28:43 -05:00
firestar5683 a0950855e0 Build 2026-10-04 21:28:43 -05:00
firestar5683 f2ccd222cf Vehicle Preference Context 2026-10-04 21:28:43 -05:00
firestar5683 5b2371e6c8 Galaxy Connection Queue 2026-10-04 21:28:43 -05:00
firestar5683 530ce968e0 Manual Vehicle Settings 2026-10-04 21:28:42 -05:00
firestar5683 4e4ebd5866 Big UI Sidebars 2026-10-04 21:28:42 -05:00
firestar5683 360802641e Build 2026-10-04 21:28:42 -05:00
firestar5683 418b295dbe Release Build Checks 2026-10-04 21:28:42 -05:00
firestar5683 1bbcc22f1c Safety Test Frames 2026-10-04 21:28:42 -05:00
firestar5683 11a83e49f8 Bolt Control Checks 2026-10-04 21:28:42 -05:00
firestarsdog 348b35788c Fix C3 Panda Start Speed 2026-10-04 21:59:42 -04:00
firestarsdog ecff3fce24 C3 Panda Rules 2026-10-04 20:46:29 -04:00
firestar5683 a379d94725 Build 2026-10-04 19:31:40 -05:00
firestar5683 2e16b3bbac Validation Fixes 2026-10-04 19:29:11 -05:00
firestar5683 3a4e8f7625 Bolt Low Gear 2026-10-04 19:23:02 -05:00
firestar5683 da1820b7db Build 2026-10-04 19:23:02 -05:00
firestar5683 db1d677610 Vehicle Cache Migration 2026-10-04 19:23:02 -05:00
firestar5683 98c6c8b334 Build 2026-10-04 19:23:02 -05:00
firestar5683 e71c919fff Hyundai EV Longitudinal 2026-10-04 19:22:46 -05:00
firestar5683 00ce937ab9 Build 2026-10-04 19:22:45 -05:00
firestar5683 7d8baeb124 Ioniq Cluster Status 2026-10-04 19:22:45 -05:00
firestar5683 9d4414538e Build 2026-10-04 19:22:45 -05:00
firestar5683 724cbb0f00 Fleet Coverage 2026-10-04 19:22:30 -05:00
firestar5683 8d8f325a1b GM Control Tests 2026-10-04 19:22:30 -05:00
firestar5683 6f96d1b544 Android Auto Tests 2026-10-04 19:22:30 -05:00
firestar5683 25faf72be2 Map Package 2026-10-04 19:22:30 -05:00
firestar5683 0220ef9b1d Model Build Compatibility 2026-10-04 19:22:29 -05:00
firestarsdog 3296616331 C3 Panda + Build 2026-10-04 19:59:30 -04:00
Prabhaav Pillai 4b94a6a115 A round of Galaxy UI bug fixes:
Mobile "Search toggles": collapses to icon and stays in top bar.
Home page multiple Refresh buttons: consolidated to one.
Nav-bar blur lost on narrow widths: glass blur encases nav, search collapses to button.
Galaxy logo/home now looks clickable (border and hover).
Toggles page dual search: page search moved to an icon button near refresh.
Global search false "could not be searched" plus result flicker: only warns when nothing loaded, no per-batch republish.
Mobile native dropdown inconsistency: unified rendering.
Toggle change caused page re-render flicker: dimming no longer tied to save state.
Missing toggles reset button: restored.
Redundant and inconsistent toggle descriptions: moved to shared GxNotice, all pages covered.
Developer tab spacing: consistent card/section layout.
Force-drive Offroad/Onroad/Auto buttons: consistent tool styling.
Galaxy Tunnel install text: accurate PWA install and benefits copy.
Colors and Layout banner/margins plus misplaced "Park" note: consistent savebar and warn banner.
Quick Select oversized intro/margins plus Reload text: icon button.
Blind Spot crop X/Y toggles hidden (kept writable via API/editor).
Crop editor link dead-end: now routes to Blind Spot Camera, renamed "and Preview".
Sound pack "Park the vehicle" full-box description: inlined into header hint.
Tools/Logs/Plots inconsistent submenu tiles (icon centering, hover/press): shared MenuTile.
Navigation bottom button hidden behind nav bar: clearance fix.
Navigation page nav bar hidden plus blurred Exit button restored.
Redundant Onroad Appearance: removed tool, Colors and Layout promoted to tool.
Plots and Analysis submenu panel inconsistent: uses MenuTile.
Theme Maker and Onroad Appearance consolidation.
2026-10-04 19:12:49 -04:00
firestarsdog 7880a4e3be Possible C3 Panda Fix 2026-10-04 18:43:17 -04:00
firestar5683 b5d02a8fff Build 2026-10-04 17:26:41 -05:00
firestar5683 dca64b67ec Preserve Model Runtime 2026-10-04 17:25:04 -05:00
firestar5683 2c8e8b5879 CI Contracts 2026-10-04 17:23:26 -05:00
firestar5683 0858e178e3 Runtime Compatibility 2026-10-04 17:23:26 -05:00
firestar5683 da4bceb5ac Map IPC Compatibility 2026-10-04 17:23:25 -05:00
firestar5683 3b305b12b4 Fast Update 2026-10-04 17:23:25 -05:00
firestar5683 a0a40f9e83 Supported Driving Defaults 2026-10-04 17:23:25 -05:00
firestar5683 dad0143c9a Vehicle Engagement and Pedal Detection 2026-10-04 17:23:24 -05:00
firestar5683 f586da6eac Tesla Stock Steering 2026-10-04 16:54:56 -05:00
firestar5683 83c42cacda Mazda Steering Control 2026-10-04 16:54:56 -05:00
firestarsdog efa2c71553 Large UI Vehicle panel bugfixes 2026-10-04 17:47:28 -04:00
Prabhaav Pillai ae720a05b7 build command to include 'galaxy' in the target list 2026-10-04 16:54:01 -04:00
firestarsdog d861b2a3a5 Vehicle panel + 2026-10-04 14:30:40 -04:00
firestar5683 e68ed75ee6 Driving Controls and Wheel Actions 2026-10-04 12:24:27 -05:00
firestar5683 6aec1f863a Vehicle Test Discovery 2026-10-04 10:58:29 -05:00
firestar5683 11a4ef2662 Honda Stock Cruise 2026-10-04 10:41:53 -05:00
firestar5683 9737753948 Honda Steering and Pedal Control 2026-10-04 10:22:18 -05:00
firestar5683 09cd3317bc Build Validation 2026-10-04 10:02:54 -05:00
firestar5683 68ad7fdfc2 Honda Radar Tracking 2026-10-04 09:40:48 -05:00
firestar5683 f40bc5b8cb Honda Clarity Identification 2026-10-04 09:40:48 -05:00
firestar5683 42e43d7043 Honda City Identification 2026-10-04 09:40:48 -05:00
firestar5683 c399146acb Honda Cruise Signal Sources 2026-10-04 09:40:48 -05:00
firestar5683 c541313f2a Honda Modified EPS 2026-10-04 09:40:47 -05:00
firestar5683 0856cb868f Honda Longitudinal Control 2026-10-04 09:40:47 -05:00
firestar5683 98826823cb Honda Transmission Detection 2026-10-04 09:40:47 -05:00
firestar5683 a79ce16bf9 Honda Controller Settings 2026-10-04 09:40:47 -05:00
firestar5683 d4f67e6b59 build 2026-10-04 09:40:47 -05:00
firestar5683 8ac3813088 Ford Safety Storage 2026-10-04 09:40:46 -05:00
firestar5683 2a5ecb035b Ford Safety Memory 2026-10-04 09:40:46 -05:00
firestar5683 266c7367f6 Ford Always On Lateral 2026-10-04 09:40:46 -05:00
firestar5683 c92b3d597d Ford Cruise Selection Tests 2026-10-04 09:40:46 -05:00
firestar5683 661aa4d108 Ford Steering Settings 2026-10-04 09:40:46 -05:00
firestar5683 e244fd106e Mach-E Camera Leads 2026-10-04 09:40:46 -05:00
firestar5683 b9c4b00c60 Ford Startup Health 2026-10-04 09:40:45 -05:00
firestar5683 92de8bc96c Ford CAN FD Control 2026-10-04 09:40:45 -05:00
firestar5683 005f6dc682 build 2026-10-04 09:40:45 -05:00
firestar5683 845a476605 Hyundai Stock AOL 2026-10-04 09:40:45 -05:00
firestar5683 e13d16c476 build 2026-10-04 09:40:45 -05:00
firestar5683 b629ecd79c Panda Power State 2026-10-04 09:40:45 -05:00
firestar5683 025b6ca3ae Hyundai Safety Profiles 2026-10-04 09:40:45 -05:00
firestar5683 ea0198cf57 G90 Steering Startup 2026-10-04 09:40:44 -05:00
firestar5683 4440104f27 Hyundai AOL Profiles 2026-10-04 09:40:44 -05:00
firestar5683 1e4c7a3b31 Hyundai Stock Cruise Steering 2026-10-04 09:40:44 -05:00
firestar5683 9debca8d36 Hyundai Torque Options 2026-10-04 09:40:44 -05:00
firestar5683 15d3dc0a1d Hyundai Steering Policies 2026-10-04 09:40:44 -05:00
firestar5683 02eb9a0748 Hyundai Automatic Identification 2026-10-04 09:40:44 -05:00
firestar5683 ec8e99df71 Palisade Steering 2026-10-04 09:40:44 -05:00
firestar5683 dec33429fc Hyundai Startup Health 2026-10-04 09:40:43 -05:00
firestar5683 a3a4c8e7e4 GM Automatic Identification 2026-10-04 09:40:43 -05:00
firestar5683 660981b819 Bolt Camera Configuration 2026-10-04 09:40:43 -05:00
firestar5683 6dcab7c8f8 build 2026-10-04 09:40:43 -05:00
firestar5683 581392189a GM Startup Checks 2026-10-04 09:40:43 -05:00
firestar5683 67fb3b5ff7 Camera Session Cleanup 2026-10-04 09:40:43 -05:00
firestar5683 d21ccb6c11 Comma 3 Audio and Startup 2026-10-04 09:40:43 -05:00
firestarsdog 01a5cc4d91 Header border 2026-10-04 07:13:17 -04:00
firestarsdog f3e6ccda01 Header size correction 2026-10-04 07:04:43 -04:00
firestarsdog 22dae22085 Text Size Standardization and Fixes 2026-10-04 06:56:35 -04:00
firestarsdog c247948fc2 Dev tools fixes 2026-10-04 05:49:07 -04:00
firestarsdog cba196e25d Big UI Parity Pass Bugfixes 2026-10-04 05:33:15 -04:00
firestarsdog 42d3611932 StarPilot Big UI Styling 2026-10-04 04:36:14 -04:00
firestar5683 d31e11b20d build 2026-10-04 00:41:11 -05:00
firestar5683 54fbde0bf3 Comma 3 Panda Support 2026-10-04 00:39:39 -05:00
firestar5683 e87f51970b Panda Sidebar Status 2026-10-04 00:19:41 -05:00
firestar5683 6edbcde6e2 build 2026-10-04 00:19:41 -05:00
firestar5683 ccc0886604 RDFv4 Default Model 2026-10-04 00:19:40 -05:00
firestarsdog 3f0ce8f1b3 Start building up big ui bluetooth panel 2026-10-04 00:55:20 -04:00
firestar5683 67a9e60e23 Params Registry Checks 2026-10-03 23:34:03 -05:00
firestar5683 e7a92e4bee Standard Camera Default 2026-10-03 23:34:02 -05:00
firestar5683 90f40a6209 Bolt Controls Startup 2026-10-03 23:34:02 -05:00
firestar5683 ebd948937d Fast Update Fix 2026-10-03 23:34:02 -05:00
firestar5683 f7a041d575 Force Offroad Dialog 2026-10-03 23:34:01 -05:00
firestarsdog 49f7f18c46 Big UI Settings Fixes 2026-10-04 00:26:30 -04:00
firestar5683 de8336a84f Dom Compatibility 2026-10-03 23:06:07 -05:00
firestar5683 4fd849375e Soundd Fix 2026-10-03 22:52:51 -05:00
firestar5683 c6ab815cb3 Galaxy Pairing Migration 2026-10-03 22:52:46 -05:00
firestarsdog 5e54e17fa3 Ungate Network/Bluetooth panels 2026-10-03 23:46:23 -04:00
firestar5683 fe112f464d Bolt No-Acc Triage 2026-10-03 22:12:11 -05:00
firestar5683 39c1767f65 Fast Updater 2026-10-03 21:22:38 -05:00
firestar5683 a8915ca858 Force Drive State Icon 2026-10-03 20:54:12 -05:00
firestar5683 048094601f Show bundled model names on the home screen 2026-10-03 20:51:23 -05:00
firestar5683 105d2262e4 Restore Big UI Bluetooth and Developer menus 2026-10-03 20:51:23 -05:00
firestar5683 d84ab9d0ec Preserve taps within Big UI controls 2026-10-03 20:51:23 -05:00
firestar5683 14b2e8459f Restart the UI after unexpected process exits 2026-10-03 20:51:23 -05:00
firestar5683 2459ba878c Start new installations with fresh driving settings 2026-10-03 20:51:23 -05:00
firestar5683 89764d2d1b Handle Migration Cleanup 2026-10-03 19:46:08 -05:00
firestar5683 3a409748ac Match AGNOS version to system30 image 2026-10-03 18:32:20 -05:00
firestar5683 aff36eadc9 Fix AGNOS image download sources 2026-10-03 18:26:08 -05:00
firestar5683 b07d3dd5d4 build 2026-10-03 17:39:42 -05:00
firestar5683 9298db235b Galaxy HTTP and settings registration
Register Galaxy managed services, recording media and settings endpoints after their underlying owners exist. Cover authentication and cross-feature request behavior at the HTTP boundary.
2026-10-03 22:34:37 +00:00
firestar5683 523a73bbf9 Background drive analysis and statistics
Implement background drive analysis and statistics with explicit worker and persistence ownership.
2026-10-03 22:34:37 +00:00
firestar5683 2bf401019a Fleet learning presentation
Expose fleet-learning operations through Galaxy presentation endpoints.
2026-10-03 22:34:37 +00:00
firestar5683 6601a500e3 Fleet-learning analysis workers
Analyze closed recordings in bounded workers, validate static vehicle context, and retain actionable progress and failures. Honor finite and unlimited process resource limits.
2026-10-03 22:34:37 +00:00
firestar5683 6f30e17b83 Drive-history summaries and plots
Expose drive history summaries and plot data through Galaxy.
2026-10-03 22:34:37 +00:00
firestar5683 1bf86e10c3 Fleet-learning data contracts
Provide offline log decoding and torque-surface data contracts for fleet learning.
2026-10-03 22:34:37 +00:00
firestar5683 36a1f43816 UI application registration
Register shared settings and application pages in both UI layouts with capability-aware wheel assignments.
2026-10-03 22:34:37 +00:00
firestar5683 64abcb4094 Controller callbacks and joined feature coverage
Wire controller callbacks into the running process and add joined planner and mode-action regression coverage.
2026-10-03 22:34:37 +00:00
firestar5683 408ab1a9d7 Galaxy device and remote-access exports
Expose Galaxy device, crash and remote-access services with the bundled frpc license.
2026-10-03 22:34:37 +00:00
firestar5683 b011a1fdcd Vehicle and control process integration
Connect shared feature owners to vehicle and control processes. Migrate retired settings once before startup consumers while preserving effective preferences and vehicle-specific numeric adjustments.
2026-10-03 22:34:37 +00:00
firestar5683 de611f978c Compact Bluetooth settings
Add compact Bluetooth settings after the transport and shared UI owners are available.
2026-10-03 22:34:37 +00:00
firestar5683 ad88da0c8d Software presentation
Expose software operations and status through Galaxy.
2026-10-03 22:34:37 +00:00
firestar5683 c7d90d1715 Bluetooth and Android Auto presentation
Expose Android Auto setup and projection layout through Galaxy interfaces.
2026-10-03 22:34:37 +00:00
firestar5683 ba4d3b249b Display and annotation presentation
Expose onroad layout preferences through Galaxy.
2026-10-03 22:34:37 +00:00
firestar5683 99a8b1c07e Bluetooth and Android Auto
Manage named devices, car-first projection reconnect, and saved head-unit layouts. Keep runtime-safe Bluetooth operations available without changing controller ownership or restarting the radio unnecessarily.
2026-10-03 22:34:37 +00:00
firestar5683 aaac7a6733 Shared settings and onroad views
Present saved configuration and live driving state through native Small and Big UI layouts. Support per-field tune resets, movable widgets, camera availability, and a unified speed card while preserving decision-bound actions.
2026-10-03 22:34:37 +00:00
firestar5683 6e66450bd9 Vision assisted annotation
Implement vision annotation observations, inference and preferences through a dedicated runtime owner.
2026-10-03 22:34:37 +00:00
firestar5683 63ceaaafca Vendored runtime changes
Extend vendored runtime helpers and diagnostics while preserving the existing compiler and model ABI.
2026-10-03 22:34:37 +00:00
firestar5683 4e46324ad5 Protocol integration coverage
Add integrated wire and protocol fixtures that exercise the registered host and vehicle adapters together.
2026-10-03 22:34:37 +00:00
firestar5683 76523bb9e2 Native host protocol and adapter registration
Wire Panda host subscriptions and AOL adapters into the native daemon build and startup path.
2026-10-03 22:34:37 +00:00
firestar5683 942228400c Longitudinal registration and planner wiring
Wire longitudinal feature selection and inputs into planner hosts after their owners are available.
2026-10-03 22:34:37 +00:00
firestar5683 0051aa6047 Audio
Add selectable alert sounds, volume ownership and sound lifecycle handling, including dismissal and repeated-alert boundaries.
2026-10-03 22:34:37 +00:00
firestar5683 7db49d7f22 AOL registration and process wiring
Register vehicle AOL adapters and runtime dispatch after the vehicle factories and native contracts exist.
2026-10-03 22:34:37 +00:00
firestar5683 7bb0b5c6a0 Volvo C1 control port
Add the Volvo C1 port with matching Python registration and exact native parameter admission.
2026-10-03 22:34:37 +00:00
firestar5683 ae34cdca55 Volkswagen identification extensions
Update Volkswagen vehicle identification and parameter coverage.
2026-10-03 22:34:37 +00:00
firestar5683 1cac872d89 Vehicle preferences and startup registration
Resolve startup preferences and controller extensions through capability-aware vehicle owners.
2026-10-03 22:34:37 +00:00
firestar5683 ee1c3e65cc Toyota ADAS and control extensions
Update Toyota ADAS messages and control interfaces with platform-specific safety tests.
2026-10-03 22:34:37 +00:00
firestar5683 cde956b100 Tesla HW1 and namespace safety
Add Tesla HW1 and supported namespace handling with explicit native admission and unknown-bit rejection.
2026-10-03 22:34:37 +00:00
firestar5683 57323c820a Subaru control extensions
Update Subaru vehicle messages and controller/native safety integration.
2026-10-03 22:34:37 +00:00
firestar5683 14cb60b8e5 Longitudinal planner owners
Implement acceleration profiles, FollowJerk, force-stop and lead-departure owners. Keep selected acceleration limits and hazard arbitration explicit at their shared boundaries.
2026-10-03 22:34:37 +00:00
firestar5683 cf0f265ac7 Vehicle policy registration
Select longitudinal policy through the shared vehicle dispatch contract.
2026-10-03 22:34:37 +00:00
firestar5683 7388de875e Mazda interface extensions
Update Mazda vehicle interface behavior with focused stock-control tests.
2026-10-03 22:34:37 +00:00
firestar5683 a71d1f3e48 Torque and lateral controllers
Integrate torque feedback, lane-change smoothing and low-speed Turn Assist through dedicated lateral owners.
2026-10-03 22:34:37 +00:00
firestar5683 697c07e20f Navigation presentation
Expose map operations and status through Galaxy presentation endpoints.
2026-10-03 22:34:37 +00:00
firestar5683 6ba0dd8708 Curve speed
Resolve curve speed and smoothing through a dedicated learning and preference owner.
2026-10-03 22:34:37 +00:00
firestar5683 c74d05643d Controller feature owners
Route controller input and cruise actions through shared transport and mode-action owners.
2026-10-03 22:34:37 +00:00
firestar5683 1ab5058bc8 Conditional experimental mode
Implement conditional driving modes with explicit scene, button and traffic state ownership.
2026-10-03 22:34:37 +00:00
firestar5683 14241d72c8 Speed limits
Select qualified speed-limit sources, preserve acceptance and override decisions, and publish distinct control and display coordinates. Carry accepted and pending source identity with each displayed decision.
2026-10-03 22:34:37 +00:00
firestar5683 59e7e272ba Nostalgia
Add the optional nostalgia behavior as an isolated feature owner.
2026-10-03 22:34:37 +00:00
firestar5683 6369f355ff Hyundai/Kia/Genesis control extensions
Integrate vehicle-specific steering, longitudinal control, startup, and handoff policies, including Ioniq, EV9, GV70, and non-SCC configurations. Preserve the corresponding native safety and CAN routing contracts.
2026-10-03 22:34:37 +00:00
firestar5683 4b8be2b826 Honda steering and AOL extensions
Integrate Honda steering and AOL adapters with platform-specific native safety coverage.
2026-10-03 22:34:37 +00:00
firestar5683 3d8751b21d GM host AOL admission policy
Apply the GM host AOL policy after its control families and capabilities are available.
2026-10-03 22:34:37 +00:00
firestar5683 536fd0c8c6 Ford curvature and control extensions
Preserve vehicle-specific lateral strategies, stock cruise switch context, and native bus permissions for the supported Ford variants. Keep controller policy separate from shared steering integration.
2026-10-03 22:34:37 +00:00
firestar5683 bc8ae160fa Chrysler control extensions
Update Chrysler vehicle parameters and controller behavior with focused platform tests.
2026-10-03 22:34:37 +00:00
firestar5683 8cb60d82fe Developer and build tooling
Provide laptop and device build entry points, cache compatibility checks and developer tooling. Keep distribution assets and model packaging available before runtime consumers.
2026-10-03 22:34:37 +00:00
firestar5683 75327f5ade UI replay and startup helpers
Provide UI replay and startup helpers for inspecting onroad rendering without a live drive.
2026-10-03 22:34:37 +00:00
firestar5683 035fd3b547 Recorded vehicle qualification fixtures
Add compact recorded vehicle fixtures and a runner for reproducible source-level qualification.
2026-10-03 22:34:37 +00:00
firestar5683 e66380e8fa Software and AGNOS
Manage software updates and AGNOS retained state through explicit manifests and update preferences.
2026-10-03 22:34:37 +00:00
firestar5683 8c9dd4b00a Sentry presentation
Expose Sentry events and notification presentation through Galaxy.
2026-10-03 22:34:37 +00:00
firestar5683 2dd715a3d5 Sentry
Implement Sentry scanning, notification state and worker cancellation through explicit policies and preferences.
2026-10-03 22:34:37 +00:00
firestar5683 b282ced7fb Project documentation and attribution
Document supported behavior, contributor attribution and third-party licenses alongside the source distribution.
2026-10-03 22:34:37 +00:00
firestar5683 f71e88ce25 Power
Resolve offroad power preferences through a dedicated owner.
2026-10-03 22:34:37 +00:00
firestar5683 1ce9f73ae2 Navigation and maps
Add navigation and map matching providers with the vendored map service sources.
2026-10-03 22:34:37 +00:00
firestar5683 2818bb072e Model management
Own model discovery, compilation and registry selection through explicit model metadata and geometry contracts.
2026-10-03 22:34:37 +00:00
firestar5683 1ff98bacf6 Galaxy web presentation
Add Galaxy web assets and client settings interfaces with their bundled vendor notices.
2026-10-03 22:34:37 +00:00
firestar5683 51a0d50187 Galaxy access and remote service
Separate Galaxy authentication and local access policy from web presentation.
2026-10-03 22:34:37 +00:00
firestar5683 e38d39358e GM factory and native registration
Register GM controllers, car state, distance-button actions and feature capabilities against the shared platform groups.
2026-10-03 22:34:37 +00:00
firestar5683 6d477cb73e GM Bolt Pedal
Implement Bolt Pedal command generation and mode selection with dedicated native safety coverage. Keep pedal actuation separate from the factory cruise families.
2026-10-03 22:34:37 +00:00
firestar5683 deaa5061b2 GM Volt and factory ACC control owners
Add distinct Volt, Silverado, Suburban and factory ACC control owners rather than sharing pedal assumptions across platforms.
2026-10-03 22:34:37 +00:00
firestar5683 696372c0e8 GM Bolt Cruise Control
Implement Bolt cruise control with its own host and native admission gates.
2026-10-03 22:34:37 +00:00
firestar5683 0de0df06e0 GM shared longitudinal policies
Provide shared GM longitudinal policy helpers for the vehicle-specific controllers.
2026-10-03 22:34:37 +00:00
firestar5683 8235b19e77 GM shared CP and message contracts
Define GM platform groups, startup preferences and CAN messages before their control families are registered.
2026-10-03 22:34:37 +00:00
firestar5683 255f0306eb Drive lifecycle and evidence
Track drive identity and evidence lifecycle so consumers reset state at the same drive boundaries.
2026-10-03 22:34:37 +00:00
firestar5683 79a5cf4fcd Connect service
Own Connect credentials, registration and upload preferences through shared access providers.
2026-10-03 22:34:37 +00:00
firestar5683 7b1cc1f8e2 Camera presentation
Serve fresh camera snapshots with explicit image responses and expose live crop controls without placing file or camera startup work on the UI thread.
2026-10-03 22:34:37 +00:00
firestar5683 d6766098ff Camera lifecycle and source qualification
Coordinate finite camera leases with manager-owned processes and reject frames that predate a request. Preserve the native frame timestamp contract across camera variants.
2026-10-03 22:34:37 +00:00
firestar5683 d734bebcb2 CI and developer qualification
Add control trace and platform qualification tools that exercise explicit safety modes and vehicle coverage.
2026-10-03 22:34:37 +00:00
firestar5683 706e760db0 Vehicle shared contracts and registry
Centralize vehicle lookup, firmware identification and dashboard limits while retaining the stock vehicle registry.
2026-10-03 22:34:37 +00:00
firestar5683 a1af95a500 Common AOL protocol and intent
Define host-to-Panda AOL intent, policy and wire contracts with neutral defaults before vehicle adapters register capabilities.
2026-10-03 22:34:37 +00:00
firestar5683 7c665bf16f Cruise speed contracts
Resolve cruise speed increments through a shared interval owner for consistent button behavior.
2026-10-03 22:34:37 +00:00
firestar5683 ed0407d982 Native safety and AOL contracts
Extend native safety and health contracts with explicit AOL state and bounded message handling. Keep vehicle-specific admissions outside the generic layer.
2026-10-03 22:34:37 +00:00
firestar5683 e996787af0 Shared UI data contracts
Introduce shared feature settings and presentation state so both UI layouts can consume the same resolved values.
2026-10-03 22:34:37 +00:00
firestar5683 b06f37e2c7 Settings schema and storage contracts
Define persistent settings, typed schema fields and storage migrations before feature owners consume them. Keep calibration and parameter readers aligned with the shared data contracts.
2026-10-03 22:34:37 +00:00
994 changed files with 8158 additions and 94292 deletions
-1
View File
@@ -34,7 +34,6 @@ tinygrad_repo/** !filter !diff !merge -text
*.so.* -text
openpilot/cereal/messaging/bridge -text
openpilot/selfdrive/pandad/pandad -text
openpilot/tools/replay/replay -text
openpilot/system/camerad/camerad -text
openpilot/system/loggerd/bootlog -text
openpilot/system/loggerd/encoderd -text
+18 -1
View File
@@ -17,7 +17,7 @@ jobs:
strategy:
fail-fast: false
matrix:
gate: [coverage, opendbc-misra, panda-misra, panda-host]
gate: [coverage, opendbc-misra, panda-misra, panda-host, mutation-list]
steps:
- uses: actions/checkout@v7
- run: ./tools/op.sh setup
@@ -30,3 +30,20 @@ jobs:
name: safety-${{ matrix.gate }}
path: ci-results/safety-${{ matrix.gate }}/
if-no-files-found: error
full_mutation:
name: full safety mutation
runs-on: ubuntu-24.04
timeout-minutes: 360
steps:
- uses: actions/checkout@v7
- run: ./tools/op.sh setup
- name: Run all discovered mutants
run: .venv/bin/python tools/ci/run_safety_qualification.py --gate mutation-full --output ci-results/safety-mutation-full
- name: Save full mutation evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: safety-mutation-full
path: ci-results/safety-mutation-full/
if-no-files-found: error
-28
View File
@@ -1,28 +0,0 @@
name: simulator driving (manual)
on:
workflow_dispatch:
permissions:
contents: read
env:
CI: 1
PYTHONPATH: ${{ github.workspace }}
jobs:
simulator_driving:
name: simulator driving
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- run: ./tools/op.sh setup
- name: Build openpilot
run: scons
- name: Driving test
timeout-minutes: 2
env:
# MetaDrive renders offscreen through panda3d's EGL pipe on llvmpipe
EGL_PLATFORM: surfaceless
run: python openpilot/tools/sim/tests/test_metadrive_bridge.py
+21 -2
View File
@@ -209,8 +209,6 @@ jobs:
openpilot/selfdrive/pandad/tests/test_pandad_canprotocol
openpilot/selfdrive/pandad/tests/test_aol_protocol
openpilot/selfdrive/pandad/tests/test_aol_wire
openpilot/selfdrive/pandad/tests/test_producer_timing
openpilot/selfdrive/pandad/tests/test_producer_sink
- name: Test map provider to host messaging
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
working-directory: mapd_repo
@@ -427,6 +425,27 @@ jobs:
max_attempts: 3
command: cd ${{ github.workspace }}/ci-artifacts && git push origin process-replay --force
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"]')
|| fromJSON('["ubuntu-24.04"]') }}
if: false # FIXME: Started to timeout recently
steps:
- uses: actions/checkout@v7
- run: ./tools/op.sh setup
- name: Build openpilot
run: scons
- name: Driving test
timeout-minutes: 2
env:
# MetaDrive renders offscreen through panda3d's EGL pipe on llvmpipe
EGL_PLATFORM: surfaceless
run: python openpilot/tools/sim/tests/test_metadrive_bridge.py
create_ui_report:
name: Create UI Report
runs-on: ${{
-3
View File
@@ -43,7 +43,6 @@ bin/
*.os
*.os-*
*.so
!openpilot/tools/replay/libmedia_mux.so
*.a
st[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]
*.unchunked
@@ -136,5 +135,3 @@ openpilot/selfdrive/modeld/models/*.unchunked
# Jetlink editable-install metadata.
/jetlink_repo/jetlink.egg-info/
# Local Google Play resolver build output (source and lockfile are versioned).
-8
View File
@@ -17,11 +17,3 @@ The Ford lateral strategies, extended CAN constructors, camera-radar filtering a
Relevant lineage includes the controller and integration work in [`db2bdff05`](https://github.com/BluePilotDev/bluepilot/commit/db2bdff05df103d71df62f45c2a3cb5211aba6e6) and [`d0aac605f`](https://github.com/BluePilotDev/bluepilot/commit/d0aac605f99d37e9da205e419f7989c1e9eaa386), extended CAN and safety work in [`8f8d6d15f`](https://github.com/BluePilotDev/bluepilot/commit/8f8d6d15f0a590f42b78de964ffb0d0af7f5d63d), and manual-turn detection in [`97867c1eb`](https://github.com/BluePilotDev/bluepilot/commit/97867c1eb57b7472f6fc3de62f0fef576e5a5497).
The vehicle strategy and CAN/native owners preserve that attribution while adapting to the current controller interfaces and shared safety limits. Upstream contributors do not maintain or endorse this adaptation. Applicable notices are retained in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
## Optional Ford angle controller
The vendored `opendbc_repo/opendbc/bluepilot_lateral` angle core, codecs and Ford-only safety hooks are adapted directly from [BluePilot bp-7.0 e1d051d7ba270261b4455068bd68f1a58db15a4a](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a), principally by Alan Polk, with John Christman, Jacob Neulight, Nathan Ingraham, Praeuner and other BluePilot, sunnypilot and comma contributors. Exact source paths and intentionally stricter safety checks are recorded in that folder. Both upstream `LICENSE` and `LICENSE.md` notices are retained. Its layout follows Alan Polk's September 2026 library proposal; this is a StarPilot adapter, not a published BluePilot library release.
## RAV4 Continental ARS510 receiver
The optional ACC-target and fused-track receivers and DBC preserve firestar5683's Dom integration from `b220a37f4bd119e402225a7aa16a7cd84878d0af` (Sea Turtles, 2026-10-13) and its ARS510 source attribution to ds-sebastian/ars510-radar `fb5b29c2b5999f34afeb6f7ac494a73949d66c67`, copyright 2026 ARS510 radar contributors. The complete MIT permission and warranty notice is retained in THIRD_PARTY_NOTICES.md. Startup/settings ownership is adapted to the current StarPilot architecture; no original firmware binaries or safety permissions are copied.
+1 -5
View File
@@ -328,16 +328,12 @@ SConscript([
'openpilot/selfdrive/locationd/SConscript',
'openpilot/selfdrive/modeld/SConscript',
'openpilot/selfdrive/ui/SConscript',
'openpilot/starpilot/audio/native/SConscript',
])
# Replay is also the isolated on-device viewer engine.
if GetOption('extras') or arch == "comma_arm64":
SConscript(['openpilot/tools/replay/SConscript'])
# Build desktop-only tools
if GetOption('extras') and arch != "comma_arm64":
SConscript([
'openpilot/tools/replay/SConscript',
'openpilot/tools/cabana/SConscript',
'openpilot/tools/jotpluggler/SConscript',
])
-38
View File
@@ -74,41 +74,3 @@ the exclusion of the other.
## BluePilot Ford support
The Ford lateral strategies, CAN constructors, camera-radar filtering and native checks include work adapted from BluePilot's `bp-7.0` source, through StarPilot `2efe6017bfe14cee4807833e05f9a7f6ba255cdb`. See [CREDITS.md](CREDITS.md#ford-support-adapted-from-bluepilot) for contributors and source revisions. The upstream extension-file and `LICENSE.md` notices reproduced above also apply to the Ford extension sources; they are retained without resolving the difference between their published license descriptions.
## Optional BluePilot angle library adapter
The angle-only adapter in `opendbc_repo/opendbc/bluepilot_lateral` is sourced directly from BluePilot bp-7.0 commit e1d051d7ba270261b4455068bd68f1a58db15a4a. The complete upstream MIT notice is retained in that folder's `LICENSE`, and the simultaneously published Custom MIT notice is retained verbatim in `LICENSE.md`. The applicable SUNNYPILOT acknowledgments reproduced above are also retained in its `CREDITS.md`. This preserves the published terms without resolving their scope or representing the adapted extension as unambiguously plain MIT.
The optional, default-disabled angle lane-centering trim additionally derives from
BluePilot bp-dev e22afa6be9b881fa784c92ebb316db47728a3d81. Its exact source and
contributors are recorded in the library CREDITS.md; the same root/opendbc MIT
and Custom MIT notices coexist unchanged at that revision. This adds attribution,
not a license-scope decision or permission grant.
## Continental ARS510 radar
The optional RAV4 radar decoders and `toyota_tss2_ars510.dbc` are adapted from
<https://github.com/ds-sebastian/ars510-radar> at
`fb5b29c2b5999f34afeb6f7ac494a73949d66c67`.
MIT License
Copyright (c) 2026 ARS510 radar contributors
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.
+3 -25
View File
@@ -44,6 +44,7 @@ for arg in "$@"; do
esac
done
original_args=("$@")
build_helper="${ROOT_DIR}/scripts/laptop_device_build.sh"
if [[ -f /AGNOS && "$(uname -s)" == Linux && "$(uname -m)" == aarch64 ]]; then
build_helper="${ROOT_DIR}/scripts/native_device_build.sh"
@@ -61,8 +62,6 @@ if [[ $# -gt 0 ]]; then
fi
shortcut_targets=()
passthrough_args=()
default_args=()
jobs_arg=""
shortcut_includes_panda=0
panda_generated_files=(panda/board/obj/gitversion.h panda/board/obj/version)
@@ -79,26 +78,11 @@ while [[ $# -gt 0 ]]; do
shortcut_targets+=(openpilot/cereal/libcereal.a openpilot/cereal/libsocketmaster.a
openpilot/cereal/messaging/bridge openpilot/cereal/services.h)
;;
-j|--jobs)
passthrough_args+=("$1")
default_args+=("$1")
if [[ $# -gt 1 ]]; then
passthrough_args+=("$2")
default_args+=("$2")
shift
fi
;;
*)
if [[ "$1" =~ ^[0-9]+$ ]]; then
if [[ "$1" =~ ^0+$ ]]; then
echo "ERROR: jobs must be positive" >&2
exit 2
fi
jobs_arg="$1"
passthrough_args+=("-j$1")
else
passthrough_args+=("$1")
default_args+=("$1")
fi
;;
esac
@@ -144,13 +128,7 @@ fi
python3 "${ROOT_DIR}/tools/release/stage_mapd_provider.py" --source "${ROOT_DIR}" --require-tracked
if [[ -n "${jobs_arg}" ]]; then
if [[ ${default_args[0]+present} ]]; then
exec "${build_helper}" build "${jobs_arg}" "${default_args[@]}"
fi
exec "${build_helper}" build "${jobs_arg}"
fi
if [[ ${default_args[0]+present} ]]; then
exec "${build_helper}" build "${default_args[@]}"
if [[ "${#original_args[@]}" -gt 0 ]]; then
exec "${build_helper}" build "${original_args[@]}"
fi
exec "${build_helper}" build
+73 -15
View File
@@ -1,20 +1,78 @@
# Jetlink runtime for StarPilot
<p align="center">
<img src="macos/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" width="128" height="128" alt="Jetlink icon">
</p>
This subtree supplies the Jetlink remote-model client used by **StarPilot**.
For installing, connecting, selecting, and running it with StarPilot, follow
[the StarPilot JetLink guide](../openpilot/starpilot/models/JETLINK.md).
<h1 align="center">Jetlink</h1>
The comma runs StarPilot; the connected host runs the upstream Jetlink application.
Host apps, installers, development references, and original history are maintained
in [ZoomPilot / Jetlink](https://github.com/zoompilot/jetlink/tree/c17cd5c).
This runtime subtree does not contain the upstream host applications or docs;
their source attribution does not require installing ZoomPilot on the comma.
<p align="center">
Run openpilot's large driving models on a computer connected to your comma.
</p>
Jetlink is experimental. Use separate power and USB 3, set up parked, and verify
StarPilot's active model status. See the local guide for recovery and limitations.
**Jetlink is experimental.** It requires zoompilot's
[`develop` branch](https://github.com/zoompilot/zoompilot/tree/develop).
If the link drops or lags while engaged, the comma says **TAKE CONTROL** and
stays engaged on the small model. Be ready to take over.
## Attribution
## Quick start
Jetlink was developed by Zeph Leggett / ZoomPilot. Copyright (c) 2026 Zeph Leggett.
The original [MIT license](LICENSE) is preserved without modification.
[Upstream README and credits](https://github.com/zoompilot/jetlink/blob/c17cd5c/README.md).
You need a **comma 3X or comma 4**, a **USB 3 data cable**, and **separate power
for the comma and computer**. Set up offroad with both devices online.
Choose your computer. Each guide covers installation and connecting the comma.
| Computer | Setup guide |
| --- | --- |
| Jetson Orin Nano Super | [Set up a Jetson](docs/jetson.md) |
| Apple silicon Mac | [Set up a Mac](docs/macos-app.md) |
| Linux PC with an NVIDIA GPU | [Set up a Linux PC](docs/linux-pc.md) |
| iPhone or iPad with USB-C | [Install with TestFlight](docs/iphone-app.md) |
| Android phone with USB 3 | [Try the experimental Android app](docs/android-app.md) |
## Demos
### Jetson
<a href="docs/images/install-demo.mp4"><img src="docs/images/install-demo.webp" width="100%" alt="Jetlink installer on a Jetson"></a>
<details>
<summary>Watch the web page</summary>
<a href="docs/images/webui-demo.mp4"><img src="docs/images/webui-demo.webp" width="100%" alt="The Jetson's web page on a phone and a computer, with simulated drive data: sign in, frame timing against the 50 ms budget, a model downloaded and prepared (sped up), the power setting applied, and keep awake"></a>
</details>
### Mac
<a href="docs/images/mac-demo.mp4"><img src="docs/images/mac-demo.webp" width="100%" alt="Jetlink for Mac: Use Model downloads and prepares a model, the comma connects over USB, and Status shows each frame against the 50 ms budget"></a>
### iPhone and iPad (experimental)
Simulator demo.
<a href="docs/images/iphone-demo.mp4"><img src="docs/images/iphone-demo.webp" width="100%" alt="Jetlink for iPhone, recorded in the iOS Simulator: Get downloads and prepares a model, the comma connects over USB, and Status shows each frame against the 50 ms budget, with timings modeled on an iPhone 17 Pro measurement"></a>
<a href="https://testflight.apple.com/join/DAsYk5sP"><img src="docs/images/testflight-badge.svg" alt="Available on TestFlight" height="40"></a>
### Android (experimental)
Emulator demo.
<a href="docs/images/android-demo.mp4"><img src="docs/images/android-demo.webp" width="100%" alt="Jetlink for Android, recorded in the Android emulator: Get downloads a model, the comma connects over USB, and Status shows each frame against the 50 ms budget. The download is sped up, the emulator's CPU preparation cut, and the model's time modeled on an estimate for a Snapdragon 8 Gen 3"></a>
<a id="comma-setup-all-platforms"></a>
<a id="jetson-or-linux-pc"></a>
<a id="linux-pc"></a>
<a id="what-to-expect-when-driving"></a>
<a id="if-something-is-wrong"></a>
<a id="more"></a>
## Help
[Setup guides](docs/README.md#set-up) ·
[Daily use](docs/using-jetlink.md) ·
[Troubleshooting](docs/troubleshooting.md) ·
[Operating limits](docs/status.md)
## License
[MIT](LICENSE).
-2
View File
@@ -83,8 +83,6 @@ function launch {
mv "$DIR" /data/safe_staging/old_openpilot
mv "${STAGING_ROOT}/finalized" "$DIR"
cd "$DIR"
# releases before the openpilot/ move can't swap AGNOS on newer AGNOS, use ours
[ ! -L system/hardware/tici/agnos.json ] && /data/safe_staging/old_openpilot/openpilot/common/hardware/comma/agnos.py --verify system/hardware/tici/agnos.json && sudo reboot
echo "Restarting launch script ${LAUNCHER_LOCATION}"
unset AGNOS_VERSION
Binary file not shown.
+4
View File
@@ -161,6 +161,7 @@ cdef class Poller:
cdef class SubSocket:
cdef cppSubSocket * socket
cdef bool is_owner
def __cinit__(self):
with nogil:
self.socket = cppSubSocket.create()
@@ -201,6 +202,7 @@ cdef class SubSocket:
cdef cppMessage *msg
with nogil:
msg = self.socket.receive(non_blocking)
if msg == NULL:
return None
else:
@@ -208,8 +210,10 @@ cdef class SubSocket:
m = msg.getData()[:sz]
with nogil:
del msg
return m
cdef class PubSocket:
cdef cppPubSocket * socket
Binary file not shown.
-2
View File
@@ -443,8 +443,6 @@ int msgq_poll(msgq_pollitem_t * items, size_t nitems, int timeout){
if (items[i].revents) num++;
}
if (timeout == 0) return num;
int ms = (timeout == -1) ? 100 : timeout;
#ifdef __APPLE__
-85
View File
@@ -1,6 +1,4 @@
#include <array>
#include <chrono>
#include <thread>
#include <unistd.h>
#include "msgq/test_runner.h"
#include "msgq/msgq.h"
@@ -22,89 +20,6 @@ TEST_CASE("ALIGN")
REQUIRE(ALIGN(99999) == 100000);
}
TEST_CASE("msgq_poll nonblocking readiness")
{
cleanup_test_queue();
msgq_queue_t writer, first, second;
msgq_new_queue(&writer, "test_queue", 1024);
msgq_init_publisher(&writer);
msgq_new_queue(&first, "test_queue", 1024);
msgq_init_subscriber(&first);
msgq_new_queue(&second, "test_queue", 1024);
msgq_init_subscriber(&second);
msgq_pollitem_t items[] = {{&first, 1}, {&second, 1}};
REQUIRE(msgq_poll(nullptr, 0, 0) == 0);
REQUIRE(msgq_poll(items, 2, 0) == 0);
REQUIRE(items[0].revents == 0);
REQUIRE(items[1].revents == 0);
char data[] = "poll payload";
msgq_msg_t outgoing, incoming;
msgq_msg_init_data(&outgoing, data, sizeof(data));
REQUIRE(msgq_msg_send(&outgoing, &writer) == sizeof(data));
REQUIRE(msgq_poll(items, 2, 0) == 2);
REQUIRE(items[0].revents == 1);
REQUIRE(items[1].revents == 1);
REQUIRE(msgq_msg_recv(&incoming, &first) == sizeof(data));
REQUIRE(memcmp(incoming.data, data, sizeof(data)) == 0);
msgq_msg_close(&incoming);
REQUIRE(msgq_poll(items, 2, 0) == 1);
REQUIRE(items[0].revents == 0);
REQUIRE(items[1].revents == 1);
REQUIRE(msgq_msg_recv(&incoming, &second) == sizeof(data));
REQUIRE(memcmp(incoming.data, data, sizeof(data)) == 0);
msgq_msg_close(&incoming);
REQUIRE(msgq_poll(items, 2, 0) == 0);
REQUIRE(items[0].revents == 0);
REQUIRE(items[1].revents == 0);
msgq_msg_close(&outgoing);
msgq_close_queue(&second);
msgq_close_queue(&first);
msgq_close_queue(&writer);
cleanup_test_queue();
}
TEST_CASE("msgq_poll waiting modes")
{
for (int timeout : {1000, -1}) {
cleanup_test_queue();
msgq_queue_t writer, reader;
msgq_new_queue(&writer, "test_queue", 1024);
msgq_init_publisher(&writer);
msgq_new_queue(&reader, "test_queue", 1024);
msgq_init_subscriber(&reader);
msgq_pollitem_t item{&reader, 1};
auto start = std::chrono::steady_clock::now();
REQUIRE(msgq_poll(&item, 1, 2) == 0);
REQUIRE(item.revents == 0);
REQUIRE(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(2));
msgq_msg_t outgoing;
msgq_msg_init_size(&outgoing, 1);
outgoing.data[0] = 'p';
std::thread sender([&]() {
std::this_thread::sleep_for(std::chrono::milliseconds(2));
msgq_msg_send(&outgoing, &writer);
});
int received = msgq_poll(&item, 1, timeout);
sender.join();
REQUIRE(received == 1);
REQUIRE(item.revents == 1);
msgq_msg_t incoming;
REQUIRE(msgq_msg_recv(&incoming, &reader) == 1);
REQUIRE(incoming.data[0] == 'p');
msgq_msg_close(&incoming);
msgq_msg_close(&outgoing);
msgq_close_queue(&reader);
msgq_close_queue(&writer);
cleanup_test_queue();
}
}
TEST_CASE("msgq_msg_init_size")
{
const size_t msg_size = 30;
Binary file not shown.
+1 -1
View File
@@ -116,7 +116,7 @@ jobs:
path: /tmp/comma_download_cache
key: car_models-${{ hashFiles('opendbc/car/tests/test_models.py', 'opendbc/car/tests/routes.py') }}-${{ matrix.job }}
- name: Test car models
timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.routes-cache.outputs.cache-hit == 'true') && 2 || 8 }}
timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.routes-cache.outputs.cache-hit == 'true') && 2 || 6 }}
run: source .venv/bin/activate && MAX_EXAMPLES=1 opendbc/car/tests/test_models.py
env:
NUM_JOBS: 4
+3 -45
View File
@@ -1,6 +1,6 @@
<!--- AUTOGENERATED FROM selfdrive/car/CARS_template.md, DO NOT EDIT. --->
# Support Information for 448 Known Cars
# Support Information for 406 Known Cars
|Make|Model|Package|Support Level|
|---|---|---|:---:|
@@ -34,50 +34,12 @@
|Audi|Q5 2017-24|All|[Not compatible](#flexray)|
|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)|
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)|
|Buick|Baby Enclave (SDGM Harness) 2020-23|Driver Assist Package|[Community](#community)|
|Buick|LaCrosse (ASCM Harness) 2017-19|Adaptive Cruise Control (ACC)|[Community](#community)|
|Buick|LaCrosse (OBD-II Harness) 2017-19|Driver Confidence Package 2|[Community](#community)|
|Buick|LaCrosse US (ASCM Harness) 2019|Adaptive Cruise Control (ACC)|[Community](#community)|
|Buick|Regal Essence (OBD-II Harness) 2018|Adaptive Cruise Control (ACC)|[Community](#community)|
|BYD|Atto 3 2022-25|All|[Dashcam mode](#dashcam)|
|Cadillac|ATS Premium Performance (OBD-II Harness) 2018|Adaptive Cruise Control (ACC)|[Community](#community)|
|Cadillac|CT6 (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Cadillac|Escalade (ASCM Harness) 2018|Adaptive Cruise Control (ACC)|[Community](#community)|
|Cadillac|Escalade (OBD-II Harness) 2017|Driver Assist Package|[Community](#community)|
|Cadillac|Escalade ESV (OBD-II Harness) 2016|Adaptive Cruise Control (ACC) & LKAS|[Community](#community)|
|Cadillac|Escalade ESV (OBD-II Harness) 2019|Adaptive Cruise Control (ACC) & LKAS|[Community](#community)|
|Cadillac|Escalade ESV Platinum (ASCM Harness) 2019|Adaptive Cruise Control (ACC)|[Community](#community)|
|Cadillac|XT4 (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Cadillac|XT4 (SDGM Harness) 2023|Driver Assist Package|[Community](#community)|
|Cadillac|XT5 (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Cadillac|XT5 (SDGM Harness) 2022|Driver Assist Package|[Community](#community)|
|Cadillac|XT6 (SDGM Harness) 2020|Driver Assist Package|[Community](#community)|
|Chevrolet|Blazer (SDGM Harness) 2019-25|Driver Assist Package|[Community](#community)|
|Chevrolet|Bolt (Camera Harness) 2022-23|ACC; recognized camera layout required|[Community](#community)|
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|[Upstream](#upstream)|
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|[Upstream](#upstream)|
|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|Chevrolet|Equinox (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Chevrolet|Malibu (ASCM Harness) 2017-19|Adaptive Cruise Control (ACC)|[Community](#community)|
|Chevrolet|Malibu (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Chevrolet|Malibu (SDGM Harness) 2019|SDGM Harness|[Community](#community)|
|Chevrolet|Malibu Hybrid (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Chevrolet|Malibu Premier (OBD-II Harness) 2017|Adaptive Cruise Control (ACC)|[Community](#community)|
|Chevrolet|Silverado 1500 2020-21|Safety Package II|[Upstream](#upstream)|
|Chevrolet|Silverado (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Chevrolet|Suburban (Camera Harness)|Manual camera configuration; recognized CAN layout required|[Community](#community)|
|Chevrolet|Suburban (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Chevrolet|Suburban (OBD-II Harness)|Configuration-specific install; observed CAN sources required|[Community](#community)|
|Chevrolet|Suburban Premier (ASCM Harness) 2016-20|Adaptive Cruise Control (ACC)|[Community](#community)|
|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|Chevrolet|Trailblazer (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Chevrolet|Traverse (SDGM Harness) 2022-23|RS, Premier, or High Country Trim|[Community](#community)|
|Chevrolet|Trax (Camera Harness)|Manual camera configuration; recognized CAN layout required|[Community](#community)|
|Chevrolet|Volt (ASCM Harness) 2017-18|Adaptive Cruise Control (ACC); custom ASCM harness install|[Community](#community)|
|Chevrolet|Volt (Camera Harness)|Manual configuration; recognized camera layout required|[Community](#community)|
|Chevrolet|Volt (CC)|Manual development configuration; installation must be verified|[Community](#community)|
|Chevrolet|Volt (OBD-II Harness) 2017-18|Adaptive Cruise Control (ACC)|[Community](#community)|
|Chevrolet|Volt (SDGM Harness) 2019|Adaptive Cruise Control (ACC) & LKAS|[Community](#community)|
|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|Chrysler|Pacifica 2019-20|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|Chrysler|Pacifica 2021-23|All|[Upstream](#upstream)|
@@ -126,12 +88,8 @@
|Genesis|GV70 Electrified (Australia Only) 2022|All|[Upstream](#upstream)|
|Genesis|GV70 Electrified (with HDA II) 2023-24|Highway Driving Assist II|[Upstream](#upstream)|
|Genesis|GV80 2023|All|[Upstream](#upstream)|
|GMC|Acadia (ASCM Harness) 2018|Adaptive Cruise Control (ACC)|[Community](#community)|
|GMC|Acadia (OBD-II Harness) 2018|Adaptive Cruise Control (ACC)|[Community](#community)|
|GMC|Sierra 1500 2020-21|Driver Alert Package II|[Upstream](#upstream)|
|GMC|Yukon 2019-20|Adaptive Cruise Control (ACC) & LKAS|[Dashcam mode](#dashcam)|
|GMC|Yukon (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Holden|Astra (OBD-II Harness) 2017|Adaptive Cruise Control (ACC)|[Community](#community)|
|Honda|Accord 2016-17|Honda Sensing|[Community](#community)|
|Honda|Accord 2018-22|All|[Upstream](#upstream)|
|Honda|Accord 2023-25|All|[Upstream](#upstream)|
@@ -153,7 +111,7 @@
|Honda|CR-V 2017-22|Honda Sensing|[Upstream](#upstream)|
|Honda|CR-V 2023-26|All|[Upstream](#upstream)|
|Honda|CR-V Hybrid 2017-22|Honda Sensing|[Upstream](#upstream)|
|Honda|CR-V Hybrid 2023-27|All|[Upstream](#upstream)|
|Honda|CR-V Hybrid 2023-26|All|[Upstream](#upstream)|
|Honda|e 2020|All|[Upstream](#upstream)|
|Honda|Fit 2018-20|Honda Sensing|[Upstream](#upstream)|
|Honda|Freed 2020|Honda Sensing|[Upstream](#upstream)|
@@ -515,4 +473,4 @@ Toyota, and the GM Global B platform.
All the cars that openpilot supports use a [CAN bus](https://en.wikipedia.org/wiki/CAN_bus) for communication between all the car's computers, however a
CAN bus isn't the only way that the computers in your car can communicate. Most, if not all, vehicles from the following
manufacturers use [FlexRay](https://en.wikipedia.org/wiki/FlexRay) instead of a CAN bus: **BMW, Mercedes, Audi, Land Rover, and some Volvo**. These cars
may one day be supported, but we have no immediate plans to support FlexRay.
may one day be supported, but we have no immediate plans to support FlexRay.
@@ -1,35 +0,0 @@
# BluePilot angle source
Adapted from BluePilotDev/bluepilot bp-7.0 at
[e1d051d7ba270261b4455068bd68f1a58db15a4a](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a),
principally Alan Polk, with John Christman, Jacob Neulight, Nathan Ingraham,
Praeuner, tonesto7, Haibin Wen and other BluePilot, sunnypilot and comma contributors.
Source paths: opendbc/sunnypilot/car/ford/lateral_angle_ext.py,
fordcan_ext.py, human_turn.py and opendbc/safety/modes/ford.h.
The simultaneous upstream LICENSE and LICENSE.md notices are retained.
This software is licensed under a custom license requiring permission for use.
This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use.
This is a StarPilot adapter to the September 2026 proposed library shape.
It is not a released BluePilot library, tag, or claim of BluePilot validation.
The existing curvature controller remains StarPilot-owned. Pinion curvature,
curvature-primary settings and the unsafe native reset bypass are not imported.
The exact upstream root notices are additionally retained as upstream-root-LICENSE
and upstream-root-LICENSE.md; LICENSE/ LICENSE.md retain the opendbc notices.
ORIGIN_HISTORY.tsv records the inspected path history with commit IDs/authors/dates.
The private compact upstream root clone preserves its reachable root history.
Pre-conversion opendbc submodule objects are not included in that root history.
Angle lane-centering trim is separately adapted from BluePilot bp-dev
[e22afa6be9b881fa784c92ebb316db47728a3d81](https://github.com/BluePilotDev/bluepilot/commit/e22afa6be9b881fa784c92ebb316db47728a3d81),
`opendbc/sunnypilot/car/ford/lane_center_trim.py` (blob
`5eedebd79e99493624f9d6af0750d1dd5713306c`). Alan Polk authored the initial
trim, model-position fallback, correction slew and default-strength changes;
ghbarker contributed reference/NaN guards and tests. The source explicitly credits
StarPilot/u/jc01rho for curvature-domain placement/geometry. The core adaptation
adds immutable host projection and conservative stale/driver-pressure suppression.
The pinned bp-7.0 controller baseline and default-disabled policy are unchanged.
Both simultaneous root/opendbc MIT and Custom MIT notices also exist unchanged at
this trim revision; all retained notices and permission acknowledgments apply
without assigning an exclusive license or claiming a new permission grant.
@@ -1,7 +0,0 @@
Copyright (c) 2020, 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.
@@ -1,21 +0,0 @@
# Custom MIT License
Copyright (c) 2024, Haibin Wen, SUNNYPILOT LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to view and modify the Software, subject to the following conditions:
1. **Permission Required**: Permission Required for Commercial, For-Profit, or Closed Source Use: Use of the Software, in whole or in part, for any commercial purposes, for-profit projects, or in closed source projects requires explicit written permission from the original author(s).
2. **Redistribution**: Any redistribution of the Software, modified or unmodified, must retain this license notice and the following acknowledgment:
"This software is licensed under a custom license requiring permission for use."
3. **Visibility**: Any project that uses the Software must visibly mention the following acknowledgment:
"This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use."
4. **No Warranty**: 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.
Contact sunnypilot Support <support@sunnypilot.ai> for permission requests.
---
Haibin Wen, SUNNYPILOT LLC
@@ -1,38 +0,0 @@
9d190417bf74563abfa6f36996f1e673e9ee0a1b Praeuner 2026-08-13T22:26:15-05:00 Update dampening factor to .25
a881646ea5edc21c0db4c4e74076e0dc14671a4e alan-polk 2026-08-10T09:04:52-05:00 Merge pull request #176 from BluePilotDev/bp-no-stall
9012f76666a5c90764fcaec40832b9a607488c27 John Christman 2026-08-05T17:49:59-04:00 anti-stall
3610e3f18a0ad37d793d7ada61dbc739ab1077a3 Nathan Ingraham 2026-07-23T22:26:29-05:00 self.path_angle_gain_lowC_highV remains the platform-specific base gain. The toggle now controls separate self.user_dampening_factor. l ow_gain_calc uses: self.path_angle_gain_lowC_highV * self.user_dampening_factor UI placement, description, default, and 0.75–1.25 range remain unchanged.
1db52bc79e607ddb00214254dd4d732615e27fe4 Nathan Ingraham 2026-07-23T00:00:51-05:00 -Added “High Speed Dampening” directly below “High Speed Adjustment Factor”, range 0.75–1.25, step 0.01, and default 1.0.
5d994c24edf63f3b9004b2963f18ef4199e5d368 Jacob Neulight 2026-07-18T20:17:04-06:00 Ford safety: optional pinion-sourced angle_meas behind STEER_ANGLE_CURVATURE SP flag
699c17d9fde62c690eed9d68eed7d731744d5137 Jacob Neulight 2026-07-18T19:53:46-06:00 Ford: publish angle-mode shadow curvature from measured curvature during overrides
b7428fee4b116d4832f91c850d54cac2730de2ef alan-polk 2026-07-18T08:52:24-05:00 restore curvature safety checks to bp-6.0 logic.
97867c1eb57b7472f6fc3de62f0fef576e5a5497 alan-polk 2026-07-16T19:18:05-05:00 Human Turn Reset for Angle (#118)
7efee67e3e643d52b973895d6c6765c5cc12e314 alan-polk 2026-07-16T08:19:43-05:00 angle mode: rescale exit-detection threshold for 20Hz cadence
1958a3501ead10ae1f496f0d11e1e8cda6e2dbcc alan-polk 2026-07-14T15:22:38-05:00 Bp 7.0 variable cleanup (#111)
8f8d6d15f0a590f42b78de964ffb0d0af7f5d63d alan-polk 2026-07-14T08:53:28-05:00 Bp 7.0 panda2 (#109)
11ac3b760671b489ca449d522f1edf240a232884 alan-polk 2026-07-09T07:53:21-05:00 angle mode: fix ROC values tuned for 100Hz cadence running at actual 20Hz
dbdcea747e65f574d326a55c9bf1a9d6c9485e22 alan-polk 2026-07-08T09:21:32-05:00 Revert "ford: add current-curvature deviation check to angle mode (panda-side)"
69a9714563622729ba304ac75354ba3fd37b0b39 alan-polk 2026-07-08T09:17:09-05:00 ford: add current-curvature deviation check to angle mode (panda-side)
be7f86a5d0dc1ea3b3f0052d4b54ff0b8a70f843 John Christman 2026-07-06T21:53:55-04:00 cast on debug
5c1fbb07dd4d7d950a01cb2cd1faddd90d41e632 alan-polk 2026-07-06T14:20:49-05:00 controllerStateBP: add angleRateLimited + curvatureRateLimited diagnostics
06d654e5f48ea8fcffce6e98e48e3a30450b5097 alan-polk 2026-06-25T23:28:51-05:00 Revert "temp blend ratio change" — restore predicted-curvature blend to 0.50
540e69fcf4861a2ce5d7ca41467effc231638e27 alan-polk 2026-06-22T08:35:44-05:00 temp blend ratio change
e3819c7e630d85cfb44858840124fe7f72f1762d alan-polk 2026-06-13T20:47:09-05:00 ford.h: don't block angle mode on the inactive curvature signal
5d343738e30267688d0ffa6eebe2ec3ca57b10ee alan-polk 2026-06-13T18:28:30-05:00 ford.h: widen path_angle ROC to 2% looser than lateral_angle_ext _soft_roc
d0aac605f99d37e9da205e419f7989c1e9eaa386 alan-polk 2026-06-13T11:15:50-05:00 BluePilot: add angle-primary lateral control mode
51d5de8991508c862e7c51333e5055425b2b8d23 alan-polk 2026-04-11T12:00:10-05:00 fixing missing punctation in ford.h
8194ceb7e0bbc112c73d393a8f0add3261f6592d alan-polk 2026-04-11T11:51:49-05:00 make safety limits symetrical but control limits asymetrical. Add 1.5s duration to detect human turn
db2bdff05df103d71df62f45c2a3cb5211aba6e6 alan-polk 2026-04-09T23:47:30-05:00 move all external logic to lateral_curv_ext.py. Apply some order of operations fixes to try and eleminate panda blocking messages.
1a8ac23b5bd5d7952c15eb913c2b1e39f14ad92e Blue Genie 2026-04-08T15:10:24-05:00 Adjust panda limits
892be076daf898d806983f847c72357accff7312 Blue Genie 2026-04-08T11:45:28-05:00 Add logging functionality
ce7d27230657159332f64a253a0faea6cf8dcc95 alan-polk 2026-04-08T11:40:02-05:00 fix rate of change limits
021b5146e80ac58fe2e81dbd848bc7cd409ef08e alan-polk 2026-04-07T16:24:38-05:00 align ford.h limits with dbc and carcontroller
7f9212f7f8dc22c4a0a22443158554f4d3652a3c tonesto7 2026-04-05T21:28:37-04:00 Merge bp-6.1 upstream changes into bp-6.0-dev (#124)
dd1a9d3fd8f60462d3d8ac6de3e7860e70e1f667 alan-polk 2025-12-12T12:22:31-06:00 convert opendbc from a submodule to a subfolder
10101012d8a4cc37951a557023824cdd995443a6 alan-polk 2026-08-10T17:23:04Z BluePilot: angle-mode lane centering trim (advanced lane positioning)
560e80e9d593badca08aa60e8ddb692e58033425 alan-polk 2026-08-11T12:50:00Z BluePilot: angle-mode lane centering falls back to model position, not zero
94bf8144d4a683e9f3408f3babf892878f3b81f4 ghbarker 2026-08-12T01:31:33Z ford ALP: restore StarPilot guards (narrow lane, laneline std, NaN params) and cap applied correction at 0.0015
80825b81568ad684fe4ec37a797d3cbaa004fdcf ghbarker 2026-08-13T01:04:02Z split applied-correction ceiling into its own PR per review
e5b92cc48b34ea43ac756a96a96687c9851a188d ghbarker 2026-08-13T01:05:07Z add tests for the restored guards
af4bc410c9ef417ba723212ceaa2a77758dd8ecb alan-polk 2026-08-14T03:24:28Z BluePilot: rate-limit the lane centering correction, not just filter it
8eea9eb68e0c4126ee1c52f251e274d5865b6743 alan-polk 2026-08-19T13:33:00Z BluePilot: lower angle-mode lane centering default strength to 0.25
Can't render this file because it contains an unexpected character in line 14 and column 85.
@@ -1,81 +0,0 @@
# BluePilot Ford angle control
StarPilot adaptation of BluePilotDev/bluepilot `bp-7.0`, pinned to
[e1d051d7ba270261b4455068bd68f1a58db15a4a](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a).
The layout follows Alan Polk's September 2026 library proposal. This is an
adaptation, not an upstream library release.
Existing curvature control remains the default. Explicit `FordLateralMode=1`
selects angle control next drive; the saved choice persists when unavailable.
Only this selector is exposed. Fixed defaults and bounds live in
`params/manifest.json`; pinion-yaw correction and curvature-primary tuning are
outside this library. Angle lane-centering trim is available as a default-disabled
core capability, separately sourced from BluePilot bp-dev
`e22afa6be9b881fa784c92ebb316db47728a3d81`; it adds no public settings.
`core/` accepts scalar/model inputs without openpilot, Params or messaging
imports. `can/` owns encoding, `safety/ford_bp.h` owns native enforcement, and
`hosts/starpilot.py` connects the host. `params/protocol.json` generates the wire
constants. The folder is vendored without submodules, LFS or an external runtime.
## Admission
Classic: Bronco Sport Mk1, Escape Mk4, Focus Mk4, Maverick Mk1, Explorer Mk6.
CAN FD: Escape Mk4.5, Expedition Mk4, F150 Mk14, F150 Lightning Mk1, Ranger Mk2,
Mustang Mach-E Mk1.
Exact classic safety parameters 32/33 map to angle 128/129; CAN FD 18/19 and
66/67 map to 130/131. Parameter 131 requires DEBUG; RELEASE retains stock 130.
Longitudinal, radar, fingerprint and parser policy remain host-owned. Admission
uses the final CarParams contract, not brand or steering-control enum alone.
AOL, alternative experiences, Transit, Edge, Mondeo, SecOC, passive, dashcam,
notCar and unsupported static flags are excluded. Auxiliary noOutput requires
parameter zero; production deployments with two Pandas remain unqualified until
actual startup, bus routing and public acknowledgments are verified.
## Safety design
The extension uses small Ford hooks without changing shared safety headers or
existing curvature namespaces. Active steering requires ordinary controls
permission, healthy physical RX and a fresh matching public Panda acknowledgment
of safety parameter, alternative experience, controls, RX validity, heartbeat and
faults. Cruise, gas/brake, checksum, counter, quality and freshness checks apply.
Neutral mode cannot grant permission.
The BluePilot reset-bypass latch is omitted. Rejected frames preserve accepted
actuator history and withdraw the shadow lease; accepted neutral commands or
permission withdrawal reset history. Lost counters recover through neutral
resynchronization. Version or checksum failure cannot fall through to curvature
control.
Active frames carry neutral curvature, offset and curvature rate, with checked
mode, ramp, precision, path angle, counter, checksum and cadence. Native enforcement
checks the shadow against the actual path across gains 0.95–1.3, bounds both by
`min(0.02, 2.4114/v²)`, and bounds the full actual-curvature interval against
measured curvature above 10 m/s. Actual path also obeys the 3.5886 m/s³ equivalent
jerk limit and rolling message-rate limit. Core quantization and timing margins
keep packed commands inside these bounds; an impossible intersection is neutral.
The versioned 0x3CA announcement preserves StarPilot's existing byte 4 bit 1
curvature flag and requires it clear in angle mode. It carries a counter, signed
shadow curvature, protocol version and checksum; its lease expires after 60 ms.
The shadow derives from the final path after rate limiting and saturation.
Control computes at 20 Hz, announces through 33 Hz LKA, then sends the latest
announced result at 20 Hz, adding at most one steering tick. Permission withdrawal
sends neutral immediately. Human-turn recovery uses bounded neutral blips without
a native bypass. The retained Dom factory steering delay is 0.22 s; pinned
BluePilot uses 0.20 s.
These stricter native limits and versioned metadata intentionally differ from
BluePilot 7.0. Identical bytes or safety decisions are not claimed for affected
commands. Runtime and physical ECU qualification remain pending.
## Attribution and notices
See `CREDITS.md`, `ORIGIN_HISTORY.tsv` and the project `THIRD_PARTY_NOTICES.md`.
Both upstream MIT and Custom MIT notices are retained verbatim in `LICENSE`,
`LICENSE.md`, `upstream-root-LICENSE` and `upstream-root-LICENSE.md`. The maintainer
reports author permission to use this adaptation under MIT. Both published notices
remain intact.
@@ -1,34 +0,0 @@
"""Adapter to the proposed BluePilot library; angle only, exact source pinned."""
from opendbc.bluepilot_lateral.can.ford import lateral, lka
from opendbc.bluepilot_lateral.core.angle import Command
from opendbc.bluepilot_lateral.hosts.starpilot import Host, qualified
class BluePilotLateral:
def __init__(self, CP, *, host=None):
if not qualified(CP):
raise ValueError("Angle mode is not configured")
self.host = host or Host(CP)
self.canfd = bool(CP.flags & 1)
self.pending = Command()
self.announced = Command()
self.announced_ns = 0
self.lka_counter = 0
self.apply_curvature_last = 0.0
def step_lateral(self, CC, CS, packer, CAN, frame, now_ns):
self.pending = self.host.update(CC, CS)
command = self.announced
# A pending permission withdrawal is immediate; an increase waits until
# the 33Hz side channel has announced the matching final actuator.
if not self.pending.active or not 0 < self.announced_ns <= now_ns <= self.announced_ns + 60_000_000:
command = Command()
self.announced = command
return [lateral(packer, CAN.main, command, canfd=self.canfd, counter=frame // 5)]
def step_lka(self, packer, CAN, now_ns):
self.announced = self.pending
self.announced_ns = now_ns
message = lka(packer, CAN.main, self.announced.shadow_curvature, self.lka_counter)
self.lka_counter = (self.lka_counter + 1) % 8
return [message]
@@ -1,37 +0,0 @@
"""Ford angle codecs, adapted from BluePilot fordcan_ext.py at e1d051d7.
Core commands use planner signs; path and shadow are negated on the wire.
The v1 side channel retains byte4 bit1 for StarPilot's curvature announcement.
"""
from opendbc.bluepilot_lateral.params import protocol as p
def checksum4(dat):
return (15 - (sum(dat[:7]) & 15)) & 15
def lka(packer, bus, shadow, counter):
addr, original, _ = packer.make_can_msg("Lane_Assist_Data1", bus, {"LaRefAng_No_Req": 0., "LaCurvature_No_Calc": 0.})
dat = bytearray(original)
raw = max(-20000, min(20000, round(-shadow / p.SHADOW_SCALE))) & 0xFFFF
dat[4] |= p.ANGLE_MASK | ((counter % p.COUNTER_MODULUS) << p.COUNTER_SHIFT)
dat[5], dat[6] = raw >> 8, raw & 255
dat[7] = p.VERSION | (checksum4(dat) << p.CHECKSUM_SHIFT)
return addr, bytes(dat), bus
def lateral(packer, bus, command, *, canfd, counter):
values = {"HandsOffCnfm_B_Rq": 0, "LatCtlRampType_D_Rq": 2 if command.active else 0,
"LatCtlPrecision_D_Rq": command.precision, "LatCtlPathOffst_L_Actl": 0.0,
"LatCtlPath_An_Actl": -command.path_angle, "LatCtlCurv_No_Actl": 0.0}
if canfd:
values.update({"LatCtl_D2_Rq": int(command.active), "LatCtlCrv_NoRate2_Actl": 0.0,
"LatCtlPath_No_Cnt": counter % 16, "LatCtlPath_No_Cs": 0})
dat = packer.make_can_msg("LateralMotionControl2", bus, values)[1]
fields = ((dat[2] << 3) | (dat[3] >> 5), (dat[6] << 3) | (dat[7] >> 5),
((dat[3] & 31) << 6) | (dat[4] >> 2), ((dat[4] & 3) << 8) | dat[5])
total = int(command.active) + counter % 16 + sum(v + (v >> 8) for v in fields)
values["LatCtlPath_No_Cs"] = 255 - (total & 255)
return packer.make_can_msg("LateralMotionControl2", bus, values)
values.update({"LatCtl_D_Rq": int(command.active), "LatCtlRng_L_Max": 0, "LatCtlCurv_NoRate_Actl": 0.0})
return packer.make_can_msg("LateralMotionControl", bus, values)
@@ -1,198 +0,0 @@
"""Angle-primary Ford steering adapted from Alan Polk's BluePilot bp-7.0.
Source e1d051d7ba270261b4455068bd68f1a58db15a4a lateral_angle_ext.py.
See ../LICENSE, ../LICENSE.md and ../CREDITS.md. This core has no vehicle,
openpilot, Params, messaging or model-constant imports.
"""
from dataclasses import dataclass
import math
from ..params import limits as bounds
from .trim import LaneCenterTrim, LaneGeometry, TrimConfig
def clip(value, low, high):
return max(low, min(high, value))
def interp(value, xs, ys):
if value <= xs[0]:
return ys[0]
for i in range(1, len(xs)):
if value < xs[i]:
return ys[i - 1] + (ys[i] - ys[i - 1]) * (value - xs[i - 1]) / (xs[i] - xs[i - 1])
return ys[-1]
@dataclass(frozen=True)
class Inputs:
active: bool
speed: float
measured_curvature: float
desired_curvature: float
steering_pressed: bool = False
steering_angle: float = 0.0
yaw_predictions: tuple = ()
time_indices: tuple = ()
lateral_delay: float = 0.15
lane_change: bool = False
lane_direction: int = 0
limit_status: int = 0
geometry: LaneGeometry | None = None
@dataclass(frozen=True)
class Command:
active: bool = False
path_angle: float = 0.0
shadow_curvature: float = 0.0
precision: int = 1
class AngleController:
def __init__(self, *, low_gain=1.0, high_gain=1.15, trim_config=None):
self.trim_config = TrimConfig() if trim_config is None else trim_config
self.trim = LaneCenterTrim()
self.low_gain = low_gain
self.high_gain = high_gain
self.path_last = 0.0
self.speed_last = 0.1
self.desired_last = 0.0
self.human_timer = 0.0
self.press_last = False
self.preturned = False
self.press_timer = 0.0
self.blip_frames = 0
self.blip_cooldown = 0.0
self.blip_hold = 0.0
self.blip_count = 0
def reset(self):
self.trim.reset()
self.path_last = 0.0
self.speed_last = 0.1
self.desired_last = 0.0
self.human_timer = 0.0
self.press_last = False
self.preturned = False
self.press_timer = 0.0
self.blip_frames = 0
self.blip_cooldown = 0.0
self.blip_hold = 0.0
self.blip_count = 0
def step(self, i: Inputs) -> Command:
values = (i.speed, i.measured_curvature, i.desired_curvature, i.steering_angle, i.lateral_delay)
if not i.active or not all(math.isfinite(v) for v in values) or i.speed < 0.0:
self.reset()
return Command()
if i.steering_pressed and not self.press_last:
self.preturned = abs(i.steering_angle) > 45.0
self.press_last = i.steering_pressed
self.human_timer = self.human_timer + 0.05 if i.steering_pressed and abs(i.steering_angle) > 45.0 else 0.0
if self.human_timer + 1e-9 >= (3.0 if self.preturned else 1.5):
self.trim.reset()
self.path_last = 0.0
self.desired_last = i.desired_curvature
self.blip_frames = self.blip_count = 0
self.blip_hold = self.blip_cooldown = self.press_timer = 0.0
return Command()
if i.steering_pressed:
self.press_timer += 0.05
else:
if self.press_timer >= 0.5 and self.blip_cooldown <= 0.0 and self.blip_frames <= 0 and abs(self.path_last) < 0.1:
self.blip_frames = 6
self.press_timer = 0.0
if self.blip_frames > 0:
self.blip_frames -= 1
self.trim.reset()
self.path_last = 0.0
self.desired_last = i.desired_curvature
if self.blip_frames == 0:
self.blip_cooldown = 2.0
return Command()
base = clip(i.lateral_delay, 0.1, 0.15) + 0.05
predictions_valid = (len(i.yaw_predictions) == len(i.time_indices) and len(i.time_indices) >= 17 and
all(math.isfinite(v) for v in (*i.yaw_predictions, *i.time_indices)) and
all(b > a for a, b in zip(i.time_indices, i.time_indices[1:], strict=False)))
curves = tuple(y / max(0.01, i.speed) for y in i.yaw_predictions) if predictions_valid else ()
entering = bool(curves) and abs(interp(base, i.time_indices, curves)) > abs(i.desired_curvature)
factor = 1.0 if entering else interp(abs(i.desired_curvature), (0.005, 0.020), (1.0, 0.0))
lookup = base + bounds.VLT_EXTRA * interp(i.speed, (11.176, 24.5872), (1.0, 0.0)) * factor
predicted = interp(lookup, i.time_indices, curves) if curves else i.desired_curvature
saturated = i.limit_status >= 2 or self.path_last >= 0.5235 * 0.9 or self.path_last <= -0.5 * 0.9
falling = abs(i.desired_curvature) < abs(self.desired_last) - 0.010
blend = bounds.BLEND * 0.25 if not entering and (i.limit_status >= 1 or saturated or falling) else bounds.BLEND
requested = predicted * blend + i.desired_curvature * (1.0 - blend)
self.desired_last = i.desired_curvature
precision = 0 if i.lane_change and ((i.lane_direction == 1 and requested < 0) or
(i.lane_direction == 2 and requested > 0)) else 1
# BluePilot's default lane-change factors are both 1.0.
# Trim requested curvature before every existing actuator/safety limiter.
requested = self.trim.update(requested, i.speed, i.geometry, self.trim_config,
active=i.active, lane_change=i.lane_change, steering_pressed=i.steering_pressed)
kappa = clip(requested, i.measured_curvature - 0.002, i.measured_curvature + 0.002) if i.speed > 9.0 else requested
low = interp(i.speed, (13.5, 26.82), (1.0, self.low_gain))
high = interp(i.speed, (13.5, 26.82), (1.3, self.high_gain))
gain = interp(abs(kappa), (0.0007, 0.001), (low, high))
path = kappa * i.speed * gain
if saturated:
last_mag, magnitude = abs(self.path_last), abs(path)
if magnitude > last_mag:
path = self.path_last
elif last_mag - magnitude > 0.02:
path = math.copysign(last_mag - 0.02, self.path_last)
elif i.limit_status >= 1:
path = clip(path, -abs(self.path_last), abs(self.path_last))
roc = interp(i.speed, (10.0, 15.0, 25.0), (0.055, 0.0425, 0.009))
path = clip(clip(path, -0.5235, 0.5), self.path_last - roc, self.path_last + roc)
# Tightening relative to bp-7.0: the shadow describes the final actuator,
# including ROC/saturation. A truthful derived curvature must fit both the
# measured-curvature interval and the modern ISO envelope. If those bands
# cannot meet the previous actuator ROC, send a neutral mode-0 frame.
effective_speed = max(i.speed, 0.1)
cap = min(bounds.CURVATURE_MAX, bounds.LATERAL_ACCEL / max(i.speed, 1.0) ** 2)
low_kappa, high_kappa = -cap, cap
if i.speed > 9.0:
low_kappa = max(low_kappa, i.measured_curvature - 0.0018)
high_kappa = min(high_kappa, i.measured_curvature + 0.0018)
# Native cannot trust a host-selected feel gain. Intersect every admitted
# gain (0.95..1.3), so the worst equivalent curvature remains bounded.
lower_gain = bounds.GAIN_MAX if low_kappa >= 0.0 else bounds.GAIN_MIN
upper_gain = bounds.GAIN_MIN if high_kappa >= 0.0 else bounds.GAIN_MAX
lower = max(-bounds.PATH_MAX, low_kappa * effective_speed * lower_gain, self.path_last - roc)
upper = min(-bounds.PATH_MIN, high_kappa * effective_speed * upper_gain, self.path_last + roc)
# Bound the actual-angle equivalent derivative at the worst admitted gain.
# 45ms core margin remains inside the native elapsed-time/50ms cap.
jerk_delta = bounds.LATERAL_JERK / max(i.speed - 1.0, 1.0) ** 2 * bounds.CORE_STEER_PERIOD_S * bounds.GAIN_MIN * effective_speed
jerk_center = self.path_last / self.speed_last * effective_speed
lower = max(lower, jerk_center - jerk_delta)
upper = min(upper, jerk_center + jerk_delta)
lower += bounds.PATH_ROUNDING_MARGIN
upper -= bounds.PATH_ROUNDING_MARGIN
if lower > upper:
self.trim.reset()
self.path_last = 0.0
return Command()
path = clip(path, lower, upper)
self.path_last = path
self.speed_last = effective_speed
shadow = path / (effective_speed * gain)
self.blip_cooldown = max(0.0, self.blip_cooldown - 0.05)
gap = i.desired_curvature - i.measured_curvature
stalled = (not i.steering_pressed and not i.lane_change and i.speed > 9.0 and abs(gap) > 0.004 and
abs(i.desired_curvature) > abs(i.measured_curvature))
if stalled:
if abs(kappa - requested) > 1e-9 and self.blip_cooldown <= 0.0:
self.blip_hold += 0.05
if self.blip_hold >= 0.5 and self.blip_count < 3 and abs(path) < 0.1:
self.blip_frames = 6
self.blip_hold = 0.0
self.blip_count += 1
else:
self.blip_hold = 0.0
if i.steering_pressed or abs(gap) < 0.002:
self.blip_count = 0
return Command(True, path, shadow, precision)
@@ -1,104 +0,0 @@
"""Pure angle lane-centering trim adapted from BluePilot bp-dev e22afa6b.
Alan Polk and ghbarker; the source also credits StarPilot/u/jc01rho.
See ../CREDITS.md, ../LICENSE and ../LICENSE.md. Geometry/confidence/fallback,
filter and slew follow lane_center_trim.py; this module receives immutable arrays
instead of a host model. Stale geometry and driver pressure additionally suppress
trim, and nonfinite confidence cannot create correction.
"""
from dataclasses import dataclass
import math
from ..params.trim import DEFAULTS, BOUNDS
def clip(value, low, high):
return max(low, min(high, value))
def interp(value, xs, ys):
if value <= xs[0]:
return ys[0]
for index in range(1, len(xs)):
if value < xs[index]:
return ys[index - 1] + (ys[index] - ys[index - 1]) * (value - xs[index - 1]) / (xs[index] - xs[index - 1])
return ys[-1]
@dataclass(frozen=True)
class TrimConfig:
enabled: bool = DEFAULTS["enabled"]
offset_m: float = DEFAULTS["offset_m"]
gain: float = DEFAULTS["gain"]
@dataclass(frozen=True)
class LaneGeometry:
position_x: tuple = ()
position_y: tuple = ()
left_x: tuple = ()
left_y: tuple = ()
right_x: tuple = ()
right_y: tuple = ()
probabilities: tuple = ()
standard_deviations: tuple = ()
fresh: bool = False
def sample(xs, ys, distance):
if (not isinstance(xs, tuple) or not isinstance(ys, tuple) or len(xs) < 2 or len(xs) != len(ys) or
not all(math.isfinite(v) for v in (*xs, *ys)) or not all(b > a for a, b in zip(xs, xs[1:], strict=False))):
return None
return interp(distance, xs, ys)
class LaneCenterTrim:
def __init__(self):
self.correction = 0.0
def reset(self):
self.correction = 0.0
def update(self, requested, speed, geometry, config, *, active, lane_change, steering_pressed=False):
try:
valid = (isinstance(config, TrimConfig) and config.enabled is True and active and not lane_change and
not steering_pressed and isinstance(geometry, LaneGeometry) and geometry.fresh is True and
math.isfinite(requested) and math.isfinite(speed) and speed >= 0.0 and
math.isfinite(config.offset_m) and BOUNDS["offset_m"][0] <= config.offset_m <= BOUNDS["offset_m"][1] and
math.isfinite(config.gain) and BOUNDS["gain"][0] <= config.gain <= BOUNDS["gain"][1])
authority = interp(speed, BOUNDS["speed_mps"], BOUNDS["speed_authority"]) if valid else 0.0
if not valid or authority <= 0.0:
self.reset()
return requested
distance = clip(speed, *BOUNDS["lookahead_m"])
model_y = sample(geometry.position_x, geometry.position_y, distance)
if model_y is None:
self.reset()
return requested
# Bad lane-only geometry retains the source's model-position/offset fallback.
scale, center = self.lane_blend(geometry, distance)
error = scale * (center - model_y) + config.offset_m
raw = 2.0 * error / distance ** 2
target = clip(raw, -BOUNDS["raw_correction"], BOUNDS["raw_correction"]) * config.gain * authority
alpha = 1.0 - math.exp(-BOUNDS["period_s"] / BOUNDS["tau_s"])
filtered = alpha * target + (1.0 - alpha) * self.correction
self.correction = clip(filtered, self.correction - BOUNDS["correction_delta"], self.correction + BOUNDS["correction_delta"])
return requested + self.correction
except (AttributeError, IndexError, TypeError, ValueError, OverflowError):
self.reset()
return requested
def lane_blend(self, geometry, distance):
try:
left = sample(geometry.left_x, geometry.left_y, distance)
right = sample(geometry.right_x, geometry.right_y, distance)
probs, stds = geometry.probabilities, geometry.standard_deviations
if (left is None or right is None or not isinstance(probs, tuple) or not isinstance(stds, tuple) or
len(probs) != 2 or len(stds) != 2 or not all(math.isfinite(p) and 0.0 <= p <= 1.0 for p in probs) or
not all(math.isfinite(s) and s >= 0.0 for s in stds)):
return 0.0, 0.0
confidence = min(*probs, interp(right - left, BOUNDS["width_m"], BOUNDS["width_tolerance"]),
interp(max(stds), BOUNDS["std"], BOUNDS["std_tolerance"]))
scale = clip(interp(confidence, BOUNDS["confidence"], (0.0, 1.0)), 0.0, 1.0)
return scale, 0.5 * (left + right)
except (AttributeError, IndexError, TypeError, ValueError, OverflowError):
return 0.0, 0.0
@@ -1,101 +0,0 @@
"""StarPilot's CP adapter; model transport and saved Params stay in the host.
Only exact final CP words derived from the existing five classic/six CANFD
extended profiles can select angle. AOL and Transit keep their current owner.
"""
from opendbc.car import structs
from opendbc.car.ford.values import CAR, FordFlags
from opendbc.bluepilot_lateral.core.angle import AngleController, Inputs
from opendbc.bluepilot_lateral.core.trim import LaneGeometry
from opendbc.bluepilot_lateral.params import limits as bounds
ANGLE_FLAG = 128
CLASSIC = frozenset({CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_ESCAPE_MK4, CAR.FORD_FOCUS_MK4,
CAR.FORD_MAVERICK_MK1, CAR.FORD_EXPLORER_MK6})
CANFD = frozenset({CAR.FORD_ESCAPE_MK4_5, CAR.FORD_EXPEDITION_MK4, CAR.FORD_F_150_MK14,
CAR.FORD_F_150_LIGHTNING_MK1, CAR.FORD_RANGER_MK2, CAR.FORD_MUSTANG_MACH_E_MK1})
TRUCKS = CANFD - {CAR.FORD_ESCAPE_MK4_5, CAR.FORD_MUSTANG_MACH_E_MK1}
def mode_word(cp, *, selected):
if (cp.brand != "ford" or cp.passive or cp.dashcamOnly or cp.notCar or cp.alternativeExperience != 0 or
cp.steerControlType != structs.CarParams.SteerControlType.angle or
not cp.pcmCruise or len(cp.safetyConfigs) not in (1, 2)):
return None
if len(cp.safetyConfigs) == 2 and (cp.safetyConfigs[0].safetyModel != structs.CarParams.SafetyModel.noOutput or
cp.safetyConfigs[0].safetyParam != 0):
return None
fd = cp.carFingerprint in CANFD
if ((fd and (not cp.flags & FordFlags.CANFD or cp.flags & ~int(FordFlags.CANFD | FordFlags.HAS_BSM))) or
(not fd and (cp.carFingerprint not in CLASSIC or cp.flags & ~int(FordFlags.HAS_BSM)))):
return None
safety = cp.safetyConfigs[-1]
long = int(cp.openpilotLongitudinalControl)
original = (18 if cp.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 else 66) + long if fd else 32 + long
expected = ANGLE_FLAG + (2 if fd else 0) + long if selected else original
return expected if safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected else None
def supported(cp):
return mode_word(cp, selected=False) is not None or mode_word(cp, selected=True) is not None
def qualified(cp):
return mode_word(cp, selected=True) is not None
def select(cp, raw):
# Absence/invalid bytes stay at current curvature. Never rewrite saved bytes.
chosen = (type(raw) is int and raw == 1) or (type(raw) in (bytes, str) and raw in (b"1", "1"))
if not chosen or mode_word(cp, selected=False) is None:
return False
cp.safetyConfigs[-1].safetyParam = ANGLE_FLAG + (2 if cp.flags & FordFlags.CANFD else 0) + int(cp.openpilotLongitudinalControl)
cp.steerActuatorDelay = 0.22
return True
def model_geometry(model, *, fresh=False):
"""Project a caller-qualified snapshot to immutable core arrays; no model imports."""
if model is None or fresh is not True:
return None
try:
position_x, position_y = tuple(model.position.x), tuple(model.position.y)
except (AttributeError, TypeError, ValueError):
return None
try:
return LaneGeometry(position_x=position_x, position_y=position_y, fresh=True, left_x=tuple(model.laneLines[1].x), left_y=tuple(model.laneLines[1].y),
right_x=tuple(model.laneLines[2].x), right_y=tuple(model.laneLines[2].y),
probabilities=(model.laneLineProbs[1], model.laneLineProbs[2]),
standard_deviations=(model.laneLineStds[1], model.laneLineStds[2]))
except (AttributeError, IndexError, TypeError, ValueError):
return LaneGeometry(position_x=position_x, position_y=position_y, fresh=True)
class Host:
def __init__(self, cp):
if not qualified(cp):
raise ValueError("BluePilot angle requires an exact admitted Ford safety word")
low, high = (0.95, 0.95) if cp.carFingerprint in TRUCKS else ((1.0, 1.05) if cp.carFingerprint in CANFD else (1.0, 1.15))
self.cp = cp
self.controller = AngleController(low_gain=low, high_gain=high)
self.inputs = None
def update(self, CC, CS):
if self.inputs is not None:
self.inputs.update()
snapshot = self.inputs.lateral_snapshot(CS.out.vEgoRaw) if self.inputs is not None else None
model, times, delay, _ = snapshot if snapshot is not None else (None, (), 0.15, True)
ready = (self.inputs is not None and self.inputs.native_permission(self.cp, max_age_ns=bounds.PANDA_AGE_NS) and
CC.enabled and CC.latActive and CS.out.cruiseState.enabled and CS.out.canValid and
not CS.out.canTimeout and not CS.out.gasPressed and not CS.out.brakePressed and
not CS.out.steerFaultTemporary and not CS.out.steerFaultPermanent and not CS.out.vehicleSensorsInvalid)
meta = model.meta if model is not None else None
return self.controller.step(Inputs(
active=ready, speed=CS.out.vEgoRaw, measured_curvature=-CS.out.yawRate / max(CS.out.vEgoRaw, 0.1),
desired_curvature=CC.actuators.curvature, steering_pressed=CS.out.steeringPressed,
steering_angle=CS.out.steeringAngleDeg, yaw_predictions=tuple(model.orientationRate.z) if model is not None else (),
time_indices=tuple(times), lateral_delay=delay,
lane_change=int(getattr(meta.laneChangeState, "raw", meta.laneChangeState)) in (1, 2, 3) if meta else False,
lane_direction=int(getattr(meta.laneChangeDirection, "raw", meta.laneChangeDirection)) if meta else 0,
limit_status=getattr(CS, "lat_ctl_lim_stat", 0),
geometry=model_geometry(model, fresh=snapshot is not None)))
@@ -1,41 +0,0 @@
"""Generate protocol constants without loading a host or vehicle module."""
import json
from pathlib import Path
root = Path(__file__).parent
spec = json.loads((root / "protocol.json").read_text())
(root / "protocol.py").write_text('"""Generated from protocol.json; layout preserves the existing curvature flag."""\n' +
"".join(f"{key.upper()} = {value!r}\n" for key, value in spec.items()))
(root / "protocol.h").write_text("#pragma once\n// Generated from protocol.json; do not allocate bit 1 to angle metadata.\n" +
"".join(f"#define FORD_BP_{key.upper()} {int(value)}U\n" for key, value in spec.items() if key != "shadow_scale"))
manifest = json.loads((root / "manifest.json").read_text())
defaults, bounds = manifest["defaults"], manifest["bounds"]
limits = {"BLEND": defaults["blend"], "VLT_EXTRA": defaults["vlt_extra_max"],
"PATH_MIN": bounds["path_angle_wire"][0], "PATH_MAX": bounds["path_angle_wire"][1],
"GAIN_MIN": bounds["gain"][0], "GAIN_MAX": bounds["gain"][1],
"CURVATURE_MAX": bounds["curvature"], "CURVATURE_ERROR": bounds["curvature_error"],
"LATERAL_ACCEL": bounds["lateral_accel"], "RX_AGE_US": bounds["rx_age_us"],
"SHADOW_AGE_US": bounds["shadow_age_us"], "PATH_ROUNDING_MARGIN": bounds["path_rounding_margin"],
"SHADOW_ROUNDING_MARGIN": bounds["shadow_rounding_margin"], "LATERAL_JERK": bounds["lateral_jerk"],
"RT_INTERVAL_US": bounds["rt_interval_us"], "STEER_PERIOD_US": bounds["steer_period_us"],
"CORE_STEER_PERIOD_S": bounds["core_steer_period_s"], "PANDA_AGE_NS": bounds["panda_age_ns"]}
(root / "limits.py").write_text('"""Generated from manifest.json; host and core use this manifest contract."""\n' +
"".join(f"{key} = {value!r}\n" for key, value in limits.items()))
(root / "limits.h").write_text("#pragma once\n// Generated from manifest.json; native and Python bounds share a source.\n" +
"".join(f"#define FORD_BP_{key} {str(value) + 'F' if isinstance(value, float) else str(value) + 'U'}\n"
for key, value in limits.items()))
# Optional angle trim settings are Python-only; native safety limits stay unchanged.
trim = manifest["angle_lane_trim"]
types = {"enabled": "bool", "offset_m": "float", "gain": "float"}
defaults_fields = "".join(f" {key}: {types[key]}\n" for key in trim["defaults"])
bounds_fields = "".join(f" {key}: {'list[float]' if isinstance(value, list) else 'float'}\n" for key, value in trim["bounds"].items())
(root / "trim.py").write_text('"""Generated Python-only angle trim defaults; no native bound or registry changes."""\n' +
"from typing import TypedDict\n\n\n" +
"class TrimDefaults(TypedDict):\n" + defaults_fields + "\n\n" +
"class TrimBounds(TypedDict):\n" + bounds_fields + "\n\n" +
f"SOURCE_REVISION = {trim['source_revision']!r}\n" +
f"DEFAULTS: TrimDefaults = {trim['defaults']!r}\n" +
"BOUNDS: TrimBounds = {\n" +
"".join(f" {key!r}: {value!r},\n" for key, value in trim["bounds"].items()) + "}\n")
@@ -1,20 +0,0 @@
#pragma once
// Generated from manifest.json; native and Python bounds share a source.
#define FORD_BP_BLEND 0.5F
#define FORD_BP_VLT_EXTRA 0.1F
#define FORD_BP_PATH_MIN -0.5F
#define FORD_BP_PATH_MAX 0.5235F
#define FORD_BP_GAIN_MIN 0.95F
#define FORD_BP_GAIN_MAX 1.3F
#define FORD_BP_CURVATURE_MAX 0.02F
#define FORD_BP_CURVATURE_ERROR 0.002F
#define FORD_BP_LATERAL_ACCEL 2.4114F
#define FORD_BP_RX_AGE_US 100000U
#define FORD_BP_SHADOW_AGE_US 60000U
#define FORD_BP_PATH_ROUNDING_MARGIN 0.0005F
#define FORD_BP_SHADOW_ROUNDING_MARGIN 1e-06F
#define FORD_BP_LATERAL_JERK 3.5886F
#define FORD_BP_RT_INTERVAL_US 250000U
#define FORD_BP_STEER_PERIOD_US 50000U
#define FORD_BP_CORE_STEER_PERIOD_S 0.045F
#define FORD_BP_PANDA_AGE_NS 300000000U
@@ -1,19 +0,0 @@
"""Generated from manifest.json; host and core use this manifest contract."""
BLEND = 0.5
VLT_EXTRA = 0.1
PATH_MIN = -0.5
PATH_MAX = 0.5235
GAIN_MIN = 0.95
GAIN_MAX = 1.3
CURVATURE_MAX = 0.02
CURVATURE_ERROR = 0.002
LATERAL_ACCEL = 2.4114
RX_AGE_US = 100000
SHADOW_AGE_US = 60000
PATH_ROUNDING_MARGIN = 0.0005
SHADOW_ROUNDING_MARGIN = 1e-06
LATERAL_JERK = 3.5886
RT_INTERVAL_US = 250000
STEER_PERIOD_US = 50000
CORE_STEER_PERIOD_S = 0.045
PANDA_AGE_NS = 300000000
@@ -1,109 +0,0 @@
{
"source_revision": "e1d051d7ba270261b4455068bd68f1a58db15a4a",
"settings": [
{
"key": "FordLateralMode",
"type": "enum",
"default": 0,
"values": {
"0": "Current curvature",
"1": "BluePilot angle"
},
"label": "Ford steering controller",
"description": "Select the steering controller for supported Ford profiles. Takes effect on the next drive.",
"modes": [
"angle"
]
}
],
"defaults": {
"blend": 0.5,
"vlt_extra_max": 0.1,
"low_speed_factor": 1.0,
"high_speed_factor": 1.0,
"dampening": 1.0,
"lane_change_factor": 1.0
},
"bounds": {
"path_angle_wire": [
-0.5,
0.5235
],
"gain": [
0.95,
1.3
],
"curvature_error": 0.002,
"lateral_accel": 2.4114,
"rx_age_us": 100000,
"shadow_age_us": 60000,
"steer_period_us": 50000,
"curvature": 0.02,
"path_rounding_margin": 0.0005,
"shadow_rounding_margin": 1e-06,
"lateral_jerk": 3.5886,
"rt_interval_us": 250000,
"core_steer_period_s": 0.045,
"panda_age_ns": 300000000
},
"angle_lane_trim": {
"source_revision": "e22afa6be9b881fa784c92ebb316db47728a3d81",
"defaults": {
"enabled": false,
"offset_m": 0.0,
"gain": 0.25
},
"bounds": {
"offset_m": [
-0.5,
0.5
],
"gain": [
0.0,
1.0
],
"width_m": [
2.4,
2.8,
3.75,
4.25
],
"width_tolerance": [
0.0,
0.81,
0.81,
0.59
],
"std": [
0.3,
0.5
],
"std_tolerance": [
0.81,
0.0
],
"confidence": [
0.6,
0.8
],
"speed_mps": [
0.0,
9.0,
15.0
],
"speed_authority": [
0.0,
0.0,
1.0
],
"lookahead_m": [
8.0,
35.0
],
"raw_correction": 0.004,
"tau_s": 0.4,
"period_s": 0.05,
"correction_delta": 0.00015
}
}
}
@@ -1,10 +0,0 @@
#pragma once
// Generated from protocol.json; do not allocate bit 1 to angle metadata.
#define FORD_BP_VERSION 1U
#define FORD_BP_ANGLE_MASK 1U
#define FORD_BP_EXTENDED_CURVATURE_MASK 2U
#define FORD_BP_COUNTER_SHIFT 2U
#define FORD_BP_COUNTER_MASK 28U
#define FORD_BP_COUNTER_MODULUS 8U
#define FORD_BP_VERSION_MASK 15U
#define FORD_BP_CHECKSUM_SHIFT 4U
@@ -1,11 +0,0 @@
{
"version": 1,
"angle_mask": 1,
"extended_curvature_mask": 2,
"counter_shift": 2,
"counter_mask": 28,
"counter_modulus": 8,
"shadow_scale": 1e-06,
"version_mask": 15,
"checksum_shift": 4
}
@@ -1,10 +0,0 @@
"""Generated from protocol.json; layout preserves the existing curvature flag."""
VERSION = 1
ANGLE_MASK = 1
EXTENDED_CURVATURE_MASK = 2
COUNTER_SHIFT = 2
COUNTER_MASK = 28
COUNTER_MODULUS = 8
SHADOW_SCALE = 1e-06
VERSION_MASK = 15
CHECKSUM_SHIFT = 4
@@ -1,45 +0,0 @@
"""Generated Python-only angle trim defaults; no native bound or registry changes."""
from typing import TypedDict
class TrimDefaults(TypedDict):
enabled: bool
offset_m: float
gain: float
class TrimBounds(TypedDict):
offset_m: list[float]
gain: list[float]
width_m: list[float]
width_tolerance: list[float]
std: list[float]
std_tolerance: list[float]
confidence: list[float]
speed_mps: list[float]
speed_authority: list[float]
lookahead_m: list[float]
raw_correction: float
tau_s: float
period_s: float
correction_delta: float
SOURCE_REVISION = 'e22afa6be9b881fa784c92ebb316db47728a3d81'
DEFAULTS: TrimDefaults = {'enabled': False, 'offset_m': 0.0, 'gain': 0.25}
BOUNDS: TrimBounds = {
'offset_m': [-0.5, 0.5],
'gain': [0.0, 1.0],
'width_m': [2.4, 2.8, 3.75, 4.25],
'width_tolerance': [0.0, 0.81, 0.81, 0.59],
'std': [0.3, 0.5],
'std_tolerance': [0.81, 0.0],
'confidence': [0.6, 0.8],
'speed_mps': [0.0, 9.0, 15.0],
'speed_authority': [0.0, 0.0, 1.0],
'lookahead_m': [8.0, 35.0],
'raw_correction': 0.004,
'tau_s': 0.4,
'period_s': 0.05,
'correction_delta': 0.00015,
}
@@ -1,236 +0,0 @@
#pragma once
// Angle-primary Ford extension adapted from Alan Polk's bp-7.0 e1d051d7.
// See ../CREDITS.md, ../LICENSE and ../LICENSE.md. No reset bypass.
#include "opendbc/bluepilot_lateral/params/protocol.h"
#include "opendbc/bluepilot_lateral/params/limits.h"
// Shared Ford stock-switch state is declared before the angle hooks below.
static bool ford_stock_switch = false;
static bool ford_bp_enabled = false;
static bool ford_bp_canfd = false;
static bool ford_bp_shadow_seen = false;
static bool ford_bp_lka_counter_seen = false;
static uint8_t ford_bp_lka_counter = 0U;
static int ford_bp_shadow_raw = 0;
static uint32_t ford_bp_shadow_us = 0U;
static int ford_bp_path_last = 0;
static bool ford_bp_control_counter_seen = false;
static uint8_t ford_bp_control_counter = 0U;
static bool ford_bp_control_seen = false;
static uint32_t ford_bp_control_us = 0U;
static float ford_bp_equivalent_last = 0.0F;
static unsigned int ford_bp_rt_msgs = 0U;
static unsigned int ford_bp_rt_msgs_prev = 0U;
static uint32_t ford_bp_rt_us = 0U;
static bool ford_bp_rx_seen[7];
static uint32_t ford_bp_rx_us[7];
static void ford_bp_invalidate_shadow(void) {
ford_bp_shadow_seen = false;
ford_bp_shadow_raw = 0;
}
static void ford_bp_withdraw(void) {
ford_bp_invalidate_shadow();
ford_bp_path_last = 0;
ford_bp_equivalent_last = 0.0F;
}
static bool ford_bp_configure(uint16_t param) {
ford_bp_enabled = ((param == 128U) || (param == 129U) || (param == 130U)) && (alternative_experience == 0);
#ifdef ALLOW_DEBUG
ford_bp_enabled |= (param == 131U) && (alternative_experience == 0);
#endif
ford_bp_canfd = (param & 2U) != 0U;
ford_bp_lka_counter_seen = false;
ford_bp_control_counter_seen = false;
ford_bp_control_seen = false;
ford_bp_lka_counter = 0U;
ford_bp_control_counter = 0U;
ford_bp_shadow_us = 0U;
ford_bp_control_us = 0U;
ford_bp_rt_msgs = 0U;
ford_bp_rt_msgs_prev = 0U;
ford_bp_rt_us = microsecond_timer_get();
ford_bp_withdraw();
for (unsigned int i = 0U; i < 7U; i++) {
ford_bp_rx_seen[i] = false;
ford_bp_rx_us[i] = 0U;
}
return ((param & 128U) == 0U) || ford_bp_enabled;
}
static bool ford_bp_health(uint32_t now);
static void ford_bp_rx(const CANPacket_t *msg) {
if (ford_bp_enabled && (msg->bus == 0U) && (GET_LEN(msg) == 8U)) {
const unsigned int ids[7] = {0x415U, 0x202U, 0x91U, 0x165U, 0x204U, 0x213U, 0x83U};
for (unsigned int i = 0U; i < 7U; i++) {
if (msg->addr == ids[i]) {
ford_bp_rx_seen[i] = true;
ford_bp_rx_us[i] = microsecond_timer_get();
}
}
// Angle-specific source freshness is stricter than the generic1s RX tick.
// Expose that withdrawal through the ordinary public controls acknowledgment.
if (controls_allowed && !ford_bp_health(microsecond_timer_get())) {
controls_allowed = false;
}
// Permission losses remove the old actuator history before any later arm.
if (!controls_allowed || brake_pressed || gas_pressed) {
ford_bp_withdraw();
}
}
}
static bool ford_bp_health(uint32_t now) {
bool valid = !safety_rx_checks_invalid && !relay_malfunction;
for (int i = 0; i < current_safety_config.rx_checks_len; i++) {
const RxStatus *status = &current_safety_config.rx_checks[i].status;
valid &= status->msg_seen && !status->lagging && status->valid_checksum &&
status->valid_quality_flag && (status->wrong_counters < MAX_WRONG_COUNTERS);
}
const unsigned int count = ford_stock_switch ? 7U : 6U;
for (unsigned int i = 0U; i < count; i++) {
const uint32_t age = ((i == 3U) || (i == 6U)) ? (2U * FORD_BP_RX_AGE_US) : FORD_BP_RX_AGE_US;
valid &= ford_bp_rx_seen[i] && (safety_get_ts_elapsed(now, ford_bp_rx_us[i]) <= age);
}
return valid;
}
static bool ford_bp_lka(const CANPacket_t *msg) {
const uint32_t now = microsecond_timer_get();
const unsigned int counter = (msg->data[4] & FORD_BP_COUNTER_MASK) >> FORD_BP_COUNTER_SHIFT;
unsigned int sum = 0U;
for (unsigned int i = 0U; i < 7U; i++) { sum += msg->data[i]; }
const unsigned int checksum = (15U - (sum & 15U)) & 15U;
// Preserve every DBC-owned bit of the packer's neutral LKA body.
const unsigned int angle = ((msg->data[2] & 0xFU) << 8) | msg->data[3];
const unsigned int curvature = (msg->data[1] << 4) | (msg->data[2] >> 4);
bool valid = !relay_malfunction && (msg->bus == 0U) && (GET_LEN(msg) == 8U) && (msg->data[0] == 0U) &&
(angle == 2048U) && (curvature == 2048U) &&
((msg->data[4] & 0xE3U) == FORD_BP_ANGLE_MASK) &&
((msg->data[7] & FORD_BP_VERSION_MASK) == FORD_BP_VERSION) &&
((msg->data[7] >> FORD_BP_CHECKSUM_SHIFT) == checksum);
const int shadow = to_signed((msg->data[5] << 8) | msg->data[6], 16);
const bool resync = (shadow == 0) && !controls_allowed;
if (ford_bp_lka_counter_seen && !resync) {
valid &= counter == ((ford_bp_lka_counter + 1U) % FORD_BP_COUNTER_MODULUS);
valid &= safety_get_ts_elapsed(now, ford_bp_shadow_us) >= 20000U;
}
valid &= SAFETY_ABS(shadow) <= 20000;
if (valid) {
ford_bp_lka_counter = (uint8_t)counter;
ford_bp_lka_counter_seen = true;
ford_bp_shadow_raw = shadow;
ford_bp_shadow_seen = !resync;
ford_bp_shadow_us = now;
} else {
ford_bp_invalidate_shadow();
}
return valid;
}
static bool ford_bp_lateral(const CANPacket_t *msg) {
const bool fd = msg->addr == 0x3D6U;
const unsigned int mode = fd ? ((msg->data[0] >> 4) & 7U) : ((msg->data[4] >> 2) & 7U);
const unsigned int curvature = fd ? ((msg->data[2] << 3) | (msg->data[3] >> 5)) :
((msg->data[0] << 3) | (msg->data[1] >> 5));
const unsigned int rate = fd ? (((unsigned int)msg->data[6] << 3) | (msg->data[7] >> 5)) :
(((msg->data[1] & 31U) << 8) | msg->data[2]);
const unsigned int path_raw = fd ? (((msg->data[3] & 31U) << 6) | (msg->data[4] >> 2)) :
(((unsigned int)msg->data[3] << 3) | (msg->data[4] >> 5));
const unsigned int offset = fd ? (((msg->data[4] & 3U) << 8) | msg->data[5]) :
(((unsigned int)msg->data[5] << 2) | (msg->data[6] >> 6));
const unsigned int precision = fd ? ((msg->data[0] >> 2) & 3U) : (msg->data[4] & 3U);
const unsigned int ramp = fd ? (msg->data[0] & 3U) : ((msg->data[6] >> 4) & 3U);
const bool active = mode == 1U;
const int path = (int)path_raw - 1000;
const uint32_t now = microsecond_timer_get();
bool valid = !relay_malfunction && (msg->bus == 0U) && (GET_LEN(msg) == 8U) && (fd == ford_bp_canfd) && (mode <= 1U) &&
(curvature == 1000U) && (rate == (fd ? 1024U : 4096U)) && (offset == 512U) &&
(precision <= 1U) && (ramp == (active ? 2U : 0U));
const unsigned int counter = (msg->data[7] >> 1) & 15U;
if (fd) {
unsigned int checksum = mode + counter;
const unsigned int fields[4] = {curvature, rate, path_raw, offset};
for (unsigned int i = 0U; i < 4U; i++) { checksum += fields[i] + (fields[i] >> 8); }
valid &= msg->data[1] == (0xFFU - (checksum & 0xFFU));
valid &= ((msg->data[0] & 0x80U) == 0U) && ((msg->data[7] & 1U) == 0U);
if (ford_bp_control_counter_seen && active) { valid &= counter == ((ford_bp_control_counter + 1U) % 16U); }
} else {
valid &= ((msg->data[6] & 0xFU) == 0U) && (msg->data[7] == 0U);
}
const float speed_for_equivalent = SAFETY_MAX(vehicle_speed.min / VEHICLE_SPEED_FACTOR, 0.1F);
const float equivalent = (float)path / (2000.0F * speed_for_equivalent);
// Time-window expiration is independent of packet acceptance: denied floods
// cannot latch the old count forever or erase accepted actuator history.
const uint32_t rt_elapsed = safety_get_ts_elapsed(now, ford_bp_rt_us);
if (rt_elapsed >= FORD_BP_RT_INTERVAL_US) {
ford_bp_rt_msgs = 0U;
ford_bp_rt_msgs_prev = 0U;
ford_bp_rt_us = now;
} else if (rt_elapsed >= (FORD_BP_RT_INTERVAL_US / 2U)) {
ford_bp_rt_msgs_prev = ford_bp_rt_msgs;
ford_bp_rt_msgs = 0U;
ford_bp_rt_us = now;
} else {
// Keep the current history while neither expiration boundary has elapsed.
}
const unsigned int rt_count = ford_bp_rt_msgs + ford_bp_rt_msgs_prev;
valid &= rt_count <= 7U; // Current20Hz curvature RT allowance; accepted frames own this count.
if (active) {
static const struct lookup_t roc = {{10.0F, 15.0F, 25.0F}, {0.0561F, 0.04335F, 0.00918F}};
const float speed_min = SAFETY_MAX(vehicle_speed.min / VEHICLE_SPEED_FACTOR, 0.1F);
const float speed_max = SAFETY_MAX(vehicle_speed.max / VEHICLE_SPEED_FACTOR, 0.1F);
const float delta_float = safety_interpolate(roc, speed_min - 1.0F) * 2000.0F;
const int delta = (int)delta_float + 1;
const float shadow = (float)ford_bp_shadow_raw * 0.000001F;
const float shadow_can = shadow * 50000.0F;
const float path_abs = SAFETY_ABS(path) / 2000.0F;
const float path_curvature_low = path_abs / (speed_max * FORD_BP_GAIN_MAX);
const float path_curvature_high = path_abs / (speed_min * FORD_BP_GAIN_MIN);
const float physical_cap = SAFETY_MIN(FORD_BP_CURVATURE_MAX, FORD_BP_LATERAL_ACCEL / (SAFETY_MAX(speed_max, 1.0F) * SAFETY_MAX(speed_max, 1.0F)));
const float relation_low = SAFETY_ABS(shadow) * speed_min * FORD_BP_GAIN_MIN * 2000.0F;
const float relation_high = SAFETY_ABS(shadow) * speed_max * FORD_BP_GAIN_MAX * 2000.0F;
valid &= lateral_controls_allowed() && controls_allowed && ford_bp_health(now) && ford_bp_shadow_seen &&
(safety_get_ts_elapsed(now, ford_bp_shadow_us) <= FORD_BP_SHADOW_AGE_US) &&
(path >= -1000) && (path <= 1047) && (SAFETY_ABS(path - ford_bp_path_last) <= delta) &&
((path == 0) || ((ford_bp_shadow_raw != 0) && ((path > 0) == (ford_bp_shadow_raw > 0)))) &&
(SAFETY_ABS(path) >= (relation_low - 2.0F)) && (SAFETY_ABS(path) <= (relation_high + 2.0F)) &&
(SAFETY_ABS(shadow) <= (physical_cap + FORD_BP_SHADOW_ROUNDING_MARGIN)) && (path_curvature_high <= (physical_cap + FORD_BP_SHADOW_ROUNDING_MARGIN));
if (speed_max > 10.0F) {
const int measured_min = curvature_state.meas.min - 100;
const int measured_max = curvature_state.meas.max + 100;
const float measured_min_float = (float)measured_min;
const float measured_max_float = (float)measured_max;
valid &= (shadow_can >= measured_min_float) && (shadow_can <= measured_max_float);
// Independent actual-actuator check; an invented shadow cannot launder path angle.
const float signed_low = (path < 0) ? (-path_curvature_high) : path_curvature_low;
const float signed_high = (path < 0) ? (-path_curvature_low) : path_curvature_high;
valid &= ((signed_low * 50000.0F) >= measured_min_float) &&
((signed_high * 50000.0F) <= measured_max_float);
}
const uint32_t elapsed = ford_bp_control_seen ? safety_get_ts_elapsed(now, ford_bp_control_us) : FORD_BP_STEER_PERIOD_US;
const float jerk_speed = SAFETY_MAX(speed_min - 1.0F, 1.0F);
const float jerk_delta = FORD_BP_LATERAL_JERK / (jerk_speed * jerk_speed) *
(SAFETY_MIN(elapsed, FORD_BP_STEER_PERIOD_US) * 0.000001F) * FORD_BP_GAIN_MIN;
valid &= SAFETY_ABS(equivalent - ford_bp_equivalent_last) <= (jerk_delta + FORD_BP_SHADOW_ROUNDING_MARGIN);
if (ford_bp_control_seen) { valid &= elapsed >= 40000U; }
} else {
valid &= path == 0;
}
if (valid) {
ford_bp_rt_msgs += 1U;
ford_bp_equivalent_last = active ? equivalent : 0.0F;
ford_bp_path_last = active ? path : 0;
ford_bp_control_counter = (uint8_t)counter;
ford_bp_control_counter_seen = true;
ford_bp_control_seen = true;
ford_bp_control_us = now;
if (!active) { ford_bp_shadow_seen = false; }
} else {
ford_bp_invalidate_shadow();
}
return valid;
}
@@ -1,101 +0,0 @@
"""Hash-bound upstream angle execution and current-curvature controller oracle.
Only imports are removed from the upstream angle/human module AST; every other
statement and function remains exact. Glue supplies current ABI types, source
fixed defaults and the source's no-op lazy-init/yaw-default adapter. No BluePilot
curvature strategy is loaded. Its diagnostic limiter cannot affect angle output.
"""
import ast
from collections import namedtuple
import hashlib
import json
from pathlib import Path
from types import SimpleNamespace
import numpy as np
from opendbc.car import structs, DT_CTRL
from opendbc.car.ford.values import CAR, CarControllerParams
from opendbc.car.ford.fordcan import CanBus
from opendbc.car.lateral import AngleSteeringLimits, apply_std_steer_angle_limits
from openpilot.selfdrive.modeld.constants import ModelConstants
ROOT = Path(__file__).with_name("reference")
MANIFEST = json.loads((ROOT / "manifest.json").read_text())
def source(name):
raw = (ROOT / (name + ".py.txt")).read_bytes()
if hashlib.sha256(raw).hexdigest() != MANIFEST[name]["sha256"]:
raise AssertionError("Reference source differs from its pinned manifest: " + name)
return ast.parse(raw.decode(), filename=MANIFEST[name]["origin"])
def execute(tree, namespace, *, names=None):
body = []
for node in tree.body:
if isinstance(node, (ast.Import, ast.ImportFrom)):
continue
if names is not None:
assigned = [target.id for target in node.targets if isinstance(target, ast.Name)] if isinstance(node, ast.Assign) else []
if getattr(node, "name", None) not in names and not any(name in names for name in assigned):
continue
body.append(node)
exec(compile(ast.Module(body=body, type_ignores=[]), "<pinned source>", "exec"), namespace)
def angle():
LateralResult = namedtuple("LateralResult", "apply_curvature curvature_rate path_offset path_angle ramp_type precision_type lateralUncertainty")
namespace = {"np": np, "clip": np.clip, "interp": np.interp, "DT_CTRL": DT_CTRL,
"CAR": CAR, "CarControllerParams": CarControllerParams, "ModelConstants": ModelConstants,
"LateralResult": LateralResult, "AngleSteeringLimits": AngleSteeringLimits,
"apply_std_steer_angle_limits": apply_std_steer_angle_limits}
execute(source("human"), namespace)
execute(source("values"), namespace, names={"_BP_ANGLE_RATE_UP", "_BP_ANGLE_RATE_DOWN", "BP_ANGLE_LIMITS"})
execute(source("angle"), namespace)
return namespace["LateralAngleExt"]
def builders():
namespace = {"CanBus": CanBus, "structs": structs}
execute(source("fordcan"), namespace, names={"calculate_lat_ctl2_checksum"})
execute(source("builders"), namespace, names={"create_lka_msg", "create_lat_ctl_msg", "create_lat_ctl2_msg", "_BP_LKA_SHADOW_CURVATURE_SCALE"})
return namespace
def curvature_controller():
# Full exact pre-angle production controller, with unchanged current imports.
namespace = {"__name__": "ford_curvature_pre_angle_reference"}
exec(compile(source("starpilot_curvature_controller"), "<current pre-angle controller>", "exec"), namespace)
return namespace["CarController"]
class Angle:
def __init__(self, identity):
self.controller = angle()()
self.controller.CP = SimpleNamespace(carFingerprint=identity)
self.controller._ensure_lateral_curv_initialized = lambda _cp: None # Exact source guard is a no-op.
self.controller.get_current_curvature = lambda cs: -cs.out.yawRate / max(cs.out.vEgoRaw, .1)
self.controller.path_angle_last = 0.
self.controller.lp = None # Read but unused by the source angle strategy.
self.controller.lane_change_factor_bp = [0., 50.]
self.controller.lane_change_factor_low = 1.
self.controller.model = None
self.controller.update_angle_params(None)
def step(self, inputs):
c = self.controller
c.sm = {"liveDelay": SimpleNamespace(lateralDelay=inputs.lateral_delay)}
c.model = SimpleNamespace(orientationRate=SimpleNamespace(z=list(inputs.yaw_predictions)),
meta=SimpleNamespace(laneChangeState=1 if inputs.lane_change else 0,
laneChangeDirection=inputs.lane_direction)) if inputs.yaw_predictions else None
cc = SimpleNamespace(latActive=inputs.active)
cs = SimpleNamespace(out=SimpleNamespace(vEgoRaw=inputs.speed, yawRate=-inputs.measured_curvature * inputs.speed,
steeringPressed=inputs.steering_pressed, steeringAngleDeg=inputs.steering_angle),
lat_ctl_lim_stat=inputs.limit_status)
return c.update_angle_strategy(cc, cs, SimpleNamespace(curvature=inputs.desired_curvature), c.CP)
def trim():
namespace = {"np": np, "interp": np.interp}
execute(source("bpdev_trim"), namespace)
return namespace["LaneCenterTrim"]
@@ -1,577 +0,0 @@
"""
BluePilot: Ford CAN-FD path-angle–primary lateral control (developer).
Steering intent is c1 from κ → θ. Converts planner/model curvature into ``path_angle`` using
PSCM short lookahead d_ref and y ≈ ½κ x² ⇒ path_angle = ½ κ d_ref (see
``bluepilot/agent_info/20_FORD_PSCM_KNOWLEDGE_PACK.md``). Predicted curvature (modelV2) is
blended with ``actuators.curvature`` per ``FordPathAngleBlendRatio`` (0 = planner only,
1 = model only).
**c0 (path_offset) is always zero on the wire, unconditionally.** Angle mode has no centering
trim -- an earlier port attempt piped a small additive trim onto path_angle through the
curv-mode ``LC_PID_controller``, but it never actually tracked lane center correctly in this
mode and was removed; only the DBC-required zero c0 remains.
**Human-turn override**: while the driver manually turns (same sustained-press + angle criteria
as ``lateral_curv_ext``, via the shared ``HumanTurnDetector``), lateral is forced inactive (mode
0, all-zero signals) instead of winding path_angle into a stale command the PSCM has to reconcile
on release -- on the Mach-E's PSCM that reconciliation cost 2-3 s of dead time before control
resumed. Mode 0 is panda-clean by construction: every ford.h check has a legitimate
!steer_control_enabled branch, so no reset-bypass latch involvement. On release, path_angle ramps
back in from zero through the soft ROC below (no jump seed) -- generous at human-turn speeds, and
admitted by ford.h's path_angle ROC check (2% looser) without any bypass.
"""
import numpy as np
from numpy import clip, interp
from opendbc.car import DT_CTRL
from opendbc.car.lateral import apply_std_steer_angle_limits
from opendbc.car.ford.values import CAR, CarControllerParams
from opendbc.sunnypilot.car.ford.lateral_curv_ext import LateralResult
from opendbc.sunnypilot.car.ford.human_turn import HumanTurnDetector
from opendbc.sunnypilot.car.ford.values_ext import BP_ANGLE_LIMITS
from selfdrive.modeld.constants import ModelConstants
# Hard-coded per-platform gain defaults.
# CAN vehicles (Escape MK4, Bronco Sport, Explorer, Maverick, Edge)
_GAIN_CAN = (1.00, 1.15)
# CAN-FD body-on-frame trucks (F-150, Lightning, Expedition, Ranger)
_GAIN_CANFD_BOF = (0.95, 0.95)
# CAN-FD unibody SUVs (Mustang Mach-E, Escape MK4.5)
_GAIN_CANFD_SUV = (1.00, 1.05)
_CANFD_BOF_CARS = frozenset({
CAR.FORD_F_150_MK14,
CAR.FORD_F_150_LIGHTNING_MK1,
CAR.FORD_EXPEDITION_MK4,
CAR.FORD_RANGER_MK2,
})
_CANFD_SUV_CARS = frozenset({
CAR.FORD_MUSTANG_MACH_E_MK1,
CAR.FORD_ESCAPE_MK4_5,
})
# DBC ``LatCtlPath_An_Actl`` (rad) — panda safety uses the same in ``ford.h``; PSCM enforces in firmware.
FORD_DBC_PATH_ANGLE_MIN = -0.5
FORD_DBC_PATH_ANGLE_MAX = 0.5235
# PSCM d_ref (m) vs speed (m/s) — 6 points; above ~55.6 m/s use plateau + optional cap to 5 m.
_PSCM_DREF_SPEEDS_MS = (0.0, 4.17, 27.78, 41.67, 50.0, 55.56)
_PSCM_DREF_M = (0.5, 0.95, 1.4, 2.075, 2.75, 3.875)
# Default blend ratio validated on F-150 fleet data (0.5s lookup time).
_FORD_PATH_ANGLE_BLEND_RATIO_DEFAULT = 0.50
# Variable lookup time (VLT): curvature_lookup_time adapts to speed and curvature magnitude.
# t_lookup = t_base + t_extra_max × speed_factor(v) × kappa_factor(|κ|)
# t_base = liveDelay.lateralDelay + DT_MDL — always matches the planner's pre-compensation floor.
# Extra lookahead collapses toward zero at high speed (PSCM responds faster)
# and at large curvature (prevents blend importing a "start unwinding" signal too early).
_DT_MDL = 0.05 # model loop period (matches common/realtime.py)
_VLT_T_EXTRA_MAX = 0.10 # max extra lookahead above t_base
_VLT_V_LOW_MS = 25.0 * 0.44704 # 25 mph — full extra lookahead at or below this speed
_VLT_V_HIGH_MS = 55.0 * 0.44704 # 55 mph — no extra lookahead at or above this speed
_VLT_KAPPA_FULL = 0.005 # 1/m — full extra lookahead below this curvature (200m+ radius)
_VLT_KAPPA_TAPER = 0.020 # 1/m — no extra lookahead above this curvature (50m radius)
# Rate cap on path_angle magnitude DECREASE during PSCM LimitReached (rad/call = 0.40 rad/s).
# Both model and planner naturally drop path_angle ~0.36 rad/s at a sharp 90° apex, while the PSCM is
# physically pinned and cannot execute the rapidly falling desired angle. The resulting actual-vs-desired
# gap (up to 47° observed) causes a snap correction the moment the PSCM is released. This cap limits
# the desired-angle drop rate to what the PSCM can reasonably track, at the cost of holding the car
# slightly more in the curve during saturation.
# BluePilot: this strategy runs once per STEER_STEP (CarControllerParams.STEER_STEP=5), i.e. once
# every 5th 100Hz control tick = 20Hz, not every tick. The original 0.004 rad/call value (and its
# "50Hz" comment, corrected above) was authored 2026-05-11 on bp-sid-simple, which had already
# switched STEER_STEP 5->1 (true 100Hz) on 2026-04-22 -- so it was tuned at 100Hz real cadence
# even though its own comment mistakenly said 50Hz. Scaled x5 here to restore the same real-world
# 0.40 rad/s (23 deg/s) unwind rate on this branch's actual 20Hz cadence.
_PSCM_SAT_UNWIND_RATE = 0.02 # rad/call (0.02 * 20Hz = 0.40 rad/s)
# Post-override stall blip. Road test 2026-07-14 (route 886240741b067740/000000bd--feb980680f)
# showed that after driver-touch episodes the Mach-E PSCM keeps reporting InProgress but honors
# path_angle at only ~0.56x (healthy hands-free delivery on the same route: ~0.95 median). The
# current-curvature deviation clip below then pins kappa_cmd at measured + CURVATURE_ERROR, so the
# command can never lead the car enough to overcome the attenuation -- a stall equilibrium the
# driver reads as "not engaging" (wire path_angle flat at ~4 deg for 4.5 s while desired kappa
# climbed to 3x measured, EPS motor current ~0 A). A short mode-0 pulse -- the identical
# panda-clean wire pattern the human-turn override sends, no ford.h involvement -- resets the
# PSCM's authority, after which path_angle ramps back in from zero through the soft ROC.
_STEER_DT = CarControllerParams.STEER_STEP * DT_CTRL # 20 Hz lateral tick (matches human_turn.py)
_STALL_GAP_MIN = 2.0 * CarControllerParams.CURVATURE_ERROR # desired must lead measured by 2x the clip tolerance
_STALL_HOLD_S = 0.5 # accumulated clip-binding time before a pulse fires
_STALL_BLIP_FRAMES = 6 # mode-0 pulse length (6 frames @ 20 Hz = 300 ms; PSCM acked mode 0 in ~150 ms on-road)
_STALL_COOLDOWN_S = 2.0 # re-arm delay after a pulse (release ramp + PSCM response time)
_STALL_MAX_BLIPS = 3 # give up on a stuck episode; devLim telemetry keeps recording the stall
# Proactive hand-off blip: any sustained driver press attenuates the PSCM (route 000000be seg 4:
# 3 s of sub-45-deg circle-exit steering left it at ~0x delivery, and the reactive detector's
# fire-after-the-stall-develops timing meant 2.4 s of dead-straight running into the next curve
# before the pulse landed). Firing the same pulse on the falling edge of a sustained press resets
# the PSCM while the car is straight and the command is small -- a 300 ms lateral gap right at
# hand-off, imperceptible, instead of a missed curve. The reactive detector above stays as backstop.
_PRESS_BLIP_MIN_S = 0.5 # press must last this long before its release earns a pulse
# The pulse releases steering for 300 ms; never fire it in a curve.
_BLIP_MAX_PATH_ANGLE = 0.10 # rad
def pscm_d_ref_m(v_ego_ms: float) -> float:
v = max(float(v_ego_ms), 0.0)
d = float(np.interp(v, _PSCM_DREF_SPEEDS_MS, _PSCM_DREF_M))
if v > _PSCM_DREF_SPEEDS_MS[-1]:
# Doc: d_ref table ends at 3.875 m; contribution saturates for high speed — cap at 5 m.
d = min(5.0, d)
return d
class LateralAngleExt:
def __init__(self, CP=None, CP_SP=None):
# Predicted-curvature blend for path_angle: pred * b + desired * (1-b); b from ``FordPathAngleBlendRatio``
self.path_angle_blend_ratio = _FORD_PATH_ANGLE_BLEND_RATIO_DEFAULT
# Max extra VLT above t_base; from ``FordVLTExtraMax`` param
self.vlt_extra_max = _VLT_T_EXTRA_MAX
# Telemetry: final path_angle (rad) after limits (see bp_card_publisher)
self.bp_path_angle_final = 0.0
# High-speed gain factors: set per-platform via carFingerprint in update_angle_params.
self.path_angle_gain_lowC_highV = 1.0 # dampening at high speed, low curvature
self.path_angle_gain_highC_highV = 1.0 # gain at high speed, high curvature
self.bp_path_angle_gain_lowC_highV = 1.0
self.bp_path_angle_gain_highC_highV = 1.0
# User-tunable "feel" multipliers: read from the angle-tuning Params below.
self.low_speed_curv_factor = 1.0
self.high_speed_curv_factor = 1.0
self.user_dampening_factor = 1.0
self.bp_low_speed_curv_factor = 1.0
self.bp_high_speed_curv_factor = 1.0
# BluePilot: angle mode's own lane-change scaling factor, independent of curvature mode's
# lane_change_factor_high_curv -- angle needs a boost (>1) where curvature needs a cut (<1).
self.lane_change_factor_high_ang = 1.0
# Telemetry: variable curvature lookup time used this frame (s)
self.bp_curvature_lookup_time = _VLT_T_EXTRA_MAX + 0.3725 # warm start at ~0.5s
# BluePilot: error-clipped kappa path_angle was derived from -- carcontroller.py reads this as
# shadow_curvature for ford.h's angle-mode deviation check. Actively consumed, not telemetry.
self.bp_kappa_cmd = 0.0
# BluePilot: rate-limit diagnostics (controllerStateBP)
self.bp_angle_rate_limited = False # path_angle soft-ROC clip actually bit this frame
self.bp_curvature_rate_limited = False # equivalent curvature would be rate-limited by curv-mode logic (sim)
self.bp_curvature_deviation_limited = False # current_curvature error-clip constrained kappa_cmd this frame
self.sim_curvature_last = 0.0 # shadow curvature-mode last for the curvatureRateLimited sim
# Exit detection: track previous desired curvature to sense when planner is actively reducing
self._desired_curvature_last = 0.0
# Human-turn override: while the driver manually turns, lateral is forced inactive (mode 0,
# all-zero signals) instead of winding path_angle into a stale command the PSCM can't cleanly
# reconcile on release (2-3 s re-engage dead time observed on Mach-E). See module docstring.
# Note: in CarController this attribute is shared with LateralCurvExt (same mixin instance) --
# only one lateral strategy runs per frame, so a single detector serves both.
self.human_turn_detector = HumanTurnDetector()
self.angle_human_turn_active = False # read by carcontroller to force mode 0
# Post-override stall blip state (see module constants). angle_stall_blip_active is read by
# carcontroller to force mode 0, exactly like angle_human_turn_active.
self.stall_blip_hold_s = 0.0 # accumulated deviation-clip-binding time toward a pulse
self.stall_blip_frames_left = 0 # remaining pulse frames; > 0 -> mode 0 on the wire
self.stall_blip_cooldown_s = 0.0 # re-arm delay after a pulse
self.stall_blip_count = 0 # pulses fired this stall episode
self.angle_stall_blip_active = False
self.press_timer_s = 0.0 # continuous steeringPressed time, for the hand-off blip
def update_angle_params(self, params):
"""Sets per-platform gain defaults and reads user angle-tuning params."""
self._ensure_lateral_curv_initialized(self.CP)
fp = getattr(self.CP, 'carFingerprint', '')
if fp in _CANFD_BOF_CARS:
low, high = _GAIN_CANFD_BOF
elif fp in _CANFD_SUV_CARS:
low, high = _GAIN_CANFD_SUV
else:
low, high = _GAIN_CAN
self.path_angle_gain_lowC_highV = low
self.path_angle_gain_highC_highV = high
if params is not None and hasattr(params, "get"):
for attr, key, min_value, max_value in (
("low_speed_curv_factor", "FordLowSpeedFactor_ang", 0.5, 1.5),
("high_speed_curv_factor", "FordHighSpeedFactor_ang", 0.5, 1.5),
("user_dampening_factor", "FordHighSpeedDampening_ang", 0.25, 1.25),
):
try:
raw = params.get(key, return_default=True)
if raw is not None and raw != b"":
setattr(self, attr, float(clip(
float(raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else raw), min_value, max_value)))
except Exception:
pass
try:
raw = params.get("lane_change_factor_high_ang", return_default=True)
if raw is not None and raw != b"":
self.lane_change_factor_high_ang = float(clip(
float(raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else raw), 0.85, 1.50))
except Exception:
pass
def update_angle_strategy(self, CC, CS, actuators, CP):
"""
Curvature from planner (+ optional predicted blend) → path_angle via ½·κ·d_ref.
c0 (path_offset) is always zero on the wire -- no centering trim in angle mode. c2 and c3 are zero.
Blended κ is not passed through Ford c2 rate / DBC limits (those target the curvature actuator).
"""
self._ensure_lateral_curv_initialized(CP)
v_ego = float(CS.out.vEgoRaw)
d_ref = pscm_d_ref_m(v_ego)
curvature_rate = 0.0
path_offset = 0.0
path_angle = 0.0
ramp_type = 0
lateral_uncertainty = 0.0
precision = 1
if not CC.latActive:
self.path_angle_last = 0.0
self.bp_path_angle_final = 0.0
self.apply_curvature_last = 0.0
self.bp_angle_rate_limited = False
self.bp_curvature_rate_limited = False
self.bp_curvature_deviation_limited = False
self.sim_curvature_last = 0.0
# Publish the shadow curvature from the measured curvature while inactive. LKA keeps
# carrying angle_mode_engaged whenever angle mode is configured (independent of
# latActive), and ford.h latches the shadow from every LKA frame -- so the latched
# value must track reality here, not sit at a stale zero. Otherwise the first enabled
# LMC frame after (re-)engage races LKA's 33Hz latch against LMC's 20Hz enable bit and
# ford.h's deviation check compares a zero shadow against real measured curvature.
# (ford.h skips the check while steer_control_enabled is 0, so the value is free to
# follow the measurement during the inactive period itself.)
self.bp_kappa_cmd = self.get_current_curvature(CS)
self.human_turn_detector.reset()
self.angle_human_turn_active = False
self.stall_blip_hold_s = 0.0
self.stall_blip_frames_left = 0
self.stall_blip_cooldown_s = 0.0
self.stall_blip_count = 0
self.angle_stall_blip_active = False
self.press_timer_s = 0.0
self.precision_type = 1
return LateralResult(
apply_curvature=0.0,
curvature_rate=0.0,
path_offset=0.0,
path_angle=0.0,
ramp_type=0,
precision_type=1,
lateralUncertainty=0.0,
)
# Human-turn override: sustained driver press + large wheel angle → force lateral inactive
# (carcontroller drops mode to 0; all signals are zero on the wire) so path_angle can't wind
# into a stale command while the driver turns. Always on in angle mode (no param gate) -- the
# curv-suffixed human-turn toggle belongs to curvature mode's reset strategy, and the Mach-E
# PSCM re-engage stall this prevents is not something a user should be able to opt out of.
# On release, no jump seed: path_angle_last is 0, so the normal flow below ramps the command
# back in through the soft ROC -- generous at human-turn speeds, no panda bypass involved.
self.angle_human_turn_active = self.human_turn_detector.update(
True, CS.out.steeringPressed, CS.out.steeringAngleDeg)
if self.angle_human_turn_active:
self.path_angle_last = 0.0
self.bp_path_angle_final = 0.0
self.apply_curvature_last = 0.0
self.bp_angle_rate_limited = False
self.bp_curvature_rate_limited = False
self.bp_curvature_deviation_limited = False
self.sim_curvature_last = 0.0
# Truthful shadow during the override (mirrors the inactive path -- see the comment
# there): the driver is steering, so the honest command is the car's actual curvature,
# and the panda-latched shadow stays current for the re-engage frame.
self.bp_kappa_cmd = self.get_current_curvature(CS)
# Keep exit detection current so resume doesn't compare against a stale pre-turn value.
self._desired_curvature_last = float(actuators.curvature)
# A human turn ends any stall episode -- its own mode 0 does the PSCM reset job. That also
# covers the press so far: only press time accumulated AFTER the latch releases should earn
# a hand-off pulse.
self.stall_blip_hold_s = 0.0
self.stall_blip_frames_left = 0
self.stall_blip_cooldown_s = 0.0
self.stall_blip_count = 0
self.angle_stall_blip_active = False
self.press_timer_s = 0.0
self.precision_type = 1
return LateralResult(
apply_curvature=0.0,
curvature_rate=0.0,
path_offset=0.0,
path_angle=0.0,
ramp_type=0,
precision_type=1,
lateralUncertainty=0.0,
)
# Proactive hand-off blip: the falling edge of a sustained press earns an immediate mode-0
# pulse (see _PRESS_BLIP_MIN_S) -- resets the PSCM's press-induced attenuation right at
# hand-off, while the car is straight and the command small, instead of waiting for the
# reactive stall detector below to watch the car miss the next curve first.
if CS.out.steeringPressed:
self.press_timer_s += _STEER_DT
else:
if (self.press_timer_s >= _PRESS_BLIP_MIN_S and self.stall_blip_cooldown_s <= 0.0
and self.stall_blip_frames_left <= 0
and abs(self.path_angle_last) < _BLIP_MAX_PATH_ANGLE):
self.stall_blip_frames_left = _STALL_BLIP_FRAMES
self.press_timer_s = 0.0
# Stall-blip pulse in progress: hold lateral inactive (mode 0, all-zero signals -- the same
# wire pattern as the human-turn override, no ford.h involvement) for _STALL_BLIP_FRAMES so the
# PSCM drops its post-override attenuation, then release; path_angle ramps back in from zero
# through the soft ROC exactly like a human-turn release. Detection lives at the end of the
# normal flow below.
if self.stall_blip_frames_left > 0:
self.stall_blip_frames_left -= 1
self.angle_stall_blip_active = True
self.path_angle_last = 0.0
self.bp_path_angle_final = 0.0
self.apply_curvature_last = 0.0
self.bp_angle_rate_limited = False
self.bp_curvature_rate_limited = False
self.bp_curvature_deviation_limited = False
self.sim_curvature_last = 0.0
# Truthful shadow during the blip (see the inactive-path comment).
self.bp_kappa_cmd = self.get_current_curvature(CS)
self._desired_curvature_last = float(actuators.curvature)
self.precision_type = 1
if self.stall_blip_frames_left <= 0:
self.stall_blip_cooldown_s = _STALL_COOLDOWN_S
return LateralResult(
apply_curvature=0.0,
curvature_rate=0.0,
path_offset=0.0,
path_angle=0.0,
ramp_type=0,
precision_type=1,
lateralUncertainty=0.0,
)
self.angle_stall_blip_active = False
self.precision_type = 1
precision = 1
LP = self.lp
desired_curvature = float(actuators.curvature)
# Variable lookup time: t_base tracks planner pre-compensation; extra tapers on high speed and large curves.
# Cap liveDelay at 0.15s for VLT purposes. liveDelay can calibrate up to ~420ms on some runs, which inflates
# VLT to 0.6s and pushes the model lookahead 5m into the curve. At that depth the model sees full peak
# curvature, kappa_entering stays True, and the exit-biased blend is permanently disabled — causing the car
# to command max path_angle through the entire apex. 0.15s gives t_base ≤ 0.20s and VLT ≤ 0.33s, restoring
# the 2.8m lookahead that kept kappa_entering False at the apex in successful earlier runs.
_t_base = float(clip(self.sm['liveDelay'].lateralDelay, 0.1, 0.15)) + _DT_MDL
_speed_factor = float(interp(v_ego, [_VLT_V_LOW_MS, _VLT_V_HIGH_MS], [1.0, 0.0]))
# Direction-aware kappa factor: on curve ENTRY (model shows more curvature at t_base than planner now),
# keep full lookahead so pre-steering begins early. On exit/apex, taper by magnitude to prevent unwind.
_kappa_at_t_base = 0.0
if self.model is not None and len(self.model.orientationRate.z) >= 17:
_curvatures_ref = np.array(self.model.orientationRate.z) / max(0.01, v_ego)
_kappa_at_t_base = abs(float(interp(_t_base, ModelConstants.T_IDXS, _curvatures_ref)))
_kappa_entering = _kappa_at_t_base > abs(desired_curvature)
if _kappa_entering:
_kappa_factor = 1.0 # curve deepening ahead: full extra lookahead for gradual entry
else:
_kappa_factor = float(interp(abs(desired_curvature), [_VLT_KAPPA_FULL, _VLT_KAPPA_TAPER], [1.0, 0.0]))
curvature_lookup_time = _t_base + self.vlt_extra_max * _speed_factor * _kappa_factor
self.bp_curvature_lookup_time = curvature_lookup_time
predicted_curvature = 0.0
if self.model is not None and len(self.model.orientationRate.z) >= 17:
curvatures = np.array(self.model.orientationRate.z) / max(0.01, v_ego)
predicted_curvature = float(
interp(curvature_lookup_time, ModelConstants.T_IDXS, curvatures)
)
b = float(self.path_angle_blend_ratio)
b = float(clip(b, 0.0, 1.0))
# Exit-biased blend: near the PSCM authority limit or while the planner is actively
# reducing curvature (exit detected), drop model prediction weight from 60% → ~15%.
# This lets the planner's natural unwind dominate instead of being diluted by a model
# prediction that still sees the curve (→ seg-14 slow unwind) or that snaps when its
# lookahead window crosses the curve exit (→ seg-17 snap + reverse PSCM hit).
# Normal gentle curves are unaffected: no PSCM limit, no falling desired → full b=0.60.
_pscm_lim = getattr(CS, 'lat_ctl_lim_stat', 0)
# In angle mode, LatCtlLim_D_Stat (→ lat_ctl_lim_stat) does not fire.
# Previously used angleState.saturated (CtrSat) as a proxy, but CtrSat fires whenever the car
# lags the commanded path_angle by > 2.5° — which happens during any normal curve entry.
# That caused a positive-feedback flat-line: under-steer → CtrSat → path_angle frozen → more under-steer.
# Use DBC-limit proximity instead: only block when path_angle is already near the ±0.5 rad CAN limits,
# which is the only condition where the anti-snap unwind rate cap makes physical sense.
_dbc_sat = (self.path_angle_last >= FORD_DBC_PATH_ANGLE_MAX * 0.90 or
self.path_angle_last <= FORD_DBC_PATH_ANGLE_MIN * 0.90)
_in_hard_sat = _pscm_lim >= 2 or _dbc_sat
# BluePilot: per-call delta threshold. The original 0.002 was authored 2026-05-07 on
# bp-sid-simple (9c3d000fd), which ran STEER_STEP=1 (true 100Hz, switched 2026-04-22) -- so it
# was tuned to trigger on planner unwind faster than 0.2 (1/m)/s. Scaled x5 here to restore
# that same real-world trigger rate on this branch's actual 20Hz cadence; unscaled it fired at
# 0.04 (1/m)/s, collapsing the model blend on mild straightening instead of genuine exits.
# Same bug class and fix as _PSCM_SAT_UNWIND_RATE and _soft_roc above.
_desired_falling = abs(desired_curvature) < abs(self._desired_curvature_last) - 0.010
_on_exit_near_limit = not _kappa_entering and (_pscm_lim >= 1 or _in_hard_sat or _desired_falling)
b_blend = float(clip(b * 0.25, 0.0, 1.0)) if _on_exit_near_limit else b
requested_curvature = predicted_curvature * b_blend + desired_curvature * (1.0 - b_blend)
self._desired_curvature_last = desired_curvature
if self.model is not None:
self.lane_change = self.model.meta.laneChangeState in (1, 2, 3)
else:
self.lane_change = False
lane_change_factor = interp(
v_ego, self.lane_change_factor_bp, [self.lane_change_factor_low, self.lane_change_factor_high_ang]
)
if self.lane_change and self.model is not None:
if self.model.meta.laneChangeDirection == 1 and requested_curvature < 0:
requested_curvature *= lane_change_factor
precision = 0
elif self.model.meta.laneChangeDirection == 2 and requested_curvature > 0:
requested_curvature *= lane_change_factor
precision = 0
self.precision_type = precision
# Use planner / predicted κ directly for the κ → path_angle map; we are not sending κ on CAN.
kappa_cmd = float(requested_curvature)
# BluePilot: clip kappa_cmd to current_curvature (measured, from yaw rate) +- CURVATURE_ERROR,
# mirroring lateral_curv_ext.py's apply_ford_curvature_limits_ext exactly (same formula, same
# v_ego > 9 gate, same CarControllerParams.CURVATURE_ERROR tolerance). Without this, kappa_cmd
# (and therefore path_angle, and the shadow_curvature sent to ford.h) can legitimately lead the
# measured curvature by more than ford.h's angle-error tolerance during normal curve entry/exit
# -- the shadow-curvature deviation check (ford_shadow_curvature_error_check) would then block
# routinely, not just on genuine pothole/override divergence. Curvature mode has always clipped
# here; this brings angle mode's actual steering intent in line with that proven behavior rather
# than only clipping the value reported to panda (which would make the check a no-op).
current_curvature = self.get_current_curvature(CS)
self.bp_curvature_deviation_limited = False
if v_ego > 9:
_kappa_cmd_pre_error_clip = kappa_cmd
kappa_cmd = float(clip(kappa_cmd, current_curvature - CarControllerParams.CURVATURE_ERROR,
current_curvature + CarControllerParams.CURVATURE_ERROR))
# BluePilot: did this clip actually constrain kappa_cmd this frame (deviation from measured,
# not rate-of-change -- see carcontroller.py)?
self.bp_curvature_deviation_limited = bool(abs(kappa_cmd - _kappa_cmd_pre_error_clip) > 1e-9)
lateral_uncertainty = 0.0 # no curvature-limit ladder until angle-mode torque display is defined
# Speed-interpolated gain: at low speed both curves use 1.0; at high speed the params take effect.
self.low_gain_calc = interp(
v_ego, [13.5, 26.82], [1.0, (self.path_angle_gain_lowC_highV * self.user_dampening_factor)]
)
self.high_gain_calc = interp(v_ego, [13.5, 26.82], [(1.30 * self.low_speed_curv_factor), (self.path_angle_gain_highC_highV * self.high_speed_curv_factor)])
# As the curve gets bigger, we will need a little boost to the signal to to not understeer
self.curvature_factor = interp(abs(kappa_cmd), [0.0007, 0.001], [self.low_gain_calc, self.high_gain_calc])
path_angle_calc = kappa_cmd * v_ego * self.curvature_factor
path_angle = path_angle_calc
# PSCM authority limit clamp.
# On CANFD Fords in angle mode, LatCtlLim_D_Stat does not fire, so _pscm_lim stays 0.
# _in_hard_sat (computed above) combines _pscm_lim >= 2 with _dbc_sat (path_angle near ±0.5 rad limit).
# LimitClose (_pscm_lim >= 1 only): block magnitude increases — exit-biased blend provides unwind.
# Hard saturation (_in_hard_sat): block increases AND rate-limit decreases to _PSCM_SAT_UNWIND_RATE.
# Without the decrease cap, model+planner drop path_angle at ~0.36 rad/s at a sharp apex,
# driving desired steering 30°+ ahead of actual while the PSCM is pinned, causing a snap when released.
if _in_hard_sat:
_last = self.path_angle_last
_last_mag = abs(_last)
_curr_mag = abs(path_angle)
if _curr_mag > _last_mag: # magnitude growing — block
path_angle = _last
elif _last_mag - _curr_mag > _PSCM_SAT_UNWIND_RATE: # decreasing too fast — rate-limit
_limited_mag = _last_mag - _PSCM_SAT_UNWIND_RATE
path_angle = float(_limited_mag if _last >= 0 else -_limited_mag)
elif _pscm_lim >= 1: # LimitClose (F150/non-angle-mode only): block increases only
path_angle = float(clip(path_angle, -abs(self.path_angle_last), abs(self.path_angle_last)))
path_angle = min(FORD_DBC_PATH_ANGLE_MAX, max(FORD_DBC_PATH_ANGLE_MIN, path_angle))
# Soft ROC limit — unconditional, slightly tighter than ford.h, applied before the
# hardware bypass in ford.h is re-enabled. Lets us observe whether the limit would
# suppress control and tune it, while the PSCM still receives the clipped value.
# BluePilot: this strategy runs once per STEER_STEP (CarControllerParams.STEER_STEP=5), i.e.
# once every 5th 100Hz control tick = 20Hz, not every tick -- ported "verbatim from bp-sid-simple"
# (2026-06-13), which runs STEER_STEP=1 (true 100Hz, switched 2026-04-22). The y-values below are
# scaled x5 from the original [0.011, 0.011, 0.0085, 0.0018] to restore the same real-world rate
# (63/63/49/10 deg/s at v=9-10/15/25) on this branch's actual 20Hz cadence. See ford.h's
# FORD_PATH_ANGLE_LIMITS, which must mirror this scaling (x1.02 looser) to stay a true backstop.
_soft_roc = float(interp(v_ego, [9., 10., 15., 25.], [0.055, 0.055, 0.0425, 0.009]))
_path_angle_pre_roc = path_angle
path_angle = float(clip(path_angle,
self.path_angle_last - _soft_roc,
self.path_angle_last + _soft_roc))
# BluePilot: did the soft ROC clip actually limit the path_angle we wanted to send this frame?
self.bp_angle_rate_limited = bool(abs(path_angle - _path_angle_pre_roc) > 1e-9)
# c0 always zero -- no centering trim in angle mode.
path_offset = 0.0
# Telemetry / state
self.bp_path_angle_gain_lowC_highV = self.path_angle_gain_lowC_highV
self.bp_path_angle_gain_highC_highV = self.path_angle_gain_highC_highV
self.bp_low_speed_curv_factor = self.low_speed_curv_factor
self.bp_high_speed_curv_factor = self.high_speed_curv_factor
self.path_angle_last = path_angle
self.bp_path_angle_final = path_angle
self.apply_curvature_last = 0.0
# BluePilot: the error-clipped kappa path_angle was derived from -- carcontroller.py reads this
# as shadow_curvature for ford.h's angle-mode deviation check (see fordcan_ext.create_lka_msg).
# Not just telemetry: an actively-consumed value, unlike the removed *_kappa_cmd_raw stubs.
# While the driver is pressing (before the human-turn override latches), the clipped planner
# kappa can't follow the wheel: the driver moves the measured curvature faster than the
# deviation clip tracks it, so the shadow can exit ford.h's error band mid-curve -- the one
# in-drive lateral safety block observed across ~3h of replayed road-test routes was exactly
# this (driver fighting a sustained curve with the mode still enabled). The honest command
# during a press is the driver's actual curvature.
self.bp_kappa_cmd = self.get_current_curvature(CS) if CS.out.steeringPressed else kappa_cmd
# BluePilot: would the equivalent curvature (kappa_cmd) have been rate-limited by curvature-mode's
# ROC (apply_std_steer_angle_limits)? kappa_cmd is already error-clipped above (same clip
# curvature mode applies), so only the rate-of-change portion remains to simulate here.
_equiv_curv_rl = apply_std_steer_angle_limits(kappa_cmd, self.sim_curvature_last, v_ego,
CS.out.steeringAngleDeg, CC.latActive, BP_ANGLE_LIMITS)
self.bp_curvature_rate_limited = bool(abs(_equiv_curv_rl - kappa_cmd) > 1e-9)
self.sim_curvature_last = float(_equiv_curv_rl)
# Post-override stall detection (mechanism in the module constants' comment). Fires the mode-0
# blip when, hands-free, desired curvature has led measured by more than 2x the deviation
# clip's tolerance while the clip was actually binding for _STALL_HOLD_S accumulated seconds.
# devLim flickers mid-stall (~63% duty on the diagnosis route), so off frames hold the
# accumulator rather than resetting it; a closed gap or driver press ends the episode.
self.stall_blip_cooldown_s = max(0.0, self.stall_blip_cooldown_s - _STEER_DT)
_stall_gap = desired_curvature - current_curvature
_stalled = (not CS.out.steeringPressed and not self.lane_change and v_ego > 9.0
and abs(_stall_gap) > _STALL_GAP_MIN
and abs(desired_curvature) > abs(current_curvature))
if _stalled:
if self.bp_curvature_deviation_limited and self.stall_blip_cooldown_s <= 0.0:
self.stall_blip_hold_s += _STEER_DT
if (self.stall_blip_hold_s >= _STALL_HOLD_S and self.stall_blip_count < _STALL_MAX_BLIPS
and abs(self.path_angle_last) < _BLIP_MAX_PATH_ANGLE):
self.stall_blip_frames_left = _STALL_BLIP_FRAMES
self.stall_blip_hold_s = 0.0
self.stall_blip_count += 1
else:
self.stall_blip_hold_s = 0.0
if CS.out.steeringPressed or abs(_stall_gap) < 0.5 * _STALL_GAP_MIN:
self.stall_blip_count = 0 # episode over: the car is tracking again or the driver took it
ramp_type = 2
return LateralResult(
apply_curvature=0.0,
curvature_rate=curvature_rate,
path_offset=path_offset,
path_angle=path_angle,
ramp_type=ramp_type,
precision_type=self.precision_type,
lateralUncertainty=lateral_uncertainty,
)
@@ -1,203 +0,0 @@
"""
BluePilot: angle-mode "advanced lane positioning" -- a curvature-domain trim toward true
lane-line center (plus an optional user left/right bias), for use with path_angle-primary
control (``lateral_angle_ext.py``).
Under curvature-primary control, lane positioning rode on a second DBC signal (path_angle as
a heading trim, c3) layered on top of curvature (c1, the primary). In angle mode, path_angle
IS the primary signal -- there is no separate trim slot on the wire. An earlier attempt ported
the old PID controller as an additive delta on the *final* path_angle value and it never tracked
lane center correctly: path_angle there is a derived quantity (``kappa_cmd * v_ego *
curvature_factor``), so an additive trim in that domain has the wrong (inverted) speed-dependence
for a lane-centering nudge, and it bypasses every limiter angle mode already applies to
``kappa_cmd`` (deviation clip, PSCM-saturation clamp, DBC clip, soft ROC).
This class instead computes a small correction in the **curvature domain**, meant to be added to
``kappa_cmd`` before any of those limiters run -- so the correction automatically inherits all of
them, with no new safety-relevant code path. This mirrors where a GM-focused fork (StarPilot,
u/jc01rho -- ``selfdrive/controls/lib/drive_helpers.py::LaneCenteringController`` on
``feat/lane-centering-camera-offset``) places their own lane-centering trim: on the universal
``desired_curvature`` signal, upstream of any car-specific actuator conversion. The geometry here
(``raw_correction = 2*error / lookahead**2``) is the same ``y ~= 1/2 * kappa * x**2`` relation
BluePilot already uses for PSCM d_ref / path_angle elsewhere in this package, solved for the
curvature that would eliminate a lateral position error at the lookahead distance.
**Model-position fallback (blend, not hard gate)**: mirrors ``lateral_curv_ext.py``'s
``path_offset`` blend of ``path_offset_position`` (the model's own predicted path -- a "no
correction" baseline) vs. ``path_offset_lanelines`` (the geometric lane center), weighted by a
laneline confidence score. The *target* we correct toward is that same blend here, so:
- Good lane lines (confidence -> 1): target -> lane-line center + offset (full centering + bias).
- No/poor lane lines (confidence -> 0): target -> the model's own current path + offset, i.e. the
correction reduces to *just* the user's left/right bias riding on top of whatever the model
already wants to do -- it never goes to zero just because lane lines dropped out. This is the
case that matters on center-stripe-only roads with a curbed edge: there's no reliable lane line
to center between, but the user's bias should still nudge the car off the curb.
Confidence uses the exact formula and breakpoints ``lateral_curv_ext.py`` already uses for
``laneline_confidence`` / ``min_laneline_confidence_bp`` (smooth width-tolerance + laneline probs,
not a hard cutoff) -- reusing an already-tuned, production formula instead of inventing a second one.
"""
import numpy as np
from numpy import interp
# Laneline confidence blend -- ported verbatim from lateral_curv_ext.py's path_offset blend
# (laneline_width_tolerance / min_laneline_confidence_bp) so both control schemes agree on what
# "good lane lines" means.
_WIDTH_TOLERANCE_BP = (2.4, 2.8, 3.75, 4.25) # m - the floor is added from StarPilot
_WIDTH_TOLERANCE_V = (0.0, 0.81, 0.81, 0.59) # Adds StarPilot's low edge
_STD_TOLERANCE_BP = (0.3, 0.5) #0.3 is from StarPilot to define bad references
_STD_TOLERANCE_V = (0.81, 0.0) #0.81 is a known good value and allows it to fade to zero as std increases
_CONFIDENCE_BP = (0.6, 0.8)
_CONFIDENCE_V = (0.0, 1.0)
# Speed ramp: reuses curvature-mode's already-tuned centering-authority envelope
# (LC_PID_speed_bp/v in lateral_curv_ext.py) instead of inventing a new one.
_SPEED_RAMP_BP = (0.0, 9.0, 15.0) # m/s
_SPEED_RAMP_V = (0.0, 0.0, 1.0)
# Lookahead distance for the position-error sample, clipped to a sane range (m).
_LOOKAHEAD_MIN_M = 8.0
_LOOKAHEAD_MAX_M = 35.0
# Hard safety ceiling on the raw correction magnitude (1/m) -- fixed, not a "feel" knob, same
# treatment as _PSCM_SAT_UNWIND_RATE / _soft_roc in lateral_angle_ext.py.
_MAX_RAW_CORRECTION = 0.004
# First-order smoothing time constant (s) -- avoids abrupt jumps in the trim.
_SMOOTH_TAU_S = 0.4
# Rate-of-change limit on the applied correction (1/m per 20 Hz tick), independent of the
# smoothing filter above. The filter alone still has its *fastest* slew immediately after a
# target jump -- its per-tick step is proportional to how far the target moved, so a large,
# sudden confidence swing (e.g. crossing into/out of a too-wide merge lane, or lane lines
# dropping out entirely while the model's own path and the laneline center disagree) can still
# produce a big single-tick correction change even though it's technically "smoothed". This caps
# that step explicitly, the same way path_angle's own soft ROC (lateral_angle_ext.py) and
# curvature mode's LC_path_angle_ROC (lateral_curv_ext.py) rate-limit their outputs. At this
# rate, crossing the full -_MAX_RAW_CORRECTION..+_MAX_RAW_CORRECTION span takes ~2.7s; a more
# typical confidence-transition jump (a fraction of that) resolves proportionally faster.
_CORRECTION_ROC_PER_TICK = 0.00015
class LaneCenterTrim:
def __init__(self):
self._correction = 0.0
def reset(self) -> None:
self._correction = 0.0
def update(self, kappa_cmd: float, model, v_ego: float, enabled: bool, offset: float,
gain: float, lat_active: bool, lane_change: bool) -> float:
"""Returns ``kappa_cmd``, nudged toward (lane-blend target + ``offset``) when active.
``offset`` (m): positive shifts the target right, negative left (same sign convention as
curvature mode's ``custom_path_offset_curv``). Applied whether or not lane lines are usable
-- see module docstring.
``gain`` (0.0-1.0): user-tunable authority -- how much of the (already magnitude-clipped)
raw correction is actually applied. 0 disables the trim's effect without disabling detection.
The applied correction is both exponentially smoothed (_SMOOTH_TAU_S) and rate-limited
(_CORRECTION_ROC_PER_TICK) -- see the constants above -- so a lane-line-confidence
transition (e.g. a merge lane too wide to be trusted, then narrowing back into range) eases
into its new target instead of snapping.
"""
if not enabled or not lat_active or lane_change or model is None:
self.reset()
return kappa_cmd
speed_factor = float(interp(v_ego, _SPEED_RAMP_BP, _SPEED_RAMP_V))
if speed_factor <= 0.0:
self.reset()
return kappa_cmd
if not (np.isfinite(offset) and np.isfinite(gain)): #stops not a number from getting to steering command; validation of number
self.reset()
return kappa_cmd
valid, raw = self._raw_correction(model, v_ego, offset)
if not valid:
# Only true failure case now: model.position itself is unusable, so there's no baseline
# to offset from at all (see _raw_correction). Lane-line-only failures fall back to the
# model-position blend instead of landing here.
self.reset()
return kappa_cmd
target = float(np.clip(raw, -_MAX_RAW_CORRECTION, _MAX_RAW_CORRECTION)) * float(np.clip(gain, 0.0, 1.0)) * speed_factor
alpha = 1.0 - np.exp(-0.05 / _SMOOTH_TAU_S) # BluePilot lateral tick is 20 Hz (dt=0.05s)
filtered = float(alpha * target + (1.0 - alpha) * self._correction)
# Rate-of-change limit -- see _CORRECTION_ROC_PER_TICK. Bounds the correction's own per-tick
# delta on top of the exponential filter above, so a lane-line-confidence transition can't
# snap the trim toward its new target in one or two frames.
self._correction = float(np.clip(filtered, self._correction - _CORRECTION_ROC_PER_TICK,
self._correction + _CORRECTION_ROC_PER_TICK))
return kappa_cmd + self._correction
@property
def correction(self) -> float:
"""Telemetry: last applied correction (1/m)."""
return self._correction
def _raw_correction(self, model, v_ego: float, offset: float) -> tuple[bool, float]:
try:
pos_x = np.asarray(model.position.x, dtype=float)
pos_y = np.asarray(model.position.y, dtype=float)
if pos_x.size < 2 or pos_x.size != pos_y.size:
return False, 0.0
if not (np.isfinite(pos_x).all() and np.isfinite(pos_y).all() and np.all(np.diff(pos_x) > 0)):
return False, 0.0
lookahead = float(np.clip(v_ego, _LOOKAHEAD_MIN_M, _LOOKAHEAD_MAX_M))
model_y = float(np.interp(lookahead, pos_x, pos_y))
except (AttributeError, IndexError, TypeError, ValueError):
return False, 0.0
# Laneline contribution, blended in by confidence. scale=0 (model-position-only, i.e. just
# the user's offset bias) whenever lines are missing, low-confidence, or structurally bad --
# see _laneline_blend for the confidence formula (ported from lateral_curv_ext.py).
scale, laneline_center_y = self._laneline_blend(model, lookahead)
target_y = model_y * (1.0 - scale) + laneline_center_y * scale
error = (target_y + offset) - model_y
raw = 2.0 * error / (lookahead ** 2)
return True, float(raw)
def _laneline_blend(self, model, lookahead: float) -> tuple[float, float]:
"""Returns (scale, laneline_center_y). scale=0 whenever lanelines can't be trusted (missing,
low-probability, structurally invalid) -- center_y is unused/meaningless in that case since
it's weighted out by scale in the caller's blend."""
try:
lane_lines = model.laneLines
probs = model.laneLineProbs
stds = model.laneLineStds # a line could exist but this verifies how sure it is of where it is
if len(lane_lines) < 3 or len(probs) < 3 or len(stds) < 3:
return 0.0, 0.0
left_x = np.asarray(lane_lines[1].x, dtype=float)
left_y = np.asarray(lane_lines[1].y, dtype=float)
right_x = np.asarray(lane_lines[2].x, dtype=float)
right_y = np.asarray(lane_lines[2].y, dtype=float)
if (left_x.size < 2 or left_x.size != left_y.size or
right_x.size < 2 or right_x.size != right_y.size):
return 0.0, 0.0
if not (np.isfinite(left_x).all() and np.isfinite(left_y).all() and
np.isfinite(right_x).all() and np.isfinite(right_y).all()):
return 0.0, 0.0
if not (np.all(np.diff(left_x) > 0) and np.all(np.diff(right_x) > 0)):
return 0.0, 0.0
left = float(np.interp(lookahead, left_x, left_y))
right = float(np.interp(lookahead, right_x, right_y))
width = right - left
# Same confidence formula as lateral_curv_ext.py's path_offset blend: width-tolerance
# (penalizes implausibly wide/merging-looking detections) combined with per-line
# probability via min() -- a single missing/unreliable line (e.g. no line on the curb
# side, only a center stripe) drags confidence toward 0 on its own.
width_tolerance = float(np.interp(width, _WIDTH_TOLERANCE_BP, _WIDTH_TOLERANCE_V))
std_tolerance = float(np.interp(max(float(stds[1]), float(stds[2])), _STD_TOLERANCE_BP, _STD_TOLERANCE_V)) #StarPilot stopped at 0.3, this fades the std through the table
confidence = min(float(probs[1]), float(probs[2]), width_tolerance, std_tolerance) #confidence is the weakest signal
scale = float(np.clip(np.interp(confidence, _CONFIDENCE_BP, _CONFIDENCE_V), 0.0, 1.0))
center_y = 0.5 * (left + right)
return scale, center_y
except (AttributeError, IndexError, TypeError, ValueError):
return 0.0, 0.0
@@ -1,377 +0,0 @@
"""
BluePilot Ford CAN message builder extensions.
Provides extended versions of the stock fordcan.py message builders with additional
parameters for BluePilot features:
- Dynamic ramp_type and precision_type for lateral control
- Split brake_actuate / precharge_actuate for smoother deceleration
- BlueCruise cluster HUD status, TJA warnings, and hands-free messaging
- Independent left/right lane line logic for LKAS UI
- ICBM button injection for intelligent cruise button management
Each function mirrors the stock fordcan.py function it replaces, with an extended
parameter list. When stock carcontroller.py is refactored, it will import these
functions instead of the stock versions.
"""
from opendbc.car import structs
from opendbc.car.ford.fordcan import CanBus, calculate_lat_ctl2_checksum
HUDControl = structs.CarControl.HUDControl
_BP_LKA_SHADOW_CURVATURE_SCALE = 1e-6 # 1/meter per raw unit, matches ford.h's decode
def create_lka_msg(packer, CAN: CanBus, lat_active: bool, hud_control,
angle_mode_engaged: bool = False, shadow_curvature: float = 0.0):
"""
Creates a CAN message for the Ford LKA Command.
BluePilot extension: accepts lat_active and hud_control parameters for future
lane departure warning integration (unused so far).
Also carries angle_mode_engaged + shadow_curvature packed into bits with no DBC signal mapped to
them -- confirmed unused (always 0, no cabana signal) on real F-150 dashcam routes. This message
is one openpilot itself originates every cycle, and ford_tx_hook already reads other fields
(LkaActvStats_D2_Req) directly out of these same bytes synchronously, in the same tx_hook call --
no separate CAN ID, no RX round-trip needed (panda does not self-receive its own TX, confirmed
2026-07-09 investigating a controlsMismatch caused by an earlier dedicated-message design).
Byte layout (bits not covered by any Lane_Assist_Data1 DBC signal):
byte 4 bit 0: angle_mode_engaged
byte 4 bits 1-4: reserved (future bools)
byte 5-6: shadow_curvature (int16, scale 1e-6 1/m)
byte 7: reserved (future value)
Must match the decode in ford.h's FORD_Lane_Assist_Data1 tx_hook check exactly.
Frequency is 33Hz.
"""
addr, dat, bus = packer.make_can_msg("Lane_Assist_Data1", CAN.main, {})
dat = bytearray(dat)
shadow_curvature_raw = int(round(shadow_curvature / _BP_LKA_SHADOW_CURVATURE_SCALE))
shadow_curvature_raw = max(-32768, min(32767, shadow_curvature_raw)) & 0xFFFF
dat[4] |= 1 if angle_mode_engaged else 0
dat[5] = (shadow_curvature_raw >> 8) & 0xFF
dat[6] = shadow_curvature_raw & 0xFF
return addr, bytes(dat), bus
def create_lat_ctl_msg(packer, CAN: CanBus, lat_active: bool, ramp_type: int, precision_type: int,
path_offset: float, path_angle: float, curvature: float, curvature_rate: float):
"""
Creates a CAN message for the Ford TJA/LCA Command (non-CAN FD).
BluePilot extension: dynamic ramp_type and precision_type parameters.
Stock hardcodes ramp_type=0 (Slow) and precision_type=1 (Precise).
Ford lane centering uses a third-order polynomial to describe the road centerline:
c0 (path_offset): lateral offset between vehicle and centerline (positive is right)
c1 (path_angle): heading angle between vehicle and centerline (positive is right)
c2 (curvature): curvature of the centerline (positive is left)
c3 (curvature_rate): rate of change of curvature
Frequency is 20Hz.
"""
values = {
"LatCtlRng_L_Max": 0, # Unknown [0|126] meter
"HandsOffCnfm_B_Rq": 0, # Unknown: 0=Inactive, 1=Active [0|1]
"LatCtl_D_Rq": 1 if lat_active else 0, # Mode: 0=None, 1=ContinuousPathFollowing, 2=InterventionLeft,
# 3=InterventionRight, 4-7=NotUsed [0|7]
"LatCtlRampType_D_Rq": ramp_type, # Ramp speed: 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3]
"LatCtlPrecision_D_Rq": precision_type, # Precision: 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3]
"LatCtlPathOffst_L_Actl": path_offset, # Path offset [-5.12|5.11] meter
"LatCtlPath_An_Actl": path_angle, # Path angle [-0.5|0.5235] radians
"LatCtlCurv_NoRate_Actl": curvature_rate, # Curvature rate [-0.001024|0.00102375] 1/meter^2
"LatCtlCurv_No_Actl": curvature, # Curvature [-0.02|0.02094] 1/meter
}
return packer.make_can_msg("LateralMotionControl", CAN.main, values)
def create_lat_ctl2_msg(packer, CAN: CanBus, mode: int, ramp_type: int, precision_type: int,
path_offset: float, path_angle: float, curvature: float,
curvature_rate: float, counter: int):
"""
Creates a CAN message for the Ford Lane Centering command (CAN FD).
BluePilot extension: dynamic ramp_type and precision_type parameters.
Stock hardcodes ramp_type=0 (Slow) and precision_type=1 (Precise).
This message replaces LateralMotionControl on CAN FD platforms and includes
counter and checksum fields.
Frequency is 20Hz.
"""
values = {
"LatCtl_D2_Rq": mode, # Mode: 0=None, 1=PathFollowingLimitedMode, 2=PathFollowingExtendedMode,
# 3=SafeRampOut, 4-7=NotUsed [0|7]
"LatCtlRampType_D_Rq": ramp_type, # 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3]
"LatCtlPrecision_D_Rq": precision_type, # 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3]
"LatCtlPathOffst_L_Actl": path_offset, # [-5.12|5.11] meter
"LatCtlPath_An_Actl": path_angle, # [-0.5|0.5235] radians
"LatCtlCurv_No_Actl": curvature, # [-0.02|0.02094] 1/meter
"LatCtlCrv_NoRate2_Actl": curvature_rate, # [-0.001024|0.001023] 1/meter^2
"HandsOffCnfm_B_Rq": 0, # 0=Inactive, 1=Active [0|1]
"LatCtlPath_No_Cnt": counter, # [0|15]
"LatCtlPath_No_Cs": 0, # [0|255]
}
# Calculate checksum (reuse stock function)
dat = packer.make_can_msg("LateralMotionControl2", 0, values)[1]
values["LatCtlPath_No_Cs"] = calculate_lat_ctl2_checksum(mode, counter, dat)
return packer.make_can_msg("LateralMotionControl2", CAN.main, values)
def create_acc_msg(packer, CAN: CanBus, long_active: bool, gas: float, accel: float, accel_pred: float,
stopping: bool, brake_actuate: bool, precharge_actuate: bool, v_ego_kph: float):
"""
Creates a CAN message for the Ford ACC Command.
BluePilot extension: split brake control into brake_actuate and precharge_actuate
(each with independent hysteresis thresholds) for smoother deceleration. Also
accepts accel_pred as a parameter instead of hardcoding -5.0.
Precharge engages slightly before full brake for smoother initial decel feel.
Both use configurable hysteresis to avoid binary on/off feel.
Frequency is 50Hz.
"""
values = {
"AccBrkTot_A_Rq": accel, # Brake total accel request: [-20|11.9449] m/s^2
"Cmbb_B_Enbl": 1 if long_active else 0, # Enabled: 0=No, 1=Yes
"AccPrpl_A_Rq": gas, # Acceleration request: [-5|5.23] m/s^2
"AccPrpl_A_Pred": accel_pred, # Predicted accel (from carcontroller, not hardcoded)
"AccResumEnbl_B_Rq": 1 if long_active else 0,
"AccVeh_V_Trg": v_ego_kph, # Target speed: [0|255] km/h
"AccBrkPrchg_B_Rq": 1 if precharge_actuate else 0, # Pre-charge brake request (independent hysteresis)
"AccBrkDecel_B_Rq": 1 if brake_actuate else 0, # Deceleration request (independent hysteresis)
"AccStopStat_B_Rq": 1 if stopping else 0,
}
return packer.make_can_msg("ACCDATA", CAN.main, values)
def create_acc_ui_msg(packer, CAN: CanBus, CP, main_on: bool, enabled: bool, fcw_alert: bool,
standstill: bool, hud_control, stock_values: dict, send_hands_free_msg: bool,
send_ui: bool, send_bars: bool, tja_warn: int, tja_msg: int):
"""
Creates a CAN message for the Ford IPC adaptive cruise, FCW and TJA status.
BluePilot extension: replaces stock show_distance_bars with explicit send_ui,
send_bars, and TJA parameters. Adds BlueCruise status 7 for hands-free cluster
UI. TJA warn/msg are set from DM state computation rather than stock passthrough.
Stock functionality is maintained by passing through unmodified signals.
Frequency is 5Hz.
"""
# Tja_D_Stat: TJA status for cluster display
if enabled:
if hud_control.leftLaneDepart:
status = 3 # ActiveInterventionLeft
elif hud_control.rightLaneDepart:
status = 4 # ActiveInterventionRight
elif send_hands_free_msg:
status = 7 # BlueCruise UI in the cluster
else:
status = 2 # Active
elif main_on:
if hud_control.leftLaneDepart:
status = 5 # ActiveWarningLeft
elif hud_control.rightLaneDepart:
status = 6 # ActiveWarningRight
else:
status = 1 # Standby
elif standstill:
status = 0 # Off
else:
status = 1 # Standby
values = {s: stock_values[s] for s in [
"HaDsply_No_Cs",
"HaDsply_No_Cnt",
"AccStopStat_D_Dsply", # ACC stopped status message
"AccTrgDist2_D_Dsply", # ACC target distance
"AccStopRes_B_Dsply",
# TjaWarn_D_Rq and TjaMsgTxt_D_Dsply are set explicitly below, not passed through
"IaccLamp_D_Rq", # iACC status icon
"AccMsgTxt_D2_Rq", # ACC text
"FcwDeny_B_Dsply", # FCW disabled
"FcwMemStat_B_Actl", # FCW enabled setting
"AccTGap_B_Dsply", # ACC time gap display setting
"CadsAlignIncplt_B_Actl",
"AccFllwMde_B_Dsply", # ACC follow mode display setting
"CadsRadrBlck_B_Actl",
"CmbbPostEvnt_B_Dsply", # AEB event status
"AccStopMde_B_Dsply", # ACC stop mode display setting
"FcwMemSens_D_Actl", # FCW sensitivity setting
"FcwMsgTxt_D_Rq", # FCW text
"AccWarn_D_Dsply", # ACC warning
"FcwVisblWarn_B_Rq", # FCW visible alert
"FcwAudioWarn_B_Rq", # FCW audio alert
"AccTGap_D_Dsply", # ACC time gap
"AccMemEnbl_B_RqDrv", # ACC adaptive/normal setting
"FdaMem_B_Stat", # FDA enabled setting
]}
values.update({
"Tja_D_Stat": status, # TJA status
"TjaWarn_D_Rq": tja_warn, # TJA warning (from DM state, not stock passthrough)
"TjaMsgTxt_D_Dsply": tja_msg, # TJA text (from DM state, not stock passthrough)
})
if CP.openpilotLongitudinalControl:
values.update({
"AccStopStat_D_Dsply": 2 if standstill else 0, # Stopping status text
"AccMsgTxt_D2_Rq": 0, # ACC text
"AccTGap_B_Dsply": 1 if send_bars else 0, # Show time gap control UI
"AccFllwMde_B_Dsply": 1 if hud_control.leadVisible else 0, # Lead indicator
"AccStopMde_B_Dsply": 1 if standstill else 0,
"AccWarn_D_Dsply": 0, # ACC warning
"AccTGap_D_Dsply": hud_control.leadDistanceBars, # Time gap
})
# Forward FCW alert from IPMA
if fcw_alert:
values["FcwVisblWarn_B_Rq"] = 1 # FCW visible alert
values["FcwAudioWarn_B_Rq"] = 1 # FCW audio alert
return packer.make_can_msg("ACCDATA_3", CAN.main, values)
def create_lkas_ui_msg(packer, CAN: CanBus, main_on: bool, enabled: bool, hands: int,
hud_control, stock_values: dict):
"""
Creates a CAN message for the Ford IPC IPMA/LKAS status.
BluePilot extension: replaces stock steer_alert bool with hands int (0-3),
and uses independent left/right lane line status logic.
hands values:
0 = HandsOn
1 = Level1 (warning without chime)
2 = Level2 (warning with chime)
3 = Suppressed
LaActvStats_D_Dsply value table (left \\ right):
Right → | Intervene | Warning | Suppress | Available | None
Intervene | 24 | 19 | 14 | 9 | 4
Warning | 23 | 18 | 13 | 8 | 3
Suppress | 22 | 17 | 12 | 7 | 2
Available | 21 | 16 | 11 | 6 | 1
None | 20 | 15 | 10 | 5 | 0
Stock functionality is maintained by passing through unmodified signals.
Frequency is 1Hz.
"""
lines = 0
if hud_control is not None:
# Determine left lane status independently
if hud_control.leftLaneDepart:
left_status = 4 # Intervene (Yellow)
elif hud_control.leftLaneVisible:
left_status = 1 # Available
else:
left_status = 2 # Suppress
# Determine right lane status independently
if hud_control.rightLaneDepart:
right_status = 20 # Intervene (Yellow)
elif hud_control.rightLaneVisible:
right_status = 5 # Available
else:
right_status = 10 # Suppress
# Combine left and right lane status
lines = left_status + right_status
values = {s: stock_values[s] for s in [
"FeatConfigIpmaActl",
"FeatNoIpmaActl",
"PersIndexIpma_D_Actl",
"AhbcRampingV_D_Rq", # AHB ramping
"LaDenyStats_B_Dsply", # LKAS error
"CamraDefog_B_Req", # Windshield heater?
"CamraStats_D_Dsply", # Camera status
"DasAlrtLvl_D_Dsply", # DAS alert level
"DasStats_D_Dsply", # DAS status
"DasWarn_D_Dsply", # DAS warning
"AhbHiBeam_D_Rq", # AHB status
"Passthru_63",
"Passthru_48",
]}
values.update({
"LaActvStats_D_Dsply": lines, # LKAS status (lane lines) [0|31]
"LaHandsOff_D_Dsply": hands, # 0=HandsOn, 1=Level1 (w/o chime), 2=Level2 (w/ chime), 3=Suppressed
})
return packer.make_can_msg("IPMA_Data", CAN.main, values)
def create_button_msg(packer, bus: int, stock_values: dict, cancel=False, resume=False,
tja_toggle=False, icbm_button=None):
"""
Creates a CAN message for the Ford SCCM buttons/switches.
BluePilot extension: adds icbm_button parameter for Intelligent Cruise Button
Management. When set, the specified CAN signal is set to 1 in the outgoing
message, enabling openpilot to emulate cruise button presses for speed adjustment.
Args:
icbm_button: Optional string signal name (e.g., "CcAslButtnSetIncPress",
"CcAslButtnSetDecPress") for ICBM button injection.
Frequency is 10Hz.
"""
values = {s: stock_values[s] for s in [
"HeadLghtHiFlash_D_Stat", # SCCM Passthrough the remaining buttons
"TurnLghtSwtch_D_Stat", # SCCM Turn signal switch
"WiprFront_D_Stat",
"LghtAmb_D_Sns",
"AccButtnGapDecPress",
"AccButtnGapIncPress",
"AslButtnOnOffCnclPress",
"AslButtnOnOffPress",
"LaSwtchPos_D_Stat",
"CcAslButtnCnclResPress",
"CcAslButtnDeny_B_Actl",
"CcAslButtnIndxDecPress",
"CcAslButtnIndxIncPress",
"CcAslButtnOffCnclPress",
"CcAslButtnOnOffCncl",
"CcAslButtnOnPress",
"CcAslButtnResDecPress",
"CcAslButtnResIncPress",
"CcAslButtnSetDecPress",
"CcAslButtnSetIncPress",
"CcAslButtnSetPress",
"CcButtnOffPress",
"CcButtnOnOffCnclPress",
"CcButtnOnOffPress",
"CcButtnOnPress",
"HeadLghtHiFlash_D_Actl",
"HeadLghtHiOn_B_StatAhb",
"AhbStat_B_Dsply",
"AccButtnGapTogglePress",
"WiprFrontSwtch_D_Stat",
"HeadLghtHiCtrl_D_RqAhb",
]}
values.update({
"CcAslButtnCnclPress": 1 if cancel else 0, # CC cancel button
"CcAsllButtnResPress": 1 if resume else 0, # CC resume button
"TjaButtnOnOffPress": 1 if tja_toggle else 0, # LCA/TJA toggle button
})
# ICBM button support — set the specified button signal to 1
if icbm_button is not None:
values[icbm_button] = 1
return packer.make_can_msg("Steering_Data_FD1", bus, values)
@@ -1,342 +0,0 @@
from opendbc.car import CanBusBase, structs
HUDControl = structs.CarControl.HUDControl
class CanBus(CanBusBase):
def __init__(self, CP=None, fingerprint=None) -> None:
super().__init__(CP, fingerprint)
@property
def main(self) -> int:
return self.offset
@property
def radar(self) -> int:
return self.offset + 1
@property
def camera(self) -> int:
return self.offset + 2
def calculate_lat_ctl2_checksum(mode: int, counter: int, dat: bytearray) -> int:
curvature = (dat[2] << 3) | ((dat[3]) >> 5)
curvature_rate = (dat[6] << 3) | ((dat[7]) >> 5)
path_angle = ((dat[3] & 0x1F) << 6) | ((dat[4]) >> 2)
path_offset = ((dat[4] & 0x3) << 8) | dat[5]
checksum = mode + counter
for sig_val in (curvature, curvature_rate, path_angle, path_offset):
checksum += sig_val + (sig_val >> 8)
return 0xFF - (checksum & 0xFF)
def create_lka_msg(packer, CAN: CanBus):
"""
Creates an empty CAN message for the Ford LKA Command.
This command can apply "Lane Keeping Aid" maneuvers, which are subject to the PSCM lockout.
Frequency is 33Hz.
"""
return packer.make_can_msg("Lane_Assist_Data1", CAN.main, {})
def create_lat_ctl_msg(packer, CAN: CanBus, lat_active: bool, path_offset: float, path_angle: float, curvature: float,
curvature_rate: float):
"""
Creates a CAN message for the Ford TJA/LCA Command.
This command can apply "Lane Centering" maneuvers: continuous lane centering for traffic jam assist and highway
driving. It is not subject to the PSCM lockout.
Ford lane centering command uses a third order polynomial to describe the road centerline. The polynomial is defined
by the following coefficients:
c0: lateral offset between the vehicle and the centerline (positive is right)
c1: heading angle between the vehicle and the centerline (positive is right)
c2: curvature of the centerline (positive is left)
c3: rate of change of curvature of the centerline
As the PSCM combines this information with other sensor data, such as the vehicle's yaw rate and speed, the steering
angle cannot be easily controlled.
The PSCM should be configured to accept TJA/LCA commands before these commands will be processed. This can be done
using tools such as Forscan.
Frequency is 20Hz.
"""
values = {
"LatCtlRng_L_Max": 0, # Unknown [0|126] meter
"HandsOffCnfm_B_Rq": 0, # Unknown: 0=Inactive, 1=Active [0|1]
"LatCtl_D_Rq": 1 if lat_active else 0, # Mode: 0=None, 1=ContinuousPathFollowing, 2=InterventionLeft,
# 3=InterventionRight, 4-7=NotUsed [0|7]
"LatCtlRampType_D_Rq": 0, # Ramp speed: 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3]
# Makes no difference with curvature control
"LatCtlPrecision_D_Rq": 1, # Precision: 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3]
# The stock system always uses comfortable
"LatCtlPathOffst_L_Actl": path_offset, # Path offset [-5.12|5.11] meter
"LatCtlPath_An_Actl": path_angle, # Path angle [-0.5|0.5235] radians
"LatCtlCurv_NoRate_Actl": curvature_rate, # Curvature rate [-0.001024|0.00102375] 1/meter^2
"LatCtlCurv_No_Actl": curvature, # Curvature [-0.02|0.02094] 1/meter
}
return packer.make_can_msg("LateralMotionControl", CAN.main, values)
def create_lat_ctl2_msg(packer, CAN: CanBus, mode: int, path_offset: float, path_angle: float, curvature: float,
curvature_rate: float, counter: int):
"""
Create a CAN message for the new Ford Lane Centering command.
This message is used on the CAN FD platform and replaces the old LateralMotionControl message. It is similar but has
additional signals for a counter and checksum.
Frequency is 20Hz.
"""
values = {
"LatCtl_D2_Rq": mode, # Mode: 0=None, 1=PathFollowingLimitedMode, 2=PathFollowingExtendedMode,
# 3=SafeRampOut, 4-7=NotUsed [0|7]
"LatCtlRampType_D_Rq": 0, # 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3]
"LatCtlPrecision_D_Rq": 1, # 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3]
"LatCtlPathOffst_L_Actl": path_offset, # [-5.12|5.11] meter
"LatCtlPath_An_Actl": path_angle, # [-0.5|0.5235] radians
"LatCtlCurv_No_Actl": curvature, # [-0.02|0.02094] 1/meter
"LatCtlCrv_NoRate2_Actl": curvature_rate, # [-0.001024|0.001023] 1/meter^2
"HandsOffCnfm_B_Rq": 0, # 0=Inactive, 1=Active [0|1]
"LatCtlPath_No_Cnt": counter, # [0|15]
"LatCtlPath_No_Cs": 0, # [0|255]
}
# calculate checksum
dat = packer.make_can_msg("LateralMotionControl2", 0, values)[1]
values["LatCtlPath_No_Cs"] = calculate_lat_ctl2_checksum(mode, counter, dat)
return packer.make_can_msg("LateralMotionControl2", CAN.main, values)
def create_acc_msg(packer, CAN: CanBus, long_active: bool, gas: float, accel: float, stopping: bool, brake_request, v_ego_kph: float):
"""
Creates a CAN message for the Ford ACC Command.
This command can be used to enable ACC, to set the ACC gas/brake/decel values
and to disable ACC.
Frequency is 50Hz.
"""
values = {
"AccBrkTot_A_Rq": accel, # Brake total accel request: [-20|11.9449] m/s^2
"Cmbb_B_Enbl": 1 if long_active else 0, # Enabled: 0=No, 1=Yes
"AccPrpl_A_Rq": gas, # Acceleration request: [-5|5.23] m/s^2
# No observed acceleration seen from this signal alone. During stock system operation, it appears to
# be the raw acceleration request (AccPrpl_A_Rq when positive, AccBrkTot_A_Rq when negative)
"AccPrpl_A_Pred": -5.0, # Acceleration request: [-5|5.23] m/s^2
"AccResumEnbl_B_Rq": 1 if long_active else 0,
# No observed acceleration seen from this signal alone
"AccVeh_V_Trg": v_ego_kph, # Target speed: [0|255] km/h
# TODO: we may be able to improve braking response by utilizing pre-charging better
# When setting these two bits without AccBrkTot_A_Rq, an initial jerk is observed and car may be able to brake temporarily with AccPrpl_A_Rq
"AccBrkPrchg_B_Rq": 1 if brake_request else 0, # Pre-charge brake request: 0=No, 1=Yes
"AccBrkDecel_B_Rq": 1 if brake_request else 0, # Deceleration request: 0=Inactive, 1=Active
"AccStopStat_B_Rq": 1 if stopping else 0,
}
return packer.make_can_msg("ACCDATA", CAN.main, values)
def create_acc_ui_msg(packer, CAN: CanBus, CP, main_on: bool, enabled: bool, fcw_alert: bool, standstill: bool,
show_distance_bars: bool, hud_control, stock_values: dict):
"""
Creates a CAN message for the Ford IPC adaptive cruise, forward collision warning and traffic jam
assist status.
Stock functionality is maintained by passing through unmodified signals.
Frequency is 5Hz.
"""
# Tja_D_Stat
if enabled:
if hud_control.leftLaneDepart:
status = 3 # ActiveInterventionLeft
elif hud_control.rightLaneDepart:
status = 4 # ActiveInterventionRight
else:
status = 2 # Active
elif main_on:
if hud_control.leftLaneDepart:
status = 5 # ActiveWarningLeft
elif hud_control.rightLaneDepart:
status = 6 # ActiveWarningRight
else:
status = 1 # Standby
else:
status = 0 # Off
values = {s: stock_values[s] for s in [
"HaDsply_No_Cs",
"HaDsply_No_Cnt",
"AccStopStat_D_Dsply", # ACC stopped status message
"AccTrgDist2_D_Dsply", # ACC target distance
"AccStopRes_B_Dsply",
"TjaWarn_D_Rq", # TJA warning
"TjaMsgTxt_D_Dsply", # TJA text
"IaccLamp_D_Rq", # iACC status icon
"AccMsgTxt_D2_Rq", # ACC text
"FcwDeny_B_Dsply", # FCW disabled
"FcwMemStat_B_Actl", # FCW enabled setting
"AccTGap_B_Dsply", # ACC time gap display setting
"CadsAlignIncplt_B_Actl",
"AccFllwMde_B_Dsply", # ACC follow mode display setting
"CadsRadrBlck_B_Actl",
"CmbbPostEvnt_B_Dsply", # AEB event status
"AccStopMde_B_Dsply", # ACC stop mode display setting
"FcwMemSens_D_Actl", # FCW sensitivity setting
"FcwMsgTxt_D_Rq", # FCW text
"AccWarn_D_Dsply", # ACC warning
"FcwVisblWarn_B_Rq", # FCW visible alert
"FcwAudioWarn_B_Rq", # FCW audio alert
"AccTGap_D_Dsply", # ACC time gap
"AccMemEnbl_B_RqDrv", # ACC adaptive/normal setting
"FdaMem_B_Stat", # FDA enabled setting
]}
values.update({
"Tja_D_Stat": status, # TJA status
})
if CP.openpilotLongitudinalControl:
values.update({
"AccStopStat_D_Dsply": 2 if standstill else 0, # Stopping status text
"AccMsgTxt_D2_Rq": 0, # ACC text
"AccTGap_B_Dsply": 1 if show_distance_bars else 0, # Show time gap control UI
"AccFllwMde_B_Dsply": 1 if hud_control.leadVisible else 0, # Lead indicator
"AccStopMde_B_Dsply": 1 if standstill else 0,
"AccWarn_D_Dsply": 0, # ACC warning
"AccTGap_D_Dsply": hud_control.leadDistanceBars, # Time gap
})
# Forwards FCW alert from IPMA
if fcw_alert:
values["FcwVisblWarn_B_Rq"] = 1 # FCW visible alert
return packer.make_can_msg("ACCDATA_3", CAN.main, values)
def create_lkas_ui_msg(packer, CAN: CanBus, main_on: bool, enabled: bool, steer_alert: bool, hud_control,
stock_values: dict):
"""
Creates a CAN message for the Ford IPC IPMA/LKAS status.
Show the LKAS status with the "driver assist" lines in the IPC.
Stock functionality is maintained by passing through unmodified signals.
Frequency is 1Hz.
"""
# LaActvStats_D_Dsply
# R Intvn Warn Supprs Avail No
# L
# Intvn 24 19 14 9 4
# Warn 23 18 13 8 3
# Supprs 22 17 12 7 2
# Avail 21 16 11 6 1
# No 20 15 10 5 0
#
# TODO: test suppress state
if enabled:
lines = 0 # NoLeft_NoRight
if hud_control.leftLaneDepart:
lines += 4
elif hud_control.leftLaneVisible:
lines += 1
if hud_control.rightLaneDepart:
lines += 20
elif hud_control.rightLaneVisible:
lines += 5
elif main_on:
lines = 0
else:
if hud_control.leftLaneDepart:
lines = 3 # WarnLeft_NoRight
elif hud_control.rightLaneDepart:
lines = 15 # NoLeft_WarnRight
else:
lines = 30 # LA_Off
hands_on_wheel_dsply = 1 if steer_alert else 0
values = {s: stock_values[s] for s in [
"FeatConfigIpmaActl",
"FeatNoIpmaActl",
"PersIndexIpma_D_Actl",
"AhbcRampingV_D_Rq", # AHB ramping
"LaDenyStats_B_Dsply", # LKAS error
"CamraDefog_B_Req", # Windshield heater?
"CamraStats_D_Dsply", # Camera status
"DasAlrtLvl_D_Dsply", # DAS alert level
"DasStats_D_Dsply", # DAS status
"DasWarn_D_Dsply", # DAS warning
"AhbHiBeam_D_Rq", # AHB status
"Passthru_63",
"Passthru_48",
]}
values.update({
"LaActvStats_D_Dsply": lines, # LKAS status (lines) [0|31]
"LaHandsOff_D_Dsply": hands_on_wheel_dsply, # 0=HandsOn, 1=Level1 (w/o chime), 2=Level2 (w/ chime), 3=Suppressed
})
return packer.make_can_msg("IPMA_Data", CAN.main, values)
def create_button_msg(packer, bus: int, stock_values: dict, cancel=False, resume=False, tja_toggle=False):
"""
Creates a CAN message for the Ford SCCM buttons/switches.
Includes cruise control buttons, turn lights and more.
Frequency is 10Hz.
"""
values = {s: stock_values[s] for s in [
"HeadLghtHiFlash_D_Stat", # SCCM Passthrough the remaining buttons
"TurnLghtSwtch_D_Stat", # SCCM Turn signal switch
"WiprFront_D_Stat",
"LghtAmb_D_Sns",
"AccButtnGapDecPress",
"AccButtnGapIncPress",
"AslButtnOnOffCnclPress",
"AslButtnOnOffPress",
"LaSwtchPos_D_Stat",
"CcAslButtnCnclResPress",
"CcAslButtnDeny_B_Actl",
"CcAslButtnIndxDecPress",
"CcAslButtnIndxIncPress",
"CcAslButtnOffCnclPress",
"CcAslButtnOnOffCncl",
"CcAslButtnOnPress",
"CcAslButtnResDecPress",
"CcAslButtnResIncPress",
"CcAslButtnSetDecPress",
"CcAslButtnSetIncPress",
"CcAslButtnSetPress",
"CcButtnOffPress",
"CcButtnOnOffCnclPress",
"CcButtnOnOffPress",
"CcButtnOnPress",
"HeadLghtHiFlash_D_Actl",
"HeadLghtHiOn_B_StatAhb",
"AhbStat_B_Dsply",
"AccButtnGapTogglePress",
"WiprFrontSwtch_D_Stat",
"HeadLghtHiCtrl_D_RqAhb",
]}
values.update({
"CcAslButtnCnclPress": 1 if cancel else 0, # CC cancel button
"CcAsllButtnResPress": 1 if resume else 0, # CC resume button
"TjaButtnOnOffPress": 1 if tja_toggle else 0, # LCA/TJA toggle button
})
return packer.make_can_msg("Steering_Data_FD1", bus, values)
@@ -1,71 +0,0 @@
"""
BluePilot: shared manual-steering-override ("human turn") detection for Ford lateral control.
Both Ford lateral strategies hand control back to the driver on a sustained manual turn, and the
detection is identical, so it lives here as one class instead of being copied into each mode. What
each mode DOES with the signal differs: curvature-primary (``lateral_curv_ext``) zeroes its command
(reset_steering + post-reset ramp); angle-primary (``lateral_angle_ext``) forces lateral inactive
(mode 0 on the wire) and ramps path_angle back in from zero through its soft ROC on release.
"""
from opendbc.car import DT_CTRL
from opendbc.car.ford.values import CarControllerParams
# Require sustained hands-on AND a large angle (avoids resetting on small wheel nudges in a curve).
HUMAN_TURN_ANGLE_DEG = 45.0
HUMAN_TURN_HOLD_S = 1.5
# When the wheel was ALREADY past HUMAN_TURN_ANGLE_DEG at first contact -- lateral control had it
# turned mid-curve -- the angle condition is pre-satisfied, so a brief corrective nudge would latch
# after only HUMAN_TURN_HOLD_S of light contact and kill steering mid-curve. Require a longer hold
# there before reading it as an intentional takeover. Route 000000bd (2026-07-14) showed the
# discriminator holds on-road: all 7 deliberate turns began with the wheel below the threshold
# (driver wound it up through 45 deg); only mid-maneuver grabs/nudges began beyond it.
HUMAN_TURN_HOLD_PRETURNED_S = 3.0
_STEER_DT = CarControllerParams.STEER_STEP * DT_CTRL # 20 Hz lateral tick
class HumanTurnDetector:
"""Latches ``active`` once the driver holds real steering pressure AND ``|wheel angle|`` >
``HUMAN_TURN_ANGLE_DEG`` continuously for ``HUMAN_TURN_HOLD_S`` — long enough to tell an
intentional turn from a brief nudge. ``just_released`` pulses True on the first frame after the
override clears, so a mode can re-seed its command as it re-engages.
Call ``update`` once per lateral tick while control is active. Modes that want the timer to zero
on disengage call ``reset`` on their inactive path; modes that want it to persist simply stop
calling ``update`` (the timer holds its value).
"""
def __init__(self):
self.hold_timer_s = 0.0
self.active = False
self._active_last = False
self._pressed_last = False
self._press_started_preturned = False
def update(self, enabled: bool, steering_pressed: bool, steering_angle_deg: float) -> bool:
self._active_last = self.active
# Was the wheel already past the angle threshold when this press began? If so the driver is
# touching a wheel that lateral control turned (mid-curve nudge), not driving a turn -- hold
# the longer HUMAN_TURN_HOLD_PRETURNED_S before latching.
if steering_pressed and not self._pressed_last:
self._press_started_preturned = abs(steering_angle_deg) > HUMAN_TURN_ANGLE_DEG
self._pressed_last = steering_pressed
if not enabled:
self.hold_timer_s = 0.0
elif steering_pressed and abs(steering_angle_deg) > HUMAN_TURN_ANGLE_DEG:
self.hold_timer_s += _STEER_DT
else:
self.hold_timer_s = 0.0
hold_req = HUMAN_TURN_HOLD_PRETURNED_S if self._press_started_preturned else HUMAN_TURN_HOLD_S
self.active = self.hold_timer_s >= hold_req
return self.active
@property
def just_released(self) -> bool:
return self._active_last and not self.active
def reset(self) -> None:
self.hold_timer_s = 0.0
self.active = False
self._active_last = False
self._pressed_last = False
self._press_started_preturned = False
@@ -1,37 +0,0 @@
{
"angle": {
"origin": "opendbc_repo/opendbc/sunnypilot/car/ford/lateral_angle_ext.py",
"sha256": "18fa5290edade52137c7c6705a2eb2d42c94d39d1fa649eae06b7828c68207b3",
"revision": "e1d051d7ba270261b4455068bd68f1a58db15a4a"
},
"human": {
"origin": "opendbc_repo/opendbc/sunnypilot/car/ford/human_turn.py",
"sha256": "82735e348c5ce3e7b2c6a28ea3e298cca71ae8867433c270f7b54a9ee315ef37",
"revision": "e1d051d7ba270261b4455068bd68f1a58db15a4a"
},
"builders": {
"origin": "opendbc_repo/opendbc/sunnypilot/car/ford/fordcan_ext.py",
"sha256": "65ad2114db7210371fc5de06de962c0e29ce396a651c1dd2d58b07a80555a1bf",
"revision": "e1d051d7ba270261b4455068bd68f1a58db15a4a"
},
"fordcan": {
"origin": "opendbc_repo/opendbc/car/ford/fordcan.py",
"sha256": "0c35b8663afcaa3a58ab028a56f076c2c9f2f0296416c0abaf5905c674672956",
"revision": "e1d051d7ba270261b4455068bd68f1a58db15a4a"
},
"values": {
"origin": "opendbc_repo/opendbc/sunnypilot/car/ford/values_ext.py",
"sha256": "74802d82314eaa27eb610810d2815d3da77003c4a1c62e0437e57e785ba25f52",
"revision": "e1d051d7ba270261b4455068bd68f1a58db15a4a"
},
"starpilot_curvature_controller": {
"origin": "opendbc_repo/opendbc/car/ford/carcontroller.py",
"sha256": "c131d1d5f4231004e00f46968988b5d5f379ec22ef8c44d7f67ded8464344855",
"revision": "d2ae98eb3959c88011ed537e7cbc033d91fd3efa"
},
"bpdev_trim": {
"origin": "opendbc_repo/opendbc/sunnypilot/car/ford/lane_center_trim.py",
"sha256": "1affc0abeaeeabaefca4741b0d75d8f249590c157340c5a9760a395d28f0a621",
"revision": "e22afa6be9b881fa784c92ebb316db47728a3d81"
}
}
@@ -1,329 +0,0 @@
import math
import numpy as np
from opendbc.can import CANPacker
from opendbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, DT_CTRL, apply_hysteresis, structs
from opendbc.car.ford import fordcan
from opendbc.car.ford.manual_turn import ManualTurnLatch
from opendbc.car.ford.aol import qualified as qualified_aol
from opendbc.car.ford.stock_cruise import FordStockCruiseButton, qualified as stock_switch_qualified
from opendbc.car.ford.curvature_preview import blend_curvature
from opendbc.car.ford import mache_can
from opendbc.car.ford.extended_classic_can import create_extended_classic_lat_ctl_msg
from opendbc.car.ford.classic_lateral import create_controller as create_classic_controller, bounded_command as classic_bounded_command
from opendbc.car.ford.generic_canfd_can import create_extended_canfd_msg
from opendbc.car.ford.generic_canfd_lateral import (
GenericCanfdLateralController, qualified as generic_canfd_qualified, bounded_command as generic_canfd_bounded_command,
)
from opendbc.car.ford.mache_lateral import MachELateralController, FordLateralResult, bounded_command, qualified as mache_qualified
from opendbc.car.ford.values import CarControllerParams, FordFlags, FordSafetyFlags, CAR
from opendbc.car.interfaces import CarControllerBase, V_CRUISE_MAX
LongCtrlState = structs.CarControl.Actuators.LongControlState
VisualAlert = structs.CarControl.HUDControl.VisualAlert
def anti_overshoot(apply_curvature, apply_curvature_last, v_ego):
diff = 0.1
tau = 5 # 5s smooths over the overshoot
dt = DT_CTRL * CarControllerParams.STEER_STEP
alpha = 1 - np.exp(-dt / tau)
lataccel = apply_curvature * (v_ego ** 2)
last_lataccel = apply_curvature_last * (v_ego ** 2)
last_lataccel = apply_hysteresis(lataccel, last_lataccel, diff)
last_lataccel = alpha * lataccel + (1 - alpha) * last_lataccel
output_curvature = last_lataccel / (max(v_ego, 1) ** 2)
return float(np.interp(v_ego, [5, 10], [apply_curvature, output_curvature]))
def apply_creep_compensation(accel: float, v_ego: float, car_fingerprint: str, *, standstill: bool, stopping: bool) -> float:
if car_fingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 and not (standstill and stopping):
return accel
creep_accel = np.interp(v_ego, [1., 3.], [0.6, 0.])
creep_accel = np.interp(accel, [0., 0.2], [creep_accel, 0.])
accel -= creep_accel
return float(accel)
def apply_brake_rate_limit(accel: float, gas: float, accel_last: float, v_ego: float, car_fingerprint: str,
*, stopping: bool, standstill: bool, fcw: bool) -> tuple[float, float]:
mild_mach_e_braking = (
car_fingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 and
-1.0 < gas < 0.0 and v_ego >= 5.0 and not (stopping or standstill or fcw)
)
if mild_mach_e_braking:
accel = max(accel, min(accel_last, 0.0) - 2.0 * CarControllerParams.ACC_CONTROL_STEP * DT_CTRL)
gas = accel
else:
accel = max(accel, accel_last - 3.5 * CarControllerParams.ACC_CONTROL_STEP * DT_CTRL)
return float(accel), float(gas)
class CarController(CarControllerBase):
def __init__(self, dbc_names, CP):
super().__init__(dbc_names, CP)
self.packer = CANPacker(dbc_names[Bus.pt])
self.CAN = fordcan.CanBus(CP)
self.manual_turn = ManualTurnLatch() if CP.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 and not CP.flags & FordFlags.LKA_STEERING else None
self.stock_cruise_button = FordStockCruiseButton()
self.manual_turn_inputs = None
self.mache_lateral = MachELateralController(CP) if mache_qualified(CP) else None
self.mache_extended_announced = False
self.generic_canfd = generic_canfd_qualified(CP)
self.classic_lateral = GenericCanfdLateralController(CP) if self.generic_canfd else create_classic_controller(CP)
self.classic_extended_announced = False
self.classic_profile = any(s.safetyModel == structs.CarParams.SafetyModel.ford and
s.safetyParam & (FordSafetyFlags.CLASSIC_EXTENDED | FordSafetyFlags.GENERIC_CANFD_EXTENDED) for s in CP.safetyConfigs)
self.mache_profile = any(s.safetyModel == structs.CarParams.SafetyModel.ford and
s.safetyParam & FordSafetyFlags.MACH_E_EXTENDED for s in CP.safetyConfigs)
self.apply_curvature_last = 0
self.anti_overshoot_curvature_last = 0
self.accel = 0.0
self.gas = 0.0
self.brake_request = False
self.main_on_last = False
self.lkas_enabled_last = False
self.steer_alert_last = False
self.lead_distance_bars_last = None
self.distance_bar_frame = 0
self.apply_lka_angle_last = 0.
self.apply_lka_curvature_last = 0.
def update(self, CC, CS, now_nanos):
can_sends = []
if (self.mache_lateral is not None or self.classic_lateral is not None) and self.manual_turn_inputs is not None:
self.manual_turn_inputs.update()
self.manual_turn_inputs.apply_blend_settings(self)
actuators = CC.actuators
hud_control = CC.hudControl
main_on = CS.out.cruiseState.available
steer_alert = hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw)
fcw_alert = hud_control.visualAlert == VisualAlert.fcw
### acc buttons ###
stock_cancel = False
stock_resume = False
if stock_switch_qualified(self.CP):
stock_cancel, stock_resume = self.stock_cruise_button.update(
bool(CS.buttons_stock_values["CcAslButtnCnclResPress"]),
CS.out.cruiseState.available, CS.out.cruiseState.enabled)
if CC.cruiseControl.cancel:
can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.camera, CS.buttons_stock_values, cancel=True))
can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.main, CS.buttons_stock_values, cancel=True))
elif (stock_cancel or stock_resume) and (self.frame % CarControllerParams.BUTTONS_STEP) == 0:
can_sends.append(fordcan.create_button_msg(
self.packer, self.CAN.camera, CS.buttons_stock_values, cancel=stock_cancel, resume=stock_resume))
can_sends.append(fordcan.create_button_msg(
self.packer, self.CAN.main, CS.buttons_stock_values, cancel=stock_cancel, resume=stock_resume))
elif CC.cruiseControl.resume and (self.frame % CarControllerParams.BUTTONS_STEP) == 0:
can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.camera, CS.buttons_stock_values, resume=True))
can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.main, CS.buttons_stock_values, resume=True))
# if stock lane centering isn't off, send a button press to toggle it off
# the stock system checks for steering pressed, and eventually disengages cruise control
elif CS.acc_tja_status_stock_values["Tja_D_Stat"] != 0 and (self.frame % CarControllerParams.ACC_UI_STEP) == 0:
can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.camera, CS.buttons_stock_values, tja_toggle=True))
### lateral control ###
# send steer msg at 20Hz
if (self.frame % CarControllerParams.STEER_STEP) == 0 and self.CP.flags & FordFlags.LKA_STEERING:
# Preserve the stock LMC heartbeat without requesting lateral action.
can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, False, 0., 0., 0., 0.,
stock_lmc=CS.lateral_motion_control))
elif (self.frame % CarControllerParams.STEER_STEP) == 0 and self.mache_lateral is not None:
permission = getattr(self.manual_turn_inputs, "assist_permission", None)
self.mache_lateral.set_assist_permission(*(permission() if permission is not None else ((), False)))
inputs = self.manual_turn_inputs.lateral_snapshot(CS.out.vEgoRaw) if self.manual_turn_inputs is not None else None
if inputs is not None:
self.mache_lateral.set_inputs(*inputs)
else:
# Withdraw stale preview while retaining current actuator/manual-turn control.
self.mache_lateral.set_inputs(None, (), 0.2, self.manual_turn_inputs.enabled if self.manual_turn_inputs is not None else True)
previous = self.mache_lateral.curvature_last
previous_path_angle = self.mache_lateral.path_angle_last
demanded = self.mache_lateral.update(CC, CS, actuators) if self.mache_extended_announced else FordLateralResult()
lateral = bounded_command(self.mache_lateral, demanded, previous, CS.out.vEgoRaw, previous_path_angle)
self.mache_lateral_demand = demanded
self.apply_curvature_last = lateral.curvature
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
can_sends.append(mache_can.create_lat_ctl2_msg(
self.packer, self.CAN, int(lateral.active), lateral.ramp_type, lateral.precision_type,
-lateral.curvature, -lateral.curvature_rate, counter, -lateral.path_angle))
elif (self.frame % CarControllerParams.STEER_STEP) == 0 and self.classic_lateral is not None:
inputs = self.manual_turn_inputs.lateral_snapshot(CS.out.vEgoRaw) if self.manual_turn_inputs is not None else None
if inputs is not None:
self.classic_lateral.set_inputs(*inputs)
else:
self.classic_lateral.set_inputs(None, (), 0.2, self.manual_turn_inputs.enabled if self.manual_turn_inputs is not None else True)
previous = self.classic_lateral.curvature_last
demanded = self.classic_lateral.update(CC, CS, actuators) if self.classic_extended_announced else FordLateralResult()
bound = generic_canfd_bounded_command if self.generic_canfd else classic_bounded_command
lateral = bound(self.classic_lateral, demanded, previous, CS.out.vEgoRaw,
-CS.out.yawRate / max(CS.out.vEgoRaw, 0.1))
self.classic_lateral_demand = demanded
self.apply_curvature_last = lateral.curvature
if self.generic_canfd:
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
can_sends.append(create_extended_canfd_msg(
self.packer, self.CAN, lateral.active, lateral.ramp_type, lateral.precision_type,
-lateral.curvature, -lateral.curvature_rate, counter))
else:
can_sends.append(create_extended_classic_lat_ctl_msg(
self.packer, self.CAN, lateral.active, lateral.ramp_type, lateral.precision_type,
-lateral.curvature, -lateral.curvature_rate))
elif (self.frame % CarControllerParams.STEER_STEP) == 0:
lateral_active = CC.latActive and not self.mache_profile and not self.classic_profile
manual_turn = False
if self.manual_turn is not None and self.manual_turn_inputs is not None:
detection_enabled, lane_change, model_ready = self.manual_turn_inputs.update()
manual_turn = self.manual_turn.update(CC, CS, float(actuators.curvature), detection_enabled, lane_change, model_ready)
if manual_turn:
lateral_active = False
self.apply_curvature_last = 0.
# Bronco and some other cars consistently overshoot curv requests
# Apply some deadzone + smoothing convergence to avoid oscillations
if self.CP.carFingerprint in (CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_F_150_MK14):
self.anti_overshoot_curvature_last = anti_overshoot(actuators.curvature, self.anti_overshoot_curvature_last, CS.out.vEgoRaw)
apply_curvature = self.anti_overshoot_curvature_last
else:
apply_curvature = actuators.curvature
# apply rate limits, curvature error limit, and clip to signal range
current_curvature = -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)
if self.manual_turn is not None and self.manual_turn_inputs is not None and lateral_active:
prediction = getattr(self.manual_turn_inputs, "preview_curvature", None)
preview = prediction(CS.out.vEgoRaw) if prediction is not None else None
if preview is not None:
apply_curvature = blend_curvature(apply_curvature, preview, current_curvature)
# No blending at low speed due to lack of torque wind-up and inaccurate current curvature
if CS.out.vEgoRaw > 9:
apply_curvature = float(np.clip(apply_curvature, current_curvature - CarControllerParams.CURVATURE_ERROR,
current_curvature + CarControllerParams.CURVATURE_ERROR))
apply_curvature = CarControllerParams.CURVATURE_LIMITS.apply_limits(apply_curvature, self.apply_curvature_last, CS.out.vEgoRaw,
0., lateral_active, CarControllerParams.STEER_STEP)
self.apply_curvature_last = 0. if manual_turn else apply_curvature
if self.CP.flags & FordFlags.CANFD:
# TODO: extended mode
# Ford uses four individual signals to dictate how to drive to the car. Curvature alone (limited to 0.02 m^-1)
# can actuate the steering for a large portion of any lateral movements. However, in order to get further control on
# steer actuation, the other three signals are necessary. Ford controls vehicles differently than most other makes.
# A detailed explanation on ford control can be found here:
# https://www.f150gen14.com/forum/threads/introducing-bluepilot-a-ford-specific-fork-for-comma3x-openpilot.24241/#post-457706
mode = 1 if lateral_active else 0
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
can_sends.append(fordcan.create_lat_ctl2_msg(self.packer, self.CAN, mode, 0., 0., -self.apply_curvature_last, 0., counter))
else:
can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, lateral_active, 0., 0., -self.apply_curvature_last, 0.))
# send lka msg at 33Hz
if (self.frame % CarControllerParams.LKA_STEP) == 0:
if self.CP.flags & FordFlags.LKA_STEERING:
source_age_ns = now_nanos - CS.lkas_available_ts_nanos
lka_active = (CC.latActive and CS.lkas_available and 0 <= source_age_ns <= 100_000_000 and
(CS.out.cruiseState.enabled or qualified_aol(self.CP, marked_only=True)) and
not CS.out.steerFaultTemporary and
not CS.out.vehicleSensorsInvalid)
if lka_active:
desired_angle = float(np.clip(actuators.steeringAngleDeg - CS.out.steeringAngleDeg, -5.8, 5.8))
self.apply_lka_angle_last = float(np.clip(desired_angle, self.apply_lka_angle_last - 1.,
self.apply_lka_angle_last + 1.))
desired_curvature = -actuators.curvature
current_curvature = CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)
if CS.out.vEgoRaw > 9.:
desired_curvature = float(np.clip(desired_curvature, current_curvature - CarControllerParams.CURVATURE_ERROR,
current_curvature + CarControllerParams.CURVATURE_ERROR))
self.apply_lka_curvature_last = CarControllerParams.LKA_CURVATURE_LIMITS.apply_limits(
desired_curvature, self.apply_lka_curvature_last, CS.out.vEgoRaw, 0., True, CarControllerParams.LKA_STEP)
else:
self.apply_lka_angle_last = 0.
self.apply_lka_curvature_last = 0.
self.apply_curvature_last = -self.apply_lka_curvature_last
direction = 2 if self.apply_lka_angle_last >= 0. else 4
can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN, lka_active, self.apply_lka_angle_last,
direction, self.apply_lka_curvature_last, transit=True))
elif self.mache_lateral is not None:
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN))
self.mache_extended_announced = True
elif self.classic_lateral is not None:
can_sends.append(mache_can.create_lka_msg(self.packer, self.CAN))
self.classic_extended_announced = True
else:
can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN))
### longitudinal control ###
# send acc msg at 50Hz
if self.CP.openpilotLongitudinalControl and (self.frame % CarControllerParams.ACC_CONTROL_STEP) == 0:
accel = actuators.accel
gas = accel
stopping = actuators.longControlState == LongCtrlState.stopping
if CC.longActive:
accel = apply_creep_compensation(accel, CS.out.vEgo, self.CP.carFingerprint,
standstill=CS.out.standstill, stopping=stopping)
accel, gas = apply_brake_rate_limit(
accel, gas, self.accel, CS.out.vEgo, self.CP.carFingerprint,
stopping=stopping, standstill=CS.out.standstill,
fcw=fcw_alert or CS.out.stockFcw or CS.out.stockAeb,
)
accel = float(np.clip(accel, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX))
gas = float(np.clip(gas, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX))
# Both gas and accel are in m/s^2, accel is used solely for braking
if not CC.longActive or gas < CarControllerParams.MIN_GAS:
gas = CarControllerParams.INACTIVE_GAS
# PCM applies pitch compensation to gas/accel, but we need to compensate for the brake/pre-charge bits
accel_due_to_pitch = 0.0
if len(CC.orientationNED) == 3:
accel_due_to_pitch = math.sin(CC.orientationNED[1]) * ACCELERATION_DUE_TO_GRAVITY
accel_pitch_compensated = accel + accel_due_to_pitch
if accel_pitch_compensated > 0.3 or not CC.longActive:
self.brake_request = False
elif accel_pitch_compensated < 0.0:
self.brake_request = True
# TODO: look into using the actuators packet to send the desired speed
can_sends.append(fordcan.create_acc_msg(self.packer, self.CAN, CC.longActive, gas, accel, stopping, self.brake_request, v_ego_kph=V_CRUISE_MAX))
self.accel = accel
self.gas = gas
### ui ###
send_ui = (self.main_on_last != main_on) or (self.lkas_enabled_last != CC.latActive) or (self.steer_alert_last != steer_alert)
# send lkas ui msg at 1Hz or if ui state changes
if (self.frame % CarControllerParams.LKAS_UI_STEP) == 0 or send_ui:
can_sends.append(fordcan.create_lkas_ui_msg(self.packer, self.CAN, main_on, CC.latActive, steer_alert, hud_control, CS.lkas_status_stock_values))
# send acc ui msg at 5Hz or if ui state changes
if hud_control.leadDistanceBars != self.lead_distance_bars_last:
send_ui = True
self.distance_bar_frame = self.frame
if (self.frame % CarControllerParams.ACC_UI_STEP) == 0 or send_ui:
show_distance_bars = self.frame - self.distance_bar_frame < 400
can_sends.append(fordcan.create_acc_ui_msg(self.packer, self.CAN, self.CP, main_on, CC.latActive,
fcw_alert, CS.out.cruiseState.standstill, show_distance_bars,
hud_control, CS.acc_tja_status_stock_values))
self.main_on_last = main_on
self.lkas_enabled_last = CC.latActive
self.steer_alert_last = steer_alert
self.lead_distance_bars_last = hud_control.leadDistanceBars
new_actuators = actuators.as_builder()
new_actuators.curvature = self.apply_curvature_last
if self.CP.flags & FordFlags.LKA_STEERING:
new_actuators.steeringAngleDeg = CS.out.steeringAngleDeg + self.apply_lka_angle_last
new_actuators.accel = self.accel
new_actuators.gas = self.gas
self.frame += 1
return new_actuators, can_sends
@@ -1,125 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from collections import namedtuple
from opendbc.car import structs
from opendbc.car.docs_definitions import CarParts, Device
from opendbc.car.ford.values import CAR
from opendbc.car.lateral import AngleSteeringLimits
ButtonType = structs.CarState.ButtonEvent.Type
Button = namedtuple('Button', ['event_type', 'can_addr', 'can_msg', 'values'])
# Ford cruise control buttons are in the Steering_Data_FD1 message (CAN ID 131)
# These signals are 1-bit flags: 1 = pressed, 0 = not pressed
#
# Note: Some buttons are combo buttons that emit multiple ButtonEvent types:
# - CcAslButtnSetIncPress emits both accelCruise (type 3) and setCruise (type 9)
# - CcAslButtnCnclResPress emits both cancel (type 5) and resumeCruise (type 10)
#
# There is also a separate CcAslButtnSetPress signal for the standalone "Set" button,
# but based on user mapping, setCruise is mapped to the combo button instead.
BUTTONS = [
# Combo button: Set + Increase (emits accelCruise when enabled, setCruise when disabled)
Button(ButtonType.accelCruise, "Steering_Data_FD1", "CcAslButtnSetIncPress", [1]),
Button(ButtonType.setCruise, "Steering_Data_FD1", "CcAslButtnSetIncPress", [1]),
# Combo button: Set + Decrease (emits decelCruise when enabled, setCruise when disabled)
Button(ButtonType.decelCruise, "Steering_Data_FD1", "CcAslButtnSetDecPress", [1]),
Button(ButtonType.setCruise, "Steering_Data_FD1", "CcAslButtnSetDecPress", [1]),
# Combo button: Cancel/Resume (emits cancel when enabled, resumeCruise when disabled)
Button(ButtonType.cancel, "Steering_Data_FD1", "CcAslButtnCnclResPress", [1]),
Button(ButtonType.resumeCruise, "Steering_Data_FD1", "CcAslButtnCnclResPress", [1]),
# Main cruise button (on/off toggle)
Button(ButtonType.mainCruise, "Steering_Data_FD1", "CcButtnOnOffPress", [1]),
]
class FordSafetyFlagsSP:
"""Sunnypilot-level safety flags for Ford.
Carried in CP_SP.safetyParam and delivered to the safety firmware as
current_safety_param_sp (the separate SP uint16, USB control 0xdf) -- NOT the main
safetyConfigs[].safetyParam. ford_init reads it with GET_FLAG(current_safety_param_sp,
...), same pattern as Subaru STOP_AND_GO (subaru_common.h). Plain int constants, not
IntFlag: CP_SP.safetyParam must stay a plain int through capnp serialization in card.
"""
STEER_ANGLE_CURVATURE = 1
# Geometry-table index for the steering-angle curvature measurement, packed into
# CP_SP.safetyParam bits 1-4 when STEER_ANGLE_CURVATURE is set. Must match the
# ford_pinion_geometry table in safety/modes/ford.h row for row (enforced by
# test_ford.py's geometry-consistency test against CarSpecs + calc_slip_factor).
# Index 0 is reserved as invalid: the firmware treats flag-set-but-no-index as feature
# off, so a half-configured param can never select the wrong geometry silently.
# FORD_EDGE_MK2 is deliberately absent: ALT_STEER_ANGLE platforms read a RELATIVE pinion
# angle (SteeringPinion_Data_Alt + learned offset) and lack the absolute measurement
# this feature needs -- the toggle no-ops there and yaw behavior is kept.
FORD_PINION_GEOMETRY_SHIFT = 1
FORD_PINION_GEOMETRY_INDEX = {
CAR.FORD_BRONCO_SPORT_MK1: 1,
CAR.FORD_ESCAPE_MK4: 2,
CAR.FORD_ESCAPE_MK4_5: 3,
CAR.FORD_EXPEDITION_MK4: 4,
CAR.FORD_EXPLORER_MK6: 5,
CAR.FORD_FOCUS_MK4: 6,
CAR.FORD_F_150_LIGHTNING_MK1: 7,
CAR.FORD_F_150_MK14: 8,
CAR.FORD_MAVERICK_MK1: 9,
CAR.FORD_MONDEO_MK5: 10,
CAR.FORD_MUSTANG_MACH_E_MK1: 11,
CAR.FORD_RANGER_MK2: 12,
}
# BluePilot: Max curvature for steering command (m^-1), from DBC file limits
CURVATURE_MAX = 0.02
# BluePilot: Curvature rate limits — 3-point breakpoints for smoother lateral control.
# Upstream opendbc uses 2-point ([5, 25]) with more conservative values.
# These allow higher rates at low speed for responsiveness, lower rates at mid-speed
# for comfort, and very low rates at highway speed for stability.
#
# Control (Python) uses stricter windup than unwind so OP stays inside panda when apply_std
# picks the wrong table vs steer_angle_cmd_checks. Safety firmware uses looser symmetric ROCs
# (former “down” table for both up/down) — see ford.h FORD_LIMITS.
# Tests: test_ford.py ANGLE_RATE_* match ford.h, not the stricter BP_ANGLE_LIMITS up row.
_BP_ANGLE_RATE_UP = ([5, 16, 25], [0.0025, 0.0012, 0.00008])
_BP_ANGLE_RATE_DOWN = ([5, 16, 25], [0.0025, 0.0014, 0.00018])
BP_ANGLE_LIMITS = AngleSteeringLimits(
0.02, # Max curvature for steering command, m^-1
_BP_ANGLE_RATE_UP,
_BP_ANGLE_RATE_DOWN,
)
def apply_bp_device_mount(car_docs, CP):
"""BluePilot: Select comma3 mount type per vehicle.
Most Ford vehicles use the angled mount due to windshield angle.
Vehicles not in this list use the standard mount.
"""
from opendbc.car.ford.values import CAR, CarHarness, FordFlags
harness = CarHarness.ford_q4 if CP.flags & FordFlags.CANFD else CarHarness.ford_q3
if CP.carFingerprint in (
CAR.FORD_BRONCO_SPORT_MK1,
CAR.FORD_MAVERICK_MK1,
CAR.FORD_F_150_MK14,
CAR.FORD_F_150_LIGHTNING_MK1,
CAR.FORD_ESCAPE_MK4_5,
CAR.FORD_MUSTANG_MACH_E_MK1,
CAR.FORD_RANGER_MK2,
CAR.FORD_EDGE_MK2,
):
car_docs.car_parts = CarParts([Device.threex_angled_mount, harness])
else:
car_docs.car_parts = CarParts([Device.threex, harness])
@@ -1,213 +0,0 @@
"""Pure angle host-independent finite inputs and explicit override/blip reset."""
import math
import unittest
from opendbc.bluepilot_lateral.core.angle import AngleController, Inputs, Command
from opendbc.bluepilot_lateral.params import limits
from opendbc.bluepilot_lateral.tests import reference
from opendbc.bluepilot_lateral.hosts.starpilot import CLASSIC, CANFD, TRUCKS
from opendbc.bluepilot_lateral.can.ford import lateral, lka
from opendbc.can import CANPacker
from types import SimpleNamespace
from dataclasses import replace
from opendbc.bluepilot_lateral.core.trim import LaneCenterTrim, LaneGeometry, TrimConfig
from opendbc.bluepilot_lateral.hosts.starpilot import model_geometry
from openpilot.selfdrive.modeld.constants import ModelConstants
class TestAngleCore(unittest.TestCase):
def test_finite_permission_and_inactive_state_clear_pending_history(self):
for value in (math.nan, math.inf, -math.inf):
for field in ("speed", "desired_curvature", "measured_curvature", "steering_angle", "lateral_delay"):
core = AngleController()
inputs = Inputs(active=True, speed=15., measured_curvature=0., desired_curvature=.001)
self.assertEqual(core.step(replace(inputs, **{field: value})), Command())
core = AngleController()
for _ in range(10):
self.assertTrue(core.step(Inputs(True, 15., 0., .001)).active)
self.assertEqual(core.step(Inputs(False, 15., 0., .001)), Command())
self.assertEqual(core.path_last, 0.)
def test_actual_equivalent_jerk_and_cap_both_signs_and_speeds(self):
for speed in (1., 9., 10., 15., 25., 40., 55.):
for direction in (-1, 1):
core = AngleController(low_gain=.95, high_gain=.95)
previous = 0.
for _ in range(60):
command = core.step(Inputs(True, speed, 0., direction * .002))
if command.active:
equivalent = command.path_angle / speed / limits.GAIN_MIN
self.assertLessEqual(abs(equivalent), min(limits.CURVATURE_MAX, limits.LATERAL_ACCEL / max(speed, 1.) ** 2))
self.assertLessEqual(abs(equivalent - previous), limits.LATERAL_JERK / max(speed - 1., 1.) ** 2 * .05)
previous = equivalent
else:
previous = 0.
def test_human_turn_and_release_blips_are_neutral_then_bounded(self):
core = AngleController()
for _ in range(61):
result = core.step(Inputs(True, 15., 0., .001, steering_pressed=True, steering_angle=60.))
self.assertFalse(result.active)
self.assertEqual(result.path_angle, 0.)
core = AngleController()
for _ in range(12):
core.step(Inputs(True, 15., 0., .001, steering_pressed=True))
for _ in range(6):
self.assertEqual(core.step(Inputs(True, 15., 0., .001)), Command())
resumed = core.step(Inputs(True, 15., 0., .001))
self.assertTrue(resumed.active)
self.assertLess(abs(resumed.path_angle), .02)
def test_pinned_bp7_pure_angle_and_actual_control_bytes_with_documented_limits(self):
builders = reference.builders()
bus = SimpleNamespace(main=0)
differences = set()
packer = CANPacker("ford_lincoln_base_pt")
for identity in sorted(CLASSIC | CANFD):
gains = (.95, .95) if identity in TRUCKS else ((1., 1.05) if identity in CANFD else (1., 1.15))
for speed in (5., 15., 25., 40.):
for direction in (-1, 1):
adapted, upstream = AngleController(low_gain=gains[0], high_gain=gains[1]), reference.Angle(identity)
for tick in range(60):
# Tiny tracking curves stay clear of all intentional envelope changes.
desired = direction * .00005 * (1. + .2 * math.sin(tick / 8.))
inputs = Inputs(True, speed, desired, desired,
yaw_predictions=tuple(desired * speed for _ in ModelConstants.T_IDXS),
time_indices=tuple(ModelConstants.T_IDXS), lateral_delay=.15,
lane_change=tick >= 30, lane_direction=1 if direction < 0 else 2)
result = upstream.step(inputs)
command = adapted.step(inputs)
self.assertTrue(command.active)
self.assertAlmostEqual(command.path_angle, result.path_angle, places=12)
self.assertEqual(command.precision, result.precision_type)
fd = identity in CANFD
actual = lateral(packer, 0, command, canfd=fd, counter=tick % 16)
expected = (builders["create_lat_ctl2_msg"](packer, bus, 1, result.ramp_type, result.precision_type,
0., -result.path_angle, 0., 0., tick % 16) if fd else
builders["create_lat_ctl_msg"](packer, bus, True, result.ramp_type, result.precision_type,
0., -result.path_angle, 0., 0.))
self.assertEqual(actual, expected)
old_shadow = builders["create_lka_msg"](packer, bus, True, SimpleNamespace(), True, upstream.controller.bp_kappa_cmd)
new_shadow = lka(packer, 0, command.shadow_curvature, tick % 8)
# bp7 packs an empty body as raw zeros; the strict native contract requires physical zeros.
self.assertEqual(old_shadow[1][:4], b"\x00\x00\x00\x00")
self.assertEqual(new_shadow[1][:4], b"\x00\x80\x08\x00")
differences.update(("physical_neutral_dbc_offsets", "versioned_side_channel"))
# Aggressive entry demonstrates the required stricter native-equivalent derivative/cap.
adapted, upstream = AngleController(low_gain=gains[0], high_gain=gains[1]), reference.Angle(identity)
previous = 0.
for _tick in range(8):
inputs = Inputs(True, speed, 0., direction * .02,
yaw_predictions=tuple(direction * .02 * speed for _ in ModelConstants.T_IDXS),
time_indices=tuple(ModelConstants.T_IDXS), lateral_delay=.15)
result, command = upstream.step(inputs), adapted.step(inputs)
if command.active:
equivalent = command.path_angle / max(speed, .1) / limits.GAIN_MIN
self.assertLessEqual(abs(equivalent), min(limits.CURVATURE_MAX, limits.LATERAL_ACCEL / max(speed, 1.) ** 2))
self.assertLessEqual(abs(equivalent - previous), limits.LATERAL_JERK / max(speed - 1., 1.) ** 2 * .05)
previous = equivalent
if abs(command.path_angle - result.path_angle) > 1e-7:
differences.add("native_cap_jerk_deviation_packing")
else:
previous = 0.
self.assertEqual(differences, {"physical_neutral_dbc_offsets", "versioned_side_channel", "native_cap_jerk_deviation_packing"})
class TestAngleTrim(unittest.TestCase):
@staticmethod
def geometry(direction=1., *, width=3.3, probability=.9):
xs = (0., 10., 40.)
return LaneGeometry(xs, (direction * .3,) * 3, xs, (-width / 2,) * 3,
xs, (width / 2,) * 3, (probability,) * 2, (.1,) * 2, True)
@staticmethod
def model(g):
return SimpleNamespace(position=SimpleNamespace(x=g.position_x, y=g.position_y),
laneLines=[SimpleNamespace(x=(), y=()), SimpleNamespace(x=g.left_x, y=g.left_y),
SimpleNamespace(x=g.right_x, y=g.right_y)],
laneLineProbs=[0., *g.probabilities], laneLineStds=[0., *g.standard_deviations])
def test_disabled_trim_preserves_all_eleven_default_commands_and_wire_bytes(self):
packer = CANPacker("ford_lincoln_base_pt")
for identity in sorted(CLASSIC | CANFD):
low, high = (.95, .95) if identity in TRUCKS else ((1., 1.05) if identity in CANFD else (1., 1.15))
for speed in (5., 15., 25., 40.):
for direction in (-1, 1):
baseline, supplied = AngleController(low_gain=low, high_gain=high), AngleController(low_gain=low, high_gain=high)
self.assertFalse(supplied.trim_config.enabled)
for tick in range(30):
i = Inputs(True, speed, 0., direction * .0002)
a, b = baseline.step(i), supplied.step(replace(i, geometry=self.geometry(direction)))
self.assertEqual(a, b)
self.assertEqual(lateral(packer, 0, a, canfd=identity in CANFD, counter=tick % 16),
lateral(packer, 0, b, canfd=identity in CANFD, counter=tick % 16))
self.assertEqual(lka(packer, 0, a.shadow_curvature, tick % 8), lka(packer, 0, b.shadow_curvature, tick % 8))
self.assertEqual(supplied.trim.correction, 0.)
def test_fresh_geometry_matches_exact_currentdev_trim_and_model_fallback(self):
for speed in (5., 9., 12., 15., 25., 40.):
for direction in (-1, 1):
adapted, upstream = LaneCenterTrim(), reference.trim()()
config = TrimConfig(True, direction * .1, .25)
for tick in range(90):
g = self.geometry(direction, width=3.3 if tick < 30 or tick >= 60 else 1.5,
probability=.9 if tick < 60 else .3)
actual = adapted.update(.0001, speed, g, config, active=True, lane_change=False)
expected = upstream.update(.0001, self.model(g), speed, True, config.offset_m, config.gain, True, False)
self.assertAlmostEqual(actual, expected, places=14)
self.assertAlmostEqual(adapted.correction, upstream.correction, places=14)
projected = model_geometry(self.model(g), fresh=True)
self.assertEqual(projected, g)
self.assertIsNone(model_geometry(self.model(g), fresh=False))
def test_invalid_stale_driver_and_lane_change_suppress_trim_and_reset_history(self):
original = self.geometry()
config = TrimConfig(True, 0., .25)
for geometry in (replace(original, fresh=False), replace(original, position_x=(0., 0., 40.)),
replace(original, position_y=(0., math.nan, 0.)), replace(original, position_y=(0.,)), None):
trim = LaneCenterTrim()
self.assertNotEqual(trim.update(0., 15., original, config, active=True, lane_change=False), 0.)
self.assertEqual(trim.update(.001, 15., geometry, config, active=True, lane_change=False), .001)
self.assertEqual(trim.correction, 0.)
for changes in ({"active": False}, {"lane_change": True}, {"steering_pressed": True}):
trim = LaneCenterTrim()
trim.update(0., 15., original, config, active=True, lane_change=False)
flags = dict(active=True, lane_change=False, steering_pressed=False)
flags.update(changes)
self.assertEqual(trim.update(.001, 15., original, config, **flags), .001)
self.assertEqual(trim.correction, 0.)
for invalid in (TrimConfig(True, math.nan, .25), TrimConfig(True, 0., math.inf), TrimConfig(True, .6, .25)):
self.assertEqual(LaneCenterTrim().update(.001, 15., original, invalid, active=True, lane_change=False), .001)
# Bad lane-only references fall back to the model+offset, not a stale lane correction.
bad = replace(original, probabilities=(math.nan, .9))
without_lines = replace(original, left_x=(), right_x=())
a, b = LaneCenterTrim(), LaneCenterTrim()
self.assertEqual(a.update(0., 15., bad, TrimConfig(True, .1, .25), active=True, lane_change=False),
b.update(0., 15., without_lines, TrimConfig(True, .1, .25), active=True, lane_change=False))
core = AngleController(trim_config=config)
core.step(Inputs(True, 15., 0., 0., geometry=original))
self.assertNotEqual(core.trim.correction, 0.)
self.assertEqual(core.step(Inputs(False, 15., 0., 0., geometry=original)), Command())
self.assertEqual(core.trim.correction, 0.)
def test_enabled_trim_remains_inside_final_curvature_jerk_and_shadow_bounds(self):
for speed in (9., 10., 15., 25., 40.):
for direction in (-1, 1):
core = AngleController(low_gain=.95, high_gain=.95, trim_config=TrimConfig(True, direction * .5, 1.))
previous, previous_trim, previous_active = 0., 0., False
for _tick in range(100):
command = core.step(Inputs(True, speed, 0., direction * .02, geometry=self.geometry(-direction)))
if command.active and previous_active:
self.assertLessEqual(abs(core.trim.correction - previous_trim), .00015 + 1e-12)
previous_trim, previous_active = core.trim.correction, command.active
if command.active:
equivalent = command.path_angle / speed / limits.GAIN_MIN
self.assertLessEqual(abs(equivalent), min(limits.CURVATURE_MAX, limits.LATERAL_ACCEL / max(speed, 1.) ** 2))
self.assertLessEqual(abs(equivalent - previous), limits.LATERAL_JERK / max(speed - 1., 1.) ** 2 * .05)
self.assertLessEqual(abs(command.shadow_curvature), min(limits.CURVATURE_MAX, limits.LATERAL_ACCEL / max(speed, 1.) ** 2))
# Shadow comes from the final path; no independent trim/shadow command can bypass bounds.
ratio = command.path_angle / (speed * command.shadow_curvature) if command.shadow_curvature else 1.
self.assertTrue(limits.GAIN_MIN - 1e-12 <= ratio <= limits.GAIN_MAX + 1e-12)
previous = equivalent
else:
previous = 0.
self.assertEqual(core.trim.correction, 0.)
@@ -1,307 +0,0 @@
"""Public packed angle/native boundaries; uses the runner-selected safety library."""
from opendbc.bluepilot_lateral.core.angle import AngleController, Inputs, Command
from opendbc.bluepilot_lateral.can.ford import lateral, lka, checksum4
from opendbc.safety.tests import common
from opendbc.safety.tests.libsafety import libsafety_py
from opendbc.car.structs import CarParams
from opendbc.can import CANPacker
class TestFordAngleNative(common.SafetyTestBase):
DBC = "ford_lincoln_base_pt"
SAFETY_MODEL = CarParams.SafetyModel.ford
SAFETY_PARAM = 128
def setUp(self):
super().setUp()
self.debug = self.safety.set_safety_hooks(CarParams.SafetyModel.allOutput, 0) == 0
self.safety.set_safety_hooks(self.SAFETY_MODEL, self.SAFETY_PARAM)
self.safety.init_tests()
self.now = 1_000_000
self.lka_count = 0
self.control_count = 0
self.word = self.SAFETY_PARAM
def packet(self, msg):
addr, dat, bus = msg
return libsafety_py.make_CANPacket(addr, bus, dat)
def rx(self, name, values, *, bus=0):
self.assertTrue(self._rx(self.packer.make_can_msg_safety(name, bus, values)))
def warm(self, speed=15.0, curvature=0.0):
self.safety.set_timer(self.now)
for _ in range(6):
self.rx("BrakeSysFeatures", {"Veh_V_ActlBrk": speed * 3.6, "VehVActlBrk_D_Qf": 3})
self.rx("EngVehicleSpThrottle2", {"Veh_V_ActlEng": speed * 3.6, "VehVActlEng_D_Qf": 3})
self.rx("Yaw_Data_FD1", {"VehYaw_W_Actl": curvature * speed, "VehYawWActl_D_Qf": 3})
self.rx("EngVehicleSpThrottle", {"ApedPos_Pc_ActlArb": 0})
self.rx("DesiredTorqBrk", {"VehStop_D_Stat": 0})
self.rx("Steering_Data_FD1", {"CcAslButtnCnclResPress": 0})
if not self.safety.get_controls_allowed():
self.rx("EngBrakeData", {"BpedDrvAppl_D_Actl": 1, "CcStat_D_Actl": 0})
self.rx("EngBrakeData", {"BpedDrvAppl_D_Actl": 1, "CcStat_D_Actl": 5})
self.assertTrue(self.safety.get_controls_allowed())
def reset_word(self, word):
self.safety.set_alternative_experience(0)
self.safety.set_safety_hooks(self.SAFETY_MODEL, word)
self.word = word
self.lka_count = self.control_count = 0
self.now += 1_000_000
self.warm()
def shadow(self, command):
msg = lka(self.packer, 0, command.shadow_curvature, self.lka_count)
self.lka_count = (self.lka_count + 1) % 8
return msg
def control(self, command):
msg = lateral(self.packer, 0, command, canfd=bool(self.word & 2), counter=self.control_count)
self.control_count = (self.control_count + 1) % 16
return msg
def send(self, msg, expected=True):
self.assertEqual(bool(self._tx(self.packet(msg))), expected)
def tick(self, command):
self.now += 50_000
self.warm()
self.send(self.shadow(command))
self.send(self.control(command))
def test_real_packer_physical_neutral_shadow_and_corruptions(self):
packer = CANPacker(self.DBC)
for word in (128, 130):
for direction in (-1, 1):
for fault in ("angle", "curvature", "checksum", "version", "counter", "replay"):
with self.subTest(word=word, direction=direction, fault=fault):
self.reset_word(word)
command = Command(True, direction * 0.005, direction * 0.005 / 15.0)
# Real RX and PCM edges in warm() establish permission, with no allowance setter.
self.assertTrue(self.safety.get_controls_allowed())
good = lka(packer, 0, command.shadow_curvature, 0)
addr, data, bus = good
self.assertEqual((addr, bus), (0x3CA, 0))
self.assertEqual(data[:4], b"\x00\x80\x08\x00")
self.assertEqual(((data[2] & 15) << 8) | data[3], 2048)
self.assertEqual((data[1] << 4) | (data[2] >> 4), 2048)
self.assertEqual(data[7] & 15, 1)
self.assertEqual(data[7] >> 4, checksum4(data))
self.assertEqual(int.from_bytes(data[5:7], "big", signed=True), round(-command.shadow_curvature * 1e6))
self.send(good)
self.send(lateral(packer, 0, command, canfd=bool(word & 2), counter=0))
self.now += 50_000
self.warm()
next_good = lka(packer, 0, command.shadow_curvature, 1)
mutated = bytearray(next_good[1])
if fault == "angle":
mutated[3] ^= 1
elif fault == "curvature":
mutated[1] ^= 1
elif fault == "checksum":
mutated[7] ^= 16
elif fault == "version":
mutated[7] ^= 1
elif fault == "counter":
mutated[4] ^= 4
if fault in ("angle", "curvature", "counter"):
mutated[7] = 1 | (checksum4(mutated) << 4)
bad = good if fault == "replay" else (addr, bytes(mutated), bus)
self.send(bad, False)
self.assertTrue(self.safety.get_controls_allowed())
self.send(lateral(packer, 0, command, canfd=bool(word & 2), counter=1), False)
# A new valid announcement restores the same bounded path without relaxing native checks.
self.now += 50_000
self.warm()
self.send(next_good)
self.send(lateral(packer, 0, command, canfd=bool(word & 2), counter=1))
def test_actual_core_packer_boundary_all_namespaces(self):
words = [128, 129, 130]
if self.debug:
words.append(131)
for word in words:
for direction in (-1, 1):
self.reset_word(word)
core = AngleController(low_gain=0.95, high_gain=0.95)
active_seen = False
for _ in range(40):
command = core.step(Inputs(True, 15.0, 0.0, direction * 0.02))
active_seen |= command.active
self.tick(command)
self.assertTrue(active_seen)
self.tick(Command())
def test_cold_and_stale_shadow_require_new_valid_announcement(self):
command = Command(True, 0.005, 0.005 / 15.0)
self.warm()
self.send(self.control(command), False)
self.send(self.shadow(command))
self.send(self.control(command))
self.now += 60_001
self.warm()
self.send(self.control(command), False)
self.now += 50_000
self.warm()
self.send(self.shadow(command))
self.send(self.control(command))
def test_invalid_shadow_version_checksum_and_dbc_bits_withdraw(self):
command = Command(True, 0.005, 0.005 / 15.0)
for index, mask in ((7, 1), (7, 16), (0, 32), (4, 2), (4, 32)):
self.reset_word(128)
valid = self.shadow(command)
addr, data, bus = valid
mutated = bytearray(data)
mutated[index] ^= mask
if index != 7:
mutated[7] = 1 | (checksum4(mutated) << 4)
self.send((addr, bytes(mutated), bus), False)
self.send(self.control(command), False)
self.send(valid)
self.send(self.control(command))
def test_replay_wrong_bus_and_length_cannot_refresh_shadow(self):
command = Command(True, 0.005, 0.005 / 15.0)
self.warm()
msg = self.shadow(command)
self.send(msg)
self.send(msg, False)
self.send(self.control(command), False)
for bus, size in ((2, 8), (0, 7), (0, 16)):
addr, dat, _ = self.shadow(command)
self.send((addr, (dat + b"\x00" * 8)[:size], bus), False)
self.now += 30_000
self.rx("EngBrakeData", {"BpedDrvAppl_D_Actl": 1, "CcStat_D_Actl": 0})
self.send(self.shadow(Command()))
self.now += 30_000
self.warm()
self.send(self.shadow(command))
self.send(self.control(command))
def test_native_actual_path_cannot_be_laundered_by_shadow(self):
for command in (Command(True, 0.0005, 0.0), Command(True, -0.0005, 0.0), Command(True, 0.5, 0.0), Command(True, -0.01, 0.001),
Command(True, 0.04, 0.002), Command(True, 0.10, 0.005)):
self.reset_word(128)
self.send(self.shadow(command))
self.send(self.control(command), False)
# A rejected command does not seed an excessive subsequent ROC.
self.reset_word(128)
large = Command(True, 0.20, 0.01)
self.send(self.shadow(large))
self.send(self.control(large), False)
self.now += 50_000
self.warm()
small = Command(True, 0.005, 0.005 / 15.0)
self.send(self.shadow(small))
self.send(self.control(small))
def test_permission_and_rx_withdrawals(self):
command = Command(True, 0.005, 0.005 / 15.0)
for source in ("brake", "gas", "cruise", "speed_quality", "yaw_checksum", "stale"):
self.reset_word(128)
self.send(self.shadow(command))
self.send(self.control(command))
self.now += 50_000
self.warm()
self.send(self.shadow(command))
if source == "brake":
self.rx("EngBrakeData", {"BpedDrvAppl_D_Actl": 2, "CcStat_D_Actl": 5})
elif source == "gas":
self.rx("EngVehicleSpThrottle", {"ApedPos_Pc_ActlArb": 50})
elif source == "cruise":
self.rx("EngBrakeData", {"BpedDrvAppl_D_Actl": 1, "CcStat_D_Actl": 3})
elif source == "speed_quality":
self.assertFalse(self._rx(self.packer.make_can_msg_safety("BrakeSysFeatures", 0, {"VehVActlBrk_D_Qf": 0})))
elif source == "yaw_checksum":
packet = self.packer.make_can_msg_safety("Yaw_Data_FD1", 0, {"VehYawWActl_D_Qf": 3})
packet[0].data[4] ^= 1
self.assertFalse(self._rx(packet))
else:
self.now += 200_001
self.safety.set_timer(self.now)
self.send(self.control(command), False)
self.send(self.control(Command()))
def test_fd_bad_crc_counter_and_mode_neutral_recovery(self):
self.reset_word(130)
command = Command(True, 0.005, 0.005 / 15.0)
self.send(self.shadow(command))
good = self.control(command)
addr, dat, bus = good
bad = bytearray(dat)
bad[1] ^= 1
self.send((addr, bytes(bad), bus), False)
self.send(good, False) # bad CRC withdrew shadow
self.now += 50_000
self.warm()
self.send(self.shadow(command))
self.send(self.control(Command())) # valid mode0 resynchronizes skipped counters
self.now += 50_000
self.warm()
self.send(self.shadow(command))
self.send(self.control(command))
self.now += 50_000
self.warm()
self.send(self.shadow(command))
self.control_count = (self.control_count + 1) % 16
self.send(self.control(command), False)
def test_adjacent_profile_version_and_release_long_denials(self):
command = Command(True, 0.005, 0.005 / 15.0)
for word in (0, 2, 4, 8, 10, 12, 18, 32, 66, 132, 134, 136, 160, 194, 256):
self.reset_word(word)
announcement = self.shadow(command)
if word & 128:
self.send(announcement, False)
else:
self._tx(self.packet(announcement)) # Existing neutral metadata is not angle authority.
self.send(self.control(command), False)
self.reset_word(131)
expected = bool(self.debug)
self.send(self.shadow(command), expected)
self.send(self.control(command), expected)
def test_rejected_metadata_does_not_erase_last_accepted_path_and_neutral_resync(self):
self.reset_word(128)
# At25m/s ROC is tight enough that erasing the accepted path would reject a same-path retry.
core = AngleController(low_gain=.95, high_gain=.95)
for _ in range(30):
self.now += 50_000
self.warm(speed=25., curvature=.001)
command = core.step(Inputs(True, 25., -.001, -.001))
self.send(self.shadow(command))
self.send(self.control(command))
self.assertGreater(abs(command.path_angle), .01)
self.now += 50_000
self.warm(speed=25., curvature=.001)
good = self.shadow(command)
addr, data, bus = good
bad = bytearray(data)
bad[7] ^= 16
self.send((addr, bytes(bad), bus), False)
self.send(good)
self.send(self.control(command))
self.now += 50_000
self.warm(speed=25., curvature=.001)
self.send(self.control(Command()))
self.now += 50_000
self.warm(speed=25., curvature=.001)
recovery = AngleController(low_gain=.95, high_gain=.95).step(Inputs(True, 25., -.001, -.001))
self.send(self.shadow(recovery))
self.send(self.control(recovery))
def test_rt_rate_denial_expires_and_neutral_can_recover(self):
self.reset_word(130)
for _ in range(8):
self.send(self.control(Command()))
self.send(self.control(Command()), False)
self.now += 250_000
self.warm()
self.send(self.control(Command()))
self.now += 50_000
self.warm()
command = Command(True, .005, .005 / 15.)
self.send(self.shadow(command))
self.send(self.control(command))
@@ -1,7 +0,0 @@
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.
@@ -1,21 +0,0 @@
# Custom MIT License
Copyright (c) 2024, Haibin Wen, SUNNYPILOT LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to view and modify the Software, subject to the following conditions:
1. **Permission Required**: Permission Required for Commercial, For-Profit, or Closed Source Use: Use of the Software, in whole or in part, for any commercial purposes, for-profit projects, or in closed source projects requires explicit written permission from the original author(s).
2. **Redistribution**: Any redistribution of the Software, modified or unmodified, must retain this license notice and the following acknowledgment:
"This software is licensed under a custom license requiring permission for use."
3. **Visibility**: Any project that uses the Software must visibly mention the following acknowledgment:
"This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use."
4. **No Warranty**: 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.
Contact sunnypilot Support <support@sunnypilot.ai> for permission requests.
---
Haibin Wen, SUNNYPILOT LLC
-14
View File
@@ -61,20 +61,6 @@ class Signal:
checksum_fields: tuple['Signal', ...] = ()
def get_raw_value(self, dat: bytes | bytearray) -> int:
msb_byte = self.msb // 8
lsb_byte = self.lsb // 8
if self.is_little_endian:
first, last = lsb_byte, msb_byte
contiguous = self.size == self.msb - self.lsb + 1
byteorder = "little"
else:
first, last = msb_byte, lsb_byte
contiguous = self.size == 8 * (last - first) + self.msb % 8 - self.lsb % 8 + 1
byteorder = "big"
if contiguous and 0 < self.size and 0 <= first <= last < len(dat):
raw = dat[first] if first == last else int.from_bytes(dat[first:last + 1], byteorder)
return (raw >> (self.lsb % 8)) & ((1 << self.size) - 1)
ret = 0
i = self.msb // 8
bits = self.size
-12
View File
@@ -56,18 +56,6 @@ class CANPacker:
def set_value(msg: bytearray, sig: Signal, ival: int) -> None:
if type(msg) is bytearray and type(sig) is Signal and type(ival) is int:
lsb = getattr(sig, 'lsb', None)
size = getattr(sig, 'size', None)
little_endian = getattr(sig, 'is_little_endian', None)
if type(lsb) is int and type(size) is int and type(little_endian) is bool:
i, shift = lsb // 8, lsb % 8
if 0 < size <= 8 - shift and 0 <= i < len(msg):
field_mask = (1 << size) - 1
mask = field_mask << shift
msg[i] = (msg[i] & ~mask) | ((ival & field_mask) << shift)
return
i = sig.lsb // 8
bits = sig.size
if sig.size < 64:
+2 -2
View File
@@ -204,8 +204,8 @@ class CANParser:
strings = [strings]
for addr in self.addresses:
for values in self.vl_all[addr].values():
values.clear()
for k in self.vl_all[addr]:
self.vl_all[addr][k].clear()
updated_addrs: set[int] = set()
for entry in strings:
@@ -1,186 +0,0 @@
"""Single-byte packing preserves bits and the original fallback semantics."""
import unittest
from opendbc.can.dbc import Signal
from opendbc.can.packer import set_value
def reference_set_value(msg, sig, ival):
# Frozen pre-optimization setter; the private AST checker binds this to source.
i = sig.lsb // 8
bits = sig.size
if sig.size < 64:
ival &= (1 << sig.size) - 1
while 0 <= i < len(msg) and bits > 0:
shift = sig.lsb % 8 if (sig.lsb // 8) == i else 0
size = min(bits, 8 - shift)
mask = ((1 << size) - 1) << shift
msg[i] &= ~mask
msg[i] |= (ival & ((1 << size) - 1)) << shift
bits -= size
ival >>= size
i = i + 1 if sig.is_little_endian else i - 1
def signal(lsb, size, little=True, kind=Signal):
return kind('TEST', lsb, lsb + size - 1, lsb, size, False, 1., 0., little)
def observed(function, factory):
msg, sig, value, events = factory()
try:
result = function(msg, sig, value)
error = None
except Exception as exc:
result, error = None, (type(exc).__name__, str(exc))
return bytes(msg), result, error, events
class TestPackerSingleByte(unittest.TestCase):
def compare(self, factory):
self.assertEqual(observed(set_value, factory), observed(reference_set_value, factory))
def test_contained_fields_preserve_neighbor_bits_both_endiannesses(self):
values = (0, 1, -1, 0x55, 0xAA, 1 << 96, -(1 << 96))
for little in (False, True):
for initial in (0, 0xA5, 255):
for shift in range(8):
for size in range(1, 9 - shift):
for value in values:
with self.subTest(little=little, initial=initial, shift=shift, size=size, value=value):
msg = bytearray((0x3C, initial, 0xC3))
mask = ((1 << size) - 1) << shift
self.assertIsNone(set_value(msg, signal(8 + shift, size, little), value))
self.assertEqual(msg, bytearray((0x3C, (initial & ~mask) | ((value << shift) & mask), 0xC3)))
def test_cross_byte_wide_zero_negative_and_clipped_geometry(self):
geometry = ((0, 0), (0, -1), (-1, 1), (-8, 8), (8, 1), (24, 8),
(0, 9), (7, 2), (4, 16), (0, 63), (0, 64), (7, 65), (16, 128))
for little in (False, True):
for lsb, size in geometry:
for length in (0, 1, 3, 17):
for value in (-1, 0, 0x55, 1 << 130, -(1 << 130)):
with self.subTest(little=little, lsb=lsb, size=size, length=length, value=value):
self.compare(lambda length=length, lsb=lsb, size=size, little=little, value=value:
(bytearray([0xA5] * length), signal(lsb, size, little), value, []))
def test_mutated_geometry_is_read_for_each_call(self):
sig = signal(0, 1)
for lsb, size, little in ((0, 1, True), (9, 6, False), (7, 9, True), (16, 64, False)):
sig.lsb, sig.size, sig.is_little_endian = lsb, size, little
expected, actual = bytearray(b'\xA5\x5A\x96'), bytearray(b'\xA5\x5A\x96')
reference_set_value(expected, sig, -7)
set_value(actual, sig, -7)
self.assertEqual(actual, expected)
def test_nonexact_buffers_preserve_fallback_writes_and_errors(self):
class Buffer(bytearray):
def __getitem__(self, key):
self.events.append(('get', key))
return super().__getitem__(key)
def __setitem__(self, key, value):
self.events.append(('set', key, value))
return super().__setitem__(key, value)
def tracked():
msg = Buffer(b'\xA5\x5A')
msg.events = []
return msg, signal(2, 3), 7, msg.events
self.compare(tracked)
for container in (bytes, list, lambda data: memoryview(bytearray(data))):
self.compare(lambda container=container: (container(b'\xA5\x5A'), signal(2, 3), 7, []))
def test_signal_subclass_attribute_side_effects_keep_original_order(self):
class TrackedSignal(Signal):
def __getattribute__(self, name):
if name in ('lsb', 'size', 'is_little_endian'):
object.__getattribute__(self, 'events').append(name)
return super().__getattribute__(name)
def factory():
sig = signal(3, 10, False, TrackedSignal)
sig.events = []
return bytearray(b'\xA5\x5A'), sig, -1, sig.events
self.compare(factory)
def test_nonexact_integer_values_and_fields_keep_original_operations(self):
class Integer(int):
def __and__(self, other):
self.events.append(('and', other))
return int(self) & other
def __floordiv__(self, other):
self.events.append(('floordiv', other))
return int(self) // other
def __mod__(self, other):
self.events.append(('mod', other))
return int(self) % other
def __lt__(self, other):
self.events.append(('lt', other))
return int(self) < other
for field in ('value', 'lsb', 'size'):
def factory(field=field):
events = []
number = Integer({'value': 7, 'lsb': 2, 'size': 3}[field])
number.events = events
sig = signal(number if field == 'lsb' else 2, number if field == 'size' else 3)
return bytearray(b'\xA5\x5A'), sig, number if field == 'value' else 7, events
self.compare(factory)
for value in (True, False, 1.5, None):
self.compare(lambda value=value: (bytearray(b'\xA5'), signal(2, 3), value, []))
for lsb, size, little in ((True, 3, True), (2, True, False), (2., 3, True),
(2, 3., True), (2, 3, 1), (2, 3, None)):
self.compare(lambda lsb=lsb, size=size, little=little: (bytearray(b'\xA5\x5A'), signal(lsb, size, little), 7, []))
def test_nonboolean_endian_truth_side_effects_keep_fallback_order(self):
class Truth:
def __init__(self, events):
self.events = events
def __bool__(self):
self.events.append('bool')
return False
def factory():
events = []
return bytearray(b'\xA5\x5A'), signal(8, 9, Truth(events)), 5, events
self.compare(factory)
def test_deleted_signal_fields_preserve_errors_and_unread_endian(self):
for field in ('lsb', 'size', 'is_little_endian'):
for lsb, size, length in ((2, 3, 1), (2, 0, 1), (16, 3, 1), (2, 3, 0)):
def factory(lsb=lsb, size=size, field=field, length=length):
sig = signal(lsb, size)
delattr(sig, field)
return bytearray([0xA5] * length), sig, 7, []
with self.subTest(field=field, lsb=lsb, size=size, length=length):
self.compare(factory)
class DivisionProbe:
def __init__(self, events):
self.events = events
def __floordiv__(self, other):
self.events.append(('floordiv', other))
raise RuntimeError('original lsb operation precedes missing size')
def missing_size():
events = []
sig = signal(2, 3)
sig.lsb = DivisionProbe(events)
del sig.size
return bytearray(b'\xA5'), sig, 7, events
self.compare(missing_size)
if __name__ == '__main__':
unittest.main()
@@ -1,47 +0,0 @@
import unittest
from opendbc.can import CANPacker, CANParser
from opendbc.can.tests import TEST_DBC
class TestParserClearValues(unittest.TestCase):
def test_empty_update_clears_every_list_without_replacing_aliases(self):
parser = CANParser(TEST_DBC, [("CAN_FD_MESSAGE", 10)], 0)
packer = CANPacker(TEST_DBC)
first = packer.make_can_msg("CAN_FD_MESSAGE", 0, {"SIGNED": 12})
second = packer.make_can_msg("CAN_FD_MESSAGE", 0, {"SIGNED": 34})
parser.update([(1_000_000, [first]), (2_000_000, [second])])
values = parser.vl_all["CAN_FD_MESSAGE"]
self.assertIs(values, parser.vl_all[first[0]])
self.assertEqual(values["SIGNED"], [12, 34])
# Extra caller-created entries are also part of the public mapping.
values["extra"] = [56]
aliases = dict(values)
timestamps = dict(parser.ts_nanos["CAN_FD_MESSAGE"])
latest = dict(parser.vl["CAN_FD_MESSAGE"])
self.assertEqual(parser.update([]), set())
self.assertEqual(set(values), set(aliases))
for key, value in aliases.items():
self.assertIs(values[key], value)
self.assertEqual(value, [])
self.assertEqual(parser.ts_nanos["CAN_FD_MESSAGE"], timestamps)
self.assertEqual(parser.vl["CAN_FD_MESSAGE"], latest)
self.assertEqual(parser._last_update_nanos, 2_000_000)
self.assertEqual(parser.last_nonempty_nanos, 2_000_000)
def test_lazily_added_message_and_repeated_empty_updates(self):
parser = CANParser(TEST_DBC, [], 0)
parser.vl["Brake_Status"]
values = parser.vl_all["Brake_Status"]
values["Signal1"] = [1, 2]
alias = values["Signal1"]
for _ in range(3):
self.assertEqual(parser.update([]), set())
self.assertIs(values["Signal1"], alias)
self.assertEqual(alias, [])
alias.append(3)
if __name__ == "__main__":
unittest.main()
@@ -1,81 +0,0 @@
import pytest
from opendbc.can.dbc import Signal
from opendbc.can.parser import MessageState
def make_signal(start, size, little, signed=False):
if little:
msb, lsb = start + size - 1, start
else:
last = start
for _ in range(size - 1):
last = last - 1 if last % 8 else last + 15
msb, lsb = start, last
return Signal("TEST", start, msb, lsb, size, signed, 0.25, -1.0, little)
def raw_reference(dat, start, size, little):
if little:
return sum(((dat[(start + bit) // 8] >> ((start + bit) % 8)) & 1) << bit for bit in range(size))
value, position = 0, start
for _ in range(size):
value = (value << 1) | ((dat[position // 8] >> (position % 8)) & 1)
position = position - 1 if position % 8 else position + 15
return value
@pytest.mark.parametrize("start,size,little", [
(3, 13, True), (7, 64, True), (0, 512, True), (504, 8, True),
(5, 13, False), (0, 64, False), (7, 512, False), (511, 1, False),
])
@pytest.mark.parametrize("signed", [False, True])
@pytest.mark.parametrize("payload", [bytes(64), bytes([255]) * 64, bytes(range(64))])
@pytest.mark.parametrize("container", [bytes, bytearray])
def test_complete_signal_raw_bits(start, size, little, signed, payload, container):
signal = make_signal(start, size, little, signed)
dat = container(payload)
original = dat[:]
assert signal.get_raw_value(dat) == raw_reference(dat, start, size, little)
assert dat == original
@pytest.mark.parametrize("msb,lsb,size,little,payload,expected", [
(23, 0, 24, True, b"\xab\xcd", 0),
(7, 16, 24, False, b"\xab\xcd", 0xABCD00),
(15, 0, 8, True, b"\xab\xcd", 0xCD),
(7, 16, 16, False, b"\xab\xcd\xef", 0xABCD),
(7, 0, 0, True, b"\xff", 0),
(7, 0, -1, False, b"\xff", 0),
(23, 24, 16, False, b"\xab\xcd", 0),
(7, 0, 8, True, b"", 0),
(7, 0, 8, False, b"", 0),
])
@pytest.mark.parametrize("container", [bytes, bytearray])
def test_partial_and_noncanonical_signal_semantics(msb, lsb, size, little, payload, expected, container):
signal = Signal("TEST", msb, msb, lsb, size, False, 1.0, 0.0, little)
assert signal.get_raw_value(container(payload)) == expected
@pytest.mark.parametrize("msb,lsb,size,little", [(7, 4, 2, True), (3, 0, 2, False)])
def test_inconsistent_signal_retains_negative_shift_error(msb, lsb, size, little):
signal = Signal("TEST", msb, msb, lsb, size, False, 1.0, 0.0, little)
with pytest.raises(ValueError, match="negative shift count"):
signal.get_raw_value(b"\xff")
def test_signal_geometry_is_rechecked_after_mutation():
signal = make_signal(0, 16, True)
assert signal.get_raw_value(b"\xab\xcd") == 0xCDAB
signal.size = 8
assert signal.get_raw_value(b"\xab\xcd") == 0xCD
@pytest.mark.parametrize("little", [False, True])
def test_parser_keeps_signed_conversion_and_scaling(little):
signal = make_signal(3, 13, little, signed=True)
dat = bytes([255]) * 8
assert signal.get_raw_value(dat) == (1 << 13) - 1
state = MessageState(address=0x123, name="TEST", size=8, signals=[signal])
assert state.parse(1_000_000, dat)
assert state.vals == [-1.25]
+13 -45
View File
@@ -1,13 +1,12 @@
import os
import time
from opendbc.car import gen_empty_fingerprint, fw_versions
from opendbc.car import gen_empty_fingerprint
from opendbc.car.can_definitions import CanRecvCallable, CanSendCallable
from opendbc.car.carlog import carlog
from opendbc.car.structs import CarParams, CarParamsT
from opendbc.car.fingerprints import eliminate_incompatible_cars, all_legacy_fingerprint_cars
from opendbc.car.fw_versions import (ObdCallback, get_fw_versions, get_fw_versions_ordered,
get_present_ecus, match_fw_to_car)
from opendbc.car.fw_versions import ObdCallback, get_fw_versions_ordered, get_present_ecus, match_fw_to_car
from opendbc.car.mock.values import CAR as MOCK
from opendbc.car.gm.pedal_capability import automatic_bolt_candidate
from opendbc.car.values import BRANDS
@@ -86,18 +85,9 @@ def can_fingerprint(can_recv: CanRecvCallable) -> tuple[str | None, dict[int, di
return automatic_bolt_candidate(car_fingerprint, finger, camera_cruise_states), finger
def _gm_fw_uses_normal_bus0() -> bool:
configured = fw_versions.FW_QUERY_CONFIGS['gm'].requests
effective = [request for brand, _, request in fw_versions.REQUESTS if brand == 'gm']
return bool(configured) and bool(effective) and all(request.bus == 0 for request in (*configured, *effective))
# **** for use live only ****
def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multiplexing: ObdCallback,
cached_params: CarParamsT | None, *, known_gm_bus0: bool = False
) -> tuple[str | None, dict, str, list[CarParams.CarFw], CarParams.FingerprintSource, bool]:
if known_gm_bus0 and not _gm_fw_uses_normal_bus0():
raise ValueError('GM discovery topology no longer uses normal bus 0')
cached_params: CarParamsT | None) -> tuple[str | None, dict, str, list[CarParams.CarFw], CarParams.FingerprintSource, bool]:
fixed_fingerprint = os.environ.get('FINGERPRINT', "")
skip_fw_query = os.environ.get('SKIP_FW_QUERY', False)
disable_fw_cache = os.environ.get('DISABLE_FW_CACHE', False)
@@ -113,26 +103,13 @@ def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_mu
cached = True
else:
carlog.warning("Getting VIN & FW versions")
if known_gm_bus0:
# An explicit registered GM selection already identifies the query family.
# Current GM requests use normal bus 0. Keep live VIN/FW collection and
# the subsequent live CAN layout; scope queries to its declared normal bus.
set_obd_multiplexing(False)
# Preserve the sendcan subscriber connection warmup of the normal OBD path.
warmup_remaining = 0.1 - (time.monotonic() - start_time)
if warmup_remaining > 0:
time.sleep(warmup_remaining)
vin_rx_addr, vin_rx_bus, vin = get_vin(can_recv, can_send, (0,))
car_fw = get_fw_versions(can_recv, can_send, set_obd_multiplexing, query_brand='gm')
ecu_rx_addrs = {(fw.responseAddress, fw.subAddress or None, fw.bus) for fw in car_fw if fw.responseAddress > 0}
else:
# enable OBD multiplexing for VIN query
# NOTE: this takes ~0.1s and is relied on to allow sendcan subscriber to connect in time
set_obd_multiplexing(True)
# VIN query only reliably works through OBDII
vin_rx_addr, vin_rx_bus, vin = get_vin(can_recv, can_send, (0, 1))
ecu_rx_addrs = get_present_ecus(can_recv, can_send, set_obd_multiplexing)
car_fw = get_fw_versions_ordered(can_recv, can_send, set_obd_multiplexing, vin, ecu_rx_addrs)
# enable OBD multiplexing for VIN query
# NOTE: this takes ~0.1s and is relied on to allow sendcan subscriber to connect in time
set_obd_multiplexing(True)
# VIN query only reliably works through OBDII
vin_rx_addr, vin_rx_bus, vin = get_vin(can_recv, can_send, (0, 1))
ecu_rx_addrs = get_present_ecus(can_recv, can_send, set_obd_multiplexing)
car_fw = get_fw_versions_ordered(can_recv, can_send, set_obd_multiplexing, vin, ecu_rx_addrs)
cached = False
exact_fw_match, fw_candidates = match_fw_to_car(car_fw, vin)
@@ -179,22 +156,13 @@ def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_mu
def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multiplexing: ObdCallback, alpha_long_allowed: bool,
is_release: bool, cached_params: CarParamsT | None = None, pre_create_hook=None,
forced_candidate: str | None = None):
if forced_candidate is not None and forced_candidate not in interfaces:
raise ValueError('forced vehicle is not registered')
if forced_candidate is not None and cached_params is not None and cached_params.carFingerprint != forced_candidate:
cached_params = None
explicit_candidate = forced_candidate if forced_candidate is not None else os.environ.get('FINGERPRINT', '')
known_gm_bus0 = (explicit_candidate in interface_names['gm'] and _gm_fw_uses_normal_bus0())
if known_gm_bus0 and cached_params is not None and (
cached_params.brand != 'gm' or cached_params.carFingerprint != explicit_candidate):
cached_params = None
if known_gm_bus0:
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(
can_recv, can_send, set_obd_multiplexing, cached_params, known_gm_bus0=True)
else:
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(can_recv, can_send, set_obd_multiplexing, cached_params)
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(can_recv, can_send, set_obd_multiplexing, cached_params)
if forced_candidate is not None:
if forced_candidate not in interfaces:
raise ValueError('forced vehicle is not registered')
candidate = forced_candidate
source = CarParams.FingerprintSource.fixed
exact_match = True
@@ -21,9 +21,6 @@ class CarController(CarControllerBase):
self.params = CarControllerParams(CP)
def update(self, CC, CS, now_nanos):
_projection = self.learning_projection_observer
if _projection is not None:
_projection.begin(CC.actuators.torque, self.frame)
can_sends = []
lkas_active = CC.latActive and self.lkas_control_bit_prev
@@ -83,16 +80,12 @@ class CarController(CarControllerBase):
# steer torque
new_torque = int(round(CC.actuators.torque * self.params.STEER_MAX))
if _projection is not None:
_projection.input(new_torque, self.params.STEER_MAX, self.apply_torque_last, CS.out.steeringTorqueEps, self.params, limiter='measured')
apply_torque = apply_meas_steer_torque_limits(new_torque, self.apply_torque_last, CS.out.steeringTorqueEps, self.params)
if not lkas_active or not lkas_control_bit:
apply_torque = 0
self.apply_torque_last = apply_torque
can_sends.append(chryslercan.create_lkas_command(self.packer, self.CP, int(apply_torque), lkas_control_bit))
if _projection is not None:
_projection.emit('chrysler.LKAS_COMMAND', apply_torque, lkas_control_bit, can_sends[-1])
self.frame += 1
@@ -100,6 +93,4 @@ class CarController(CarControllerBase):
new_actuators.torque = self.apply_torque_last / self.params.STEER_MAX
new_actuators.torqueOutputCan = self.apply_torque_last
if _projection is not None:
_projection.finish(new_actuators.torque)
return new_actuators, can_sends
+3 -29
View File
@@ -1,8 +1,6 @@
import math
import numpy as np
from opendbc.can import CANPacker
from opendbc.bluepilot_lateral import BluePilotLateral
from opendbc.bluepilot_lateral.hosts.starpilot import qualified as angle_qualified
from opendbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, DT_CTRL, apply_hysteresis, structs
from opendbc.car.ford import fordcan
from opendbc.car.ford.manual_turn import ManualTurnLatch
@@ -17,7 +15,6 @@ from opendbc.car.ford.generic_canfd_lateral import (
GenericCanfdLateralController, qualified as generic_canfd_qualified, bounded_command as generic_canfd_bounded_command,
)
from opendbc.car.ford.mache_lateral import MachELateralController, FordLateralResult, bounded_command, qualified as mache_qualified
from opendbc.car.ford.new_port_curvature import create_controller as create_new_port_curvature, bounded_command as new_port_bounded_command
from opendbc.car.ford.values import CarControllerParams, FordFlags, FordSafetyFlags, CAR
from opendbc.car.interfaces import CarControllerBase, V_CRUISE_MAX
@@ -69,7 +66,6 @@ class CarController(CarControllerBase):
self.packer = CANPacker(dbc_names[Bus.pt])
self.CAN = fordcan.CanBus(CP)
self.bp_lat = BluePilotLateral(CP) if angle_qualified(CP) else None
self.manual_turn = ManualTurnLatch() if CP.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 and not CP.flags & FordFlags.LKA_STEERING else None
self.stock_cruise_button = FordStockCruiseButton()
self.manual_turn_inputs = None
@@ -77,7 +73,6 @@ class CarController(CarControllerBase):
self.mache_extended_announced = False
self.generic_canfd = generic_canfd_qualified(CP)
self.classic_lateral = GenericCanfdLateralController(CP) if self.generic_canfd else create_classic_controller(CP)
self.new_port_curvature = create_new_port_curvature(CP)
self.classic_extended_announced = False
self.classic_profile = any(s.safetyModel == structs.CarParams.SafetyModel.ford and
s.safetyParam & (FordSafetyFlags.CLASSIC_EXTENDED | FordSafetyFlags.GENERIC_CANFD_EXTENDED) for s in CP.safetyConfigs)
@@ -98,7 +93,7 @@ class CarController(CarControllerBase):
def update(self, CC, CS, now_nanos):
can_sends = []
if (self.mache_lateral is not None or self.classic_lateral is not None or self.new_port_curvature is not None) and self.manual_turn_inputs is not None:
if (self.mache_lateral is not None or self.classic_lateral is not None) and self.manual_turn_inputs is not None:
self.manual_turn_inputs.update()
self.manual_turn_inputs.apply_blend_settings(self)
@@ -135,10 +130,7 @@ class CarController(CarControllerBase):
### lateral control ###
# send steer msg at 20Hz
if (self.frame % CarControllerParams.STEER_STEP) == 0 and self.bp_lat is not None:
can_sends += self.bp_lat.step_lateral(CC, CS, self.packer, self.CAN, self.frame, now_nanos)
self.apply_curvature_last = self.bp_lat.apply_curvature_last
elif (self.frame % CarControllerParams.STEER_STEP) == 0 and self.CP.flags & FordFlags.LKA_STEERING:
if (self.frame % CarControllerParams.STEER_STEP) == 0 and self.CP.flags & FordFlags.LKA_STEERING:
# Preserve the stock LMC heartbeat without requesting lateral action.
can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, False, 0., 0., 0., 0.,
stock_lmc=CS.lateral_motion_control))
@@ -183,22 +175,6 @@ class CarController(CarControllerBase):
can_sends.append(create_extended_classic_lat_ctl_msg(
self.packer, self.CAN, lateral.active, lateral.ramp_type, lateral.precision_type,
-lateral.curvature, -lateral.curvature_rate))
elif (self.frame % CarControllerParams.STEER_STEP) == 0 and self.new_port_curvature is not None:
inputs = self.manual_turn_inputs.lateral_snapshot(CS.out.vEgoRaw) if self.manual_turn_inputs is not None else None
self.new_port_curvature.set_inputs(*(inputs if inputs is not None else
(None, (), 0.2, self.manual_turn_inputs.enabled if self.manual_turn_inputs is not None else True)))
previous = self.new_port_curvature.curvature_last
demanded = self.new_port_curvature.update(CC, CS, actuators)
lateral = new_port_bounded_command(self.new_port_curvature, demanded, previous, CS.out.vEgoRaw,
-CS.out.yawRate / max(CS.out.vEgoRaw, 0.1))
self.new_port_curvature_demand = demanded
self.apply_curvature_last = lateral.curvature
if self.CP.flags & FordFlags.CANFD:
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
can_sends.append(fordcan.create_lat_ctl2_msg(self.packer, self.CAN, int(lateral.active), 0., 0.,
-lateral.curvature, 0., counter))
else:
can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, lateral.active, 0., 0., -lateral.curvature, 0.))
elif (self.frame % CarControllerParams.STEER_STEP) == 0:
lateral_active = CC.latActive and not self.mache_profile and not self.classic_profile
manual_turn = False
@@ -246,9 +222,7 @@ class CarController(CarControllerBase):
# send lka msg at 33Hz
if (self.frame % CarControllerParams.LKA_STEP) == 0:
if self.bp_lat is not None:
can_sends += self.bp_lat.step_lka(self.packer, self.CAN, now_nanos)
elif self.CP.flags & FordFlags.LKA_STEERING:
if self.CP.flags & FordFlags.LKA_STEERING:
source_age_ns = now_nanos - CS.lkas_available_ts_nanos
lka_active = (CC.latActive and CS.lkas_available and 0 <= source_age_ns <= 100_000_000 and
(CS.out.cruiseState.enabled or qualified_aol(self.CP, marked_only=True)) and
@@ -6,7 +6,6 @@ THIRD_PARTY_NOTICES.md.
"""
from dataclasses import replace
from typing import Protocol
import numpy as np
@@ -24,7 +23,7 @@ class ExplorerLateralController(FordLateralController):
super().__init__(CP)
self.manual_turn_detected = False
def _manual_turn(self, CC, CS, desired: float, driver_assisting: bool = False) -> bool:
def _manual_turn(self, CC, CS, desired, driver_assisting) -> bool:
self.manual_turn_detected = super()._manual_turn(CC, CS, desired, driver_assisting)
return self.manual_turn_detected
@@ -48,13 +47,7 @@ def qualified(CP) -> bool:
return safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected
class CurvatureOwner(Protocol):
manual_turn_detected: bool
curvature_last: float
path_angle_last: float
def bounded_command(owner: CurvatureOwner, demanded: FordLateralResult,
def bounded_command(owner: ExplorerLateralController, demanded: FordLateralResult,
previous: float, speed: float, measured: float, *, absolute_cap: float | None = None) -> FordLateralResult:
"""Keep classic demand inside the modern common ISO/jerk envelope."""
limits = CarControllerParams.CURVATURE_LIMITS
+1 -1
View File
@@ -35,7 +35,7 @@ class CarInterface(CarInterfaceBase):
ret.radarUnavailable = Bus.radar not in DBC[candidate]
ret.steerControlType = structs.CarParams.SteerControlType.angle
ret.steerActuatorDelay = 0.05 if ret.flags & FordFlags.LKA_STEERING else 0.22
ret.steerActuatorDelay = 0.05 if ret.flags & FordFlags.LKA_STEERING else 0.2
ret.steerLimitTimer = 1.0
ret.steerAtStandstill = True
@@ -51,9 +51,6 @@ MACH_E_TURN_IN_RATE_MIN_DEFICIT = 0.002
MACH_E_UNWIND_LOOKAHEAD_EXTRA = 0.80
MACH_E_UNWIND_FULL_LAG_CURVATURE = 0.0005
MACH_E_UNWIND_PREVIEW_LAG_CURVATURE = 0.002
MACH_E_UNWIND_PREVIEW_HOLD_SECONDS = 0.10
MACH_E_UNWIND_PREVIEW_RISE_TOLERANCE = 0.0002
MACH_E_UNWIND_RELEASE_GAIN = 0.40
MACH_E_DIRECTION_CHANGE_MIN_SPEED = 9.0
MACH_E_DIRECTION_CHANGE_LOOKAHEAD_RAMP_SPEED = 10.0
MACH_E_DIRECTION_CHANGE_LOOKAHEAD_FULL_SPEED = 12.0
@@ -181,7 +178,6 @@ class FordLateralController:
self.path_angle_driver_cooldown = 0.0
self.desired_curvature_last = 0.0
self.turn_in_preview_hold_timer = 0.0
self.unwind_preview_hold_timer = 0.0
self.assist_engagement_retained = False
def set_assist_permission(self, panda_states, healthy: bool):
@@ -329,13 +325,12 @@ class FordLateralController:
precision = 0
return requested, precision
def _unwind_preview(self, desired: float, predicted: float, current: float, v_ego: float,
allow_plateau: bool = False) -> float:
def _unwind_preview(self, desired: float, predicted: float, current: float, v_ego: float) -> float:
if self.CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1:
return predicted
if desired * current <= 0.0 or desired * predicted <= 0.0 or desired * self.desired_curvature_last <= 0.0:
return predicted
if abs(desired) >= abs(self.desired_curvature_last) and not allow_plateau:
if abs(desired) >= abs(self.desired_curvature_last):
return predicted
preview = self._predicted_curvature(v_ego, self._curvature_lookahead() + MACH_E_UNWIND_LOOKAHEAD_EXTRA)
if desired * preview <= 0.0 or abs(preview) >= min(abs(desired), abs(predicted), abs(current)):
@@ -365,50 +360,6 @@ class FordLateralController:
return 0.0
return float(np.interp(v_ego, [2.0, 3.0, 14.0, 15.0], [0.0, 1.0, 1.0, 0.0]))
def _unwind_preview_plateau(self, desired: float, v_ego: float, steering_pressed: bool, lane_change: bool) -> bool:
if (self.CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1 or not self.CP.flags & FordFlags.CANFD or
not 5.0 < v_ego < 15.0 or steering_pressed or lane_change or
desired * self.desired_curvature_last <= 0.0 or
abs(desired) - abs(self.desired_curvature_last) > MACH_E_UNWIND_PREVIEW_RISE_TOLERANCE):
self.unwind_preview_hold_timer = 0.0
return False
if abs(desired) < abs(self.desired_curvature_last):
self.unwind_preview_hold_timer = MACH_E_UNWIND_PREVIEW_HOLD_SECONDS
return False
self.unwind_preview_hold_timer = max(0.0, self.unwind_preview_hold_timer - STEER_DT)
return self.unwind_preview_hold_timer > 1e-9
def _unwind_request(self, desired: float, predicted: float, command_predicted: float, requested: float) -> float:
delta = command_predicted - predicted
if (self.CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1 or not self.CP.flags & FordFlags.CANFD or
not np.isfinite([desired, predicted, command_predicted, requested]).all() or
desired * command_predicted <= 0.0 or desired * delta >= 0.0):
return requested
release_weight = float(np.interp(abs(delta), [0.002, 0.006], [0.0, 1.0]))
blend = float(np.interp(abs(desired), [0.0, 0.001], [self.curvature_blend_low, self.curvature_blend_high]))
preview_authority = float(np.clip(blend / 0.4, 0.0, 1.0))
return float(np.clip(requested + MACH_E_UNWIND_RELEASE_GAIN * preview_authority * release_weight * delta,
min(desired, command_predicted), max(desired, command_predicted)))
def _reversal_followthrough_preview(self, desired: float, predicted: float, current: float, v_ego: float,
steering_pressed: bool, lane_change: bool) -> float:
if (self.CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1 or not self.CP.flags & FordFlags.CANFD or
steering_pressed or lane_change or desired * current >= 0.0):
return predicted
speed_weight = float(np.interp(v_ego, [8.0, 9.0, 14.0, 16.0], [0.0, 1.0, 1.0, 0.0]))
if speed_weight == 0.0:
return predicted
preview = self._predicted_curvature(v_ego, self._curvature_lookahead() + MACH_E_TURN_IN_LOOKAHEAD_EXTRA)
if not np.isfinite(preview) or desired * preview <= 0.0 or np.sign(desired) * (preview - predicted) <= 0.0:
return predicted
preview_weight = float(np.interp(
abs(preview), [MACH_E_DIRECTION_CHANGE_MIN_PREVIEW_CURVATURE, MACH_E_DIRECTION_CHANGE_FULL_PREVIEW_CURVATURE],
[0.0, 1.0]))
lag_weight = float(np.interp(
abs(current), [MACH_E_DIRECTION_CHANGE_MIN_LAG_CURVATURE, MACH_E_DIRECTION_CHANGE_FULL_LAG_CURVATURE],
[0.0, 1.0]))
return predicted + speed_weight * preview_weight * lag_weight * (preview - predicted)
def _turn_in_preview_weight(self, desired: float, preview: float, current: float,
allow_plateau: float = 0.0) -> float:
if self.CP.carFingerprint not in FORD_CONSERVATIVE_PREVIEW_CARS:
@@ -623,7 +574,6 @@ class FordLateralController:
self.path_angle_driver_cooldown = 0.0
self.desired_curvature_last = 0.0
self.turn_in_preview_hold_timer = 0.0
self.unwind_preview_hold_timer = 0.0
return FordLateralResult()
desired = float(actuators.curvature)
@@ -638,7 +588,6 @@ class FordLateralController:
self.path_angle_last = 0.0
self.desired_curvature_last = 0.0
self.turn_in_preview_hold_timer = 0.0
self.unwind_preview_hold_timer = 0.0
return FordLateralResult(active=not (
manual_turn and self.CP.carFingerprint in FORD_MANUAL_TURN_LATCH_CARS))
@@ -696,18 +645,10 @@ class FordLateralController:
if turn_in_weight > 0.0:
turn_in_target = float(np.copysign(max(abs(desired), abs(turn_in_predicted)), desired))
predicted = float(np.interp(turn_in_weight, [0.0, 1.0], [predicted, turn_in_target]))
unwind_plateau = self._unwind_preview_plateau(
desired, v_ego, bool(CS.out.steeringPressed), self._lane_change()[0])
command_predicted = predicted
canfd_mach_e = self.CP.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 and self.CP.flags & FordFlags.CANFD
if (not allow_opposite_preview or canfd_mach_e) and not CS.out.steeringPressed and not self._lane_change()[0]:
command_predicted = self._unwind_preview(desired, predicted, current, v_ego, unwind_plateau)
unwind_selected = command_predicted != predicted
command_predicted = self._reversal_followthrough_preview(
desired, command_predicted, current, v_ego, bool(CS.out.steeringPressed), self._lane_change()[0])
if not allow_opposite_preview and not CS.out.steeringPressed and not self._lane_change()[0]:
command_predicted = self._unwind_preview(desired, predicted, current, v_ego)
requested, precision = self._blend_and_scale(desired, command_predicted, v_ego, current, allow_opposite_preview)
if unwind_selected:
requested = self._unwind_request(desired, predicted, command_predicted, requested)
if v_ego > 9.0:
error_limit = self._curvature_error_limit(
@@ -735,7 +676,7 @@ class FordLateralController:
curvature_rate = 0.0
if (self.CP.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 and self.CP.flags & FordFlags.CANFD and
unwind_selected and command_predicted * curvature_rate > 0.0):
command_predicted != predicted and command_predicted * curvature_rate > 0.0):
curvature_rate = 0.0
curvature_rate = self._turn_in_curvature_rate(
@@ -1,69 +0,0 @@
"""Reuse the current Ford curvature strategy for exact Edge/Mondeo callers.
Only scalar curvature crosses the existing native boundary; no protocol extension.
The reused strategy retains its required provenance and notices in lateral_strategy.py.
"""
from dataclasses import replace
import math
from opendbc.car import structs
from opendbc.car.ford.explorer_lateral import bounded_command as shared_bound
from opendbc.car.ford.lateral_strategy import FordLateralController, FordLateralResult
from opendbc.car.ford.values import CAR, FordFlags
PROFILES = {
CAR.FORD_EDGE_MK2: (int(FordFlags.NEW_PORT | FordFlags.ALT_STEER_ANGLE), 8),
CAR.FORD_MONDEO_MK5: (int(FordFlags.NEW_PORT | FordFlags.CANFD), 10),
}
def qualified(cp):
profile = PROFILES.get(cp.carFingerprint)
if (profile is None or cp.brand != "ford" or cp.passive or cp.dashcamOnly or cp.notCar or
cp.alternativeExperience not in (0, 32) or not cp.pcmCruise or
cp.steerControlType != structs.CarParams.SteerControlType.angle or
len(cp.safetyConfigs) not in (1, 2) or (cp.alternativeExperience == 32 and len(cp.safetyConfigs) != 1)):
return False
flags, word = profile
if (int(cp.flags) & ~int(FordFlags.HAS_BSM)) != flags:
return False
if len(cp.safetyConfigs) == 2 and (cp.safetyConfigs[0].safetyModel != structs.CarParams.SafetyModel.noOutput or
cp.safetyConfigs[0].safetyParam != 0):
return False
safety = cp.safetyConfigs[-1]
return safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == word + int(cp.openpilotLongitudinalControl)
class NewPortCurvatureController(FordLateralController):
def __init__(self, cp):
if not qualified(cp):
raise ValueError("Edge/Mondeo curvature requires its exact current native profile")
super().__init__(cp)
self.manual_turn_detected = False
def _manual_turn(self, CC, CS, desired: float, driver_assisting: bool = False) -> bool:
self.manual_turn_detected = super()._manual_turn(CC, CS, desired, driver_assisting)
return self.manual_turn_detected
def update(self, CC, CS, actuators):
out = CS.out
if (not CC.latActive or not out.canValid or out.canTimeout or out.steerFaultTemporary or
out.steerFaultPermanent or out.vehicleSensorsInvalid or
not all(math.isfinite(value) for value in (out.vEgoRaw, out.yawRate, out.steeringAngleDeg, actuators.curvature))):
self.human_turn.reset()
self.curvature_samples.clear()
self.curvature_last = self.path_angle_last = 0.0
self.manual_turn_detected = False
return FordLateralResult()
return super().update(CC, CS, actuators)
def create_controller(cp):
return NewPortCurvatureController(cp) if qualified(cp) else None
def bounded_command(owner, demanded, previous, speed, measured):
cap = (3.0 - 9.81 * .06) / max(speed, 1.) ** 2 if owner.CP.flags & FordFlags.CANFD else None
command = shared_bound(owner, demanded, previous, speed, measured, absolute_cap=cap)
# These profiles keep their current curvature-only native/codec contract.
return replace(command, curvature_rate=0.0, path_angle=0.0, ramp_type=0, precision_type=1)
@@ -27,9 +27,6 @@ class FordStockCruiseButton:
def qualified(CP) -> bool:
from opendbc.bluepilot_lateral.hosts.starpilot import qualified as angle_qualified
if angle_qualified(CP):
return not CP.openpilotLongitudinalControl
if CP.alternativeExperience == 32 and len(CP.safetyConfigs) != 1:
return False
"""Existing stock profiles with the paired physical-driver button contract."""
@@ -27,7 +27,6 @@ def test_generic_actual_card_publication_reaches_blend_settings(identity, monkey
with OpenpilotPrefix():
saved = Params()
saved.put_bool('OpenpilotEnabledToggle', True, block=True)
saved.put_bool('AlwaysOnLateral', False, block=True)
saved.put_bool('AlphaLongitudinalEnabled', alpha, block=True)
saved.put_bool('IsReleaseBranch', release, block=True)
saved.put_bool('DisableOpenpilotLongitudinal', disable, block=True)
@@ -231,9 +231,7 @@ def test_mach_e_reversal_does_not_reapply_old_direction_at_desired_zero_crossing
result = controller.update(SimpleNamespace(latActive=True), car_state(speed=12.0, curvature=sign * 0.00462),
SimpleNamespace(curvature=-sign * 0.00034))
assert result.active
# Reversal followthrough selects -sign*.005; the existing .006 error
# envelope around sign*.00462 bounds the opposite demand to -sign*.00138.
assert result.curvature == pytest.approx(-sign * 0.00138)
assert result.curvature == pytest.approx(-sign * 0.00034)
assert result.path_angle == 0.0
@pytest.mark.parametrize("sign", (-1, 1))
@@ -417,11 +415,7 @@ def test_final_typed_cp_binds_real_provider_and_freshness():
cp = params(CAR.FORD_MUSTANG_MACH_E_MK1)
assert cp.safetyConfigs[0].safetyParam == (19 if cp.openpilotLongitudinalControl else 18)
ci = CarInterface(cp)
# Native qualification may run with an unwritable home. Keep the real
# messaging prefix and its filesystem lifecycle inside this fixture.
with tempfile.TemporaryDirectory() as directory, \
patch("openpilot.common.prefix.Paths.comma_home", return_value=directory + "/comma"), \
patch("openpilot.common.prefix.Paths.log_root", return_value=directory + "/logs"), OpenpilotPrefix():
with OpenpilotPrefix(), tempfile.TemporaryDirectory() as directory:
preferences = Params(directory)
preferences.put_bool('FordHumanTurnDetection', True, block=True)
configure_controller(ci, preferences)
@@ -747,141 +741,3 @@ def test_mach_e_mild_braking_actual_wire_and_driver_override():
assert values['AccPrpl_A_Rq'] == pytest.approx(expected if active and request > -1. else -5., abs=.01)
assert values['Cmbb_B_Enbl'] == int(active)
assert values['AccResumEnbl_B_Rq'] == int(active)
# Dom Green Garden / Sea Turtles / Waffles actual demand update regressions.
@pytest.mark.parametrize("sign", (-1, 1))
def test_mach_e_unwind_plateau_sequence_and_reset(controller, monkeypatch, sign):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
controller.CP.flags = FordFlags.CANFD
controller.model = SimpleNamespace(orientationRate=SimpleNamespace(z=[0.0] * 33),
meta=SimpleNamespace(laneChangeState=0, laneChangeDirection=0))
monkeypatch.setattr(controller, "_predicted_curvature", lambda v, t: sign * (0.009 if t < 0.5 else 0.004))
controller.desired_curvature_last = sign * 0.012
controller.curvature_last = sign * 0.009
state = car_state(speed=10.0, curvature=sign * 0.009)
CC = SimpleNamespace(latActive=True)
commands = [controller.update(CC, state, SimpleNamespace(curvature=sign * x)).curvature for x in (0.011, 0.011)]
assert commands[1] == pytest.approx(commands[0])
for _ in range(6):
result = controller.update(CC, state, SimpleNamespace(curvature=sign * 0.011))
assert sign * result.curvature > sign * commands[0]
assert controller.unwind_preview_hold_timer == 0.0
controller.unwind_preview_hold_timer = 0.10
state.out.steeringPressed = True
controller.update(CC, state, SimpleNamespace(curvature=sign * 0.011))
assert controller.unwind_preview_hold_timer == 0.0
controller.unwind_preview_hold_timer = 0.10
assert not controller.update(SimpleNamespace(latActive=False), state, SimpleNamespace(curvature=0.0)).active
assert controller.unwind_preview_hold_timer == 0.0
state.out.steeringPressed = False
controller.update(CC, state, SimpleNamespace(curvature=sign * 0.011))
assert controller.unwind_preview_hold_timer == 0.0
@pytest.mark.parametrize("rate_sign", (-1, 1))
def test_mach_e_unwind_plateau_rate_consistency(controller, monkeypatch, rate_sign):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
controller.CP.flags = FordFlags.CANFD
controller.model = SimpleNamespace(orientationRate=SimpleNamespace(z=[0.0] * 33),
meta=SimpleNamespace(laneChangeState=0, laneChangeDirection=0))
controller.desired_curvature_last = 0.010
controller.curvature_last = 0.010
controller.unwind_preview_hold_timer = 0.10
rate = rate_sign * 0.0001
controller.curvature_samples.append(0.010 - rate * STEER_DT * 10.0)
monkeypatch.setattr(controller, "_predicted_curvature", lambda v, t: 0.010 if t < 0.5 else 0.004)
result = controller.update(SimpleNamespace(latActive=True), car_state(speed=10.0, curvature=0.011),
SimpleNamespace(curvature=0.010))
assert result.curvature_rate == pytest.approx(0.0 if rate_sign > 0 else rate)
@pytest.mark.parametrize("manual_turn", (False, True))
def test_mach_e_unwind_plateau_resets_when_stopped_or_manual_turn_latched(controller, manual_turn):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
controller.CP.flags = FordFlags.CANFD
controller.unwind_preview_hold_timer = 0.10
controller.manual_turn_latched = manual_turn
state = car_state(speed=10.0 if manual_turn else 0.0, steering_angle=20.0 if manual_turn else 0.0)
controller.update(SimpleNamespace(latActive=True), state, SimpleNamespace(curvature=0.010))
assert controller.unwind_preview_hold_timer == 0.0
@pytest.mark.parametrize("sign", (-1, 1))
def test_mach_e_unwind_release_update_yields_and_reverses(controller, monkeypatch, sign):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
controller.CP.flags = FordFlags.CANFD
controller.model = SimpleNamespace(orientationRate=SimpleNamespace(z=[0.0] * 33),
meta=SimpleNamespace(laneChangeState=0, laneChangeDirection=0))
monkeypatch.setattr(controller, "_predicted_curvature", lambda v, t: sign * (0.014 if t < 0.5 else 0.004))
controller.desired_curvature_last = sign * 0.016
controller.curvature_last = sign * 0.012
CC = SimpleNamespace(latActive=True)
state = car_state(speed=10.0, curvature=sign * 0.010)
result = controller.update(CC, state, SimpleNamespace(curvature=sign * 0.012))
assert result.curvature == pytest.approx(sign * 0.010)
assert result.path_angle == 0.0
state.out.steeringPressed = True
controller.update(CC, state, SimpleNamespace(curvature=sign * 0.012))
assert controller.unwind_preview_hold_timer == 0.0
state.out.steeringPressed = False
state.out.yawRate = -sign * 0.004 * state.out.vEgoRaw
for _ in range(20):
result = controller.update(CC, state, SimpleNamespace(curvature=-sign * 0.008))
assert sign * result.curvature < 0.0
assert controller.unwind_preview_hold_timer == 0.0
assert not controller.update(SimpleNamespace(latActive=False), state, SimpleNamespace(curvature=0.0)).active
@pytest.mark.parametrize("sign", (-1, 1))
def test_mach_e_reversal_zero_crossing_after_preview_sequence(controller, monkeypatch, sign):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
controller.CP.flags = FordFlags.CANFD
controller.lookahead = 0.4
controller.desired_curvature_last = sign * 0.00155
controller.curvature_last = -sign * 0.00065
monkeypatch.setattr(controller, "_predicted_curvature",
lambda _v, t: sign * 0.0007 if t < 1.0 else -sign * 0.005)
result = controller.update(SimpleNamespace(latActive=True), car_state(speed=12.0, curvature=sign * 0.00462),
SimpleNamespace(curvature=-sign * 0.00034))
assert result.active
assert result.curvature == pytest.approx(-sign * 0.00138)
assert result.path_angle == 0.0
@pytest.mark.parametrize("sign", (-1, 1))
def test_dom_unwind_release_actual_caller_keeps_modern_envelope_and_wire(sign):
import struct
cp = params(CAR.FORD_MUSTANG_MACH_E_MK1)
cc = CarController(DBC[cp.carFingerprint], cp)
cs = CarState(cp)
cs.update(cs.get_can_parsers(cp))
cs.out = structs.CarState(vEgoRaw=10., yawRate=-sign * .10).as_reader()
control = structs.CarControl(latActive=True, enabled=True)
control.actuators.curvature = sign * .012
model = SimpleNamespace(orientationRate=SimpleNamespace(z=[sign * (.14 if i < 5 else .04) for i in range(33)]),
meta=SimpleNamespace(laneChangeState=0, laneChangeDirection=0))
cc.manual_turn_inputs = SimpleNamespace(update=lambda: None, apply_blend_settings=lambda _: None, enabled=True,
lateral_snapshot=lambda _: (model, tuple(i*.1 for i in range(33)), .2, True))
cc.update(control.as_reader(), cs, 0)
cc.mache_lateral.desired_curvature_last = sign * .016
cc.mache_lateral.curvature_last = sign * .012
cc.frame = 5
actual, frames = cc.update(control.as_reader(), cs, 50_000_000)
assert cc.mache_lateral_demand.curvature == pytest.approx(sign * .010)
assert abs(actual.curvature - sign * .012) <= MAX_LATERAL_JERK / 100. * STEER_DT + 1e-12
assert abs(actual.curvature) <= min(.02, MAX_LATERAL_ACCEL / 100.)
assert cc.mache_lateral.curvature_last == cc.apply_curvature_last
# Actuators.curvature is Float32; internal emitted histories are doubles.
assert actual.curvature == struct.unpack("f", struct.pack("f", cc.apply_curvature_last))[0]
parser = CANParser(DBC[cp.carFingerprint][Bus.pt], [('LateralMotionControl2', 20)], cc.CAN.main)
parser.update([(50_000_000, frames)])
fields = parser.vl['LateralMotionControl2']
assert fields['LatCtl_D2_Rq'] == 1
assert fields['LatCtlCurv_No_Actl'] == pytest.approx(-actual.curvature, abs=1.01e-5)
assert fields['LatCtlPath_An_Actl'] == 0.
control.latActive = False
cc.frame = 10
actual, _ = cc.update(control.as_reader(), cs, 100_000_000)
assert actual.curvature == 0.
assert cc.mache_lateral.unwind_preview_hold_timer == 0.
@@ -1,206 +0,0 @@
"""Current-strategy Edge/Mondeo wiring and current native scalar-curvature proof.
Source-only authored cases; runtime must bind actual DEBUG/RELEASE libraries.
"""
import os
from pathlib import Path
import unittest
from unittest.mock import patch
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.starpilot.tests import test_vehicle_preferences as startup
from openpilot.starpilot.lateral.tests import test_lane_runtime as lane
from opendbc.car import gen_empty_fingerprint, structs
from opendbc.car.ford.new_port_curvature import qualified, create_controller
from opendbc.car.ford.tests.test_three_ports import params as factory
from opendbc.car.ford.values import CAR, FordFlags
from opendbc.safety.tests import test_ford_aol as physical
from opendbc.safety.tests.libsafety import libsafety_py
PROFILES = (CAR.FORD_EDGE_MK2, CAR.FORD_MONDEO_MK5)
class TestNewPortCurvature(unittest.TestCase):
def test_exact_existing_factory_profiles_and_no_namespace_or_topology_inference(self):
for identity in PROFILES:
for alpha, release in ((False, False), (True, False), (False, True), (True, True)):
cp = factory(identity, alpha, release)
expected_long = bool(alpha and (identity == CAR.FORD_EDGE_MK2 or not release))
self.assertTrue(cp.pcmCruise)
self.assertEqual(cp.openpilotLongitudinalControl, expected_long)
self.assertEqual(cp.safetyConfigs[-1].safetyParam, (8 if identity == CAR.FORD_EDGE_MK2 else 10) + int(expected_long))
self.assertTrue(qualified(cp))
self.assertIsNotNone(create_controller(cp))
original = cp.to_bytes()
cp.clear_write_flag()
create_controller(cp)
self.assertEqual(cp.to_bytes(), original)
cp.clear_write_flag()
for field, value in (("passive", True), ("dashcamOnly", True), ("notCar", True), ("alternativeExperience", 64),
("pcmCruise", False), ("openpilotLongitudinalControl", not expected_long)):
with structs.CarParams.from_bytes(original) as prior:
changed = prior.as_builder()
setattr(changed, field, value)
self.assertFalse(qualified(changed))
for word in (0, 2, 12, 18, 32, 66, 128, 130, 65535):
with structs.CarParams.from_bytes(original) as prior:
changed = prior.as_builder()
changed.safetyConfigs[-1].safetyParam = word
self.assertIsNone(create_controller(changed))
cp.alternativeExperience = 32
self.assertTrue(qualified(cp))
cp.safetyConfigs = [cp.safetyConfigs[-1], cp.safetyConfigs[-1]]
self.assertFalse(qualified(cp))
self.assertIsNone(create_controller(factory(CAR.FORD_TRANSIT_MK5)))
self.assertIsNone(create_controller(factory(CAR.FORD_BRONCO_SPORT_MK1)))
def test_saved_card_controls_actual_parser_provider_sender_and_native_both_signs(self):
safety = libsafety_py.libsafety
for identity in PROFILES:
for direction in (-1, 1):
with self.subTest(identity=identity, direction=direction), OpenpilotPrefix(), \
patch.dict(os.environ, {"REPLAY": "1", "SIMULATION": "1"}), \
patch("openpilot.selfdrive.controls.controlsd.messaging.PubMaster"):
saved = Params()
helper = startup.TestVehicleStartupPreferences()
helper.setUp()
helper.params = saved
native = physical.TestFordAolDriverIntent()
try:
for key, value in (("OpenpilotEnabledToggle", True), ("SafeMode", False), ("AlwaysOnLateral", False),
("AlphaLongitudinalEnabled", False), ("IsReleaseBranch", False)):
saved.put_bool(key, value, block=True)
observed = gen_empty_fingerprint()
observed[0][0x5A] = 8
observed[2].update({0x3D6: 8, 0x186: 8})
host, constructed, cp = helper.start(identity, key="FordHumanTurnDetection", observed=observed,
capture=lambda ci: (ci.CP.safetyConfigs[-1].safetyParam, ci.CC.new_port_curvature is not None))
self.assertEqual(constructed, [(8 if identity == CAR.FORD_EDGE_MK2 else 10, True)])
self.assertEqual(host.CI.CP.to_dict(), cp.to_dict())
self.assertTrue(qualified(cp))
owner = host.CI.CC.new_port_curvature
inputs = host.CI.CC.manual_turn_inputs
self.assertIsNotNone(inputs)
inputs.sm = messaging.SubMaster(["modelV2", "lateralDelay"])
host.sm = messaging.SubMaster(["carControl"])
controls = Controls()
native.setUp()
native.word, native.tick = cp.safetyConfigs[-1].safetyParam, 0
safety.set_alternative_experience(0)
self.assertEqual(safety.set_safety_hooks(structs.CarParams.SafetyModel.ford, native.word), 0)
safety.init_tests()
received, sent, active_curvatures = [], [], []
def rx(name, values, *, native=native, received=received):
self.assertLessEqual(set(values), set(native.packer.dbc.name_to_msg[name].sigs))
frame = native.packer.make_can_msg(name, 0, values)
addr, data, bus = frame
self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, data)))
received.append(frame)
host.publish_sendcan = lambda frames, valid=True, sent=sent: sent.extend(frames) if valid else self.fail("Invalid Card trace")
host.ci_initialized = True
host.CI.update([])
saw_human_zero = saw_fault_zero = saw_stale_model = saw_rate_neutral = False
for tick in range(600):
received.clear()
with patch.object(native, "rx", rx):
native.pump(1, main=0 if tick < 30 else 4, speed=15.)
now = (1_000_000 + native.tick * 10_000) * 1000
pressed = 100 <= tick < 430
fault = 460 <= tick < 480
extra: list[tuple[str, int, dict[str, float]]] = [("Cluster_Info1_FD1", 0, {"AccEnbl_B_RqDrv": 1}),
("BodyInfo_3_FD1", 0, {}), ("RCMStatusMessage2_FD1", 0, {"FirstRowBuckleDriver": 1}),
("INSTRUMENT_PANEL", 0, {}), ("IPMA_Data", 2, {}), ("ACCDATA", 2, {}),
("ACCDATA_2", 2, {}), ("ACCDATA_3", 2, {}),
("EPAS_INFO", 0, {"SteeringColumnTorque": 2 if pressed else 0, "EPAS_Failure": 2 if fault else 0})]
if cp.flags & FordFlags.ALT_STEER_ANGLE:
extra += [("ParkAid_Data", 0, {"ExtSteeringAngleReq2": 60 if pressed else 0, "EPASExtAngleStatReq": 0, "ApaSys_D_Stat": 0}),
("SteeringPinion_Data_Alt", 0, {"StePinRelInit_An_Sns": 60 if pressed else 0})]
else:
extra += [("SteeringPinion_Data", 0, {"StePinComp_An_Est": 60 if pressed else 0, "StePinCompAnEst_D_Qf": 3})]
host_sources = [native.packer.make_can_msg(name, bus, values) for name, bus, values in extra]
for addr, data, bus in host_sources:
self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, data)))
received.extend(host_sources)
state = host.CI.update([(now, list(received))])
if tick < 40:
continue
self.assertTrue(state.canValid)
self.assertFalse(state.canTimeout)
self.assertTrue(safety.get_controls_allowed())
with patch("openpilot.starpilot.controller_extensions.time.monotonic_ns", return_value=now), \
patch("openpilot.selfdrive.car.card.time.monotonic", return_value=now / 1e9), \
patch.object(inputs.sm, "update", return_value=None):
feed_events = []
with patch.object(controls.sm, "update_msgs", side_effect=lambda _, events, collector=feed_events: collector.extend(events)):
lane.feed(controls, now, tick, speed=15.)
car_state = messaging.new_message("carState", valid=True, logMonoTime=now)
car_state.carState = state
preview = messaging.new_message("modelV2", valid=True, logMonoTime=now)
preview.modelV2 = lane.model()
preview.modelV2.action.desiredCurvature = direction * .0002
predicted = .012 + (tick - 430) * .0002 if 430 <= tick < 460 else .0012
preview.modelV2.orientationRate.z = [direction * predicted * 15.] * 33
delay = messaging.new_message("lateralDelay", valid=True, logMonoTime=now)
delay.lateralDelay.lateralDelay = .38
replacements = {"carState": car_state.as_reader(), "modelV2": preview.as_reader(), "lateralDelay": delay.as_reader()}
controls.sm.update_msgs(now / 1e9, [event for event in feed_events
if event.which() not in replacements] + list(replacements.values()))
if tick < 490:
inputs.sm.update_msgs(now / 1e9, [preview.as_reader(), delay.as_reader()])
command, controller_log = controls.state_control()
self.assertTrue(command.enabled)
control_events = []
def capture_control(service, event, collector=control_events):
if service == "carControl":
collector.append(messaging.log_from_bytes(event.to_bytes()))
with patch.object(controls.pm, "send", side_effect=capture_control):
controls.publish(command, controller_log)
self.assertEqual(len(control_events), 1)
self.assertEqual(control_events[0].carControl.to_dict(), command.to_dict())
self.assertEqual(control_events[0].valid, state.canValid)
host.sm.update_msgs(now / 1e9, control_events)
command = host.sm["carControl"]
host.can_log_mono_time = now
sent.clear()
host.controls_update(state, command)
if tick > 510:
self.assertIsNone(owner.model)
saw_stale_model = True
for addr, data, bus in sent:
if addr not in (0x3CA, 0x3D3, 0x3D6):
continue
self.assertTrue(safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, data)), (identity, tick, hex(addr)))
if addr == 0x3CA:
self.assertEqual(data[4] & 3, 0) # Existing neutral body, no extension announcement.
elif addr in (0x3D3, 0x3D6):
fd = addr == 0x3D6
mode = (data[0] >> 4) & 7 if fd else (data[4] >> 2) & 7
curvature = ((data[2] << 3) | (data[3] >> 5)) - 1000 if fd else ((data[0] << 3) | (data[1] >> 5)) - 1000
rate = (data[6] << 3) | (data[7] >> 5) if fd else ((data[1] & 31) << 8) | data[2]
self.assertEqual(rate, 1024 if fd else 4096)
if 430 <= tick < 460 and host.CI.CC.new_port_curvature_demand.curvature_rate != 0:
saw_rate_neutral = True
if 400 <= tick < 425:
self.assertEqual((mode, curvature), (0, 0))
saw_human_zero = True
if fault:
self.assertEqual((mode, curvature), (0, 0))
saw_fault_zero = True
if 70 <= tick < 100 and mode and curvature:
active_curvatures.append(curvature)
self.assertAlmostEqual(owner._curvature_lookahead(), .38, places=6)
self.assertGreater(direction * host.CI.CC.new_port_curvature_demand.curvature, .0002)
self.assertTrue(active_curvatures)
self.assertTrue(all(value * direction < 0 for value in active_curvatures))
self.assertTrue(saw_human_zero and saw_fault_zero and saw_stale_model and saw_rate_neutral)
self.assertEqual(Path(saved.get_param_path("FordHumanTurnDetection")).read_bytes(), b"1")
finally:
helper.doCleanups()
safety.set_alternative_experience(0)
safety.set_safety_hooks(structs.CarParams.SafetyModel.noOutput, 0)
@@ -31,7 +31,6 @@ def test_actual_card_publication_reaches_ford_extensions(identity, alpha, releas
with OpenpilotPrefix():
saved = Params()
saved.put_bool('OpenpilotEnabledToggle', True, block=True)
saved.put_bool('AlwaysOnLateral', False, block=True)
saved.put_bool('AlphaLongitudinalEnabled', alpha, block=True)
saved.put_bool('IsReleaseBranch', release, block=True)
saved.put_bool('DisableOpenpilotLongitudinal', disable, block=True)
@@ -55,7 +55,6 @@ class TestFordThreePorts(unittest.TestCase):
with self.subTest(candidate=candidate):
stock = params(candidate)
self.assertFalse(stock.openpilotLongitudinalControl)
self.assertAlmostEqual(stock.steerActuatorDelay, 0.05 if candidate == CAR.FORD_TRANSIT_MK5 else 0.22, places=7)
self.assertTrue(stock.safetyConfigs[-1].safetyParam & FordSafetyFlags.NEW_PORT)
self.assertFalse(stock.safetyConfigs[-1].safetyParam & FordSafetyFlags.LONG_CONTROL)
requested = params(candidate, alpha=True)
@@ -63,16 +62,14 @@ class TestFordThreePorts(unittest.TestCase):
self.assertTrue(requested.safetyConfigs[-1].safetyParam & FordSafetyFlags.LONG_CONTROL)
if candidate == CAR.FORD_MONDEO_MK5:
self.assertFalse(params(candidate, alpha=True, release=True).openpilotLongitudinalControl)
self.assertAlmostEqual(params(CAR.FORD_MUSTANG_MACH_E_MK1).steerActuatorDelay, 0.22, places=7)
self.assertFalse(params(CAR.FORD_BRONCO_SPORT_MK1).openpilotLongitudinalControl)
self.assertTrue(params(CAR.FORD_BRONCO_SPORT_MK1, alpha=True).openpilotLongitudinalControl)
def test_real_edge_angle_and_mondeo_gear_parsers(self):
cases: tuple[tuple[CAR, str, dict[str, float]], ...] = (
for candidate, source_name, source_values in (
(CAR.FORD_EDGE_MK2, "TransGearData", {"GearLvrPos_D_Actl": 4}),
(CAR.FORD_MONDEO_MK5, "Gear_Shift_by_Wire_FD1", {"TrnRng_D_RqGsm": 4}),
)
for candidate, source_name, source_values in cases:
):
with self.subTest(candidate=candidate):
cp = params(candidate)
self.assertEqual(cp.transmissionType, structs.CarParams.TransmissionType.automatic)
-1
View File
@@ -48,7 +48,6 @@ class FordSafetyFlags(IntFlag):
MACH_E_EXTENDED = 16
CLASSIC_EXTENDED = 32
EXPLORER_EXTENDED = CLASSIC_EXTENDED # Existing public profile spelling.
BLUEPILOT_ANGLE = 128
GENERIC_CANFD_EXTENDED = 64
+11 -31
View File
@@ -262,7 +262,6 @@ class CarController(CarControllerBase):
def __init__(self, dbc_names, CP):
super().__init__(dbc_names, CP)
self.long_pitch = True
self.long_pitch_input = None
self.bolt_removed_cancel_credit_used = 0
self.bolt_pedal_removed = is_bolt_pedal_removed_profile(CP) or is_bolt_pedal_removed_profile(CP, stock_only=True)
self.bolt_present_no_acc_stock = is_bolt_present_no_acc_pedal_profile(CP, stock_only=True)
@@ -298,7 +297,6 @@ class CarController(CarControllerBase):
self.paddle_handoff_frames = 0
self.bolt_acc_pedal_friction_release_frames = 0
self.bolt_acc_pedal_friction_low_speed_active = False
self.bolt_acc_pedal_stop_hold = False
self.lka_steering_cmd_counter = 0
self.lka_icon_status_last = (False, False)
@@ -441,10 +439,6 @@ class CarController(CarControllerBase):
return self.regen_paddle_pressed
def update(self, CC, CS, now_nanos, *, startup_keepalive=False):
_projection = self.learning_projection_observer
if _projection is not None:
_projection.begin(CC.actuators.torque, self.frame)
long_pitch = self.long_pitch
if ((self.camera_pedal_profile is not None and self.camera_pedal_profile.volt or self.volt_cc_pedal_profile is not None)
and self.frame % 25 == 0):
self.longitudinal_maneuver_mode = bool(self.longitudinal_maneuver_input is not None and
@@ -603,8 +597,6 @@ class CarController(CarControllerBase):
if lat_active and not (self.ordinary_cc_profile and CC.enabled and not CS.out.cruiseState.enabled and not aol_lateral):
new_torque = int(round(actuators.torque * self.params.STEER_MAX))
if _projection is not None:
_projection.input(new_torque, self.params.STEER_MAX, self.apply_torque_last, CS.out.steeringTorque, self.params, limiter='driver')
apply_torque = apply_driver_steer_torque_limits(new_torque, self.apply_torque_last, CS.out.steeringTorque, self.params)
else:
apply_torque = 0
@@ -613,8 +605,6 @@ class CarController(CarControllerBase):
self.apply_torque_last = apply_torque
idx = self.lka_steering_cmd_counter % 4
can_sends.append(gmcan.create_steering_control(self.packer_pt, CanBus.POWERTRAIN, apply_torque, idx, lat_active))
if _projection is not None:
_projection.emit('gm.ASCMLKASteeringCmd', apply_torque, lat_active, can_sends[-1])
CS.steering_authority.command(now_nanos, lat_active)
status_bolt = self.CP.carFingerprint in NO_ACC_BOLT_CAR and (
@@ -635,7 +625,7 @@ class CarController(CarControllerBase):
pedal = self.hybrid_command.update(
actuators.accel, CC.enabled and CC.longActive, CS.out,
stopping=actuators.longControlState == LongCtrlState.stopping, resume=CC.cruiseControl.resume,
orientation=CC.orientationNED if long_pitch else None, acc_tune=self.gm_acc_tune)
orientation=CC.orientationNED if self.long_pitch else None, acc_tune=self.gm_acc_tune)
else:
self.hybrid_command.withdraw()
pedal = 0.
@@ -688,7 +678,7 @@ class CarController(CarControllerBase):
ready = runtime_allowed and physical and source_current and sensor_current
pedal = 0.
if ready:
gas, _ = volt_demands(actuators.accel, CS.out.vEgo, CC.orientationNED if long_pitch else None,
gas, _ = volt_demands(actuators.accel, CS.out.vEgo, CC.orientationNED if self.long_pitch else None,
.75, self.CP.mass, self.CP.wheelbase, -4., 2., 400)
pedal = self.volt_cc_pedal_command.update(
actuators.accel, CC.enabled and CC.longActive, CS.out,
@@ -725,7 +715,7 @@ class CarController(CarControllerBase):
if ready:
pedal, self.apply_gas, self.apply_brake = self.silverado_pedal_command.update(
actuators.accel, CC.longActive, CS.out, stopping=actuators.longControlState == LongCtrlState.stopping,
resume=CC.cruiseControl.resume, orientation=CC.orientationNED if long_pitch else None)
resume=CC.cruiseControl.resume, orientation=CC.orientationNED if self.long_pitch else None)
else:
if not sources_ready:
self.silverado_pedal_command.prime_recovery()
@@ -745,7 +735,7 @@ class CarController(CarControllerBase):
if ready:
pedal, self.apply_gas, self.apply_brake = self.conventional_pedal_command.update(
actuators.accel, CC.longActive and CC.enabled, CS.out, stopping=actuators.longControlState == LongCtrlState.stopping,
resume=CC.cruiseControl.resume, orientation=CC.orientationNED if long_pitch else None)
resume=CC.cruiseControl.resume, orientation=CC.orientationNED if self.long_pitch else None)
else:
if not sources_ready:
self.conventional_pedal_command.prime_recovery()
@@ -757,11 +747,6 @@ class CarController(CarControllerBase):
can_sends.append(gmcan.create_pedal_command(self.packer_pt, pedal, (self.frame // 4) % 4))
elif self.CP.flags & GMFlags.PEDAL_LONG.value and self.CP.openpilotLongitudinalControl and self.camera_pedal_profile is None:
active, stock_ownership_clear, in_regen_gear = self.bolt_pedal_admission(CC, CS, now_nanos)
self.bolt_acc_pedal_stop_hold = (
self.CP.carFingerprint == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL and is_bolt_pedal_profile(self.CP) and
active and actuators.longControlState == LongCtrlState.stopping and not CC.cruiseControl.resume and
(self.bolt_acc_pedal_stop_hold or CS.out.standstill)
)
if not active or not in_regen_gear or self.maneuver_paddle_mode == "off":
self.bolt_regen_hold = False
elif self.maneuver_paddle_mode == "force":
@@ -818,12 +803,9 @@ class CarController(CarControllerBase):
0 < gear_stamp <= now_nanos and now_nanos - gear_stamp <= 100_000_000 and
(CS.out.standstill or math.isfinite(CS.out.vEgo) and CS.out.vEgo < cutoff) and
actuators.longControlState != LongCtrlState.stopping and
bolt_pedal_launch_gas(actuators.accel, CS.out.vEgo, CC.orientationNED if long_pitch else None,
bolt_pedal_launch_gas(actuators.accel, CS.out.vEgo, CC.orientationNED if self.long_pitch else None,
self.CP, acc_tune=self.gm_acc_tune)):
pedal = 18. / 255.
if self.bolt_acc_pedal_stop_hold:
pedal = 0.0
self.pedal_steady = 0.0
can_sends.append(gmcan.create_pedal_command(self.packer_pt, pedal, (self.frame // 4) % 16))
if CC.enabled:
self.paddle_handoff_frames = 2
@@ -896,10 +878,10 @@ class CarController(CarControllerBase):
else:
if self.CP.carFingerprint == CAR.CHEVROLET_SUBURBAN:
self.apply_gas, self.apply_brake = suburban_gateway_demands(
actuators.accel, CS.out.vEgo, CC.orientationNED if long_pitch else None, self.CP, acc_tune=self.gm_acc_tune)
actuators.accel, CS.out.vEgo, CC.orientationNED if self.long_pitch else None, self.CP, acc_tune=self.gm_acc_tune)
elif self.ordinary_camera_long:
self.apply_gas, self.apply_brake = ascm_demands(
actuators.accel, CS.out.vEgo, CC.orientationNED if long_pitch else None, self.CP,
actuators.accel, CS.out.vEgo, CC.orientationNED if self.long_pitch else None, self.CP,
min_gas=-540, max_gas=2698, inactive_gas=-500, brake_threshold=0.0, acc_tune=self.gm_acc_tune,
truck_tuning=self.truck_tuning, lead_visible=CC.hudControl.leadVisible,
set_speed_error=max(CC.hudControl.setSpeed - CS.out.vEgo, 0.0), stopping=stopping)
@@ -910,14 +892,14 @@ class CarController(CarControllerBase):
stop_speed=0.35 if self.CP.carFingerprint == CAR.CHEVROLET_BLAZER else 0.25, acc_tune=self.gm_acc_tune)
elif self.ordinary_ascm_long:
self.apply_gas, self.apply_brake = ascm_demands(
actuators.accel, CS.out.vEgo, CC.orientationNED if long_pitch else None, self.CP, acc_tune=self.gm_acc_tune)
actuators.accel, CS.out.vEgo, CC.orientationNED if self.long_pitch else None, self.CP, acc_tune=self.gm_acc_tune)
elif self.bolt_euv_long:
self.apply_gas, self.apply_brake = bolt_euv_demands(
actuators.accel, CS.out.vEgo, CC.orientationNED if long_pitch else None,
actuators.accel, CS.out.vEgo, CC.orientationNED if self.long_pitch else None,
self.CP.mass, self.CP.wheelbase, self.params.MAX_BRAKE, acc_tune=self.gm_acc_tune)
elif self.volt_gateway_long or self.volt_ascm_long or self.volt_camera_long or self.volt_sdgm_long:
self.apply_gas, self.apply_brake = volt_demands(
actuators.accel, CS.out.vEgo, CC.orientationNED if long_pitch else None,
actuators.accel, CS.out.vEgo, CC.orientationNED if self.long_pitch else None,
VOLT_CAMERA_GRADE_STOP_SPEED if self.volt_ascm_long or self.volt_camera_long or self.volt_sdgm_long else VOLT_GATEWAY_GRADE_STOP_SPEED,
self.CP.mass, self.CP.wheelbase, self.params.ACCEL_MIN, self.params.ACCEL_MAX, self.params.MAX_BRAKE,
min_gas=-540 if self.volt_camera_long or self.volt_sdgm_long else -650,
@@ -928,7 +910,7 @@ class CarController(CarControllerBase):
if self.gm_acc_tune else actuators.accel)
if self.CP.carFingerprint in VOLT_GRADE_CAR:
gas_demand, brake_demand = volt_grade_demands(
actuators.accel, CS.out.vEgo, CC.orientationNED if long_pitch else None,
actuators.accel, CS.out.vEgo, CC.orientationNED if self.long_pitch else None,
VOLT_GATEWAY_GRADE_STOP_SPEED if self.CP.networkLocation == NetworkLocation.gateway else VOLT_CAMERA_GRADE_STOP_SPEED,
self.params.ACCEL_MIN, self.params.ACCEL_MAX)
self.apply_gas = float(np.interp(gas_demand, self.params.GAS_LOOKUP_BP, self.params.GAS_LOOKUP_V))
@@ -1324,6 +1306,4 @@ class CarController(CarControllerBase):
self.CP.openpilotLongitudinalControl and self.frame % keepalive_step == 0):
can_sends += gmcan.create_adas_keepalive(CanBus.POWERTRAIN)
self.frame += 1
if _projection is not None:
_projection.finish(new_actuators.torque)
return new_actuators, can_sends
@@ -328,7 +328,7 @@ class TestAscmIntercept(unittest.TestCase):
self.assertAlmostEqual(cp.minEnableSpeed, -1. if car == CAR.CADILLAC_ESCALADE_ESV_2019_ASCM else 5 / 3.6, places=6)
self.assertTrue(cp.safetyConfigs[0].safetyParam & GMSafetyFlags.ASCM_INTERCEPT)
self.assertFalse(cp.safetyConfigs[0].safetyParam & GMSafetyFlags.HW_CAM_LONG)
self.assertEqual(car.config.car_docs[0].support_type.value, "Community")
self.assertEqual(car.config.car_docs, [])
self.assertNotIn(car, FINGERPRINTS)
self.assertNotIn(car, FW_VERSIONS)
@@ -74,7 +74,6 @@ class TestCameraAccPedal(unittest.TestCase):
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'REPLAY': '1'}):
settings = Params()
settings.put_bool('OpenpilotEnabledToggle', True, block=True)
settings.put_bool('AlwaysOnLateral', False, block=True)
for identity in CAMERA_IDS:
for camera in (False, True):
for be in (False, True):
@@ -130,7 +130,7 @@ class TestCcGatewayStock(unittest.TestCase):
def test_manual_identity_and_stock_ownership(self):
self.assertEqual(len(CC_GATEWAY_STOCK_CAR), 9)
for car in CC_GATEWAY_STOCK_CAR:
self.assertEqual(car.config.car_docs[0].support_type.value, "Community")
self.assertEqual(car.config.car_docs, [])
automatic = {CAR.CADILLAC_CT6_CC, CAR.CADILLAC_XT5_CC, CAR.CHEVROLET_EQUINOX_CC,
CAR.CHEVROLET_MALIBU_CC, CAR.CHEVROLET_SUBURBAN_CC, CAR.CHEVROLET_TRAILBLAZER_CC}
self.assertEqual(car in FINGERPRINTS, car in automatic)
@@ -37,7 +37,7 @@ class TestSdgmStock(unittest.TestCase):
self.assertTrue(flags & GMSafetyFlags.SDGM)
self.assertFalse(flags & GMSafetyFlags.HW_CAM_LONG)
self.assertEqual(cp.radarUnavailable, not radar)
self.assertEqual(car.config.car_docs[0].support_type.value, "Community")
self.assertEqual(car.config.car_docs, [])
self.assertNotIn(car, FINGERPRINTS)
self.assertNotIn(car, FW_VERSIONS)
@@ -108,7 +108,6 @@ class TestSilveradoCcPedal(unittest.TestCase):
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}):
store = Params()
store.put_bool('OpenpilotEnabledToggle', True, block=True)
store.put_bool('AlwaysOnLateral', False, block=True)
store.put_bool('DisableOpenpilotLongitudinal', disabled, block=True)
fp = fingerprint(removed=removed)
cp = CarInterface.get_params(IDENTITY, fp, [], False, False, False)
+112 -149
View File
@@ -5,7 +5,7 @@ from types import MappingProxyType
from opendbc.car import Bus, PlatformConfig, DbcDict, Platforms, CarSpecs
from opendbc.car.structs import CarParams
from opendbc.car.docs_definitions import CarDocs, CarFootnote, CarHarness, CarParts, Column, SupportType
from opendbc.car.docs_definitions import CarDocs, CarFootnote, CarHarness, CarParts, Column
from opendbc.car.fw_query_definitions import FwQueryConfig, Request, StdQueries
Ecu = CarParams.Ecu
@@ -140,14 +140,6 @@ class GMFlags(IntFlag):
VOLT_CAMERA_NO_ACCEL_POS = NO_ACCELERATOR_POS_MSG
_GM_CONTROL_FLAGS_MASK = ~int(GMFlags.HAS_BSM)
_GM_PEDAL_LONG_FLAG = int(GMFlags.PEDAL_LONG)
_GM_NO_CAMERA_FLAG = int(GMFlags.NO_CAMERA)
_GM_NO_ACCELERATOR_POS_FLAG = int(GMFlags.NO_ACCELERATOR_POS_MSG)
_BOLT_REMOVED_CONTROL_FLAGS = _GM_PEDAL_LONG_FLAG | _GM_NO_CAMERA_FLAG
_CC_PEDAL_ALLOWED_FLAGS = _BOLT_REMOVED_CONTROL_FLAGS | _GM_NO_ACCELERATOR_POS_FLAG
VOLT_ONE_PEDAL_BASE_WORDS = (0x4084, 0xC084, 0x4287, 0x4687, 0x4A87, 0x4E87, 0x4087, 0x5087, 0x5487, 0xC1D1, 0xC1D3)
VOLT_ONE_PEDAL_WORDS = {start + index: word for start in (0xD100, 0xD110)
for index, word in enumerate(VOLT_ONE_PEDAL_BASE_WORDS)}
@@ -167,7 +159,7 @@ def gm_control_word(cp: CarParams) -> int:
hybrid = malibu_hybrid_profile(cp)
if hybrid is not None:
return 5 | (8 if hybrid.pedal and hybrid.longitudinal else 0)
if word in _BOLT_REMOVED_SAFETY_WORDS and (is_bolt_pedal_removed_profile(cp) or is_bolt_pedal_removed_profile(cp, stock_only=True)):
if is_bolt_pedal_removed_profile(cp) or is_bolt_pedal_removed_profile(cp, stock_only=True):
return int(BOLT_PEDAL_STOCK_WORDS[cp.carFingerprint] if not cp.openpilotLongitudinalControl else BOLT_PEDAL_WORDS[cp.carFingerprint])
if word in VOLT_CC_PEDAL_PROFILES and volt_cc_pedal_profile(cp) is not None:
return 5
@@ -225,7 +217,7 @@ def apply_volt_one_pedal(cp: CarParams, enabled: bool, auto_hold_enabled: bool)
def control_flags(cp: CarParams) -> int:
"""Exclude only the informational BSM bit from exact control-profile admission."""
flags = int(cp.flags) & _GM_CONTROL_FLAGS_MASK
flags = int(cp.flags) & ~int(GMFlags.HAS_BSM)
profile = camera_acc_pedal_profile(cp)
if profile is not None and profile.volt:
flags &= ~int(GMFlags.PEDAL_LONG)
@@ -459,10 +451,7 @@ class GMCarDocs(CarDocs):
package: str = "Adaptive Cruise Control (ACC)"
def init_make(self, CP: CarParams):
if (CP.carFingerprint in ASCM_INTERCEPT_CAR or CP.carFingerprint in CC_GATEWAY_STOCK_CAR or
CP.carFingerprint in (CAR.CHEVROLET_VOLT_CC, CAR.CHEVROLET_MALIBU_HYBRID_CC)):
self.car_parts = CarParts.common([CarHarness.custom])
elif CP.networkLocation == CarParams.NetworkLocation.fwdCamera:
if CP.networkLocation == CarParams.NetworkLocation.fwdCamera:
if CP.carFingerprint in SDGM_CAR:
self.car_parts = CarParts.common([CarHarness.gmsdgm])
else:
@@ -472,13 +461,6 @@ class GMCarDocs(CarDocs):
self.car_parts = CarParts.common([CarHarness.obd_ii])
@dataclass
class GMCommunityCarDocs(GMCarDocs):
merged: bool = False
support_type: SupportType = SupportType.COMMUNITY
support_link: str = "#community"
@dataclass(frozen=True, kw_only=True)
class GMCarSpecs(CarSpecs):
tireStiffnessFactor: float = 0.444 # not optimized yet
@@ -493,81 +475,105 @@ class GMPlatformConfig(PlatformConfig):
})
@dataclass
class GMASCMPlatformConfig(GMPlatformConfig):
def init(self):
# ASCM is supported, but due to a janky install and hardware configuration, we are not showing in the car docs
self.car_docs = []
@dataclass
class GMSDGMPlatformConfig(GMPlatformConfig):
def init(self):
# Don't show in docs until the harness is sold. See https://github.com/commaai/openpilot/issues/32471
self.car_docs = []
@dataclass
class GMCCGatewayPlatformConfig(GMPlatformConfig):
def init(self):
# Manual-only identities; installation and route evidence is not available.
self.car_docs = []
@dataclass
class GMCameraStockPlatformConfig(GMPlatformConfig):
def init(self):
# Explicit identities pending independent install and route qualification.
self.car_docs = []
class CAR(Platforms):
HOLDEN_ASTRA = GMPlatformConfig(
[GMCommunityCarDocs("Holden Astra (OBD-II Harness) 2017")],
HOLDEN_ASTRA = GMASCMPlatformConfig(
[GMCarDocs("Holden Astra 2017")],
GMCarSpecs(mass=1363, wheelbase=2.662, steerRatio=15.7, centerToFrontRatio=0.4),
)
CHEVROLET_VOLT = GMPlatformConfig(
[GMCarDocs("Chevrolet Volt (OBD-II Harness) 2017-18", support_type=SupportType.COMMUNITY, support_link="#community",
min_enable_speed=0, video="https://youtu.be/QeMCN_4TFfQ")],
CHEVROLET_VOLT = GMASCMPlatformConfig(
[GMCarDocs("Chevrolet Volt 2017-18", min_enable_speed=0, video="https://youtu.be/QeMCN_4TFfQ")],
GMCarSpecs(mass=1607, wheelbase=2.69, steerRatio=15.7, centerToFrontRatio=0.45, tireStiffnessFactor=1.0),
)
CHEVROLET_VOLT_CC = GMPlatformConfig(
[GMCarDocs("Chevrolet Volt (CC)", "Manual development configuration; installation must be verified",
merged=False, support_type=SupportType.COMMUNITY, support_link="#community")],
[], # Manual development identity; no fingerprint, firmware alias or public driving claim.
CHEVROLET_VOLT.specs, dbc_dict=CHEVROLET_VOLT.dbc_dict,
)
CHEVROLET_VOLT_ASCM = GMPlatformConfig(
[GMCarDocs("Chevrolet Volt (ASCM Harness) 2017-18", "Adaptive Cruise Control (ACC); custom ASCM harness install",
support_type=SupportType.COMMUNITY, support_link="#community")], CHEVROLET_VOLT.specs,
CHEVROLET_VOLT_ASCM = GMASCMPlatformConfig(
[GMCarDocs("Chevrolet Volt ASCM Harness 2017-18")], CHEVROLET_VOLT.specs,
)
CADILLAC_ATS = GMPlatformConfig(
[GMCommunityCarDocs("Cadillac ATS Premium Performance (OBD-II Harness) 2018")],
CADILLAC_ATS = GMASCMPlatformConfig(
[GMCarDocs("Cadillac ATS Premium Performance 2018")],
GMCarSpecs(mass=1601, wheelbase=2.78, steerRatio=15.3),
)
CHEVROLET_MALIBU = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Malibu Premier (OBD-II Harness) 2017")],
CHEVROLET_MALIBU = GMASCMPlatformConfig(
[GMCarDocs("Chevrolet Malibu Premier 2017")],
GMCarSpecs(mass=1496, wheelbase=2.83, steerRatio=15.8, centerToFrontRatio=0.4),
)
CHEVROLET_MALIBU_ASCM = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Malibu (ASCM Harness) 2017-19")], CHEVROLET_MALIBU.specs,
CHEVROLET_MALIBU_ASCM = GMASCMPlatformConfig(
[GMCarDocs("Chevrolet Malibu ASCM Harness 2017-19")], CHEVROLET_MALIBU.specs,
)
GMC_ACADIA = GMPlatformConfig(
[GMCommunityCarDocs("GMC Acadia (OBD-II Harness) 2018", video="https://www.youtube.com/watch?v=0ZN6DdsBUZo")],
GMC_ACADIA = GMASCMPlatformConfig(
[GMCarDocs("GMC Acadia 2018", video="https://www.youtube.com/watch?v=0ZN6DdsBUZo")],
GMCarSpecs(mass=1975, wheelbase=2.86, steerRatio=14.4, centerToFrontRatio=0.4),
)
GMC_ACADIA_ASCM = GMPlatformConfig(
[GMCommunityCarDocs("GMC Acadia (ASCM Harness) 2018")], GMC_ACADIA.specs,
GMC_ACADIA_ASCM = GMASCMPlatformConfig(
[GMCarDocs("GMC Acadia ASCM Harness 2018")], GMC_ACADIA.specs,
)
BUICK_LACROSSE = GMPlatformConfig(
[GMCommunityCarDocs("Buick LaCrosse (OBD-II Harness) 2017-19", "Driver Confidence Package 2")],
BUICK_LACROSSE = GMASCMPlatformConfig(
[GMCarDocs("Buick LaCrosse 2017-19", "Driver Confidence Package 2")],
GMCarSpecs(mass=1712, wheelbase=2.91, steerRatio=15.8, centerToFrontRatio=0.4),
)
BUICK_LACROSSE_ASCM = GMPlatformConfig(
[GMCommunityCarDocs("Buick LaCrosse (ASCM Harness) 2017-19")], BUICK_LACROSSE.specs,
BUICK_LACROSSE_ASCM = GMASCMPlatformConfig(
[GMCarDocs("Buick LaCrosse ASCM Harness 2017-19")], BUICK_LACROSSE.specs,
)
BUICK_LACROSSE_ASCM_19US = GMPlatformConfig(
[GMCommunityCarDocs("Buick LaCrosse US (ASCM Harness) 2019")], BUICK_LACROSSE.specs,
BUICK_LACROSSE_ASCM_19US = GMASCMPlatformConfig(
[GMCarDocs("Buick LaCrosse US ASCM Harness 2019")], BUICK_LACROSSE.specs,
)
BUICK_REGAL = GMPlatformConfig(
[GMCommunityCarDocs("Buick Regal Essence (OBD-II Harness) 2018")],
BUICK_REGAL = GMASCMPlatformConfig(
[GMCarDocs("Buick Regal Essence 2018")],
GMCarSpecs(mass=1714, wheelbase=2.83, steerRatio=14.4, centerToFrontRatio=0.4),
)
CADILLAC_ESCALADE = GMPlatformConfig(
[GMCommunityCarDocs("Cadillac Escalade (OBD-II Harness) 2017", "Driver Assist Package")],
CADILLAC_ESCALADE = GMASCMPlatformConfig(
[GMCarDocs("Cadillac Escalade 2017", "Driver Assist Package")],
GMCarSpecs(mass=2564, wheelbase=2.95, steerRatio=17.3),
)
CADILLAC_ESCALADE_ASCM = GMPlatformConfig(
[GMCommunityCarDocs("Cadillac Escalade (ASCM Harness) 2018")], CADILLAC_ESCALADE.specs,
CADILLAC_ESCALADE_ASCM = GMASCMPlatformConfig(
[GMCarDocs("Cadillac Escalade ASCM Harness 2018")], CADILLAC_ESCALADE.specs,
)
CADILLAC_ESCALADE_ESV = GMPlatformConfig(
[GMCommunityCarDocs("Cadillac Escalade ESV (OBD-II Harness) 2016", "Adaptive Cruise Control (ACC) & LKAS")],
CADILLAC_ESCALADE_ESV = GMASCMPlatformConfig(
[GMCarDocs("Cadillac Escalade ESV 2016", "Adaptive Cruise Control (ACC) & LKAS")],
GMCarSpecs(mass=2739, wheelbase=3.302, steerRatio=17.3, tireStiffnessFactor=1.0),
)
CADILLAC_ESCALADE_ESV_2019 = GMPlatformConfig(
[GMCommunityCarDocs("Cadillac Escalade ESV (OBD-II Harness) 2019", "Adaptive Cruise Control (ACC) & LKAS")],
CADILLAC_ESCALADE_ESV_2019 = GMASCMPlatformConfig(
[GMCarDocs("Cadillac Escalade ESV 2019", "Adaptive Cruise Control (ACC) & LKAS")],
CADILLAC_ESCALADE_ESV.specs,
)
CADILLAC_ESCALADE_ESV_2019_ASCM = GMPlatformConfig(
[GMCommunityCarDocs("Cadillac Escalade ESV Platinum (ASCM Harness) 2019")], CADILLAC_ESCALADE_ESV_2019.specs,
CADILLAC_ESCALADE_ESV_2019_ASCM = GMASCMPlatformConfig(
[GMCarDocs("Cadillac Escalade ESV Platinum ASCM Harness 2019")], CADILLAC_ESCALADE_ESV_2019.specs,
)
CHEVROLET_SUBURBAN = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Suburban (OBD-II Harness)", "Configuration-specific install; observed CAN sources required")],
CarSpecs(mass=2731, wheelbase=3.302, steerRatio=17.3, centerToFrontRatio=0.49),
[], CarSpecs(mass=2731, wheelbase=3.302, steerRatio=17.3, centerToFrontRatio=0.49),
)
CHEVROLET_SUBURBAN_ASCM = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Suburban Premier (ASCM Harness) 2016-20")],
CHEVROLET_SUBURBAN_ASCM = GMASCMPlatformConfig(
[GMCarDocs("Chevrolet Suburban Premier ASCM Harness 2016-20")],
CHEVROLET_SUBURBAN.specs,
)
CHEVROLET_BOLT_EUV = GMPlatformConfig(
@@ -577,10 +583,7 @@ class CAR(Platforms):
],
GMCarSpecs(mass=1669, wheelbase=2.63779, steerRatio=16.8, centerToFrontRatio=0.4, tireStiffnessFactor=1.0),
)
CHEVROLET_BOLT_ACC_2022_2023 = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Bolt (Camera Harness) 2022-23", "ACC; recognized camera layout required")],
CHEVROLET_BOLT_EUV.specs,
)
CHEVROLET_BOLT_ACC_2022_2023 = GMCameraStockPlatformConfig([], CHEVROLET_BOLT_EUV.specs)
CHEVROLET_BOLT_CC_2017 = GMPlatformConfig(
[GMCarDocs("Chevrolet Bolt EV 2017", "Cruise control with installed pedal interceptor")],
CHEVROLET_BOLT_EUV.specs,
@@ -614,95 +617,63 @@ class CAR(Platforms):
[GMCarDocs("Chevrolet Trailblazer 2021-22")],
GMCarSpecs(mass=1345, wheelbase=2.64, steerRatio=16.8, centerToFrontRatio=0.4, tireStiffnessFactor=1.0),
)
CADILLAC_XT4 = GMPlatformConfig(
[GMCommunityCarDocs("Cadillac XT4 (SDGM Harness) 2023", "Driver Assist Package")],
CADILLAC_XT4 = GMSDGMPlatformConfig(
[GMCarDocs("Cadillac XT4 2023", "Driver Assist Package")],
GMCarSpecs(mass=1660, wheelbase=2.78, steerRatio=14.4, centerToFrontRatio=0.4),
)
CADILLAC_XT5 = GMPlatformConfig(
[GMCommunityCarDocs("Cadillac XT5 (SDGM Harness) 2022", "Driver Assist Package")],
CADILLAC_XT5 = GMSDGMPlatformConfig(
[GMCarDocs("Cadillac XT5 2022", "Driver Assist Package")],
GMCarSpecs(mass=1810, wheelbase=2.86, steerRatio=16.34, centerToFrontRatio=0.5, tireStiffnessFactor=1.0),
)
CADILLAC_XT6 = GMPlatformConfig(
[GMCommunityCarDocs("Cadillac XT6 (SDGM Harness) 2020", "Driver Assist Package")],
CADILLAC_XT6 = GMSDGMPlatformConfig(
[GMCarDocs("Cadillac XT6 2020", "Driver Assist Package")],
GMCarSpecs(mass=2050, wheelbase=2.86, steerRatio=16.5, centerToFrontRatio=0.4),
)
CHEVROLET_BLAZER = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Blazer (SDGM Harness) 2019-25", "Driver Assist Package")],
CHEVROLET_BLAZER = GMSDGMPlatformConfig(
[GMCarDocs("Chevrolet Blazer 2019-25", "Driver Assist Package")],
GMCarSpecs(mass=1850, wheelbase=3.10, steerRatio=17.9, centerToFrontRatio=0.4, tireStiffnessFactor=1.0),
)
CHEVROLET_VOLT_2019 = GMPlatformConfig(
[GMCarDocs("Chevrolet Volt (SDGM Harness) 2019", "Adaptive Cruise Control (ACC) & LKAS",
support_type=SupportType.COMMUNITY, support_link="#community")],
CHEVROLET_VOLT_2019 = GMSDGMPlatformConfig(
[GMCarDocs("Chevrolet Volt 2019", "Adaptive Cruise Control (ACC) & LKAS")],
GMCarSpecs(mass=1607, wheelbase=2.69, steerRatio=15.7, centerToFrontRatio=0.45),
)
CHEVROLET_TRAVERSE = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Traverse (SDGM Harness) 2022-23", "RS, Premier, or High Country Trim")],
CHEVROLET_TRAVERSE = GMSDGMPlatformConfig(
[GMCarDocs("Chevrolet Traverse 2022-23", "RS, Premier, or High Country Trim")],
GMCarSpecs(mass=1955, wheelbase=3.07, steerRatio=17.9, centerToFrontRatio=0.4),
)
CHEVROLET_MALIBU_SDGM = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Malibu (SDGM Harness) 2019", "SDGM Harness")],
CHEVROLET_MALIBU_SDGM = GMSDGMPlatformConfig(
[GMCarDocs("Chevrolet Malibu 2019", "SDGM Harness")],
CHEVROLET_MALIBU.specs,
)
BUICK_BABYENCLAVE = GMPlatformConfig(
[GMCommunityCarDocs("Buick Baby Enclave (SDGM Harness) 2020-23", "Driver Assist Package")],
BUICK_BABYENCLAVE = GMSDGMPlatformConfig(
[GMCarDocs("Buick Baby Enclave 2020-23", "Driver Assist Package")],
GMCarSpecs(mass=2050, wheelbase=2.86, steerRatio=16.0, centerToFrontRatio=0.5, tireStiffnessFactor=1.0),
)
GMC_YUKON = GMPlatformConfig(
[GMCarDocs("GMC Yukon 2019-20", "Adaptive Cruise Control (ACC) & LKAS")],
GMCarSpecs(mass=2490, wheelbase=2.94, steerRatio=17.3, centerToFrontRatio=0.5, tireStiffnessFactor=1.0),
)
CHEVROLET_SUBURBAN_CAMERA = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Suburban (Camera Harness)", "Manual camera configuration; recognized CAN layout required")],
CarSpecs(mass=2731, wheelbase=3.302, steerRatio=17.3, centerToFrontRatio=0.49),
CHEVROLET_SUBURBAN_CAMERA = GMCameraStockPlatformConfig(
[], CarSpecs(mass=2731, wheelbase=3.302, steerRatio=17.3, centerToFrontRatio=0.49),
)
CHEVROLET_TRAX = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Trax (Camera Harness)", "Manual camera configuration; recognized CAN layout required")],
CarSpecs(mass=1365, wheelbase=2.7, steerRatio=16.4, centerToFrontRatio=0.4),
CHEVROLET_TRAX = GMCameraStockPlatformConfig(
[], CarSpecs(mass=1365, wheelbase=2.7, steerRatio=16.4, centerToFrontRatio=0.4),
)
CHEVROLET_VOLT_CAMERA = GMPlatformConfig(
[GMCarDocs("Chevrolet Volt (Camera Harness)", "Manual configuration; recognized camera layout required",
merged=False, support_type=SupportType.COMMUNITY, support_link="#community")],
GMCarSpecs(mass=1607, wheelbase=2.69, steerRatio=15.7, centerToFrontRatio=0.45, tireStiffnessFactor=1.0),
CHEVROLET_VOLT_CAMERA = GMCameraStockPlatformConfig(
[], GMCarSpecs(mass=1607, wheelbase=2.69, steerRatio=15.7, centerToFrontRatio=0.45, tireStiffnessFactor=1.0),
dbc_dict=CHEVROLET_VOLT.dbc_dict,
)
CADILLAC_CT6_CC = GMPlatformConfig(
[GMCommunityCarDocs("Cadillac CT6 (CC)", "Cruise control; configuration-specific installation")],
GMCarSpecs(mass=2358, wheelbase=3.11, steerRatio=17.7, centerToFrontRatio=0.4, tireStiffnessFactor=1.0),
)
CADILLAC_XT4_CC = GMPlatformConfig([GMCommunityCarDocs("Cadillac XT4 (CC)", "Cruise control; configuration-specific installation")], CADILLAC_XT4.specs)
CADILLAC_XT5_CC = GMPlatformConfig(
[GMCommunityCarDocs("Cadillac XT5 (CC)", "Cruise control; configuration-specific installation")],
replace(CADILLAC_XT5.specs, tireStiffnessFactor=1.0),
)
CHEVROLET_EQUINOX_CC = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Equinox (CC)", "Cruise control; configuration-specific installation")],
CHEVROLET_EQUINOX.specs,
)
CHEVROLET_MALIBU_HYBRID_CC = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Malibu Hybrid (CC)", "Cruise control; configuration-specific installation")],
GMCarSpecs(mass=1450, wheelbase=2.8, steerRatio=15.8, centerToFrontRatio=0.4),
)
CHEVROLET_MALIBU_CC = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Malibu (CC)", "Cruise control; configuration-specific installation")],
GMCarSpecs(mass=1450, wheelbase=2.8, steerRatio=18.25,
centerToFrontRatio=0.4, tireStiffnessFactor=0.997),
)
CHEVROLET_SILVERADO_CC = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Silverado (CC)", "Cruise control; configuration-specific installation")],
GMCarSpecs(mass=2994, wheelbase=3.75, steerRatio=16.3, tireStiffnessFactor=1.0),
)
CHEVROLET_SUBURBAN_CC = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Suburban (CC)", "Cruise control; configuration-specific installation")],
replace(CHEVROLET_SUBURBAN_ASCM.specs, tireStiffnessFactor=1.0),
)
CHEVROLET_TRAILBLAZER_CC = GMPlatformConfig(
[GMCommunityCarDocs("Chevrolet Trailblazer (CC)", "Cruise control; configuration-specific installation")],
CHEVROLET_TRAILBLAZER.specs,
)
GMC_YUKON_CC = GMPlatformConfig(
[GMCommunityCarDocs("GMC Yukon (CC)", "Cruise control; configuration-specific installation")],
GMCarSpecs(mass=2541, wheelbase=2.95, steerRatio=16.3, centerToFrontRatio=0.4, tireStiffnessFactor=1.0),
)
CADILLAC_CT6_CC = GMCCGatewayPlatformConfig([], GMCarSpecs(mass=2358, wheelbase=3.11, steerRatio=17.7, centerToFrontRatio=0.4, tireStiffnessFactor=1.0))
CADILLAC_XT4_CC = GMCCGatewayPlatformConfig([], CADILLAC_XT4.specs)
CADILLAC_XT5_CC = GMCCGatewayPlatformConfig([], replace(CADILLAC_XT5.specs, tireStiffnessFactor=1.0))
CHEVROLET_EQUINOX_CC = GMCCGatewayPlatformConfig([], CHEVROLET_EQUINOX.specs)
CHEVROLET_MALIBU_HYBRID_CC = GMPlatformConfig([], GMCarSpecs(mass=1450, wheelbase=2.8, steerRatio=15.8, centerToFrontRatio=0.4))
CHEVROLET_MALIBU_CC = GMCCGatewayPlatformConfig([], GMCarSpecs(mass=1450, wheelbase=2.8, steerRatio=18.25,
centerToFrontRatio=0.4, tireStiffnessFactor=0.997))
CHEVROLET_SILVERADO_CC = GMCCGatewayPlatformConfig([], GMCarSpecs(mass=2994, wheelbase=3.75, steerRatio=16.3, tireStiffnessFactor=1.0))
CHEVROLET_SUBURBAN_CC = GMCCGatewayPlatformConfig([], replace(CHEVROLET_SUBURBAN_ASCM.specs, tireStiffnessFactor=1.0))
CHEVROLET_TRAILBLAZER_CC = GMCCGatewayPlatformConfig([], CHEVROLET_TRAILBLAZER.specs)
GMC_YUKON_CC = GMCCGatewayPlatformConfig([], GMCarSpecs(mass=2541, wheelbase=2.95, steerRatio=16.3, centerToFrontRatio=0.4, tireStiffnessFactor=1.0))
class CruiseButtons:
@@ -875,18 +846,16 @@ BOLT_PEDAL_REMOVED_CARS = (CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_201
CAR.CHEVROLET_BOLT_CC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL)
BOLT_PEDAL_REMOVED_WORDS = MappingProxyType({candidate: 0xE700 + index for index, candidate in enumerate(BOLT_PEDAL_REMOVED_CARS)})
BOLT_PEDAL_REMOVED_STOCK_WORDS = MappingProxyType({candidate: 0xE710 + index for index, candidate in enumerate(BOLT_PEDAL_REMOVED_CARS)})
_BOLT_REMOVED_SAFETY_WORDS = frozenset((*BOLT_PEDAL_REMOVED_WORDS.values(), *BOLT_PEDAL_REMOVED_STOCK_WORDS.values()))
def is_bolt_pedal_removed_profile(cp, *, stock_only=False):
words = BOLT_PEDAL_REMOVED_STOCK_WORDS if stock_only else BOLT_PEDAL_REMOVED_WORDS
try:
return (cp.brand == 'gm' and cp.carFingerprint in words and
int(cp.flags) & _GM_CONTROL_FLAGS_MASK == _BOLT_REMOVED_CONTROL_FLAGS and
cp.transmissionType == CarParams.TransmissionType.direct and
return (cp.brand == 'gm' and cp.carFingerprint in words and cp.transmissionType == CarParams.TransmissionType.direct and
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and cp.radarUnavailable and
not (cp.passive or cp.dashcamOnly or cp.notCar or cp.alphaLongitudinalAvailable) and
bool(cp.openpilotLongitudinalControl) is not stock_only and bool(cp.pcmCruise) is stock_only and
int(cp.flags) & ~int(GMFlags.HAS_BSM) == int(GMFlags.PEDAL_LONG | GMFlags.NO_CAMERA) and
len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
int(cp.safetyConfigs[0].safetyParam) == words[cp.carFingerprint])
except (AttributeError, IndexError, TypeError, ValueError):
@@ -902,7 +871,7 @@ def is_bolt_pedal_profile(cp, *, stock_only=False):
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and
not (cp.passive or cp.dashcamOnly or cp.notCar or cp.alphaLongitudinalAvailable) and
bool(cp.openpilotLongitudinalControl) is not stock_only and bool(cp.pcmCruise) is stock_only and
int(cp.flags) & _GM_CONTROL_FLAGS_MASK == _GM_PEDAL_LONG_FLAG and len(cp.safetyConfigs) == 1 and
control_flags(cp) == GMFlags.PEDAL_LONG.value and len(cp.safetyConfigs) == 1 and
cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
cp.safetyConfigs[0].safetyParam == words[cp.carFingerprint])
@@ -1102,15 +1071,13 @@ def volt_camera_pedal_word(profile, auto_hold: bool, one_pedal: bool) -> int:
def camera_acc_pedal_profile(cp):
try:
if cp.brand != 'gm' or cp.carFingerprint not in CAMERA_ACC_PEDAL_CAR:
return None
if len(cp.safetyConfigs) != 1:
return None
profile = CAMERA_ACC_PEDAL_PROFILES.get(int(cp.safetyConfigs[0].safetyParam))
if profile is None:
return None
flags = (_GM_PEDAL_LONG_FLAG | (_GM_NO_CAMERA_FLAG if profile.removed else 0) |
(_GM_NO_ACCELERATOR_POS_FLAG if profile.brake_source == BrakeSource.F1 else 0))
flags = int(GMFlags.PEDAL_LONG | (GMFlags.NO_CAMERA if profile.removed else 0) |
(GMFlags.NO_ACCELERATOR_POS_MSG if profile.brake_source == BrakeSource.F1 else 0))
identity = (CAR.CHEVROLET_VOLT if profile.topology == "gateway" else
CAR.CHEVROLET_VOLT_ASCM if profile.topology == "ascm" else
CAR.CHEVROLET_VOLT_2019 if profile.topology == "sdgm" else CAR.CHEVROLET_VOLT_CAMERA)
@@ -1119,7 +1086,7 @@ def camera_acc_pedal_profile(cp):
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and (profile.volt or cp.radarUnavailable) and
(profile.topology != "gateway" or not cp.radarUnavailable) and
(profile.topology not in ("ascm", "sdgm") or profile.radar != cp.radarUnavailable) and
not cp.passive and not cp.dashcamOnly and not cp.notCar and int(cp.flags) & _GM_CONTROL_FLAGS_MASK == flags and
not cp.passive and not cp.dashcamOnly and not cp.notCar and int(cp.flags) & ~int(GMFlags.HAS_BSM) == flags and
cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
bool(cp.openpilotLongitudinalControl) == profile.longitudinal and
bool(cp.pcmCruise) == (not profile.longitudinal and profile.topology == "camera") and
@@ -1160,14 +1127,12 @@ def is_conventional_cc_pedal_profile(cp):
if is_silverado_cc_pedal_profile(cp):
return True
try:
if cp.brand != 'gm' or cp.carFingerprint not in ORDINARY_CC_CAR:
return False
flags = control_flags(cp)
removed = bool(flags & GMFlags.NO_CAMERA)
return (cp.brand == 'gm' and cp.carFingerprint in ORDINARY_CC_CAR and
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and cp.radarUnavailable and
not cp.passive and not cp.dashcamOnly and not cp.notCar and
bool(flags & _GM_PEDAL_LONG_FLAG) and not flags & ~_CC_PEDAL_ALLOWED_FLAGS and
bool(flags & GMFlags.PEDAL_LONG) and not flags & ~int(GMFlags.PEDAL_LONG | GMFlags.NO_CAMERA | GMFlags.NO_ACCELERATOR_POS_MSG) and
not cp.pcmCruise and len(cp.safetyConfigs) == 1 and
cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
int(cp.safetyConfigs[0].safetyParam) ==
@@ -1182,15 +1147,13 @@ SILVERADO_CC_PEDAL_WORDS = ((0xC182, 0xC183), (0xC184, 0xC185))
def is_silverado_cc_pedal_profile(cp):
"""Exact Silverado interceptor owner, including its distinct stock cancellation."""
try:
if cp.brand != 'gm' or cp.carFingerprint != CAR.CHEVROLET_SILVERADO_CC:
return False
flags = control_flags(cp)
removed = bool(flags & GMFlags.NO_CAMERA)
word = SILVERADO_CC_PEDAL_WORDS[not cp.openpilotLongitudinalControl][removed]
return (cp.brand == 'gm' and cp.carFingerprint == CAR.CHEVROLET_SILVERADO_CC and
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and cp.radarUnavailable and
not cp.passive and not cp.dashcamOnly and not cp.notCar and
bool(flags & _GM_PEDAL_LONG_FLAG) and not flags & ~_CC_PEDAL_ALLOWED_FLAGS and
bool(flags & GMFlags.PEDAL_LONG) and not flags & ~int(GMFlags.PEDAL_LONG | GMFlags.NO_CAMERA | GMFlags.NO_ACCELERATOR_POS_MSG) and
not cp.pcmCruise and not cp.alphaLongitudinalAvailable and len(cp.safetyConfigs) == 1 and
cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and int(cp.safetyConfigs[0].safetyParam) == word)
except (AttributeError, IndexError, TypeError, ValueError):
@@ -120,9 +120,6 @@ class CarController(CarControllerBase):
self.last_torque = 0.0
def update(self, CC, CS, now_nanos):
_projection = self.learning_projection_observer
if _projection is not None:
_projection.begin(CC.actuators.torque, self.frame)
actuators = CC.actuators
hud_control = CC.hudControl
hud_v_cruise = hud_control.setSpeed / CS.v_cruise_factor if hud_control.speedVisible else 255
@@ -138,12 +135,9 @@ class CarController(CarControllerBase):
gas, brake = 0.0, 0.0
# *** rate limit steer ***
if _projection is not None:
_projection.capture_honda(self.modified_civic_steering, CS)
torque_cmd = self.modified_civic_steering.update(CC, CS) if self.modified_civic_steering is not None else actuators.torque
limited_torque = rate_limit(torque_cmd, self.last_torque, -self.params.STEER_DELTA_DOWN * DT_CTRL,
self.params.STEER_DELTA_UP * DT_CTRL)
_projection_previous = self.last_torque if _projection is not None else 0.
self.last_torque = limited_torque
# *** apply brake hysteresis ***
@@ -161,10 +155,6 @@ class CarController(CarControllerBase):
# Preserve integer CAN rounding while steering limits are owned by the platform.
apply_torque = int(np.interp(-limited_torque * self.params.STEER_MAX,
self.params.STEER_LOOKUP, self.params.STEER_LOOKUP_V))
if _projection is not None:
_projection.input(apply_torque, self.params.STEER_MAX, _projection_previous, CS.out.steeringTorque, self.params,
limiter='normalized_rate', mapping='honda_lookup', codec_input=torque_cmd,
lookup_x=self.params.STEER_LOOKUP, lookup_y=self.params.STEER_LOOKUP_V, limited_input=limited_torque)
# Send CAN commands
can_sends = []
@@ -176,8 +166,6 @@ class CarController(CarControllerBase):
# Send steering command.
can_sends.append(hondacan.create_steering_control(self.packer, self.CAN, apply_torque, CC.latActive, self.tja_control))
if _projection is not None:
_projection.emit('honda.STEERING_CONTROL', apply_torque, CC.latActive, can_sends[-1])
# wind brake from air resistance decel at high speed
wind_brake = np.interp(CS.out.vEgo, [0.0, 2.3, 35.0], [0.001, 0.002, 0.15])
@@ -287,6 +275,4 @@ class CarController(CarControllerBase):
new_actuators.torqueOutputCan = apply_torque
self.frame += 1
if _projection is not None:
_projection.finish(new_actuators.torque)
return new_actuators, can_sends
@@ -1190,7 +1190,6 @@ FW_VERSIONS = {
(Ecu.fwdRadar, 0x18dab0f1, None): [
b'8S302-3A0-A060\x00\x00',
b'8S302-3A0-A220\x00\x00',
b'8S302-3A0-A230\x00\x00',
b'8S302-3C0-Q050\x00\x00',
b'8S302-3D4-A050\x00\x00',
b'8S302-3D4-A060\x00\x00',
+1 -6
View File
@@ -70,16 +70,11 @@ def temporary_restriction(cp, state):
(state.gearShifter not in (gears.drive, gears.sport, gears.low, gears.brake) or state.steerFaultTemporary))
def native_observation(native, *, latched, panda_ready, restricted, now_ns, pending_since_ns,
previously_acknowledged=False, host_requested_lateral=False):
def native_observation(native, *, latched, panda_ready, restricted, now_ns, pending_since_ns):
lost = not panda_ready or native is None
if not latched or lost or restricted:
return 0, lost, False
if native.requestedLateral and native.lateralAllowed:
return 0, False, False
if (previously_acknowledged and host_requested_lateral and
not native.requestedLateral and not native.lateralAllowed):
# A current host request lost an established grant, not a cold handshake.
return 0, False, True
pending_since_ns = pending_since_ns or now_ns
return pending_since_ns, False, now_ns - pending_since_ns >= 200_000_000
@@ -5,7 +5,7 @@ import pytest
from opendbc.car import gen_empty_fingerprint, structs
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.stock_aol import CLASSIC_BOSCH, NIDEC, RADARLESS, qualified, temporary_restriction, native_observation
from opendbc.car.honda.stock_aol import CLASSIC_BOSCH, NIDEC, RADARLESS, qualified, temporary_restriction
from opendbc.car.honda.values import CAR, HondaFlags
from openpilot.starpilot.aol.intent import AolSettings, AOL_TOGGLE
from openpilot.starpilot.car.honda.aol import policy_for, create_intent, native_accepts_cp
@@ -347,37 +347,6 @@ def test_actual_card_active_observation_requires_fresh_applied_command(monkeypat
assert tick(previous=True, long=True)["standard_active"]
assert intent.allowed_latch # Actual normal long activity also proves active engagement.
# The actual Card caller consumes the acknowledged native permission. Its
# immediate rejection boundary remains strict; no host grace is extended.
tick(enabled=False)
cs = state(True)
native = replace(native, requestedLateral=True, lateralAllowed=True,
observedMonoTime=now + 10_000_000, validUntilMonoTime=now + 210_000_000)
tick(enabled=False)
assert not intent.allowed_latch # The previously active latch toggles OFF.
cs = state(False)
native = replace(native, observedMonoTime=now + 10_000_000)
tick(enabled=False)
cs = state(True)
native = replace(native, observedMonoTime=now + 10_000_000)
tick(enabled=False)
assert intent.allowed_latch # Release then a new physical press toggles ON.
cs = state(False)
native = replace(native, observedMonoTime=now + 10_000_000)
assert tick(enabled=False)["native_rejection_ns"] == 0
assert intent.allowed_latch
native = replace(native, lateralAllowed=False, observedMonoTime=now + 10_000_000)
observed_native = native.observedMonoTime
assert tick(enabled=False)["native_rejection_ns"] == observed_native
assert not intent.allowed_latch # Native denial immediately revokes independent intent.
native = replace(native, lateralAllowed=True, observedMonoTime=now + 10_000_000)
assert tick(enabled=False)["native_rejection_ns"] == 0
assert not intent.allowed_latch # Permission recovery alone cannot recreate a gesture.
cs = state(True)
native = replace(native, observedMonoTime=now + 10_000_000)
tick(enabled=False)
assert intent.allowed_latch # A genuinely new LKAS edge can arm again.
generic = AolCardIntent(intent.settings)
host.aol_card_intent = generic
@@ -388,97 +357,3 @@ def test_actual_card_active_observation_requires_fresh_applied_command(monkeypat
monkeypatch.setattr(generic, "update", generic_boundary)
with pytest.raises(BoundaryReached):
host.state_update()
@pytest.mark.parametrize("acknowledged,requested,reset", ((False, True, False), (True, True, True),
(True, False, False), (False, False, False)))
def test_native_cold_pending_vs_requested_ack_loss(acknowledged, requested, reset):
native = SimpleNamespace(requestedLateral=False, lateralAllowed=False)
result = native_observation(native, latched=True, panda_ready=True, restricted=False,
now_ns=1_000_000_000, pending_since_ns=0,
previously_acknowledged=acknowledged, host_requested_lateral=requested)
assert result == ((0, False, True) if reset else (1_000_000_000, False, False))
if not reset:
assert native_observation(native, latched=True, panda_ready=True, restricted=False,
now_ns=1_200_000_000, pending_since_ns=result[0],
previously_acknowledged=acknowledged, host_requested_lateral=requested) == (result[0], False, True)
@pytest.mark.parametrize("changed", ("cold", "session", "panda", "cp", "manual_off", "restriction",
"software_pause", "host_withdrawal", "invalid_axis", "stale_axis",
"replaced_producer", "older_native", "resuming_pause", "pending_pause", "requested_denial", "unpaired_event",
"fault_event", "invalid_event", "stale_event", "none"))
def test_actual_card_ack_loss_requires_current_requested_same_identity(changed):
from openpilot.selfdrive.car.card import Car
from openpilot.starpilot.aol.transport_pause import TransportPauseFeedback
from openpilot.starpilot.aol.wire import SafetyState
cp = final_cp(CAR.HONDA_CIVIC_2022, alpha=True, hybrid=True)
cp.alternativeExperience |= 32
now = 2_000_000_000
native = SafetyState(1, True, now, now + 200_000_000, int(cp.safetyConfigs[0].safetyModel.raw),
cp.safetyConfigs[0].safetyParam, True, False, True, False, "panda", "axis")
axis = SimpleNamespace(qualified=True, sessionId="axis", observedMonoTime=now, validUntilMonoTime=now+30_000_000,
faultSessionId="axis", faultReason="none", faultEventMonoTime=now, sequence=1,
nativeAcknowledged=True, desiredLateral=True, lateralActive=True, sourceCarStateMonoTime=now,
intentProducerSessionId="producer")
class Samples(dict):
pass
sm = Samples(aolAxisState=axis, onroadEvents=[])
sm.valid = sm.alive = sm.seen = {"aolAxisState": True, "onroadEvents": True}
sm.logMonoTime = {"aolAxisState": now, "onroadEvents": now}
host = Car.__new__(Car)
host.CP, host.sm, host.slc_producer_session = cp, sm, "producer"
host.aol_transport_feedback = TransportPauseFeedback(cp)
kwargs = dict(latched=True, panda_ready=True, restricted=False, now_ns=now, pending_since_ns=0)
assert host.aol_honda_native_observation(native, **kwargs) == (0, False, False)
if changed == "cold":
host._honda_aol_acknowledgment = None
elif changed == "session":
native = replace(native, axisSessionId="replacement")
elif changed == "panda":
native = replace(native, pandaSerial="replacement")
elif changed == "cp":
host.CP = final_cp(CAR.HONDA_CIVIC_2022, alpha=False, hybrid=True)
elif changed == "manual_off":
kwargs['latched'] = False
elif changed == "restriction":
kwargs['restricted'] = True
elif changed == "software_pause":
axis.faultReason = "availabilityPause"
host.aol_transport_feedback.retention_qualified = True
elif changed == "host_withdrawal":
axis.desiredLateral = False
elif changed == "invalid_axis":
sm.valid = {"aolAxisState": False}
elif changed == "stale_axis":
kwargs['now_ns'] += 40_000_000
elif changed == "replaced_producer":
axis.intentProducerSessionId = "replacement"
elif changed == "resuming_pause":
axis.lateralActive = False
elif changed == "pending_pause":
host.aol_transport_feedback.awaiting = True
elif changed == "unpaired_event":
axis.faultEventMonoTime -= 1
elif changed == "fault_event":
sm["onroadEvents"] = [SimpleNamespace(immediateDisable=True, softDisable=False)]
elif changed == "invalid_event":
sm.valid = {"aolAxisState": True, "onroadEvents": False}
elif changed == "stale_event":
axis.faultEventMonoTime = sm.logMonoTime["onroadEvents"] = now-1_500_000_001
elif changed == "older_native":
native = replace(native, observedMonoTime=now-1)
withdrawn = replace(native, requestedLateral=False, lateralAllowed=False)
if changed == "requested_denial":
withdrawn = replace(withdrawn, requestedLateral=True)
result = host.aol_honda_native_observation(withdrawn, **kwargs)
if changed == "none":
assert result == (0, False, True)
else:
assert not result[2] # No unexpected-loss cause is inferred from ambiguous zero.
if changed in ("manual_off", "restriction"):
assert result == (0, False, False)
if changed in ("cold", "session", "panda", "cp", "manual_off"):
assert host._honda_aol_acknowledgment is None
+1 -1
View File
@@ -256,7 +256,7 @@ class CAR(Platforms):
HONDA_CRV_6G = HondaBoschCANFDPlatformConfig(
[
HondaCarDocs("Honda CR-V 2023-26", "All"),
HondaCarDocs("Honda CR-V Hybrid 2023-27", "All"),
HondaCarDocs("Honda CR-V Hybrid 2023-26", "All"),
],
CarSpecs(mass=1703, wheelbase=2.7, steerRatio=16.2, centerToFrontRatio=0.42),
)
@@ -62,8 +62,7 @@ def candidate_from_stock(cp, *, alpha_requested, native_qualified=False):
candidate.alphaLongitudinalAvailable = True
candidate.openpilotLongitudinalControl = True
candidate.pcmCruise = False
from opendbc.car.hyundai.radar_interface import blended_track_radar
candidate.radarUnavailable = cp.radarUnavailable if blended_track_radar(cp) else True
candidate.radarUnavailable = True
candidate.safetyConfigs[0].safetyParam |= HyundaiSafetyFlags.LONG.value
# Modern mixed alpha namespace2004/2014; cancel/LDA semantics are local.
candidate.stopAccel = -.85
@@ -1,36 +0,0 @@
"""Selected safety slot for the exact first-generation captured CANFD owner."""
from opendbc.car.structs import CarParams
from opendbc.car.hyundai.values import CAR, HyundaiFlags
def topology_index(cp):
if len(cp.safetyConfigs) == 1:
return 0 if cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.hyundaiCanfd else None
if len(cp.safetyConfigs) != 2:
return None
auxiliary, owner = cp.safetyConfigs
required = HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEER_MSG
excluded = (HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT |
HyundaiFlags.CANFD_ALT_BUTTONS | HyundaiFlags.CANFD_CAMERA_SCC | HyundaiFlags.CCNC | HyundaiFlags.HYBRID)
if (cp.brand != 'hyundai' or cp.carFingerprint not in (CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN) or
cp.passive or cp.dashcamOnly or cp.notCar or cp.steerControlType != CarParams.SteerControlType.torque or
cp.flags & required != required or cp.flags & excluded or
auxiliary.safetyModel != CarParams.SafetyModel.noOutput or auxiliary.safetyParam != 0 or
owner.safetyModel != CarParams.SafetyModel.hyundaiCanfd):
return None
return 1
def config_index(cp):
owner = topology_index(cp)
if owner != 1:
return owner
raw = int(cp.safetyConfigs[owner].safetyParam)
if cp.openpilotLongitudinalControl and not cp.pcmCruise:
if raw == 0x15 and cp.alternativeExperience == 0:
return 1
if cp.carFingerprint == CAR.KIA_EV6 and raw == 0x815 and cp.alternativeExperience == 32:
return 1
elif not cp.openpilotLongitudinalControl and cp.pcmCruise and cp.alternativeExperience == 0 and raw in (0x11, 0x811):
return 1
return None
@@ -3,7 +3,6 @@
from opendbc.car.structs import CarParams
from opendbc.car.hyundai.values import CAR, HyundaiFlags
from opendbc.car.hyundai.hyundaicanfd import CanBus
from opendbc.car.hyundai.canfd_owner import config_index
STOCK_AOL_MARKER = 0x0800
STOCK_EV_CARS = frozenset(
@@ -28,24 +27,22 @@ STOCK_AOL_WORDS = frozenset(
and (not extra & 0x2000 or (topology in (32, 40, 48, 176) and gas != 1))
)
STOCK_AOL_FLAGS = (
HyundaiFlags.CANFD
| HyundaiFlags.EV
| HyundaiFlags.HYBRID
| HyundaiFlags.CANFD_LKA_STEER_MSG
| HyundaiFlags.CANFD_LKA_STEER_MSG_ALT
| HyundaiFlags.CANFD_ALT_BUTTONS
| HyundaiFlags.CANFD_CAMERA_SCC
| HyundaiFlags.CANFD_ALT_GEARS
| HyundaiFlags.CANFD_ALT_GEARS_2
| HyundaiFlags.CANFD_RADAR_SCC
| HyundaiFlags.CANFD_NO_RADAR_DISABLE
| HyundaiFlags.CCNC
)
def _base_word(cp, *, experiences=(0,)):
owner = config_index(cp)
allowed = STOCK_AOL_FLAGS
allowed = (
HyundaiFlags.CANFD
| HyundaiFlags.EV
| HyundaiFlags.HYBRID
| HyundaiFlags.CANFD_LKA_STEER_MSG
| HyundaiFlags.CANFD_LKA_STEER_MSG_ALT
| HyundaiFlags.CANFD_ALT_BUTTONS
| HyundaiFlags.CANFD_CAMERA_SCC
| HyundaiFlags.CANFD_ALT_GEARS
| HyundaiFlags.CANFD_ALT_GEARS_2
| HyundaiFlags.CANFD_RADAR_SCC
| HyundaiFlags.CANFD_NO_RADAR_DISABLE
| HyundaiFlags.CCNC
)
try:
factory = CAR(cp.carFingerprint)
except ValueError:
@@ -62,7 +59,8 @@ def _base_word(cp, *, experiences=(0,)):
or cp.steerControlType != CarParams.SteerControlType.torque
or not cp.flags & HyundaiFlags.CANFD
or int(cp.flags) & ~int(allowed)
or owner is None
or len(cp.safetyConfigs) != 1
or cp.safetyConfigs[0].safetyModel != CarParams.SafetyModel.hyundaiCanfd
):
return None
# EV identity is static; hybrid fuel is selected by the actual 0xFA fingerprint.
@@ -76,8 +74,7 @@ def _base_word(cp, *, experiences=(0,)):
if (ev and hybrid) or (alt_lka and not lka) or (lka and camera):
return None
bus = CanBus(cp)
offset = 4 * owner
if (bus.ACAN - offset, bus.ECAN - offset, bus.CAM - offset) != ((0, 1, 2) if lka else (1, 0, 2)):
if (bus.ACAN, bus.ECAN, bus.CAM) != ((0, 1, 2) if lka else (1, 0, 2)):
return None
word = (1 if ev else 2 if hybrid else 0) | (16 if lka else 0) | (128 if alt_lka else 0)
word |= (32 if alt_buttons else 0) | (8 if camera else 0)
@@ -92,7 +89,7 @@ def qualified(cp, *, marked_only=False):
word = _base_word(cp)
if word is None or cp.openpilotLongitudinalControl or not cp.pcmCruise:
return False
return cp.safetyConfigs[config_index(cp)].safetyParam in ((word | STOCK_AOL_MARKER,) if marked_only else (word, word | STOCK_AOL_MARKER))
return cp.safetyConfigs[0].safetyParam in ((word | STOCK_AOL_MARKER,) if marked_only else (word, word | STOCK_AOL_MARKER))
LONG_AOL_WORDS = frozenset((0x0815, 0x0895))
@@ -6,30 +6,21 @@ from opendbc.can.parser import MAX_BAD_COUNTER
from opendbc.car.hyundai.ecu_startup import HyundaiECUStartup, Outcome
from opendbc.car.hyundai.hyundaicanfd import CanBus
from opendbc.car.hyundai.values import HyundaiFlags
from opendbc.car.hyundai.canfd_owner import topology_index
def first_generation(cp):
owner = topology_index(cp)
if owner is None:
return False
buses = CanBus(cp)
offset = 4 * owner
excluded = (HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT |
HyundaiFlags.CANFD_ALT_BUTTONS | HyundaiFlags.CANFD_CAMERA_SCC | HyundaiFlags.CCNC |
HyundaiFlags.HYBRID)
return (cp.flags & HyundaiFlags.CANFD and
cp.flags & HyundaiFlags.EV and cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG and
not cp.flags & excluded and (buses.ACAN, buses.ECAN, buses.CAM) == (offset, offset + 1, offset + 2) and
not cp.flags & excluded and CanBus(cp).ACAN == 0 and CanBus(cp).ECAN == 1 and
not cp.passive and not cp.dashcamOnly and not cp.notCar)
class CapturedADRVStartup(HyundaiECUStartup):
def __init__(self, cp, callbacks, *, label, template_type):
buses = CanBus(cp)
super().__init__(cp, callbacks, address=0x730, bus=buses.ECAN, label=label)
self.template_bus = buses.ACAN
self.camera_bus = buses.CAM
super().__init__(cp, callbacks, address=0x730, bus=1, label=label)
self.template_type = template_type
self.template = None
@@ -44,7 +35,7 @@ class CapturedADRVStartup(HyundaiECUStartup):
if not floor < stamp <= now or now - stamp > 150_000_000:
continue
for msg in packet:
if msg.src != self.template_bus or msg.address != 0x51:
if msg.src != 0 or msg.address != 0x51:
continue
try:
candidate = self.template_type.capture(bytes(msg.dat))
@@ -75,7 +66,7 @@ class CapturedADRVStartup(HyundaiECUStartup):
return False
pt = ci.can_parsers[Bus.pt]
cam = ci.can_parsers[Bus.cam]
if pt.bus != self.bus or cam.bus != self.camera_bus:
if pt.bus != 1 or cam.bus != 2:
return False
names = ['ACCELERATOR', 'TCS', 'WHEEL_SPEEDS', 'MDPS', 'CRUISE_BUTTONS', ci.CS.gear_msg_canfd]
if not ci.CP.openpilotLongitudinalControl:
@@ -149,9 +149,6 @@ class CarController(CarControllerBase):
self._ray_disengage_frame = None
def update(self, CC, CS, now_nanos):
_projection = self.learning_projection_observer
if _projection is not None:
_projection.begin(CC.actuators.torque, self.frame)
actuators = CC.actuators
if self.CP.carFingerprint == CAR.KIA_EV6_2025 and self.CP.dashcamOnly:
return actuators.as_builder(), []
@@ -169,8 +166,6 @@ class CarController(CarControllerBase):
if self.CP.flags & HyundaiFlags.CANFD and self.angle_vm is None:
self.params = CarControllerParams(self.CP, CS.out.vEgoRaw)
new_torque = int(round(actuators.torque * self.params.STEER_MAX))
if _projection is not None:
_projection.input(new_torque, self.params.STEER_MAX, self.apply_torque_last, CS.out.steeringTorque, self.params, limiter='driver')
apply_torque = (0 if self.angle_vm is not None else
apply_driver_steer_torque_limits(new_torque, self.apply_torque_last, CS.out.steeringTorque, self.params))
@@ -250,13 +245,10 @@ class CarController(CarControllerBase):
new_actuators.accel = accel
self.frame += 1
if _projection is not None:
_projection.finish(new_actuators.torque)
return new_actuators, can_sends
def create_can_msgs(self, apply_steer_req, apply_torque, torque_fault, set_speed_in_units, accel, stopping,
hud_control, actuators, CS, CC, *, lead_data=None):
_projection = self.learning_projection_observer
can_sends = []
# HUD messages
@@ -275,8 +267,6 @@ class CarController(CarControllerBase):
icon = 2 if CC.enabled or CC.latActive else 3 if disengaging else 1
if self.CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG:
can_sends.append(hyundaican.create_blended_steering(self.packer, self.CAN, apply_torque, apply_steer_req, icon))
if _projection is not None:
_projection.emit('hyundai.BLENDED_TORQUE', apply_torque, apply_steer_req, can_sends[-1])
if self.blended_longitudinal is not None and self.blended_longitudinal.owner.active():
can_sends.extend(hyundaican.create_lkas11_can_canfd_blended(
self.packer, self.frame, self.CP, apply_torque, apply_steer_req, torque_fault, CS.lkas11,
@@ -287,22 +277,16 @@ class CarController(CarControllerBase):
CS.blended_camera_current):
can_sends.append(hyundaicanfd.create_suppress_lfa(self.packer, self.CAN, CS.lfa_block_msg, False))
else:
_projection_packet_index = len(can_sends) if _projection is not None else 0
can_sends.extend(hyundaican.create_lkas11_can_canfd_blended(
self.packer, self.frame, self.CP, apply_torque, apply_steer_req, torque_fault, CS.lkas11,
sys_warning, sys_state, CC.enabled, hud_control.leftLaneVisible, hud_control.rightLaneVisible,
left_lane_warning, right_lane_warning, CS.msg_364))
if _projection is not None:
_projection.emit('hyundai.BLENDED_LKAS11', apply_torque, apply_steer_req, can_sends[_projection_packet_index])
elif self.CP.carFingerprint != CAR.KIA_RAY_EV or self._ray_lkas11_active:
can_sends.append(hyundaican.create_lkas11(self.packer, self.frame, self.CP, apply_torque, apply_steer_req,
torque_fault, CS.lkas11, sys_warning, sys_state, hud_enabled,
hud_control.leftLaneVisible, hud_control.rightLaneVisible,
left_lane_warning, right_lane_warning,
ray_lka_icon=(self._ray_lfa_icon or 1) if self.CP.carFingerprint == CAR.KIA_RAY_EV else None))
if _projection is not None and can_sends:
if not is_blended(self.CP) and (self.CP.carFingerprint != CAR.KIA_RAY_EV or self._ray_lkas11_active):
_projection.emit('hyundai.LKAS11', apply_torque, apply_steer_req, can_sends[-1])
if self.CP.carFingerprint == CAR.KIA_RAY_EV:
self._ray_lkas11_active = True
@@ -361,7 +345,6 @@ class CarController(CarControllerBase):
return can_sends
def create_canfd_msgs(self, apply_steer_req, apply_torque, set_speed_in_units, accel, stopping, hud_control, CS, CC, now_nanos):
_projection = self.learning_projection_observer
can_sends = []
canfd_lfa_icon = (self.canfd_dash_icons.update(self.frame, CC.enabled, CC.latActive)
if self.canfd_dash_icons is not None else None)
@@ -461,8 +444,6 @@ class CarController(CarControllerBase):
steering_status_active |= CC.latActive
can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN,
steering_status_active, apply_steer_req, apply_torque))
if _projection is not None:
_projection.emit('hyundai.CANFD_TORQUE', apply_torque, apply_steer_req, can_sends[-1])
# prevent LFA from activating on LKA steering cars by sending "no lane lines detected" to ADAS ECU
if (
@@ -1,26 +1,14 @@
"""Hyundai prepublication ECU transaction; sent disable is not confirmed ownership."""
from enum import Enum
from collections.abc import Callable
from typing import TypedDict, cast
from copy import deepcopy
import time
from opendbc.car import structs
from opendbc.car.disable_ecu import disable_ecu
from opendbc.car.hyundai.values import HyundaiSafetyFlags
from opendbc.car.hyundai.canfd_owner import config_index
from opendbc.car.isotp_parallel_query import IsoTpParallelQuery
class PreparedSafetyConfig(TypedDict):
safetyParam: int
class PreparedParams(TypedDict):
safetyConfigs: list[PreparedSafetyConfig]
alternativeExperience: int
class Outcome(Enum):
STOCK_UNTOUCHED = 'stock_untouched'
SENT_UNCONFIRMED = 'sent_unconfirmed'
@@ -72,8 +60,7 @@ class HyundaiECUStartup:
self.callbacks[1](frames)
def _restore(self):
admission = cast(Callable[[], bool], self.admission)
if not admission():
if not self.admission():
return False
recv, send = self.callbacks
session = self._query(send, recv, self.bus, [(self.address, None)], [b'\x10\x03'], [b'\x50\x03'])
@@ -139,12 +126,11 @@ class HyundaiECUStartup:
return
from opendbc.car.hyundai.ev6_aol import qualified as ev6_aol_qualified
if self.outcome is Outcome.SENT_UNCONFIRMED and ev6_aol_qualified(ci.CP, marked_only=True):
expected = deepcopy(cast(PreparedParams, self.prepared_cp))
expected = deepcopy(self.prepared_cp)
configs = expected.get('safetyConfigs', [])
owner = config_index(ci.CP)
if owner is None or len(configs) != owner + 1 or configs[owner]['safetyParam'] != 0x15 or expected['alternativeExperience'] != 0:
if len(configs) != 1 or configs[0]['safetyParam'] != 0x15 or expected['alternativeExperience'] != 0:
raise RuntimeError('EV6 prepared LONG profile cannot be finalized')
configs[owner]['safetyParam'] |= 0x0800
configs[0]['safetyParam'] |= 0x0800
expected['alternativeExperience'] = 32
if ci.CP.to_dict() != expected:
raise RuntimeError('EV6 AOL finalization changed unrelated CarParams')
@@ -154,12 +140,11 @@ class HyundaiECUStartup:
if (self.outcome not in (Outcome.STOCK_UNTOUCHED, Outcome.STOCK_RESTORED) or
not stock_aol_qualified(ci.CP, marked_only=True)):
raise RuntimeError('Hyundai startup does not admit this AOL configuration')
expected = deepcopy(cast(PreparedParams, self.prepared_cp))
expected = deepcopy(self.prepared_cp)
configs = expected.get('safetyConfigs', [])
owner = config_index(ci.CP)
if owner is None or len(configs) != owner + 1 or configs[owner]['safetyParam'] not in (0x11, 0x91):
if len(configs) != 1 or configs[0]['safetyParam'] not in (0x11, 0x91):
raise RuntimeError('Hyundai startup stock profile cannot be finalized')
configs[owner]['safetyParam'] |= 0x0800
configs[0]['safetyParam'] |= 0x0800
if ci.CP.to_dict() != expected:
raise RuntimeError('Hyundai AOL finalization changed unrelated CarParams')
self.prepared_cp = expected
+1 -2
View File
@@ -1,6 +1,5 @@
"""First-generation EV6 physical authorization for the existing torque lease."""
from opendbc.car.hyundai.canfd_stock_aol import _base_word
from opendbc.car.hyundai.canfd_owner import config_index
from opendbc.car.hyundai.ev6_startup import eligible
from opendbc.car.hyundai.values import Buttons
@@ -10,7 +9,7 @@ def qualified(cp, *, marked_only=False):
return False
if _base_word(cp, experiences=(0, 32)) != 0x11:
return False
word = cp.safetyConfigs[config_index(cp)].safetyParam
word = cp.safetyConfigs[0].safetyParam
return (word == 0x815 and cp.alternativeExperience == 32) or (
not marked_only and word == 0x15 and cp.alternativeExperience == 0)
@@ -1,10 +1,9 @@
import math
from binascii import crc_hqx
from typing import cast
import numpy as np
from opendbc.car import CanBusBase, CanData
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.crc import CRC16_XMODEM
from opendbc.car.hyundai.ioniq6_bsm import BlindspotStatus, FRONT_NEUTRAL_BODY, rear_body
from opendbc.car.hyundai.values import CAR, HyundaiFlags
@@ -214,8 +213,8 @@ def create_acc_control(packer, CAN, enabled, accel_last, accel, stopping, gas_ov
object_distance, object_relative, object_valid, object_status = 1., 0., 0, 2
else:
visible = bool(lead_visible)
object_distance = float(np.clip(cast(float, lead_distance) if visible else 0., 0., 204.7))
object_relative = float(np.clip(cast(float, lead_rel_speed) if visible else 0., -16.4, 34.7))
object_distance = float(np.clip(lead_distance if visible else 0., 0., 204.7))
object_relative = float(np.clip(lead_rel_speed if visible else 0., -16.4, 34.7))
object_valid = int(not visible)
object_status = 0 if not (enabled and visible) else 1 if gas_override else 2
@@ -393,15 +392,11 @@ def create_adrv_messages(packer, CAN, frame, *, template=None, drive_gear=False,
values = {
}
from opendbc.car.hyundai.gv70_template import GV70Template
if template is not None:
if type(CAN.ACAN) is not int or type(CAN.offset) is not int:
raise ValueError('Captured ADRV requires integer CAN routing')
local_bus = CAN.ACAN - CAN.offset
packet = (template.frame(frame, drive_gear, local_bus, speed=speed) if isinstance(template, GV70Template) else
template.frame(frame, drive_gear, local_bus))
ret.append(CanData(packet.address, packet.dat, CAN.ACAN))
if isinstance(template, GV70Template):
ret.append(template.frame(frame, drive_gear, CAN.ACAN, speed=speed))
else:
ret.append(packer.make_can_msg("ADRV_0x51", CAN.ACAN, values))
ret.append(template.frame(frame, drive_gear, CAN.ACAN) if template is not None else
packer.make_can_msg("ADRV_0x51", CAN.ACAN, values))
ret.extend(create_fca_warning_light(packer, CAN, frame))
@@ -437,7 +432,11 @@ def create_adrv_messages(packer, CAN, frame, *, template=None, drive_gear=False,
def hkg_can_fd_checksum(address: int, sig, d: bytearray) -> int:
crc = crc_hqx(d[2:] + (address & 0xFFFF).to_bytes(2, "little"), 0)
crc = 0
for i in range(2, len(d)):
crc = ((crc << 8) ^ CRC16_XMODEM[(crc >> 8) ^ d[i]]) & 0xFFFF
crc = ((crc << 8) ^ CRC16_XMODEM[(crc >> 8) ^ ((address >> 0) & 0xFF)]) & 0xFFFF
crc = ((crc << 8) ^ CRC16_XMODEM[(crc >> 8) ^ ((address >> 8) & 0xFF)]) & 0xFFFF
if len(d) == 8:
crc ^= 0x5F29
elif len(d) == 16:
@@ -11,7 +11,7 @@ from opendbc.car.interfaces import CarInterfaceBase
from opendbc.car.disable_ecu import disable_ecu
from opendbc.car.hyundai.carcontroller import CarController
from opendbc.car.hyundai.carstate import CarState
from opendbc.car.hyundai.radar_interface import RadarInterface, blended_track_radar
from opendbc.car.hyundai.radar_interface import RadarInterface
ButtonType = structs.CarState.ButtonEvent.Type
Ecu = structs.CarParams.Ecu
@@ -150,8 +150,6 @@ class CarInterface(CarInterfaceBase):
else:
ret.flags |= HyundaiFlags.CANFD_ALT_GEARS.value
if candidate in (CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN) and CAN.offset >= 8:
ret.dashcamOnly = True
cfgs = [get_safety_config(structs.CarParams.SafetyModel.hyundaiCanfd), ]
if CAN.ECAN >= 4:
cfgs.insert(0, get_safety_config(structs.CarParams.SafetyModel.noOutput))
@@ -250,7 +248,7 @@ class CarInterface(CarInterfaceBase):
radar_start = KONA_NON_SCC_FCA_RADAR_ADDR if candidate == CAR.HYUNDAI_KONA_NON_SCC else RADAR_START_ADDR
if DBC[ret.carFingerprint].get(Bus.radar) == HYUNDAI_G90_RADAR_DBC:
ret.radarUnavailable = not all(fingerprint[1].get(addr) == 8 for addr in range(0x500, 0x520))
elif blended_track_radar(ret) or DBC[ret.carFingerprint].get(Bus.radar) == HYUNDAI_GV70_RADAR_DBC:
elif DBC[ret.carFingerprint].get(Bus.radar) == HYUNDAI_GV70_RADAR_DBC:
ret.radarUnavailable = not all(fingerprint[0].get(addr) == 32 for addr in range(0x210, 0x220))
elif DBC[ret.carFingerprint].get(Bus.radar) == HYUNDAI_MRR30_RADAR_DBC:
ret.radarUnavailable = fingerprint[radar_bus(ret)].get(MRR30_RADAR_START_ADDR) != 32
@@ -301,8 +299,6 @@ class CarInterface(CarInterfaceBase):
ret.autoResumeSng = False
ret.minEnableSpeed = -1.0
ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.LONG.value
ret.longitudinalTuning.kiBP = [0.0, 3.0, 6.0, 35.0]
ret.longitudinalTuning.kiV = [0.07, 0.10, 0.15, 0.24]
if candidate == CAR.KIA_RAY_EV and not ret.openpilotLongitudinalControl:
ret.dashcamOnly = True
@@ -3,8 +3,7 @@ import math
from opendbc.can import CANParser
from opendbc.car import Bus, structs
from opendbc.car.interfaces import RadarInterfaceBase
from opendbc.car.hyundai.values import (CAR, DBC, HyundaiFlags, HYUNDAI_MRR35_RADAR_DBC, HYUNDAI_MRR30_RADAR_DBC,
HYUNDAI_GV70_RADAR_DBC, HYUNDAI_G90_RADAR_DBC, is_blended)
from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags, HYUNDAI_MRR35_RADAR_DBC, HYUNDAI_MRR30_RADAR_DBC, HYUNDAI_GV70_RADAR_DBC, HYUNDAI_G90_RADAR_DBC
RADAR_START_ADDR = 0x500
RADAR_MSG_COUNT = 32
@@ -22,16 +21,6 @@ MRR35_RADAR_MSG_COUNT = 32
MRR35_RADAR_CYCLE_NS = 50_000_000 # 20 Hz
def blended_track_radar(CP) -> bool:
# Exact mixed classic PT / HDA-II steering identity, independently observed
# by the existing factory. Never relabel another classic or CANFD car.
return bool(CP.brand == 'hyundai' and is_blended(CP) and CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG)
def radar_dbc(CP):
return HYUNDAI_GV70_RADAR_DBC if blended_track_radar(CP) else DBC[CP.carFingerprint].get(Bus.radar)
def radar_bus(CP) -> int:
# Frozen Ioniq 6 camera-SCC wiring exposes the MRR35 object bank on bus 1;
# its LKA/HDA-II wiring and the other MRR35 cars expose it on bus 0.
@@ -43,28 +32,28 @@ def radar_bus(CP) -> int:
def get_radar_can_parser(CP):
if radar_dbc(CP) is None:
if Bus.radar not in DBC[CP.carFingerprint]:
return None
start, count = radar_message_range(CP)
mrr35 = radar_dbc(CP) == HYUNDAI_MRR35_RADAR_DBC
g90 = radar_dbc(CP) == HYUNDAI_G90_RADAR_DBC
gv70 = radar_dbc(CP) == HYUNDAI_GV70_RADAR_DBC
mrr35 = DBC[CP.carFingerprint].get(Bus.radar) == HYUNDAI_MRR35_RADAR_DBC
g90 = DBC[CP.carFingerprint].get(Bus.radar) == HYUNDAI_G90_RADAR_DBC
gv70 = DBC[CP.carFingerprint].get(Bus.radar) == HYUNDAI_GV70_RADAR_DBC
messages = [(f"RADAR_TRACK_{addr:x}",
20 if mrr35 and addr == start + count - 1 else
float("nan") if mrr35 or g90 and addr >= start + RADAR_MSG_COUNT else 20 if gv70 else 50) for addr in range(start, start + count)]
mrr30 = radar_dbc(CP) == HYUNDAI_MRR30_RADAR_DBC
return CANParser(radar_dbc(CP), messages, radar_bus(CP) if mrr35 or mrr30 or gv70 else 1)
mrr30 = DBC[CP.carFingerprint].get(Bus.radar) == HYUNDAI_MRR30_RADAR_DBC
return CANParser(DBC[CP.carFingerprint][Bus.radar], messages, radar_bus(CP) if mrr35 or mrr30 or gv70 else 1)
def radar_message_range(CP) -> tuple[int, int]:
if radar_dbc(CP) == HYUNDAI_G90_RADAR_DBC:
if DBC[CP.carFingerprint].get(Bus.radar) == HYUNDAI_G90_RADAR_DBC:
return RADAR_START_ADDR, G90_RADAR_MSG_COUNT
if radar_dbc(CP) in (HYUNDAI_MRR30_RADAR_DBC, HYUNDAI_GV70_RADAR_DBC):
if DBC[CP.carFingerprint].get(Bus.radar) in (HYUNDAI_MRR30_RADAR_DBC, HYUNDAI_GV70_RADAR_DBC):
return MRR30_RADAR_START_ADDR, MRR30_RADAR_MSG_COUNT
if radar_dbc(CP) == HYUNDAI_MRR35_RADAR_DBC:
if DBC[CP.carFingerprint].get(Bus.radar) == HYUNDAI_MRR35_RADAR_DBC:
return MRR35_RADAR_START_ADDR, MRR35_RADAR_MSG_COUNT
if radar_dbc(CP) == "hyundai_mrrevo14f_radar_generated":
if DBC[CP.carFingerprint].get(Bus.radar) == "hyundai_mrrevo14f_radar_generated":
return MRREVO14F_RADAR_START_ADDR, MRREVO14F_RADAR_MSG_COUNT
return RADAR_START_ADDR, RADAR_MSG_COUNT
@@ -74,10 +63,10 @@ class RadarInterface(RadarInterfaceBase):
super().__init__(CP)
self.updated_messages = set()
self.start_addr, self.msg_count = radar_message_range(CP)
self.g90 = radar_dbc(CP) == HYUNDAI_G90_RADAR_DBC
self.g90 = DBC[CP.carFingerprint].get(Bus.radar) == HYUNDAI_G90_RADAR_DBC
self.mrrevo14f = self.start_addr == MRREVO14F_RADAR_START_ADDR
self.mrr35 = self.start_addr == MRR35_RADAR_START_ADDR
self.gv70 = radar_dbc(CP) == HYUNDAI_GV70_RADAR_DBC
self.gv70 = DBC[CP.carFingerprint].get(Bus.radar) == HYUNDAI_GV70_RADAR_DBC
self.mrr30 = self.start_addr == MRR30_RADAR_START_ADDR and not self.gv70
self.last_mrr35_trigger_ns = 0
self.trigger_msg = self.start_addr + (RADAR_MSG_COUNT if self.g90 else self.msg_count) - 1
+5 -45
View File
@@ -1,13 +1,7 @@
import numpy as np
from opendbc.car import rate_limit
RAY_PEDAL_COMMAND_CAP = 0.70
RAY_PEDAL_CAP_SPEED_BP = [0.0, 12.0, 20.0, 25.0]
RAY_PEDAL_CAP_V = [0.55, 0.55, 0.65, RAY_PEDAL_COMMAND_CAP]
RAY_PEDAL_OFFSET_SPEED_BP = [0.0, 2.0, 4.0, 8.0, 12.0, 16.0, 20.0, 25.0]
RAY_PEDAL_OFFSET_V = [0.08, 0.13, 0.20, 0.32, 0.42, 0.51, 0.565, 0.60]
RAY_PEDAL_ACCEL_GAIN = 0.22
RAY_PEDAL_DECEL_GAIN = 0.55
RAY_PEDAL_COMMAND_CAP = 0.55
RAY_PEDAL_RATE_UP = 0.02
RAY_PEDAL_RATE_DOWN = 0.06
RAY_PEDAL_OVERSPEED_CUTOFF = 0.5
@@ -40,52 +34,18 @@ def create_ray_pedal_command(packer, gas_amount, idx):
return packer.make_can_msg("GAS_COMMAND", 0, values)
def get_ray_pedal_target(accel, speed, set_speed):
if not all(np.isfinite(x) for x in (accel, speed, set_speed)) or not 1.0 <= set_speed <= 40.0:
def ray_pedal_gas(last, speed, accel, set_speed):
if not np.isfinite(set_speed) or not 1.0 <= set_speed <= 40.0:
return 0.0
speed_error = set_speed - speed
if speed_error <= -RAY_PEDAL_OVERSPEED_CUTOFF:
return 0.0
pedal_offset = float(np.interp(speed, RAY_PEDAL_OFFSET_SPEED_BP, RAY_PEDAL_OFFSET_V))
pedal_cap = float(np.interp(speed, RAY_PEDAL_CAP_SPEED_BP, RAY_PEDAL_CAP_V))
gain = RAY_PEDAL_DECEL_GAIN if accel < 0.0 else RAY_PEDAL_ACCEL_GAIN
target = float(np.clip(pedal_offset + accel * gain, 0.0, pedal_cap))
pedal_offset = float(np.interp(speed, [0., 2., 4., 8., 12., 20.], [0.08, 0.13, 0.20, 0.32, 0.42, 0.48]))
target = float(np.clip(pedal_offset + accel * (2.0 if accel < 0.0 else 0.22), 0.0, RAY_PEDAL_COMMAND_CAP))
if speed_error < 0.0:
target *= float(np.clip(0.65 * (1.0 + speed_error / RAY_PEDAL_OVERSPEED_CUTOFF), 0.0, 1.0))
elif speed_error < RAY_PEDAL_TAPER_BELOW_TARGET:
target *= 0.65 + 0.35 * speed_error / RAY_PEDAL_TAPER_BELOW_TARGET
return target
def ray_pedal_gas(last, speed, accel, set_speed):
target = get_ray_pedal_target(accel, speed, set_speed)
if target <= 0.001:
return 0.0
return min(rate_limit(target, last, -RAY_PEDAL_RATE_DOWN, RAY_PEDAL_RATE_UP), target)
class RayPedalLongitudinalPolicy:
"""Own the selected Ray proportional gains and consume its supported factory integral gains."""
friction_variant = False
stopping_decel_rate = 1.0
def __init__(self, cp):
self.kp = ((0.0, 5.0, 15.0, 35.0), (0.095, 0.085, 0.065, 0.050))
self.ki = (tuple(cp.longitudinalTuning.kiBP), tuple(cp.longitudinalTuning.kiV))
def reset(self):
pass
def feedforward(self, target, _speed, _last_output):
return target
def prepare_pid(self, _pid, _target, _error, _speed, _last_output, _limits, *, should_stop, has_lead):
return False
def shape_output(self, output, _target, _error, _speed):
return output
def longitudinal_policy_for(cp):
return RayPedalLongitudinalPolicy(cp) if ray_pedal_enabled(cp) else None
@@ -148,14 +148,13 @@ class TestBlendedLifecycle(unittest.TestCase):
before = cp.to_dict()
candidate = candidate_from_stock(cp, alpha_requested=True, native_qualified=True)
self.assertIsNotNone(candidate)
expected_radar_unavailable = radar_unavailable if hda2 else True
self.assertEqual(candidate.radarUnavailable, expected_radar_unavailable)
self.assertTrue(candidate.radarUnavailable)
self.assertTrue(candidate.openpilotLongitudinalControl)
self.assertFalse(candidate.pcmCruise)
self.assertEqual(candidate.safetyConfigs[0].safetyParam, 0x2014 if hda2 else 0x2004)
expected = dict(before)
expected.update(alphaLongitudinalAvailable=True, openpilotLongitudinalControl=True,
pcmCruise=False, radarUnavailable=expected_radar_unavailable, stopAccel=candidate.stopAccel,
pcmCruise=False, radarUnavailable=True, stopAccel=candidate.stopAccel,
safetyConfigs=candidate.to_dict()['safetyConfigs'])
self.assertEqual(candidate.to_dict(), expected)
self.assertEqual(cp.to_dict(), before)
@@ -238,8 +237,8 @@ class TestBlendedLifecycle(unittest.TestCase):
calls.append('ack')
context.owner = SimpleNamespace(sent=sent)
car = card.Car.__new__(card.Car)
vars(car)['vehicle_startup'] = context
vars(car)['pm'] = SimpleNamespace(send=lambda *args: calls.append('published'))
car.vehicle_startup = context
car.pm = SimpleNamespace(send=lambda *args: calls.append('published'))
card.Car.publish_sendcan(car, [], valid=True)
self.assertEqual(calls, ['published', 'ack'])
@@ -321,23 +320,17 @@ class TestBlendedLifecycle(unittest.TestCase):
calls = []
CS = SimpleNamespace()
car = card.Car.__new__(card.Car)
def state_update(*, defer_radar=False):
self.assertTrue(defer_radar)
return CS, None
vars(car)['state_update'] = state_update
car._deferred_radar_can = None
vars(car)['RI'] = SimpleNamespace(update=lambda can_list: None)
vars(car)['state_publish'] = lambda *args: calls.append('state')
car.state_update = lambda: (CS, None)
car.state_publish = lambda *args: calls.append('state')
car.CP = SimpleNamespace(passive=False)
car.ci_initialized = False
class SM(dict):
seen = {'onroadEvents': False}
vars(car)['sm'] = SM(onroadEvents=[])
vars(car)['vehicle_startup'] = SimpleNamespace(owner=object(), maintain=lambda **kw: calls.append(('maintain', kw)))
vars(car)['startup_panda_configured'] = lambda: True
vars(car)['controls_update'] = lambda *args: self.fail('Controls are not initialized')
car.sm = SM(onroadEvents=[])
car.vehicle_startup = SimpleNamespace(owner=object(), maintain=lambda **kw: calls.append(('maintain', kw)))
car.startup_panda_configured = lambda: True
car.controls_update = lambda *args: self.fail('Controls are not initialized')
card.Car.step(car)
self.assertEqual(calls, [('maintain', {'configured': True}), 'state'])
self.assertIs(car.CS_prev, CS)

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