Compare commits

..

588 Commits

Author SHA1 Message Date
Prabhaav Pillai 88a3b5e2a5 Add car control sections and update projections in settings for AA support 2026-10-27 17:10:33 -04:00
Prabhaav Pillai 8ac6b224d7 Add support for host runtime and Bluetooth integration in StarPilot 2026-10-25 17:51:38 -04:00
firestar5683 d43477ec41 Refresh release receipt after Vienna sign polish 2026-10-24 15:12:30 -05:00
firestarsdog 0f981ad88f Vienna Sign Polish 2026-10-24 16:09:36 -04:00
Dom dbd28f84c4 Narrow Node executable before projection contract subprocess 2026-10-24 14:52:09 -05:00
firestar5683 f95f5a88cf Apply shared host warning policy to inactive AOL source pauses 2026-10-24 14:50:30 -05:00
firestar5683 5bef40f51c Cover shared AOL source pauses across all makes and Volt owners 2026-10-24 14:42:42 -05:00
firestarsdog 6b9b8a4dfb Unify SLC signs for clarity and fix Android Auto layout saving 2026-10-24 15:38:38 -04:00
firestar5683 823f6923c8 Retain AOL intent through fully withdrawn source pauses 2026-10-24 14:28:39 -05:00
firestar5683 9e6dabb149 Schedule slow Toyota caller modules from measured CI timings 2026-10-24 13:26:26 -05:00
firestar5683 9712480382 Retain Ford AOL intent through temporary EPS recovery with gas held 2026-10-24 12:42:09 -05:00
firestar5683 e5265f88b9 Preserve the local test runner discovery interface 2026-10-24 12:28:58 -05:00
firestar5683 4a9881ff5b Prevent synchronized crash-session uploads during driving 2026-10-24 12:25:09 -05:00
firestar5683 b00ca4c52e Retain armed reverse intent through an inactive source gap 2026-10-24 12:24:46 -05:00
firestar5683 25cbb14cbd Balance vehicle caller CI workloads from hosted timings 2026-10-24 11:38:52 -05:00
firestar5683 1ebbff5cd9 Isolate Android Auto rendering from driving cores 2026-10-24 11:29:24 -05:00
firestar5683 163751cb7e Keep wheel actions bound across equivalent CarParams layouts 2026-10-24 11:01:12 -05:00
Dom 91b378cc5b test: keep capture recovery publication on the synthetic clock 2026-10-24 10:58:33 -05:00
firestar5683 3f693e1735 Keep Ford angle AOL within native steering limits through turns and pedals 2026-10-24 10:40:09 -05:00
firestar5683 d646a5dea8 Format source study regression tests 2026-10-24 10:29:09 -05:00
firestar5683 0b54917f90 Keep bounded source trials progressing across road variation 2026-10-24 10:17:07 -05:00
firestar5683 e15bd6bf3d Keep Android Auto below controls priority 2026-10-24 10:11:30 -05:00
firestar5683 baf9a3fff6 Prevent Android Auto renderer CPU starvation 2026-10-24 10:08:26 -05:00
firestar5683 3b081b2fc2 Format source recovery regression tests 2026-10-24 09:55:07 -05:00
firestar5683 24126eafdc Keep source learning progressing after interrupted trials 2026-10-24 09:45:44 -05:00
firestar5683 44efc8d935 Allow Android Auto renderer cold startup to finish 2026-10-24 09:45:31 -05:00
firestar5683 5dddf9ae1d Stabilize native registry and asynchronous source fixtures 2026-10-24 00:10:31 -05:00
firestar5683 b863214df8 Preserve original RELEASE skip accounting across shards 2026-10-23 23:41:28 -05:00
firestar5683 6efe2e5379 Keep CI fixtures current and partition complete RELEASE contracts 2026-10-23 23:24:10 -05:00
firestar5683 04c590f7c4 Exercise composite MAIN freshness through native steering transmission 2026-10-23 22:52:46 -05:00
firestar5683 39d30d89c1 Correct pose interval fixture typing 2026-10-23 22:48:49 -05:00
firestar5683 567bde2b4e Balance independent Toyota caller tests across custom jobs 2026-10-23 22:44:02 -05:00
firestar5683 4f4cb4e7d9 Align source fixtures and cover composite MAIN freshness 2026-10-23 22:43:56 -05:00
firestar5683 15dc2cb26c Distribute the growing lifecycle catalog across more jobs 2026-10-23 22:35:57 -05:00
firestar5683 317f08d15a Separate eager startup fixtures from deferred takeover 2026-10-23 22:32:50 -05:00
firestar5683 c0ae7127ef Keep vehicle refusal fixtures aligned with qualified configurations 2026-10-23 22:31:54 -05:00
firestar5683 b50b5cc1a3 Allow slow source tracking experiments from measured native sensitivity 2026-10-23 22:29:52 -05:00
firestar5683 e9d39b4f5e Repair learner receipt test fixtures 2026-10-23 22:27:02 -05:00
firestar5683 d5700b6910 Account for combined clock resolution in motion admission 2026-10-23 21:54:31 -05:00
firestar5683 b0c054d881 Reacquire SDGM Volt steering after inactive brake feedback 2026-10-23 21:39:15 -05:00
firestar5683 d4dbb83a55 Build matching Toyota pedal and hold firmware 2026-10-23 21:29:59 -05:00
firestar5683 b6b820a275 Support shared Toyota pedal and brake hold paths 2026-10-23 21:26:01 -05:00
firestar5683 d8a218b807 Restore GM AOL arming from persistent cruise main 2026-10-23 21:02:48 -05:00
firestar5683 0a2ee300e7 Build matching BluePilot angle and Ford AOL firmware 2026-10-23 20:58:48 -05:00
firestar5683 1996510d7a Enable BluePilot angle steering with retained Ford AOL 2026-10-23 20:52:06 -05:00
firestar5683 a8e10e4e52 Restore compatible model runtime 2026-10-23 20:48:09 -05:00
Dom a5f43c1d4b Use typed boolean values in Ford preference cleanup regression 2026-10-23 20:41:03 -05:00
firestar5683 a25dcb535a Align model publication regression with repository lint rules 2026-10-23 20:38:01 -05:00
firestar5683 0172b49987 Restore Dom model publication guard after dropped camera frames 2026-10-23 20:34:08 -05:00
firestar5683 c5ae97f216 Dispatch shared AOL validation only to the current vehicle owner 2026-10-23 20:13:42 -05:00
firestar5683 d2f3acc248 Keep calibrated configuration across learner receipt gaps 2026-10-23 20:10:59 -05:00
firestar5683 976d2a2222 Reject unrelated deadline and F1 owners before qualification 2026-10-23 20:06:11 -05:00
firestar5683 a67202cfb4 Build matching Toyota DSU bypass firmware 2026-10-23 20:01:28 -05:00
firestar5683 4194390dc1 Support Toyota DSU bypass on camera bus 2026-10-23 19:59:49 -05:00
firestar5683 276f3561ce Preserve Ford hands-free display preference across ignition 2026-10-23 19:58:59 -05:00
Dom 7afcf3a8ac Align Prius CI fixtures with qualified hardware profiles 2026-10-23 19:53:04 -05:00
firestar5683 7403185bd8 Keep Ford steering selection visible with AOL enabled 2026-10-23 19:46:20 -05:00
firestar5683 df22d36413 Balance lifecycle CI jobs using measured runtimes 2026-10-23 19:14:21 -05:00
firestar5683 656ba431e9 Update model runtime test contracts 2026-10-23 19:07:47 -05:00
firestar5683 50fc142dd7 Handle unavailable Python Bluetooth socket connections 2026-10-23 19:04:37 -05:00
firestar5683 2a7ebf190c Build matching RAV4 Hybrid pedal and CAN filter firmware 2026-10-23 19:04:07 -05:00
firestar5683 c276b589c7 Support RAV4 Hybrid pedal and CAN filter together 2026-10-23 19:02:36 -05:00
Dom 04b8eb0cd3 Correct manual Matrix four-byte test fixtures 2026-10-23 18:29:58 -05:00
firestar5683 806783c148 Keep receipt callback checks after the output drain 2026-10-23 18:22:41 -05:00
firestar5683 c7e6c27eda Use explicit manual Matrix test fixtures 2026-10-23 18:19:33 -05:00
firestar5683 1a7c20ab6c Compile native RDF Chestnut model 2026-10-23 18:13:43 -05:00
firestar5683 d2d2399609 Build matching Toyota radar and LTA pedal firmware 2026-10-23 18:13:27 -05:00
firestar5683 0cc30a9b30 Support Toyota radar and LTA pedals 2026-10-23 18:11:57 -05:00
firestar5683 1e7d9c311f Preserve FLM capture with native model startup 2026-10-23 18:10:40 -05:00
firestar5683 0eec2e2a55 Bound native model loading memory 2026-10-23 18:09:38 -05:00
firestar5683 748e7b13fc Fix model runtime test fixtures 2026-10-23 18:08:05 -05:00
firestar5683 1f17185669 Keep F1 Volt pedal retention bound to physical receipts 2026-10-23 18:07:54 -05:00
firestar5683 6fae1942e4 Load optional learning capture before the controls loop 2026-10-23 18:06:10 -05:00
firestar5683 094332da2c Build matching EPS73 Prius CAN filter firmware 2026-10-23 17:51:41 -05:00
firestar5683 16eaf005fe Support EPS73 Prius four-byte CAN filters 2026-10-23 17:48:29 -05:00
firestar5683 12601d1fac Pin model runtime source imports before ambient hooks 2026-10-23 17:38:06 -05:00
firestar5683 0022fde1cb Fix Auto Learn collection recovery after evidence gaps 2026-10-23 17:36:10 -05:00
firestar5683 332540a931 Preserve GM cold-start wait provenance 2026-10-23 17:24:35 -05:00
firestar5683 d28aa489ca Compile older Chestnut models 2026-10-23 17:13:33 -05:00
firestar5683 a5eda8a63a Fix Auto Learn authorization and tracking delivery timing 2026-10-23 17:13:21 -05:00
firestar5683 6e5879e035 Preserve stock angle lateral recovery within original source deadline 2026-10-23 17:12:44 -05:00
firestar5683 d377f5efd7 Keep GM lateral intent armed through startup and pedal withdrawal 2026-10-23 17:01:06 -05:00
firestar5683 db297a341b Reuse exact Hyundai deadline admission during control cycles 2026-10-23 16:55:56 -05:00
firestar5683 091682eb06 Fix native model static checks 2026-10-23 16:39:50 -05:00
firestar5683 c779fc4672 Keep Hyundai factory cruise alive until controls are ready 2026-10-23 16:30:25 -05:00
firestar5683 26bc78b69d Compile native Chestnut models 2026-10-23 16:17:25 -05:00
Dom 3a1d0bc615 Restore selfdrived executable mode 2026-10-23 16:13:30 -05:00
firestar5683 7897829d9b Preserve observed worker exit evidence 2026-10-23 16:09:42 -05:00
firestar5683 1353dc4943 Support manual Matrix CAN filter configurations 2026-10-23 16:09:21 -05:00
firestar5683 f50aaa1d6d Handle Hyundai longitudinal withdrawal before brake receipt 2026-10-23 16:03:05 -05:00
firestar5683 076cda2d6c Support stock Prius retrofit AOL 2026-10-23 15:54:01 -05:00
firestar5683 37dc1533ef Reuse exact native qualification across all vehicle makes 2026-10-23 15:42:44 -05:00
firestar5683 f9bf710396 Fix audio integration static checks 2026-10-23 15:33:38 -05:00
firestar5683 04030a447e Use a typed failure callback in HKG admission regression 2026-10-23 15:31:32 -05:00
firestar5683 eb955e391c Reduce repeated HKG source deadline qualification 2026-10-23 15:29:10 -05:00
firestar5683 95d87910f9 Reuse exact native configuration admission for EV6 and EV9 2026-10-23 15:25:08 -05:00
prabhaavp 4b54f61deb Merge pull request #245 from dirwin31:Stardust-Spotify-Companion
Improve Spotify Connect recovery and Bluetooth phone audio
2026-10-23 16:16:54 -04:00
Prabhaav Pillai c14afb153e Merge branch 'SecretGoodStarPilot' into pr/dirwin31/245 2026-10-23 16:16:34 -04:00
Prabhaav Pillai 5755042c96 - First-time pairing interrupted by the new login timeout.
- IPv6-only startup blocked indefinitely.
- Saved initial volume overridden with 100%.
- Unbounded diagnostic logging in the audio loop.
- Hidden speaker-disconnection status while paused.
- Duplicate slider handlers, redundant CSS, verbose documentation, and test lint issues.
2026-10-23 16:15:21 -04:00
firestar5683 e1c73f2359 Keep superseded CI runs cancelled 2026-10-23 14:56:13 -05:00
firestar5683 0c03e1b1b8 Reduce Hyundai longitudinal configuration copy overhead 2026-10-23 14:52:29 -05:00
firestar5683 63fffaa27b Build matching Toyota Matrix stock-control firmware 2026-10-23 14:48:41 -05:00
firestar5683 eac8cea6ee Support Toyota Matrix stock-control AOL 2026-10-23 14:40:44 -05:00
firestar5683 4b5a99e66d Restore independent AOL arming on non-SCC Hyundai vehicles 2026-10-23 14:37:40 -05:00
firestar5683 0a7529919f Balance current AOL lifecycle workloads 2026-10-23 14:14:05 -05:00
Dom 77f54a22f0 CI: balance lifecycle shards using measured native caller cost 2026-10-23 14:09:22 -05:00
firestar5683 1887a45a68 Refresh Galaxy build receipt 2026-10-23 13:34:13 -05:00
Prabhaav Pillai d45c1f2b1b Group Galaxy audio settings under Android Auto and standardize banners 2026-10-23 14:25:42 -04:00
firestar5683 2f68bdedca Build matching Ford hands-free display firmware 2026-10-23 13:12:45 -05:00
firestar5683 dcb7d5de82 Keep Ford hands-free display active through driver turns 2026-10-23 13:10:24 -05:00
firestar5683 657a9462ce Preserve SDGM Volt MAIN intent through startup and braking 2026-10-23 12:59:58 -05:00
firestar5683 8da7e4f493 Link local Galaxy access from app installation 2026-10-23 12:41:57 -05:00
firestar5683 db24c48edc Report bounded learner source observation refusals 2026-10-23 12:41:37 -05:00
firestar5683 01a31c1f2f Build matching original Prius four-byte filter artifacts 2026-10-23 12:25:50 -05:00
firestar5683 9d2d8f3a31 Support original Prius four-byte CAN filters 2026-10-23 12:17:52 -05:00
firestar5683 7df9af125d Keep confirmed vision signs current during cooldown 2026-10-23 12:08:22 -05:00
firestar5683 dc9a5743ee Avoid repeated work for retained learning poses 2026-10-23 12:00:57 -05:00
Dom 165651b096 Register complete HKG module in lifecycle CI 2026-10-23 11:55:24 -05:00
firestar5683 a75d3fd787 Include camera conversion in vision CPU budget 2026-10-23 11:45:33 -05:00
firestar5683 b00a235274 Fix alert preview test startup 2026-10-23 11:43:54 -05:00
firestar5683 b0c6319a1a Restore GV70 two-Panda AOL ownership and stopping parity 2026-10-23 11:37:31 -05:00
firestar5683 2993c8fb07 Restore vision candidate follow-up cadence 2026-10-23 11:20:42 -05:00
firestar5683 c7e47a0289 Reorder lateral settings 2026-10-23 11:18:11 -05:00
firestar5683 733253c7c5 Preserve ASCM Volt intent across complete Card source gaps 2026-10-23 10:18:57 -05:00
dirwin31 f487123e4c Merge SecretGoodStarPilot into Stardust-Spotify-Companion 2026-10-23 07:47:13 -07:00
firestar5683 131739bda6 Format audio backend diagnostics and fixture assertions 2026-10-23 09:44:46 -05:00
firestar5683 90ca300382 Avoid duplicate native history queue in recovery fixtures 2026-10-23 09:34:16 -05:00
firestar5683 9d50b3734c Report active audio playback backend 2026-10-23 09:25:22 -05:00
firestar5683 7fe4b5b550 Bound learner history before tracking initialization 2026-10-23 09:14:39 -05:00
Dom 5961cbd675 Use real alert manager in lane change tests 2026-10-23 09:11:12 -05:00
firestar5683 f19c719d76 Align learner recovery and pilot refusal fixtures 2026-10-23 09:02:15 -05:00
firestar5683 6e72874e55 Strengthen numerical learner regression checks 2026-10-23 08:40:39 -05:00
firestar5683 dc2b45ef1e Reduce vision proposal decoding overhead 2026-10-23 08:34:37 -05:00
firestar5683 9b1ebf0635 Show steering pause warning only for acknowledged lateral requests 2026-10-23 08:32:01 -05:00
firestar5683 4d203961da Align learner protocol and lifecycle fixtures 2026-10-23 08:29:20 -05:00
firestar5683 7c25601821 Track actual poll deliveries in learner fixtures 2026-10-23 08:05:49 -05:00
firestar5683 09dbb29d5a Queue original CAN evidence in decoder fixtures 2026-10-23 08:00:07 -05:00
firestar5683 dc86e03e60 Build matching original Prius smartDSU artifacts 2026-10-23 07:40:24 -05:00
firestar5683 da51312a6f Support original Prius smartDSU configurations 2026-10-23 07:27:12 -05:00
firestar5683 b98d9a1e6e Balance measured Toyota caller workloads 2026-10-23 07:12:39 -05:00
Dom 18b6a4a458 Align event frame archive fixture 2026-10-23 07:07:26 -05:00
firestar5683 6d201df86a Align UI fixtures and complete Galaxy setting descriptions 2026-10-23 06:36:08 -05:00
firestar5683 43007a6318 Build documentation with the standard library 2026-10-23 06:35:32 -05:00
firestar5683 80ca622ea6 Align Toyota caller fixtures with qualified AOL and native acknowledgments 2026-10-23 06:27:12 -05:00
firestar5683 266ee4ce07 Model suppressed ADRV diagnostic replies in startup regressions 2026-10-23 06:25:05 -05:00
firestar5683 b46431793b Align drive and session fixtures with current contracts 2026-10-23 06:23:00 -05:00
firestar5683 c06d1dfe16 Verify Ford event-first SOFT retention through its current owner 2026-10-23 06:07:05 -05:00
firestar5683 2d2f662bdf Balance independent Toyota cases and audit existing map GPU guards 2026-10-23 06:06:24 -05:00
firestar5683 ed3f88ef52 Scope HKG startup fixture preparation to Hyundai interfaces 2026-10-23 06:02:47 -05:00
firestar5683 29ab44fc4d Align model compiler and steering-delay test fixtures 2026-10-23 05:56:55 -05:00
firestar5683 b42499038d Load native safety in fresh Volkswagen caller tests 2026-10-23 05:52:43 -05:00
Dom 5dea258223 Align custom pytest discovery contract with complete shard gate 2026-10-23 05:37:12 -05:00
firestar5683 7d2c0aafea Distribute custom pytest workloads without dropping cases 2026-10-23 05:10:16 -05:00
Dom 92f0c26228 Cover Toyota camera identity contradiction latch 2026-10-23 04:47:13 -05:00
firestar5683 3b22e29c20 Prepare classic HKG startup owners in family regression callers 2026-10-23 04:44:38 -05:00
firestar5683 206907f651 Preserve armed intent through split SOFT recovery 2026-10-23 04:42:58 -05:00
Dom e0bea55fa9 Align uploader and torque layout test fixtures 2026-10-23 04:39:20 -05:00
Dom 7c795af543 Compact Toyota camera test fixture without changing data 2026-10-23 04:25:15 -05:00
firestar5683 8b9338a0a6 Preserve Chrysler physical cancel in location fault tests 2026-10-23 04:09:50 -05:00
firestar5683 9a2d62445d Align Toyota SecOC tests with qualified RAV4 angle support 2026-10-23 04:04:20 -05:00
firestar5683 314c92e51f Build matching Toyota camera filter artifacts 2026-10-23 04:00:12 -05:00
firestar5683 af84074472 Support camera Toyota radar filter configurations 2026-10-23 03:55:56 -05:00
Dom 4a38356a5f Apply onroad graphics fixtures to unittest runner 2026-10-23 03:53:13 -05:00
firestar5683 ecc1db481b Balance independent lifecycle workloads within existing deadlines 2026-10-23 03:51:56 -05:00
firestar5683 6eb9df9642 Align Tesla screen settings fixture with Alpha availability 2026-10-23 03:14:37 -05:00
firestar5683 8c814777f6 Align HKG unit callers with shared intent and startup contracts 2026-10-23 03:12:42 -05:00
firestar5683 9e6a49742e Isolate synthetic state machine event rules from CAN faults 2026-10-23 03:11:33 -05:00
firestar5683 359549db26 Align generic wheel policy fixture with current capabilities 2026-10-23 03:04:50 -05:00
firestar5683 468770cabd Retain host test crash and timeout evidence 2026-10-23 03:03:56 -05:00
firestar5683 1ec8ca7abb Align HKG location regressions with shared armed intent policy 2026-10-23 02:28:18 -05:00
Dom d049218089 Align Toyota legacy filter safety tests 2026-10-23 02:08:27 -05:00
firestarsdog 7a8e17dda5 fix(galaxy): expand timeline for readable history 2026-10-23 02:55:27 -04:00
firestar5683 d48e34a3a0 Build matching Toyota legacy filter artifacts 2026-10-23 01:49:56 -05:00
firestar5683 6f4fdfb851 Support legacy Toyota four-byte CAN filter profiles 2026-10-23 01:42:12 -05:00
firestarsdog 7011142a49 feat(replay): keep seek targets visible while scrubbing 2026-10-23 02:30:11 -04:00
Dom d048a8eb98 Align EV9 stopping marker test contract 2026-10-23 01:29:12 -05:00
Dom 22b2f2a898 Preserve bootstub binary output modes 2026-10-23 01:17:30 -05:00
Dom 4e74d8890d Align interface and confirmation test contracts 2026-10-23 00:59:33 -05:00
firestar5683 7e2cf618e0 Cover HKG native catalog admission and isolate inherited test policy 2026-10-23 00:49:31 -05:00
firestar5683 1f8205231f Scope complete build identity to lifecycle gates 2026-10-23 00:26:57 -05:00
firestarsdog 4bee14a4d7 build(replay): refresh ARM64 prebuilt and receipt 2026-10-23 01:24:45 -04:00
firestarsdog 68629dcb82 Fix compact UI confirmations, text overlap, and one-way Display controls 2026-10-23 01:24:18 -04:00
firestarsdog 29da914430 feat(replay): color timelines to show drive history 2026-10-23 01:24:18 -04:00
Dom c4c5d276d5 Align vehicle CI provenance fixtures 2026-10-23 00:22:53 -05:00
firestar5683 a012e58a99 Align controller and UI fixtures with current contracts 2026-10-23 00:15:50 -05:00
firestar5683 c8d2a8aaf0 Preserve exact ASCM Volt steering receipts across receive ordering 2026-10-23 00:14:03 -05:00
firestar5683 b2296b4844 Pin lifecycle build inputs across independent shards 2026-10-23 00:04:03 -05:00
Dom 403e360551 Synchronize simulated startup readiness fixture 2026-10-22 23:54:46 -05:00
firestar5683 c1d966997d Run Ford legacy long denial with an explicit release fixture 2026-10-22 23:51:04 -05:00
firestar5683 143b9b5d4f Bound Subaru StopStart pulse with integer nanosecond deadline 2026-10-22 23:50:06 -05:00
firestar5683 6053d3e6ff Restore HKG stock angle arming and steering recovery 2026-10-22 23:47:42 -05:00
Dom 9e08f0b1bb Align GM startup tests with retained lateral intent 2026-10-22 23:45:07 -05:00
firestar5683 1e189a1192 Restore legacy Toyota PCM stop latch transitions 2026-10-22 23:42:56 -05:00
firestar5683 84fb671740 Type Ford CAN and developer panel test fixtures 2026-10-22 23:38:16 -05:00
firestar5683 86b752f9d2 Record qualified Subaru smoothing startup metadata in replay reference 2026-10-22 23:37:28 -05:00
Dom 2fe363a1f8 Align Ford CI fixture typing 2026-10-22 23:24:20 -05:00
firestar5683 989beaae3c Shard complete lifecycle qualification by module 2026-10-22 23:21:56 -05:00
firestar5683 d6273c7137 Verify Vision adoption and command identity 2026-10-22 23:18:11 -05:00
firestar5683 1a2cd5e55d Merge current family fixes with qualified Toyota CAN filters 2026-10-22 23:10:14 -05:00
firestar5683 3149512d97 Build qualified Chrysler native guards 2026-10-22 23:09:38 -05:00
firestar5683 edb618e910 Qualify Chrysler native guards without changing safety limits 2026-10-22 23:04:49 -05:00
firestar5683 482e3cd7f2 Restore Dom mode transitions for qualified Volt longitudinal control 2026-10-22 22:57:39 -05:00
firestar5683 687bc6835d Build Ford configuration parity firmware and pandad 2026-10-22 22:54:30 -05:00
firestar5683 aaa2f687ea Complete Ford release long manual and multi-Panda support 2026-10-22 22:49:49 -05:00
firestar5683 c9fbbc0331 Merge gateway MEB support and qualified Toyota replay metadata 2026-10-22 22:48:54 -05:00
firestar5683 e24c677109 Refresh qualified Toyota standstill metadata expectations 2026-10-22 22:47:53 -05:00
firestar5683 996542ffef Build gateway MEB native safety 2026-10-22 22:41:30 -05:00
firestar5683 dbc5af5d55 Merge current native artifacts with Toyota filter configurations 2026-10-22 22:41:18 -05:00
firestar5683 f99d84daa8 Restore Toyota EPS8 four-byte CAN filter configurations 2026-10-22 22:41:05 -05:00
firestar5683 b98f1bfce5 Restore gateway MEB independent lateral and saved speed control 2026-10-22 22:40:45 -05:00
firestar5683 1fdc8b89e8 Preserve HKG AOL arming and original source deadlines 2026-10-22 22:33:16 -05:00
Dom da6e0bd0a9 Align CI formatting and Mici toggle fixture 2026-10-22 22:28:48 -05:00
firestar5683 c642fd9427 Keep classic HKG discovery on observed normal wiring 2026-10-22 22:22:36 -05:00
firestar5683 0147c90469 Build GM safety checks 2026-10-22 22:13:50 -05:00
firestar5683 4e956b46ec Keep GM safety checks explicit and remove unreachable forwarding 2026-10-22 22:12:36 -05:00
firestar5683 107e99a5fc Merge Tesla safety firmware with current qualification fixes 2026-10-22 22:10:23 -05:00
firestar5683 7610f33129 Qualify Subaru RELEASE AVH guard and ordinary engagement 2026-10-22 22:09:05 -05:00
firestar5683 5acadf1d91 Build Tesla Pre-AP safety cleanup firmware 2026-10-22 22:08:34 -05:00
firestar5683 33ca04f895 Resolve Tesla Pre-AP native safety diagnostics 2026-10-22 22:07:42 -05:00
firestarsdog 0171603f6d Fix compact Driving Controls availability so the menu opens its editor 2026-10-22 23:06:18 -04:00
firestar5683 cac674d270 Run Honda native lifecycle tests under both CI runners 2026-10-22 22:05:31 -05:00
firestar5683 faa890e903 Exercise GM interceptor release guards in native coverage 2026-10-22 22:04:47 -05:00
firestar5683 c7fe28fdfd Merge current tests with Toyota smartDSU priority 2026-10-22 21:55:14 -05:00
firestar5683 cc9161776a Preserve Toyota smartDSU priority over camera traffic 2026-10-22 21:55:13 -05:00
firestar5683 8e1bb6995c Repair Bluetooth and HTTP caller qualification 2026-10-22 21:51:18 -05:00
firestar5683 1ad498846f Type Rivian native test payloads explicitly 2026-10-22 21:49:07 -05:00
firestar5683 fe5b2c66da Merge current qualification tests with Toyota smartDSU artifacts 2026-10-22 21:46:10 -05:00
firestar5683 f7ca352105 Build legacy Toyota smartDSU artifacts 2026-10-22 21:46:00 -05:00
firestar5683 8a3e80cddd Restore executable modes for daemon entrypoints 2026-10-22 21:44:22 -05:00
firestar5683 d429cc3c90 Verify Toyota angle factory and independent LTA health 2026-10-22 21:44:16 -05:00
firestar5683 6095ea559c Accept current native layout documents 2026-10-22 21:42:06 -05:00
firestar5683 cdb7e7a5d4 Reject missing Auto Learn withdrawal receipts 2026-10-22 21:39:39 -05:00
firestar5683 95155234e8 Publish explicit Auto Learn source withdrawals 2026-10-22 21:37:28 -05:00
firestar5683 de94ce4ad8 Qualify native variants and current caller fixtures 2026-10-22 21:36:56 -05:00
firestar5683 ab2baa98ea Cover Rivian unsupported profiles and permanent EPS retirement 2026-10-22 21:35:57 -05:00
prabhaavp 4e46321160 Merge pull request #238 from dirwin31:PR-Smaller-4
App and web: Companion apps, Galaxy pages, Spotify, Bluetooth and Android Auto screens
2026-10-22 22:31:29 -04:00
firestar5683 15a78b9b33 Maintain Auto Learn source delivery cadence 2026-10-22 21:29:34 -05:00
Prabhaav Pillai a57cd3d54a Merge SecretGoodStarPilot into PR-Smaller-4, preserving both Galaxy feature sets 2026-10-22 22:29:25 -04:00
firestar5683 82f3baaebb Merge qualified Nissan coverage and lateral lifecycle fixes 2026-10-22 21:29:16 -05:00
firestar5683 6dff9ba47f Test unsupported Nissan AOL safety words 2026-10-22 21:28:13 -05:00
firestar5683 9e191161d7 Merge current vehicle fixes with legacy Toyota smartDSU support 2026-10-22 21:24:55 -05:00
firestar5683 011e46b8ed Preserve armed lateral intent through temporary warnings 2026-10-22 21:22:55 -05:00
firestar5683 9ce2893646 Build qualified Outback and Subaru smoothing artifacts 2026-10-22 21:14:26 -05:00
firestar5683 4d8467ce37 Restore legacy Toyota smartDSU control paths 2026-10-22 21:14:16 -05:00
firestar5683 c6a366b013 Restore Outback angle and frozen Subaru model smoothing 2026-10-22 21:13:43 -05:00
Prabhaav Pillai d921cf6559 Refactor Galaxy tools into a searchable nested catalog
Render tool menus from the catalog and update navigation, guides, and tests
2026-10-22 22:03:31 -04:00
firestar5683 15578da372 Preserve HKG angle AOL intent through healthy stops 2026-10-22 20:59:02 -05:00
firestar5683 6148a86dc1 Preserve Tesla schema and radar fixture contracts 2026-10-22 20:49:31 -05:00
firestar5683 a0fd5ea025 Include native pytest cases in safety coverage 2026-10-22 20:47:05 -05:00
firestar5683 16509c3803 Expose Alpha Longitudinal in Vehicle Controls 2026-10-22 20:46:13 -05:00
firestar5683 0f1cce2285 Refresh stock longitudinal planner expectations 2026-10-22 20:45:40 -05:00
firestar5683 83a90e89b4 Merge qualified HKG and MQB source 2026-10-22 20:15:07 -05:00
firestar5683 82367fd413 Build MQB direct-drive native safety 2026-10-22 20:14:23 -05:00
firestar5683 ca2373a9a0 Refresh Auto Learn inputs after queued decoding 2026-10-22 20:14:03 -05:00
firestar5683 d4027487c0 Repair HKG startup and native qualification fixtures 2026-10-22 20:12:30 -05:00
firestar5683 9cf41c7b38 Restore MQB direct-drive lateral and longitudinal ownership 2026-10-22 20:08:25 -05:00
firestar5683 f9a06b7d1a Merge qualified Forester and shared build artifacts 2026-10-22 20:05:30 -05:00
firestar5683 dc1e248d03 Build qualified Forester angle artifacts 2026-10-22 20:02:48 -05:00
firestar5683 2d58df950b Share Subaru angle receive storage across exact profiles 2026-10-22 20:02:08 -05:00
firestar5683 9983d3a4cb Merge preview and CI fixes with Toyota filter support 2026-10-22 20:00:23 -05:00
firestar5683 08c0efb01d Merge qualified shared source 2026-10-22 19:59:30 -05:00
firestar5683 8adbeddcbb Accept forwarded Toyota radar-filter advertisements 2026-10-22 19:59:30 -05:00
firestar5683 2c352b90b4 Keep offset sign previews within the visible canvas 2026-10-22 19:57:34 -05:00
firestar5683 0852af76e3 Preserve validated CI import provenance 2026-10-22 19:57:15 -05:00
firestar5683 d747e5ca08 Restore Forester first generation angle control 2026-10-22 19:52:53 -05:00
firestar5683 6633870fb3 Merge CI contracts and recording artifacts with Toyota filter support 2026-10-22 19:52:02 -05:00
firestar5683 a9edd2ca3b Accept forwarded Toyota smartDSU status copies 2026-10-22 19:50:42 -05:00
firestar5683 4840e9e1d9 Build Auto Learn recording and replay 2026-10-22 19:49:51 -05:00
firestar5683 94f88b41a9 Repair CI contracts and preview rendering 2026-10-22 19:44:47 -05:00
firestar5683 2077b98d76 Merge Subaru qualification tests with Toyota parity 2026-10-22 19:41:55 -05:00
firestar5683 8bd7ac53c7 Build EPS8 Toyota smartDSU artifacts 2026-10-22 19:41:23 -05:00
firestar5683 332d235693 Qualify Subaru native discovery and receiver guards 2026-10-22 19:39:12 -05:00
firestar5683 7aea14397c Merge Rivian static corrections with Toyota parity 2026-10-22 19:39:01 -05:00
firestar5683 951947cef2 Merge CI schema and Rivian static qualification 2026-10-22 19:35:52 -05:00
firestar5683 3f06976373 Merge current CI and GM source fixes with Toyota parity 2026-10-22 19:34:46 -05:00
firestar5683 26025d80dd Build EPS8 Toyota smartDSU artifacts 2026-10-22 19:33:38 -05:00
firestar5683 e18bf451d0 Fix SDGM Volt Alpha lateral startup and withdrawal 2026-10-22 19:32:53 -05:00
firestar5683 84087746d7 Align PE protocol checks with supported Alpha profile 2026-10-22 19:32:52 -05:00
firestar5683 6a8c1cf8cf Build Rivian native static corrections 2026-10-22 19:32:20 -05:00
firestar5683 ec957d6430 Fix Rivian native static diagnostics 2026-10-22 19:31:45 -05:00
firestar5683 27c7ba7827 Preserve CI schema and replay contracts 2026-10-22 19:31:25 -05:00
firestar5683 194d6fc106 Merge current safety and Auto Learn fixes for Toyota qualification 2026-10-22 19:29:03 -05:00
firestar5683 23ca75140a Build EPS8 Toyota smartDSU artifacts 2026-10-22 19:28:08 -05:00
firestar5683 d0a90d10ef Share Toyota safety RX workspace 2026-10-22 19:25:32 -05:00
firestar5683 778eed3a60 Make Honda atomic state and fixtures statically verifiable 2026-10-22 19:24:28 -05:00
firestar5683 62dc081f70 Keep Auto Learn source publication responsive 2026-10-22 19:10:12 -05:00
firestar5683 204a6f8534 Restore EPS8 Toyota smartDSU ownership 2026-10-22 19:09:43 -05:00
firestar5683 4413c29923 Restore MQB fixed and second-Panda lateral ownership 2026-10-22 19:04:28 -05:00
Dom 43360170dd Align PE marker and Subaru build variant tests 2026-10-22 18:42:16 -05:00
firestar5683 281f5b6b3a Strengthen PE release and pedal regression coverage 2026-10-22 18:32:02 -05:00
firestar5683 9ec0303f93 Preserve PE independent lateral intent alongside Alpha longitudinal 2026-10-22 18:13:36 -05:00
firestar5683 bb51c66589 Build qualified Subaru angle and startup changes 2026-10-22 18:04:07 -05:00
firestar5683 8c5f4bae13 Restore Ascent angle target shaping 2026-10-22 18:03:21 -05:00
firestar5683 9c5da263cc Merge qualified shared source 2026-10-22 18:02:10 -05:00
firestar5683 8025957925 Merge current shared source for HKG qualification 2026-10-22 17:50:11 -05:00
firestar5683 d4a49cb8c1 Allow tuning preparation to be turned off 2026-10-22 17:50:08 -05:00
firestar5683 bb39b9f388 Build guarded Legacy AVH startup request 2026-10-22 17:49:21 -05:00
firestar5683 7926a909e1 Restore guarded Legacy AVH startup request 2026-10-22 17:48:36 -05:00
Danny b4acca0798 Spotify: Preserve final volume behavior on the newer base 2026-10-22 15:47:15 -07:00
dirwin31 c7cc700e4e BlueALSA: Bump to v5.0.0 2026-10-22 15:47:15 -07:00
dirwin31 89509ba3c7 BT: Dont try and re connect to established audio Stream 2026-10-22 15:47:15 -07:00
Danny 843c364494 Test 2026-10-22 15:47:15 -07:00
Danny b5a58c741f BT: Track Skipping Mute Toggles 2026-10-22 15:47:15 -07:00
Danny f702c707e5 Use Colored Bar 2026-10-22 15:47:15 -07:00
Danny ce980ed98f Spotify: Account for network starting later 2026-10-22 15:47:15 -07:00
Danny 5814e26a53 Spotify: Advertise across different networks better, Swap Devices More Seemlessly 2026-10-22 15:47:15 -07:00
firestar5683 2cc8849544 Restore four-byte Toyota radar filter variants 2026-10-22 17:46:06 -05:00
Dom fb6b75a81d Align vehicle contract tests with intended safety ownership 2026-10-22 17:44:38 -05:00
firestar5683 7f584d7e58 Deliver Ford MAIN rearm after prior feedback clears 2026-10-22 17:43:06 -05:00
Danny c777a9ec8a iOS: Add the Galaxy companion with live device pages and CarPlay
Add LAN and Bluetooth connections, secure pairing, device-matched Galaxy page
caching, phone location and CarPlay navigation. Include connection recovery,
diagnostics, regression tests and a simulator CI build. Add shared companion
docs and host tests, and remove obsolete bundled-web lint exceptions.
2026-10-22 15:37:20 -07:00
Danny f63e715d2d Android: Add the StarPilot companion and signed release workflow
Connect over LAN or Bluetooth with network discovery, build-matched Galaxy
pages, phone GPS and a background uplink. Guide pairing with readiness retries,
cancellable progress and bond cleanup. Include app updates, regression tests,
the current APK, and a workflow to test, build and sign releases.
2026-10-22 15:37:20 -07:00
Danny e11ce3071d Galaxy: Add companion, Spotify and phone-aware web controls
Add companion installation and updates, Spotify setup and playback, destination
sharing, and Android Auto controls. Adapt Galaxy pages to phone transports and
device type, improve Bluetooth pairing feedback, and cover the new web flows.
2026-10-22 15:37:20 -07:00
Danny a531afd02a Bluetooth: Coordinate companion pairing and Android Auto ownership
Expose pairing readiness and coordinate adapter hand-offs with Android Auto.
Show app pairings separately, support individual removal, and improve pairing
controls on both comma screens. Add Galaxy companion endpoints and web asset
manifests, preserve the plain camera look, and fix headless speed-limit shapes.
2026-10-22 15:37:20 -07:00
firestar5683 a57a08843a Maintain Auto Learn history through pauses 2026-10-22 17:35:15 -05:00
firestar5683 3c592b58b0 Recover Auto Learn capture after interruptions 2026-10-22 17:34:49 -05:00
firestar5683 b781fa90d7 Recognize four-byte RAV4 radar filter advertisement 2026-10-22 17:32:58 -05:00
firestar5683 59c29e6e9c Build Ford lateral session handoff firmware 2026-10-22 17:13:31 -05:00
firestar5683 ba906bb038 Start fresh Ford lateral sessions on driver activation 2026-10-22 17:13:02 -05:00
firestar5683 ab1aeda540 Restore Avalon smartDSU ownership 2026-10-22 17:10:18 -05:00
firestar5683 46051ce2b4 Restore legacy Toyota smartDSU ownership 2026-10-22 17:00:52 -05:00
firestar5683 5ac0bbab48 Enable Turn Signal Guidance by default 2026-10-22 16:57:10 -05:00
firestar5683 236fd3942f Build guarded Legacy Stop Start startup request 2026-10-22 16:56:39 -05:00
firestar5683 c6db508304 Restore guarded Legacy Stop Start startup request 2026-10-22 16:55:56 -05:00
firestar5683 3305afdf98 Enable torque tuning for every torque controller 2026-10-22 16:52:31 -05:00
firestar5683 08d38e9bdf Increase clear-road encouragement 2026-10-22 16:51:45 -05:00
firestar5683 c8bbe72a2a Correct Palisade AOL status relay ownership 2026-10-22 16:45:09 -05:00
Dom 57bfa98906 Fix Rivian and Sentry test static checks 2026-10-22 16:37:30 -05:00
firestar5683 38bee61910 Authorize EV9 lateral intent from physical cruise SET release 2026-10-22 16:30:49 -05:00
firestar5683 9748e4a449 Pulse newly displayed speed limits 2026-10-22 16:24:13 -05:00
firestar5683 ea61d3002c Restore Camry cluster radar parser 2026-10-22 16:21:46 -05:00
firestar5683 3f8f1c69e4 Restore Toyota camera pedal configurations 2026-10-22 16:17:12 -05:00
firestar5683 7ac512caf2 Build Subaru angle and LKAS display repair 2026-10-22 16:17:02 -05:00
firestar5683 e3070b7b42 Preserve Subaru angle bounds and current LKAS display 2026-10-22 16:16:26 -05:00
firestar5683 de28764611 Merge qualified Toyota angle and Small UI updates 2026-10-22 16:15:10 -05:00
firestar5683 3366652afa Build independent RAV4 angle lateral control 2026-10-22 16:14:16 -05:00
firestar5683 928aeb8fe2 Restore original Small UI speed limit sign 2026-10-22 16:07:32 -05:00
firestar5683 40afc1bad2 Restore independent RAV4 angle lateral control 2026-10-22 16:05:39 -05:00
firestar5683 ecc570756a Build guarded Subaru stop and go 2026-10-22 15:55:14 -05:00
firestar5683 a7c78e97b0 Restore guarded Dom Subaru stop and go 2026-10-22 15:54:33 -05:00
firestar5683 9b1bbf7d21 Turn Signal Guidance 2026-10-22 15:47:13 -05:00
firestar5683 e70f7ea88b Preserve the original EV6 output proof across an input timeout 2026-10-22 15:43:58 -05:00
firestar5683 e72e3a875b Restore classic Honda alpha lateral ownership 2026-10-22 15:40:40 -05:00
firestar5683 db741fd789 Cover unbelted lateral recovery fault-marker vetoes 2026-10-22 15:36:09 -05:00
firestar5683 2325f052f0 Fix Subaru software cruise parked settings import 2026-10-22 15:33:32 -05:00
firestar5683 b41fdc6545 Restore selected Honda and CAN-FD Panda ownership 2026-10-22 15:33:11 -05:00
firestar5683 c03714aab2 Merge qualified Mazda startup with planner test typing 2026-10-22 15:31:48 -05:00
firestar5683 b79ea58759 Build Mazda MAIN-on lateral startup 2026-10-22 15:28:42 -05:00
firestar5683 66f0ec1371 Restore Mazda MAIN-on lateral startup 2026-10-22 15:28:09 -05:00
Dom 40e84d4a54 Type encouragement planner test options 2026-10-22 15:25:44 -05:00
firestar5683 057b881da8 Build guarded Subaru software cruise 2026-10-22 15:21:12 -05:00
firestar5683 7924313af9 Restore Dom Subaru software cruise with guarded stock buttons 2026-10-22 15:20:37 -05:00
firestar5683 7ebd1b46a7 Fit compact speed limit sign to visible offset 2026-10-22 15:18:04 -05:00
firestar5683 6a9df653ab Build Rivian GEN1 always-on lateral 2026-10-22 15:17:58 -05:00
firestar5683 036d2f0fee Place Rivian H7 receive workspace in AXI SRAM 2026-10-22 15:17:26 -05:00
firestar5683 d0a252d34b Merge shared vehicle qualification updates 2026-10-22 15:17:11 -05:00
Dom 11b26d3511 Align Hyundai startup fixtures with staged handoff 2026-10-22 15:07:02 -05:00
firestar5683 17f3920a55 Restore Rivian GEN1 always-on lateral 2026-10-22 15:05:24 -05:00
firestar5683 456c1955a1 Restore Honda Nidec pedal lateral ownership 2026-10-22 14:59:39 -05:00
firestar5683 744032b961 Encouragement 2026-10-22 14:58:27 -05:00
firestar5683 6a773e7d2d Restore Camry and C-HR radar CAN filter ownership 2026-10-22 14:51:22 -05:00
Dom f87e6e4093 Audit concrete parameterized vehicle test classes 2026-10-22 14:46:12 -05:00
firestar5683 4c7854dea8 Preserve armed lateral intent during unbelted pause recovery 2026-10-22 14:38:52 -05:00
firestar5683 45a73a439b Restore Honda CAN-FD and alternate radar lateral ownership 2026-10-22 14:25:55 -05:00
firestar5683 8462a39b65 Build Volkswagen cold MAIN lateral startup 2026-10-22 14:13:32 -05:00
firestar5683 29546c710a Restore MQB cold MAIN lateral startup 2026-10-22 14:12:53 -05:00
firestar5683 d8d39d1b35 Restore Auto Learn producer admission 2026-10-22 14:08:07 -05:00
firestar5683 178c8694f2 Restore Mazda camera stop-sign observations 2026-10-22 14:00:06 -05:00
firestar5683 2985ed9f6d Restore independent EV9 lateral control with Alpha longitudinal 2026-10-22 13:56:27 -05:00
firestar5683 bd764bbf2e Reduce Chestnut model startup overhead 2026-10-22 13:52:19 -05:00
firestar5683 8e27decb08 Restore 2022 RAV4 radar CAN filter and lateral ownership 2026-10-22 13:43:31 -05:00
firestar5683 1382b62b00 Retain armed lateral intent through inactive control receipts 2026-10-22 13:41:06 -05:00
firestar5683 ffaa466852 Build RAV4 angle-control radar CAN filter ownership 2026-10-22 13:28:11 -05:00
firestar5683 0c0d4bc8cb Restore RAV4 angle-control radar CAN filter ownership 2026-10-22 13:26:13 -05:00
Dom c78aeed361 Align classic CI with safety build modes and fix test typing 2026-10-22 13:05:30 -05:00
firestar5683 c145bf602f Fix RAV4 Continental radar setting authorization 2026-10-22 13:00:15 -05:00
firestar5683 7e19d85b8b Build Subaru stock-cruise always-on lateral 2026-10-22 12:57:47 -05:00
firestar5683 4523d17ecd Restore supported Subaru stock-cruise always-on lateral 2026-10-22 12:57:05 -05:00
firestar5683 9306f865e8 Add Ioniq left paddle Bookmark assignment 2026-10-22 12:51:07 -05:00
Peter Nguyen 3464d40526 2026-10-22 Sentry Discord and webhook alerts attach the saved wide and cabin images again
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 12:34:39 -05:00
firestar5683 b51b8dafd6 Handle Galaxy update tunnel disconnects 2026-10-22 12:34:31 -05:00
firestar5683 46c752caea Build Volkswagen gateway OP-long lateral independence 2026-10-22 12:28:56 -05:00
firestar5683 01479b56b9 Restore gateway MQB OP-long lateral independence 2026-10-22 12:27:51 -05:00
firestar5683 a64dd65025 Fix replay torque bar freshness 2026-10-22 12:04:37 -05:00
firestar5683 838e93b934 Test Honda EPS recovery and Reverse native lifecycle 2026-10-22 11:59:59 -05:00
firestar5683 b88e2eccb9 Gate stock MQB AOL lifecycles in vehicle CI 2026-10-22 11:57:28 -05:00
firestar5683 86db7a63ba Build Volkswagen stock always-on lateral 2026-10-22 11:55:19 -05:00
firestar5683 6120da2d54 Restore stock MQB always-on lateral 2026-10-22 11:54:11 -05:00
firestar5683 5277fc139f Retain lateral intent while a newer native FULL grant is acknowledged 2026-10-22 11:45:13 -05:00
firestar5683 cef4eec062 Restore Galaxy update reconnect screen 2026-10-22 11:44:05 -05:00
firestar5683 2d10707628 Gate shared AOL lifecycles in vehicle CI 2026-10-22 11:37:43 -05:00
firestar5683 d0400456cf Keep neutral classic SCC replacement running during initialization 2026-10-22 11:26:01 -05:00
firestar5683 872de3b085 Warn when a soft fault withdraws independent lateral control 2026-10-22 11:23:45 -05:00
firestar5683 e6f7d9c473 Fix GM control-loop load and lateral recovery after lag 2026-10-22 11:22:03 -05:00
firestar5683 e57c9a5989 Retain stock Ford intent when braking overlaps temporary EPS loss 2026-10-22 11:19:01 -05:00
firestar5683 deefc26c85 Retain armed lateral intent when an axis publication expires 2026-10-22 11:15:57 -05:00
firestar5683 1f66903899 Recover Auto Learn after calibration delivery gaps 2026-10-22 11:11:11 -05:00
firestar5683 98a6ca21f3 Build Ford accelerator-override lateral safety firmware 2026-10-22 11:08:22 -05:00
firestar5683 a3b270cbfc Retain Ford lateral intent when cruise engages under accelerator override 2026-10-22 11:06:50 -05:00
firestar5683 3ce8e82c73 Retain Mazda lateral intent at standstill 2026-10-22 11:03:51 -05:00
firestar5683 0970a7b685 Retain Mazda lateral intent through Reverse 2026-10-22 10:59:50 -05:00
firestar5683 36ecda9f25 Exercise EV6 Alpha startup in release regression coverage 2026-10-22 10:55:07 -05:00
firestar5683 0cdc3287b5 Preserve Mazda lateral intent for mapped Cancel actions 2026-10-22 10:53:47 -05:00
firestar5683 d69a9f4db5 Restore Small UI sign and parked Galaxy offroad 2026-10-22 10:52:58 -05:00
firestar5683 9ab83d8d59 Refresh prebuilt inventory after Nissan test typing 2026-10-22 10:52:11 -05:00
Dom f8c91e6c62 Fix Nissan test CAN values typing 2026-10-22 10:46:20 -05:00
firestar5683 86be96c44c Withdraw Toyota pedal steering when MAIN expires 2026-10-22 10:17:30 -05:00
firestar5683 82f9995887 Build Nissan always-on lateral 2026-10-22 10:13:01 -05:00
firestar5683 9af5e64787 Reuse Nissan safety RX workspace 2026-10-22 10:10:36 -05:00
firestar5683 075b73e940 Restore Nissan stock-cruise always-on lateral 2026-10-22 10:05:08 -05:00
firestar5683 e914d761b1 Improve Auto Learn feedback 2026-10-22 08:44:35 -05:00
firestar5683 f33c8605da Preserve stock lateral intent after temporary steering faults 2026-10-22 08:37:24 -05:00
firestar5683 155b105a44 Restore mapped classic Hyundai MAIN and LKAS AOL controls 2026-10-22 08:23:03 -05:00
firestar5683 ce66b2ed7d Test Ford AOL retention across temporary location loss 2026-10-22 08:21:35 -05:00
firestar5683 d36a4b8888 Cover Chrysler stock ACC location recovery 2026-10-22 08:12:00 -05:00
firestar5683 fb943dd85a Retain qualified lateral intent across temporary withdrawals 2026-10-22 08:09:06 -05:00
firestar5683 371000a848 Preserve Ford AOL intent with the initial integrity epoch 2026-10-22 08:06:00 -05:00
firestar5683 33952c42b9 Build Tesla screen AOL safety firmware 2026-10-22 07:58:35 -05:00
firestar5683 7731bee396 Preserve Tesla screen AOL intent through temporary inhibition 2026-10-22 07:55:42 -05:00
firestar5683 74d2ef9698 Initialize classic AOL dash icons after profile finalization 2026-10-22 07:49:23 -05:00
firestar5683 ccdd137671 Build Lexus IS DSU MAIN AOL 2026-10-22 07:27:57 -05:00
firestar5683 5480ae3caa Restore Lexus IS DSU MAIN lateral ownership 2026-10-22 07:24:49 -05:00
firestar5683 2b42135302 Validate published prebuilt receipts before device updates 2026-10-22 07:04:46 -05:00
Prabhaav Pillai 8f2e33dead (Another eight) round of Galaxy UI bug fixes
Made card headers, icons, spacing, and controls more consistent across Galaxy pages.
Improved vehicle selection and Bluetooth device layouts.
Refined Model Manager filters and moved running model status into its four-item summary.
Moved Jetlink above Controls.
Updated UI tests to match the revised layouts.
2026-10-22 04:12:20 -04:00
Prabhaav Pillai 91e1ce8466 Improve error handling and UI elements for software status 2026-10-22 03:49:43 -04:00
Prabhaav Pillai ee83b3010a (Another seventh) round of Galaxy UI bug fixes
Improved consistency across Galaxy pages
Reworked the Model Manager layout, filters, downloads, randomizer, and Jetlink controls.
Improved vehicle selection and the Galaxy pairing and installation flow.
Fixed settings-row rendering, Android Auto upload timeouts, and Sentry event ID validation.
Updated tests for current UI behavior - all 102 JavaScript and 797 Python Galaxy tests pass.
2026-10-22 03:39:05 -04:00
firestarsdog d773f87897 ui: refine speed-limit signs and speed display hierarchy 2026-10-22 03:22:11 -04:00
firestarsdog d2dbeddcf1 ui: unify speed-limit signs for clearer confirmation cues 2026-10-22 02:49:00 -04:00
Dom aab66cd015 Fix device artifact test fixture for FLM native binding 2026-10-21 23:43:36 -05:00
firestar5683 b151ca11cb Global FLM Auto Learn 2026-10-21 23:28:39 -05:00
firestar5683 21c74f72ef Honor Panda fault policy before ECU takeover 2026-10-21 22:50:55 -05:00
firestar5683 4b9de265bf Handle Ioniq 6 native brake withdrawal before host sampling 2026-10-21 22:48:16 -05:00
firestar5683 70799d5bc3 Fix GM steering companion receive ordering 2026-10-21 22:45:01 -05:00
firestarsdog a5f18d35cd Restore C4 MAX fade; contain SLC offsets for readability 2026-10-21 23:12:12 -04:00
prabhaavp 9ab16c0838 Merge pull request #240 from dirwin31:3X-Widgets
Allow Full Widget Editing and Arrangement on 3X
2026-10-21 22:39:15 -04:00
Prabhaav Pillai 51dd8d48ce Removed duplicate font opacity handling 2026-10-21 22:38:59 -04:00
firestar5683 55a40280fb Merge latest tests with vehicle regression coverage 2026-10-21 21:18:38 -05:00
Danny f964651e60 No Bounds 2026-10-21 21:37:24 -04:00
Danny 4366e6ac7a Allow Full Widget Editing and Arrangement on 3X, add AA only widgets to 3X screen 2026-10-21 21:37:24 -04:00
firestar5683 55bbf6b98a Require emitted EV9 steering and select native test variants 2026-10-21 19:59:07 -05:00
Dom 77347170f8 Clarify SLC UI test preconditions and method mocks 2026-10-21 19:58:59 -05:00
firestar5683 d77bea5972 Pair GV60 longitudinal startup with exact native heartbeat safety 2026-10-21 19:56:15 -05:00
firestar5683 1973ad6439 Defer EV9 ECU takeover until replacement controls are ready 2026-10-21 19:37:46 -05:00
firestar5683 44beeb7606 Preserve HKG READY evidence across batched CAN frames 2026-10-21 18:51:48 -05:00
firestar5683 4854bc74b6 Preserve Mazda lateral control on unassigned Cancel 2026-10-21 18:38:17 -05:00
firestarsdog 3d2d094c8b SLC Fixes 2026-10-21 19:25:05 -04:00
firestarsdog 0972fec4ef SLC Styling 2026-10-21 19:25:05 -04:00
firestarsdog ab8368f287 Big SLC Overhaul 2026-10-21 19:22:59 -04:00
Dom 0299391cf8 Use pinned capnproto dependency in host heartbeat tests 2026-10-21 17:53:46 -05:00
firestar5683 db73e3bdbb Verify HKG AOL fault recovery against emitted steering commands 2026-10-21 16:49:14 -05:00
Dom 7edb10e149 Fix vision typing and projection HTTP test fixtures 2026-10-21 15:47:55 -05:00
firestar5683 4f3110af3a Preserve classic HKG AOL intent through location input faults 2026-10-21 15:41:17 -05:00
firestar5683 c7c3dbd699 Preserve HKG stock CAN-FD AOL intent during location input loss 2026-10-21 14:59:29 -05:00
firestar5683 58a4514321 Prepare ordinary HKG HDA-II takeover before publishing longitudinal control 2026-10-21 14:24:11 -05:00
firestar5683 4b3017a7fc Keep registered Ford CAN-FD discovery on normal bus 0 2026-10-21 13:59:11 -05:00
firestar5683 8e6cd917e4 Cover Ioniq 5 PE READY stock fallback with captured startup CAN 2026-10-21 12:56:37 -05:00
Dom ab9b2d7624 Make CI fixture preconditions and AST body types explicit 2026-10-21 12:49:11 -05:00
firestar5683 5ffbe27aaa Keep Ioniq 5 PE READY startup stock until ECU takeover is proven 2026-10-21 12:47:27 -05:00
firestar5683 66a573747d Prepare Ioniq 5 PE longitudinal takeover from a proven parked READY state 2026-10-21 12:45:05 -05:00
firestar5683 0c1f0b3d8d Retain Confirmed Vision Speed Limits 2026-10-21 12:43:45 -05:00
firestar5683 b3c26b5824 Match Dom hands-free cluster scope across Ford CAN-FD 2026-10-21 12:36:48 -05:00
firestar5683 43eea2e977 Restore Vision Speed Limits 2026-10-21 12:30:55 -05:00
firestar5683 3c687b55f7 Fix GM startup and steering receipt timing faults 2026-10-21 12:16:00 -05:00
prabhaavp 3372a71b4a Merge pull request #237 from dirwin31:PR-Smaller-3
Android Auto: Car screen widgets, navigation overlay, now playing and diagnostics
2026-10-21 13:11:34 -04:00
Prabhaav Pillai 9632656c59 Trim trivial verbose comments 2026-10-21 13:10:49 -04:00
Prabhaav Pillai 5a2e9d2606 AA documentation update and improve network recovery logic 2026-10-21 13:07:44 -04:00
firestar5683 1477a59fcd Prepare radar HDA-II takeover before publishing longitudinal control 2026-10-21 11:59:37 -05:00
firestar5683 8090e48441 Preserve HKG stock AOL intent through temporary location input loss 2026-10-21 11:44:58 -05:00
firestar5683 297f56f29a Match Dom Mach-E cluster messages with stock idle ownership 2026-10-21 11:32:09 -05:00
Dom 4fff2bba47 Assert captured messages before GM successor test mutations 2026-10-21 11:20:54 -05:00
firestar5683 4f1b668aea Keep Mach-E stock display ownership before lateral engagement 2026-10-21 11:10:14 -05:00
firestar5683 1319c60fda Preserve fresh GM lateral authority while braking 2026-10-21 10:54:26 -05:00
firestar5683 223c4f1322 Enforce current Hyundai startup evidence before controller output 2026-10-21 10:51:38 -05:00
Dom d3c0e9df8b Use typed CAN packets in Volt lifecycle fixture 2026-10-21 10:41:28 -05:00
firestar5683 e9abadd2f4 Capture F1 Volt withdrawal receipts in Card 2026-10-21 10:18:27 -05:00
firestar5683 9de201d18d Prepare radar HDA-I takeover before publishing longitudinal control 2026-10-21 09:54:27 -05:00
firestar5683 b300093058 Build conventional-cruise F1 Volt lateral startup 2026-10-21 09:52:58 -05:00
firestar5683 97a50dead6 Restore conventional-cruise F1 Volt lateral startup 2026-10-21 09:52:19 -05:00
Dom 210e6504fe Align updater test mocks with offroad status keyword 2026-10-21 09:39:51 -05:00
firestar5683 31d888ba79 Build RAV4 TSS2 pedal pairing 2026-10-21 09:35:25 -05:00
firestar5683 5ee8d662a7 Restore RAV4 TSS2 pedal interceptor pairing 2026-10-21 09:33:25 -05:00
firestar5683 c569a798ba Build Corolla TSS2 pedal pairing 2026-10-21 09:08:33 -05:00
firestar5683 f334ac0693 Restore Corolla TSS2 pedal interceptor pairing 2026-10-21 09:06:29 -05:00
firestar5683 defaf64c0e Allow Forced Offroad Updates 2026-10-21 08:31:44 -05:00
firestar5683 ba6eac264e Fix Big Model Cold Startup 2026-10-21 08:23:17 -05:00
firestar5683 4ed9546240 Test Subaru automatic stock startup ownership 2026-10-21 05:35:53 -05:00
firestar5683 1cbf3b927e Test Honda automatic stock startup and native handoff 2026-10-21 05:26:08 -05:00
firestar5683 a4e1631228 Keep Hyundai startup testers until controller publication succeeds 2026-10-21 04:12:13 -05:00
firestar5683 21ee77ea30 Prepare classic Hyundai ECU takeover before publishing longitudinal control 2026-10-21 03:41:28 -05:00
Dom fc6d6604aa Make bounded route inventory test limits explicit 2026-10-21 03:34:52 -05:00
firestar5683 cb85a84e30 Preserve AOL intent across native updates and classic cruise cancellation 2026-10-21 02:03:14 -05:00
firestar5683 708a68adeb Preserve Hyundai diagnostic settle time during stock restoration 2026-10-21 00:52:55 -05:00
firestar5683 67c1fb6a64 Preserve independent Bolt lateral below cruise engagement speed 2026-10-21 00:10:13 -05:00
firestar5683 fc2a89b988 Preserve GM MAIN intent through parked startup 2026-10-20 23:57:45 -05:00
firestar5683 f1434fe34d Retain Sonata Hybrid lateral intent when cruise is canceled 2026-10-20 23:51:40 -05:00
firestar5683 da628907d0 Preserve Hyundai diagnostic continuity across startup owners 2026-10-20 23:43:42 -05:00
firestar5683 5748148e7c Arm classic stock AOL after successful full engagement 2026-10-20 23:27:39 -05:00
firestar5683 8824bea044 Optimize FLM job encoding 2026-10-20 23:19:02 -05:00
firestar5683 2fb333f279 Build RAV4 Hybrid smartDSU ownership 2026-10-20 23:13:16 -05:00
firestar5683 4000b44866 Verify Toyota RX workspace reconfiguration 2026-10-20 23:13:14 -05:00
firestar5683 10be50fd7c Share Toyota RX checks across exact filter profiles 2026-10-20 23:08:42 -05:00
firestar5683 ad84d01235 Restore RAV4 Hybrid smartDSU ownership 2026-10-20 23:00:30 -05:00
firestar5683 ad3fa91b09 Restore recorded Sonata Hybrid diagnostic admission 2026-10-20 22:21:40 -05:00
firestar5683 01b06dcc71 Keep lateral renewal across stock cruise disengagement 2026-10-20 22:17:53 -05:00
Prabhaav Pillai 409601216b Build 2026-10-20 23:05:29 -04:00
Prabhaav Pillai d7db526f8c Revert "Add bundled Ion headers to device include path (WSL Fix)"
This reverts commit b209e6a655.
2026-10-20 23:05:04 -04:00
Prabhaav Pillai 4b1466f5eb Revert "Fix WSL device build headers and FFmpeg linkage"
Reverts the source changes from commit 470ab5cc70. The generated prebuilt manifest remains with the rebuilt artifacts.
2026-10-20 23:05:04 -04:00
firestar5683 51646bf4cb Receive fresh lateral inputs before withdrawing AOL 2026-10-20 22:04:30 -05:00
Dom 6ca072af44 Narrow test fixture types before mutation 2026-10-20 21:56:51 -05:00
firestar5683 ea916e50bd Restore classic Hyundai AOL steering dashboard icons 2026-10-20 21:43:04 -05:00
firestar5683 5cf5637d65 Detect refused steering on qualified SDGM Volts 2026-10-20 21:42:46 -05:00
firestar5683 2abc15c222 Preserve stock ASCM Volt lateral intent below cruise speed 2026-10-20 21:33:52 -05:00
firestarsdog 42d0beabee test: avoid build env cap'n proto links 2026-10-20 22:32:31 -04:00
firestar5683 f5f8a331f1 Preserve G90 diagnostic continuity through controller startup 2026-10-20 21:32:06 -05:00
firestar5683 a278aa8fba Preserve GPS daemon executable mode 2026-10-20 21:23:22 -05:00
firestar5683 ec856f38e6 Retain stock Ford AOL intent across short command gaps 2026-10-20 21:21:56 -05:00
firestar5683 f0f9b43bef Restore car GPS fixes on the standard logging channel 2026-10-20 21:21:52 -05:00
firestar5683 0b01007a9b Bound FLM replay memory 2026-10-20 21:20:01 -05:00
firestar5683 b9a8a41784 Build Jeep Grand Cherokee brake hold 2026-10-20 21:16:58 -05:00
firestar5683 2ad95d4176 Restore Jeep Grand Cherokee stock ACC brake hold 2026-10-20 21:12:26 -05:00
Danny e26202eae2 Bluetooth and Spotify: Phone audio, pairing bridge and Spotify player 2026-10-20 21:05:02 -05:00
firestar5683 4da8dd6c3f Build Panda authority-based siren warning 2026-10-20 21:02:58 -05:00
firestar5683 c0b16fe8cc Separate armed AOL heartbeat from Panda siren activity 2026-10-20 21:00:57 -05:00
Danny 7775d42443 FAQ update 2026-10-20 20:09:00 -04:00
Danny ab34e5d8a5 Trailing TLS OpenHeadUnit Fix 2026-10-20 20:09:00 -04:00
Danny 12b6dadf17 Android Auto: Car screen widgets, navigation overlay, now playing and diagnostics 2026-10-20 20:09:00 -04:00
prabhaavp 7ea822bbb1 Merge pull request #236 from dirwin31:PR-Smaller-2
Bluetooth and Spotify: Phone audio, pairing bridge and Spotify player
2026-10-20 20:06:28 -04:00
Prabhaav Pillai 86aa912748 Fix Tests for AA 2026-10-20 20:04:53 -04:00
Prabhaav Pillai 3c7898921b Safe app key removal after Bluetooth forget succeeds only 2026-10-20 20:03:38 -04:00
firestar5683 f6a03dce08 Build Ford AOL safety firmware 2026-10-20 18:52:57 -05:00
firestar5683 31c5010e11 Preserve Ford AOL intent across qualified physical pauses 2026-10-20 18:51:56 -05:00
Danny 7ea5058365 Bluetooth and Spotify: Phone audio, pairing bridge and Spotify player 2026-10-20 19:42:21 -04:00
firestar5683 7c9b45bb5b Build Tesla Pre-AP startup and safety artifacts 2026-10-20 18:39:50 -05:00
firestar5683 f4b8982ee0 Restore calibrated Tesla Pre-AP pedal control 2026-10-20 18:35:44 -05:00
Prabhaav Pillai 470ab5cc70 Fix WSL device build headers and FFmpeg linkage 2026-10-20 19:23:48 -04:00
Prabhaav Pillai cdcf6a0dd1 Build verified device artifacts 2026-10-20 19:22:40 -04:00
firestar5683 28a017e12e Reject stale and malformed Tesla HW1 radar observations 2026-10-20 18:03:22 -05:00
firestar5683 2a73dc50ff Restore Hyundai ECU response handling before longitudinal takeover 2026-10-20 18:02:47 -05:00
firestar5683 bd490d2ee3 Keep Transit steering angle encoding inside native limits 2026-10-20 18:01:32 -05:00
firestar5683 edb2be5716 Build Prius ZSS support 2026-10-20 17:59:09 -05:00
James f5c56bc631 ZSS support
Co-Authored-By: Erich Moraga <33645296+erichmoraga@users.noreply.github.com>
2026-10-20 17:56:01 -05:00
firestar5683 fc753b3900 Build Chrysler lateral intent 2026-10-20 17:48:18 -05:00
Dom b801a9ff42 Merge published navigation and device build updates 2026-10-20 17:44:27 -05:00
firestar5683 764b94f2ab Restore Chrysler stock ACC lateral intent 2026-10-20 17:41:20 -05:00
Dom de0092349d Fix navigation GPS carrier subscriptions
Use GPS transport services in navtilesd and MapFeed, preserving phone GPS through starpilotNavigation. Add startup subscription regressions.

Source-only correction; no prebuilt receipt or native artifacts changed.
2026-10-20 17:39:33 -05:00
Prabhaav Pillai b209e6a655 Add bundled Ion headers to device include path (WSL Fix) 2026-10-20 18:21:15 -04:00
prabhaavp d2db94e177 Merge pull request #235 from dirwin31:PR-Smaller-1
Core: Companion phone uplink, phone GPS, cellular off switch and usage stats
2026-10-20 18:16:33 -04:00
Prabhaav Pillai cfaf5b05b2 Refactor cellular data toggle descriptions for clarity and update GPS validation logic 2026-10-20 18:13:47 -04:00
Jeffrey Wong fdcf1db290 Build: keep the hash-pinned model guard libraries instead of recompiling them 2026-10-20 17:00:04 -05:00
firestar5683 68f0dde601 Build Volt conventional-cruise F1 firmware 2026-10-20 16:12:24 -05:00
firestar5683 19e0619ae9 Support no-camera Volt conventional cruise with F1 brakes 2026-10-20 16:09:48 -05:00
firestar5683 12d77dd2d6 Fix Ioniq reverse latch and left-paddle support 2026-10-20 15:49:56 -05:00
firestar5683 7d14e28c4d Build removed-camera Volt cruise-fault safety firmware 2026-10-20 15:43:46 -05:00
firestar5683 749dba88f5 Reject faulted cruise engagement on removed-camera Volts 2026-10-20 15:41:56 -05:00
firestarsdog 6bc930a07b Color border cleanup 2026-10-20 16:14:25 -04:00
firestarsdog 219dc02f11 Keep AOL cyan on gas without longitudinal control 2026-10-20 14:21:03 -04:00
firestar5683 37bd6981ad Build removed-camera Volt CAN isolation firmware 2026-10-20 13:03:25 -05:00
firestar5683 ded5368547 Keep removed-camera Volt traffic off the absent camera bus 2026-10-20 12:58:05 -05:00
firestar5683 aeb33f3ae7 Build Toyota stationary AOL continuity fix 2026-10-20 12:44:58 -05:00
firestar5683 9f2830b94b Merge current vehicle fixes for Toyota continuity 2026-10-20 12:41:08 -05:00
firestar5683 b07d3f94e2 Keep Toyota AOL armed through stationary output gaps 2026-10-20 12:41:07 -05:00
firestar5683 afd4cde972 Fix Volt source handoffs and transient clock sampling 2026-10-20 12:14:47 -05:00
firestarsdog d2cc8e282e + Gray Border Override for All Engagement
Update
2026-10-20 12:52:54 -04:00
Prabhaav Pillai 18505b6af7 Update updater status message for clarity 2026-10-20 12:09:10 -04:00
Prabhaav Pillai 223f1869eb Fix "This scan was incomplete. More local segments may exist." 2026-10-20 12:06:19 -04:00
firestar5683 c871086830 Build Bolt pedal AOL recovery firmware 2026-10-20 11:04:30 -05:00
Prabhaav Pillai 662e152627 Add DualShock 4 profile to joystick controller configurations Part 2 2026-10-20 12:02:59 -04:00
firestar5683 b432b14a3b Fix Bolt pedal startup, resume, and AOL override recovery 2026-10-20 10:58:04 -05:00
Prabhaav Pillai 1f733a74ef Add DualShock 4 profile to joystick controller configurations 2026-10-20 11:47:59 -04:00
Danny 2919499c61 Build qualified Honda AOL recovery firmware 2026-10-20 10:43:45 -05:00
Danny 9deea7d855 Honda: Keep longitudinal through temporary EPS faults and ignore camera CAN interrupt-rate faults 2026-10-20 10:41:03 -05:00
firestar5683 73c3ddd45c Keep HKG angle AOL armed through temporary stop restrictions 2026-10-20 09:59:17 -05:00
firestar5683 e96ba4205f Preserve stock HKG lateral ownership through delayed brake status 2026-10-20 09:29:55 -05:00
Danny 40abb31b4d Honda: Keep longitudinal through temporary EPS faults and ignore camera CAN interrupt-rate faults 2026-10-20 07:27:23 -07:00
Danny a8d121bf45 Core: Update developer panel tests for the cellular off switch 2026-10-20 06:08:45 -07:00
Danny 786a1981b6 Mapd: Ship the binary built with phone GPS support 2026-10-20 06:03:40 -07:00
Danny 8f628ba48b Core: Companion phone uplink, phone GPS, cellular off switch and usage stats 2026-10-20 06:03:37 -07:00
firestar5683 5c27cc667b Fix HKG brake continuity and duplicate PE startup receipts 2026-10-20 06:04:21 -05:00
firestar5683 9685faaed1 Build qualified HKG MAIN and angle AOL firmware 2026-10-20 03:09:13 -05:00
firestar5683 11a3272049 Preserve HKG MAIN rearm and mapped angle AOL cancellation 2026-10-20 03:08:45 -05:00
firestarsdog d9bd0a06a9 Galaxy Routes Gate Update 2026-10-20 03:34:53 -04:00
firestarsdog 651fc56ae6 Local Routes Explain 2026-10-20 03:27:17 -04:00
firestarsdog 63f6655dce Update camera colors 2026-10-20 03:04:39 -04:00
firestarsdog 2cb5b05746 Tweak disengaged muted effect 2026-10-20 02:45:39 -04:00
firestarsdog cda3693dd8 Unify Technicolor camera grading to distinguish engagement states 2026-10-20 02:20:29 -04:00
Prabhaav Pillai f23e0449ff Pause stars on slow scrolling in Firefox Mobile 2026-10-20 02:11:22 -04:00
Prabhaav Pillai 722ce72d33 Use restart background for Galaxy loading screen 2026-10-20 01:55:27 -04:00
Prabhaav Pillai 7f1196d00e Fix Galaxy update handoff and reuse boot screen 2026-10-20 01:49:49 -04:00
firestar5683 5b9713a042 Preserve qualified Sonata AOL through cruise CANCEL 2026-10-20 00:21:03 -05:00
1897 changed files with 191165 additions and 12013 deletions
+6
View File
@@ -12,6 +12,12 @@ openpilot/selfdrive/modeld/models/big_*_tinygrad.pkl -text
*.woff2 -text
*.wav -text
openpilot/starpilot/galaxy/web/vendor/vue/vue.esm-browser.js -text
*.apk -text
openpilot/starpilot/galaxy/bin/hev-socks5-tunnel_linux_arm64 -text
openpilot/starpilot/soloist/pulseaudio/bin/pulseaudio -text
openpilot/starpilot/soloist/pulseaudio/bin/pactl -text
openpilot/starpilot/soloist/bluealsa/bin/bluealsa-aplay -text
openpilot/starpilot/soloist/bluealsa/bin/bluealsad -text
openpilot/selfdrive/car/tests/test_models_segs.txt -text
openpilot/common/hardware/comma/updater -text
@@ -0,0 +1,68 @@
name: Android companion release
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: android-companion-release
cancel-in-progress: false
jobs:
build:
name: Test, build and sign the companion APK
runs-on: ubuntu-latest
timeout-minutes: 30
environment: android-release
defaults:
run:
working-directory: openpilot/starpilot/companion/android
steps:
- uses: actions/checkout@v7
with:
submodules: false
lfs: false
persist-credentials: false
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Install Android SDK platform and build tools
run: yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "platforms;android-37" "build-tools;37.0.0"
- name: Unit tests
run: ./gradlew testDebugUnitTest
- name: Restore signing key
env:
KEYSTORE_BASE64: ${{ secrets.COMPANION_KEYSTORE_BASE64 }}
run: |
umask 077
printf '%s' "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/companion.jks"
- name: Build and sign the APK
env:
COMPANION_KEYSTORE: ${{ runner.temp }}/companion.jks
COMPANION_KEY_PASSWORD: ${{ secrets.COMPANION_KEY_PASSWORD }}
run: ./build_apk.sh
- name: Remove signing key
if: always()
run: rm -f "$RUNNER_TEMP/companion.jks"
- name: Check the APK
working-directory: openpilot/starpilot/galaxy/bin
run: |
if unzip -l starpilot-companion.apk | grep -q 'assets/galaxy/'; then
echo 'APK still bundles Galaxy' >&2
exit 1
fi
BT="$ANDROID_HOME/build-tools/37.0.0"
"$BT/aapt2" dump badging starpilot-companion.apk | head -1
"$BT/apksigner" verify --print-certs starpilot-companion.apk | grep -E 'certificate (DN|SHA-256)'
cat starpilot-companion.apk.version
- uses: actions/upload-artifact@v4
with:
name: starpilot-companion-apk
path: |
openpilot/starpilot/galaxy/bin/starpilot-companion.apk
openpilot/starpilot/galaxy/bin/starpilot-companion.apk.version
if-no-files-found: error
retention-days: 30
+1 -2
View File
@@ -29,11 +29,10 @@ jobs:
steps:
- uses: commaai/timeout@v1
- uses: actions/checkout@v7
- run: ./tools/op.sh setup
# Build
- name: Build docs
run: ./tools/op.sh docs --build
run: python3 docs/serve.py --build
# Push to docs.comma.ai
- uses: actions/checkout@v7
+69
View File
@@ -0,0 +1,69 @@
name: iOS companion
on:
push:
branches: [SecretGoodStarPilot, secretgood-ios-integration]
paths:
- 'openpilot/starpilot/companion/ios/**'
- 'openpilot/starpilot/galaxy_bluetooth/**'
- '.github/workflows/ios-companion.yaml'
pull_request:
paths:
- 'openpilot/starpilot/companion/ios/**'
- 'openpilot/starpilot/galaxy_bluetooth/**'
- '.github/workflows/ios-companion.yaml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ios-companion-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
app:
name: Swift tests and iOS Simulator build
runs-on: macos-26
timeout-minutes: 25
defaults:
run:
working-directory: openpilot/starpilot/companion/ios
steps:
- uses: actions/checkout@v4
with:
submodules: false
lfs: false
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Run companion regressions
env:
CI_PRIMARY_REPOSITORY_PATH: ${{ github.workspace }}/openpilot/starpilot/companion/ios
run: sh ci_scripts/ci_post_clone.sh
- name: Build for iOS Simulator
run: >-
xcodebuild -project GalaxyBluetooth.xcodeproj
-scheme GalaxyBluetooth -configuration Debug
-sdk iphonesimulator -destination 'generic/platform=iOS Simulator'
-derivedDataPath "$RUNNER_TEMP/galaxy-ios-build"
CODE_SIGNING_ALLOWED=NO build
bridge:
name: Comma Bluetooth bridge tests
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
submodules: false
lfs: false
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install bridge test dependencies
run: python -m pip install pycryptodome
- name: Run bridge regressions
run: python -m unittest discover -s openpilot/starpilot/galaxy_bluetooth/tests -v
+194 -8
View File
@@ -34,6 +34,11 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v7
- name: Validate published device build receipt
if: github.event_name == 'push' && github.ref == 'refs/heads/SecretGoodStarPilot'
run: python3 tools/ci/check_prebuilt_receipt.py --revision HEAD
- name: Check shipped device runtime dependencies
run: python3 -m unittest tools.laptop_device_build.tests.test_runtime_dependencies -v
- name: Validate tracked dependencies
run: python3 tools/vendor/check.py --revision HEAD
- run: ./tools/op.sh setup
@@ -196,13 +201,8 @@ jobs:
timeout-minutes: ${{ contains(runner.name, 'nsc') && 2 || 20 }}
env:
RAYLIB_BACKEND: headless
STARPILOT_TEST_DIAGNOSTICS: ci-results/host-test-diagnostics/unittest
run: tools/op.sh test --json-output unit-test-results.json
- name: Test custom module-level pytest cases
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
timeout-minutes: 15
env:
RAYLIB_BACKEND: headless
run: python3 tools/ci/run_host_tests.py --pytest
- name: Test GM vehicle configurations and CAN behavior
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
timeout-minutes: 5
@@ -227,13 +227,139 @@ jobs:
STARPILOT_PYTHON: ${{ github.workspace }}/.venv/bin/python
run: go test -mod=readonly -race -run '^Test(MsgqNativeLayout|Shadow(GoPythonIPC|ExecutableHostGpsIPC))$' -count=1 -v
- name: Save complete unit test results
if: ${{ always() && (hashFiles('unit-test-results.json') != '' || hashFiles('gm-test-results.json') != '') }}
if: ${{ always() && (hashFiles('unit-test-results.json') != '' || hashFiles('gm-test-results.json') != '' || hashFiles('ci-results/host-test-diagnostics/**') != '') }}
uses: actions/upload-artifact@v4
with:
name: unit-test-results
path: |
unit-test-results.json
gm-test-results.json
ci-results/host-test-diagnostics/
if-no-files-found: error
custom_pytest:
name: custom pytest (${{ matrix.shard }})
runs-on: ubuntu-24.04
timeout-minutes: 35
permissions:
contents: read
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
steps:
- uses: actions/checkout@v7
- name: Provide JavaScript runtime for Galaxy contract tests
uses: actions/setup-node@v7
with:
node-version: '24'
package-manager-cache: false
- name: Provide pinned map provider toolchain
uses: actions/setup-go@v7
with:
go-version: '1.25.1'
cache-dependency-path: mapd_repo/go.sum
- name: Fetch pinned map provider dependencies
working-directory: mapd_repo
env:
GOTOOLCHAIN: local
run: go mod download
- run: ./tools/op.sh setup
id: setup
- name: Build openpilot
id: build
run: scons
- name: Provide H.264 encoder for Android Auto integration tests
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: |
uv pip install --python .venv/bin/python --only-binary av av==19.0.1
.venv/bin/python -c "import av; assert 'libx264' in av.codecs_available"
- name: Test every assigned custom pytest node serially
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
timeout-minutes: 15
env:
RAYLIB_BACKEND: headless
run: .venv/bin/python -m tools.ci.custom_pytest_shards run --index "${{ matrix.shard }}" --count 24 --output "ci-results/custom-pytest-${{ matrix.shard }}"
- name: Save collection, every outcome, JUnit and durable worker diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: custom-pytest-${{ matrix.shard }}
path: ci-results/custom-pytest-${{ matrix.shard }}/
if-no-files-found: error
custom_pytest_complete:
name: complete custom pytest catalog
needs: custom_pytest
if: ${{ always() && !cancelled() }}
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v4
with:
pattern: custom-pytest-*
path: ci-results/custom-pytest-shards
- name: Require every case passed or audited capability-unavailable
run: python3 -m tools.ci.custom_pytest_shards aggregate --directory ci-results/custom-pytest-shards --count 24 --output ci-results/custom-pytest-complete.json
- name: Save complete custom catalog verdict
if: always()
uses: actions/upload-artifact@v4
with:
name: custom-pytest-complete
path: ci-results/custom-pytest-complete.json
if-no-files-found: error
aol_lifecycles:
name: AOL lifecycles (${{ matrix.shard }})
runs-on: ubuntu-24.04
timeout-minutes: 35
permissions:
contents: read
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
env:
FUZZ_SEED: '0'
steps:
- uses: actions/checkout@v7
- run: ./tools/op.sh setup
- name: Build native host dependencies
run: scons
- name: Gate every assigned lifecycle module serially
timeout-minutes: 10
run: python3 tools/ci/run_vehicle_tests.py --suite aol-lifecycle --shard-index "${{ matrix.shard }}" --shard-count 16 --output "ci-results/aol-lifecycle-${{ matrix.shard }}"
- name: Save every lifecycle outcome and native build
if: always()
uses: actions/upload-artifact@v4
with:
name: aol-lifecycle-${{ matrix.shard }}
path: ci-results/aol-lifecycle-${{ matrix.shard }}/
if-no-files-found: error
aol_lifecycle_complete:
name: complete AOL lifecycle catalog
needs: aol_lifecycles
if: ${{ always() && !cancelled() }}
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v4
with:
pattern: aol-lifecycle-*
path: ci-results/aol-lifecycles
- name: Require every full-catalog case exactly once and passed
run: python3 tools/ci/lifecycle_shards.py ci-results/aol-lifecycles --count 16 --output ci-results/aol-lifecycle-complete.json
- name: Save complete catalog verdict
if: always()
uses: actions/upload-artifact@v4
with:
name: aol-lifecycle-complete
path: ci-results/aol-lifecycle-complete.json
if-no-files-found: error
map_provider:
@@ -269,7 +395,7 @@ jobs:
strategy:
fail-fast: false
matrix:
suite: [interfaces, safety-debug, safety-release]
suite: [interfaces, safety-debug]
env:
FUZZ_SEED: '0'
steps:
@@ -295,6 +421,57 @@ jobs:
path: ci-results/${{ matrix.suite }}/
if-no-files-found: error
safety_release_contracts:
name: RELEASE vehicle contracts (${{ matrix.shard }})
runs-on: ubuntu-24.04
timeout-minutes: 35
permissions:
contents: read
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3]
env:
FUZZ_SEED: '0'
steps:
- uses: actions/checkout@v7
- run: ./tools/op.sh setup
- name: Build native host dependencies
run: scons
- name: Run every assigned RELEASE target serially
timeout-minutes: 20
run: python3 tools/ci/run_vehicle_tests.py --suite safety-release --shard-index "${{ matrix.shard }}" --shard-count 4 --output "ci-results/safety-release-${{ matrix.shard }}"
- name: Save every RELEASE outcome and native build
if: always()
uses: actions/upload-artifact@v4
with:
name: safety-release-${{ matrix.shard }}
path: ci-results/safety-release-${{ matrix.shard }}/
if-no-files-found: error
safety_release_complete:
name: complete RELEASE vehicle catalog
needs: safety_release_contracts
if: ${{ always() && !cancelled() }}
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v4
with:
pattern: safety-release-*
path: ci-results/safety-release-shards
- name: Require every RELEASE case exactly once and passed
run: python3 tools/ci/lifecycle_shards.py ci-results/safety-release-shards --suite safety-release --count 4 --output ci-results/safety-release-complete.json
- name: Save complete RELEASE verdict
if: always()
uses: actions/upload-artifact@v4
with:
name: safety-release-complete
path: ci-results/safety-release-complete.json
if-no-files-found: error
recorded_vehicles:
name: recorded vehicles (${{ matrix.variant }})
runs-on: ubuntu-24.04
@@ -398,6 +575,15 @@ jobs:
with:
name: process_replay_diff.txt
path: openpilot/selfdrive/test/process_replay/diff.txt
- name: Save complete replay outputs for review
uses: actions/upload-artifact@v7
if: always()
with:
name: process-replay-outputs
path: |
openpilot/selfdrive/test/process_replay/fakedata/*.zst
openpilot/selfdrive/test/process_replay/coverage.json
if-no-files-found: error
- name: Upload diff report
uses: actions/upload-artifact@v7
if: always() && github.event_name == 'pull_request'
+5
View File
@@ -18,6 +18,7 @@ a.out
.venv-linux-arm64/
bin/
!openpilot/starpilot/galaxy/bin/
!openpilot/starpilot/soloist/pulseaudio/bin/
# created at launch for comma hardware PYTHONPATH (PC uses editable installs via pyproject.toml)
/msgq
@@ -44,6 +45,8 @@ bin/
*.os-*
*.so
!openpilot/tools/replay/libmedia_mux.so
!openpilot/starpilot/soloist/pulseaudio/lib/*.so
!openpilot/starpilot/soloist/pulseaudio/modules/*.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
@@ -138,3 +141,5 @@ openpilot/selfdrive/modeld/models/*.unchunked
/jetlink_repo/jetlink.egg-info/
# Local Google Play resolver build output (source and lockfile are versioned).
openpilot/starpilot/flm/_primitive_walk.cpp
+8
View File
@@ -25,3 +25,11 @@ The vendored `opendbc_repo/opendbc/bluepilot_lateral` angle core, codecs and For
## 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.
## Spotify Bluetooth output
Spotify's Bluetooth output binds the comma's BlueALSA A2DP source to the selected speaker through a process-local ALSA configuration, leaving the system ALSA default, soundd and the warning speaker untouched. That approach is adapted from firestar5683's RoadScore Bluetooth output (`roadscore/prototype/bluetooth_output.py`, firestar5683/StarPilot `RoadScore` at `249eb88b3a397db847e4a595d7e23b7e85d329e5`). The Soloist player itself is never included: each user downloads it from Spotify, and the PulseAudio server that carries its audio is vendored unmodified from Ubuntu (see THIRD_PARTY_NOTICES.md).
## Tesla Pre-AP pedal control
The Pre-AP configuration, pedal codec/feedback, VirtualDAS feedforward table, grade compensation, torque-zero estimator and vehicle-specific gains preserve firestar5683's intentional Dom work, including `7837a3c1aab`, `aed0b7fdb9e9` and pinned `b220a37f4bd119e402225a7aa16a7cd84878d0af`. Original author attribution: firestar5683 <168790843+firestar5683@users.noreply.github.com>. Startup/calibration ownership, current upstream button/PID interfaces and native command bounds are adapted to the current StarPilot architecture; the old permissive native gateway is not transplanted. Existing source/license notices remain intact.
+1
View File
@@ -329,6 +329,7 @@ SConscript([
'openpilot/selfdrive/ui/SConscript',
'openpilot/starpilot/audio/native/SConscript',
'openpilot/starpilot/models/runtime_bridge/SConscript',
'openpilot/starpilot/flm/SConscript',
])
# Replay is also the isolated on-device viewer engine.
+28
View File
@@ -112,3 +112,31 @@ 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.
## PulseAudio server for Spotify
`openpilot/starpilot/soloist/pulseaudio/` contains unmodified AArch64 binaries from Ubuntu 24.04 packages: PulseAudio 16.1 (LGPL-2.1-or-later), libltdl (LGPL-2.1-or-later), libtdb (LGPL-3.0-or-later), libsoxr (LGPL-2.1-or-later), liborc and speexdsp (BSD-style). The complete copyright and license notices are in that directory's `licenses/`, package hashes in `PACKAGES.sha512`, and the corresponding source is available from Ubuntu's archive for the exact versions listed in its README. The libraries are dynamically linked and may be replaced with `update.sh`.
## Galaxy Android companion tunnel
`openpilot/starpilot/galaxy/bin/hev-socks5-tunnel_linux_arm64` is the upstream
[hev-socks5-tunnel 2.18.0](https://github.com/heiher/hev-socks5-tunnel/releases/tag/2.18.0)
static Linux ARM64 release, under the MIT license. Its notice is retained in
`openpilot/starpilot/galaxy/bin/LICENSE.hev-socks5-tunnel`; the adjacent SHA-256
file pins the downloaded artifact. The Android companion uses the Kotlin
standard library (Apache-2.0) and the Gradle wrapper (Apache-2.0); no third-party
UI framework is bundled.
## Galaxy Bluetooth companion source snapshot
The Galaxy Bluetooth phone bridge and iOS app from https://github.com/firestar5683/StarPilot
(natehuby76, PRs #224/#225) are included under `openpilot/starpilot/galaxy_bluetooth/`
and `openpilot/starpilot/companion/ios/`, with their license files (`UPSTREAM-LICENSE`, `openpilot/starpilot/companion/ios/UPSTREAM-GALAXY-LICENSE`).
The bridge runs on openpilot's Python with PyCryptodome 3.23.0 (BSD/public-domain
components, from opendbc). Its other dependencies are vendored unmodified, as
pure-Python sources, in `openpilot/starpilot/galaxy_bluetooth/vendor/`:
dbus-next 0.2.3 (MIT), ifaddr 0.2.0 (MIT) and Zeroconf 0.151.3
(LGPL-2.1-or-later). Their license texts are in `vendor/licenses/`, and
`vendor/VENDOR.json` records each PyPI source file and its SHA-256. Zeroconf is
kept as separate, unmodified source files that can be replaced with another
compatible version, as the LGPL requires.
Executable
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
profile="${1:-c4}"
case "${profile}" in
c3) big=1 ;;
c4) big=0 ;;
*) echo "Usage: ./device [c3|c4]" >&2; exit 2 ;;
esac
# Run the real manager with host hardware/cloud disabled. The manager owns the
# UI and Galaxy processes, just as it does on a comma device.
export STARPILOT_CLOUD_DISABLED=1
export BIG="${big}"
export BLOCK="${BLOCK:+${BLOCK},}replay_owner,map_snapshot_operations"
exec "${ROOT_DIR}/dev" device
+6 -1
View File
@@ -10,7 +10,12 @@ $Go.import("pfeifer.dev/mapd/cereal/custom");
# DO rename the structs
# DON'T change the identifier (e.g. @0x81c2f05a394cf4af)
struct CustomReserved0 @0x81c2f05a394cf4af {
struct StarPilotCarControl @0x81c2f05a394cf4af {
# The first three fields are pointer slots mapd does not read.
hudControl @0 :AnyPointer;
steeringLimitInfo @1 :AnyPointer;
navigation @2 :AnyPointer;
phoneGps @3 :StarPilotCarState.Gps; # sourceMonoTime is comma receive time minus the phone's fix age
}
struct CustomReserved1 @0xaedffd8f31e7b55d {
+405 -331
View File
@@ -9,69 +9,137 @@ import (
math "math"
)
type CustomReserved0 capnp.Struct
type StarPilotCarControl capnp.Struct
// CustomReserved0_TypeID is the unique identifier for the type CustomReserved0.
const CustomReserved0_TypeID = 0x81c2f05a394cf4af
// StarPilotCarControl_TypeID is the unique identifier for the type StarPilotCarControl.
const StarPilotCarControl_TypeID = 0x81c2f05a394cf4af
func NewCustomReserved0(s *capnp.Segment) (CustomReserved0, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0})
return CustomReserved0(st), err
func NewStarPilotCarControl(s *capnp.Segment) (StarPilotCarControl, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 4})
return StarPilotCarControl(st), err
}
func NewRootCustomReserved0(s *capnp.Segment) (CustomReserved0, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0})
return CustomReserved0(st), err
func NewRootStarPilotCarControl(s *capnp.Segment) (StarPilotCarControl, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 4})
return StarPilotCarControl(st), err
}
func ReadRootCustomReserved0(msg *capnp.Message) (CustomReserved0, error) {
func ReadRootStarPilotCarControl(msg *capnp.Message) (StarPilotCarControl, error) {
root, err := msg.Root()
return CustomReserved0(root.Struct()), err
return StarPilotCarControl(root.Struct()), err
}
func (s CustomReserved0) String() string {
func (s StarPilotCarControl) String() string {
str, _ := text.Marshal(0x81c2f05a394cf4af, capnp.Struct(s))
return str
}
func (s CustomReserved0) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr {
func (s StarPilotCarControl) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr {
return capnp.Struct(s).EncodeAsPtr(seg)
}
func (CustomReserved0) DecodeFromPtr(p capnp.Ptr) CustomReserved0 {
return CustomReserved0(capnp.Struct{}.DecodeFromPtr(p))
func (StarPilotCarControl) DecodeFromPtr(p capnp.Ptr) StarPilotCarControl {
return StarPilotCarControl(capnp.Struct{}.DecodeFromPtr(p))
}
func (s CustomReserved0) ToPtr() capnp.Ptr {
func (s StarPilotCarControl) ToPtr() capnp.Ptr {
return capnp.Struct(s).ToPtr()
}
func (s CustomReserved0) IsValid() bool {
func (s StarPilotCarControl) IsValid() bool {
return capnp.Struct(s).IsValid()
}
func (s CustomReserved0) Message() *capnp.Message {
func (s StarPilotCarControl) Message() *capnp.Message {
return capnp.Struct(s).Message()
}
func (s CustomReserved0) Segment() *capnp.Segment {
func (s StarPilotCarControl) Segment() *capnp.Segment {
return capnp.Struct(s).Segment()
}
// CustomReserved0_List is a list of CustomReserved0.
type CustomReserved0_List = capnp.StructList[CustomReserved0]
// NewCustomReserved0 creates a new list of CustomReserved0.
func NewCustomReserved0_List(s *capnp.Segment, sz int32) (CustomReserved0_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz)
return capnp.StructList[CustomReserved0](l), err
func (s StarPilotCarControl) HudControl() (capnp.Ptr, error) {
return capnp.Struct(s).Ptr(0)
}
// CustomReserved0_Future is a wrapper for a CustomReserved0 promised by a client call.
type CustomReserved0_Future struct{ *capnp.Future }
func (s StarPilotCarControl) HasHudControl() bool {
return capnp.Struct(s).HasPtr(0)
}
func (f CustomReserved0_Future) Struct() (CustomReserved0, error) {
func (s StarPilotCarControl) SetHudControl(v capnp.Ptr) error {
return capnp.Struct(s).SetPtr(0, v)
}
func (s StarPilotCarControl) SteeringLimitInfo() (capnp.Ptr, error) {
return capnp.Struct(s).Ptr(1)
}
func (s StarPilotCarControl) HasSteeringLimitInfo() bool {
return capnp.Struct(s).HasPtr(1)
}
func (s StarPilotCarControl) SetSteeringLimitInfo(v capnp.Ptr) error {
return capnp.Struct(s).SetPtr(1, v)
}
func (s StarPilotCarControl) Navigation() (capnp.Ptr, error) {
return capnp.Struct(s).Ptr(2)
}
func (s StarPilotCarControl) HasNavigation() bool {
return capnp.Struct(s).HasPtr(2)
}
func (s StarPilotCarControl) SetNavigation(v capnp.Ptr) error {
return capnp.Struct(s).SetPtr(2, v)
}
func (s StarPilotCarControl) PhoneGps() (StarPilotCarState_Gps, error) {
p, err := capnp.Struct(s).Ptr(3)
return StarPilotCarState_Gps(p.Struct()), err
}
func (s StarPilotCarControl) HasPhoneGps() bool {
return capnp.Struct(s).HasPtr(3)
}
func (s StarPilotCarControl) SetPhoneGps(v StarPilotCarState_Gps) error {
return capnp.Struct(s).SetPtr(3, capnp.Struct(v).ToPtr())
}
// NewPhoneGps sets the phoneGps field to a newly
// allocated StarPilotCarState_Gps struct, preferring placement in s's segment.
func (s StarPilotCarControl) NewPhoneGps() (StarPilotCarState_Gps, error) {
ss, err := NewStarPilotCarState_Gps(capnp.Struct(s).Segment())
if err != nil {
return StarPilotCarState_Gps{}, err
}
err = capnp.Struct(s).SetPtr(3, capnp.Struct(ss).ToPtr())
return ss, err
}
// StarPilotCarControl_List is a list of StarPilotCarControl.
type StarPilotCarControl_List = capnp.StructList[StarPilotCarControl]
// NewStarPilotCarControl creates a new list of StarPilotCarControl.
func NewStarPilotCarControl_List(s *capnp.Segment, sz int32) (StarPilotCarControl_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 4}, sz)
return capnp.StructList[StarPilotCarControl](l), err
}
// StarPilotCarControl_Future is a wrapper for a StarPilotCarControl promised by a client call.
type StarPilotCarControl_Future struct{ *capnp.Future }
func (f StarPilotCarControl_Future) Struct() (StarPilotCarControl, error) {
p, err := f.Future.Ptr()
return CustomReserved0(p.Struct()), err
return StarPilotCarControl(p.Struct()), err
}
func (p StarPilotCarControl_Future) HudControl() *capnp.Future {
return p.Future.Field(0, nil)
}
func (p StarPilotCarControl_Future) SteeringLimitInfo() *capnp.Future {
return p.Future.Field(1, nil)
}
func (p StarPilotCarControl_Future) Navigation() *capnp.Future {
return p.Future.Field(2, nil)
}
func (p StarPilotCarControl_Future) PhoneGps() StarPilotCarState_Gps_Future {
return StarPilotCarState_Gps_Future{Future: p.Future.Field(3, nil)}
}
type CustomReserved1 capnp.Struct
@@ -3327,306 +3395,312 @@ func NewMapdOut_GpsSource_List(s *capnp.Segment, sz int32) (MapdOut_GpsSource_Li
return capnp.NewEnumList[MapdOut_GpsSource](s, sz)
}
const schema_b526ba661d550a59 = "x\xda\x94z\x0d\x90T\xd5\x95\xff9\xef\xf5L\xcf'" +
"M\xf3\x1a\x01\x01\xdb!\x928\xc41\xc0\xc8?\x0eh" +
"\x86\xa1g@\xa8\xeea\xde<>\x1c\x94\xaa\xdc\xe9w" +
"\xa7\xe7\xc1\x9b\xf7\x9a\xf7^\xcfLSX \x81*\xf4" +
"\x8f\x15\xcdj\x82)\xd9\xf8\x81\xb5\x9a\x95\x84du\x8b" +
"XX\x0b\x14\xa9B\x17\xaa4\xa5)c\xcc\xfaQ\xd9" +
"\xd2d\xb5\x8cY\xad5\xd9X\xbdu\xee\xed\xaf\xf9\x88" +
"8E\xd5t\xf7\xef\x9cw\xee\xb9\xe7\x9e{\xee\xf9\xdd" +
"\xc7r6\x7fmhE\xf3?\x8d\x81bdkjj" +
"\x0b\xd1\xdd\x03\xef:\xc1O\x0e@t!\x16\x06\x1a\xb6" +
".\x1ez\xfe\xab\xcfA(\x0c\xd0\xfe\x8b5;P\xbb" +
"\xbc&\x0cj\xe1\xa7\x9f$;v\xfc\xe9\xfc\xdd\xd3h" +
"=MZ/\x08\xad\xdb\x13\xe7\xbf;\xf8\xd5\x1f\x1d\x86" +
"\xe8B\xa5\xa2\x05\xd8\xfe\xf0\x9aM\xa8\x9d\\#\xd5\xdd" +
"\x10`\xa1\xed\xc5\xdc\xed?^p\xdf\x03\xa0\xb7a}" +
"\xe1\xbc\xf3o\x8d\xdb.\xdc\xf9\xdfP\x83\xa4\xb3t\xed" +
"\xbd\xa8\xdd\xba\x96\xbev\xac}K\x01,\xfc\xe3\xd7?" +
"\xcaY{w|\x1f\xa2_Q\x0a\x07?\xfd\xf4+\xed" +
"\xff\xf5\xf1\x09\xb2\xbc'q\x10\xb5\xc3\x890\x80vw" +
"\xe2\x07\x80\x859\xcfb&s\xf1\xf2#\xd3x\xfaJ" +
"b\x10\xb5\xffL\x90\xa7m\xbf\x8d\xab\xdd\xe1\xe1\xc7\xa6" +
"\xd1:\x97\xd8\x81\xdakB\xcbV\xdb\xd7~6X\xfb" +
"\x04\xe8\x0b\xb1J\xadF\xe8\xfd\x8c\xf4~\x99\x90\x8fl" +
"G\xc0\x8a_\xfaB\x9cU\xa5^K:\xabz\xaeF" +
"\xad\xa7\xe7k\x00\xed\xacg,\x0cX\xe8N\x9c}\xe9" +
"\xe4[O<9%Z\x7fH\x1dD\xed\xf3\x14\xcd\xe9" +
"/\xa9o\x02\x16\xb6\x9e\xd0\x7fs\xc3\xe8\xe5'\xa7\xf1" +
"\xf6\xf3\xd4\x0e\xd4\xa2\xbd\xe4\xedO~\x9b~\xe8\xff?" +
"\xf5\xd1?O\xb1\xf7aju\xc5\xdef\xc0\xc2\xce\xe7" +
"\xde[\xf1\xdd\xcf\xdf\xfa\xc94\xf6\x9a{w\xa0\xd6\"" +
"\xecu\xbe\xf5\xc7\xbbR\xd7n?5\xed\xa8\x83\xa5Q" +
"/b\xb2\xc5J;\xa7\xa7\xd1\xfa\x90|C\xa15|" +
"\xe6\x1f\x1e4\xd6t\xbf0\x8d\xd6\xef\xc8\xd6\xc7)\xd2" +
":\xa4\xde\xf46\xbf\xe5\xba\xf3\xd3h]&\xadw\x84" +
"\xd6G\xff\xf2\xadWn\xbeu\xd7\xc5\xc9\xab\xa2\x90\xde" +
"\x0b\xa99\xa8]\xa6\xb9\xb6\xbf\x98\x8a\xd3\xaa\xac\x1e\xd8" +
"\x9e\xb5_\xfb\xd1\xbfO7t/\x0d-\x1c|\xf9\xe0" +
"\xa3\x99\xff}\xe3\xfb\x97\xa7\x1b\x9a\xb4\xde\x11Z\x8b/" +
"\xa4\xdf|38\xf8\xca\x94\x10\xbf\xd0\xbb\x0e\xb5\xcb\xbd" +
"b\xd8^\x91\x0c\x1d\x1dg\x1e\xbbt\xef\xff\xfc\x9a\xbc" +
"T'o\xac\xcd\x9bP\xbb\xbc\x99\x16\xe4\xc5\xcd\xef\x01" +
"\x16V%\xbb\xbfsl\xfb\xf7\xdf\x98f\xf4\x93};" +
"P;\xd7G\xa3w|\xe7\xa5\x1f\xfe\xa0\xe6\x07\xffA" +
"&\x95Ij\xc7\xfb\xd6\xa1v\xb2\x8fL>\xdd7\x06" +
"X\xb8\xfe\x95_\x7ftC\xcf\xb3\x7f\x9e\xb2c\x9a\xf5" +
"]\xa8\xb5\xe8\xa4\xb9X\xcf\x00V6\xdfD\xb3r+" +
"n$\xed\x9d\xfa<\x80v\xae\x87)m\x17\x1c?\xbe" +
"\xb1\xfe\x83\xab>\x99\xc6\xd7\x91\x81\x1d\xa8\xdd=@\xbe" +
">\x11\xca~\xbe\xe6\xd0}\x7f\x99Fk'i\xed\x11" +
"Z\xcb\xde_\xda\xfb\xde\x9d\xdf\xfa\xeb\x94x\xa6\x06\x06" +
"Qc\x03\xe4\xe4\xce\x81\xfd\x80\x85\xc7\xfd\xbe\x8b/\xed" +
"|\xe2\xaf\x93\x9d\x14\x8b~|\xe0 j?\x1b\x10\xe1" +
"\x1a\x10\xd1\x8f\xfe\xeb\xd8\x91\x0f\xbb\x06\xff6\xcd\xf0m" +
"w\x0c\xa2\xd6u\x07\x0d\xbf\xff\xd8\xa9\xf7\x8ccG\x0a" +
"\x933I\xcc|\xf1\x1d\xcf\xa3\xb6\xea\x0era\xc5\x1d" +
"?\x85\xc2\xdf\xfd\xf7P!\xcd=\xce\xeco\xa4C9" +
"?pG\xbe\x91\x16\x1f7\xa6Y\xd6\xc9\xaeN\x88\x1f" +
"\xfd\xdc\xe7\xde(W\xcd\x9b\xfa\x10g\xa2\xbf\xfcJ\xfa" +
")\x9657:\xd9\\\xb0%\x9f\xe5\x00}\x88\xfa\x19" +
"T\x00\xb46e\x13\x00\xce\xd2Z\x95\x9f\x03`Dk" +
"U\x1e\x07\xc0\xd9Z\xab\xf2c\x00\x8cj\xad\xcay\x00" +
"\x9c\xa3\xb5*o\x00`\xa3\xd6\xa6\x0c\x02\xa0\xa6\xb5*" +
"/\x01`Lk\x13\x9f\xa8\xadP\xf6\x02\xa0\xa2\xb5)" +
"\xbb\x00p\xae\xd6*~_\xa5-U\xfe\x08\x80\xf3\xb4" +
"V\xe5W\x008_kS\xde\x05\xc0\x05\xda\x0a\xf1\xfb" +
"jm\x95\xf2C\x00\\\xa8\xad\x12\xe3.\xd2V\x09{" +
"\x8b\xb5\x0e\xf1\xfb\x1a\xadC\xf8\xa5\x16\x7f\x87\xb4\x0e\xe1" +
"O\\\xeb\x10v\xaf\xd5n\x15\xf6Z\xda\xbb\x94\xd5\x08" +
"\x805\xdaF\xe5y\x00\xac\xd56\x0a\x07\x9a\xb5\x1ee" +
"\x07\x006i]\xc2\xb1%\xda\xad\xe2\xc1\xafh]\xc2" +
"\xf0u\xc5\xcf\xa5\xed]\xca\x1c2\xd0\xa0mT\xee\x05" +
"\xc0\xafj\x1b\x95?\x03\xe0\xd7\xdaS\xca\x12\x12\\\xaf" +
"m\x15!hm\x1fP\x14\x02\x96\xb5\xef\x94_\xbe\xae" +
"1\xe5{\x00x\x83\xc6\xc4\xa3m\x1a\x13\xc1\x0b\x17\x7f" +
"\xd7iL9\x08\x80\xf5\xdaN\xf1y\xa36@>\x14" +
"Lw\xcc\xb1]f\x02@\xc1\xe7\xc1\x16\xe6e8\x06" +
"I\x16p\x8f\xd9\xf1\xaet\x9a\xdb\x84\x1bY\xceML" +
"Z#V\xb0yh(\xec\xf3`\x12\x9ap\x9dH\xe0" +
"\xb9B9\xc5\xb2\x89\\\x8d7\xca\x85<\xe1:$\x00" +
"\x9f\x07\xdb,\xdfr\x9dDn\x82H\x95\x0f%\xddL" +
"\x92CxT\x8e'4\x15\xa9*|\"\x97\xba\xa4\x8f" +
"\xd5\xb2\x94\xe5H\xf16\x80\x82\xc7i&\x06\x87\xce " +
"\xb0\x9c\x8c_\xf0\xd9(7x\x10@D\xfe\xe4A\x8f" +
"\xc3\x06m\xe8\x94\xc3O6\xb6\xd5\xe7B\xce\x8dHI" +
",\xa6\xa2L\x90e9G\xb3<{E\xcc\xbeJ\x1a" +
".>y\x9bk\x9bI\x85\xf9\x81\xc1\xb9#TI\x13" +
"\x83\xaa(\x0bt\x13W\xbd\xdd\x93\xc1\xaet\xb8\x18x" +
"\x81*\x12\xddb\x8d\xf0\xcdCC>\x0fd \xba\xf9" +
"\x10\xcb\xa1\x1d$\x99\xc3\xb7[a3\x18.\xbb\x8c\xa5" +
"\xb8\xc5E\xe0\x0a\x14\x18R\xc7\x9c\x1dPD\xac0\x05" +
"\x84\xd0~\x9evkFF\xb8crSH\x9c\x8cO" +
"ke\xd8\xeeX\xb7;\xe6\xacw\xbd^>.\x1dH" +
"Fh\xb2\x95\xb9o\xcdN\x90Z\xe1\xa2\x94\xe6n\xa8" +
"\xa59\x07\xdb\x87-\x9b'\x86\x99\x93\xb1\x9c\x8c\xc1;" +
"\xa5\xba\x18\xbd\x8f{>Z~\xc0\x9d\x80\x04r\xd9\xd2" +
"\xccIs\xbb\xdb\x85N\x99\x9b\xc5\xf4\xd8\xe4\x83\xea:" +
"\xc5\x1f\x86\x0b\x91\x9c\x97\xe6bQ\xc7\x03\xee)\x0e\xb3" +
"\xcba\x9e\x90\x8eB\x8c%q<9a\x0e2{\xfb" +
"<+\xeezV\x90/\xe3\xaa4CN\xf3~\xbe'" +
"gy\xdc\xa7\xdd\x90\xc5\xa0\xc0\xe830\xb2X\x1aM" +
".G\x9f\xc7}_\xd9\xc0\xfc-nWQc\xc2x" +
"]\xe6\xae\x9c\xafR\xf8\xe5jVkUb'@%" +
"\xa8L\x85V\xddUsAy\x88z1\xc4\xe6Q\xee" +
"y\x96\xc9+\x8a\xb4j\x09\xd71\xad\xc0r'\x07\xa3" +
"\xb4\x03\x8daf\xbac\x09\xe6\x19\x01\x0bPDO@" +
"8\x96rMno[)\xa7R\xc46d\xfd\xa4\x9b" +
"f\x112H\xf0&\xdfu\xfa\x90\x05\xc3\xebm\x97\x15" +
"g-\xb0Z\x16\x0co\xe1\xe3\xc2\x05\x01\xb0`x\x9d" +
"[=d\xd1\x14Y\xa2\xf5\x88x\x0e\xb3\xcb\x87E\xcd" +
"4\x87\x85\x110\xaf\xcf\xb2\xdd\xa0\xe8,\xbfqC\xd6" +
"\x97\x87\xc6Mj\x08 \x84t\xe0\xe2^\x00\xe3NT" +
"\xd1\x18F\x05\xa3\x881$\x9c\xe3&\x00\xc3$<K" +
"\xb8\xa2\xc4\xc4A3\x82\xfd\x00\x86M\xf88\xe1\xaa\x1a" +
"C\x15@\xcb\x09\xfd\x80\xf0\x03\x84\x87\xeab\x18\x02\xd0" +
"\xee\xc2\x95\x00\xc68\xe1\x87\x08\xaf\xa9\x8fa\x0d\xf5\xef" +
"\xb8\x03\xc08@\xf8Q\xc2k\x1bbX\x0b\xa0\xdd\x83" +
"?\x040\x8e\x12~\x8c\xf0pc\x8c\x0eh\xed!\xbc" +
"\x17\xc08F\xf8\x09\xc2\xeb\x9abX\x07\xa0=&\xf4" +
"O\x10~\x8a\xf0\xfa\xe6\x18\xd6\x03h'\xd1\x030\x9e" +
"!\xfc4\xe1\x0d\xe1\x186\x00h\xcf\xe1\xe3\x00\xc6i" +
"\xc2/\xa0\x82-\x8dJ\x0c\x1b\x01\xb4s\xb8\x1a\xc08" +
"C\xf8ET\x10\x9bb\xd8\x04\xa0\xfd\x12\x97\x01\x18g" +
"\x09~\x13\x15,\xf8.m\x99\x94\x0b\x9d\x8eKi\x85" +
"\xf5\xa0`=`\xc1f\x81\x15\xe4L\x0et\xbc\x82\x82" +
"\x8d\x84\xb9N\x86@@^\xc6\x98=E/\xee\x8bm" +
"\xd4\x00\x0a6\x00\x16\x069\xf3,'\xd3\x0d*\xcf\x94" +
"\xc1a\xd7\xb3\xf6\xbaN\x80\xcc\xeeJ\xa7s\x1eS\xd3" +
"\xf9\xb2p\x94{\x81\x95f6JQ:O\x07\xe0D" +
"k]X\x14uW[\xf5e\xa5LC\\\xc8\xcax" +
"\xce\xb1\xc6\xc5\xec\xfc\x80\x8ddS\x96\x1d\xb6-\x1fk" +
"@\xc1\x1a\xc0\xcea\xe6\xaf\xb7\xc6\x11AA\x04\x8c\x8c" +
"\xf6\xf6t\xe3,\xc0>\x15\x85\x81Y\x80_\x98\x96\xd4" +
"\xc3l\xce\x057\x1al$ksJ\xcc\x9c/\x92r" +
">%X\xb4k\x1d5\"\xd1\x8e\x95\xd4\x87DW\xd0" +
"\xa9\xafF\xdb\x08\x0cE\x97R/P\x13m\xd9\x05\xb0" +
"?\xe7\xecv\xdc1'\xee\xb8\x1b\xb2~\xc1q\x13\xee" +
"(\xf7\x18\xa8\x19\xbe\xdfqS,H\x0f\x17F\xe8/" +
"7{\xa1\xd3\x95%\xa4\x08$AV\xe1/\xdd\x99\x85" +
"\xcd\x15+g\xd8\xcau|\x99V\x8e\xf61\x9d\x1a\x9b" +
"\xd5\\ \"P\xda\x96\xd1\x87\xef\x05\xd0\x1fQQ\x7f" +
"\xaa\xb2'\xa3On\x02\xd0O\xa8\xa8\x9f\xaal\xc8\xe8" +
"\xc9e\x00\xfaS*\xea\x17*\xbb1z\x8e4\xcf\xaa" +
"\xa8_R\x10Cb'F_<\x08\xa0_TQ\x7f" +
"\x95\xb6!\x8am\x18}e\x10@\x7fYE\xfdM\x05" +
"\xcb\x9d\x0c\xf6yn\x86\xea$\xb5\x90\x95f\x1c\x10g" +
"S\xd2\x94\x8e8\xea\xc7@\xc1&\xc0H\x96\x05\xc3\xa5" +
"\x1c\x98]aC\x80\"\x1b\xb2\xae/**\x08{e" +
"bS\xb4g\xbbn\xb6\x9f\x05\x1c\xbbh\x013\xbc\x92" +
"\xbb%\x09\x84S\x96SFKAU\xff^n\xa9\xb9" +
"@\xaf\xc3\xeak\x84\xfa]U\x0c\xa9\xbe\xbfPJ=" +
"\x88P\xf2\x156d}\x83\xf65 \xd7\x8f\x94+\xe3" +
"\xd3\xea:\xaa,*U\x16\xb5\xaa2\x9eT\xa9R<" +
"E\xf8\xb3jUe\xfc\x99J\x15\xf0\x14\xe1gT\x05" +
"\xb1X\x18\x7f\xa1R\xa1;M\xf0\x05R\x0f\xa1,\x8c" +
"\xe7T*\xbcg\x09\xbfDx\x8d\"\x0b\xe3\x8b\xea\xf3" +
"\x00\xc6%\xc2_'\xbcV\x95\x85\xf151\xec\xcb\x84" +
"\xbfIx8$\x0b\xe3o\x84\xfd\xd7\x09\xff=\xe1u" +
"\xaa,\x8c\xef\xa8T\x18\x7fO\xf8\x9f\x08\xaf\x0f\xc9\xc2" +
"\xf8\xa1J\x85\xf1\x03\xc2?#\xbc\xa1F\x16\xc6O\xd5" +
"\xef\x01\x18\x9f\x11\x1e\x0a)\x18m\xac\x95\x85\x11C\xbf" +
"\x020\xeaB*\x1a1\xc2\x9b\xde\x96\x951\x1a\"\x7f" +
"\x9a\x08\x9fOx\xf3\xe2\x186\x03hsCT\xf0g" +
"\x13\xbe\x88\xf0Y\xef\xc4p\x16\x80\xb6 D~\xce'" +
"\xfc:\xc2#u1\x8c\x00h-\xa1\x97\x00\x8c\xeb\x09" +
"\xbf\x89\xf0\xd9\xf51\x9cM4,D\xf1YN\xf8-" +
"\x84G\x1bb\x18\x05\xd0:B4\xaf[\x08\xbf\x8d\xf0" +
"9\x91\x18\xce\x01\xd0zB\x83\x00F7\xe1}\x84k" +
"\x8d1\xd4\x00\xb4T\xe8\xe7\x00F\x1f\xe1w\x12\x1ek" +
"\x8aa\x0c@\x1b\x08\xd1\x81r'\xe1\xc3\x84\xcfm\x8e" +
"\xe1\\:\xf8B\x14\x1f\x93\xf0,\xe1W-\x8a\xe1U" +
"t\xf0\x09\xfd,\xe1\xfb\x08\x9f\xf7n\x0c\xe7\x01hy" +
"\xe1\xcf>\xc2\x8f\x10>\x7fq\x0c\xe7\x03h\x87C\xbb" +
"\x00\x8cC\x84\xdfO\xf8\x82\xba\x18.\x00\xd0\xee\x13\xf1" +
"9B\xf8\x83\x84_]\x13\xc3\xab\x01\xb4\x07\x84\x9f\x0f" +
"\x12\xfe(\xe1\x0b\xaf\x89\xe1B\x00\xed\xb8\xf0\xe7\x11\xc2" +
"\x9f\"|Q<\x86\x8b\x00\xb4'E<O\x10~\x8a" +
"\xf0\xc5\xd7\xc5p1\xa5g\x88\x0e\xe8g\x08?M\xf8" +
"5\x0d1\xbc\x86\x0e>\x11\x9fg\x09?Kx\xbc1" +
"\x86q\x00\xed\x051\xaf\xb3\x84_\"\xfc\xda\xa6\x18^" +
"Ky(\xf0K\x84\xbfNxKs\x0c[(\x0fC" +
"\x07\x01\x8cW\x09\x7f;\xa4\xe0\xfe1\x96\xefe#\xbc" +
"T\x0a:\xc7X\xbe\x9f\x0f\x95~\x16<\x97\x99$\xaf" +
"\xaa\x16\xf2\xd8IZ#\xa0ZAy[;\xc5&\x18" +
":e\xcb5E\x80\x12\xef\xb6:\xfd\x80\xda\xdb\x92B" +
"\xe70\xdb\xcb<\xb3l\x9d\xf4oc{\x19\xa8\xd3\x80" +
"\xe8\x99\xdd\x16=\xafV\x0c\x14\x989j\xf9\xae\x97\x87" +
"\xb81\xe1$\xa6\x87\xba\xccQ\x0bI(]\x98\"S" +
"J\xb2\xa2\xdd4V\x1cs\x1d\xbe\x9d\xe5K\x07e\xdc" +
"f\x0e\xf7\xb1\x16\x14\xac\x05,\x04\x96\xcd\x93D\x1bU" +
"n\x96T\xca\x91Q\xac\xc0\xc8e2\xdc\x0f\xb8)\x8c" +
"W\x9d\xe8~Q\x00\x9d\xe6Dw\xb9\x1fX#TH" +
"\xcd~\x97\x99\xdb-S\x0d\x86\xcbBZ\x07\xeab!" +
"\xcc\xc7\x03\x8cT\xee\x0f\x011\x02X0\xadbT\xd7" +
"{\xee\xc8v\x96O\xc4\xb9CM\x7f\xb9\xc7(\x12O" +
",1\xcf*\x8fF\x88/y\xa3|r\xfc\xc6X\xde" +
"\xe06O#\x15\x7fyc\x81\x91\xca5Pq\xe4\xd2" +
"\x9c\xd1\x92\x9d{P\x1d\x90a+3<\xc6\xf2\x09\x88" +
"\xd8\xcc\xf71R\xb9\x94\x93O\xc7\xc7X~\xa3Yj" +
"M\x0a\xe9b\xf3>\x89\xabT2O\x14\xfem\x1c\xe2" +
"\x1eM\x07\xc3\xa0`\xb8\x18\x1d\xeaD@\xcd\xd1 \xe5" +
"\x93\xa3\xe8b\xa6r@`\xa4r\x94T\xa4)\xd7q" +
"\xb7@\xb8\xba!L\xbb#\xd9\\\xc0M\x142\x8bv" +
"@Y&\xbb\xc8\x0d\x1c\x1d\xee1y2\x96eY\xcf" +
"5si\xee\xa1\xc1}\xf2\xb1\"\xf9\xc2^\xbfLU" +
"$\xbd\x15\xd1\xa6\x96\xa2N4\x09\xd1\xd5\xa2\xa9\xaa_" +
"\x07@\xbb'\xb0\xd2\xfb\xb3\xdcKs'\x98IO\xb3" +
"\xaa\xba\xa7\x99\xee\xf8\xa5\xbcKt\xbaN\xc0\xc7E?" +
"\xd3$\x06_,;\xba\xb9\xcbDG\xd7\xbc\x0e`\xff" +
"\x90\xc7\xf9\x18\xcbG\xd2V\x90/ur3\xf1d\xc5" +
"\x8c\xba\xb1\xb0\xb9b\xa67q\xdf\x9c\xe9\x00\xabf\xfa" +
"\xc0\x8a+\x05S\xdc\xf5\xa1Cq\x8c\x95\xfb\xc2\xbb\xa8" +
"\xdb\x1bWQ?T\xd5\x17\xde\xbd\x12@\xdf\xa7\xa2~" +
"DA,\xb6\x85\x87\x97\x00\xe8\x07T\xd4\x8fR[8" +
"[\xb6\x85\xf7\xd0\xd3\x87T\xd4\xef\xaf4\"\xd1\xfb\xa8" +
"W<\xaa\xa2~L\xc1H\x90\xcfR\x8a\x97\xdf\xed\x14" +
"w\xd9\x10\xb1\xd5\xd2\xbe\x0e\xfb\x81W\xee\xfc\x06]\xd7" +
".\xef\xd6]E\xe2Z]\xecg\x12\x93\xe53\x0db" +
"\xfb\x95\x1e\xb8\xadX?\xa8z\x88\xfdp\xb3\x88\xcec" +
"2%\x1f\xde$R\xf2\xa1]\x82d<\xb0R\x90\x8c" +
"{\xfa\x05\xc98L*\xb5\xd1\xbb\x06\x010\x1c\xcd\x13" +
"X\x17\xcdy\xb4S\xa3{\xe8\xb9\x86\xe8\x08=\xd7\x18" +
"\xb5\xe8\xa3)\xcaI\xb39\xca\xaa\xc8Ia\xc4\x0d\\" +
"o\x8c\xe5\x01\xa0\xf2=\x92\xb4\x9c\xdd\xf1\xc0\xcb9\xbb" +
"\x0b\xe2o\xd2r\x00w\xef\xcfz\xd6\x08\xf3\xf2\x85\xe2" +
"g\x12\xc2\x96\xb3\xbb\xe0s*j\xcc\x03\xccW\xbe\xc7" +
"\xf3d\xa3\x10\x10\xf1c\x9e0_\xfe.\xcc\x17rN" +
"\x9a\xe6lAd\xc8\xe2f\xc1\xe3\xbeer*\xff\x16" +
"\xb3\x0b\xb65j9\x19\xea\x7f=\xce\x83+\xd2\x14Z" +
"\xd2>\xd7r\x02yy0\xbb\x9c\x8d\x8cR\xe7\xdb*" +
"\xeavU6Z\xfd\x00\xfa\xb0\x8az@\xcdqH\xa6" +
"\xe3\x1e\x02\xb3*\xea\xfb(\x1dkd:\xe6\xf7V\x92" +
"\xf9K\x13\xe8t\xce\x1beA\xce\x83\xcaA[\x08\xe4" +
"\x05&\x87N\xdb\xa5\x922\x85+|\x89\x1d\xdf\xfee" +
"\x08[_\x89\xc9\x88\xd2Z\x8eC+\xc5\xe1z\x15\xf5" +
"\x9b\xaa\xe2\xb0\x82\xa6\xbc\\E\xfd\x96\x99\xcc\xee\x0a\x0e" +
"\x10q\x16tE\\\xdd\x89\xd5\x10\xf1m]&\x12\xba" +
"E&\xf4\xe2M\"\xa1\x17,\x01\x888\xae\xc3\x0b\x16" +
"\x9d\xe5\x0e\xb3)Q\xf8x\xf9{8\xcd\xbc/\x1cr" +
"\xc2\x15R$`\x01\xd7CX\xf5>7\x8aK\xc2\x1b" +
"\xb2\xbe~\xa0\xcc\x9a\xf6\xa8\xd4\xf6f\x89F\xec\xabf" +
"MyAk\xf6\x11~\xa4\x9a5\x1dV\xa9-=D" +
"\xf8\xfd\x84\xab(i\xd3}\xea\xe3\x00\xc6\xfd\x84?\"" +
"h\x93*i\xd3\xc3\xc2\xfe1\xc2O\x08\xda\x14\x92\xb4" +
"\xe91\x95\xda\xd2G\x09\x7fF\xd0\xa6\x1aI\x9b\x9e\x16" +
"v\x9e!\xfc\xb4\xa0M\xb5\x926=\xa7\x9e\x9f@\xcb" +
"\xea\xc2\x926\x9d\x13\xac\xef\x0c\xe1\x17\x05m\xaa\x93\xb4" +
"\xe9\x97\x82f] \xfceA\x9b\xea%m\xba,X" +
"_\x85\xae56H\xda\xf4\x9a\xf0\xf3U\xc2\xdf&\xbc" +
"\xa9Q\xd2\xa6\xdf\x09\x9a\xf56\xe1\x1f\x10\xde\xdc$i" +
"\xd3\x1fTj\xdb\xdf'\xfc\x13\xc2g5K\xda\xf4\xb1" +
"\xa0q\x9f\xa8*\xf6\x0b\xd64K\xb2\xa6\xcf\xc5\xb0\x7f" +
"#\xf5:\xc1\x9a\"\x925\xd5\x88.?D\xdd\xf9l" +
"\xc1\x9afK\xd6\xd4,XD\x85\xad\xcd\x89J\xd64" +
"7\xf4\xfc\x04V\xa6\xa9\x925\xb5\x84\xc8\xcd\xeb\x08_" +
".X\xd3\x1c\xc9\x9a\xda\x84\x9d\x1b\x08\xbfY\xb0&M" +
"\xb2\xa6UB\xfff\xc2\xbb\x05k\x8aI\xd6\xd4%X" +
"M\x85\x95\xcd\x9b+YSJ\xd8I\x12~\xbb`M" +
"WI\xd6\xb4U\xb0\x8b-\x84\x7f[\xb0\xa6y\x925" +
"\xed\x14l\xb0\xc2\xd6\xae^+Y\x13\x17\xe3\x0e\x13\x1e" +
"\x08\xd6\xd4%Y\xd3\x1ea\xa7\xc2\xd6\x16\xad\x93\xac)" +
"/\xf0q\xc2\x0f\x09\xd6\x94\x90\xac\xe9n\x81\x1f \xfc" +
"\xa8`M\xdd\x925\xdd#\xfc,\xb35\x8cK\xd2\xf4" +
"@h\x09\x80q\x94\xe0c!\x05\xc5}\xb6\xdd\xe7q" +
"\x88\xf8~U\xf7\xca\xec1\x96\xf77;X|\x1b\xd4" +
"\xd3)^qT\x14\x06=\xb6\x9b'\xad\x0c\x84\x87\x03" +
"\xbf\x8c\x9a\xcc\x1f\x1et\x99\x87f\xd5\xcdv\xb9\xa6\x99" +
"|\xda\xb1\xca\x0d\xbc\xb8\xe1\xe6&L\x95$]'C" +
"\xd2\xf0t\xcf)\xdb\xb8\x97/iT?\xbf\x9f\xa7\xdd" +
"\x0d\x9cy\xe5'\x86\\/\xcd\x13.\x03\xd5\x0f\xca\xa0" +
"\xe5\xf71o7\xd1\x82\xca\xc0Y\x96\xf3y\x92\x05\x10" +
"\xa1\xc9O\x82]\x07E\xf5\xb3\x1c\x86v\x15\x01r\xbd" +
"`\x03g\x80\x95\xf1\x02\x8f\x0d\x0dY\xe9\x14\xba\xa6|" +
"GTM\x102\xcc7\x02\x96\xde]=\xee\x88kr" +
"\x9a\x05L\x98\xa8,l}\x1e\xc4\xf9\x17.RW\xa7" +
"m\xbbc\xd5\x11\xaa,G\xe0f\x0d+\x83N\x99\xc5" +
"\xc9\x17-S\x8dJ<\xe9b)\x9e8Y\xb6\x8dc" +
")\xde\x93\x9e\xcdr\x93\xd9)6\x0e\xf1~\x9e\xe1\xce" +
"D\\\xc4\xad+\x1dX\xa3\xbc2\xe1\x80\xfb6K$" +
"z\xa0\xd3\xc9\xb0L\x95\xa9\xa2\xa0\x1b-\x9f\x0b\x11N" +
"\x91\xf5\xa2\x1bty#\xd5\x09#\xb2\xf96\x9ap\xc2" +
"\xcbY~\xd5@\"\xf7\xa6\x95ds\xb6\xcf\xbb\x1c\x13" +
"\xe2\x1bl\xcb\xe4%<\x9c\xc9\xfa8\xbbr`\x14\xef" +
"\xfcfp.\xdf<\xc3\xce\xfd\xff]I\x7f{\x91\x95" +
"\x0aR\x1a\xceg\xc5)*\xbb\x94U\xb2-l\xeb\x17" +
"\xa7h\xab<E\x97n\x12ma\xcb2\x80\xfd\xe9\x9c" +
"\xe7\x11i\xcaz\xdc\xb4\xd2\x01\x074\x0bY\xd7\xf7\xad" +
"A\x9b\x97\xceV\xc7\x14\xbb 2\xc4,\xfb\x8a\x17\xe1" +
"\xdd\xc5+\xd7\xf2\x8d\xeb\xa4k\xe0\xd5\x00\xfa\x83*\xea" +
"\x8fV5\x16\xc7\xfb\xab\xee\x86\x15\x94\xae?\xb9\xa3\xea" +
"nXUd\x83u\xf2 \x80\xfe\x8c\x8a\xfa\xe9\xca5" +
"\xf0s\xf4\xf4\xb3*\xea/W]\x03_&\xc5K*" +
"\xea\xef+\xd8\xc9DvMJW\x9b\xe6Z\xc9\x1d7" +
"`\xf6z\xcb\x06\x95\xfbX\x07\x0a\xd6A\xd5\xf517" +
"\xd7[6\xf7\xa1,\xb1\x8b\xef\xad\x00\xfd\xd2-qS" +
"\xf1MAI\x84\xdd<`\x96\xedC\xe5\x1a\xb9\xfc\x1f" +
"6\x8a\xd7\xc83!\x06\x13\xb2\xe0J\xc1/\xbdV\xeb" +
"\xee\x94>\x08\xeaZ^\x83\x1ej\xee\xbaU\xd4\xfb\x14" +
",-A\x8a\xa2\x9dTQ\xbf\xbdj\x09\xb6R\x10\xb7" +
"\xa8\xa8\x7f[\xa9\x9ar\x15\x15\x9aY\xd4\xfe/\x00\x00" +
"\xff\xff\x00\xd1\x99X"
const schema_b526ba661d550a59 = "x\xda\x94Z}lT\xd7\x95?\xe7\xbd\xb1\xc7\x9f\x8c" +
"\x877\x04\x0c&\x13(\xd0\x98\xc6i\x8c\xf16\xa6I" +
"]c\x03\xc1\x1a\x83\xc7\xc3G@A\xea\xf5\xbc\xeb\x99" +
"\x07\xcf\xefM\xde{c{\x10\x11\x84\x82\x04YP\x93" +
"n\xd2\x92*lC\x02R\xda\x0d-\xed\x86\x15\x1bQ" +
"-AD\"Y\x90\x92\xaa\xa9\x9a4\xbbI\xa3JI" +
"\xba\x89\xd2v\x13m\xb6\xdb\xe8\xad\xce\xbd\xf3\xf1\xfc\xd1" +
"\x10\xff\xc3\xf8\xfd\xcey\xe7\x9e{\xee\xb9\xe7\x9e\xdf}" +
"\xdc\xf1\xf5\xfao\x86\xda\x1b3\x0d\xa0$\x1f\xae\xaa\xf6" +
"\xa3{v\xbccy?9\x00\xd1E\xe8\xef\xa8\xdb\xba" +
"x\xe4\xf9\x15\xe7!\x14\x06\xe8h\xae\xde\x89Z[u" +
"\x18T\xff\xa7\x1f'\xbav\xfe\xf1\xf2\x83\x93\xb5\xaa\x84" +
"Zm\xf5^\xd4\x16W\x87\x01\xb4\xe6\xeaw\x01\xfd{" +
"{/\x7fgx\xc5\x0f\x0fCt\x91RQ\x06\xec\xc0" +
"p?j\xf3\xc2\xf4R4l\x87\x00\xfd\xb6\x97\xf2\xf7" +
"\xfe\xb8\xf9\xf8#\x90l\xc3Z\xff\xb2\xf5o\xf5\xdb\xae" +
"\xdc\xf7\xdfP\x85\xa4s\xbc\xfe!\xd4\xce\xd4\xd3\x9f\xa7" +
"\xea\xdfR\x00\xfd\x7f\xfc\xcaGyc\xef\xce\xefA\xf4" +
"K\x8a\x7f\xf0\x93O\xbe\xd4\xf1_\x7f:M\x96_\x9f" +
"s\x10\xb5\x0f\xe7\x90\x0f\xef\xcf\xf9>\xa0?\xf79\xcc" +
"d\xae^\x7fb\x86iuE\x86Q\x1b\x88\xd0\xb4\xda" +
"~\x1bW\xfb\xc2\xd9S3h\xb5Fv\xa2v\xb7\xd0" +
"2\xd5\x8eo~:\\\xfd4$\x17\xe1\xb4\xd97\x93" +
"^[D\xbe\xb2\x1d\x01+~%\x17\xe1\x9c\x80:E" +
"\xa8\xe3d\xd3B\xd4\xce6}\x19\xa0\xe3\xa5\xa6\xf10" +
"\xa0\xdf\xd7{\xe9\xe5\xb3o=}fZ\xb4\x92\xcd\x07" +
"Q\xe3\xcd4'\xd6\xfc5@\x7f\xeb\xe9\xe4\xeb\xb7\x8d" +
"]?3\x83\xb7\xbcy'j\x85f\xf2\xf6'\xbfM" +
"?\xf6\xf7\xcf|\xf4O\xd3\xecmm^S\xb1\xb7\x19" +
"\xd0\xdfu\xfe\xdd\xf6\xef|\xf6\xd6Of\xb0\x97'{" +
"G\x85\xbd\xee\xb7\xfe\xf0\xc0\xc0-\xdb\xcf\xcd8\xeap" +
"i\xd4\xab\x98Xb\xa4\xad\x0b3hm%[\x86\xd0" +
"\xca^\xfc\x87GS_\xef\xfb\xc5\x0cZ\xeb\xc8\xd6\x0e" +
"\xa1uH]\xfd6\xbfk\xd9\xe5\x19\xb4:Ik\xa3" +
"\xd0\xfa\xe8\x9f\xbf\xf1\xea\x9dw\xef\xbe:uU\x14\xd2" +
"[\xde<\x17\xb5N\x9akG{s\x9cVe\xcd\x8e" +
"\xed9\xf3\xb5\x1f\xfe\xfbLC/\xa4\xa1\x17\x92\xd1W" +
"\x0e>\x99\xf9\xbf7\xbew}\xa6\xa1Ik\xa3\xd0Z" +
"|%\xfd\xe6\x9b\xde\xc1W\xa7\x85x\xf9\xc2\xb5\xa8u" +
".\x14\xc3.\x14\xc9\xd0\xd5u\xf1\xd4\xb5\x87\xfe\xe7\xd7" +
"\xe4\xa5:\xc5\xe2\x92E\xfd\xa8u.\xa2\x05i_D" +
"\x1b\xa73\xd1\xf7\xed\x13\xdb\xbf\xf7\xc6\x0c\xa3\xcfk\xd9" +
"\x89Zk\x0b\x8d\xde\xf5\xed\x97\x7f\xf0\xfd\xaa\xef\xff'" +
"\x99T\xa6\xa8U\xb5\xacEm^\x0b\x99\x8c\xb6\x8c\x03" +
"\xfa\xb7\xbe\xfa\xeb\x8fn[\xf7\xdc\x9f\xa7\xed\x98|\xcb" +
"n\xd4\x8e\x0a\xcd\xc3-\x19\xc0\xca\xe6\x9blVn\xc5" +
"\x9f\x91\xf6\x8b-\xf3\x01:\xae\xb7\x84)m\x9bO\x9e" +
"\xdcX\xfb\xc1M\x1f\xcf\xe0\xebk\xcbv\xa2\xf6\xfe2" +
"\xf2\xf5\xe9P\xee\xb3\xaf\x1f:\xfe\xbf3h\xbdHZ" +
"\xaf\x0b\xad\x95\xef-\xdf\xf4\xee}\xdf\xf8\xcb\xb4x\x9e" +
"_6\x8c\xdaK\xcb\xc8\xc9\x17\x97\xed\x07\xf4\x9fr\x07" +
"\xaf\xbe\xbc\xeb\xe9\xbfLuR,z\xd5\xf2\x83\xa85" +
"/\x17\xe1Z.\xa2\x1f\xfd\x97\xf1#\x1f\xf6\x0c\xffu" +
"\x86\xe1\x1f[1\x8c\xda\x8fV\xd0\xf0\xfbO\x9c{7" +
"u\xe2\x88?5\x93\xc4\xcc\x0f\xafx\x1e\xb5\x93+\xc8" +
"\x85\xc7W\xfc\x14\xda\xfc4w83\xbf\x9a\x0e\xe5]" +
"\xcf\x1e\xfdjZ\xfc\xdc\x9ef9+\xb7\xa6W<\x0c" +
"q\x97;c\\\xd5W\x0f\"~\xae~\xcac\xce\xa0" +
"a\xda^/sz\xbbm\xcbsls\x101\xd9\xa4" +
"\x86\x00B\x08\x10e;\x01\x92\xdfR1i*\x18E" +
"\x8c!\x81\xc6w\x01\x92\xa6\x8a\xc9\x09\x05\xa3\x8a\x12C" +
"\x05 \x9a'MO\xc5\xe4\x01\x05\xa3\xaa\x1aC\x15 " +
"\xfa@?@r\x9f\x8a\xc9#\x0a\xfa\xd9\xbc\xdeKc" +
"\x80j\x9b8\x17\x14\x9c\x0b\xe8\xbb\x1e\xe7\x8eae0" +
"a\x8c\x1a\xdeFk\x04\xed\xb2\xccbcF\x86y\x06" +
"\xa8\xb6U\x06sY\xdb\xe2\x1br.\x00`S\xa5\x80" +
"\x03b\x13|\xfel\x07XN\xdfh\xe5\xf2\xde\x96B" +
"\x8e\x03\xd0D/\x92\xebZ\x9b\xd2\x0f\x80s\xb4V\xe5" +
"\xe7\x00\x18\xd1Z\x95\xa7\xc8\xb8\xd6\xaa\xfc\x18\x00\xa3Z" +
"\xabr\x19\x00\xe7j\xad\xca\x1b\x00X\xaf\xb5)\xc3\x00" +
"\xa8i\xad\xca\xcb\x00\x18\xd3\xda\xc4/j\xed\xca^\x00" +
"T\xb46e7\x00\xce\xd3Z\xc5\xf3M\xdar\xe5\x0f" +
"\x008_kU~\x09\x80\x0b\xb46\xe5\x1d\x00l\xd6" +
"\xda\xc5\xf3B\xadS\xf9\x01\x00.\xd2:\xc5\xb8-Z" +
"\xa7\xb0\xb7X\xeb\x12\xcf7k]\xc2/\xb5\xf8\x1c\xd2" +
"\xba\x84?q\xadK\xd8\xbdE\xbb[\xd8[\xd2\xd1\xa3" +
"\xacA\x00\xac\xd26*\xcf\x03`\xb5\xb6Q8\xd0\xa8" +
"\xadSv\x02`\x83\xd6#\x1c[\xaa\xdd-^\xfc\x92" +
"\xd6#\x0c/+\xfe.\xef\xe8Q\xe6\x92\x81:m\xa3" +
"\xf2\x10\x00\xae\xd06*\x7f\x06\xc0/w\x0c(KI" +
"p\xab\xb6U\x84\xa0\xb5c\x87\xa2\x10\xb0\xb2c\x97\xfc" +
"\xe3+\x1aS\xbe\x0b\x80\xb7iL\xbc\xda\xa61\x11\xbc" +
"p\xf1\xb9Fc\xcaA\x00\xac\xd5v\x89\xdf\xdb\xb5\x1d" +
"\xe4\x83\xaf\xdb\xe3\x96i3\x1d\x00|\x97{[\x98\x93" +
"\xe1\xe8%\x98\xc7\x1df\xc6{\xd2in\x12\x9e\xcaq" +
"\xae\xcb\x14\xd9<2\x12v\xb97\x05\xed\xb5\xad\x08e" +
"/\xc1\x03,\xd7\x9b\xafr\xc6\xb8\x90\xf7\xca\xb4\x06\x97" +
"{\xdb\x0c\xd7\xb0\xad\xde\xfc$\x91*_J\xd8\x99\x04" +
"\x87\xf0\x98\x1cOh*RU\xf8D.\xf5H\x1f\x83" +
"\xb2\x01\xc3\x92\xe2m\x00\xbe\xc3i&)\x0e\xdd\x9eg" +
"X\x19\xd7w\xd9\x18Oq\xcf\x83\x88|\xe4\xde:\x8b" +
"\x0d\x9b\xd0-\x87\x9fjl\xab\xcb\x85\x9c\xa7\"%\xb1" +
"\x98\x8a2I\x96\xe3\x1c\xf5\xf2\xec\x151\xfb\x804\\" +
"|\xf3\x1e\xdb\xd4\x13\x0as\xbd\x14\xe7\x96P%M\xf4" +
"\x02Q\x16h?W\x9d=S\xc1\x9et\xb8\x18x\x81" +
"*\x12\xddb\x8c\xf2\xcd##.\xf7d \xfa\xf8\x08" +
"\xcb\xa3\xe9%\x98\xc5\xb7\x1ba\xdd\xcb\x96]\xc6R\xdc" +
"\xe2\"p>\x05\x86\xd41oz\x14\x11#L\x01!" +
"t\x88\xa7\xed\xaa\xd1Qn\xe9\\\x17\x12+\xe3\xd2Z" +
"\xa5L{\xbc\xcf\x1e\xb7\xd6\xdb\xce&>!\x1dHD" +
"h\xb2\x95\xb9o\xcdM\x92\x1a\xe1\xa2\x94\xe6\x9eRK" +
"s\xf6\xb6g\x0d\x93\xf7f\x99\x951\xacL\x8awK" +
"u1\xfa w\\4\\\x8f[\x1e\x09\xe4\xb2\xa5\x99" +
"\x95\xe6f\x9f\x0d\xdd27\x8b\xe9\xd1\xefR\x1d*>" +
"\xa4l\x88\xe4\x9d4\x17\x8b:\xe1qG\xb1\x98Y\x0e" +
"\xf3\xa4t\x14b,\x89\xe3\x89Is\x90\xd9;\xe8\x18" +
"q\xdb1\xbcB\x19W\xa5\x19r\x9a\x0f\xf1\xfb\xf3\x86" +
"\xc3]\xda\x0d9\xf4|F\xbf^*\x87\xa5\xd1\xe4r" +
"\x0c:\xdcu\x95\x0d\xcc\xddb\xf7\x145&\x8d\xd7\xa3" +
"\xef\xce\xbb*\x85_\xaefP\xab\x12;\x01*^e" +
"*\xb4\xea\xb6\x9a\xf7\xcaC\xd4\x8a!6\x8fq\xc71" +
"t^Q\xa4U\xeb\xb5-\xdd\xf0\x0c{j0J;" +
"0\x95e\xba=\xde\xcb\x9c\x94\xc7<\x14\xd1\x13\x10\x8e" +
"\x0f\xd8:7\xb7\xad\x92S)b\x1brn\xc2N\xb3" +
"\x08\x19$\xb8\xdf\xb5\xadAd^v\xbdi\xb3\xe2\xac" +
"\x05V\xcd\xbc\xec\x16>!\\\x10\x00\xf3\xb2k\xed\xe0" +
"\x90ESd\x89\xd6#\xe2X\xcc,\x1f\x16U78" +
"\x1a\xc9Y~\xbb8s\xe8\xd0X]:\x1d\xb5]\xb8" +
"\x17 u\x1f\xaa\x98\xcab\xe5\x80\xd48\xf6\x03\xa4t" +
"\xc2sX9#\xb5Q\x1c\x02H\x99\x84O`\xe5\x98" +
"\xd4\xf2B\xdf#\xfc\x00\xe1\xa1\x9a\x18\x86\x00\xb4\x07p" +
"\x15@j\x82\xf0C\x84W\xd5\xc6\xb0\x0a@{\x10w" +
"\x02\xa4\x0e\x10~\x8c\xf0\xea\xba\x18V\x03hG\xf1\x07" +
"\x00\xa9c\x84\x9f <\\\x1f\xa3&B{\x0c\x1f\x02" +
"H\x9d \xfc4\xe15\x0d1\xac\x01\xd0N\x09\xfd\xd3" +
"\x84\x9f#\xbc\xb61\x86\xb5\x00\xdaYt\x00R\xcf\x12" +
"~\x81\xf0\xbap\x0c\xeb\x00\xb4\xf3\xf8\x14@\xea\x02\xe1" +
"WP\xc1%\xf5J\x0c\xeb\x01\xb4\x17p\x0d@\xea\"" +
"\xe1WQAl\x88a\x03\xf5L\xb8\x12 u\x89\xe0" +
"7QA\xdf\xb5i\xcb\x0c\xd8\xd0m\xd9\x94VX\x0b" +
"\x0a\xd6\x02\xfa&\xf3\x0c/\xafs:\xd3\xebA\xc1z" +
"\xc2l+C /c\xcc\x9c\xa6\x17w\xc56\xaa" +
"\x03\x05\xeb\x00\xfda\xce\xa8\x9b\xe8\x03\x95g\xca`\xd6" +
"v\x8c\xbd\xb6\xe5!3{\xd2\xe9\xbc\xc3\xd4t\xa1," +
"\x1c\xe3\x8eg\xa4\x99\x89R\x94.\xd0\x018\xd9Z\x0f" +
"\x16E}A\xab\xae\xac\x94i\x88\x0bY\x19\xcf[\xc6" +
"\x84\x98\x9d\xeb\xb1\xd1\xdc\x80a\x86M\xc3\xc5*P\xb0" +
"\x0a\xb0;\xcb\xdc\xf5\xc6\x04\"(\x88\x80\x91\xb1M\xeb" +
"\xfap\x0e\xe0\xa0\x8a\xc2\xc0\x9c@\x0fS\xf57z\x98" +
"\xcdy\xef\xf6\x14\x1b\xcd\x99\x9c\x123\xef\x8a\xa4\\ " +
"\x9a\xb0\x9e\xb5\xd4\x88D\xbbVQ\x1f\x12m\xa7S_" +
"\x8d\xb6\x11\x18\x8a.\xa7^\xa0*\xbad7\xc0\xfe\xbc" +
"\xb5\xc7\xb2\xc7\xad\xb8eo\xc8\xb9\xbee\xf7\xdac\xdc" +
"a\xa0f\xf8~\xcb\x1e`^:\xeb\x8f\xd2\xbf\\\xdf" +
"\x04\xdd\xb6,!E \x01\xb2\x0a\x7f\xe1>4\xac\xb7" +
"\xaf\xbaQ#:\xa5q\xed\xba\x91>\x85\x81\xf61\x9d" +
"\x1a\x9b\xd5\xbc'\"PnZ\x1f\x7f\x08 \xf9\x84\x8a" +
"\xc9g\x02M\xeb\x19jEO\xab\x98<\x17hZ\xcf" +
"\xae\x04H>\xa3b\xf2J\xa0i}\x814/\xa9\x98" +
"\xbc\xa6 \x86\xc4N\x8c\xbet\x10 yU\xc5\xe4\xaf" +
"h\x1b\xa2\xd8\x86\xd1W\x87\x01\x92\xaf\xa8\x98|S\xc1" +
"r'\x83\x83\x8e\x9d\xa1:)\xfa\xd32a(\xf6\xa7" +
"n\xe9\x88\xa3~\x0c\x14l\x00\x8c\xe4\x98\x97-\xe5@" +
"S\x85\xb1\x01\x8al\xc8\xd9\xae\xa8\xa8\xb2\xdf-\x93\xaf" +
"\xa2=\xd3\xb6sC\xcc\xe3\xd8C\x0b\x98\xe1\x95\xdc-" +
"I <`Xe\xb4\x14T\xf5o\xe5\x96\x9a\xf7\x92" +
"5\x18\xbc\xea\xa8\xdd\x1d`q\xb5C~)\xf5 B" +
"\xc9\xe7o\xc8\xb9)\xda\xd7\x80<y\xa4\\\x19\x7f\xa4" +
"\xae\xa5\xca\xa2ReQ\x03\x95\xf1\xacJ\x95\xe2\x19\xc2" +
"\x9fS\x03\x95\xf1g*U\xc0s\x84_T\x15\xc4b" +
"a\xfcW\x95\x0a\xdd\x05\x82\xaf\x90z\x08ea|A" +
"\xa5\xc2{\x89\xf0k\x84W)\xb20\xbe\xa4>\x0f\x90" +
"\xbaF\xf8o\x08\xafVea|M\x0c\xfb\x0a\xe1o" +
"\x12\x1e\x0e\xc9\xc2\xf8\xba\xb0\xff\x1b\xc2\x7fOx\x8d*" +
"\x0b\xe3\xefT*\x8c\xbf'\xfc\x8f\x84\xd7\x86da\xfc" +
"P\xa5\xc2\xf8\x01\xe1\x9f\x12^W%\x0b\xe3'\xeaw" +
"\x01R\x9f\x12\x1e\x0a)\x18\xad\xaf\x96\x85\x11C\xbf\x04" +
"H\xd5\x84TL\xc5\x08ox[V\xc6h\x88\xfci" +
" |\x01\xe1\x8d\x8bc\xd8\x08\xa0\xcd\x0bQ\xc1o\"" +
"\xbc\x85\xf09\xbf\x8b\xe1\x1c\x00\xad9D~. |" +
"\x19\xe1\x91\x9a\x18F\x00\xb4%\xa1\x97\x01R\xb7\x12\xbe" +
"\x9a\xf0\xa6\xda\x186\x11\x9f\x0fQ|\xee \xfc.\xc2" +
"\xa3u1\x8c\x02h]!\x9a\xd7]\x84\xdfC\xf8\xdc" +
"H\x0c\xe7\x02h\xebB\xc3\x00\xa9>\xc2\x07\x09\xd7\xea" +
"c\xa8\x01h\x03\xa1\x9f\x03\xa4\x06\x09\xbf\x8f\xf0XC" +
"\x0cc\x00\xda\x8e\x10\x1d(\xf7\x11\x9e%|^c\x0c" +
"\xe7\xd1\xc1\x17\xa2\xf8\xe8\x84\xe7\x08\xbf\xa9%\x867\xd1" +
"\xc1'\xf4s\x84\xef#|\xfe;1\x9c\x0f\xa0\x15\x84" +
"?\xfb\x08?B\xf8\x82\xc51\\\x00\xa0\x1d\x0e\xed\x06" +
"H\x1d\"\xfca\xc2\x9bkb\xd8\x0c\xa0\x1d\x17\xf19" +
"B\xf8\xa3\x84/\xac\x8a\xe1B\x00\xed\x11\xe1\xe7\xa3\x84" +
"?I\xf8\xa2\x9bc\xb8\x08@;)\xfcy\x82\xf0g" +
"\x08o\x89\xc7\xb0\x05@;#\xe2y\x9a\xf0s\x84/" +
"^\x16\xc3\xc5\x94\x9e!:\xa0\x9f%\xfc\x02\xe17\xd7" +
"\xc5\xf0f:\xf8D|\x9e#\xfc\x12\xe1\xf1\xfa\x18\xc6" +
"\x01\xb4_\x88y]\"\xfc\x1a\xe1\xb74\xc4\xf0\x16\xca" +
"C\x81_#\xfc7\x84/i\x8c\xe1\x12\xca\xc3\xd0A" +
"\x80\xd4\xaf\x08\x7f;\xa4\xe0\xfeqV\xd8\xc4Fy\xa9" +
"\x14t\x8f\xb3\xc2\x10\x1f)=\xfa\x8e\xcdt\x92\x07\xaa" +
"\x85<v\x12\xc6(\xa8\x86W\xde\xd6V\xb1\x09\x86n" +
"\xd9rM\x13\xa0\xc4\xfb\x8cn\xd7\xa3\xf6\xb6\xa4\xd0\x9d" +
"e{\x99\xa3\x97\xad\x93\xfe=l/\x03u\x06\x10\x1d" +
"\xbd\xcf\xa0\xf7\xd5\x8a\x01\x9f\xe9c\x86k;\x05\x88\xa7" +
"&\x9d\xc4\xf4R\x8f>f \x09\xa5\x0b\xd3dJI" +
"V\xb4\x9b\xc6\x8ac\xb6\xc5\xb7\xb3B\xe9\xa0\x8c\x9b\xcc" +
"\xe2.V\x83\x82\xd5\x80\xbeg\x98<A\xb4Q\xe5z" +
"I\xa5\x1c\x19\xc5\xf0R\xf9L\x86\xbb\x1e\xd7\x85\xf1\xc0" +
"\x89\xee\x16\x05\xd0\xadOv\x97\xbb\x9e1J\x85T\x1f" +
"\xb2\x99\xbe\xdd\xd0U/[\x16\xd2:P\x17\x0ba>" +
"\xe1a\xa4r\xc7\x09\x88\x11@_7\x8aQ]\xef\xd8" +
"\xa3\xdbY\xa17\xce-j\xfa\xcb=F\x91xb\x89" +
"y\x06<\x1a%\xbe\xe4\x8c\xf1\xa9\xf1\x1bg\x85\x147" +
"y\x1a\xa9\xf8\xcb\x1b\x0b\x8cT\xae\xaa\x8a#\x97\xe6\x8c" +
"\x86\xec\xdc\xbd`@\xb2F&;\xce\x0a\xbd\x101\x99" +
"\xebb\xa4rq(\xdf\x8e\x8f\xb3\xc2F\xbd\xd4\x9a\xf8" +
"\xe9b\xf3>\x85\xabT2O\x14\xfem\x1c\xe2\x0eM" +
"\x07\xc3\xa0`\xb8\x18\x1d\xeaD@\xcd\xd3 \xe5\x93\xa3" +
"\xe8b\xa6r@`\xa4r\x94T\xa4\x03\xb6eo\x81" +
"p\xb0!L\xdb\xa3\xb9\xbc\xc7u\x142\x83v@Y" +
"&\xbb\xc8\x0d\x1c-\xee0y2\x96e9\xc7\xd6\xf3" +
"i\xee`\x8a\xbb\xe4cE\xf2\xb9\xbd~\x99\xaaHz" +
"+\xa2M-E\x8dh\x12\xa2kDSU\xbb\x16\x80" +
"v\x8fg\xa4\xf7\xe7\xb8\x93\xe6\x967\x9b\x9e\xa63\xd8" +
"\xd3\xcct\xfcR\xde\x89K8>!\xfa\x99\x061\xf8" +
"b\xd9\xd1\xcd[):\xba\xc6\xb5\x00\xfbG\x1c\xce\xc7" +
"Y!\x926\xbcB\xa9\x93\x9b\x8d'\xed\xb3\xea\xc6\xc2" +
"z\xfb\xeaY\xb6o_\x9b\xed\x00\x9d\xb3}\xa1\xfdF" +
"\xc1\x14w}hQ\x1cc\xe5\xbe\xf0\x01\xea\xf6&T" +
"L\x1e\x0a\xf4\x85\x0f\xae\xaa\\Qb\xb1-<\xbc\x14" +
" y@\xc5\xe41j\x0b\x9bd[x\x94\xde>\xa4" +
"b\xf2\xe1J#\x12=N\xbd\xe21\x15\x93'\x14\x8c" +
"x\x85\x1c\xa5x\xf9\xfbSq\x97\x8d\x10[-\xed\xeb" +
"\xb0\xeb9\xe5\xceo\xd8\xb6\xcd\xf2n\xdd]$\xae\xc1" +
"b?\x9b\x98\xdc1\xdb v\xdc\xe8\x85{\x8a\xf5\x83" +
"\xaa\x87\xd8\x0fw\x8a\xe8\x9c\x92)\xf9x\xbfH\xc9\xc7" +
"v\x0b\x92\xf1\xc8*A2\x8e\x0e\x09\x92q\x98T\xaa" +
"\xa3\x0f\x0c\x03`8Z \xb0&\x9awh\xa7F\xef" +
"\xa7\xf7\xea\xa2\xa3\xf4^}\xd4\xa0\x9f\x86('\xcd\xc6" +
"(\x0b\x90\x13\x7f\xd4\xf6lg\x9c\x15\x00\xa0\xf2w$" +
"aX{\xe2\x9e\x93\xb7\xf6\xf8\xe2\xdf\x84a\x01\xee\xd9" +
"\x9fs\x8cQ\xe6\x14\xfc\xe2o\x02\xc2\x86\xb5\xc7w9" +
"\x155\xe6\x00\x16*\x7f\xc7\x0bd\xc3\xf7\x88\xf81G" +
"\x98/\xff-\xcc\xfby+Ms6 2bp\xdd" +
"w\xb8k\xe8\x9c\xca\xbf\xc1L\xdf4\xc6\x0c+C\xfd" +
"\xaf\xc3\xb9wC\x9aBK:h\x1b\x96'/\x0f\x02" +
"W\xeb\xfd3]\xad\x0f\x01$\xb3*&=j\x8eC" +
"2\x1d\xef'0\xa7br\x1f\xa5c\x95L\xc7\xc2\xde" +
"J2\x7fa\x02\x9d\xce;c\xcc\xcb;P9h}" +
"O^`r\xe86m*)\xd3\xb8\xc2\x17\xd8\xf1\x1d" +
"_\x84\xb0\x0d\x96\x98\x8c(\xad\xe58\xb4R\x1cnU" +
"1\xb9:\x10\x87v\x9a\xf2\x1d*&\xef\x9a\xcd\xecn" +
"\xe0\x00\x11gAW\xc4\xd5\x9dX\x0d\x11\xdf\xd6\x95\"" +
"\xa1\x97\xc8\x84^\xdc/\x12\xbay)@\xc4\xb2-\xee" +
"\x1bt\x96[\xcc\xa4D\xe1\x13\xe5\xbf\xc3i\xe6|\xe1" +
"\xaf+\xa9\x88\xc7<\x9e\x0ca\xe0\x9bs\x14\x97\x867" +
"\xe4\xdc\xe4\x812k\xba_\xa5\xb67G4b_\x90" +
"5\x15\x04\xad\xd9G\xf8\x91 k:\xacR[z\x88" +
"\xf0\x87\x09WQ\xd2\xa6\xe3\xeaS\x00\xa9\x87\x09\x7fB" +
"\xd0&U\xd2\xa6\xc7\x85\xfd\x13\x84\x9f\x16\xb4)$i" +
"\xd3)\x95\xda\xd2'\x09\x7fV\xd0\xa6*I\x9b~$" +
"\xec<K\xf8\x05A\x9b\xaa%m:\xaf^\x9eD\xcb" +
"j\xc2\x926\xbd X\xdfE\xc2\xaf\x0a\xdaT#i" +
"\xd3\x8b\x82f]!\xfc\x15A\x9bj%m\xba.X" +
"_\x85\xae\xd5\xd7I\xda\xf4\x9a\xf0\xf3W\x84\xbfMx" +
"C\xbd\xa4M\xff!h\xd6\xdb\x84\x7f@xc\x83\xa4" +
"M\xef\xab\xd4\xb6\xbfG\xf8\xc7\x84\xcfi\x94\xb4\xe9O" +
"\x82\xc6}\xac\xaa8$X\xd3\x1c\xc9\x9a>\x13\xc3\xfe" +
"\x95\xd4k\x04k\x8aH\xd6T%\xba\xfc\x10u\xe7M" +
"\x8255I\xd6\xd4(XD\x85\xad\xcd\x8dJ\xd64" +
"/\xf4\xfc$V\xa6\xa9\x925-\x09\x91\x9b\xcb\x08\xbf" +
"C\xb0\xa6\xb9\x925\xb5\x09;\xb7\x11~\xa7`M\x9a" +
"dM\x9dB\xffN\xc2\xfb\x04k\x8aI\xd6\xd4#X" +
"M\x85\x95\xcd\x9f'Y\xd3\x80\xb0\x93 \xfc^\xc1\x9a" +
"n\x92\xaci\xab`\x17[\x08\xff\x96`M\xf3%k" +
"\xda%\xd8`\x85\xad-\xfc\xa6dM\\\x8c\x9b%\xdc" +
"\x13\xac\xa9G\xb2\xa6\xfb\x85\x9d\x0a[kY+YS" +
"A\xe0\x13\x84\x1f\x12\xac\xa9W\xb2\xa6\x07\x05~\x80\xf0" +
"c\x825\xf5I\xd6tT\xf8Yfk\x18\x97\xa4\xe9" +
"\x91\xd0R\x80\xd41\x82O\x84\x14\x14\xf7\xd9\xe6\xa0\xc3" +
"!\xe2\xba\x81\xee\x95\x99\xe3\xac\xe0n\xb6\xb0\xf85h" +
"]\xb7\xf8\xc4QQ\x18v\xd8\x1e\x9e02\x10\xcez" +
"n\x19\xd5\x99\x9b\x1d\xb6\x99\x83z\xe0f\xbb\\\xd3t" +
">\xe3X\xe5\x06^\xdcps\x1d\xa6K\x12\xb6\x95!" +
"ix\xa6\xf7\x94m\xdc)\x944\x82\xef\xef\xe7i{" +
"\x03gN\xf9\x8d\x11\xdbI\xf3^\x9b\x81\xeaze\xd0" +
"p\x07\x99\xb3\x87hAe\xe0\x1c\xcb\xbb<\xc1<\x88" +
"\xd0\xe4\xa7\xc0\xb6\x85\xa2\xfa\x19\x16C3@\x80l\xc7" +
"\xdb\xc0\x19`e<\xcfa##Fz\x00m]~" +
"#\x0a\x12\x84\x0csS\x1eK\xef\x09\x8e;j\xeb\x9c" +
"f\x01\x93&*\x0b\xdb\xa0\x03q\xfe\xb9\x8b\xd4\xd3m" +
"\x9a\xf6x0B\x95\xe5\xf0\xec\\\xca\xc8\xa0Ufq" +
"\xf2C\xcbt\xa3\x12O\xd8X\x8a'N\x95m\xe3X" +
"\x8a\xf7\x94ws\\g\xe6\x00\x9b\x80\xf8\x10\xcfpk" +
"2.\xe2\xd6\x93\xf6\x8c1^\x99\xb0\xc7]\x93\xf5\xf6" +
"\xae\x83n+\xc32\x01SEA\x1f\x1a.\x17\"\x9c" +
"&\xdb\x84\xb6\xd7\xe3\x8c\x06\x13Fd\xf3=4\xe1^" +
"'o\xb8\x81\x81D\xee\xcd(\xc9\xe5M\x97\xf7X:" +
"\xc47\x98\x86\xceKx8\x93sg\xf9\x8d{\xca\xb9" +
"|\xe7,;\xf7\xbf\xbb\x91\xfe\xf6\"+\x15\xa44\\" +
"\xc8\x89STv)\x9d\xb2-l\x1b\x12\xa7h\xab<" +
"E\x97\xf7\x8b\xb6p\xc9J\x80\xfd\xe9\xbc\xe3\x10i\xca" +
"9\\7\xd2\x1e\x07\xd4\xfd\x9c\xed\xba\xc6\xb0\xc9Kg" +
"\xab\xa5\x8b]\x10\x19a\x86y\xc3\x8b\xf0\xbe\xe2\x95k" +
"\xf9\xc6u\xca5\xf0\x1a\x80\xe4\xa3*&\x9f\x0c4\x16" +
"'\x87\x02w\xc3\x0aJ\xd7\xcf\xec\x0c\xdc\x0d\xab\x8al" +
"\xb0\xce\x1e\x04H>\xabb\xf2B\xe5\x1a\xf8<\xbd\xfd" +
"\x9c\x8a\xc9W\x02\xd7\xc0\xd7I\xf1\x9a\x8a\xc9\xf7\x14\xec" +
"f\"\xbb\xa6\xa4\xabIs\xad\xe4\x8e\xed1s\xbda" +
"\x82\xca]\xac\x01\x05k p}\xcc\xf5\xf5\x86\xc9]" +
"(K\xcc\xe2w+@\xb7tK\xdcP\xfcRP\x12" +
"a\x1f\xf7\x98a\xbaP\xb9F.\xff\xa7\x92\xe25\xf2" +
"l\x88\xc1\xa4,\xb8Q\xf0K\x9f\xd5\xfa\xba\xa5\x0f\x82" +
"\xba\x96\xd7`\x1d5w}*&\x07\x15,-\xc1\x00" +
"E;\xa1b\xf2\xde\xc0\x12l\xa5 nQ1\xf9-" +
"%0\xe5\x00\x15\x9a]\xd4\xfe?\x00\x00\xff\xff\xd6\xbd" +
"\x94\xef"
func RegisterSchema(reg *schemas.Registry) {
reg.Register(&schemas.Schema{
+113 -13
View File
@@ -1,8 +1,11 @@
package cereal
import (
"encoding/json"
"log/slog"
"math"
"os"
"regexp"
"capnproto.org/go/capnp/v3"
"pfeifer.dev/mapd/cereal/custom"
@@ -15,6 +18,8 @@ const (
externalFixMaxAge = 500_000_000 // 10 Hz source: 500 ms
internalFixMaxAge = 2_000_000_000 // 1 Hz source: 2 s
carFixMaxAge = 2_500_000_000 // Optional CAN observation: 2.5 s
phoneFixMaxAge = 10_000_000_000 // Companion app in the background sends sparsely: 10 s
gpsModeRefresh = 1_000_000_000 // Galaxy navigation setting reread interval
maxHorizontalAccuracy = 50.0 // input sanity ceiling, not road-match qualification
maxClockPairSkew = 1_000_000 // an interrupted pair cannot date a source event
maxClockOffsetDrift = 1_000_000 // a larger BOOTTIME/MONOTONIC step fences queued fixes
@@ -39,8 +44,53 @@ const (
GpsSourceInternal
GpsSourceExternal
GpsSourceCar
GpsSourcePhone
)
// Galaxy navigation's GPS source setting: backup uses the phone only without
// comma or car GPS, comma never uses it, phone prefers it whenever it has a fix.
const (
GpsModeBackup = "backup"
GpsModeComma = "comma"
GpsModePhone = "phone"
)
var storagePrefix = regexp.MustCompile(`^[A-Za-z0-9_-]{1,64}$`)
func navigationSettingsPath() string {
prefix := os.Getenv("OPENPILOT_PREFIX")
if prefix == "" {
return "/data/starpilot/navigation/settings.json"
}
if !storagePrefix.MatchString(prefix) {
return ""
}
return "/data/starpilot-" + prefix + "/navigation/settings.json"
}
// ReadGpsMode returns the saved choice, or backup when it is missing or invalid.
func ReadGpsMode() string {
path := navigationSettingsPath()
if path == "" {
return GpsModeBackup
}
raw, err := os.ReadFile(path)
if err != nil || len(raw) > 1<<20 {
return GpsModeBackup
}
var settings struct {
GpsSource string `json:"gpsSource"`
}
if json.Unmarshal(raw, &settings) != nil {
return GpsModeBackup
}
switch settings.GpsSource {
case GpsModeComma, GpsModePhone:
return settings.GpsSource
}
return GpsModeBackup
}
type GpsSample struct {
Location log.GpsLocationData
Source GpsSource
@@ -60,7 +110,12 @@ type GpsSub struct {
gpsLocation Subscriber[log.GpsLocationData]
gpsLocationExternal Subscriber[log.GpsLocationData]
carGps *Subscriber[custom.StarPilotCarState_Gps]
phoneGps *Subscriber[custom.StarPilotCarState_Gps]
car gpsCandidate
phone gpsCandidate
mode func() string
modeValue string
modeReadAt uint64
internal gpsCandidate
external gpsCandidate
selected GpsSource
@@ -83,6 +138,8 @@ func GpsFixFreshAt(source GpsSource, fixMonoTime, now uint64) bool {
return now-fixMonoTime <= externalFixMaxAge
case GpsSourceCar:
return now-fixMonoTime <= carFixMaxAge
case GpsSourcePhone:
return now-fixMonoTime <= phoneFixMaxAge
case GpsSourceInternal:
return now-fixMonoTime <= internalFixMaxAge
default:
@@ -134,6 +191,35 @@ func (s *GpsSub) clearCandidates() {
s.internal = gpsCandidate{}
s.external = gpsCandidate{}
s.car = gpsCandidate{}
s.phone = gpsCandidate{}
}
func (s *GpsSub) gpsMode(now uint64) string {
if s.modeValue == "" || now < s.modeReadAt || now-s.modeReadAt >= gpsModeRefresh {
read := s.mode
if read == nil {
read = ReadGpsMode
}
s.modeValue, s.modeReadAt = read(), now
}
return s.modeValue
}
// updateWrapped admits a StarPilotCarState.Gps-shaped fix dated by its own source time.
func (c *gpsCandidate) updateWrapped(event DecodedEvent[custom.StarPilotCarState_Gps], sensor log.GpsLocationData_SensorSource,
clock clockSample, barrierMono, maxAge uint64) bool {
sourceTime := event.Value.SourceMonoTime()
if sourceTime == 0 || sourceTime > event.LogMonoTime || event.LogMonoTime > clock.monoAfter {
return false
}
location, err := carGpsLocation(event.Value)
if err != nil {
return false
}
location.SetSource(sensor)
valid := event.Valid && !math.IsNaN(float64(location.Speed())) && !math.IsInf(float64(location.Speed()), 0) && location.Speed() >= 0 &&
!math.IsNaN(location.Altitude()) && !math.IsInf(location.Altitude(), 0)
return c.updateWithAge(DecodedEvent[log.GpsLocationData]{Value: location, Valid: valid, LogMonoTime: sourceTime}, clock, barrierMono, maxAge)
}
func (s *GpsSub) clockReady(clock clockSample) bool {
@@ -166,7 +252,7 @@ func (s *GpsSub) ReadSample() (sample GpsSample, success bool) {
clock = s.clock()
}
ready := s.clockReady(clock)
internalNew, externalNew, carNew := false, false, false
internalNew, externalNew, carNew, phoneNew := false, false, false, false
if event, ok := s.gpsLocation.ReadEvent(); ok && ready {
internalNew = s.internal.update(event, clock, s.barrierMono)
}
@@ -176,23 +262,29 @@ func (s *GpsSub) ReadSample() (sample GpsSample, success bool) {
if s.carGps != nil {
if event, ok := s.carGps.ReadEvent(); ok && ready {
sourceTime := event.Value.SourceMonoTime()
if sourceTime > 0 && sourceTime <= event.LogMonoTime && event.LogMonoTime <= clock.monoAfter {
location, err := carGpsLocation(event.Value)
if err == nil {
carNew = s.car.updateWithAge(DecodedEvent[log.GpsLocationData]{Value: location, Valid: event.Valid && !math.IsNaN(float64(location.Speed())) && !math.IsInf(float64(location.Speed()), 0) && location.Speed() >= 0 && !math.IsNaN(location.Altitude()) && !math.IsInf(location.Altitude(), 0), LogMonoTime: sourceTime}, clock, s.barrierMono, carFixMaxAge)
}
}
carNew = s.car.updateWrapped(event, log.GpsLocationData_SensorSource_car, clock, s.barrierMono, carFixMaxAge)
}
}
if s.phoneGps != nil {
if event, ok := s.phoneGps.ReadEvent(); ok && ready {
// The enum has no phone value; android is the closest.
phoneNew = s.phone.updateWrapped(event, log.GpsLocationData_SensorSource_android, clock, s.barrierMono, phoneFixMaxAge)
}
}
mode := s.gpsMode(clock.boot)
phoneFresh := ready && mode != GpsModeComma && GpsFixFreshAt(GpsSourcePhone, s.phone.fixMonoTime, clock.boot)
source := GpsSourceNone
if ready && GpsFixFreshAt(GpsSourceExternal, s.external.fixMonoTime, clock.boot) {
if phoneFresh && mode == GpsModePhone {
source = GpsSourcePhone
} else if ready && GpsFixFreshAt(GpsSourceExternal, s.external.fixMonoTime, clock.boot) {
source = GpsSourceExternal
} else if ready && GpsFixFreshAt(GpsSourceInternal, s.internal.fixMonoTime, clock.boot) {
source = GpsSourceInternal
} else if ready && GpsFixFreshAt(GpsSourceCar, s.car.fixMonoTime, clock.boot) {
source = GpsSourceCar
} else if phoneFresh {
source = GpsSourcePhone
}
sample.SourceChanged = source != s.selected
if sample.SourceChanged {
@@ -215,6 +307,10 @@ func (s *GpsSub) ReadSample() (sample GpsSample, success bool) {
sample.Location = s.car.location
sample.FixMonoTime = s.car.fixMonoTime
sample.NewFix = carNew
case GpsSourcePhone:
sample.Location = s.phone.location
sample.FixMonoTime = s.phone.fixMonoTime
sample.NewFix = phoneNew
default:
return sample, false
}
@@ -234,24 +330,28 @@ func (s *GpsSub) Close() {
if s.carGps != nil {
s.carGps.Sub.Msgq.Close()
}
if s.phoneGps != nil {
s.phoneGps.Sub.Msgq.Close()
}
}
func GetGpsSub() (gpsSub GpsSub) {
return GpsSub{
carGps: optionalCarGpsSubscriber(),
carGps: optionalWrappedGpsSubscriber("starpilotCarState", CarGpsReader),
phoneGps: optionalWrappedGpsSubscriber("starpilotNavigation", PhoneGpsReader),
gpsLocation: NewSubscriber("gpsLocation", GpsLocationReader, true, ms.Settings.SubscriberSettings.ShadowGpsLocation),
gpsLocationExternal: NewSubscriber("gpsLocationExternal", GpsLocationExternalReader, true, ms.Settings.SubscriberSettings.ShadowGpsLocationExternal),
}
}
func optionalCarGpsSubscriber() (subscriber *Subscriber[custom.StarPilotCarState_Gps]) {
func optionalWrappedGpsSubscriber(service string, reader func(log.Event) (custom.StarPilotCarState_Gps, error)) (subscriber *Subscriber[custom.StarPilotCarState_Gps]) {
defer func() {
if recover() != nil {
subscriber = nil
slog.Warn("Optional car GPS subscriber unavailable")
slog.Warn("Optional GPS subscriber unavailable", "service", service)
}
}()
value := NewSubscriber("starpilotCarState", CarGpsReader, true, false)
value := NewSubscriber(service, reader, true, false)
return &value
}
+1 -1
View File
@@ -2623,7 +2623,7 @@ struct Event {
# DO change the name of the field and struct
# DON'T change the ID (e.g. @107)
# DON'T change which struct it points to
customReserved0 @107 :Custom.CustomReserved0;
starpilotNavigation @107 :Custom.StarPilotCarControl;
customReserved1 @108 :Custom.CustomReserved1;
starpilotCarState @109 :Custom.StarPilotCarState;
customReserved3 @110 :Custom.CustomReserved3;
File diff suppressed because it is too large Load Diff
+125
View File
@@ -0,0 +1,125 @@
package cereal
import (
"capnproto.org/go/capnp/v3"
"pfeifer.dev/mapd/cereal/custom"
"pfeifer.dev/mapd/cereal/log"
"testing"
)
func phoneGpsWire(t *testing.T, source, envelope uint64) []byte {
t.Helper()
message, segment, err := capnp.NewMessage(capnp.SingleSegment(nil))
if err != nil {
t.Fatal(err)
}
event, err := log.NewRootEvent(segment)
if err != nil {
t.Fatal(err)
}
event.SetLogMonoTime(envelope)
event.SetValid(true)
navigation, err := event.NewStarpilotNavigation()
if err != nil {
t.Fatal(err)
}
gps, err := navigation.NewPhoneGps()
if err != nil {
t.Fatal(err)
}
gps.SetSourceMonoTime(source)
gps.SetHasFix(true)
gps.SetLatitude(41)
gps.SetLongitude(-111)
gps.SetAltitude(100)
gps.SetSpeed(12)
gps.SetBearingDeg(90)
gps.SetHorizontalAccuracy(5)
gps.SetVerticalAccuracy(5)
gps.SetBearingAccuracyDeg(10)
gps.SetSpeedAccuracy(1)
raw, err := message.Marshal()
if err != nil {
t.Fatal(err)
}
return raw
}
func attachPhone(s *GpsSub, mode *string) *[][]byte {
queue := [][]byte{}
s.phoneGps = &Subscriber[custom.StarPilotCarState_Gps]{reader: PhoneGpsReader, maxBytes: 250 * 1024, readBytes: func() []byte {
if len(queue) == 0 {
return nil
}
value := queue[0]
queue = queue[1:]
return value
}}
s.mode = func() string { return *mode }
return &queue
}
func TestPhoneGpsFollowsTheGalaxySetting(t *testing.T) {
now := uint64(10_000_000_000)
mode := GpsModeBackup
s, internal, _ := testGpsSub(&now)
car := attachCar(s)
phone := attachPhone(s, &mode)
*phone = append(*phone, phoneGpsWire(t, now-300_000_000, now))
sample, ok := s.ReadSample()
if !ok || sample.Source != GpsSourcePhone || sample.FixMonoTime != now-300_000_000 || sample.Location.Longitude() != -111 {
t.Fatalf("backup without comma or car GPS: %+v %v", sample, ok)
}
*car = append(*car, carGpsWire(t, now-1, now, true, true))
if sample, ok = s.ReadSample(); !ok || sample.Source != GpsSourceCar {
t.Fatal("car ranks above the phone in backup")
}
*internal = append(*internal, gpsWire(t, GpsSourceInternal, now, true, true, 1, 2, 5))
if sample, ok = s.ReadSample(); !ok || sample.Source != GpsSourceInternal {
t.Fatal("comma ranks above the phone in backup")
}
mode = GpsModePhone
now += gpsModeRefresh
*phone = append(*phone, phoneGpsWire(t, now-100_000_000, now))
*internal = append(*internal, gpsWire(t, GpsSourceInternal, now, true, true, 1, 2, 5))
if sample, ok = s.ReadSample(); !ok || sample.Source != GpsSourcePhone {
t.Fatal("prefer phone")
}
now += phoneFixMaxAge
*internal = append(*internal, gpsWire(t, GpsSourceInternal, now, true, true, 1, 2, 5))
if sample, ok = s.ReadSample(); !ok || sample.Source != GpsSourceInternal {
t.Fatal("prefer phone falls back to comma once the phone fix expires")
}
mode = GpsModeComma
now += gpsModeRefresh
*phone = append(*phone, phoneGpsWire(t, now, now))
if sample, ok = s.ReadSample(); !ok || sample.Source != GpsSourceInternal {
t.Fatal("comma and car only keeps comma GPS")
}
now += internalFixMaxAge + 1
*phone = append(*phone, phoneGpsWire(t, now, now))
if _, ok = s.ReadSample(); ok {
t.Fatal("comma and car only must never use the phone")
}
}
func TestPhoneGpsModeIsRereadAtMostOnceASecond(t *testing.T) {
now := uint64(10_000_000_000)
reads := 0
s, _, _ := testGpsSub(&now)
s.mode = func() string { reads++; return GpsModeBackup }
s.ReadSample()
now += gpsModeRefresh - 1
s.ReadSample()
if reads != 1 {
t.Fatalf("mode reread early: %d", reads)
}
now++
s.ReadSample()
if reads != 2 {
t.Fatalf("mode not reread: %d", reads)
}
}
+8
View File
@@ -45,3 +45,11 @@ func CarGpsReader(evt log.Event) (custom.StarPilotCarState_Gps, error) {
}
return state.Gps()
}
func PhoneGpsReader(evt log.Event) (custom.StarPilotCarState_Gps, error) {
navigation, err := evt.StarpilotNavigation()
if err != nil {
return custom.StarPilotCarState_Gps{}, err
}
return navigation.PhoneGps()
}
+1
View File
@@ -61,6 +61,7 @@ var ServiceQueueSize = map[string]int64{
"selfdriveState": QUEUE_SIZE_SMALL,
"gpsLocation": QUEUE_SIZE_SMALL,
"starpilotCarState": QUEUE_SIZE_SMALL,
"starpilotNavigation": QUEUE_SIZE_MEDIUM,
"gpsLocationExternal": QUEUE_SIZE_SMALL,
"liveLocationKalman": QUEUE_SIZE_SMALL,
"gpsNMEA": QUEUE_SIZE_SMALL,
+1 -1
View File
@@ -176,7 +176,7 @@ func (s *State) ProcessGps(sample cereal.GpsSample, success bool, load MapLoader
pos := m.PosFromLocation(location)
s.Position = pos
// Unknown CAN direction or speed uncertainty cannot create confident road evidence.
if sample.Source == cereal.GpsSourceCar && (math.IsNaN(float64(location.BearingAccuracyDeg())) || math.IsInf(float64(location.BearingAccuracyDeg()), 0) || location.BearingAccuracyDeg() <= 0 || location.BearingAccuracyDeg() >= 180 || math.IsNaN(float64(location.SpeedAccuracy())) || math.IsInf(float64(location.SpeedAccuracy()), 0) || location.SpeedAccuracy() >= 100) {
if (sample.Source == cereal.GpsSourceCar || sample.Source == cereal.GpsSourcePhone) && (math.IsNaN(float64(location.BearingAccuracyDeg())) || math.IsInf(float64(location.BearingAccuracyDeg()), 0) || location.BearingAccuracyDeg() <= 0 || location.BearingAccuracyDeg() >= 180 || math.IsNaN(float64(location.SpeedAccuracy())) || math.IsInf(float64(location.SpeedAccuracy()), 0) || location.SpeedAccuracy() >= 100) {
s.clearRoadDerived()
s.roadFixMonoTime = 0
s.RoadStatus = custom.MapdOut_SampleStatus_noMatch
Binary file not shown.
@@ -38,6 +38,8 @@ class Inputs:
lane_direction: int = 0
limit_status: int = 0
geometry: LaneGeometry | None = None
speed_min: float | None = None
speed_max: float | None = None
@dataclass(frozen=True)
@@ -85,6 +87,12 @@ class AngleController:
if not i.active or not all(math.isfinite(v) for v in values) or i.speed < 0.0:
self.reset()
return Command()
speed_min = max(i.speed, 0.1) if i.speed_min is None else i.speed_min
speed_max = max(i.speed, 0.1) if i.speed_max is None else i.speed_max
if (not all(math.isfinite(v) for v in (speed_min, speed_max)) or
not 0 < speed_min <= max(i.speed, 0.1) <= speed_max):
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
@@ -152,7 +160,7 @@ class AngleController:
# 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)
cap = min(bounds.CURVATURE_MAX, bounds.LATERAL_ACCEL / max(speed_max, 1.0) ** 2)
low_kappa, high_kappa = -cap, cap
if i.speed > 9.0:
low_kappa = max(low_kappa, i.measured_curvature - 0.0018)
@@ -161,8 +169,10 @@ class AngleController:
# 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)
lower_speed = speed_max if low_kappa >= 0.0 else speed_min
upper_speed = speed_min if high_kappa >= 0.0 else speed_max
lower = max(-bounds.PATH_MAX, low_kappa * lower_speed * lower_gain, self.path_last - roc)
upper = min(-bounds.PATH_MIN, high_kappa * upper_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
@@ -1,7 +1,7 @@
"""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.
extended profiles can select angle. Combined AOL retains its original Ford profile identity.
"""
from opendbc.car import structs
from opendbc.car.ford.values import CAR, FordFlags
@@ -18,7 +18,7 @@ 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
if (cp.brand != "ford" or cp.passive or cp.dashcamOnly or cp.notCar or cp.alternativeExperience not in (0, 32) or
cp.steerControlType != structs.CarParams.SteerControlType.angle or
not cp.pcmCruise or len(cp.safetyConfigs) not in (1, 2)):
return None
@@ -32,7 +32,13 @@ def mode_word(cp, *, selected):
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
manual = cp.transmissionType == structs.CarParams.TransmissionType.manual
if cp.transmissionType not in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.manual):
return None
if cp.alternativeExperience == 32:
expected = original | (256 if manual else 0) | (ANGLE_FLAG if selected else 0)
else:
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
@@ -49,7 +55,10 @@ def select(cp, raw):
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)
if cp.alternativeExperience == 32:
cp.safetyConfigs[-1].safetyParam |= ANGLE_FLAG
else:
cp.safetyConfigs[-1].safetyParam = ANGLE_FLAG + (2 if cp.flags & FordFlags.CANFD else 0) + int(cp.openpilotLongitudinalControl)
cp.steerActuatorDelay = 0.22
return True
@@ -79,16 +88,40 @@ class Host:
self.cp = cp
self.controller = AngleController(low_gain=low, high_gain=high)
self.inputs = None
self.tx_blocked = 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)
aol = self.cp.alternativeExperience == 32
ordinary = CC.enabled and CS.out.cruiseState.enabled and not CS.out.gasPressed and not CS.out.brakePressed
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
(aol and CS.out.cruiseState.available or ordinary) and CC.latActive and CS.out.canValid and
not CS.out.canTimeout and
(not aol or CS.out.gearShifter in (structs.CarState.GearShifter.drive,
structs.CarState.GearShifter.sport, structs.CarState.GearShifter.low)) and
not CS.out.steerFaultTemporary and not CS.out.steerFaultPermanent and not CS.out.vehicleSensorsInvalid)
# Rejected frames do not advance Panda's accepted counter/path history.
# One real blocked-TX increment requires an accepted neutral resynchronization,
# rather than sending an indefinitely rejected chain of active counters.
counter_reader = getattr(self.inputs, "native_tx_blocked", None)
blocked = counter_reader() if ready and callable(counter_reader) else None
if not ready:
self.tx_blocked = None
elif type(blocked) is int and 0 <= blocked <= 0xFFFFFFFF:
changed = self.tx_blocked is not None and blocked != self.tx_blocked
self.tx_blocked = blocked
ready = not changed
speed_min = speed_max = None
speed_window = getattr(CS, "bluepilot_speed_window", None)
if speed_window is not None:
low, high, step = speed_window
# Allow source progress across the existing 100 ms CAN freshness window.
margin = max(0.05, 5 * step)
speed_min = max(0.1, min(low, CS.out.vEgoRaw) - margin)
speed_max = max(high, CS.out.vEgoRaw, 0.1) + margin
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),
@@ -98,4 +131,4 @@ class Host:
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)))
geometry=model_geometry(model, fresh=snapshot is not None), speed_min=speed_min, speed_max=speed_max))
@@ -41,6 +41,9 @@ static bool ford_bp_configure(uint16_t param) {
#ifdef ALLOW_DEBUG
ford_bp_enabled |= (param == 131U) && (alternative_experience == 0);
#endif
const bool joint = ((unsigned int)alternative_experience == 32U) &&
((param == 146U) || (param == 147U) || (param == 160U) || (param == 161U) || (param == 194U) || (param == 195U));
ford_bp_enabled |= joint;
ford_bp_canfd = (param & 2U) != 0U;
ford_bp_lka_counter_seen = false;
ford_bp_control_counter_seen = false;
@@ -62,6 +65,16 @@ static bool ford_bp_configure(uint16_t param) {
static bool ford_bp_health(uint32_t now);
static bool ford_bp_permission(void) {
bool permitted = false;
if (ford_aol_enabled) {
permitted = (ford_aol_permission_mask() & 1U) != 0U;
} else {
permitted = lateral_controls_allowed() && controls_allowed && !brake_pressed && !gas_pressed;
}
return permitted;
}
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};
@@ -77,7 +90,7 @@ static void ford_bp_rx(const CANPacket_t *msg) {
controls_allowed = false;
}
// Permission losses remove the old actuator history before any later arm.
if (!controls_allowed || brake_pressed || gas_pressed) {
if (!ford_bp_permission() || !ford_bp_health(microsecond_timer_get())) {
ford_bp_withdraw();
}
}
@@ -113,7 +126,7 @@ static bool ford_bp_lka(const CANPacket_t *msg) {
((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;
const bool resync = (shadow == 0) && !ford_bp_permission();
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;
@@ -193,7 +206,7 @@ static bool ford_bp_lateral(const CANPacket_t *msg) {
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 &&
valid &= ford_bp_permission() && 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)))) &&
+23 -1
View File
@@ -11,6 +11,7 @@ from opendbc.car.fw_versions import (ObdCallback, get_fw_versions, get_fw_versio
from opendbc.car.mock.values import CAR as MOCK
from opendbc.car.gm.pedal_capability import automatic_bolt_candidate
from opendbc.car.gm.discovery import automatic_volt_candidate
from opendbc.car.ford.discovery import normal_bus0_selection as ford_normal_bus0_selection, normal_bus0_queries as ford_normal_bus0_queries
from opendbc.car.hyundai.discovery import normal_bus_topology
from opendbc.car.values import BRANDS
from opendbc.car.vin import get_vin, is_valid_vin, VIN_UNKNOWN
@@ -97,8 +98,10 @@ def _gm_fw_uses_normal_bus0() -> bool:
# **** 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
cached_params: CarParamsT | None, *, known_gm_bus0: bool = False, known_ford_bus0: bool = False
) -> tuple[str | None, dict, str, list[CarParams.CarFw], CarParams.FingerprintSource, bool]:
if known_ford_bus0 and (known_gm_bus0 or not ford_normal_bus0_queries()):
raise ValueError('Ford discovery topology no longer uses normal bus 0')
if known_gm_bus0 and not _gm_fw_uses_normal_bus0():
raise ValueError('GM discovery topology no longer uses normal bus 0')
fixed_fingerprint = os.environ.get('FINGERPRINT', "")
@@ -128,6 +131,18 @@ def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_mu
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}
elif known_ford_bus0:
# A registered CAN-FD selection identifies Ford's query family, not live
# ECU authority. Keep normal CAN2 wiring during discovery: the unrelated
# all-brand OBD mux scan can produce a CAN2 error/interrupt storm.
set_obd_multiplexing(False)
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='ford', normal_bus_only=True)
ecu_rx_addrs = {(fw.responseAddress, fw.subAddress or None, fw.bus) for fw in car_fw if fw.responseAddress > 0}
elif normal_bus_topology(can_recv):
set_obd_multiplexing(False)
@@ -198,12 +213,19 @@ def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multip
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())
known_ford_bus0 = ford_normal_bus0_selection(explicit_candidate)
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_ford_bus0 and cached_params is not None and (
cached_params.brand != 'ford' 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)
elif known_ford_bus0:
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(
can_recv, can_send, set_obd_multiplexing, cached_params, known_ford_bus0=True)
else:
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(can_recv, can_send, set_obd_multiplexing, cached_params)
@@ -0,0 +1,45 @@
"""Exact stock-ACC Chrysler AOL admission. Dashcam variants remain excluded."""
from opendbc.car import structs
from opendbc.car.chrysler.values import CAR, ChryslerFlags
CHRYSLER_AOL_EXPERIENCE = 32
CHRYSLER_AOL_CLASSIC = frozenset((
CAR.CHRYSLER_PACIFICA_2018_HYBRID, CAR.CHRYSLER_PACIFICA_2019_HYBRID,
CAR.CHRYSLER_PACIFICA_2018, CAR.CHRYSLER_PACIFICA_2020, CAR.DODGE_DURANGO,
CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019,
))
CHRYSLER_AOL_CARS = CHRYSLER_AOL_CLASSIC | {CAR.RAM_1500_5TH_GEN}
def qualified(cp, *, marked_only=False):
if (cp is None or cp.brand != 'chrysler' or cp.carFingerprint not in CHRYSLER_AOL_CARS or
cp.notCar or cp.passive or cp.dashcamOnly or not cp.pcmCruise or
cp.openpilotLongitudinalControl or cp.alphaLongitudinalAvailable or
cp.steerControlType != structs.CarParams.SteerControlType.torque or
len(cp.safetyConfigs) != 1 or
cp.alternativeExperience not in ((CHRYSLER_AOL_EXPERIENCE,) if marked_only else (0, CHRYSLER_AOL_EXPERIENCE))):
return False
flags = ChryslerFlags.HIGHER_MIN_STEERING_SPEED | ChryslerFlags.HAS_BSM | ChryslerFlags.STEERING_SPEED_BYPASS
jeep = cp.carFingerprint in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019)
if jeep:
flags |= ChryslerFlags.JEEP_BRAKE_HOLD_AVAILABLE
expected = (1 if cp.carFingerprint == CAR.RAM_1500_5TH_GEN else
8 if cp.carFingerprint == CAR.CHRYSLER_PACIFICA_2019_HYBRID and cp.alternativeExperience == 32 else 0)
return bool(not int(cp.flags) & ~int(flags) and
cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.chrysler and
int(cp.safetyConfigs[0].safetyParam) in ((0, 4) if jeep else (expected,)) and
cp.lateralTuning.which() in (('torque',) if cp.carFingerprint == CAR.RAM_1500_5TH_GEN else ('pid', 'torque')))
def temporary_restriction(cp, state):
gears = structs.CarState.GearShifter
return bool(qualified(cp, marked_only=True) and state is not None and
state.canValid and not state.canTimeout and not state.steerFaultPermanent and
(state.gearShifter not in (gears.drive, gears.low) or state.steerFaultTemporary or
state.standstill or state.vEgo < cp.minSteerSpeed))
def lkas_button_supported(cp):
# Dom's Chrysler whitelist did not enable LKAS AOL. Preserve that feature
# disposition until an intentional end-to-end activation requirement exists.
return False
@@ -0,0 +1,25 @@
"""Optional stock-ACC stop retention for exactly the two Grand Cherokee ports."""
def brake_hold_supported(cp):
from opendbc.car import structs
from opendbc.car.chrysler.values import CAR, ChryslerFlags
return bool(cp is not None and cp.brand == "chrysler" and
cp.carFingerprint in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019) and
cp.flags & ChryslerFlags.JEEP_BRAKE_HOLD_AVAILABLE and cp.pcmCruise and
not cp.openpilotLongitudinalControl and not cp.passive and not cp.dashcamOnly and not cp.notCar and
len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.chrysler and
cp.safetyConfigs[0].safetyParam in (0, 4))
def apply_brake_hold(cp, requested):
if brake_hold_supported(cp):
cp.safetyConfigs[0].safetyParam = 4 if requested else 0
def create_das_3_command(packer, counter_offset, brake_decel, das_3):
values = das_3.copy()
values.update(ACC_AVAILABLE=1, ACC_ACTIVE=1, ACC_GO=0, ACC_STANDSTILL=0, ACC_DECEL_REQ=1,
ACC_DECEL=brake_decel, ACC_BRK_PREP=0, ENGINE_TORQUE_REQUEST_MAX=0, GR_MAX_REQ=2,
COUNTER=(int(das_3["COUNTER"]) + counter_offset) % 16)
return packer.make_can_msg("DAS_3", 0, values)
@@ -1,7 +1,12 @@
import os
import time
from opendbc.can import CANPacker
from opendbc.car import Bus, DT_CTRL
from opendbc.car.lateral import apply_meas_steer_torque_limits
from opendbc.car.chrysler import chryslercan
from opendbc.car.chrysler.aol_contract import qualified as aol_qualified
from opendbc.car.chrysler.brake_hold import brake_hold_supported, create_das_3_command
from opendbc.car.chrysler.values import CUSW_CARS, RAM_CARS, RAM_DT, CarControllerParams, ChryslerFlags
from opendbc.car.structs import CarState
from opendbc.car.interfaces import CarControllerBase
@@ -16,6 +21,11 @@ class CarController(CarControllerBase):
self.last_lkas_falling_edge = 0
self.lkas_control_bit_prev = False
self.last_button_frame = 0
self.jeep_hold_replay = "REPLAY" in os.environ
self.jeep_decel = -2.0
self.last_das_3_counter = -1
self.jeep_hold_cancelled = False
self.jeep_cruise_was_active = False
self.packer = CANPacker(dbc_names[Bus.pt])
self.params = CarControllerParams(CP)
@@ -33,12 +43,17 @@ class CarController(CarControllerBase):
das_bus = 2 if self.CP.carFingerprint in RAM_CARS else 0
# ACC cancellation
if CC.cruiseControl.cancel:
pause_stock_acc = bool(aol_qualified(self.CP, marked_only=True) and
CS.aol_pause_longitudinal and 0 < CS.aol_pause_longitudinal_stamp_ns <= now_nanos and
now_nanos - CS.aol_pause_longitudinal_stamp_ns <= 30_000_000 and
CS.out.canValid and not CS.out.canTimeout)
if CC.cruiseControl.cancel or (pause_stock_acc and CS.out.cruiseState.enabled):
self.last_button_frame = self.frame
can_sends.append(chryslercan.create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, cancel=True))
# ACC resume from standstill
elif CC.cruiseControl.resume:
elif (CC.cruiseControl.resume and not pause_stock_acc and
(not aol_qualified(self.CP, marked_only=True) or CS.out.cruiseState.enabled)):
self.last_button_frame = self.frame
can_sends.append(chryslercan.create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, resume=True))
@@ -94,6 +109,14 @@ class CarController(CarControllerBase):
if _projection is not None:
_projection.emit('chrysler.LKAS_COMMAND', apply_torque, lkas_control_bit, can_sends[-1])
if brake_hold_supported(self.CP) and self.CP.safetyConfigs[0].safetyParam == 4:
withdraw = bool(aol_qualified(self.CP, marked_only=True) and CS.aol_pause_longitudinal and
0 < CS.aol_pause_longitudinal_stamp_ns <= now_nanos and
now_nanos - CS.aol_pause_longitudinal_stamp_ns <= 30_000_000)
self.update_jeep_brake_hold(CC, CS, now_nanos, can_sends, withdraw=withdraw)
else:
CS.brake_hold = False
self.frame += 1
new_actuators = CC.actuators.as_builder()
@@ -103,3 +126,43 @@ class CarController(CarControllerBase):
if _projection is not None:
_projection.finish(new_actuators.torque)
return new_actuators, can_sends
def update_jeep_brake_hold(self, CC, CS, now_nanos, can_sends, *, withdraw=False):
# CAN Event/parser timestamps are BOOTTIME. Card passes MONOTONIC for
# ordinary live Chrysler controllers, and original CAN time during replay.
# Select the established replay contract explicitly; never infer an offset
# or renew the parser's original source timestamp at command time.
now_boot_ns = now_nanos if self.jeep_hold_replay else time.clock_gettime_ns(time.CLOCK_BOOTTIME)
if (not CS.das_3 or not CS.out.canValid or CS.out.canTimeout or
not 0 <= now_boot_ns - CS.das_3_timestamp <= 100_000_000):
CS.brake_hold = False
return
counter = int(CS.das_3["COUNTER"])
changed = counter != self.last_das_3_counter
self.last_das_3_counter = counter
active = CS.out.cruiseState.enabled
if active and not self.jeep_cruise_was_active:
self.jeep_hold_cancelled = False
self.jeep_cruise_was_active = active
if CC.cruiseControl.cancel or CS.jeep_driver_cancel or withdraw:
self.jeep_hold_cancelled = True
if self.jeep_hold_cancelled:
CS.brake_hold = False
return
decel = float(CS.das_3["ACC_DECEL"])
if not CS.brake_hold and CS.out.cruiseState.enabled and decel < -0.5 and CS.out.standstill:
CS.brake_hold = True
self.jeep_decel = -2.0
if (CC.cruiseControl.cancel or CS.jeep_driver_cancel or withdraw or CS.out.gasPressed or CS.out.brakePressed or
CS.out.gearShifter != CarState.GearShifter.drive or not CS.out.standstill or
not CS.out.cruiseState.available or CS.out.accFaulted):
CS.brake_hold = False
return
if not CS.brake_hold:
return
if CS.out.cruiseState.enabled:
self.jeep_decel = max(-3.0, min(-0.5, min(self.jeep_decel, decel)))
return
can_sends.append(create_das_3_command(self.packer, 2 if changed else 3, self.jeep_decel, CS.das_3))
if self.frame % 10 == 0:
can_sends.append(chryslercan.create_cruise_buttons(self.packer, CS.button_counter + 1, 0, resume=True))
+22 -1
View File
@@ -1,5 +1,6 @@
from opendbc.can import CANDefine, CANParser
from opendbc.car import Bus, create_button_events, structs
from opendbc.car.chrysler.aol_contract import qualified as aol_qualified
from opendbc.car.chrysler.values import CUSW_CARS, DBC, STEER_THRESHOLD, RAM_CARS, ChryslerFlags
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.interfaces import CarStateBase
@@ -23,6 +24,13 @@ class CarState(CarStateBase):
self.shifter_values = can_define.dv["GEAR"]["PRNDL"]
self.distance_button = 0
self.aol_pause_longitudinal = False
self.aol_pause_longitudinal_stamp_ns = 0
self.aol_buttons = {ButtonType.mainCruise: 0, ButtonType.cancel: 0}
self.brake_hold = False
self.das_3 = {}
self.das_3_timestamp = 0
self.jeep_driver_cancel = False
def update(self, can_parsers) -> structs.CarState:
cp = can_parsers[Bus.pt]
@@ -80,6 +88,11 @@ class CarState(CarStateBase):
ret.cruiseState.nonAdaptive = cp_cruise.vl["DAS_4"]["ACC_STATE"] in (1, 2) # 1 NormalCCOn and 2 NormalCCSet
ret.cruiseState.standstill = cp_cruise.vl["DAS_3"]["ACC_STANDSTILL"] == 1
ret.accFaulted = cp_cruise.vl["DAS_3"]["ACC_FAULTED"] != 0
if self.CP.flags & ChryslerFlags.JEEP_BRAKE_HOLD_AVAILABLE:
self.das_3 = dict(cp_cruise.vl["DAS_3"])
self.jeep_driver_cancel = bool(cp.vl["CRUISE_BUTTONS"]["ACC_Cancel"])
self.das_3_timestamp = cp_cruise.ts_nanos["DAS_3"]["COUNTER"]
ret.brakeHoldActive = self.brake_hold
if self.CP.carFingerprint in RAM_CARS:
# Auto High Beam isn't Located in this message on chrysler or jeep currently located in 729 message
@@ -97,7 +110,15 @@ class CarState(CarStateBase):
self.lkas_car_model = cp_cam.vl["DAS_6"]["CAR_MODEL"]
self.button_counter = cp.vl["CRUISE_BUTTONS"]["COUNTER"]
ret.buttonEvents = create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
button_events = create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
if aol_qualified(self.CP, marked_only=True):
buttons = cp.vl["CRUISE_BUTTONS"]
values = {ButtonType.mainCruise: bool(buttons["Cruise_OnOff"] or buttons["ACC_OnOff"]),
ButtonType.cancel: bool(buttons["ACC_Cancel"])}
for button, value in values.items():
button_events += create_button_events(value, self.aol_buttons[button], {1: button})
self.aol_buttons[button] = value
ret.buttonEvents = button_events
return ret
@@ -57,6 +57,7 @@ class CarInterface(CarInterfaceBase):
elif candidate in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019):
ret.steerActuatorDelay = 0.2
ret.flags |= ChryslerFlags.JEEP_BRAKE_HOLD_AVAILABLE.value
ret.lateralTuning.init('pid')
ret.lateralTuning.pid.kpBP, ret.lateralTuning.pid.kiBP = [[9., 20.], [9., 20.]]
@@ -0,0 +1,235 @@
"""Real Jeep parser/controller stock-stop retention; no system speed authority."""
import os
import unittest
from unittest.mock import patch
from opendbc.can import CANPacker, CANParser
from opendbc.car import Bus, gen_empty_fingerprint, structs
from opendbc.car.chrysler.brake_hold import apply_brake_hold, brake_hold_supported
from opendbc.car.chrysler.interface import CarInterface
from opendbc.car.chrysler.values import CAR, DBC
def params(candidate=CAR.JEEP_GRAND_CHEROKEE, *, enabled=True):
cp = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], False, False, False)
apply_brake_hold(cp, enabled)
return cp
class PhysicalCan:
def __init__(self, ci):
ci.update([])
self.ci = ci
self.packers = {bus: CANPacker(parser.dbc.name) for bus, parser in ci.can_parsers.items()}
def frames(self, tick, *, active=True, decel=-2.0, main=True, gas=0.0, brake=0, speed=0.0, gear=4, fault=0, omit=()):
values = {
'DAS_3': {
'ACC_ACTIVE': int(active),
'ACC_AVAILABLE': int(main),
'ACC_DECEL': decel,
'ACC_STANDSTILL': 1,
'ACC_FAULTED': fault,
'ENGINE_TORQUE_REQUEST': 13.25,
'DISABLE_FUEL_SHUTOFF': 1,
'COLLISION_BRK_PREP': 1,
'COUNTER': (14 + tick // 2) % 16,
},
'DAS_4': {'ACC_SET_SPEED_KPH': 100, 'ACC_STATE': 3},
'GEAR': {'PRNDL': gear},
'ECM_5': {'Accelerator_Position': gas},
'ESP_1': {'Brake_Pedal_State': brake},
'SPEED_1': {'SPEED_LEFT': speed, 'SPEED_RIGHT': speed},
}
return [
self.packers[bus].make_can_msg(parser.dbc.addr_to_msg[address].name, parser.bus, values.get(parser.dbc.addr_to_msg[address].name, {}))
for bus, parser in self.ci.can_parsers.items()
for address in sorted(parser.addresses)
if parser.dbc.addr_to_msg[address].name not in omit
]
def apply_at_boot(ci, command, now):
with patch('opendbc.car.chrysler.carcontroller.time.clock_gettime_ns', return_value=now):
return ci.apply(command.as_reader(), now)
class TestJeepBrakeHold(unittest.TestCase):
def setUp(self):
prior = os.environ.pop('REPLAY', None)
def restore():
if prior is None:
os.environ.pop('REPLAY', None)
else:
os.environ['REPLAY'] = prior
self.addCleanup(restore)
def ready(self, candidate=CAR.JEEP_GRAND_CHEROKEE):
ci = CarInterface(params(candidate))
stream = PhysicalCan(ci)
cc = structs.CarControl(enabled=True)
for tick in range(20):
ci.update([(100_000_000_000 + tick * 10_000_000, stream.frames(tick))])
apply_at_boot(ci, cc, 100_000_000_000 + tick * 10_000_000)
self.assertTrue(ci.CS.out.canValid and ci.CS.brake_hold)
return ci, stream, cc
def test_two_exact_platforms_stock_ownership_default_off_and_siblings(self):
for candidate in CAR:
for release in (False, True):
cp = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], True, release, False)
expected = candidate in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019)
self.assertEqual(brake_hold_supported(cp), expected)
before = cp.to_dict()
apply_brake_hold(cp, True)
self.assertEqual(cp.safetyConfigs[0].safetyParam, 4 if expected else before['safetyConfigs'][0]['safetyParam'])
self.assertFalse(cp.openpilotLongitudinalControl)
self.assertTrue(cp.pcmCruise)
apply_brake_hold(cp, False)
self.assertEqual(cp.to_dict(), before)
def test_real_parser_copied_fields_decel_counter_wrap_and_repeat(self):
for candidate in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019):
ci, stream, cc = self.ready(candidate)
decoder = CANParser(DBC[candidate][Bus.pt], [('DAS_3', 0)], 0)
for tick in range(20, 55):
now = 100_000_000_000 + tick * 10_000_000
previous = ci.CC.last_das_3_counter
ci.update([(now, stream.frames(tick, active=False))])
_, frames = apply_at_boot(ci, cc, now)
hold = [frame for frame in frames if frame[0] == 0x1F4]
self.assertEqual(len(hold), 1)
decoder.update([(now, hold)])
parsed = decoder.vl['DAS_3']
stock = ci.CS.das_3
self.assertEqual(parsed['COUNTER'], (int(stock['COUNTER']) + (2 if int(stock['COUNTER']) != previous else 3)) % 16)
for name in ('ENGINE_TORQUE_REQUEST', 'DISABLE_FUEL_SHUTOFF', 'COLLISION_BRK_PREP', 'ACC_FAULTED'):
self.assertEqual(parsed[name], stock[name])
for name, expected in {
'ACC_ACTIVE': 1,
'ACC_AVAILABLE': 1,
'ACC_GO': 0,
'ACC_STANDSTILL': 0,
'ACC_DECEL_REQ': 1,
'GR_MAX_REQ': 2,
'ACC_BRK_PREP': 0,
'ENGINE_TORQUE_REQUEST_MAX': 0,
}.items():
self.assertEqual(parsed[name], expected)
self.assertAlmostEqual(parsed['ACC_DECEL'], -2.0, delta=0.004885)
resumes = [frame for frame in frames if frame[0] == 0x23B]
self.assertEqual(bool(resumes), tick % 10 == 0)
def test_driver_main_fault_and_source_withdrawal(self):
for stimulus in ('gas', 'brake', 'speed', 'gear', 'main', 'fault', 'cancel', 'stale'):
ci, stream, cc = self.ready()
args = (
{stimulus: {'gas': 1.0, 'brake': 1, 'speed': 1.0, 'gear': 1, 'main': False, 'fault': 1}.get(stimulus)} if stimulus not in ('cancel', 'stale') else {}
)
now = 100_200_000_000
if stimulus == 'cancel':
cc.cruiseControl.cancel = True
ci.update([(now, stream.frames(20, active=False, **args))])
if stimulus == 'stale':
now += 100_000_001
_, frames = apply_at_boot(ci, cc, now)
self.assertFalse(ci.CS.brake_hold, stimulus)
self.assertFalse(any(frame[0] == 0x1F4 for frame in frames), stimulus)
def test_stock_braking_strength_clamped_to_original_envelope(self):
ci, stream, cc = self.ready()
for tick in range(20, 30):
now = 100_000_000_000 + tick * 10_000_000
ci.update([(now, stream.frames(tick, decel=-4.0))])
apply_at_boot(ci, cc, now)
self.assertEqual(ci.CC.jeep_decel, -3.0)
def test_real_parser_crc_fault_withdraws_hold(self):
ci, stream, cc = self.ready()
for tick in range(20, 32):
now = 100_000_000_000 + tick * 10_000_000
frames = stream.frames(tick, active=False)
for index, (address, data, bus) in enumerate(frames):
if address == 0x1F4:
invalid = bytearray(data)
invalid[-1] ^= 1
frames[index] = (address, bytes(invalid), bus)
ci.update([(now, frames)])
_, emitted = apply_at_boot(ci, cc, now)
self.assertGreater(now - ci.CS.das_3_timestamp, 100_000_000)
self.assertFalse(ci.CS.brake_hold)
self.assertFalse(any(frame[0] == 0x1F4 for frame in emitted))
def test_commanded_cancel_requires_fresh_stock_acc_engagement(self):
ci, stream, cc = self.ready()
cc.cruiseControl.cancel = True
ci.update([(100_200_000_000, stream.frames(20))])
apply_at_boot(ci, cc, 100_200_000_000)
cc.cruiseControl.cancel = False
for tick in range(21, 25):
now = 100_000_000_000 + tick * 10_000_000
ci.update([(now, stream.frames(tick))])
apply_at_boot(ci, cc, now)
self.assertFalse(ci.CS.brake_hold)
ci.update([(100_250_000_000, stream.frames(25, active=False))])
apply_at_boot(ci, cc, 100_250_000_000)
ci.update([(100_260_000_000, stream.frames(26))])
apply_at_boot(ci, cc, 100_260_000_000)
self.assertTrue(ci.CS.brake_hold)
def test_actual_factory_default_off_and_siblings_emit_no_hold(self):
for candidate in CAR:
for requested in (False, True):
if requested and candidate in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019):
continue
ci = CarInterface(params(candidate, enabled=requested))
stream = PhysicalCan(ci)
cc = structs.CarControl()
for tick in range(20):
now = 100_000_000_000 + tick * 10_000_000
ci.update([(now, stream.frames(tick, active=False))])
_, frames = apply_at_boot(ci, cc, now)
self.assertFalse(ci.CS.brake_hold)
self.assertFalse(any(frame[0] == 0x1F4 for frame in frames))
def test_actual_parser_reverse_then_drive_does_not_restore_disabled_stock_long(self):
ci, stream, cc = self.ready()
ci.update([(100_200_000_000, stream.frames(20, active=False, gear=2))])
_, frames = apply_at_boot(ci, cc, 100_200_000_000)
self.assertEqual(ci.CS.out.gearShifter, structs.CarState.GearShifter.reverse)
self.assertFalse(ci.CS.brake_hold)
self.assertFalse(any(frame[0] == 0x1F4 for frame in frames))
ci.update([(100_210_000_000, stream.frames(21, active=False, gear=4))])
_, frames = apply_at_boot(ci, cc, 100_210_000_000)
self.assertFalse(ci.CS.brake_hold)
self.assertFalse(any(frame[0] == 0x1F4 for frame in frames))
def test_original_can_boot_age_is_independent_of_controller_monotonic_time(self):
ci, stream, cc = self.ready()
boot_ns = 100_200_000_000
ci.update([(boot_ns, stream.frames(20, active=False))])
with patch('opendbc.car.chrysler.carcontroller.time.clock_gettime_ns', return_value=boot_ns):
_, frames = ci.apply(cc.as_reader(), boot_ns - 9_000_000_000)
self.assertTrue(ci.CS.brake_hold)
self.assertTrue(any(frame[0] == 0x1F4 for frame in frames))
def test_original_boot_source_future_and_stale_are_not_restamped(self):
for offset in (-1, 100_000_001):
ci, stream, cc = self.ready()
source = 100_200_000_000
ci.update([(source, stream.frames(20, active=False))])
with patch('opendbc.car.chrysler.carcontroller.time.clock_gettime_ns', return_value=source + offset):
_, frames = ci.apply(cc.as_reader(), source)
self.assertFalse(ci.CS.brake_hold)
self.assertFalse(any(frame[0] == 0x1F4 for frame in frames))
def test_replay_uses_original_can_time_in_the_explicit_replay_domain(self):
with patch.dict(os.environ, {'REPLAY': '1'}):
ci, stream, cc = self.ready()
source = 100_200_000_000
ci.update([(source, stream.frames(20, active=False))])
with patch('opendbc.car.chrysler.carcontroller.time.clock_gettime_ns', side_effect=AssertionError('no host clock in original-time replay')):
_, frames = ci.apply(cc.as_reader(), source)
self.assertTrue(any(frame[0] == 0x1F4 for frame in frames))
@@ -12,6 +12,8 @@ Ecu = CarParams.Ecu
class ChryslerSafetyFlags(IntFlag):
RAM_DT = 1
RAM_HD = 2
AOL_PACIFICA_HYBRID_SET_GUARD = 8
JEEP_BRAKE_HOLD = 4
class ChryslerFlags(IntFlag):
@@ -19,6 +21,7 @@ class ChryslerFlags(IntFlag):
HIGHER_MIN_STEERING_SPEED = 1
HAS_BSM = 2 # blind spot monitoring
STEERING_SPEED_BYPASS = 4
JEEP_BRAKE_HOLD_AVAILABLE = 8
@dataclass
+44 -7
View File
@@ -1,18 +1,56 @@
from opendbc.car import structs
from opendbc.car.ford.values import CAR, FordFlags
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
AOL_EXPERIENCE = 32
CLASSIC = frozenset((CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_ESCAPE_MK4, CAR.FORD_FOCUS_MK4,
CAR.FORD_MAVERICK_MK1, CAR.FORD_EXPLORER_MK6))
GENERIC_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))
AOL_WORDS = frozenset((8, 9, 10, 11, 12, 13, 18, 19, 32, 33, 66, 67))
MANUAL = int(FordSafetyFlags.MANUAL)
BASE_WORDS = frozenset((8, 9, 10, 11, 12, 13, 18, 19, 32, 33, 66, 67))
ANGLE = 128
ANGLE_BASE_WORDS = frozenset((18, 19, 32, 33, 66, 67))
AOL_WORDS = (BASE_WORDS | frozenset(word | ANGLE for word in ANGLE_BASE_WORDS))
AOL_WORDS |= frozenset(word | MANUAL for word in AOL_WORDS)
def aol_safety_config(cp):
if len(cp.safetyConfigs) not in (1, 2):
return None
if len(cp.safetyConfigs) == 2:
passive = cp.safetyConfigs[0]
if passive.safetyModel != structs.CarParams.SafetyModel.noOutput or passive.safetyParam != 0:
return None
owner = cp.safetyConfigs[-1]
return owner if owner.safetyModel == structs.CarParams.SafetyModel.ford else None
def base_safety_param(cp):
owner = aol_safety_config(cp)
if owner is None:
return -1
manual = cp.transmissionType == structs.CarParams.TransmissionType.manual
if cp.transmissionType not in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.manual):
return -1
word = int(owner.safetyParam)
if (word & MANUAL and not manual) or (manual and cp.alternativeExperience == AOL_EXPERIENCE and not word & MANUAL):
return -1
if word & ANGLE:
if cp.alternativeExperience == 0:
from opendbc.bluepilot_lateral.hosts.starpilot import qualified as angle_qualified
if not angle_qualified(cp):
return -1
base = 18 if cp.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 else (66 if cp.carFingerprint in GENERIC_CANFD else 32)
return base | int(cp.openpilotLongitudinalControl)
if word not in AOL_WORDS:
return -1
word &= ~ANGLE
return word & ~MANUAL
def qualified(cp, *, marked_only=False):
if (cp.brand != 'ford' or cp.carFingerprint not in tuple(CAR) or cp.passive or cp.notCar or cp.dashcamOnly or
cp.steerControlType != structs.CarParams.SteerControlType.angle or
cp.transmissionType != structs.CarParams.TransmissionType.automatic or len(cp.safetyConfigs) != 1 or
cp.alternativeExperience not in ((32,) if marked_only else (0, 32))):
return False
flags = int(cp.flags) & ~int(FordFlags.HAS_BSM)
@@ -30,10 +68,9 @@ def qualified(cp, *, marked_only=False):
base = 12
else:
return False
safety = cp.safetyConfigs[0]
return (cp.pcmCruise and
safety.safetyModel == structs.CarParams.SafetyModel.ford and
safety.safetyParam == (base | int(cp.openpilotLongitudinalControl)))
safety = aol_safety_config(cp)
return bool(cp.pcmCruise and safety is not None and
base_safety_param(cp) == (base | int(cp.openpilotLongitudinalControl)))
def temporary_restriction(cp, state):
+34 -14
View File
@@ -75,6 +75,9 @@ class CarController(CarControllerBase):
self.manual_turn_inputs = None
self.mache_lateral = MachELateralController(CP) if mache_qualified(CP) else None
self.mache_extended_announced = False
self.mache_ui_owned = False
self.hands_free_cluster_input = None
self.cluster_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)
@@ -98,6 +101,10 @@ class CarController(CarControllerBase):
def update(self, CC, CS, now_nanos):
can_sends = []
if self.hands_free_cluster_input is not None:
self.hands_free_cluster_input.update()
if not CC.latActive:
self.mache_ui_owned = False
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:
self.manual_turn_inputs.update()
self.manual_turn_inputs.apply_blend_settings(self)
@@ -138,6 +145,7 @@ class CarController(CarControllerBase):
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
self.mache_ui_owned |= bool(self.bp_lat.announced.active and self.bp_lat.pending.active)
elif (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.,
@@ -156,6 +164,7 @@ class CarController(CarControllerBase):
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.mache_ui_owned |= bool(lateral.active)
self.apply_curvature_last = lateral.curvature
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
can_sends.append(mache_can.create_lat_ctl2_msg(
@@ -322,25 +331,36 @@ class CarController(CarControllerBase):
self.accel = accel
self.gas = gas
# Track the actual outgoing LKA announcement for every CAN-FD display owner.
if any(address == 0x3CA for address, _, _ in can_sends):
self.cluster_extended_announced = True
### 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))
# Acquire the display after the first active replacement, then retain it while
# AOL stays active, including a manual-turn steering handoff (as on Dom).
aol_mache_ui = self.CP.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 and qualified_aol(self.CP, marked_only=True)
ui_enabled = CC.latActive and (not aol_mache_ui or self.mache_ui_owned)
if not aol_mache_ui or ui_enabled:
send_ui = (self.main_on_last != main_on) or (self.lkas_enabled_last != ui_enabled) 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, ui_enabled, 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
# 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))
if (self.frame % CarControllerParams.ACC_UI_STEP) == 0 or send_ui:
show_distance_bars = self.frame - self.distance_bar_frame < 400
hands_free_cluster = bool(self.cluster_extended_announced and
self.hands_free_cluster_input is not None and self.hands_free_cluster_input.enabled)
can_sends.append(fordcan.create_acc_ui_msg(self.packer, self.CAN, self.CP, main_on, ui_enabled,
fcw_alert, CS.out.cruiseState.standstill, show_distance_bars,
hud_control, CS.acc_tja_status_stock_values, hands_free_cluster))
self.main_on_last = main_on
self.lkas_enabled_last = CC.latActive
self.lkas_enabled_last = ui_enabled
self.steer_alert_last = steer_alert
self.lead_distance_bars_last = hud_control.leadDistanceBars
+20
View File
@@ -35,10 +35,13 @@ class CarState(CarStateBase):
self.lc_button = 0
self.cancel_button = False
self.cancel_resume_button = FordStockCruiseButton()
self.cruise_buttons = {ButtonType.accelCruise: False, ButtonType.decelCruise: False}
self.steering_angle_offset_deg = 0.0
self.lkas_available = False
self.lkas_available_ts_nanos = 0
self.lateral_motion_control = None
self.bluepilot_speed_samples = [0] * 6 if CP.safetyConfigs and CP.safetyConfigs[-1].safetyParam & 128 else None
self.bluepilot_speed_window = None
def get_car_gps(self):
return self.car_gps_tracker.get()
@@ -62,6 +65,14 @@ class CarState(CarStateBase):
# car speed
ret.vEgoRaw = cp.vl["BrakeSysFeatures"]["Veh_V_ActlBrk"] * CV.KPH_TO_MS
if self.bluepilot_speed_samples is not None:
# Match Panda's six raw ABS speed samples, including its millimetre/second rounding.
for value in cp.vl_all["BrakeSysFeatures"]["Veh_V_ActlBrk"]:
self.bluepilot_speed_samples.append(math.floor(value * CV.KPH_TO_MS * 1000 + .5))
self.bluepilot_speed_samples = self.bluepilot_speed_samples[-6:]
samples = self.bluepilot_speed_samples
step = max(abs(b - a) for a, b in zip(samples, samples[1:], strict=False)) / 1000
self.bluepilot_speed_window = (min(samples) / 1000, max(samples) / 1000, step)
ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
ret.yawRate = cp.vl["Yaw_Data_FD1"]["VehYaw_W_Actl"]
ret.standstill = cp.vl["DesiredTorqBrk"]["VehStop_D_Stat"] == 1
@@ -169,6 +180,13 @@ class CarState(CarStateBase):
if self.CP.openpilotLongitudinalControl:
button_events += create_button_events(self.cancel_button, prev_cancel_button, {1: ButtonType.cancel})
for button, signals in (
(ButtonType.accelCruise, ('CcAslButtnSetIncPress', 'CcAslButtnResIncPress', 'CcAslButtnIndxIncPress')),
(ButtonType.decelCruise, ('CcAslButtnSetDecPress', 'CcAslButtnResDecPress', 'CcAslButtnIndxDecPress')),
):
pressed = any(self.buttons_stock_values[signal] for signal in signals)
button_events += create_button_events(pressed, self.cruise_buttons[button], {1: button})
self.cruise_buttons[button] = pressed
ret.buttonEvents = button_events
@@ -207,6 +225,8 @@ class CarState(CarStateBase):
gear = ("Gear_Shift_by_Wire_FD1" if CP.flags & FordFlags.CANFD else
"TransGearData" if CP.flags & FordFlags.ALT_STEER_ANGLE else "PowertrainData_10")
pt_messages.append((gear, 10))
if CP.transmissionType == TransmissionType.manual:
pt_messages.append(("BCM_Lamp_Stat_FD1", 10))
if CP.flags & FordFlags.LKA_STEERING:
pt_messages.append(("Lane_Assist_Data3_FD1", 30))
cam_messages.append(("LateralMotionControl", 20))
@@ -6,6 +6,7 @@ CREDITS.md and THIRD_PARTY_NOTICES.md; this module only selects admitted callers
"""
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.explorer_lateral import ExplorerLateralController, bounded_command as bounded_command, qualified as explorer_qualified
from opendbc.car.ford.lateral_strategy import FordLateralController
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
@@ -16,8 +17,6 @@ CLASSIC_EXTENDED_CARS = GENERIC_CLASSIC_CARS | {CAR.FORD_EXPLORER_MK6}
def qualified(cp):
if cp.alternativeExperience == 32 and len(cp.safetyConfigs) != 1:
return False
if cp.carFingerprint == CAR.FORD_EXPLORER_MK6:
return explorer_qualified(cp)
if (cp.brand != "ford" or cp.carFingerprint not in GENERIC_CLASSIC_CARS or
@@ -29,7 +28,7 @@ def qualified(cp):
return False
safety = cp.safetyConfigs[-1]
expected = int(FordSafetyFlags.CLASSIC_EXTENDED | (FordSafetyFlags.LONG_CONTROL if cp.openpilotLongitudinalControl else 0))
return safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected
return safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(cp) == expected
class GenericClassicLateralController(FordLateralController):
@@ -0,0 +1,36 @@
"""Normal-bus discovery for explicitly selected Ford CAN-FD vehicles."""
from opendbc.car.ford.values import CAR, FordFlags
def normal_bus0_queries():
# Verify both configured and effective requests before omitting the OBD sweep.
# Essential queries use normal bus0 and its exact auxiliary Panda bus4 copy.
# As-built remains bus0; bus1 logging cannot provide unique evidence.
from opendbc.car import fw_versions
configured = fw_versions.FW_QUERY_CONFIGS['ford'].requests
effective = [request for brand, _, request in fw_versions.REQUESTS if brand == 'ford']
if not configured or not effective:
return False
def signature(request):
return (tuple(request.request), tuple(request.response), request.rx_offset, frozenset(request.whitelist_ecus))
for requests in (configured, effective):
normal = {signature(request) for request in requests if request.bus == 0}
essential = {signature(request) for request in requests if request.bus == 0 and not request.logging}
auxiliary = {signature(request) for request in requests if request.bus == 4 and not request.logging}
if not normal or not essential or auxiliary != essential:
return False
for request in requests:
if request.bus == 0:
continue
if request.bus == 4 and not request.logging and signature(request) in essential:
continue
if request.bus == 1 and request.logging and signature(request) in normal:
continue
return False
return True
def normal_bus0_selection(candidate):
return bool(candidate in CAR and CAR(candidate).config.flags & FordFlags.CANFD and
not CAR(candidate).config.flags & FordFlags.LKA_STEERING and normal_bus0_queries())
@@ -11,6 +11,7 @@ from typing import Protocol
import numpy as np
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.lateral_strategy import FordLateralController, FordLateralResult, FORD_CURVATURE_LIMITS, STEER_DT
from opendbc.car.ford.values import CAR, CarControllerParams, FordFlags, FordSafetyFlags
@@ -34,8 +35,6 @@ class ExplorerLateralController(FordLateralController):
def qualified(CP) -> bool:
if CP.alternativeExperience == 32 and len(CP.safetyConfigs) != 1:
return False
if (CP.brand != "ford" or CP.carFingerprint != CAR.FORD_EXPLORER_MK6 or
CP.flags & ~int(FordFlags.HAS_BSM) or CP.passive or CP.dashcamOnly or CP.notCar or
CP.alternativeExperience not in (0, 32) or len(CP.safetyConfigs) not in (1, 2)):
@@ -45,7 +44,7 @@ def qualified(CP) -> bool:
return False
safety = CP.safetyConfigs[-1]
expected = int(FordSafetyFlags.EXPLORER_EXTENDED | (FordSafetyFlags.LONG_CONTROL if CP.openpilotLongitudinalControl else 0))
return safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected
return safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(CP) == expected
class CurvatureOwner(Protocol):
+5 -2
View File
@@ -32,10 +32,11 @@ def create_lka_msg(packer, CAN: CanBus, active=False, apply_angle=0., direction=
if transit:
values = {"LaRefAng_No_Req": 0., "LaCurvature_No_Calc": 0., "LdwActvIntns_D_Req": 3}
if active:
# 5.8 degrees rounds outside native raw 24..4072 at 0.05 mrad/count.
values = {
"LkaDrvOvrrd_D_Rq": 0,
"LkaActvStats_D2_Req": direction,
"LaRefAng_No_Req": math.radians(max(-5.8, min(5.8, apply_angle))) * 1000.,
"LaRefAng_No_Req": max(-101.2, min(101.2, math.radians(max(-5.8, min(5.8, apply_angle))) * 1000.)),
"LaRampType_B_Req": int(abs(apply_angle) >= 5.),
"LaCurvature_No_Calc": max(-0.01023, min(0.01023, curvature)),
"LdwActvStats_D_Req": 0,
@@ -145,7 +146,7 @@ def create_acc_msg(packer, CAN: CanBus, long_active: bool, gas: float, accel: fl
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):
show_distance_bars: bool, hud_control, stock_values: dict, hands_free_cluster: bool = False):
"""
Creates a CAN message for the Ford IPC adaptive cruise, forward collision warning and traffic jam
assist status.
@@ -161,6 +162,8 @@ def create_acc_ui_msg(packer, CAN: CanBus, CP, main_on: bool, enabled: bool, fcw
status = 3 # ActiveInterventionLeft
elif hud_control.rightLaneDepart:
status = 4 # ActiveInterventionRight
elif hands_free_cluster:
status = 7 # Hands-free assistance display
else:
status = 2 # Active
elif main_on:
@@ -4,6 +4,7 @@ The reused BluePilot-derived strategy retains its original attribution in
lateral_strategy.py, CREDITS.md and THIRD_PARTY_NOTICES.md.
"""
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.explorer_lateral import bounded_command as shared_bound
from opendbc.car.ford.lateral_strategy import FordLateralController
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
@@ -14,8 +15,6 @@ SOURCE_ACCEL_CAP = 3.0 - 9.81 * .06
def qualified(cp):
if cp.alternativeExperience == 32 and len(cp.safetyConfigs) != 1:
return False
if (cp.brand != "ford" or cp.carFingerprint not in GENERIC_CANFD_CARS or
not cp.flags & FordFlags.CANFD or cp.flags & ~int(FordFlags.CANFD | FordFlags.HAS_BSM) or
cp.passive or cp.dashcamOnly or cp.notCar or cp.alternativeExperience not in (0, 32) or
@@ -27,7 +26,7 @@ def qualified(cp):
s = cp.safetyConfigs[-1]
expected = int(FordSafetyFlags.GENERIC_CANFD_EXTENDED | FordSafetyFlags.CANFD |
(FordSafetyFlags.LONG_CONTROL if cp.openpilotLongitudinalControl else 0))
return s.safetyModel == structs.CarParams.SafetyModel.ford and s.safetyParam == expected
return s.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(cp) == expected
class GenericCanfdLateralController(FordLateralController):
+5 -10
View File
@@ -55,12 +55,12 @@ class CarInterface(CarInterfaceBase):
new_port = bool(ret.flags & FordFlags.NEW_PORT)
if candidate in GENERIC_CANFD_CARS:
ret.alphaLongitudinalAvailable = not is_release
ret.alphaLongitudinalAvailable = True
ret.steerActuatorDelay = 0.22
longitudinal_requested = bool(alpha_long and not is_release)
longitudinal_requested = bool(alpha_long)
elif candidate == CAR.FORD_MUSTANG_MACH_E_MK1:
ret.alphaLongitudinalAvailable = not is_release
longitudinal_requested = bool(alpha_long and not is_release)
ret.alphaLongitudinalAvailable = True
longitudinal_requested = bool(alpha_long)
elif candidate in CLASSIC_EXTENDED_CARS:
# Original shared classic ownership is explicit stock/OP-long selection.
# Actuator delay is live; modern LongControl already supplies the original P=0.
@@ -68,7 +68,7 @@ class CarInterface(CarInterfaceBase):
ret.steerActuatorDelay = 0.22
longitudinal_requested = bool(alpha_long)
else:
ret.alphaLongitudinalAvailable = (not is_release if ret.flags & FordFlags.CANFD else True) if new_port else ret.radarUnavailable
ret.alphaLongitudinalAvailable = True if new_port else ret.radarUnavailable
longitudinal_requested = bool(alpha_long and ret.alphaLongitudinalAvailable) if new_port else alpha_long or not ret.radarUnavailable
if longitudinal_requested:
ret.safetyConfigs[-1].safetyParam |= FordSafetyFlags.LONG_CONTROL.value
@@ -139,11 +139,6 @@ class CarInterface(CarInterfaceBase):
and ret.safetyConfigs[-1].safetyParam in (2, 3)
)
if mach_e_extended:
if is_release:
# The native CANFD LONG profile is DEBUG-only; RELEASE stays stock.
ret.alphaLongitudinalAvailable = False
ret.openpilotLongitudinalControl = False
ret.safetyConfigs[-1].safetyParam = FordSafetyFlags.CANFD.value
ret.safetyConfigs[-1].safetyParam |= FordSafetyFlags.MACH_E_EXTENDED.value
if (candidate in GENERIC_CANFD_CARS and ret.flags & FordFlags.CANFD and
not ret.flags & ~int(FordFlags.CANFD | FordFlags.HAS_BSM) and
@@ -17,6 +17,7 @@ from dataclasses import replace
import numpy as np
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
from opendbc.car.lateral import MAX_LATERAL_JERK
from opendbc.car.ford.lateral_strategy import (
@@ -99,8 +100,6 @@ class MachELateralController(FordLateralController):
def qualified(CP) -> bool:
if CP.alternativeExperience == 32 and len(CP.safetyConfigs) != 1:
return False
if (CP.brand != "ford" or CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1 or
not CP.flags & FordFlags.CANFD or CP.flags & ~int(FordFlags.CANFD | FordFlags.HAS_BSM) or
CP.passive or CP.dashcamOnly or CP.notCar or CP.alternativeExperience not in (0, 32) or
@@ -111,7 +110,7 @@ def qualified(CP) -> bool:
return False
safety = CP.safetyConfigs[-1]
return (safety.safetyModel == structs.CarParams.SafetyModel.ford and
safety.safetyParam == int(FordSafetyFlags.CANFD | FordSafetyFlags.MACH_E_EXTENDED |
base_safety_param(CP) == int(FordSafetyFlags.CANFD | FordSafetyFlags.MACH_E_EXTENDED |
(FordSafetyFlags.LONG_CONTROL if CP.openpilotLongitudinalControl else 0)))
@@ -7,6 +7,7 @@ from dataclasses import replace
import math
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
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
@@ -22,7 +23,7 @@ def qualified(cp):
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)):
len(cp.safetyConfigs) not in (1, 2)):
return False
flags, word = profile
if (int(cp.flags) & ~int(FordFlags.HAS_BSM)) != flags:
@@ -31,7 +32,7 @@ def qualified(cp):
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)
return safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(cp) == word + int(cp.openpilotLongitudinalControl)
class NewPortCurvatureController(FordLateralController):
@@ -1,6 +1,7 @@
"""Ford physical cancel/resume switch context; no new cruise authority."""
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
from opendbc.car.ford.classic_lateral import CLASSIC_EXTENDED_CARS
from opendbc.car.ford.generic_canfd_lateral import qualified as generic_canfd_qualified
@@ -30,8 +31,6 @@ 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."""
if (CP.brand != "ford" or CP.carFingerprint not in tuple(CAR) or CP.openpilotLongitudinalControl or CP.alternativeExperience not in (0, 32) or
CP.passive or CP.dashcamOnly or CP.notCar or len(CP.safetyConfigs) not in (1, 2) or
@@ -41,17 +40,17 @@ def qualified(CP) -> bool:
CP.safetyConfigs[0].safetyParam != 0):
return False
safety = CP.safetyConfigs[-1]
if safety.safetyModel != structs.CarParams.SafetyModel.ford or safety.safetyParam not in (2, 8, 10, 12, 18, 32, 66):
if safety.safetyModel != structs.CarParams.SafetyModel.ford or base_safety_param(CP) not in (2, 8, 10, 12, 18, 32, 66):
return False
if safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == 32:
if safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(CP) == 32:
return CP.carFingerprint in CLASSIC_EXTENDED_CARS and not CP.flags & ~int(FordFlags.HAS_BSM)
if safety.safetyParam == 66:
if base_safety_param(CP) == 66:
return generic_canfd_qualified(CP)
expected = (int(FordSafetyFlags.CANFD) if CP.flags & FordFlags.CANFD else 0)
expected |= int(FordSafetyFlags.NEW_PORT) if CP.flags & FordFlags.NEW_PORT else 0
expected |= int(FordSafetyFlags.LKA_STEERING) if CP.flags & FordFlags.LKA_STEERING else 0
if safety.safetyParam == 18:
if base_safety_param(CP) == 18:
if CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1 or expected != int(FordSafetyFlags.CANFD):
return False
expected |= int(FordSafetyFlags.MACH_E_EXTENDED)
return safety.safetyParam == expected
return base_safety_param(CP) == expected
@@ -20,18 +20,24 @@ PROFILES = (
@pytest.mark.parametrize('identity,base_word', PROFILES)
@pytest.mark.parametrize('alpha', (False, True))
def test_unmarked_profiles_keep_actual_ordinary_controls(identity, base_word, alpha, monkeypatch):
@pytest.mark.parametrize('topology', ('automatic', 'manual', 'multi-panda', 'manual-multi'))
def test_unmarked_profiles_keep_actual_ordinary_controls(identity, base_word, alpha, topology, monkeypatch):
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
from opendbc.car.ford.tests.test_three_ports import params
monkeypatch.setenv('REPLAY', '1')
monkeypatch.setenv('AOL_REPLAY_RUNTIME', '0')
cp = params(identity, alpha, False)
offset = 4 if topology in ('multi-panda', 'manual-multi') else 0
manual = topology in ('manual', 'manual-multi')
fp = gen_empty_fingerprint()
if not manual:
fp[offset][0x5A] = 8
fp[offset + 2].update({0x3D6: 8, 0x186: 8})
cp = CarInterface.get_params(identity, fp, [], alpha, True, False)
assert cp.alternativeExperience == 0
assert cp.safetyConfigs[0].safetyParam == base_word | int(alpha)
assert cp.safetyConfigs[-1].safetyParam == base_word | int(alpha)
with OpenpilotPrefix():
saved = Params()
saved.put_bool('AlwaysOnLateral', False, block=True)
@@ -67,15 +73,17 @@ def run_startup(identity, base_word, alpha, release, topology, monkeypatch):
if topology != "manual":
fingerprint[offset][0x5A] = 8
fingerprint[offset + 2].update({0x3D6: 8, 0x186: 8})
expected_long = alpha and not release
expected_word = base_word | int(expected_long)
expected_long = alpha
initial_word = base_word | int(expected_long)
expected_word = initial_word | (256 if topology == "manual" else 0)
initial = CarInterface.get_params(identity, fingerprint, [], expected_long, release, False)
initial.carVin = "0" * 17
initial.carFw = []
admitted = topology == "automatic"
admitted = True
expected = initial.to_dict()
expected["alternativeExperience"] = 32 if admitted else 0
assert initial.safetyConfigs[-1].safetyParam == expected_word
expected["safetyConfigs"][-1]["safetyParam"] = expected_word
assert initial.safetyConfigs[-1].safetyParam == initial_word
assert initial.openpilotLongitudinalControl == expected_long
assert not initial.passive and not initial.dashcamOnly
with OpenpilotPrefix():
@@ -145,7 +153,7 @@ def run_startup(identity, base_word, alpha, release, topology, monkeypatch):
controls.publish(command, lateral_log)
ci.apply(command.as_reader(), 1_000_000_000)
if admitted:
from openpilot.starpilot.aol.wire import SAFETY_SERVICE, SafetyState, encode_safety
from openpilot.starpilot.aol.wire import SAFETY_SERVICE, PandaSlot, SafetyState, encode_safety
for tick, (acknowledged, stale) in enumerate(((False, False), (True, False), (True, True)), 1):
now = 1_000_000_000 + tick * 10_000_000
@@ -160,8 +168,11 @@ def run_startup(identity, base_word, alpha, release, topology, monkeypatch):
axis.aolAxisState.nativeAcknowledged = acknowledged
native = messaging.new_message(SAFETY_SERVICE, 0, valid=True, logMonoTime=now)
native.aolSafetyWire = encode_safety(SafetyState(
1, True, now, now + 200_000_000, int(card.CP.safetyConfigs[0].safetyModel.raw),
card.CP.safetyConfigs[0].safetyParam, acknowledged, False, True, False, 'fixture-panda', 'ford-current'))
1, True, now, now + 200_000_000, int(card.CP.safetyConfigs[-1].safetyModel.raw),
card.CP.safetyConfigs[-1].safetyParam, acknowledged, False, True, False, 'fixture-panda', 'ford-current', sourcePandaStatesMonoTime=now,
pandaInventory=tuple(PandaSlot(index, 'fixture-panda' if index == len(card.CP.safetyConfigs) - 1 else 'passive-panda',
int(config.safetyModel.raw), config.safetyParam, 32, False, False)
for index, config in enumerate(card.CP.safetyConfigs))))
controls.sm.update_msgs(now / 1e9, [axis.as_reader(), native.as_reader()])
active_command, _ = controls.state_control()
assert active_command.enabled
@@ -182,7 +193,7 @@ def test_actual_card_aol_publication_and_disabled_controls(identity, word, alpha
@pytest.mark.parametrize("identity,word", PROFILES)
@pytest.mark.parametrize("topology", ("manual", "multi-panda"))
def test_aol_denied_topology_preserves_actual_ordinary_startup(identity, word, topology, monkeypatch):
def test_aol_manual_and_multi_panda_keep_actual_ordinary_startup(identity, word, topology, monkeypatch):
run_startup(identity, word, False, False, topology, monkeypatch)
@@ -17,7 +17,7 @@ def test_generic_actual_card_publication_reaches_blend_settings(identity, monkey
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.car.card import Car, alpha_long_requested
from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.starpilot.controller_extensions import ManualTurnInputs
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
@@ -34,7 +34,7 @@ def test_generic_actual_card_publication_reaches_blend_settings(identity, monkey
saved.put_bool('FordHumanTurnDetection', False, block=True)
for key, value in (("FordCurvatureBlendLow", 0.25), ("FordCurvatureBlendHigh", 0.65), ("FordCurvatureLaneChangeFactor", 1.1)):
saved.put(key, value, block=True)
cp = params(identity, alpha_long_requested(saved, is_release=release), release)
cp = params(identity, alpha, release)
cp.carVin = '0' * 17
cp.carFw = []
expected = cp.to_dict()
@@ -0,0 +1,83 @@
"""Dom CAN-FD cluster scope through actual factory, setting UI and controller."""
from pathlib import Path
from types import SimpleNamespace
import pytest
from opendbc.can import CANParser
from opendbc.car import structs
from opendbc.car.ford.tests.test_mache_ui_owner import setup, UI
from opendbc.car.ford.values import CAR, FordFlags
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.starpilot.car.ford.cluster_preferences import KEY, qualified
from openpilot.starpilot.controller_extensions import configure_controller
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
from openpilot.starpilot.ui.feature_settings_state import row_change
DOM_CANFD = (
CAR.FORD_MUSTANG_MACH_E_MK1, 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_MONDEO_MK5,
)
def test_scope_covers_every_dom_canfd_profile():
assert set(DOM_CANFD) == {car for car in CAR if car.config.flags & FordFlags.CANFD and not car.config.flags & FordFlags.LKA_STEERING}
@pytest.mark.parametrize('identity', DOM_CANFD)
@pytest.mark.parametrize('aol', [False, True])
@pytest.mark.parametrize('enabled', [False, True])
def test_all_dom_profiles_have_shared_setting_and_correct_active_status(identity, aol, enabled):
with OpenpilotPrefix():
saved = Params()
controller, state = setup(aol=aol, identity=identity)
assert qualified(controller.CP)
owner = FeatureSettingsOwner(saved, lambda group: True, vehicle_params=lambda: controller.CP,
vehicle_fingerprint=lambda: str(identity))
row = next(r for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY)
assert row.available and row.value == 'Off'
if enabled:
assert owner.apply(row_change(row, 1))
configure_controller(SimpleNamespace(CP=controller.CP, CC=controller), saved)
assert controller.hands_free_cluster_input.enabled == enabled
command = structs.CarControl()
for _ in range(10):
_, frames = controller.update(command.as_reader(), state, controller.frame * 10_000_000)
if identity == CAR.FORD_MUSTANG_MACH_E_MK1 and aol:
assert not UI.intersection(address for address, _, _ in frames)
command.latActive = True
parser = CANParser('ford_lincoln_base_pt', [('ACCDATA_3', 5)], 0)
statuses = []
for _ in range(110):
_, frames = controller.update(command.as_reader(), state, controller.frame * 10_000_000)
for frame in frames:
if frame[0] == 0x18A:
parser.update([(controller.frame * 10_000_000, [frame])])
statuses.append(parser.vl['ACCDATA_3']['Tja_D_Stat'])
assert statuses and set(statuses) == {7 if enabled else 2}
assert controller.cluster_extended_announced
@pytest.mark.parametrize('identity', [car for car in CAR if car not in DOM_CANFD])
def test_non_canfd_fords_do_not_show_cluster_setting(identity):
with OpenpilotPrefix():
saved = Params()
controller, _ = setup(identity=identity)
assert not qualified(controller.CP)
configure_controller(SimpleNamespace(CP=controller.CP, CC=controller), saved)
assert controller.hands_free_cluster_input is None
owner = FeatureSettingsOwner(saved, lambda group: True, vehicle_params=lambda: controller.CP,
vehicle_fingerprint=lambda: str(identity))
assert KEY not in {r.key for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows}
def test_invalid_profile_does_not_enable_display_preference():
with OpenpilotPrefix():
saved = Params()
Path(saved.get_param_path(KEY)).write_bytes(b'1')
controller, _ = setup()
controller.CP.safetyConfigs[0].safetyParam = 0
assert not qualified(controller.CP)
configure_controller(SimpleNamespace(CP=controller.CP, CC=controller), saved)
assert controller.hands_free_cluster_input is None
@@ -10,14 +10,14 @@ from opendbc.car.ford.values import CAR, FordFlags
@pytest.mark.parametrize("car", sorted(GENERIC_CANFD_CARS))
@pytest.mark.parametrize("alpha,release", ((False,False),(True,False),(False,True),(True,True)))
def test_actual_profile_and_release_long_denial(car, alpha, release):
def test_actual_profile_and_release_long_support(car, alpha, release):
fp = gen_empty_fingerprint()
fp[0][0x5A] = 8
fp[2].update({0x3D6:8,0x186:8})
cp = CarInterface.get_params(car, fp, [], alpha, release, False)
assert cp.alphaLongitudinalAvailable == (not release)
assert cp.openpilotLongitudinalControl == (alpha and not release)
assert cp.safetyConfigs[-1].safetyParam == (67 if alpha and not release else 66)
assert cp.alphaLongitudinalAvailable
assert cp.openpilotLongitudinalControl == alpha
assert cp.safetyConfigs[-1].safetyParam == (67 if alpha else 66)
assert cp.steerActuatorDelay == pytest.approx(.22)
assert qualified(cp)
@@ -212,8 +212,8 @@ def test_mache_camera_source_preserves_exact_factory_words(alpha, release, bsm):
if bsm:
fp[0].update({0x3A6: 8, 0x3A7: 8})
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fp, [], alpha, release, False)
assert cp.safetyConfigs[-1].safetyParam == (19 if alpha and not release else 18)
assert cp.openpilotLongitudinalControl == (alpha and not release)
assert cp.safetyConfigs[-1].safetyParam == (19 if alpha else 18)
assert cp.openpilotLongitudinalControl == alpha
assert not cp.radarUnavailable
radar = RadarInterface(cp)
assert radar.rcp.bus == 2 and radar.generic_canfd_lead
@@ -14,15 +14,15 @@ def fingerprint(offset=0, secoc=False):
class TestMachEProfile(unittest.TestCase):
def test_actual_params_stock_debug_long_and_release_stock(self):
for alpha, release, word in ((False, False, 18), (True, False, 19), (False, True, 18), (True, True, 18)):
def test_actual_params_stock_debug_long_and_release_long(self):
for alpha, release, word in ((False, False, 18), (True, False, 19), (False, True, 18), (True, True, 19)):
with self.subTest(alpha=alpha, release=release):
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fingerprint(), [], alpha, release, False)
self.assertEqual(cp.safetyConfigs[-1].safetyParam, word)
self.assertEqual(cp.openpilotLongitudinalControl, word == 19)
self.assertEqual(cp.alternativeExperience, 0)
if release:
self.assertFalse(cp.alphaLongitudinalAvailable)
self.assertTrue(cp.alphaLongitudinalAvailable)
def test_secoc_unrelated_flags_and_ae_do_not_select_extension(self):
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fingerprint(secoc=True), [], False, False, False)
@@ -0,0 +1,113 @@
"""Mach-E display preference, warning priority, and ownership regressions."""
from dataclasses import replace
from pathlib import Path
from types import SimpleNamespace
import pytest
from opendbc.can import CANParser
from opendbc.car import structs
from opendbc.car.ford import fordcan
from opendbc.car.ford.tests.test_mache_ui_owner import setup, UI
from opendbc.car.ford.values import CAR
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.starpilot.car.ford.cluster_preferences import KEY, FordClusterPreference, read_choice
from openpilot.starpilot.controller_extensions import configure_controller
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
from openpilot.starpilot.ui.feature_settings_state import row_change
def status(owner, state, command, parser):
for _ in range(30):
_, frames = owner.update(command.as_reader(), state, owner.frame * 10_000_000)
for address, data, bus in frames:
if address == 0x18A:
parser.update([(owner.frame * 10_000_000, [(address, data, bus)])])
return parser.vl['ACCDATA_3']
raise AssertionError('No active cluster message')
def test_saved_opt_in_through_actual_controller_configuration_and_warning_priority():
with OpenpilotPrefix():
saved = Params()
owner, state = setup()
configure_controller(SimpleNamespace(CP=owner.CP, CC=owner), saved)
assert isinstance(owner.hands_free_cluster_input, FordClusterPreference)
assert not owner.hands_free_cluster_input.enabled
parser = CANParser('ford_lincoln_base_pt', [('ACCDATA_3', 5)], 0)
command = structs.CarControl(latActive=True)
state.acc_tja_status_stock_values['FcwAudioWarn_B_Rq'] = 1
assert status(owner, state, command, parser)['Tja_D_Stat'] == 2
Path(saved.get_param_path(KEY)).write_bytes(b'1')
for _ in range(101):
owner.update(command.as_reader(), state, owner.frame * 10_000_000)
assert status(owner, state, command, parser)['Tja_D_Stat'] == 7
command.hudControl.leftLaneDepart = True
assert status(owner, state, command, parser)['Tja_D_Stat'] == 3
command.hudControl.leftLaneDepart = False
command.hudControl.rightLaneDepart = True
assert status(owner, state, command, parser)['Tja_D_Stat'] == 4
command.hudControl.rightLaneDepart = False
fields = status(owner, state, command, parser)
assert fields['FcwAudioWarn_B_Rq'] == 1
command.hudControl.visualAlert = structs.CarControl.HUDControl.VisualAlert.fcw
assert status(owner, state, command, parser)['FcwVisblWarn_B_Rq'] == 1
command.latActive = False
for _ in range(110):
_, frames = owner.update(command.as_reader(), state, owner.frame * 10_000_000)
assert not UI.intersection(address for address, _, _ in frames)
Path(saved.get_param_path(KEY)).write_bytes(b'0')
command.latActive = True
for _ in range(101):
owner.update(command.as_reader(), state, owner.frame * 10_000_000)
assert status(owner, state, command, parser)['Tja_D_Stat'] == 2
@pytest.mark.parametrize('raw', [None, b'0', b'1', b'yes', b'1\n', b'', b'1' * 20])
def test_preference_only_accepts_exact_dom_opt_in(raw):
with OpenpilotPrefix():
saved = Params()
if raw is not None:
Path(saved.get_param_path(KEY)).write_bytes(raw)
enabled, observed, _, _ = read_choice(saved)
assert enabled == (raw == b'1')
assert observed == (b'' if raw is not None and len(raw) > 8 else raw)
@pytest.mark.parametrize('enabled,main,left,right,expected', [
(True, True, False, False, 7), (True, True, True, False, 3), (True, True, False, True, 4),
(True, True, True, True, 3), (False, True, False, False, 1),
(False, True, True, False, 5), (False, True, False, True, 6), (False, False, False, False, 0),
])
def test_hands_free_flag_does_not_override_departure_standby_or_off(enabled, main, left, right, expected):
owner, state = setup()
hud = structs.CarControl.HUDControl(leftLaneDepart=left, rightLaneDepart=right)
frame = fordcan.create_acc_ui_msg(owner.packer, owner.CAN, owner.CP, main, enabled, False, False, False,
hud, state.acc_tja_status_stock_values, True)
parser = CANParser('ford_lincoln_base_pt', [('ACCDATA_3', 5)], 0)
parser.update([(1, [frame])])
assert parser.vl['ACCDATA_3']['Tja_D_Stat'] == expected
def test_shared_feature_ui_saves_opt_in_and_rejects_onroad_stale_or_wrong_vehicle():
with OpenpilotPrefix():
saved = Params()
controller, _ = setup()
parked = [True]
owner = FeatureSettingsOwner(saved, lambda group: parked[0],
vehicle_params=lambda: controller.CP,
vehicle_fingerprint=lambda: str(controller.CP.carFingerprint))
row = next(r for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY)
assert row.value == 'Off' and row.available
request = row_change(row, 1)
assert request is not None
parked[0] = False
assert not owner.apply(request)
parked[0] = True
assert not owner.apply(replace(request, vehicle_fingerprint=str(CAR.FORD_F_150_MK14)))
assert owner.apply(request)
assert read_choice(saved)[0] == 1
assert not owner.apply(request)
row = next(r for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY)
assert row.value == 'On'
@@ -0,0 +1,157 @@
"""Recorded Mach-E turn inputs retain Dom display state across steering handoff."""
from pathlib import Path
from types import SimpleNamespace
import pytest
from opendbc.can import CANParser
from opendbc.car import gen_empty_fingerprint, structs
from opendbc.car.ford.carcontroller import CarController
from opendbc.car.ford.carstate import CarState
from opendbc.car.ford.interface import CarInterface
from opendbc.car.ford.values import CAR, DBC
from opendbc.safety.tests.libsafety import libsafety_py
from opendbc.safety.tests import test_ford_aol
from openpilot.common.params import Params, ParamKeyFlag, ParamKeyType
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.starpilot.car.ford.cluster_preferences import FordClusterPreference, KEY
class RecordedPreview:
def __init__(self):
self.model = SimpleNamespace(orientationRate=SimpleNamespace(z=[0.] * 33),
meta=SimpleNamespace(laneChangeState=0, laneChangeDirection=0))
def update(self):
pass
def apply_blend_settings(self, controller):
controller.mache_lateral.set_blend_settings(.4, .4, .85)
def lateral_snapshot(self, speed):
return self.model, ModelConstants.T_IDXS, .2, True
def assist_permission(self):
return (), False
def run_turn(*, hands_free, angle, torque, desired, speed, yaw):
native = test_ford_aol.TestFordAolDriverIntent()
native.setUp()
native.reset(18)
native.arm()
with OpenpilotPrefix():
saved = Params()
Path(saved.get_param_path(KEY)).write_bytes(b"1" if hands_free else b"0")
fingerprint = gen_empty_fingerprint()
fingerprint[0].update({0x5A: 8, 0x3A6: 8, 0x3A7: 8})
fingerprint[2].update({0x3D6: 8, 0x186: 8})
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fingerprint, [], False, False, False)
cp.alternativeExperience = 32
assert cp.safetyConfigs[-1].safetyParam == 18
assert cp.flags == 3
controller = CarController(DBC[cp.carFingerprint], cp)
controller.hands_free_cluster_input = FordClusterPreference(saved)
controller.manual_turn_inputs = RecordedPreview()
state = CarState(cp)
state.update(state.get_can_parsers(cp))
cs = structs.CarState(canValid=True, vEgo=speed, vEgoRaw=speed, yawRate=yaw,
gearShifter=structs.CarState.GearShifter.drive)
cs.cruiseState.available = True
command = structs.CarControl(latActive=False)
command.actuators.curvature = desired
state.acc_tja_status_stock_values['FcwVisblWarn_B_Rq'] = 1
state.acc_tja_status_stock_values['FcwAudioWarn_B_Rq'] = 1
parser = CANParser('ford_lincoln_base_pt', [('ACCDATA_3', 5)], 0)
steering = []
displays = []
def step():
native.pump(1, mask=int(command.latActive), speed=speed, curvature=yaw / speed)
state.out = cs.as_reader()
_, frames = controller.update(command.as_reader(), state, (1_000_000 + native.tick * 10_000) * 1000)
for address, data, bus in frames:
assert native.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
if address == 0x3D6:
steering.append(bytes(data))
if address == 0x18A:
parser.update([(native.tick * 10_000_000, [(address, data, bus)])])
displays.append(dict(parser.vl['ACCDATA_3']))
return frames
# Startup remains stock until an active replacement is actually accepted.
for _ in range(25):
assert not {address for address, _, _ in step()} & {0x18A, 0x3D8}
assert native.safety.safety_fwd_hook(2, 0x18A) == 0
native.arm()
command.latActive = True
for _ in range(60):
step()
assert displays and displays[-1]['Tja_D_Stat'] == (7 if hands_free else 2)
assert native.safety.aol_get_permission_mask() == 1
assert native.safety.safety_fwd_hook(2, 0x18A) == -1
# Both original onset tuples have driver input, a right signal and >12deg.
cs.steeringPressed = True
cs.steeringAngleDeg = angle
cs.steeringTorque = torque
cs.rightBlinker = True
count = len(displays)
for _ in range(40):
frames = step()
if any(address == 0x3D6 for address, _, _ in frames):
assert ((steering[-1][0] >> 4) & 7) == 0
assert native.safety.safety_fwd_hook(2, 0x3D6) == 0
assert native.safety.safety_fwd_hook(2, 0x18A) == -1
assert native.safety.safety_fwd_hook(2, 0x3D8) == -1
assert native.safety.aol_get_permission_mask() == 1
assert len(displays) > count
for display in displays[count:]:
assert display['Tja_D_Stat'] == (7 if hands_free else 2)
assert display['FcwVisblWarn_B_Rq'] == 1
assert display['FcwAudioWarn_B_Rq'] == 1
cs.steeringPressed = False
cs.steeringAngleDeg = 0.
cs.steeringTorque = 0.
cs.rightBlinker = False
for _ in range(50):
step()
assert ((steering[-1][0] >> 4) & 7) == 1
assert displays[-1]['Tja_D_Stat'] == (7 if hands_free else 2)
command.latActive = False
for _ in range(25):
assert not {address for address, _, _ in step()} & {0x18A, 0x3D8}
assert native.safety.safety_fwd_hook(2, 0x18A) == 0
assert native.safety.safety_fwd_hook(2, 0x3D8) == 0
assert native.safety.aol_get_permission_mask() == 0
return steering
@pytest.mark.parametrize('angle,torque,desired,speed,yaw', [
(-12.2, -1.6875, .000041086183, 2.55, -.006),
(-38.7, -2., -.000324735534, 2.563888788, -.033),
])
def test_recorded_turn_keeps_dom_display_and_hands_free_does_not_change_steering(angle, torque, desired, speed, yaw):
hands_free = run_turn(hands_free=True, angle=angle, torque=torque, desired=desired, speed=speed, yaw=yaw)
plain = run_turn(hands_free=False, angle=angle, torque=torque, desired=desired, speed=speed, yaw=yaw)
assert plain == hands_free
@pytest.mark.parametrize('saved', [False, True])
@pytest.mark.parametrize('flag', [ParamKeyFlag.CLEAR_ON_MANAGER_START, ParamKeyFlag.CLEAR_ON_ONROAD_TRANSITION,
ParamKeyFlag.CLEAR_ON_OFFROAD_TRANSITION, ParamKeyFlag.CLEAR_ON_IGNITION_ON])
def test_cluster_preference_survives_native_cleanup(tmp_path, saved, flag):
params = Params(str(tmp_path))
key = 'FordHandsFreeCluster'
assert params.check_key(key) and params.get_type(key) == ParamKeyType.BOOL
params.put(key, saved, block=True)
sentinel = Path(params.get_param_path('FordUnregisteredSentinel'))
sentinel.write_bytes(b'1')
params.clear_all(flag)
assert params.get(key) == saved and not sentinel.exists()
reopened = Params(str(tmp_path))
assert reopened.get(key) == saved
assert Path(reopened.get_param_path(key)).read_bytes() == (b'1' if saved else b'0')
assert FordClusterPreference(reopened).enabled == saved
@@ -0,0 +1,60 @@
"""Mach-E AOL must not inject UI while native safety forwards stock UI."""
import pytest
from opendbc.car import gen_empty_fingerprint, structs
from opendbc.car.ford.carcontroller import CarController
from opendbc.car.ford.carstate import CarState
from opendbc.car.ford.interface import CarInterface
from opendbc.car.ford.values import CAR, DBC
from openpilot.common.prefix import OpenpilotPrefix
UI = {0x3D8, 0x18A}
def setup(*, aol=True, identity=CAR.FORD_MUSTANG_MACH_E_MK1):
fingerprint = gen_empty_fingerprint()
fingerprint[0][0x5A] = 8
fingerprint[2].update({0x3D6: 8, 0x186: 8})
with OpenpilotPrefix():
cp = CarInterface.get_params(identity, fingerprint, [], False, False, False)
controller = CarController(DBC[cp.carFingerprint], cp)
# Card finalizes AOL qualification after the controller is constructed.
cp.alternativeExperience = 32 if aol else 0
state = CarState(cp)
state.update(state.get_can_parsers(cp))
cs = structs.CarState(canValid=True, vEgo=15., vEgoRaw=15., gearShifter=structs.CarState.GearShifter.drive)
cs.cruiseState.available = True
state.out = cs.as_reader()
return controller, state
def test_mache_idle_leaves_stock_display_and_fcw_untouched_then_owns_active_ui():
owner, state = setup()
command = structs.CarControl()
# Real stock UI/FCW fields remain available to be forwarded by native safety.
state.acc_tja_status_stock_values['FcwVisblWarn_B_Rq'] = 1
state.acc_tja_status_stock_values['Tja_D_Stat'] = 1
for _ in range(110):
_, frames = owner.update(command.as_reader(), state, owner.frame * 10_000_000)
assert not UI.intersection(address for address, _, _ in frames)
command.latActive = True
active_ui = set()
for _ in range(110):
_, frames = owner.update(command.as_reader(), state, owner.frame * 10_000_000)
active_ui.update(address for address, _, _ in frames if address in UI)
assert active_ui == UI
command.latActive = False
for _ in range(110):
_, frames = owner.update(command.as_reader(), state, owner.frame * 10_000_000)
assert not UI.intersection(address for address, _, _ in frames)
assert state.acc_tja_status_stock_values['FcwVisblWarn_B_Rq'] == 1
@pytest.mark.parametrize('identity,aol', [(CAR.FORD_MUSTANG_MACH_E_MK1, False), (CAR.FORD_F_150_MK14, True)])
def test_other_display_ownership_contracts_keep_existing_idle_ui(identity, aol):
owner, state = setup(aol=aol, identity=identity)
seen = set()
for _ in range(110):
_, frames = owner.update(structs.CarControl().as_reader(), state, owner.frame * 10_000_000)
seen.update(address for address, _, _ in frames if address in UI)
assert seen == UI
@@ -28,7 +28,7 @@ class TestNewPortCurvature(unittest.TestCase):
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))
expected_long = bool(alpha)
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))
@@ -156,6 +156,7 @@ class TestNewPortCurvature(unittest.TestCase):
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()))
@@ -20,7 +20,7 @@ def test_actual_card_publication_reaches_ford_extensions(identity, alpha, releas
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.car.card import Car, alpha_long_requested
from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.starpilot.controller_extensions import ManualTurnInputs
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
@@ -36,7 +36,7 @@ def test_actual_card_publication_reaches_ford_extensions(identity, alpha, releas
saved.put_bool('IsReleaseBranch', release, block=True)
saved.put_bool('DisableOpenpilotLongitudinal', disable, block=True)
saved.put_bool('FordHumanTurnDetection', False, block=True)
cp = params(identity, alpha_long_requested(saved, is_release=release), release)
cp = params(identity, alpha, release)
cp.carVin = '0' * 17
cp.carFw = []
expected = cp.to_dict()
@@ -1,4 +1,6 @@
"""Original switch context, state event and reached controller regressions."""
import pytest
from opendbc.can import CANPacker
from opendbc.car import Bus, structs, gen_empty_fingerprint
from opendbc.car.ford.interface import CarInterface
@@ -39,6 +41,27 @@ def test_actual_parser_cancel_edges_are_op_long_only_and_contextual():
assert cancel == (expected if op_long else [])
@pytest.mark.parametrize('vehicle', [CAR.FORD_ESCAPE_MK4, CAR.FORD_MUSTANG_MACH_E_MK1])
@pytest.mark.parametrize('op_long', [False, True])
@pytest.mark.parametrize('signal,button', [
('CcAslButtnSetIncPress', 'accelCruise'), ('CcAslButtnResIncPress', 'accelCruise'),
('CcAslButtnIndxIncPress', 'accelCruise'), ('CcAslButtnSetDecPress', 'decelCruise'),
('CcAslButtnResDecPress', 'decelCruise'), ('CcAslButtnIndxDecPress', 'decelCruise'),
])
def test_directional_cruise_edges_reach_software_cruise_only_with_op_long(vehicle, op_long, signal, button):
cp = params(vehicle, alpha=op_long)
state = CarState(cp)
parsers = state.get_can_parsers(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
for i, (pressed, expected) in enumerate(((0, []), (1, [True]), (1, []), (0, [False]))):
parsers[Bus.pt].update([(1_000_000_000 + i * 10_000_000, [
packer.make_can_msg('Steering_Data_FD1', parsers[Bus.pt].bus, {signal: pressed}),
])])
out = state.update(parsers)
edges = [event.pressed for event in out.buttonEvents if event.type == getattr(structs.CarState.ButtonEvent.Type, button)]
assert edges == (expected if op_long else [])
def test_reached_controller_preserves_precedence_cadence_and_stock_bits():
cp = params(CAR.FORD_MUSTANG_MACH_E_MK1, alpha=False)
controller = CarController(DBC[cp.carFingerprint], cp)
@@ -62,7 +62,7 @@ class TestFordThreePorts(unittest.TestCase):
self.assertTrue(requested.openpilotLongitudinalControl)
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.assertTrue(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)
@@ -0,0 +1,39 @@
"""Transit CAN angle quantization must stay within the native encoded envelope."""
import pytest
from opendbc.car import gen_empty_fingerprint
from opendbc.car.ford.fordcan import CanBus, create_lka_msg
from opendbc.safety.tests.libsafety import libsafety_py
from opendbc.safety.tests.test_ford_aol import TestFordAolDriverIntent as _Native
@pytest.mark.parametrize('word', (12, 13))
@pytest.mark.parametrize('sign,endpoint', ((1, 4072), (-1, 24)))
def test_saturated_angle_with_physical_cruise_stays_inside_native_envelope(sign, endpoint, word):
native = _Native(methodName='runTest')
native.setUp()
try:
native.reset(word)
native.pump(40, main=3)
native.pump(10, main=4, mask=1)
assert native.safety.get_controls_allowed()
for angle in (1., 2., 3., 4., 5., 5.8, 6.):
native.pump(3, main=4, mask=1)
address, data, bus = create_lka_msg(native.packer, CanBus(fingerprint=gen_empty_fingerprint()), True, sign * angle,
2 if sign > 0 else 4, 0., transit=True)
raw_angle = ((data[2] & 15) << 8) | data[3]
assert 24 <= raw_angle <= 4072
if angle >= 5.8:
assert raw_angle == endpoint
assert native.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
# The adjacent encoded values remain prohibited by unchanged native safety.
rejected_angle = 4073 if sign > 0 else 23
rejected = bytearray(data)
rejected[2] = (rejected[2] & 0xF0) | (rejected_angle >> 8)
rejected[3] = rejected_angle & 0xFF
assert not native.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, rejected))
address, data, bus = create_lka_msg(native.packer, CanBus(fingerprint=gen_empty_fingerprint()), False, sign * 6., transit=True)
assert data[0] >> 5 == 0
assert native.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
finally:
native.tearDown()
+4 -3
View File
@@ -48,6 +48,7 @@ class FordSafetyFlags(IntFlag):
MACH_E_EXTENDED = 16
CLASSIC_EXTENDED = 32
EXPLORER_EXTENDED = CLASSIC_EXTENDED # Existing public profile spelling.
MANUAL = 256
BLUEPILOT_ANGLE = 128
GENERIC_CANFD_EXTENDED = 64
@@ -328,12 +329,12 @@ FW_QUERY_CONFIG = FwQueryConfig(
whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire],
logging=True,
),
Request(
*[Request(
[StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST],
[StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE],
whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire],
bus=0,
),
bus=bus,
) for bus in (0, 4)],
*[Request(
[StdQueries.TESTER_PRESENT_REQUEST, ford_asbuilt_block_request(block_id)],
[StdQueries.TESTER_PRESENT_RESPONSE, ford_asbuilt_block_response(block_id)],
+29 -2
View File
@@ -1,6 +1,9 @@
"""Independent lateral axes for exact source-qualified Bolt and Volt owners."""
import math
from functools import lru_cache
from capnp.lib.capnp import _DynamicStructBuilder, _DynamicStructReader
from opendbc.car.gm.cancel_remap import aol_enabled, valid_alternative_experience
from opendbc.car.structs import CarParams
@@ -35,11 +38,11 @@ GM_AOL_WORDS = (frozenset(range(0xE600, 0xE608)) | frozenset(range(0xE610, 0xE61
0xC1D1, 0xC1D3,
0xC110, 0xC111, 0xC120, 0xC121, 0xC130, 0xC131, 0xC140, 0xC141,
0xE800, 0xE801, 0xE802, 0xE803, 0xE804, 0xE805,
0xC150, 0xC151, 0xC160, 0xC161, 0xC162, 0xC170, 0xC171, 0xC172, 0xC173, 0xC180, 0xC181, 0xC182, 0xC183, 0xC184, 0xC185, 0xC186, 0xC187,
0xC150, 0xC151, 0xC160, 0xC161, 0xC162, 0xC163, 0xC170, 0xC171, 0xC172, 0xC173, 0xC180, 0xC181, 0xC182, 0xC183, 0xC184, 0xC185, 0xC186, 0xC187,
))
def qualified_gm(cp) -> bool:
def _qualified_gm_uncached(cp) -> bool:
try:
if (cp.brand != 'gm' or cp.notCar or cp.passive or cp.dashcamOnly or
cp.steerControlType != CarParams.SteerControlType.torque or
@@ -94,6 +97,30 @@ def qualified_gm(cp) -> bool:
return False
# Qualification depends only on the exact finalized CarParams value. Cache no
# live vehicle state, native grant, source timestamp, or output eligibility.
@lru_cache(maxsize=8)
def _qualified_gm_bytes(key: bytes) -> bool:
with CarParams.from_bytes(key) as cp:
return _qualified_gm_uncached(cp)
def qualified_gm(cp) -> bool:
cp_type = type(cp)
if cp_type is _DynamicStructBuilder or cp_type is _DynamicStructReader:
try:
if cp.schema == CarParams.schema:
if cp.brand != 'gm':
return False
snapshot = cp.copy() if cp_type is _DynamicStructBuilder else cp.as_builder()
key = snapshot.to_bytes()
if len(key) <= 64 * 1024:
return _qualified_gm_bytes(key)
except Exception:
pass
return _qualified_gm_uncached(cp)
def lateral_request(cp, cc) -> bool:
"""Only a finalized independent-axis owner may relax its cruise-active lateral gate."""
return (qualified_gm(cp) and aol_enabled(cp) and cc.latActive)
+29 -10
View File
@@ -1,4 +1,4 @@
"""Mode-transition output shaping for admitted Bolt longitudinal owners."""
"""Mode-transition output shaping for admitted GM longitudinal owners."""
class BoltModeTransition:
@@ -37,7 +37,7 @@ class BoltModeTransition:
if output < 0.0 and output < last_output:
progress = min(1.0, self.timer / self.duration)
urgency = abs(output / -4.0)
urgency_smooth = min(1.0, urgency ** 0.4)
urgency_smooth = min(1.0, urgency**0.4)
blend = 1.0 - (1.0 - progress) * (1.0 - urgency_smooth)
output = last_output + (output - last_output) * blend
return output
@@ -45,16 +45,32 @@ class BoltModeTransition:
def policy_for(cp):
from opendbc.car.structs import CarParams
from opendbc.car.gm.values import (CAR, GMFlags, GMSafetyFlags, is_bolt_cc_profile, is_bolt_euv_longitudinal,
is_ordinary_cc_profile, is_ordinary_camera_profile, is_conventional_cc_pedal_profile)
from opendbc.car.gm.values import (
CAR,
GMFlags,
GMSafetyFlags,
is_bolt_cc_profile,
is_bolt_euv_longitudinal,
is_ordinary_cc_profile,
is_ordinary_camera_profile,
is_conventional_cc_pedal_profile,
is_volt_longitudinal,
)
from opendbc.car.gm.longitudinal import policy_for as pedal_policy_for
if not cp.openpilotLongitudinalControl:
return None
if (is_conventional_cc_pedal_profile(cp) or is_ordinary_camera_profile(cp, longitudinal=True) or
is_ordinary_cc_profile(cp) or is_bolt_cc_profile(cp) or is_bolt_euv_longitudinal(cp)):
if (
is_volt_longitudinal(cp)
or is_conventional_cc_pedal_profile(cp)
or is_ordinary_camera_profile(cp, longitudinal=True)
or is_ordinary_cc_profile(cp)
or is_bolt_cc_profile(cp)
or is_bolt_euv_longitudinal(cp)
):
return BoltModeTransition()
from opendbc.car.gm.values import gm_control_word
base = GMSafetyFlags.HW_CAM | GMSafetyFlags.EV | GMSafetyFlags.PEDAL_LONG | GMSafetyFlags.PADDLE_SCHED
words = {
CAR.CHEVROLET_BOLT_CC_2017: base | GMSafetyFlags.NO_ACC | GMSafetyFlags.BOLT_2017,
@@ -62,9 +78,12 @@ def policy_for(cp):
CAR.CHEVROLET_BOLT_CC_2022_2023: base | GMSafetyFlags.NO_ACC | GMSafetyFlags.BOLT_GEN2,
CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL: base | GMSafetyFlags.BOLT_ACC_PEDAL | GMSafetyFlags.BOLT_GEN2,
}
if (pedal_policy_for(cp) is not None and not cp.flags & GMFlags.CC_LONG.value and
len(cp.safetyConfigs) == 1 and
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and
gm_control_word(cp) == words.get(cp.carFingerprint)):
if (
pedal_policy_for(cp) is not None
and not cp.flags & GMFlags.CC_LONG.value
and len(cp.safetyConfigs) == 1
and cp.networkLocation == CarParams.NetworkLocation.fwdCamera
and gm_control_word(cp) == words.get(cp.carFingerprint)
):
return BoltModeTransition()
return None
+11 -2
View File
@@ -1,4 +1,5 @@
from opendbc.car.gm.one_pedal import VoltOnePedal
from opendbc.car.gm.steering_authority import EPS_STATUS_TIMEOUT_NS
from opendbc.car.gm.values import gm_control_word, is_volt_one_pedal, camera_acc_pedal_profile, volt_cc_pedal_profile
from opendbc.car.gm.longitudinal import volt_sng_release
from opendbc.car.gm.auto_hold import AutoHold, config_for as auto_hold_config_for, hold_brake as estimate_hold_brake
@@ -575,7 +576,15 @@ class CarController(CarControllerBase):
if self.CP.openpilotLongitudinalControl:
stock_steer_ready = (stock_steer_ready and CS.pedal_sensor_healthy and
0 < CS.pedal_sensor_ts_nanos <= now_nanos <= CS.pedal_sensor_ts_nanos + PEDAL_SENSOR_TIMEOUT_NS)
lat_active = CC.latActive and stock_steer_ready and not CS.steering_authority.latched
# An SDGM EPS can report inactive while the driver holds the brake. Emit
# neutral for that measured pause so releasing the brake starts a new request.
# Keep the existing acquisition deadline and genuine refusal latch intact.
sdgm_brake_pause = False
if self.volt_sdgm_long and CS.out.brakePressed:
steering = CS.steering_authority
sdgm_brake_pause = (steering.enabled and steering.status == 0 and
0 < steering.status_ns <= now_nanos <= steering.status_ns + EPS_STATUS_TIMEOUT_NS)
lat_active = CC.latActive and stock_steer_ready and not CS.steering_authority.latched and not sdgm_brake_pause
# Steering (Active: 50Hz, inactive: 10Hz)
steer_step = self.params.STEER_STEP if lat_active else self.params.INACTIVE_STEER_STEP
@@ -1200,7 +1209,7 @@ class CarController(CarControllerBase):
self.bolt_removed_cancel_credit_used = CS.conventional_cancel_credit.credit_ns
can_sends.append(gmcan.create_buttons(self.packer_pt, CanBus.POWERTRAIN, (CS.buttons_counter + 1) % 4, CruiseButtons.CANCEL))
if self.CP.networkLocation == NetworkLocation.fwdCamera and not self.bolt_cc_profile:
if self.CP.networkLocation == NetworkLocation.fwdCamera and not self.bolt_cc_profile and not self.volt_camera_removed:
# Silence "Take Steering" alert sent by camera, forward PSCMStatus with HandsOffSWlDetectionStatus=1
if self.frame % 10 == 0:
can_sends.append(gmcan.create_pscm_status(self.packer_pt, CanBus.CAMERA, CS.pscm_status))
+13 -3
View File
@@ -17,7 +17,8 @@ from opendbc.car.gm.conventional_pedal import CancelCredit
from opendbc.car.gm.values import (DBC, AccState, CruiseButtons, STEER_THRESHOLD, SDGM_CAR, ALT_ACCS,
ASCM_INTERCEPT_CAR, ORDINARY_ASCM_CAR, ORDINARY_SDGM_CAR, GMFlags, GMSafetyFlags, NO_ACC_BOLT_CAR,
CC_GATEWAY_STOCK_CAR, requires_camera_state_sources, is_conventional_cc_pedal_profile, is_silverado_cc_pedal_profile,
is_volt_cc_profile, is_silverado_cc_stock_profile, is_ordinary_cc_profile, is_malibu_cc_f1_profile, VOLT_BSM_CAR, CAR,
is_volt_cc_profile, is_volt_cc_f1_profile, is_silverado_cc_stock_profile, is_ordinary_cc_profile,
is_malibu_cc_f1_profile, VOLT_BSM_CAR, CAR,
is_volt_gateway_profile, is_volt_gateway_alternate_brake, is_bolt_cc_profile, BOLT_CC_WORDS,
is_bolt_pedal_profile, is_bolt_pedal_removed_profile, is_bolt_present_no_acc_pedal_profile, is_volt_camera_removed,
is_ordinary_camera_profile, is_ordinary_camera_removed)
@@ -268,7 +269,7 @@ class CarState(CarStateBase):
pt_cp.ts_nanos["ECMCruiseControl"]["CruiseActive"],
pt_cp.ts_nanos["ASCMSteeringButton"]["RollingCounter"],
pt_cp.ts_nanos["ECMEngineStatus"]["CruiseMainOn"],
(pt_cp.ts_nanos["EBCMBrakePedalPosition"]["BrakePedalPosition"] if is_malibu_cc_f1_profile(self.CP) else
(pt_cp.ts_nanos["EBCMBrakePedalPosition"]["BrakePedalPosition"] if is_malibu_cc_f1_profile(self.CP) or is_volt_cc_f1_profile(self.CP) else
pt_cp.ts_nanos["ECMAcceleratorPos"]["BrakePedalPos"]),
pt_cp.ts_nanos["ECMPRDNL2"]["PRNDL2"],
pt_cp.ts_nanos["AcceleratorPedal2"]["AcceleratorPedal2"])
@@ -405,7 +406,10 @@ class CarState(CarStateBase):
source_be_brake = (gm_control_word(self.CP) &
(GMSafetyFlags.ASCM_INTERCEPT | GMSafetyFlags.SDGM).value and
not gm_control_word(self.CP) & GMSafetyFlags.BRAKE_C9.value)
if is_malibu_cc_f1_profile(self.CP):
if is_volt_cc_f1_profile(self.CP):
ret.brakePressed = (pt_cp.vl["EBCMBrakePedalPosition"]["BrakePedalPosition"] >= 6 or
pt_cp.vl["ECMEngineStatus"]["BrakePressed"] != 0)
elif is_malibu_cc_f1_profile(self.CP):
ret.brakePressed = (pt_cp.vl["EBCMBrakePedalPosition"]["BrakePedalPosition"] >= 21 or
pt_cp.vl["EBCMBrakePedalPosition"]["BrakePressed"] != 0 or
pt_cp.vl["ECMEngineStatus"]["BrakePressed"] != 0)
@@ -510,6 +514,9 @@ class CarState(CarStateBase):
pt_cp.vl["EBCMFrictionBrakeStatus"]["FrictionBrakeUnavailable"] == 1)
ret.cruiseState.enabled = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] != AccState.OFF
if is_volt_camera_removed(self.CP):
# A missing camera can leave the ECU faulted; that is not engaged cruise.
ret.cruiseState.enabled &= pt_cp.vl["AcceleratorPedal2"]["CruiseState"] != AccState.FAULTED
ret.cruiseState.standstill = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.STANDSTILL
if (self.CP.carFingerprint in CC_GATEWAY_STOCK_CAR or self.CP.carFingerprint == CAR.CHEVROLET_VOLT_CC):
# AcceleratorPedal2 CruiseState is an ACC state, not a conventional-cruise fault source.
@@ -761,6 +768,9 @@ class CarState(CarStateBase):
if CP.carFingerprint == CAR.CHEVROLET_VOLT_CC:
pt_messages.append(("EBCMRegenPaddle", 50))
if is_volt_cc_f1_profile(CP):
pt_messages = [(name, frequency) for name, frequency in pt_messages if name != "ECMAcceleratorPos"]
pt_messages.append(("EBCMBrakePedalPosition", 100))
if is_bolt_cc_profile(CP):
pt_messages += [("PSCMStatus", 10), ("ECMCruiseControl", 10), ("ASCMSteeringButton", 33),
+4 -1
View File
@@ -20,4 +20,7 @@ def automatic_volt_candidate(candidate, fingerprint, candidates):
any(pt.get(address) != size for address, size in required.items()) or
any(address in pt for address in (0xBE, 0x201, 0x2FF, 0x320))):
return candidate
return CAR.CHEVROLET_VOLT_CAMERA
# The recorded car has ordinary cruise, no ACC and no camera. The absent
# camera is not evidence of an ACC actuator owner. Read actual ECM cruise
# activity and retain button-only control; explicit ACC selections are above.
return CAR.CHEVROLET_VOLT_CC
+14 -5
View File
@@ -16,7 +16,7 @@ from opendbc.car.gm.values import (volt_cc_pedal_profile, CAR, CarControllerPara
ORDINARY_CC_CAR, ORDINARY_CC_WORD, SILVERADO_CC_PEDAL_WORDS,
is_silverado_cc_pedal_profile, is_conventional_cc_pedal_profile,
CAMERA_STOCK_CAR, ORDINARY_CAMERA_CAR, ORDINARY_CAMERA_ALPHA_CAR, camera_acc_pedal_profile,
VOLT_BSM_CAR, BOLT_CC_WORDS, is_bolt_cc_profile, BOLT_PEDAL_REMOVED_WORDS,
VOLT_BSM_CAR, VOLT_CC_F1_WORD, BOLT_CC_WORDS, is_bolt_cc_profile, BOLT_PEDAL_REMOVED_WORDS,
is_bolt_pedal_removed_profile, is_bolt_present_no_acc_pedal_profile)
from opendbc.car.gm.values import (malibu_hybrid_profile, MALIBU_HYBRID_SOURCES,
is_volt_sdgm_accepted_envelope, volt_sdgm_accepted_accel_max)
@@ -229,14 +229,23 @@ class CarInterface(CarInterfaceBase):
if candidate == CAR.CHEVROLET_VOLT_CC:
# Require the complete source layout for either automatic or manual identity.
required = {0xbe: 6, 0x3d1: 8, 0xc9: 8, 0x1e1: 7, 0x1f5: 8, 0x34a: 5, 0x1c4: 8, 0xbd: 7}
admitted = not is_release and all(fingerprint[CanBus.POWERTRAIN].get(addr) == size for addr, size in required.items())
pt = fingerprint[CanBus.POWERTRAIN]
f1 = 0xBE not in pt and pt.get(0xF1) == 6
required = {0xF1 if f1 else 0xBE: 6, 0x3d1: 8, 0xc9: 8, 0x1e1: 7, 0x1f5: 8, 0x34a: 5, 0x1c4: 8, 0xbd: 7}
if f1:
# Admit the recorded no-ACC source graph, never a broken pedal/camera graph.
required.update({0x184: 8, 0xD3: 2, 0x130: 1, 0x17D: 6, 0x210: 4, 0x236: 5})
f1_layout = (not any(address in pt for address in (0x201, 0x2FF, 0x320)) and
not any(address in fingerprint[CanBus.CAMERA] for address in (0x180, 0x320, 0x370)) and
not fingerprint[CanBus.OBSTACLE])
admitted = (not is_release and (not f1 or f1_layout) and
all(fingerprint[CanBus.POWERTRAIN].get(addr) == size for addr, size in required.items()))
ret.networkLocation = NetworkLocation.gateway
ret.radarUnavailable = True
ret.alphaLongitudinalAvailable = False
ret.openpilotLongitudinalControl = admitted
ret.pcmCruise = not admitted
ret.flags = GMFlags.CC_LONG.value if admitted else 0
ret.flags = int(GMFlags.CC_LONG | (GMFlags.NO_ACCELERATOR_POS_MSG if f1 else 0)) if admitted else 0
ret.minEnableSpeed = 24 * CV.MPH_TO_MS
ret.minSteerSpeed = 7 * CV.MPH_TO_MS
ret.longitudinalActuatorDelay = 1.0
@@ -245,7 +254,7 @@ class CarInterface(CarInterfaceBase):
if not admitted:
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.noOutput)]
else:
ret.safetyConfigs[0].safetyParam = int(GMSafetyFlags.EV | GMSafetyFlags.NO_ACC)
ret.safetyConfigs[0].safetyParam = VOLT_CC_F1_WORD if f1 else int(GMSafetyFlags.EV | GMSafetyFlags.NO_ACC)
elif candidate in CC_GATEWAY_STOCK_CAR:
# Start with stock PCM ownership; qualified button-control owners override below.
@@ -1,5 +1,5 @@
from opendbc.car import structs
from opendbc.car.gm.values import GMFlags, camera_acc_pedal_profile, is_volt_ascm_longitudinal
from opendbc.car.gm.values import GMFlags, camera_acc_pedal_profile, is_volt_ascm_longitudinal, is_volt_sdgm_accepted_envelope
# PSCMStatus is expected at 10 Hz. Three periods bound first acquisition and missing feedback.
EPS_STATUS_TIMEOUT_NS = 300_000_000
@@ -8,7 +8,8 @@ ButtonType = structs.CarState.ButtonEvent.Type
def monitored_profile(CP: structs.CarParams) -> bool:
return (is_volt_ascm_longitudinal(CP) and camera_acc_pedal_profile(CP) is None and
return ((is_volt_ascm_longitudinal(CP) or is_volt_sdgm_accepted_envelope(CP)) and
camera_acc_pedal_profile(CP) is None and
not CP.flags & GMFlags.PEDAL_LONG)
@@ -46,9 +46,9 @@ def fixture(identity, removed=False, metric=False, alpha=False, present=False):
return cp, ci, CANPacker(DBC[identity][Bus.pt])
def feed(ci, packer, now, counter=0, gas=False, active=True, speed=20, stock=15, brake=False, regen=False, camera=True):
def feed(ci, packer, now, counter=0, gas=False, active=True, speed=20, stock=15, brake=False, regen=False, camera=True, eps_status=0):
vals = {
'PSCMStatus': {},
'PSCMStatus': {'LKATorqueDeliveredStatus': eps_status},
'ECMCruiseControl': {'CruiseActive': int(active), 'CruiseSetSpeed': stock * 3.6},
'ECMEngineStatus': {'CruiseMainOn': 1, 'BrakePressed': int(brake)},
'AcceleratorPedal2': {'AcceleratorPedal2': 30 if gas else 0},
@@ -1032,13 +1032,16 @@ class TestBoltPaddleModes(unittest.TestCase):
for candidate in PEDAL_BOLT_CAR:
with self.subTest(candidate=candidate), OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1'}):
settings = Params()
settings.put_bool('AlwaysOnLateral', False, block=True)
card = TestGmAol.card(params(candidate, pedal=True, camera=True), settings)
ci, cp = card.CI, card.CP
self.assertIsNotNone(ci.CC.maneuver_paddle_input)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
safety = libsafety_py.libsafety
self.assertEqual(safety.set_safety_hooks(cp.safetyConfigs[0].safetyModel.raw, cp.safetyConfigs[0].safetyParam), 0)
# This ordinary pedal test must not inherit the previous test's AOL policy.
safety.init_tests()
safety.set_alternative_experience(cp.alternativeExperience)
self.assertEqual(safety.set_safety_hooks(cp.safetyConfigs[0].safetyModel.raw, cp.safetyConfigs[0].safetyParam), 0)
recorder = TestGmBoltPedalSafety()
recorder.safety = safety
control = structs.CarControl()
@@ -0,0 +1,83 @@
"""SDGM route26: reacquire EPS assistance after a measured brake pause."""
from types import SimpleNamespace
import pytest
from opendbc.can import CANPacker
from opendbc.car import Bus, structs
from opendbc.car.gm.interface import CarInterface
from opendbc.car.gm.tests.test_volt_camera_control import feed_camera
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
from opendbc.car.gm.values import DBC
def frame(ci, packer, tick, *, brake=False, status=1):
now = 1_000_000_000 + tick * 10_000_000
_, rows = feed_camera(SimpleNamespace(update=lambda _: None), packer, now,
counter=tick % 4, brake=brake, active=False, eps_status=status)
rows = [row for row in rows if row[0] != 0xBE and (tick % 10 == 0 or row[0] != 0x184)]
rows.append(packer.make_can_msg('ECMAcceleratorPos', 0, {'BrakePedalPos': 30 if brake else 0}))
cs = ci.update([(now, rows)])
control = structs.CarControl(enabled=False, latActive=True, longActive=False)
control.actuators.torque = .2
control.hudControl.setSpeed = 30.
_, sends = ci.apply(control.as_reader(), now)
steering = [data for address, data, bus in sends if address == 0x180 and bus == 0]
return cs, steering
@pytest.mark.parametrize('c9', [False, True])
@pytest.mark.parametrize('recovered', [False, True])
def test_sdgm_brake_pause_emits_neutral_then_reacquires_or_keeps_refusal(c9, recovered):
cp = sdgm_params(brake_c9=c9)
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
for tick in range(40):
frame(ci, packer, tick)
assert ci.CS.steering_authority.seen_active
emitted = []
for tick in range(40, 140):
cs, steering = frame(ci, packer, tick, brake=True, status=0)
emitted += steering
assert cs.brakePressed
assert not ci.CS.steering_authority.latched
assert emitted and all(not (data[0] & 8) and data[:2] in (b'\x00\x00', b'\x10\x00', b'\x20\x00', b'\x30\x00') for data in emitted)
emitted = []
for tick in range(140, 185):
cs, steering = frame(ci, packer, tick, status=1 if recovered and tick >= 150 else 0)
emitted += steering
assert any(data[0] & 8 for data in emitted)
assert ci.CS.steering_authority.latched == (not recovered)
assert cs.steerFaultTemporary == (not recovered)
if recovered:
assert ci.CS.steering_authority.seen_active
@pytest.mark.parametrize('status', [1, 2, 3])
def test_sdgm_active_feedback_and_factory_faults_are_not_brake_recovery(status):
cp = sdgm_params()
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
emitted = []
for tick in range(40):
cs, steering = frame(ci, packer, tick, brake=True, status=status)
emitted += steering
assert any(data[0] & 8 for data in emitted)
assert cs.steerFaultTemporary == (status == 2)
assert cs.steerFaultPermanent == (status == 3)
@pytest.mark.parametrize('alpha', [False, True])
@pytest.mark.parametrize('brake', [False, True])
def test_sdgm_brake_recovery_does_not_enable_unengaged_stock_control(alpha, brake):
cp = sdgm_params(alpha=alpha)
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
emitted = []
for tick in range(20):
_, steering = frame(ci, packer, tick, brake=brake, status=0)
if tick >= 10:
emitted += steering
assert emitted
# The stock ACC fixture is disengaged; this recovery never enables it.
assert all(bool(data[0] & 8) == (alpha and not brake) for data in emitted)
@@ -0,0 +1,44 @@
"""Recorded SDGM route18: a steering request is not proof of EPS assistance."""
import pytest
from opendbc.can import CANPacker
from opendbc.car import Bus
from opendbc.car.gm.interface import CarInterface
from opendbc.car.gm.tests import test_ascm_intercept as ascm_fixture
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
from opendbc.car.gm.values import DBC
@pytest.mark.parametrize("c9", [False, True])
def test_sdgm_long_refused_steering_is_not_silently_active(c9):
cp = sdgm_params(alpha=True, brake_c9=c9)
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
case = ascm_fixture.TestAscmIntercept(methodName="runTest")
for tick in range(40):
cs = case.authority_frame(ci, packer, tick, status=1)
for tick in range(40, 81):
cs = case.authority_frame(ci, packer, tick, status=0)
assert ci.CS.steering_authority.latched
assert cs.steerFaultTemporary
@pytest.mark.parametrize("alpha", [False, True])
@pytest.mark.parametrize("status,active,speed,driver", [(1, True, 8., 0.), (0, False, 8., 0.),
(0, True, 0., 0.), (0, True, 8., 3.), (2, True, 8., 0.), (3, True, 8., 0.)])
def test_sdgm_monitor_preserves_deliberate_pauses_and_factory_faults(alpha, status, active, speed, driver):
cp = sdgm_params(alpha=alpha)
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
case = ascm_fixture.TestAscmIntercept(methodName="runTest")
for tick in range(60):
cs = case.authority_frame(ci, packer, tick, status=status, active=active and alpha, speed=speed, driver=driver)
assert not ci.CS.steering_authority.latched
assert cs.steerFaultTemporary == (status == 2)
assert cs.steerFaultPermanent == (status == 3)
@pytest.mark.parametrize("c9", [False, True])
def test_sdgm_refusal_requires_physical_disable_neutral_and_fresh_rearm(monkeypatch, c9):
# Reuse the full original parser/controller physical sequence for this exact factory CP.
monkeypatch.setattr(ascm_fixture, "params", lambda *args, **kwargs: sdgm_params(brake_c9=c9, radar=True))
ascm_fixture.TestAscmIntercept(methodName="runTest").test_volt_steering_loss_requires_driver_rearm()
@@ -91,6 +91,13 @@ class TestVoltAlternateBrake(unittest.TestCase):
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
for tick, expected in enumerate(baseline['tx']):
# Keep every frozen vehicle-side byte. Only the removed camera's
# obsolete PSCM replacement is intentionally absent in this topology.
if is_volt_camera_removed(cp):
self.assertIn(name, ('removed-0', 'removed-1'))
camera_status = [message for message in expected if message[0] == 0x184 and message[2] == 2]
self.assertEqual(len(camera_status), int(tick % 10 == 0))
expected = [message for message in expected if not (message[0] == 0x184 and message[2] == 2)]
now = 1_000_000_000 + tick * 10_000_000
speed = 20. if tick < 40 or tick >= 100 else 0. if tick < 80 else 6.
active, brake, gas = tick < 60 or tick >= 90, 60 <= tick < 65, 65 <= tick < 70
@@ -1,4 +1,4 @@
"""Recorded camera-bypassed Volt discovery must retain distinct existing owners."""
"""Recorded no-camera/no-ACC Volt discovery must retain distinct existing owners."""
from contextlib import ExitStack
from copy import deepcopy
import json
@@ -33,20 +33,21 @@ def discover(fingerprint):
@pytest.mark.parametrize('alpha', [False, True])
@pytest.mark.parametrize('release', [False, True])
def test_recorded_collision_reaches_existing_camera_removed_owner(alpha, release):
def test_recorded_collision_reaches_conventional_cruise_owner(alpha, release):
fp = recorded_fingerprint()
with patch.object(car_helpers, 'automatic_volt_candidate', side_effect=lambda candidate, *_: candidate):
old, _ = discover(fp)
assert old is None # The legacy gateway and CC pedal signatures both survive.
selected, observed = discover(fp)
assert selected == CAR.CHEVROLET_VOLT_CAMERA and observed == fp
assert selected == CAR.CHEVROLET_VOLT_CC and observed == fp
cp = CarInterface.get_params(selected, observed, [], alpha, release, False)
assert not cp.dashcamOnly and cp.networkLocation == structs.CarParams.NetworkLocation.fwdCamera
assert cp.flags & GMFlags.VOLT_CAMERA_REMOVED and cp.flags & GMFlags.VOLT_CAMERA_NO_ACCEL_POS
assert is_volt_camera_removed(cp)
assert cp.openpilotLongitudinalControl == (alpha and not release)
assert cp.pcmCruise == (not alpha or release)
assert cp.safetyConfigs[0].safetyParam == (0xC151 if alpha and not release else 0xC150)
assert cp.dashcamOnly == release and cp.networkLocation == structs.CarParams.NetworkLocation.gateway
assert not is_volt_camera_removed(cp)
assert cp.openpilotLongitudinalControl == (not release)
assert cp.pcmCruise == release
assert cp.safetyConfigs[0].safetyParam == (0 if release else 0xC163)
if not release:
assert cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG
@pytest.mark.parametrize('address,length', [(0xD3, 2), (0x130, 1), (0x17D, 6), (0x210, 4), (0x236, 5),
@@ -125,8 +126,10 @@ def test_auto_card_and_controls_use_correct_profile_but_start_with_commands_off(
fp = recorded_fingerprint()
packets = [CanData(address, bytes(size), bus) for bus, messages in fp.items() for address, size in messages.items()]
def receive(wait_for_one=False):
return [packets] if wait_for_one else []
def identify(*args, **kwargs):
return car_helpers.get_car(receive, *args[1:], **kwargs)
with OpenpilotPrefix(), ExitStack() as cleanup:
@@ -142,11 +145,11 @@ def test_auto_card_and_controls_use_correct_profile_but_start_with_commands_off(
card = Car()
cleanup.callback(card.vehicle_startup.close)
cleanup.callback(card.params._finalizer)
assert card.CP.carFingerprint == CAR.CHEVROLET_VOLT_CAMERA
assert card.CP.carFingerprint == CAR.CHEVROLET_VOLT_CC
assert card.CP.fingerprintSource == structs.CarParams.FingerprintSource.can
assert not card.CP.dashcamOnly and not card.CP.passive
assert card.CP.safetyConfigs[0].safetyParam == 0xC150
assert not card.CP.openpilotLongitudinalControl
assert card.CP.safetyConfigs[0].safetyParam == 0xC163
assert card.CP.openpilotLongitudinalControl
controls = Controls()
cleanup.callback(controls.params._finalizer)
assert controls.CP.carFingerprint == card.CP.carFingerprint
@@ -25,8 +25,8 @@ def camera_params(alpha=True, release=False, camera_length=6, radar=False, sascm
return CarInterface.get_params(CAR.CHEVROLET_VOLT_CAMERA, fingerprint, [], alpha, release, False)
def feed_camera(ci, packer, now, *, counter=0, speed=20., gas=False, brake=False, regen=False, low=False, active=True):
_, messages = feed(ci, packer, now, counter=counter, speed=speed, gas=gas, brake=brake, regen=regen)
def feed_camera(ci, packer, now, *, counter=0, speed=20., gas=False, brake=False, regen=False, low=False, active=True, eps_status=0):
_, messages = feed(ci, packer, now, counter=counter, speed=speed, gas=gas, brake=brake, regen=regen, eps_status=eps_status)
messages = [message for message in messages if message[0] not in (0x1C4, 0x1F5)]
messages += [packer.make_can_msg("AcceleratorPedal2", 0, {"CruiseState": 2 if active else 0, "AcceleratorPedal2": 30 if gas else 0}),
packer.make_can_msg("ECMPRDNL2", 0, {"PRNDL2": 6 if low else 4}),
@@ -0,0 +1,60 @@
"""Route 11177: a removed camera must never receive forwarded or host traffic."""
import pytest
from opendbc.can import CANPacker
from opendbc.car import Bus, structs
from opendbc.car.gm.interface import CarInterface
from opendbc.car.gm.tests.test_volt_camera_removed import removed_params, feed_removed
from opendbc.car.gm.tests.test_volt_camera_control import camera_params, feed_camera
from opendbc.car.gm.values import CAR, DBC
from opendbc.safety.tests.libsafety import libsafety_py
@pytest.mark.parametrize('alpha', [False, True])
@pytest.mark.parametrize('alternate', [False, True])
def test_removed_camera_controller_uses_only_vehicle_bus(alpha, alternate):
cp = removed_params(alpha=alpha, alternate=alternate)
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
sent = []
for tick in range(24):
now = 1_000_000_000 + tick * 40_000_000
cs, _ = feed_removed(ci, packer, now, counter=tick % 4, active=False)
assert cs.canValid and not cs.canTimeout
cc = structs.CarControl(enabled=False, latActive=False, longActive=False)
_, frames = ci.apply(cc.as_reader(), now + 2)
assert all(frame[2] == 0 for frame in frames)
sent.extend(frames)
assert any(frame[0] == 0x180 for frame in sent)
@pytest.mark.parametrize('word', [0xC150, 0xC151])
def test_removed_camera_native_has_no_forwarding_or_camera_transmit(word):
safety = libsafety_py.libsafety
release = safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) != 0
assert safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, word) == 0
safety.init_tests()
if release and word == 0xC151:
assert not safety.safety_config_valid()
return
for bus in (0, 1, 2):
for address in range(0x800):
assert safety.safety_fwd_hook(bus, address) == -1
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CAMERA][Bus.pt])
frame = packer.make_can_msg('PSCMStatus', 2, {})
assert not safety.safety_tx_hook(libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]))
@pytest.mark.parametrize('alpha', [False, True])
def test_connected_camera_retains_its_status_delivery(alpha):
cp = camera_params(alpha=alpha)
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
sent = []
for tick in range(12):
now = 1_000_000_000 + tick * 40_000_000
cs, _ = feed_camera(ci, packer, now, counter=tick % 4, active=False)
assert cs.canValid and not cs.canTimeout
_, frames = ci.apply(structs.CarControl().as_reader(), now + 2)
sent.extend(frames)
assert any(frame[0] == 0x184 and frame[2] == 2 for frame in sent)
@@ -0,0 +1,65 @@
"""A missing-camera ACC fault must not masquerade as engaged cruise."""
import pytest
from opendbc.can import CANPacker
from opendbc.car import Bus, structs
from opendbc.car.gm import gmcan
from opendbc.car.gm.interface import CarInterface
from opendbc.car.gm.tests.test_volt_camera_removed import feed_removed, removed_params
from opendbc.car.gm.values import DBC
from opendbc.safety.tests.libsafety import libsafety_py
@pytest.mark.parametrize('alpha', [False, True])
@pytest.mark.parametrize('alternate', [False, True])
@pytest.mark.parametrize('state', [0, 1, 2, 3, 4])
def test_removed_camera_cruise_state_keeps_real_fault(alpha, alternate, state):
cp = removed_params(alpha=alpha, alternate=alternate)
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
for tick in range(20):
now = 1_000_000_000 + tick * 40_000_000
feed_removed(ci, packer, now, counter=tick % 4)
status = packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': state})
out = ci.update([(now + 2, [status])])
assert out.canValid
assert out.accFaulted == (state == 3)
assert out.cruiseState.enabled == (state in (1, 2, 4))
assert out.cruiseState.standstill == (state == 4)
@pytest.mark.parametrize('active', [1, 2, 4])
def test_stock_removed_camera_fault_withdraws_native_cruise_and_cancel(active):
cp = removed_params(alpha=False)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
safety = libsafety_py.libsafety
assert safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, 0xC150) == 0
safety.init_tests()
safety.set_timer(1_000_000)
def rx(name, values):
address, data, bus = packer.make_can_msg(name, 0, values)
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data))
def cancel(counter):
address, data, bus = gmcan.create_buttons(packer, 0, counter, 6)
return safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
rx('ECMEngineStatus', {'CruiseMainOn': 1})
rx('AcceleratorPedal2', {'CruiseState': active})
rx('ASCMSteeringButton', {'ACCAlwaysOne': 1, 'ACCButtons': 1, 'RollingCounter': 0,
'SteeringButtonChecksum': 0xFF})
assert safety.get_controls_allowed()
assert cancel(0)
rx('AcceleratorPedal2', {'CruiseState': 3})
assert not safety.get_controls_allowed()
rx('ASCMSteeringButton', {'ACCAlwaysOne': 1, 'ACCButtons': 1, 'RollingCounter': 1,
'SteeringButtonChecksum': 0xFF + 0x4EF})
assert not cancel(1)
# A subsequent physical active state can recover; a fault is not a latch exemption.
rx('AcceleratorPedal2', {'CruiseState': active})
assert safety.get_controls_allowed()
rx('ASCMSteeringButton', {'ACCAlwaysOne': 1, 'ACCButtons': 1, 'RollingCounter': 2,
'SteeringButtonChecksum': 0xFF + 2 * 0x4EF})
safety.set_timer(1_040_001)
assert cancel(2)
@@ -0,0 +1,130 @@
"""No-camera/no-ACC Volt: independent F1 brake source, conventional cruise only."""
import pytest
from types import SimpleNamespace
from opendbc.can import CANPacker
from opendbc.car import Bus, structs
from opendbc.car.gm.interface import CarInterface
from opendbc.car.gm.tests.test_volt_camera_automatic import discover, recorded_fingerprint
from opendbc.car.gm.tests.test_bolt_cc import feed
from opendbc.car.gm.values import CAR, DBC, is_volt_cc_profile
from opendbc.safety.tests.libsafety import libsafety_py
def params(release=False):
return CarInterface.get_params(CAR.CHEVROLET_VOLT_CC, recorded_fingerprint(), [], False, release, False)
def tick(ci, packer, now, *, counter=0, active=True, gas=False, brake=False, regen=False, analog=0, fault=True):
_, frames = feed(SimpleNamespace(update=lambda messages: None), packer, now,
counter=counter, active=active, stock=60. / 3.6, gas=gas, brake=brake, regen=regen, speed=20.)
frames = [m for m in frames if m[2] == 0 and m[0] not in (0xBE, 0x1C4, 0x1F5)]
frames += [packer.make_can_msg('EBCMBrakePedalPosition', 0, {'BrakePedalPosition': analog}),
packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': 3 if fault else 0, 'AcceleratorPedal2': 30 if gas else 0}),
packer.make_can_msg('ECMPRDNL2', 0, {'PRNDL2': 4})]
return ci.update([(now, frames)]), frames
def test_recorded_noacc_collision_uses_conventional_owner():
assert discover(recorded_fingerprint())[0] == CAR.CHEVROLET_VOLT_CC
cp = params()
assert not cp.dashcamOnly and is_volt_cc_profile(cp)
assert cp.networkLocation == structs.CarParams.NetworkLocation.gateway
assert cp.safetyConfigs[0].safetyParam == 0xC163
assert not cp.alphaLongitudinalAvailable and cp.radarUnavailable
@pytest.mark.parametrize('active', [False, True])
@pytest.mark.parametrize('fault', [False, True])
def test_actual_f1_parser_uses_ordinary_cruise_despite_unused_acc_field(active, fault):
cp = params()
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
for i in range(30):
cs, _ = tick(ci, packer, 1_000_000_000 + i * 40_000_000, counter=i % 4, active=active, fault=fault)
assert cs.canValid and not cs.canTimeout
assert cs.cruiseState.enabled == active
assert not cs.accFaulted and not cs.cruiseState.standstill
assert cs.cruiseState.speed == pytest.approx(60. / 3.6)
assert ci.CS.volt_cc_physical.sources_current(2_160_000_000)
@pytest.mark.parametrize('brake,analog,regen', [(False, 0, False), (True, 0, False), (False, 6, False), (False, 0, True)])
def test_f1_driver_inputs_are_preserved_host(brake, analog, regen):
cp = params()
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
for i in range(30):
now = 1_000_000_000 + i * 40_000_000
cs, frames = tick(ci, packer, now, counter=i % 4, brake=brake, analog=analog, regen=regen)
assert cs.brakePressed == (brake or analog >= 6)
assert cs.regenBraking == regen
@pytest.mark.parametrize('driver', ['none', 'foot', 'analog', 'paddle', 'gas'])
def test_native_f1_stock_permission_and_driver_withdrawal(driver):
safety = libsafety_py.libsafety
debug = safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) == 0
cp = params()
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
assert safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, 0xC163) == 0
safety.init_tests()
for i in range(30):
now = 1_000_000_000 + i * 40_000_000
safety.set_timer(now // 1000)
_, frames = tick(ci, packer, now, counter=i % 4, gas=driver == 'gas', brake=driver == 'foot',
analog=6 if driver == 'analog' else 0, regen=driver == 'paddle')
for address, data, bus in frames:
safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data))
if not debug:
assert not safety.safety_config_valid()
return
safety.safety_tick()
assert safety.safety_config_valid()
assert bool(safety.get_controls_allowed()) == (driver in ('none', 'gas'))
for address, length, bus in [(0x315, 5, 0), (0x2CB, 8, 0), (0x200, 6, 0), (0x184, 8, 2)]:
assert not safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, bytes(length)))
for bus in (0, 1, 2):
for address in (0x180, 0x184, 0x1E1, 0x315, 0x2CB):
assert safety.safety_fwd_hook(bus, address) == -1
def test_release_f1_factory_keeps_existing_volt_cc_denial():
cp = params(release=True)
assert cp.dashcamOnly and not cp.openpilotLongitudinalControl
assert cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.noOutput
def test_native_f1_button_slots_cannot_be_reused_or_survive_stale_sources():
safety = libsafety_py.libsafety
debug = safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) == 0
cp = params()
ci = CarInterface(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
assert safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, 0xC163) == 0
safety.init_tests()
for i in range(40):
now = 1_000_000_000 + i * 40_000_000
safety.set_timer(now // 1000)
_, frames = tick(ci, packer, now, counter=i % 4)
for address, data, bus in frames:
safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data))
ci.CC.frame = 156
command = structs.CarControl(enabled=True, latActive=True, longActive=True)
command.hudControl.setSpeed = 25.
command.actuators.accel = 1.
_, frames = ci.apply(command.as_reader(), now + 1)
assert all(bus == 0 for _, _, bus in frames)
assert not any(address in (0x315, 0x2CB, 0x2CD, 0x200, 0x370) for address, _, _ in frames)
buttons = [m for m in frames if m[0] == 0x1E1]
assert len(buttons) == 1
address, data, bus = buttons[0]
packet = libsafety_py.make_CANPacket(address, bus, data)
assert bool(safety.safety_tx_hook(packet)) == debug
assert not safety.safety_tx_hook(packet)
safety.set_timer(now // 1000 + 300_001)
assert not safety.safety_tx_hook(packet)
safety.set_timer(now // 1000 + 2_000_000)
safety.safety_tick()
assert not safety.safety_config_valid()
@@ -69,7 +69,7 @@ class TestVoltSdgmControl(unittest.TestCase):
setup(cp)
for tick in range(16):
now = 1_000_000_000 + tick * 40_000_000
out, sources = feed_camera(ci, packer, now, counter=tick % 4, gas=tick >= 8, low=tick % 2 == 0)
out, sources = feed_camera(ci, packer, now, counter=tick % 4, gas=tick >= 8, low=tick % 2 == 0, eps_status=1)
self.assertTrue(out.canValid)
for source in sources:
native("rx", source, now // 1000)
+11 -2
View File
@@ -827,15 +827,24 @@ STEER_THRESHOLD = 1.0
DBC = CAR.create_dbc_map()
VOLT_CC_F1_WORD = 0xC163
def is_volt_cc_f1_profile(cp: CarParams) -> bool:
return is_volt_cc_profile(cp) and bool(cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG)
def is_volt_cc_profile(cp: CarParams) -> bool:
"""Exact development-only conventional-cruise owner, with either startup speed choice."""
try:
return (cp.brand == 'gm' and cp.carFingerprint == CAR.CHEVROLET_VOLT_CC and
cp.networkLocation == CarParams.NetworkLocation.gateway and not cp.alphaLongitudinalAvailable and
not cp.pcmCruise and cp.radarUnavailable and
not cp.passive and not cp.dashcamOnly and not cp.notCar and control_flags(cp) == int(GMFlags.CC_LONG) and
not cp.passive and not cp.dashcamOnly and not cp.notCar and
control_flags(cp) == int(GMFlags.CC_LONG | (GMFlags.NO_ACCELERATOR_POS_MSG if cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG else 0)) and
len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
int(cp.safetyConfigs[0].safetyParam) == int(GMSafetyFlags.EV | GMSafetyFlags.NO_ACC))
int(cp.safetyConfigs[0].safetyParam) ==
(VOLT_CC_F1_WORD if cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG else int(GMSafetyFlags.EV | GMSafetyFlags.NO_ACC)))
except (AttributeError, IndexError, TypeError, ValueError):
return False
@@ -7,7 +7,7 @@ def qualified(cp):
return (cp.carFingerprint == CAR.HONDA_ACCORD_11G and cp.brand == "honda" and
not cp.passive and not cp.dashcamOnly and not cp.notCar and
cp.transmissionType in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.cvt) and
cp.pcmCruise and not cp.openpilotLongitudinalControl and cp.alternativeExperience == 0 and
cp.pcmCruise and not cp.openpilotLongitudinalControl and cp.alternativeExperience in (0, 32) and
len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.hondaBosch and
cp.safetyConfigs[0].safetyParam == (80 | int(bool(cp.flags & HondaFlags.BOSCH_ALT_BRAKE))) and
not int(cp.flags) & ~(int(CAR.HONDA_ACCORD_11G.config.flags) |
@@ -20,6 +20,10 @@ class AccordMvlStock:
self.last_lkas_button_frame = 0
def update(self, packer, can, cp, control, state, frame):
# The independent session can outlive the PCM. Its prior control envelope
# must not keep camera button substitution alive after physical withdrawal.
if cp.alternativeExperience == 32 and (state.out.brakePressed or not state.out.cruiseState.enabled):
return []
if not control.enabled or frame % 4 or control.cruiseControl.cancel or control.cruiseControl.resume:
return []
if (self.lkas_button_send_remaining == 0 and state.lkas_hud["LKAS_READY"] and
@@ -36,7 +36,10 @@ def qualified(cp):
cp.openpilotLongitudinalControl and not cp.pcmCruise and
not cp.passive and not cp.dashcamOnly and not cp.notCar and len(configs) == 1 and
configs[0].safetyModel == structs.CarParams.SafetyModel.hondaNidec and
configs[0].safetyParam == (36 if cp.flags & HondaFlags.NIDEC_ALT_SCM_MESSAGES else 32))
(configs[0].safetyParam == (36 if cp.flags & HondaFlags.NIDEC_ALT_SCM_MESSAGES else 32) or
(len(cp.safetyConfigs) == 1 and cp.alternativeExperience == 32 and
configs[0].safetyParam == ((36 if cp.flags & HondaFlags.NIDEC_ALT_SCM_MESSAGES else 32) | 512 |
(256 if cp.carFingerprint == CAR.HONDA_ODYSSEY_TWN else 0)))))
def create_command(packer, gas_amount, counter):
+79 -25
View File
@@ -1,5 +1,5 @@
from opendbc.car import structs
from opendbc.car.honda.values import CAR, HondaFlags
from opendbc.car.honda.values import CAR, HondaFlags, HondaSafetyFlags
CLASSIC_BOSCH = frozenset((CAR.HONDA_NBOX_2G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC_BOSCH,
CAR.HONDA_CIVIC_BOSCH_DIESEL, CAR.HONDA_CRV_5G, CAR.HONDA_CRV_HYBRID, CAR.ACURA_RDX_3G,
@@ -26,40 +26,94 @@ NIDEC = frozenset((
CAR.ACURA_MDX_3G_MMR,
CAR.ACURA_TLX_1G,
))
BOSCH_WORDS = frozenset((0, 1, 8, 9, 10, 11))
NIDEC_WORDS = frozenset((0, 4, 260))
CANFD_STOCK = frozenset((CAR.HONDA_CRV_6G, CAR.HONDA_PILOT_4G,
CAR.HONDA_PASSPORT_4G, CAR.ACURA_MDX_4G_MMR, CAR.ACURA_TLX_2G_MMR))
ALT_RADAR_STOCK = frozenset((CAR.ACURA_RDX_3G_MMR, CAR.HONDA_ODYSSEY_5G_MMR,
CAR.ACURA_TLX_2G, CAR.ACURA_MDX_4G))
BOSCH_WORDS = frozenset((0, 1, 8, 9, 10, 11, 16, 17, 80, 81, 512, 513, 1058, 1059, 1570, 1571, 2082, 2083, 2211))
NIDEC_WORDS = frozenset((0, 4, 260, 544, 548, 804))
MANUAL_SELECTION_PORTS = CLASSIC_BOSCH | NIDEC | RADARLESS | CANFD_STOCK | ALT_RADAR_STOCK | {CAR.HONDA_ACCORD_11G}
# qualified() runs several times per 100 Hz frame in card and selfdrived, so
# everything that depends only on this module is computed once, and its flag
# tests use plain ints: IntFlag operators cost microseconds each in Python.
_DETECTED = int(HondaFlags.BOSCH_EXT_HUD | HondaFlags.BOSCH_ALT_BRAKE | HondaFlags.HAS_BSM |
HondaFlags.EPS_MODIFIED | HondaFlags.HYBRID | HondaFlags.GAS_INTERCEPTOR)
_ALLOWED_FLAGS = {str(car): int(car.config.flags) | _DETECTED
for car in CLASSIC_BOSCH | NIDEC | RADARLESS | CANFD_STOCK | ALT_RADAR_STOCK | {CAR.HONDA_ACCORD_11G}}
_BOSCH, _BOSCH_RADARLESS, _BOSCH_ALT_BRAKE = int(HondaFlags.BOSCH), int(HondaFlags.BOSCH_RADARLESS), int(HondaFlags.BOSCH_ALT_BRAKE)
_NIDEC, _NIDEC_ALT_SCM_MESSAGES = int(HondaFlags.NIDEC), int(HondaFlags.NIDEC_ALT_SCM_MESSAGES)
_UNSUPPORTED = int(HondaFlags.BOSCH_ALT_RADAR)
_FIXED = structs.CarParams.FingerprintSource.fixed
_TRANSMISSIONS = (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.cvt)
_HONDA_NIDEC, _HONDA_BOSCH = structs.CarParams.SafetyModel.hondaNidec, structs.CarParams.SafetyModel.hondaBosch
def qualified(cp, *, marked_only=False):
if (cp.brand != 'honda' or cp.passive or cp.notCar or cp.dashcamOnly or len(cp.safetyConfigs) != 1 or
cp.fingerprintSource == structs.CarParams.FingerprintSource.fixed or
cp.transmissionType not in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.cvt) or
if (cp.brand != 'honda' or cp.passive or cp.notCar or cp.dashcamOnly or len(cp.safetyConfigs) not in (1, 2) or
(len(cp.safetyConfigs) == 2 and not (cp.carFingerprint in CANFD_STOCK and
cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.noOutput and cp.safetyConfigs[0].safetyParam == 0)) or
(cp.fingerprintSource == _FIXED and cp.carFingerprint not in MANUAL_SELECTION_PORTS) or cp.transmissionType not in _TRANSMISSIONS or
cp.alternativeExperience not in ((32,) if marked_only else (0, 32))):
return False
safety = cp.safetyConfigs[0]
identity = next((candidate for candidate in CLASSIC_BOSCH | NIDEC | RADARLESS if candidate == cp.carFingerprint), None)
if identity is None:
safety = cp.safetyConfigs[-1]
fingerprint = str(cp.carFingerprint)
allowed = _ALLOWED_FLAGS.get(fingerprint)
if allowed is None:
return False
detected = int(HondaFlags.BOSCH_EXT_HUD | HondaFlags.BOSCH_ALT_BRAKE | HondaFlags.HAS_BSM | HondaFlags.EPS_MODIFIED | HondaFlags.HYBRID)
if int(cp.flags) & ~(int(identity.config.flags) | detected):
flags = int(cp.flags)
if flags & ~allowed:
return False
flags = HondaFlags(cp.flags)
if flags & (HondaFlags.BOSCH_CANFD | HondaFlags.BOSCH_ALT_RADAR):
if flags & int(HondaFlags.GAS_INTERCEPTOR) and fingerprint not in NIDEC:
return False
if cp.carFingerprint in NIDEC and flags & HondaFlags.NIDEC and not flags & HondaFlags.BOSCH:
expected = 4 if flags & HondaFlags.NIDEC_ALT_SCM_MESSAGES else 0
if cp.carFingerprint == CAR.HONDA_ODYSSEY_TWN and cp.alternativeExperience == 32:
if fingerprint in ALT_RADAR_STOCK:
expected = int(bool(flags & _BOSCH_ALT_BRAKE))
if cp.openpilotLongitudinalControl:
expected |= int(HondaSafetyFlags.BOSCH_LONG)
if cp.alternativeExperience == 32:
expected |= int(HondaSafetyFlags.ALT_RADAR_AOL | HondaSafetyFlags.AOL_BOSCH_LONG)
if fingerprint == CAR.ACURA_MDX_4G and cp.alternativeExperience == 32:
expected |= int(HondaSafetyFlags.STOCK_TJA)
return (bool(flags & _BOSCH) and bool(flags & int(HondaFlags.BOSCH_ALT_RADAR)) and
not bool(flags & int(HondaFlags.BOSCH_CANFD)) and bool(cp.pcmCruise) == (not cp.openpilotLongitudinalControl) and
safety.safetyModel == _HONDA_BOSCH and safety.safetyParam == expected)
if flags & _UNSUPPORTED:
return False
if fingerprint == CAR.HONDA_ACCORD_11G:
from opendbc.car.honda.accord_mvl_stock import qualified as qualified_mvl
return qualified_mvl(cp)
if fingerprint in CANFD_STOCK:
expected = 16 | int(bool(flags & _BOSCH_ALT_BRAKE))
return (bool(flags & _BOSCH) and bool(flags & int(HondaFlags.BOSCH_CANFD)) and
cp.pcmCruise and not cp.openpilotLongitudinalControl and
safety.safetyModel == _HONDA_BOSCH and safety.safetyParam == expected)
if flags & int(HondaFlags.BOSCH_CANFD):
return False
if fingerprint in NIDEC and flags & _NIDEC and not flags & _BOSCH:
expected = 4 if flags & _NIDEC_ALT_SCM_MESSAGES else 0
pedal = bool(flags & int(HondaFlags.GAS_INTERCEPTOR))
if pedal:
expected |= int(HondaSafetyFlags.GAS_INTERCEPTOR)
if cp.alternativeExperience == 32:
expected |= int(HondaSafetyFlags.NIDEC_PEDAL_AOL)
if fingerprint == CAR.HONDA_ODYSSEY_TWN and cp.alternativeExperience == 32:
expected |= 256
return (cp.pcmCruise and cp.openpilotLongitudinalControl and
safety.safetyModel == structs.CarParams.SafetyModel.hondaNidec and safety.safetyParam == expected)
if cp.carFingerprint in CLASSIC_BOSCH and flags & HondaFlags.BOSCH and not flags & HondaFlags.BOSCH_RADARLESS:
expected = int(bool(flags & HondaFlags.BOSCH_ALT_BRAKE))
return (cp.pcmCruise and not cp.openpilotLongitudinalControl and
safety.safetyModel == structs.CarParams.SafetyModel.hondaBosch and safety.safetyParam == expected)
if cp.carFingerprint in RADARLESS and flags & HondaFlags.BOSCH_RADARLESS:
expected = 8 | int(bool(flags & HondaFlags.BOSCH_ALT_BRAKE)) | (2 if cp.openpilotLongitudinalControl else 0)
return (bool(cp.pcmCruise) == (not pedal) and cp.openpilotLongitudinalControl and
safety.safetyModel == _HONDA_NIDEC and safety.safetyParam == expected)
if fingerprint in CLASSIC_BOSCH and flags & _BOSCH and not flags & _BOSCH_RADARLESS:
expected = int(bool(flags & _BOSCH_ALT_BRAKE))
if cp.openpilotLongitudinalControl:
expected |= int(HondaSafetyFlags.BOSCH_LONG)
if fingerprint == CAR.ACURA_RDX_3G:
expected |= int(HondaSafetyFlags.RDX_HIGH_GAS)
if cp.alternativeExperience == 32:
expected |= int(HondaSafetyFlags.CLASSIC_LONG_AOL | HondaSafetyFlags.AOL_BOSCH_LONG)
return (bool(cp.pcmCruise) == (not cp.openpilotLongitudinalControl) and
safety.safetyModel == structs.CarParams.SafetyModel.hondaBosch and safety.safetyParam == expected)
safety.safetyModel == _HONDA_BOSCH and safety.safetyParam == expected)
if fingerprint in RADARLESS and flags & _BOSCH_RADARLESS:
expected = 8 | int(bool(flags & _BOSCH_ALT_BRAKE)) | (2 if cp.openpilotLongitudinalControl else 0)
return (bool(cp.pcmCruise) == (not cp.openpilotLongitudinalControl) and
safety.safetyModel == _HONDA_BOSCH and safety.safetyParam == expected)
return False
@@ -26,7 +26,7 @@ def test_factory_keeps_stock_acc_without_long_handover(alpha, release):
assert not qualified(cp)
cp = final_cp()
cp.alternativeExperience = 32
assert not qualified(cp)
assert qualified(cp)
def test_real_parser_controller_camera_button_cadence_and_override_exclusion():
@@ -0,0 +1,93 @@
"""Exact DEBUG alpha owner retains physical LAT intent without restoring LONG."""
import os
from unittest.mock import patch
import pytest
from opendbc.car import structs
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.radar_interface import RadarInterface
from opendbc.car.honda.tests.test_alt_radar_stock_aol_parity import IDENTITIES, factory
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
@pytest.mark.parametrize('identity', IDENTITIES)
@pytest.mark.parametrize('alt', (False, True))
def test_actual_alpha_factory_card_serialization_and_explicit_off(identity, alt):
from opendbc.car.honda.stock_aol import qualified
from openpilot.selfdrive.car.card import Car
from openpilot.starpilot.car.honda.aol import native_accepts_cp, policy_for
initial = factory(identity, alt=alt, alpha=True)
assert initial.openpilotLongitudinalControl and not initial.pcmCruise
assert qualified(initial)
addition = 1568 if str(identity) == 'ACURA_MDX_4G' else 1056
assert policy_for(initial).safety_param_addition == addition
base_word = initial.safetyConfigs[0].safetyParam
for enabled in (False, True):
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '1'}):
saved = Params()
saved.put_bool('OpenpilotEnabledToggle', True, block=True)
saved.put_bool('AlwaysOnLateral', enabled, block=True)
cp = initial.as_reader().as_builder()
card = Car(CI=CarInterface(cp), RI=RadarInterface(cp))
assert card.aol_qualified == enabled
assert card.CP.safetyConfigs[0].safetyParam == base_word | (addition if enabled else 0)
assert card.CP.alternativeExperience == (32 if enabled else 0)
assert card.CP.openpilotLongitudinalControl and not card.CP.pcmCruise
with structs.CarParams.from_bytes(saved.get('CarParams')) as published:
assert published.to_dict() == card.CP.to_dict()
assert native_accepts_cp(card.CP, 20, card.CP.safetyConfigs[0].safetyParam) == enabled
@pytest.mark.parametrize('identity', IDENTITIES)
@pytest.mark.parametrize('alt', (False, True))
def test_actual_native_set_brake_and_host_axes_do_not_auto_resume_long(identity, alt):
from opendbc.safety.tests.libsafety import libsafety_py
from opendbc.safety.tests.test_honda_stock_aol import TestHondaStockAol
from openpilot.starpilot.aol.tests.stock_lateral_test_helpers import honda_family_case, car_state
with OpenpilotPrefix():
case = honda_family_case('honda', True)
case.family_cp = factory(identity, alt=alt, alpha=True).as_reader()
native = TestHondaStockAol(methodName='runTest')
try:
sd, _, drive, card, receive = case.family_callers(True)
word = card.CP.safetyConfigs[0].safetyParam
assert word in (1058, 1059, 1570, 1571)
native.configure(('alt-alpha', 20, word, 1, card.CI.CC.packer.dbc.name, False, 'GEARBOX_AUTO', 4))
native.stream()
native.normal_engage() # Actual SET falling edge from checksummed CAN.
native.safety.aol_set_host_request(3)
if not native.debug:
assert native.safety.aol_get_permission_mask() == 0
return # RELEASE native refusal is authoritative, never fabricated grant.
assert native.safety.aol_get_permission_mask() == 3
axis, _, command = drive(2, allowed=True, native_request_long=True,
panda_controls_allowed=bool(native.safety.get_controls_allowed()))
assert axis.lateralActive and axis.longitudinalActive and command.latActive and command.longActive
native.press_brake()
assert not native.safety.get_controls_allowed()
native.safety.aol_set_host_request(1)
permission = native.safety.aol_get_permission_mask()
assert permission == 1
state = car_state(brake=True)
axis, _, command = drive(3, observed_state=state, allowed=bool(permission & 1), native_request_long=False,
panda_controls_allowed=bool(native.safety.get_controls_allowed()))
assert axis.lateralActive and command.latActive
assert not axis.longitudinalActive and not command.longActive
assert receive(10_030_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'], state=state)
native.stream()
axis, _, command = drive(4, native_request_long=False, allowed=bool(native.safety.aol_get_permission_mask() & 1),
panda_controls_allowed=bool(native.safety.get_controls_allowed()))
assert not native.safety.get_controls_allowed()
assert axis.lateralActive and command.latActive and not axis.longitudinalActive and not command.longActive
native.stream(gear=2)
assert native.safety.aol_get_permission_mask() == 0
native.stream()
assert native.safety.aol_get_permission_mask() == 1
native.stream(eps=7)
native.stream(eps=0)
assert native.safety.aol_get_permission_mask() == 0
finally:
libsafety_py.libsafety.set_alternative_experience(0)
case.doCleanups()
@@ -0,0 +1,109 @@
"""ALT_RADAR stock AOL keeps actual factory ownership and exact MDX TJA semantics."""
import pytest
from opendbc.car import gen_empty_fingerprint, structs
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.values import CAR, HondaSafetyFlags
IDENTITIES = (CAR.ACURA_RDX_3G_MMR, CAR.HONDA_ODYSSEY_5G_MMR, CAR.ACURA_TLX_2G, CAR.ACURA_MDX_4G)
def factory(identity, *, alt=False, release=False, alpha=False):
fp = gen_empty_fingerprint()
fp[1][0x1A3] = 8
if alt:
fp[1][0x1BE] = 3
return CarInterface.get_params(identity, fp, [], alpha, release, False)
@pytest.mark.parametrize('identity', IDENTITIES)
@pytest.mark.parametrize('alt', (False, True))
@pytest.mark.parametrize('release', (False, True))
def test_alt_radar_stock_factory_and_native_marking(identity, alt, release):
from opendbc.car.honda.stock_aol import qualified
from openpilot.starpilot.car.honda.aol import native_accepts_cp, policy_for
cp = factory(identity, alt=alt, release=release)
if identity == CAR.ACURA_RDX_3G_MMR and release:
assert cp.dashcamOnly and not qualified(cp)
return
assert qualified(cp)
assert cp.pcmCruise and not cp.openpilotLongitudinalControl
policy = policy_for(cp)
assert policy.explicit_latch and policy.normal_runtime_supported
expected = int(HondaSafetyFlags.STOCK_TJA) if identity == CAR.ACURA_MDX_4G else 0
assert policy.safety_param_addition == expected
base = cp.safetyConfigs[0].safetyParam
cp.safetyConfigs[0].safetyParam |= expected
cp.alternativeExperience = 32
assert qualified(cp, marked_only=True)
assert cp.safetyConfigs[0].safetyParam == base | expected
assert native_accepts_cp(cp, 20, base | expected)
bad = cp.as_reader().as_builder()
bad.safetyConfigs[0].safetyParam ^= 512
assert not qualified(bad)
assert qualified(factory(identity, alt=alt, alpha=True))
@pytest.mark.parametrize('identity', IDENTITIES)
@pytest.mark.parametrize('alt', (False, True))
def test_actual_card_controls_and_native_sources_use_final_stock_cp(identity, alt):
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.starpilot.aol.tests.stock_lateral_test_helpers import honda_family_case
from opendbc.safety.tests.libsafety import libsafety_py
from opendbc.safety.tests.test_honda_stock_aol import TestHondaStockAol
cp = factory(identity, alt=alt)
with OpenpilotPrefix():
case = honda_family_case('honda', False)
case.family_cp = cp.as_reader()
native = TestHondaStockAol(methodName='runTest')
try:
sd, _, drive, card, receive = case.family_callers(False)
assert cp.pcmCruise and not card.CP.openpilotLongitudinalControl
word = card.CP.safetyConfigs[0].safetyParam
native.configure(('alt-radar', 20, word, 1, card.CI.CC.packer.dbc.name, False, 'GEARBOX_AUTO', 4))
native.stream()
native.normal_engage()
native.press_brake()
assert not native.safety.get_controls_allowed()
permission = native.safety.aol_get_permission_mask()
assert permission == 1
axis, _, command = drive(2, allowed=bool(permission & 1), native_request_long=False,
panda_controls_allowed=bool(native.safety.get_controls_allowed()))
assert axis.lateralActive and command.latActive
assert not axis.longitudinalActive and not command.longActive
assert receive(10_020_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
native.stream(eps=1)
permission = native.safety.aol_get_permission_mask()
assert permission == int(identity == CAR.ACURA_MDX_4G)
if identity == CAR.ACURA_MDX_4G:
# The configured current parser explicitly regards this status as healthy.
from opendbc.can import CANPacker
packer = CANPacker(card.CI.CC.packer.dbc.name)
msg = packer.make_can_msg('STEER_STATUS', 1, {'STEER_STATUS': 1})
card.CI.update([])
parsed = card.CI.update([(10_030_000_000, [msg])])
assert not parsed.steerFaultPermanent and not parsed.steerFaultTemporary
axis, _, command = drive(3, allowed=bool(permission & 1), native_request_long=False,
panda_controls_allowed=bool(native.safety.get_controls_allowed()))
assert bool(axis.lateralActive) == bool(permission & 1)
assert not command.longActive
native.stream(eps=7)
native.stream(eps=0)
assert native.safety.aol_get_permission_mask() == 0
finally:
libsafety_py.libsafety.set_alternative_experience(0)
case.doCleanups()
@pytest.mark.parametrize('word', (512, 513))
def test_tja_marker_cannot_be_reused_by_other_host_identity(word):
from opendbc.car.honda.stock_aol import qualified
for identity in (CAR.ACURA_RDX_3G_MMR, CAR.HONDA_ODYSSEY_5G_MMR, CAR.ACURA_TLX_2G):
cp = factory(identity)
cp.alternativeExperience = 32
cp.safetyConfigs[0].safetyParam = word
assert not qualified(cp)
cp = factory(CAR.ACURA_MDX_4G)
cp.alternativeExperience = 32
cp.safetyConfigs = [structs.CarParams.SafetyConfig(safetyModel='noOutput'), cp.safetyConfigs[0]]
assert not qualified(cp)
@@ -0,0 +1,199 @@
"""Factory, Card/Controls, real parser and native Honda stock CANFD contracts."""
from unittest.mock import patch
import pytest
from opendbc.can import CANPacker
from opendbc.car import Bus, gen_empty_fingerprint, structs
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.values import CAR, HondaFlags
from openpilot.common.prefix import OpenpilotPrefix
IDENTITIES = (CAR.HONDA_CRV_6G, CAR.HONDA_PILOT_4G, CAR.HONDA_PASSPORT_4G,
CAR.ACURA_MDX_4G_MMR, CAR.ACURA_TLX_2G_MMR)
def factory(identity, *, alt=False, cvt=False, hybrid=False, offset=0):
fp = gen_empty_fingerprint()
fp[offset][0x191 if cvt else 0x1A3] = 8
if alt:
fp[offset][0x1BE] = 3
if hybrid:
fp[offset][0x184] = 8
return CarInterface.get_params(identity, fp, [], True, False, False)
@pytest.mark.parametrize('identity', IDENTITIES)
@pytest.mark.parametrize('alt', (False, True))
@pytest.mark.parametrize('cvt,hybrid', ((False, False), (True, True)))
def test_actual_factory_exact_stock_aol_admission_and_negative_profiles(identity, alt, cvt, hybrid):
from opendbc.car.honda.stock_aol import qualified
from openpilot.starpilot.car.honda.aol import policy_for, native_accepts_cp
cp = factory(identity, alt=alt, cvt=cvt, hybrid=hybrid)
assert cp.pcmCruise and not cp.openpilotLongitudinalControl
assert not cp.alphaLongitudinalAvailable
assert bool(cp.flags & HondaFlags.HYBRID) == hybrid
assert cp.safetyConfigs[0].safetyParam == 16 | int(alt)
assert qualified(cp)
policy = policy_for(cp)
assert policy.explicit_latch and policy.normal_runtime_supported
assert policy.alternative_experience_addition == 32 and policy.safety_param_addition == 0
cp.alternativeExperience = 32
assert qualified(cp, marked_only=True)
assert native_accepts_cp(cp, 20, 16 | int(alt))
for field, value in (('passive', True), ('dashcamOnly', True), ('notCar', True),
('transmissionType', 'manual'),
('flags', int(cp.flags) | (1 << 30))):
bad = cp.as_reader().as_builder()
setattr(bad, field, value)
assert not qualified(bad)
for word in (18, 19, 80, 81, 82, 0x4010):
bad = cp.as_reader().as_builder()
bad.safetyConfigs[0].safetyParam = word
assert not qualified(bad)
assert not native_accepts_cp(bad, 20, word)
assert qualified(factory(identity, offset=4))
@pytest.mark.parametrize('identity', IDENTITIES)
@pytest.mark.parametrize('aol', (False, True))
@pytest.mark.parametrize('release', (False, True))
def test_actual_card_final_cp_controls_startup_preserves_explicit_off(identity, aol, release, monkeypatch):
from opendbc.car.honda.tests.test_stock_startup import exercise_startup
exercise_startup(identity, True, release, aol, 16, monkeypatch)
@pytest.mark.parametrize('identity', (CAR.HONDA_PILOT_4G_MMR,))
def test_distinct_or_new_platform_does_not_inherit_this_owner(identity):
from opendbc.car.honda.stock_aol import qualified
cp = factory(identity)
assert not qualified(cp)
def frames(ci, *, cruise=0, lkas=0, brake=False, gear=4, eps=0, main=True):
intended = {
'SCM_FEEDBACK': {'MAIN_ON': int(main)},
'SCM_BUTTONS': {'CRUISE_BUTTONS': 0, 'CRUISE_SETTING': lkas},
'ENGINE_DATA': {'XMISSION_SPEED': 45.},
'WHEEL_SPEEDS': {f'WHEEL_SPEED_{wheel}': 45. for wheel in ('FL', 'FR', 'RL', 'RR')},
'POWERTRAIN_DATA': {'ACC_STATUS': cruise, 'BRAKE_PRESSED': int(brake), 'BRAKE_SWITCH': int(brake), 'PEDAL_GAS': 0},
'BRAKE_MODULE': {'BRAKE_PRESSED': int(brake)},
'STEER_STATUS': {'STEER_STATUS': eps, 'STEER_TORQUE_SENSOR': 0},
ci.CS.gearbox_msg: {'GEAR_SHIFTER': gear},
'SEATBELT_STATUS': {'SEATBELT_DRIVER_LATCHED': 1, 'SEATBELT_DRIVER_LAMP': 0},
}
result = []
for role, parser in ci.can_parsers.items():
packer = CANPacker(parser.dbc.name)
# Keep packer counters across frames; a new packer per publication is invalid.
packer = ci._canfd_test_packers.setdefault(role, packer)
for address in sorted(parser.addresses):
name = parser.dbc.addr_to_msg[address].name
values = intended.get(name, {}) if role == Bus.pt else {}
assert values.keys() <= parser.dbc.name_to_msg[name].sigs.keys()
result.append(packer.make_can_msg(name, parser.bus, values))
return result
@pytest.mark.parametrize('alt', (False, True))
@pytest.mark.parametrize('offset', (0, 4))
def test_parsed_native_brake_reverse_temporary_and_real_host_controller_withdrawal(alt, offset, tmp_path):
from opendbc.safety.tests.libsafety import libsafety_py
from openpilot.starpilot.aol.tests.stock_lateral_test_helpers import honda_family_case
safety = libsafety_py.libsafety
cp = factory(CAR.HONDA_CRV_6G, alt=alt, offset=offset)
silent = None
if offset:
import shutil
library = tmp_path / 'independent-silent-panda.so'
shutil.copyfile(libsafety_py.libpath, library)
silent = libsafety_py.ffi.dlopen(str(library))
silent.init_tests()
silent.set_alternative_experience(32)
assert silent.set_safety_hooks(cp.safetyConfigs[0].safetyModel.raw, 0) == 0
silent.set_aol_test_heartbeat(True)
silent.aol_set_host_request(3)
assert silent.aol_get_permission_mask() == 0
with OpenpilotPrefix():
case = honda_family_case('honda', False)
case.family_cp = cp.as_reader()
try:
sd, controls, drive, card, receive = case.family_callers(False)
assert card.CP.alternativeExperience == 32
ci = card.CI
ci.update([])
ci._canfd_test_packers = {}
safety.init_tests()
safety.set_alternative_experience(32)
assert safety.set_safety_hooks(20, 16 | int(alt)) == 0
safety.set_aol_test_heartbeat(True)
def parse(tick, **kwargs):
now = 10_000_000_000 + tick * 10_000_000
data = frames(ci, **kwargs)
safety.set_timer((now // 1000) & 0xFFFFFFFF)
for address, payload, bus in data:
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus - offset, payload))
safety.aol_set_host_request(1)
return now, ci.update([(now, data)])
for tick in range(-40, 0):
parse(tick)
parse(0, cruise=1) # Accepted real PCM rising edge, not fabricated controlsAllowed.
assert safety.get_controls_allowed()
_, cs = parse(1, cruise=0, brake=True)
assert cs.brakePressed and not safety.get_controls_allowed()
assert safety.aol_get_permission_mask() == 1
# Existing fixture transports exact current native values to actual SD/Controls.
# No native permission is fabricated; host session serialization is synthetic.
for tick in range(2, 7):
now, cs = parse(tick)
request, permission = safety.aol_get_request_mask(), safety.aol_get_permission_mask()
axis, _, command = drive(tick, observed_state=structs.CarState(**cs.to_dict()),
allowed=bool(permission & 1), native_request_lat=bool(request & 1),
native_request_long=False, panda_controls_allowed=bool(safety.get_controls_allowed()))
assert axis.lateralActive and command.latActive
assert not axis.longitudinalActive and not command.longActive
assert receive(now, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'],
state=structs.CarState(**cs.to_dict()))
# Exercise Card's real initialization and apply caller, with only wire
# publication captured. The command is the real Controls publication.
card.sm.update_msgs(now / 1e9, [controls.pm.messages['carControl']])
outgoing = []
with patch('openpilot.selfdrive.car.card.time.monotonic_ns', return_value=now), \
patch('openpilot.selfdrive.car.card.time.monotonic', return_value=now / 1e9), \
patch.object(card, 'publish_sendcan', side_effect=lambda sends, valid, _outgoing=outgoing: _outgoing.extend(sends) if valid else None):
card.controls_update(cs, command.as_reader())
steering = [f for f in outgoing if f[0] == 0xE4]
assert len(steering) == 1
for address, payload, bus in steering:
packet = libsafety_py.make_CANPacket(address, bus - offset, payload)
assert safety.safety_tx_hook(packet)
if silent is not None:
assert not silent.safety_tx_hook(packet)
assert silent.aol_get_permission_mask() == 0
for tick, kwargs, expected in ((7, {'gear': 2}, 0), (8, {'eps': 6}, 0), (9, {}, 1),
(10, {'eps': 7}, 0), (11, {}, 0)):
now, cs = parse(tick, **kwargs)
permission = safety.aol_get_permission_mask()
assert permission == expected
axis, _, command = drive(tick, observed_state=structs.CarState(**cs.to_dict()), allowed=bool(permission & 1),
native_request_long=False, panda_controls_allowed=bool(safety.get_controls_allowed()))
assert not command.longActive and not axis.longitudinalActive
if not expected:
assert not command.latActive and not axis.lateralActive
finally:
safety.set_alternative_experience(0)
if silent is not None:
silent.set_alternative_experience(0)
case.doCleanups()
@pytest.mark.parametrize('identity', IDENTITIES)
def test_canfd_does_not_borrow_classic_can2_diagnostic_exception(identity):
from openpilot.starpilot.aol.panda_faults import blocking_fault_mask, blocking_fault_list
from openpilot.cereal import log
cp = factory(identity)
cp.alternativeExperience = 32
assert blocking_fault_mask(1 << 3, cp)
assert blocking_fault_list([log.PandaState.FaultType.interruptRateCan2], cp)
@@ -0,0 +1,126 @@
"""Actual normal startup for every established classic Bosch alpha identity."""
import os
from unittest.mock import patch
import pytest
from opendbc.car import structs, gen_empty_fingerprint
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.stock_aol import CLASSIC_BOSCH, qualified
from opendbc.car.honda.values import CAR, HondaFlags
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.common.params import Params
@pytest.mark.parametrize('identity', sorted(CLASSIC_BOSCH, key=str))
@pytest.mark.parametrize('alt', (False, True))
def test_actual_normal_card_sd_controls_startup(identity, alt):
from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD
fp = gen_empty_fingerprint()
fp[1][0x1A3] = 8
if alt:
fp[1][0x1BE] = 3
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}):
params = Params()
params.put_bool('OpenpilotEnabledToggle', True, block=True)
params.put_bool('AlwaysOnLateral', True, block=True)
cp = CarInterface.get_params(identity, fp, [], True, False, False)
ci = CarInterface(cp)
card = Car(CI=ci, RI=ci.RadarInterface(cp))
assert card.aol_qualified
sd, controls = SelfdriveD(CP=card.CP), Controls()
assert sd.aol_replay and controls.aol_replay # Normal settings, no replay override.
assert card.CP.alternativeExperience == 32 and qualified(card.CP, marked_only=True)
expected_alt = bool(identity.config.flags & HondaFlags.BOSCH_ALT_BRAKE) or (alt and identity in (CAR.HONDA_ACCORD, CAR.ACURA_RDX_3G))
assert bool(card.CP.flags & HondaFlags.BOSCH_ALT_BRAKE) == expected_alt
expected = 2082 | int(expected_alt) | (128 if identity == CAR.ACURA_RDX_3G else 0)
assert card.CP.safetyConfigs[0].safetyParam == expected
assert not card.aol_card_intent.allowed_latch
with structs.CarParams.from_bytes(params.get('CarParams')) as published:
assert published.to_dict() == card.CP.to_dict() == controls.CP.to_dict()
assert CarInterface(card.CP).CC.bosch_longitudinal is not None and ci.CC.bosch_longitudinal is not None
from opendbc.safety.tests.libsafety import libsafety_py
from openpilot.starpilot.aol.tests.honda_can import HondaCan
safety = libsafety_py.libsafety
codec = HondaCan(ci)
safety.init_tests()
safety.set_alternative_experience(32)
assert safety.set_safety_hooks(20, expected) == 0
safety.set_aol_test_heartbeat(True)
def observe(tick, **values):
now = 1_000_000 + tick * 10_000
safety.set_timer(now)
frames = codec.frames(**values)
for address, payload, bus in frames:
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, payload))
safety.aol_set_host_request(3)
return ci.update([(now * 1000, frames)])
try:
for tick in range(40):
observe(tick)
assert safety.aol_get_permission_mask() == 0
observe(40, cruise_button=3)
observe(41)
assert safety.get_controls_allowed() and safety.aol_get_permission_mask() == 3
observed = observe(42, brake=True)
assert observed.brakePressed and not safety.get_controls_allowed()
assert safety.aol_get_permission_mask() == 1
observe(43, eps=6)
assert safety.aol_get_permission_mask() == 0
observe(44)
assert safety.aol_get_permission_mask() == 1
reverse = [value for value, name in ci.CS.shifter_values.items() if name == 'R']
assert len(reverse) == 1
observed = observe(45, gear=reverse[0])
assert observed.gearShifter == structs.CarState.GearShifter.reverse
assert safety.aol_get_permission_mask() == 0
observe(46)
assert safety.aol_get_permission_mask() == 1
observe(47, eps=7)
observe(48)
assert safety.aol_get_permission_mask() == 0
finally:
safety.set_alternative_experience(0)
@pytest.mark.parametrize('identity', sorted(CLASSIC_BOSCH, key=str))
def test_classic_alpha_rejects_wrong_namespace_manual_and_diagnostic_borrowing(identity):
from openpilot.starpilot.car.honda.aol import policy_for
from openpilot.starpilot.aol.panda_faults import blocking_fault_mask
fp = gen_empty_fingerprint()
fp[1][0x1A3] = 8
cp = CarInterface.get_params(identity, fp, [], True, False, False)
assert qualified(cp)
cp.safetyConfigs[0].safetyParam |= policy_for(cp).safety_param_addition
cp.alternativeExperience = 32
assert qualified(cp, marked_only=True)
assert blocking_fault_mask(1 << 3, cp)
for word in (34, 35, 163, 1058, 1570, 2080, 2084, 2210):
bad = cp.as_reader().as_builder()
bad.safetyConfigs[0].safetyParam = word
assert not qualified(bad, marked_only=True)
cp.transmissionType = 'manual'
assert not qualified(cp)
@pytest.mark.parametrize('word,ceiling', ((2082, 2000), (2083, 2000), (2211, 2200)))
def test_actual_native_classic_alpha_gas_ceiling_and_brake_withdrawal(word, ceiling):
from opendbc.safety.tests.test_honda_stock_aol import TestHondaStockAol, CASES
case = TestHondaStockAol()
try:
case.configure(next(row for row in CASES if row[2] == word))
case.stream()
case.normal_engage()
case.safety.aol_set_host_request(3)
assert case.safety.aol_get_permission_mask() == 3
for gas, allowed in ((ceiling, True), (ceiling + 1, False)):
packet = case.packet('ACC_CONTROL', {'GAS_COMMAND': gas, 'ACCEL_COMMAND': 0, 'BRAKE_REQUEST': 0})
assert bool(case.safety.safety_tx_hook(packet)) == allowed
case.press_brake()
assert case.safety.aol_get_permission_mask() == 1
assert not case.safety.safety_tx_hook(case.packet('ACC_CONTROL', {'GAS_COMMAND': 1, 'ACCEL_COMMAND': 0, 'BRAKE_REQUEST': 0}))
finally:
case.tearDown()
@@ -0,0 +1,58 @@
"""Real forced-selection factory must not strand established manual-only ports."""
import os
from unittest.mock import patch
import pytest
from opendbc.can.dbc import DBC as CANDBC
from opendbc.car import Bus, car_helpers, gen_empty_fingerprint, structs
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.values import CAR, DBC
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
IDS = (CAR.HONDA_CRV_SA, CAR.HONDA_CLARITY, CAR.HONDA_ACCORD_9G, CAR.ACURA_MDX_3G, CAR.ACURA_MDX_3G_MMR, CAR.ACURA_TLX_1G, CAR.HONDA_FIT_4G, CAR.ACURA_INTEGRA)
@pytest.mark.parametrize('identity', IDS)
def test_real_registered_manual_selection_reaches_card_controls_without_native_grant(identity):
from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.controls.controlsd import Controls
from opendbc.car.honda.stock_aol import qualified
from opendbc.safety.tests.libsafety import libsafety_py
fp = gen_empty_fingerprint()
messages = CANDBC(DBC[identity][Bus.pt]).name_to_msg
gear = messages.get('GEARBOX_AUTO') or messages.get('GEARBOX_CVT')
fp[0][gear.address] = gear.size
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '1', 'FINGERPRINT': ''}):
saved = Params()
saved.put_bool('OpenpilotEnabledToggle', True, block=True)
saved.put_bool('AlwaysOnLateral', True, block=True)
# Patch acquisition only. Actual get_car applies forced selection/source=fixed
# and runs the real factory/controller. Never relabel fixed as automatic FW.
observation = (None, fp, '0' * 17, [], structs.CarParams.FingerprintSource.can, False)
with patch.object(car_helpers, 'fingerprint', return_value=observation):
ci = car_helpers.get_car(lambda *a: [], lambda *a: None, lambda *a: None, False, False, forced_candidate=str(identity))
assert ci.CP.fingerprintSource == structs.CarParams.FingerprintSource.fixed
assert qualified(ci.CP)
card = Car(CI=ci, RI=CarInterface.RadarInterface(ci.CP))
assert card.aol_qualified and card.CP.alternativeExperience == 32
assert not card.aol_card_intent.allowed_latch
with structs.CarParams.from_bytes(saved.get('CarParams')) as published:
assert published.fingerprintSource == structs.CarParams.FingerprintSource.fixed
assert published.to_dict() == card.CP.to_dict()
controls = Controls()
assert controls.CP.to_dict() == card.CP.to_dict()
safety = libsafety_py.libsafety
try:
safety.init_tests()
safety.set_alternative_experience(32)
cfg = card.CP.safetyConfigs[0]
assert safety.set_safety_hooks(cfg.safetyModel.raw, cfg.safetyParam) == 0
safety.set_aol_test_heartbeat(True)
safety.aol_set_host_request(1)
assert safety.aol_get_permission_mask() == 0 # Selection supplies no physical authority.
finally:
safety.set_alternative_experience(0)
@@ -0,0 +1,119 @@
"""Nidec pedal AOL preserves the actual selected sender and current sensor gate."""
import os
from unittest.mock import patch
import pytest
from opendbc.car import structs
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.radar_interface import RadarInterface
from opendbc.car.honda.tests.test_nidec_interceptor import IDENTITIES, factory
from opendbc.car.honda.values import CAR
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
@pytest.mark.parametrize('identity', IDENTITIES)
def test_actual_pedal_factory_card_and_reconstructed_controller_keep_the_sender(identity):
from opendbc.car.honda.stock_aol import qualified
from opendbc.car.honda.nidec_interceptor import qualified as pedal_qualified
from openpilot.selfdrive.car.card import Car
from openpilot.starpilot.car.honda.aol import native_accepts_cp
original = factory(identity)
assert pedal_qualified(original) and qualified(original)
base = original.safetyConfigs[0].safetyParam
for enabled in (False, True):
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION':'1', 'AOL_REPLAY_RUNTIME':'1'}):
saved = Params()
saved.put_bool('OpenpilotEnabledToggle', True, block=True)
saved.put_bool('AlwaysOnLateral', enabled, block=True)
cp = original.as_reader().as_builder()
card = Car(CI=CarInterface(cp), RI=RadarInterface(cp))
expected = base | ((512 | (256 if identity == CAR.HONDA_ODYSSEY_TWN else 0)) if enabled else 0)
assert card.CP.safetyConfigs[0].safetyParam == expected
assert card.CP.alternativeExperience == (32 if enabled else 0)
assert card.aol_qualified == enabled
assert not card.CP.pcmCruise and card.CP.openpilotLongitudinalControl
with structs.CarParams.from_bytes(saved.get('CarParams')) as published:
assert published.to_dict() == card.CP.to_dict()
restored = published.as_builder()
# Construct from FINAL published params, not merely the pre-marker object.
rebuilt = CarInterface(restored)
assert rebuilt.CC.nidec_interceptor is not None
assert pedal_qualified(restored)
assert native_accepts_cp(restored, 1, expected) == enabled
bad = restored.as_reader().as_builder()
bad.safetyConfigs[0].safetyParam |= 0x4000
assert not qualified(bad) and not pedal_qualified(bad)
CASES = (
('pedal', 1, 544, 0, 'honda_civic_touring_2016_can_generated', False, 'GEARBOX_CVT', 4),
('pedal-alt', 1, 548, 0, 'acura_ilx_2016_can_generated', True, 'GEARBOX_AUTO', 4),
('pedal-twn', 1, 804, 0, 'honda_odyssey_twn_2018_generated', True, 'GEARBOX_AUTO', 4),
)
@pytest.mark.parametrize('profile', CASES)
@pytest.mark.parametrize('loss', ('sensor_fault', 'sensor_expired', 'wrong_bus', 'bad_tx_crc'))
def test_actual_pedal_native_brake_sensor_and_transmitted_crc_contract(profile, loss):
from opendbc.car.honda.nidec_interceptor import create_command
from opendbc.safety.tests.test_honda_stock_aol import TestHondaStockAol
from opendbc.safety.tests.libsafety import libsafety_py
native = TestHondaStockAol(methodName='runTest')
try:
native.configure(profile)
native.stream()
native.normal_engage()
native.safety.aol_set_host_request(3)
assert native.safety.get_controls_allowed()
assert native.safety.aol_get_permission_mask() == 3
active = create_command(native.packer, .5, 0)
# CANPackerSafety inherits the actual packer; use its real message encoding.
def packet(msg):
return libsafety_py.make_CANPacket(msg[0], msg[2], msg[1])
assert native.safety.safety_tx_hook(packet(active))
native.press_brake()
assert not native.safety.get_controls_allowed()
assert native.safety.aol_get_permission_mask() == 1
assert not native.safety.safety_tx_hook(packet(active))
neutral = create_command(native.packer, 0., 1)
assert native.safety.safety_tx_hook(packet(neutral))
native.stream()
assert not native.safety.get_controls_allowed() # No longitudinal auto-resume.
if loss == 'sensor_fault':
assert native.safety.safety_rx_hook(native.packet('GAS_SENSOR', {'STATE':1}))
assert native.safety.aol_get_permission_mask() == 0
elif loss in ('sensor_expired', 'wrong_bus'):
native.safety.set_timer(1_200_001)
native.stream(omit='GAS_SENSOR')
if loss == 'wrong_bus':
assert native.safety.safety_rx_hook(native.packet('GAS_SENSOR', {'STATE':0}, 1))
assert native.safety.aol_get_permission_mask() == 0
else:
data = bytearray(neutral[1])
data[5] ^= 1
assert not native.safety.safety_tx_hook(libsafety_py.make_CANPacket(neutral[0], neutral[2], data))
if loss != 'bad_tx_crc':
native.stream()
assert native.safety.aol_get_permission_mask() == 0
finally:
libsafety_py.libsafety.set_alternative_experience(0)
@pytest.mark.parametrize('identity', (CAR.HONDA_CIVIC, CAR.ACURA_ILX, CAR.HONDA_ODYSSEY_TWN))
def test_actual_pedal_card_sd_controls_lat_only_preserves_long_pause(identity):
from openpilot.starpilot.aol.tests.stock_lateral_test_helpers import honda_family_case
with OpenpilotPrefix():
case = honda_family_case('honda', True)
case.family_cp = factory(identity).as_reader()
try:
sd, _, drive, card, receive = case.family_callers(False)
assert card.CI.CC.nidec_interceptor is not None
axis, _, command = drive(2, native_request_long=False, panda_controls_allowed=False)
assert axis.lateralActive and command.latActive
assert not axis.longitudinalActive and not command.longActive
assert receive(10_020_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
finally:
case.doCleanups()
@@ -0,0 +1,152 @@
"""Exact current selected CP and two-Panda owner, never a selection-made grant."""
from dataclasses import replace
import pytest
from opendbc.car import gen_empty_fingerprint, structs, Bus, car_helpers
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.hondacan import CanBus
from opendbc.car.honda.values import CAR, DBC
from opendbc.car.honda.stock_aol import CANFD_STOCK, CLASSIC_BOSCH, NIDEC, RADARLESS, ALT_RADAR_STOCK, qualified
from opendbc.car.honda.tests.test_manual_honda_port_aol import IDS
from openpilot.starpilot.aol.vehicle import native_matches_cp
from openpilot.starpilot.aol.runtime import native_inventory_matches_cp
from openpilot.starpilot.aol.wire import PandaSlot, SafetyState
from openpilot.starpilot.car.honda.aol import policy_for
EXPECTED_PORTS = CLASSIC_BOSCH | NIDEC | RADARLESS | CANFD_STOCK | ALT_RADAR_STOCK | {CAR.HONDA_ACCORD_11G}
MANUAL_SELECTION_PORTS = CLASSIC_BOSCH | NIDEC | RADARLESS | CANFD_STOCK | ALT_RADAR_STOCK | {CAR.HONDA_ACCORD_11G}
@pytest.mark.parametrize('identity', sorted(EXPECTED_PORTS - set(IDS), key=str))
def test_remaining_registered_selection_uses_actual_factory_card_controls_native_zero(identity):
import os
from unittest.mock import patch
from opendbc.can.dbc import DBC as CANDBC
from opendbc.safety.tests.libsafety import libsafety_py
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.common.params import Params
from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.controls.controlsd import Controls
fp = gen_empty_fingerprint()
preliminary = CarInterface.get_params(identity, fp, [], False, False, False)
messages = CANDBC(DBC[identity][Bus.pt]).name_to_msg
gear = messages.get('GEARBOX_AUTO') or messages.get('GEARBOX_CVT')
fp[CanBus(preliminary).pt][gear.address] = gear.size
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '1', 'FINGERPRINT': ''}):
params = Params()
params.put_bool('OpenpilotEnabledToggle', True, block=True)
params.put_bool('AlwaysOnLateral', True, block=True)
observation = (None, fp, '0' * 17, [], structs.CarParams.FingerprintSource.can, False)
with patch.object(car_helpers, 'fingerprint', return_value=observation):
ci = car_helpers.get_car(lambda *a: [], lambda *a: None, lambda *a: None, False, False, forced_candidate=str(identity))
assert ci.CP.fingerprintSource == structs.CarParams.FingerprintSource.fixed
assert qualified(ci.CP)
assert identity in MANUAL_SELECTION_PORTS
card = Car(CI=ci, RI=ci.RadarInterface(ci.CP))
assert card.aol_qualified and not card.aol_card_intent.allowed_latch
assert Controls().CP.to_dict() == card.CP.to_dict()
safety = libsafety_py.libsafety
try:
safety.init_tests()
safety.set_alternative_experience(32)
cfg = card.CP.safetyConfigs[0]
assert safety.set_safety_hooks(cfg.safetyModel.raw, cfg.safetyParam) == 0
safety.set_aol_test_heartbeat(True)
safety.aol_set_host_request(1)
assert safety.aol_get_permission_mask() == 0
finally:
safety.set_alternative_experience(0)
@pytest.mark.parametrize('identity', sorted(CANFD_STOCK, key=str))
@pytest.mark.parametrize('alt', (False, True))
def test_actual_two_panda_factory_matches_only_active_native_owner_and_complete_inventory(identity, alt):
fp = gen_empty_fingerprint()
fp[4][0x1A3] = 8
if alt:
fp[4][0x1BE] = 3
cp = CarInterface.get_params(identity, fp, [], True, False, False)
assert len(cp.safetyConfigs) == 2 and str(cp.safetyConfigs[0].safetyModel) == 'noOutput'
assert cp.pcmCruise and not cp.openpilotLongitudinalControl
assert qualified(cp)
cp.alternativeExperience = policy_for(cp).alternative_experience_addition
word = 16 | int(alt)
assert qualified(cp, marked_only=True)
assert native_matches_cp(cp, 20, word)
assert not native_matches_cp(cp, int(cp.safetyConfigs[0].safetyModel.raw), 0)
assert CarInterface(cp).CC.CAN.pt == 4
slots = (PandaSlot(0, 'silent', int(cp.safetyConfigs[0].safetyModel.raw), 0, 32), PandaSlot(1, 'honda', 20, word, 32))
state = SafetyState(1, True, 10_000_000_000, 10_200_000_000, 20, word, False, False, False, False, 'honda', 'session', 10_000_000_000, slots)
assert native_inventory_matches_cp(state, cp)
assert not state.lateralAllowed and not state.longitudinalAllowed
assert not native_inventory_matches_cp(replace(state, pandaInventory=()), cp) # Legacy v1 cannot admit two slots.
for bad_slots in (
slots[::-1],
(slots[0], replace(slots[1], hardwareSerial='silent')),
(replace(slots[0], controlsAllowed=True), slots[1]),
(slots[0], replace(slots[1], safetyRxChecksInvalid=True)),
(slots[0], replace(slots[1], faults=1)),
(slots[0], replace(slots[1], heartbeatLost=True)),
):
assert not native_inventory_matches_cp(replace(state, pandaInventory=bad_slots), cp)
bad = cp.as_reader().as_builder()
bad.safetyConfigs[0].safetyParam = 1
assert not qualified(bad)
bad = cp.as_reader().as_builder()
bad.safetyConfigs[1].safetyParam = 18
assert not qualified(bad)
assert not native_matches_cp(bad, 20, 18)
@pytest.mark.parametrize('identity', sorted(CANFD_STOCK, key=str))
def test_actual_two_slot_card_publishes_final_cp_and_keeps_initial_intent_unarmed(identity):
import os
from unittest.mock import patch
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.common.params import Params
from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.controls.controlsd import Controls
fp = gen_empty_fingerprint()
fp[4][0x1A3] = 8
initial = CarInterface.get_params(identity, fp, [], False, False, False)
with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '1'}):
params = Params()
params.put_bool('OpenpilotEnabledToggle', True, block=True)
params.put_bool('AlwaysOnLateral', True, block=True)
ci = CarInterface(initial)
card = Car(CI=ci, RI=ci.RadarInterface(initial))
assert card.aol_qualified and card.CP.alternativeExperience == 32
assert not card.aol_card_intent.allowed_latch
with structs.CarParams.from_bytes(params.get('CarParams')) as published:
assert published.to_dict() == card.CP.to_dict()
assert Controls().CP.to_dict() == card.CP.to_dict()
@pytest.mark.parametrize('identity', sorted(CANFD_STOCK, key=str))
def test_actual_two_slot_card_sd_controls_consume_exact_v2_inventory(identity):
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.starpilot.aol.tests.stock_lateral_test_helpers import honda_family_case
fp = gen_empty_fingerprint()
fp[4][0x1A3] = 8
cp = CarInterface.get_params(identity, fp, [], False, False, False)
with OpenpilotPrefix():
case = honda_family_case('honda', False)
case.family_cp = cp.as_reader()
try:
sd, _, drive, card, receive = case.family_callers(False)
axis, _, command = drive(2, native_request_long=False, panda_controls_allowed=False)
assert axis.lateralActive and command.latActive
assert not axis.longitudinalActive and not command.longActive
assert receive(10_020_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
denied, _, command = drive(3, native_valid=False, native_request_long=False)
assert not denied.lateralActive and not command.latActive
finally:
case.doCleanups()
@@ -34,7 +34,7 @@ STARTUP_CASES = (
)
def exercise_final_cp_publication_and_disabled_startup(identity, monkeypatch, firmware=()):
def exercise_final_cp_publication_and_disabled_startup(identity, monkeypatch, firmware=(), *, automatic_firmware=False):
from opendbc.car import car_helpers
from openpilot.cereal import messaging
from openpilot.common.params import Params
@@ -60,7 +60,9 @@ def exercise_final_cp_publication_and_disabled_startup(identity, monkeypatch, fi
initial = CarInterface.get_params(identity, fingerprint, list(firmware), False, False, False)
initial.carVin = "0" * 17
initial.carFw = list(firmware)
if firmware:
if automatic_firmware:
initial.fingerprintSource = structs.CarParams.FingerprintSource.fw
if firmware and not automatic_firmware:
assert initial.flags & HondaFlags.EPS_MODIFIED
assert not initial.dashcamOnly
expected = initial.to_dict()
@@ -73,8 +75,18 @@ def exercise_final_cp_publication_and_disabled_startup(identity, monkeypatch, fi
try:
observed = (identity, fingerprint, initial.carVin, list(firmware), initial.fingerprintSource, True)
with monkeypatch.context() as discovery:
discovery.setattr(car_helpers, "fingerprint", lambda *args: observed)
discovery.setattr(card_module, "can_comm_callbacks", lambda *args: (lambda *args: [], lambda frames: None))
if automatic_firmware:
discovery.setenv("FINGERPRINT", "")
discovery.setenv("SKIP_FW_QUERY", "")
discovery.setenv("DISABLE_FW_CACHE", "1")
discovery.setattr(car_helpers, "get_vin", lambda *args: (-1, -1, initial.carVin))
discovery.setattr(car_helpers, "get_present_ecus", lambda *args: set())
discovery.setattr(car_helpers, "can_fingerprint", lambda *args: (None, fingerprint))
discovery.setattr(car_helpers, "get_fw_versions_ordered", lambda *args: list(firmware))
discovery.setattr(card_module, "obd_callback", lambda *args: lambda enabled: None)
else:
discovery.setattr(car_helpers, "fingerprint", lambda *args: observed)
discovery.setattr(card_module, "can_comm_callbacks", lambda *args: (lambda *args, **kwargs: [], lambda frames: None))
initial_can = messaging.new_message("can", 1)
discovery.setattr(card_module.messaging, "recv_one_retry", lambda socket: initial_can)
card = Car()
@@ -98,7 +110,22 @@ def exercise_final_cp_publication_and_disabled_startup(identity, monkeypatch, fi
command, lateral_log = controls.state_control()
assert not command.enabled and not command.latActive and not command.longActive
controls.publish(command, lateral_log)
ci.apply(command.as_reader(), 1_000_000_000)
_, outgoing = ci.apply(command.as_reader(), 1_000_000_000)
if automatic_firmware:
from opendbc.safety.tests.libsafety import libsafety_py
safety = libsafety_py.libsafety
safety.init_tests()
safety.set_alternative_experience(card.CP.alternativeExperience)
try:
assert safety.set_safety_hooks(card.CP.safetyConfigs[0].safetyModel.raw,
card.CP.safetyConfigs[0].safetyParam) == 0
assert not safety.get_controls_allowed()
steering = ci.CC.packer.dbc.name_to_msg["STEERING_CONTROL"].address
assert any(address == steering and bus == ci.CC.CAN.lkas for address, _, bus in outgoing)
for address, data, bus in outgoing:
assert safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
finally:
safety.set_alternative_experience(0)
finally:
del controls, card, ci, subscriber
gc.collect()
@@ -121,3 +148,18 @@ def test_known_modified_eps_final_cp_publication_and_disabled_startup(identity,
firmware = (structs.CarParams.CarFw(ecu=structs.CarParams.Ecu.eps, address=0x18DA30F1,
subAddress=0, fwVersion=version, brand="honda"),)
exercise_final_cp_publication_and_disabled_startup(identity, monkeypatch, firmware)
@pytest.mark.parametrize("identity", (CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_2022, CAR.HONDA_CIVIC))
def test_automatic_fw_final_cp_and_disabled_native_handoff(identity, monkeypatch):
"""Acquisition is controlled; real FW matching, Card factory and native TX run."""
from opendbc.car.fw_versions import match_fw_to_car
from opendbc.car.honda.fingerprints import FW_VERSIONS
firmware = [structs.CarParams.CarFw(ecu=ecu, address=address, subAddress=sub or 0,
fwVersion=versions[0], brand="honda")
for (ecu, address, sub), versions in FW_VERSIONS[identity].items()]
exact, matches = match_fw_to_car(firmware, "0" * 17, log=False)
assert exact and matches == {identity}
exercise_final_cp_publication_and_disabled_startup(identity, monkeypatch, firmware,
automatic_firmware=True)
@@ -31,7 +31,7 @@ def final_cp(identity, alpha=False, release=False, *, hybrid=False):
def test_actual_factory_admission_and_marker(identity, alpha, release, hybrid):
cp = final_cp(identity, alpha, release, hybrid=hybrid)
assert bool(cp.flags & HondaFlags.HYBRID) == hybrid
expected = not (identity in CLASSIC_BOSCH and cp.openpilotLongitudinalControl)
expected = True
assert qualified(cp) == expected
if not expected:
return
@@ -46,7 +46,12 @@ def test_actual_factory_admission_and_marker(identity, alpha, release, hybrid):
assert qualified(cp, marked_only=True)
assert policy_for(cp).ordinary_axis_ack_required
assert cp.alternativeExperience == 32
assert cp.safetyConfigs[0].safetyParam == (260 if identity == CAR.HONDA_ODYSSEY_TWN else before["safetyConfigs"][0]["safetyParam"])
expected_word = before["safetyConfigs"][0]["safetyParam"]
if identity == CAR.HONDA_ODYSSEY_TWN:
expected_word |= 256
if identity in CLASSIC_BOSCH and cp.openpilotLongitudinalControl:
expected_word |= 2080
assert cp.safetyConfigs[0].safetyParam == expected_word
assert cp.openpilotLongitudinalControl == before["openpilotLongitudinalControl"]
assert cp.pcmCruise == before["pcmCruise"]
assert native_accepts_cp(cp, cp.safetyConfigs[0].safetyModel.raw, cp.safetyConfigs[0].safetyParam)
@@ -60,7 +65,7 @@ def test_actual_factory_admission_and_marker(identity, alpha, release, hybrid):
def test_manual_unknown_namespace_and_extra_panda_denied(identity, hybrid):
cp = final_cp(identity, hybrid=hybrid)
cp.fingerprintSource = structs.CarParams.FingerprintSource.fixed
assert not qualified(cp)
assert qualified(cp) # Explicit registered selection retains truthful metadata.
cp = final_cp(identity, hybrid=hybrid)
cp.transmissionType = structs.CarParams.TransmissionType.manual
assert not qualified(cp)
@@ -196,9 +201,9 @@ def test_active_engagement_arms_without_raw_cruise_resumes_lateral_and_preserves
snapshot = SimpleNamespace(allowedLatch=allowed, pauseLateral=pause_lat, pauseLongitudinal=pause_long)
native = SimpleNamespace(requestedLateral=True, requestedLongitudinal=False,
lateralAllowed=True, longitudinalAllowed=False)
kwargs = dict(standard_lateral=False, standard_longitudinal=False, intent=snapshot,
car_state=cs.as_reader(), initialized=True, model_ready=True,
no_entry=False, immediate_disable=False, dm_lockout=False, pause_brake_mps=5.)
kwargs = {"standard_lateral": False, "standard_longitudinal": False, "intent": snapshot,
"car_state": cs.as_reader(), "initialized": True, "model_ready": True,
"no_entry": False, "immediate_disable": False, "dm_lockout": False, "pause_brake_mps": 5.}
decision = decide_axes(native=native, **kwargs)
assert decision.lateral_active == (not temporary)
assert not decision.longitudinal_active
@@ -472,7 +477,7 @@ def test_actual_card_ack_loss_requires_current_requested_same_identity(changed):
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)
kwargs = {"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
@@ -5,6 +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 qualified
from opendbc.car.honda.values import CAR, HondaFlags, DBC
@@ -65,10 +66,11 @@ def exercise_startup(identity, alpha, release, aol, base_word, monkeypatch, firm
initial.carFw = list(firmware)
assert initial.safetyConfigs[0].safetyParam == base_word
expected = initial.to_dict()
explicit_owner = not (classic and initial.openpilotLongitudinalControl)
expected["alternativeExperience"] = 32 if aol and explicit_owner else 0
if aol and not explicit_owner:
expected["safetyConfigs"][0]["safetyParam"] = base_word | 32
classic_alpha = classic and initial.openpilotLongitudinalControl
expected["alternativeExperience"] = 32 if aol else 0
if aol and classic_alpha:
# Classic alpha now uses the explicit owner: CLASSIC_LONG_AOL | AOL_BOSCH_LONG.
expected["safetyConfigs"][0]["safetyParam"] = base_word | 2080
if aol and identity == CAR.HONDA_ODYSSEY_TWN:
expected["safetyConfigs"][0]["safetyParam"] = 260
subscriber = messaging.sub_sock("carParams", timeout=100, conflate=True)
@@ -87,7 +89,11 @@ def exercise_startup(identity, alpha, release, aol, base_word, monkeypatch, firm
assert ci.CP is card.CP and ci.CC is not None
assert card.aol_qualified is aol
if aol:
assert card.aol_card_intent.explicit_latch is explicit_owner
assert card.aol_card_intent.explicit_latch is True
if classic_alpha:
legacy = card.CP.as_reader().as_builder()
legacy.safetyConfigs[0].safetyParam = base_word | 32
assert not qualified(legacy, marked_only=True)
assert not card.aol_card_intent.allowed_latch
with structs.CarParams.from_bytes(saved.get("CarParams")) as published:
assert published.to_dict() == expected
@@ -108,7 +114,7 @@ def exercise_startup(identity, alpha, release, aol, base_word, monkeypatch, firm
assert intent.producerSessionId == card.slc_producer_session
controls = Controls()
assert controls.CP.to_dict() == expected
assert controls.ordinary_axis_ack_required is (aol and explicit_owner)
assert controls.ordinary_axis_ack_required is aol
now = 1_000_000_000
feed(controls, now, 0, active=False, enabled=False, can_valid=False, can_timeout=True)
if aol:
+4
View File
@@ -92,6 +92,10 @@ class HondaSafetyFlags(IntFlag):
RADARLESS = 8
BOSCH_CANFD = 16
BOSCH_CANFD_MVL = 64
CLASSIC_LONG_AOL = 2048 # Exact marked DEBUG classic Bosch alpha owner.
ALT_RADAR_AOL = 1024 # Exact marked DEBUG alpha owner; physical EPS/gear token.
NIDEC_PEDAL_AOL = 512 # Model1 only; distinct from model20 STOCK_TJA.
STOCK_TJA = 512 # Marked MDX4G AOL; EPS status1 is healthy TJA lockout.
class HondaFlags(IntFlag):
@@ -0,0 +1,43 @@
"""Correlate recent local steering rejection echoes; never authorize output."""
from collections import deque
import time
class AngleTxRecovery:
def __init__(self, address, bus, clock=None):
self.clock = time.monotonic_ns if clock is None else clock
self.address = address
self.bus = bus
self.sent = deque(maxlen=4)
self.last_packet_ns = 0
self.pending_frame = -1
self.pending_sent_ns = -1
def record(self, frames, frame):
now_ns = self.clock()
for address, data, bus in frames:
if address == self.address and bus == self.bus and len(data) == 32 and (data[9] >> 4 & 3) == 2:
self.sent.append((frame, now_ns, bytes(data)))
def observe(self, packets, frame):
now_ns = self.clock()
for stamp, frames in packets:
if stamp <= self.last_packet_ns:
continue
self.last_packet_ns = stamp
for address, data, bus in frames:
if address != self.address or bus != self.bus + 0xC0 or len(data) != 32:
continue
# Consume an exact recent command once. The original local send clock
# also bounds delayed echoes when controller updates have been paused.
match = next(((sent_frame, sent_ns, sent_data) for sent_frame, sent_ns, sent_data in self.sent
if 0 < frame - sent_frame <= 3 and 0 <= now_ns - sent_ns <= 30_000_000 and sent_data == bytes(data)), None)
if match is not None:
self.sent.remove(match)
self.pending_frame = frame
self.pending_sent_ns = match[1]
def take_rejection(self, frame):
pending = self.pending_frame == frame and 0 <= self.clock() - self.pending_sent_ns <= 30_000_000
self.pending_frame = -1
return pending
@@ -112,6 +112,14 @@ def stock_word(cp):
def qualified(cp, *, marked_only=False):
# The paired EV9 LONG owner uses its own RX/TX and native policy; share only
# the established host permission/temporary-restriction handling.
if cp.carFingerprint == CAR.HYUNDAI_IONIQ_5_PE and cp.openpilotLongitudinalControl:
from opendbc.car.hyundai.pe_long_aol import qualified as pe_qualified
return pe_qualified(cp, marked_only=True)
if cp.carFingerprint == CAR.KIA_EV9 and cp.openpilotLongitudinalControl:
from opendbc.car.hyundai.ev9_long_aol import qualified as ev9_qualified
return ev9_qualified(cp, marked_only=True)
identity = str(cp.carFingerprint)
if identity not in MODELS or cp.brand != 'hyundai':
return False
@@ -183,6 +191,12 @@ def command_allowed(cp, state):
def temporary_restriction(cp, state):
from opendbc.car.hyundai.pe_long_aol import qualified as pe_qualified, temporary_restriction as pe_restricted
if pe_qualified(cp, marked_only=True):
return pe_restricted(cp, state)
from opendbc.car.hyundai.ev9_long_aol import qualified as ev9_qualified, temporary_restriction as ev9_restricted
if ev9_qualified(cp, marked_only=True):
return ev9_restricted(cp, state)
gears = structs.CarState.GearShifter
return bool(
state is not None
@@ -29,7 +29,7 @@ def config_index(cp):
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:
if cp.carFingerprint in (CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN) 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
@@ -97,18 +97,23 @@ def qualified(cp, *, marked_only=False):
LONG_AOL_WORDS = frozenset((0x0815, 0x0895))
LONG_EV_CARS = frozenset((CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_KONA_EV_2ND_GEN))
INDEPENDENT_LONG_BUTTON_CARS = LONG_EV_CARS | {CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN}
def qualified_long(cp, *, marked_only=False):
if cp.carFingerprint == CAR.KIA_EV6:
from opendbc.car.hyundai.ev6_aol import qualified as ev6_qualified
return ev6_qualified(cp, marked_only=marked_only)
if cp.carFingerprint not in LONG_EV_CARS or not cp.openpilotLongitudinalControl or cp.pcmCruise:
if cp.carFingerprint not in INDEPENDENT_LONG_BUTTON_CARS or not cp.openpilotLongitudinalControl or cp.pcmCruise:
return False
if cp.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN:
from opendbc.car.hyundai.gv70_startup import eligible
if not eligible(cp):
return False
word = _base_word(cp, experiences=(0, 32))
if word not in (0x11, 0x91):
return False
raw = int(cp.safetyConfigs[0].safetyParam)
raw = int(cp.safetyConfigs[config_index(cp)].safetyParam)
return (raw == (word | 0x804) and cp.alternativeExperience == 32) or (
not marked_only and raw == (word | 4) and cp.alternativeExperience == 0
)
@@ -3,9 +3,10 @@ import time
from opendbc.car import Bus
from opendbc.can.parser import MAX_BAD_COUNTER
from opendbc.car.hyundai.ecu_startup import HyundaiECUStartup, Outcome
from opendbc.car.hyundai.ecu_startup import Outcome
from opendbc.car.hyundai.ecu_continuity import ContinuousECUStartup
from opendbc.car.hyundai.hyundaicanfd import CanBus
from opendbc.car.hyundai.values import HyundaiFlags
from opendbc.car.hyundai.values import CAR, HyundaiFlags
from opendbc.car.hyundai.canfd_owner import topology_index
@@ -24,7 +25,7 @@ def first_generation(cp):
not cp.passive and not cp.dashcamOnly and not cp.notCar)
class CapturedADRVStartup(HyundaiECUStartup):
class CapturedADRVStartup(ContinuousECUStartup):
def __init__(self, cp, callbacks, *, label, template_type):
buses = CanBus(cp)
super().__init__(cp, callbacks, address=0x730, bus=buses.ECAN, label=label)
@@ -33,6 +34,10 @@ class CapturedADRVStartup(HyundaiECUStartup):
self.template_type = template_type
self.template = None
def continuity_required(self):
return (self.cp.carFingerprint in (CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN) and
self.cp.openpilotLongitudinalControl and first_generation(self.cp))
def _capture(self):
floor = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
deadline = time.monotonic() + 1.
@@ -91,6 +96,9 @@ class CapturedADRVStartup(HyundaiECUStartup):
return False
return True
def sources_current(self, ci, now_ns):
return self.outcome is not Outcome.SENT_UNCONFIRMED or self._sources(ci, now_ns, floor=self.floor_ns)
def _warm_stock(self, ci):
deadline = time.monotonic() + 3.
while time.monotonic() < deadline:
@@ -11,6 +11,8 @@ from opendbc.car.hyundai.ev9_longitudinal import (
BlindspotWarningOutput,
)
from opendbc.car.hyundai import ev9_long_sender
from opendbc.car.hyundai.ev9_long_aol import qualified as ev9_long_aol_qualified
from opendbc.car.hyundai.pe_long_aol import qualified as pe_long_aol_qualified
from opendbc.car.common.filter_simple import FirstOrderFilter
from opendbc.car.lateral import get_max_angle_vm, get_max_angle_delta_vm
import numpy as np
@@ -23,6 +25,7 @@ from opendbc.car.hyundai.hyundaicanfd import CanBus
from opendbc.car.hyundai.ray_pedal import create_ray_pedal_command, ray_pedal_gas, ray_pedal_enabled
from opendbc.car.hyundai.ioniq6_longitudinal import Ioniq6LongitudinalPolicy
from opendbc.car.hyundai.canfd_dash_icons import CanFDDashIcons
from opendbc.car.hyundai.classic_scc_aol import qualified as qualified_classic_stock_aol
from opendbc.car.hyundai.canfd_stock_aol import qualified as qualified_canfd_stock_aol
from opendbc.car.hyundai.ev6_aol import qualified as qualified_ev6_aol
from opendbc.car.hyundai.g90_longitudinal import G90LongitudinalPolicy
@@ -31,7 +34,8 @@ from opendbc.car.hyundai.g90_lead import G90LeadState, eligible as g90_lead_elig
from opendbc.car.hyundai.gv70_camera_lead import eligible as gv70_lead_eligible
from opendbc.car.hyundai.canfd_lead import select_lead, ABSENT
from opendbc.car.hyundai.torque_ev_scc import eligible as torque_ev_scc_eligible, options as torque_ev_scc_options, clock_boot_ns, fallback_lead
from opendbc.car.hyundai.ccnc_ev_stock import replacement_requested
from opendbc.car.hyundai.ccnc_ev_stock import replacement_requested, qualified as qualified_ccnc_ev_stock
from opendbc.car.hyundai.angle_tx_recovery import AngleTxRecovery
from opendbc.car.hyundai.values import is_blended, HyundaiFlags, HyundaiSafetyFlags, Buttons, CarControllerParams, CAR
from opendbc.car.interfaces import CarControllerBase
from opendbc.car.vehicle_model import VehicleModel
@@ -98,6 +102,8 @@ def process_hud_alert(enabled, fingerprint, hud_control):
class CarController(CarControllerBase):
def __init__(self, dbc_names, CP):
super().__init__(dbc_names, CP)
from opendbc.car.hyundai.gv60_startup import qualified as gv60_long_qualified
self.gv60_longitudinal = gv60_long_qualified(CP)
self.CAN = CanBus(CP)
self.params = CarControllerParams(CP)
self.packer = CANPacker(dbc_names[Bus.pt])
@@ -105,6 +111,9 @@ class CarController(CarControllerBase):
self.angle_vm = VehicleModel(CP) if CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING else None
self.apply_angle_last = 0.0
self.angle_gain_last = 0.0
self.angle_tx_recovery = (AngleTxRecovery(0x110, self.CAN.ACAN)
if CP.carFingerprint in (CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9) and
(qualified_angle_aol(CP, marked_only=True) or qualified_ccnc_ev_stock(CP)) else None)
self.accel_last = 0
self.apply_torque_last = 0
@@ -144,6 +153,8 @@ class CarController(CarControllerBase):
self.ioniq6_bsm_enabled = ioniq6_status
self.ev6_aol_status = CP.carFingerprint == CAR.KIA_EV6 and (qualified_canfd_stock_aol(CP) or qualified_ev6_aol(CP))
self.canfd_dash_icons = CanFDDashIcons() if ioniq6_status or self.torque_ev_scc_enabled or self.ev6_aol_status else None
self.classic_dash_icons = CanFDDashIcons() if qualified_classic_stock_aol(CP, marked_only=True) and CP.carFingerprint != CAR.KIA_RAY_EV else None
self.classic_lfa_icon = None
self.ioniq6_bsm_counter = 0
self.ioniq6_bsm_last_can_ns = 0
self._ray_pedal = ray_pedal_enabled(CP)
@@ -157,9 +168,10 @@ class CarController(CarControllerBase):
self._ray_disengage_frame = None
def update(self, CC, CS, now_nanos):
if self.pe_longitudinal is not None and (self.pe_startup is None or not self.pe_startup.can_emit(self)):
pending_startup_required = self.pe_longitudinal is not None or self.ev9_longitudinal is not None
if pending_startup_required and (self.pe_startup is None or not self.pe_startup.can_emit(self)):
return structs.CarControl.Actuators.new_message(), []
if self.pe_longitudinal is not None and not self.pe_startup.handed_off:
if pending_startup_required and not self.pe_startup.handed_off:
CC = structs.CarControl.new_message().as_reader()
_projection = self.learning_projection_observer
if _projection is not None:
@@ -168,6 +180,11 @@ class CarController(CarControllerBase):
if self.CP.carFingerprint == CAR.KIA_EV6_2025 and self.CP.dashcamOnly:
return actuators.as_builder(), []
hud_control = CC.hudControl
# Card finalizes the AOL safety marker after CarInterface constructs us.
if self.classic_dash_icons is None and self.CP.carFingerprint != CAR.KIA_RAY_EV and qualified_classic_stock_aol(self.CP, marked_only=True):
self.classic_dash_icons = CanFDDashIcons()
if self.classic_dash_icons is not None:
self.classic_lfa_icon = self.classic_dash_icons.update(self.frame, CC.enabled, CC.latActive)
if self.CP.carFingerprint == CAR.KIA_RAY_EV:
if CC.latActive:
self._ray_disengage_frame = None
@@ -266,6 +283,8 @@ class CarController(CarControllerBase):
new_actuators.steeringAngleDeg = self.apply_angle_last
new_actuators.accel = accel
if self.angle_tx_recovery is not None:
self.angle_tx_recovery.record(can_sends, self.frame)
self.frame += 1
if _projection is not None:
_projection.finish(new_actuators.torque)
@@ -316,7 +335,8 @@ class CarController(CarControllerBase):
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))
ray_lka_icon=(self._ray_lfa_icon or 1) if self.CP.carFingerprint == CAR.KIA_RAY_EV else None,
aol_lka_icon=(self.classic_lfa_icon or 1) if self.classic_lfa_icon is not None 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])
@@ -365,7 +385,7 @@ class CarController(CarControllerBase):
lfa_icon = 2 if CC.enabled or CC.latActive else 3 if disengaging else 0
can_sends.append(hyundaican.create_blended_lfahda(self.packer, self.CAN, self.frame, lfa_icon))
else:
can_sends.append(hyundaican.create_lfahda_mfc(self.packer, CC.enabled))
can_sends.append(hyundaican.create_lfahda_mfc(self.packer, CC.enabled, lfa_icon=self.classic_lfa_icon))
# 5 Hz ACC options
if self.frame % 20 == 0 and self.CP.openpilotLongitudinalControl and not self._ray_pedal and self.blended_longitudinal is None:
@@ -410,7 +430,9 @@ class CarController(CarControllerBase):
safety_speed = max(CS.out.vEgoRaw - 1.0, 1.0)
bound = min(get_max_angle_vm(safety_speed, self.ev9_legacy_envelope, self.params),
get_max_angle_vm(safety_speed, self.angle_vm, self.params))
ev9_lateral_allowed = lateral_request_allowed(CC, CS.out, CS.angle_steering_fault)
ev9_lateral_allowed = lateral_request_allowed(CC, CS.out, CS.angle_steering_fault,
independent=(ev9_long_aol_qualified(self.CP, marked_only=True) or
pe_long_aol_qualified(self.CP, marked_only=True)))
active = bool(ev9_lateral_allowed and CC.latActive and abs(measured) <= bound and abs(self.apply_angle_last) <= bound)
self.ev9_angle_filter.update_alpha(float(np.interp(CS.out.vEgo, [5.0, 10.0, 20.0], [0.2, 0.1, 0.0])))
desired = self.ev9_angle_filter.update(float(np.clip(CC.actuators.steeringAngleDeg,
@@ -453,6 +475,9 @@ class CarController(CarControllerBase):
angle_active = angle_aol_active
if ccnc_ev_stock:
angle_active = ccnc_ev_replacement
if self.angle_tx_recovery is not None and self.angle_tx_recovery.take_rejection(self.frame):
self.apply_angle_last = CS.out.steeringAngleDeg
self.angle_gain_last = 0.0
desired_angle = float(np.clip(CC.actuators.steeringAngleDeg, -self.params.ANGLE_LIMITS.STEER_ANGLE_MAX,
self.params.ANGLE_LIMITS.STEER_ANGLE_MAX))
safety_speed = max(CS.out.vEgoRaw - 1.0, 1.0)
@@ -545,6 +570,8 @@ class CarController(CarControllerBase):
self.packer, self.CAN, self.frame, template=self.adrv_template,
drive_gear=CS.out.gearShifter == structs.CarState.GearShifter.drive,
speed=CS.out.vEgoRaw if self.CP.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN else None))
if self.gv60_longitudinal and self.frame % 4 == 0:
can_sends.append(hyundaicanfd.create_gv60_radar_heartbeat(self.frame // 4, CS.out.brakePressed, CS.out.gasPressed))
if self.ioniq6_longitudinal is not None and self.frame % 4 == 0:
can_sends.append(hyundaicanfd.create_ioniq6_radar_heartbeat(self.frame // 4, CS.out.brakePressed, CS.out.gasPressed))
if self.ioniq6_bsm_enabled and self.frame % 5 == 0:
+62 -11
View File
@@ -1,3 +1,4 @@
from opendbc.car.hyundai.left_paddle import has_left_paddle
from opendbc.car.hyundai.ioniq5pe_long_profile import qualified as pe_long_qualified
from opendbc.car.hyundai.ioniq5pe_ready import PEReadySource
from opendbc.car.hyundai.blended_stock_aol import qualified as qualified_blended_stock, qualified_alpha as qualified_blended_alpha, MixedStockLkasSources
@@ -7,6 +8,8 @@ from opendbc.car.hyundai.ev9_camera_lead import EV9CameraLead
from opendbc.car.hyundai.canfd_camera_lead import CANFDCameraLead
from opendbc.car.hyundai.torque_ev_scc import eligible as torque_ev_scc_eligible
from opendbc.car.hyundai.ev9_longitudinal import qualified as ev9_long_qualified
from opendbc.car.hyundai.canfd_angle_aol import qualified as angle_aol_qualified
from opendbc.car.hyundai.long_buttons import LongCruiseButtons, qualified as long_buttons_qualified
from collections import deque
import copy
import math
@@ -64,8 +67,10 @@ class CarState(CarStateBase):
self.forte_lkas_sources = ClassicSccLkasSources(CP.carFingerprint)
if qualified_blended_stock(CP) or qualified_blended_alpha(CP):
self.forte_lkas_sources = MixedStockLkasSources(CP)
self.long_buttons = (LongCruiseButtons(track_main=CP.carFingerprint != CAR.HYUNDAI_IONIQ_5_PE)
if long_buttons_qualified(CP) else None)
self.pe_long = pe_long_qualified(CP)
self.pe_ready = PEReadySource() if self.pe_long else None
self.pe_ready = PEReadySource() if self.pe_long or ev9_long_qualified(CP) else None
self.ev9_long = ev9_long_qualified(CP) or self.pe_long
self.ev9_camera_lead = (CANFDCameraLead(CP, CanBus(CP).ECAN) if self.pe_long else
EV9CameraLead(CP) if self.ev9_long else None)
@@ -161,10 +166,23 @@ class CarState(CarStateBase):
return not any(cp.vl[name][signal] != 0 or any(value != 0 for value in cp.vl_all[name][signal])
for name, signal in signals)
def update_button_enable(self, buttonEvents):
if self.long_buttons is not None:
return self.long_buttons.button_enable
return super().update_button_enable(buttonEvents)
def create_main_button_events(self, cur_button, prev_button):
if self.long_buttons is not None:
return [event for current, previous in self.long_buttons.main_transitions
for event in create_button_events(current, previous, {1: ButtonType.mainCruise})]
return create_button_events(cur_button, prev_button, {1: ButtonType.mainCruise})
def create_cruise_button_events(self, cur_button, prev_button, samples=()):
# Preserve every real edge in an alpha parser batch, including a complete
# Cancel press/release while the host is already enabled.
if is_blended(self.CP) and self.CP.openpilotLongitudinalControl:
if self.long_buttons is not None:
return [event for current, previous in self.long_buttons.cruise_transitions
for event in create_button_events(current, previous, BUTTONS_DICT)]
if (is_blended(self.CP) and self.CP.openpilotLongitudinalControl or
samples and self.CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING and angle_aol_qualified(self.CP, marked_only=True)):
events = []
for button in samples:
events.extend(create_button_events(int(button), prev_button, BUTTONS_DICT))
@@ -326,6 +344,31 @@ class CarState(CarStateBase):
else:
self.lkas11 = copy.copy(cp_cam.vl["LKAS11"])
self.clu11 = copy.copy(cp.vl["CLU11"])
from opendbc.car.hyundai.classic_long_aol import independent_buttons_qualified
self.ev6_aol_samples = ()
self.ev6_aol_source_healthy = False
if independent_buttons_qualified(self.CP):
from opendbc.can.parser import MAX_BAD_COUNTER
source = cp.message_states.get(cp.dbc.name_to_msg['CLU11'].address)
self.ev6_aol_sample_stamp_ns = int(source.timestamps[-1]) if source is not None and source.timestamps else 0
self.ev6_aol_timeout_ns = int(source.timeout_threshold) if source is not None else 0
now = cp._last_update_nanos
bus_timeout = cp.bus_timeout
self.ev6_aol_source_healthy = bool(
not bus_timeout and source is not None and source.timestamps
and source.valid(now, bus_timeout) and source.counter_fail < MAX_BAD_COUNTER
and 0 < self.ev6_aol_sample_stamp_ns <= now
and now - self.ev6_aol_sample_stamp_ns <= source.timeout_threshold)
values = cp.vl_all['CLU11']
self.ev6_aol_samples = tuple((main, 0, cruise) for main, cruise in zip(
values['CF_Clu_CruiseSwMain'], values['CF_Clu_CruiseSwState'], strict=True))
if self.long_buttons is not None:
values = cp.vl_all['CLU11']
self.long_buttons.observe((main, False, cruise) for main, cruise in zip(
values['CF_Clu_CruiseSwMain'], values['CF_Clu_CruiseSwState'], strict=True))
ret.cruiseState.available = self.long_buttons.available(ret.cruiseState.available)
self.steer_state = cp.vl["MDPS12"]["CF_Mdps_ToiActive"] # 0 NOT ACTIVE, 1 ACTIVE
prev_cruise_buttons = self.cruise_buttons[-1]
prev_main_buttons = self.main_buttons[-1]
@@ -348,7 +391,7 @@ class CarState(CarStateBase):
button_events = [*self.create_cruise_button_events(self.cruise_buttons[-1], prev_cruise_buttons,
cp.vl_all["CLU11"]["CF_Clu_CruiseSwState"]),
*create_button_events(self.main_buttons[-1], prev_main_buttons, {1: ButtonType.mainCruise}),
*self.create_main_button_events(self.main_buttons[-1], prev_main_buttons),
*create_button_events(self.lda_button, prev_lda_button, {1: ButtonType.lkas})]
if self.forte_lkas_sources is not None and getattr(self.forte_lkas_sources, 'active', True):
@@ -498,9 +541,11 @@ class CarState(CarStateBase):
from opendbc.car.hyundai.ev6_aol import qualified as ev6_aol_qualified
self.ev6_aol_samples = ()
self.ev6_aol_source_healthy = False
from opendbc.car.hyundai.canfd_stock_aol import LONG_EV_CARS, qualified_long
if ev6_aol_qualified(self.CP) or (
self.CP.carFingerprint in LONG_EV_CARS and qualified_long(self.CP, marked_only=True)):
from opendbc.car.hyundai.canfd_stock_aol import INDEPENDENT_LONG_BUTTON_CARS, qualified_long
from opendbc.car.hyundai.ev9_long_aol import qualified as ev9_long_aol_qualified
from opendbc.car.hyundai.pe_long_aol import qualified as pe_long_aol_qualified
if ev6_aol_qualified(self.CP) or ev9_long_aol_qualified(self.CP, marked_only=True) or pe_long_aol_qualified(self.CP, marked_only=True) or (
self.CP.carFingerprint in INDEPENDENT_LONG_BUTTON_CARS and qualified_long(self.CP, marked_only=True)):
from opendbc.can.parser import MAX_BAD_COUNTER
source = cp.message_states.get(cp.dbc.name_to_msg[self.cruise_btns_msg_canfd].address)
self.ev6_aol_sample_stamp_ns = int(source.timestamps[-1]) if source is not None and source.timestamps else 0
@@ -514,6 +559,11 @@ class CarState(CarStateBase):
and now - self.ev6_aol_sample_stamp_ns <= source.timeout_threshold)
values = cp.vl_all[self.cruise_btns_msg_canfd]
self.ev6_aol_samples = tuple(zip(values['ADAPTIVE_CRUISE_MAIN_BTN'], values['LDA_BTN'], values['CRUISE_BUTTONS'], strict=True))
if self.long_buttons is not None:
values = cp.vl_all[self.cruise_btns_msg_canfd]
self.long_buttons.observe(zip(values['ADAPTIVE_CRUISE_MAIN_BTN'], values['NORMAL_CRUISE_MAIN_BTN'],
values['CRUISE_BUTTONS'], strict=True))
ret.cruiseState.available = self.long_buttons.available(ret.cruiseState.available)
prev_cruise_buttons = self.cruise_buttons[-1]
prev_main_buttons = self.main_buttons[-1]
prev_lda_button = self.lda_button
@@ -521,7 +571,7 @@ class CarState(CarStateBase):
self.main_buttons.extend(cp.vl_all[self.cruise_btns_msg_canfd]["ADAPTIVE_CRUISE_MAIN_BTN"])
self.lda_button = cp.vl[self.cruise_btns_msg_canfd]["LDA_BTN"]
prev_left_paddle = self.left_paddle
self.left_paddle = cp.vl["CRUISE_BUTTONS"]["LEFT_PADDLE"] if self.CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 else 0
self.left_paddle = cp.vl["CRUISE_BUTTONS"]["LEFT_PADDLE"] if has_left_paddle(self.CP) else 0
self.buttons_counter = cp.vl[self.cruise_btns_msg_canfd]["COUNTER"]
if not self.CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING and self.CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CARNIVAL_ALT_RESUME:
self.cruise_buttons_alt_ts_ns = cp.ts_nanos["CRUISE_BUTTONS_ALT"]["COUNTER"]
@@ -535,8 +585,9 @@ class CarState(CarStateBase):
if self.CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING:
self.angle_lkas_status = copy.copy(cp_cam.vl["LKAS_ALT" if self.CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT else "LKAS"])
ret.buttonEvents = [*create_button_events(self.cruise_buttons[-1], prev_cruise_buttons, BUTTONS_DICT),
*create_button_events(self.main_buttons[-1], prev_main_buttons, {1: ButtonType.mainCruise}),
ret.buttonEvents = [*self.create_cruise_button_events(self.cruise_buttons[-1], prev_cruise_buttons,
cp.vl_all[self.cruise_btns_msg_canfd]["CRUISE_BUTTONS"]),
*self.create_main_button_events(self.main_buttons[-1], prev_main_buttons),
*create_button_events(self.lda_button, prev_lda_button, {1: ButtonType.lkas}),
*create_button_events(self.left_paddle, prev_left_paddle, {1: ButtonType.altButton2})]
@@ -69,3 +69,9 @@ def native_accepts(cp, model, param):
and model == int(cp.safetyConfigs[0].safetyModel.raw)
and param == aol_word(cp)
)
def independent_buttons_qualified(cp, *, marked_only=True):
return bool(cp.carFingerprint in CLASSIC_LONG_IDS and qualified(cp, marked_only=marked_only) and
cp.alternativeExperience in ((AOL_EXPERIENCE,) if marked_only else (0, AOL_EXPERIENCE)) and
aol_word(cp) in (0xC04, 0xC05, 0xC06, 0xC44, 0xC45, 0xC46))
+16 -7
View File
@@ -6,20 +6,29 @@ NORMAL_BUS_LAYOUT = {(2, 0x110): 32, (2, 0x362): 32, (1, 0x35): 32,
(1, 0x1CF): 8, (0, 0x3A5): 24}
# Classic HKG steering, chassis and stock SCC are already visible on normal
# harness wiring. This selects a diagnostic query family, never a vehicle or
# control profile. Fresh firmware and CAN fingerprinting still own identity.
CLASSIC_NORMAL_BUS_LAYOUT = {(0, 0x251): 8, (0, 0x386): 8, (0, 0x394): 8,
(0, 0x4F1): 4, (2, 0x340): 8, (0, 0x420): 8, (0, 0x421): 8}
def normal_bus_topology(can_recv, *, clock=time.monotonic):
deadline = clock() + .12
observed = {}
layouts = (NORMAL_BUS_LAYOUT, CLASSIC_NORMAL_BUS_LAYOUT)
observed = [{} for _ in layouts]
for _ in range(24):
if clock() >= deadline:
break
for packet in can_recv(wait_for_one=True):
for frame in packet:
key = (frame.src, frame.address)
if key in NORMAL_BUS_LAYOUT:
length = len(frame.dat)
if length != NORMAL_BUS_LAYOUT[key]:
return False
observed[key] = length
if observed == NORMAL_BUS_LAYOUT:
for layout, received in zip(layouts, observed, strict=True):
if key in layout:
length = len(frame.dat)
if length != layout[key]:
return False
received[key] = length
if any(received == layout for layout, received in zip(layouts, observed, strict=True)):
return True
return False

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