Compare commits

..

314 Commits

Author SHA1 Message Date
Jason Wen 175835273f cap it so we don't cry 2026-09-01 00:35:09 -04:00
Jason Wen 5c24e128d8 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025-wee 2026-09-01 00:16:21 -04:00
Jason Wen f46d9a7bba Bump opendbc 2026-08-28 01:18:40 -04:00
Jason Wen cfb38312db Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2026-08-27 16:57:56 -04:00
Jason Wen 6fd4278e02 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-08-25 02:03:09 -04:00
DevTekVE 451cc3445a Bump opendbc 2026-08-23 16:35:28 +02:00
Jason Wen 1daa2d9081 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-08-17 10:29:01 -04:00
Jason Wen 7a9099818f Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2026-08-14 17:57:37 -04:00
Jason Wen ef4fa48f33 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-08-14 17:18:06 -04:00
Jason Wen e426ced4c0 bump 2026-08-11 17:47:18 -04:00
Jason Wen 7681ba763b Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2026-08-11 17:47:12 -04:00
Jason Wen b04d666a68 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-08-10 04:27:58 -04:00
Jason Wen fb026b86a3 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2026-08-06 21:58:57 -04:00
Jason Wen cff4c5e589 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-08-06 21:58:50 -04:00
Jason Wen e0cca951f3 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-07-25 08:52:04 -04:00
Jason Wen 2439c048ed Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2026-07-20 15:12:17 -04:00
Jason Wen 7b54d7d7a0 bump 2026-07-20 09:51:59 -04:00
Jason Wen b2bdb5787f Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
#	openpilot/tools/plotjuggler/layouts/analyzing-panda-block-angle-hkg.xml
#	openpilot/tools/plotjuggler/layouts/analyzing-torque-angle-hkg.xml
#	openpilot/tools/plotjuggler/layouts/hkg_angle_control.xml
#	openpilot/tools/plotjuggler/layouts/safety-limits-angle-kkg.xml
#	pyproject.toml
#	scripts/lint/lint.sh
2026-07-20 09:50:52 -04:00
Jason Wen 282f517b78 upstream changes 2026-06-09 00:18:40 -04:00
Jason Wen 1779144ff9 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
#	scripts/lint/lint.sh
2026-06-08 23:53:43 -04:00
Jason Wen 54fb2750ba Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2026-05-09 11:47:25 -04:00
DevTekVE 14431ab77f Merge branch 'master' into hkg-angle-steering-2025 2026-05-05 10:37:34 +02:00
Jason Wen 9d45db41b3 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
#	system/version.py
2026-05-03 16:08:40 -04:00
Jason Wen b57c593d92 bump 2026-04-20 23:31:05 -04:00
nayan 0674c42866 good bot
fix state
2026-04-19 08:52:19 +02:00
nayan 1f7bcf246a Bringing sl change to validate 2026-04-18 09:25:12 +02:00
DevTekVE b0512dc523 Revert "safety: dynamically relax lateral jerk limits during accel conflicts"
This reverts commit b0644a37e3b5a7b941bd4347f4c7ef0f25f44fbc.
2026-04-12 11:25:31 +02:00
DevTekVE cd88fd3850 Bringing shane's improvements on the angle steering branch 2026-04-10 12:09:12 +02:00
DevTekVE ff78eaeba1 Merge branch 'master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-04-10 11:38:19 +02:00
DevTekVE 92c8aeeb92 lint 2026-04-10 11:34:29 +02:00
DevTekVE ecaa20e7be Add source map configuration to VSCode launch settings
- Enables better debugging by mapping sources to `${workspaceFolder}/opendbc/safety`.
2026-04-05 15:10:21 +02:00
DevTekVE 859c98c9d8 Refactor PlotJuggler layouts and optimize custom math equations
- Introduced new tabs for `Smoothing and Torque ceilings`.
- Updated custom math equations for cleaner logic and added new snippets for angle smoothing, ceiling brackets, and roll compensation.
2026-04-05 12:14:13 +02:00
DevTekVE 69cbe8c6ed Enhance torque reduction logic with speed and steering error adjustments
- Introduced speed-dependent and error-sensitive dynamic torque ceilings.
- Improved interpolation for smoother torque application.
2026-04-04 11:55:14 +02:00
DevTekVE 1aafe92fc0 Remove hyundai_canfd_ccnc.dbc and update dependencies
- Deleted `_hyundai_canfd_ccnc.dbc` and its import references across related files.
- Merged relevant signals and comments into `hyundai_canfd_og.dbc` for consolidated usage.
- Cleaned up obsolete imports in `hyundai_canfd.dbc`.
2026-04-04 08:46:09 +02:00
DevTekVE adb6b9fb12 Test changing priority for reading dbc files to in memory first 2026-04-04 08:29:57 +02:00
DevTekVE f67a9f4624 Revised steering angle smoothing matrix and logic cleanup
- Adjusted `SMOOTHING_ANGLE_VEGO_MATRIX` to refine torque smoothing at mid-range speeds.
- Removed unused deadzone logic for cleaner and more consistent angle smoothing.
2026-04-03 18:10:05 +02:00
DevTekVE 094e834ac4 cleanup: remove unused datafile references and expand ignored patterns
- Deleted `<previouslyLoaded_Datafiles>` sections from PlotJuggler layouts to streamline configuration files.
- Added `.ipynb` files to the `pyproject.toml` ignore list for cleaner tooling.
2026-04-03 17:19:18 +02:00
DevTekVE e4067060b9 Adding ioniq 9 and updating ioniq 5 pe n-line fingerprint 2026-04-03 17:06:15 +02:00
DevTekVE 86a14640b5 Tune speed-dependent steering smoothing to eliminate EPS whine
Reimplemented an Exponential Moving Average (EMA) filter on the requested
steering angle (`apply_angle`). The model's raw high-frequency micro-corrections
at low speeds cause acoustic resonance (whine) in the EPS motor. This filter
dynamically adjusts the smoothing factor (alpha) based on vehicle speed to
silence the EPS at a crawl while maintaining zero-latency precision on the highway.

Key Behaviors & Speed Matrix:
* Deadzone: Ignores angle changes ≤ 0.1° to preserve straight-line tracking.
* 0 km/h (0 mph) -> Alpha: 0.05 (Max smoothing to eliminate stationary vibration)
* 30.6 km/h (19 mph) -> Alpha: 0.10 (Heavy smoothing for stable residential turning)
* 39.6 km/h (25 mph) -> Alpha: 0.30 (Moderate smoothing)
* 49.7 km/h (31 mph) -> Alpha: 0.60 (Light smoothing for responsive city driving)
* 80.0 km/h (50 mph) -> Alpha: 1.00 (Zero smoothing / raw signal for high-speed precision)
2026-04-03 16:13:05 +02:00
DevTekVE 59fb84f0d5 Remove HKG angle control tuning components and dependencies
- Deleted HKG-specific angle tuning settings and related UI elements.
    - Removed tuning parameter handling and smoothing logic from carcontroller.
    - Simplifies codebase and eliminates unused parameters.
2026-04-03 15:33:57 +02:00
DevTekVE 78de201ce7 Bring it back to 360 for hda1s 2026-04-03 15:14:37 +02:00
DevTekVE 17e49f69cc refactor: update parameterized tests and extend Hyundai Ioniq 5 PE model years
- Replaced `parameterized.expand` with new `parameterized` syntax for cleaner test definitions.
- Added 2026 model year to Hyundai Ioniq 5 PE in `car_list.json`.
2026-04-03 14:39:45 +02:00
DevTekVE bec75ebdd8 remove: TorqueReductionGainController and its associated tests
- Fully deprecates the no longer used torque reduction logic.
- Cleans up outdated functionality for clarity and maintainability.
2026-04-03 14:14:07 +02:00
DevTekVE 473832efa7 Merge branch 'master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-04-03 14:00:54 +02:00
DevTekVE 2cceeb3552 fix: clip steering angle to respect angle limits when latActive is off
- Prevents potential out-of-bound steering angle values.
- Ensures compliance with defined steering angle constraints.
2026-04-03 01:06:37 +02:00
DevTekVE 923a47f713 Reverting all the new control changes until we properly test them on a few variants to ensure it is safe.
Revert "upstream pending tune"

This reverts commit b51d9af9a0.

Revert "show torque reduction gain"

This reverts commit ded0b506d6.

Revert "must gate"

This reverts commit 8b60649eed.

Revert "bump"

This reverts commit 221c219fca.

Revert "temp: comment out blind-spot monitoring signals due to DBC changes"

This reverts commit 790a762a05.
2026-04-01 09:29:53 +02:00
DevTekVE 790a762a05 temp: comment out blind-spot monitoring signals due to DBC changes
- Avoided crash caused by missing signals in updated DBC definitions.
- Added a TODO to revisit and validate blind-spot logic.
2026-04-01 08:55:26 +02:00
Jason Wen 221c219fca bump 2026-03-31 21:57:58 -04:00
Jason Wen 8b60649eed must gate 2026-03-31 21:42:06 -04:00
Jason Wen ded0b506d6 show torque reduction gain 2026-03-31 07:46:13 -04:00
Jason Wen b51d9af9a0 upstream pending tune 2026-03-31 06:56:52 -04:00
Jason Wen 4460ce8166 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-03-18 03:43:10 -04:00
Jason Wen f1aa0c7f78 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2026-03-07 01:49:09 -05:00
Jason Wen e7c8126fd9 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-03-05 02:01:04 -05:00
Jason Wen 125999c364 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-03-01 16:34:41 -05:00
Jason Wen 5b25ea7f99 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2026-02-28 15:47:57 -05:00
Jason Wen b7e2631286 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-02-27 21:56:34 -05:00
Jason Wen a838871189 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-02-19 01:51:17 -05:00
Jason Wen 1827331599 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-02-17 20:12:13 -05:00
Jason Wen 5c777bbe01 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2026-02-13 23:41:06 -05:00
Jason Wen fb97f993d1 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2026-02-13 17:32:10 -05:00
Jason Wen 94b67077e3 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-02-12 23:35:46 -05:00
Jason Wen 14f17699b9 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2026-02-11 00:35:20 -05:00
Jason Wen 80e27d5cbb Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2026-02-10 23:41:27 -05:00
Jason Wen 039dbcd877 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2026-02-09 01:46:00 -05:00
Jason Wen 8c134ae555 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-02-08 20:04:01 -05:00
Jason Wen 276c7a2b34 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-02-02 22:40:14 -05:00
Jason Wen 0e2dbcebfa Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-01-26 11:45:29 -05:00
Jason Wen bbb7760a95 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2026-01-19 01:43:21 -05:00
Jason Wen 23a27b2fbf Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2026-01-09 18:48:43 -05:00
Jason Wen 8b78107a40 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2025-12-31 00:49:56 -05:00
Jason Wen c27b6007de wrong bump? 2025-12-26 10:08:31 -05:00
Jason Wen 36c2dce247 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2025-12-26 10:08:23 -05:00
Jason Wen 8035039731 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2025-12-23 00:52:24 -05:00
Jason Wen 8aa6c9440f Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2025-12-20 17:01:25 -05:00
Jason Wen 34ef40fd81 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2025-12-18 00:19:20 -05:00
Jason Wen 4d044d7618 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2025-12-15 02:25:04 -05:00
Jason Wen 6247e3dc84 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2025-12-13 01:59:06 -05:00
Jason Wen b1131289b7 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2025-12-07 01:37:16 -05:00
Jason Wen 35dc7d661e Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2025-12-07 00:11:54 -05:00
DevTekVE c39b2dad94 Merge branch 'master' into hkg-angle-steering-2025 2025-11-29 10:12:31 +01:00
Jason Wen e6b769245c Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
#	selfdrive/ui/sunnypilot/SConscript
#	selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc
#	selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h
#	selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc
2025-11-25 18:54:02 -05:00
DevTekVE 8b94f8b2f8 Merge branch 'master' into hkg-angle-steering-2025 2025-11-06 18:31:16 +01:00
Jason Wen dec014cd17 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	sunnypilot/selfdrive/car/interfaces.py
2025-11-04 17:45:05 -05:00
DevTekVE 7b40272866 Add sunnypilot-specific stats logging and handling
- Introduced `StatLogSP` for sunnypilot-specific metrics.
- Integrated stats collection and submission pathways for sunnylink.
- Extended parameters and handlers to support additional metrics.
- Added gzip compression and base64 encoding for oversized payload handling.
2025-11-04 21:20:16 +01:00
DevTekVE 506456e7f0 Merge branch 'master' into hkg-angle-steering-2025 2025-11-01 13:31:23 +01:00
DevTekVE 4ea4b9d177 Merge branch 'master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2025-10-31 06:59:39 +01:00
DevTekVE 0e2313dc31 Merge branch 'master' into hkg-angle-steering-2025 2025-10-18 11:14:45 +02:00
Jason Wen 79ea7db103 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2025-10-17 23:42:01 -04:00
Jason Wen f9ae9192fa Merge branch 'ui-icbm-universal' into hkg-angle-steering-2025 2025-10-17 21:55:00 -04:00
Jason Wen 7ec23006c6 check this 2025-10-17 21:54:44 -04:00
Jason Wen 2be9447a6c Merge branch 'ui-icbm-universal' into hkg-angle-steering-2025 2025-10-17 21:19:09 -04:00
Jason Wen cfd926778e always init true 2025-10-17 21:19:03 -04:00
Jason Wen a97a67e3d0 need 2025-10-17 21:17:31 -04:00
Jason Wen 518b6de08d Merge branch 'ui-icbm-universal' into hkg-angle-steering-2025 2025-10-17 21:15:59 -04:00
Jason Wen 6933e3bcdb fix cruise toggles 2025-10-17 21:15:43 -04:00
Jason Wen 05e0ca8bee some more 2025-10-17 20:46:36 -04:00
Jason Wen 410614fcf3 single location 2025-10-17 20:26:18 -04:00
Jason Wen e2bc0996ef Merge branch 'ui-icbm-universal' into hkg-angle-steering-2025 2025-10-17 12:21:47 -04:00
Jason Wen 1be0c20cf5 oops 2025-10-17 12:21:37 -04:00
Jason Wen 839143b9ed oops 2025-10-17 12:20:23 -04:00
Jason Wen bce86637ae Merge branch 'ui-icbm-universal' into hkg-angle-steering-2025 2025-10-17 12:15:23 -04:00
Jason Wen b833d3ee89 ui: update ICBM-related settings handling 2025-10-17 12:14:44 -04:00
Jason Wen e0441dfb4b Merge branch 'sla-event' into hkg-angle-steering-2025 2025-10-17 11:58:42 -04:00
Jason Wen 62ec40bba6 Speed Limit Assist: update active event handling 2025-10-17 11:58:19 -04:00
Jason Wen 15c6d38028 bump 2025-10-16 17:05:58 -04:00
Jason Wen 56eb9f555c Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	sunnypilot/selfdrive/controls/lib/e2e_alerts_helper.py
2025-10-16 01:12:10 -04:00
Jason Wen 2f9951df02 Merge branch 'e2e-alert-state-machine' into hkg-angle-steering-2025 2025-10-15 23:55:40 -04:00
Jason Wen 6030bf4da3 less 2025-10-15 23:52:03 -04:00
Jason Wen 074694d660 lead depart: only arm if we have a confirmed close lead for over a second after allowing alert 2025-10-15 23:48:13 -04:00
Jason Wen df35f48f3b magic 2025-10-15 22:56:08 -04:00
Jason Wen 4fb9704540 time based 2025-10-15 22:51:40 -04:00
Jason Wen 48cbe266fc 10 frames for both 2025-10-15 22:50:42 -04:00
Jason Wen 21aa7ff367 rename 2025-10-15 22:47:08 -04:00
Jason Wen 7caf05dd51 not used 2025-10-15 22:41:00 -04:00
Jason Wen 2d779f5db9 E2E Helper: universal state machine 2025-10-15 22:38:57 -04:00
Jason Wen 18208f1da0 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2025-10-15 17:41:56 -04:00
Jason Wen 5a3c6ddf57 gate lka angle steering out of alpha long 2025-10-14 23:25:00 -04:00
Jason Wen eaa8732ab0 Merge branch 'e2e-alerts-cooldown' into hkg-angle-steering-2025 2025-10-14 14:44:00 -04:00
Jason Wen 6f0284c84f try preventing startup false trigger 2025-10-14 14:43:47 -04:00
Jason Wen 999ea03f23 try preventing startup false trigger 2025-10-14 14:42:42 -04:00
Jason Wen 0975db3ff1 Merge branch 'e2e-alerts-cooldown' into hkg-angle-steering-2025 2025-10-14 14:31:59 -04:00
Jason Wen 8d70a8b80a only when long not engaged 2025-10-14 14:31:48 -04:00
Jason Wen da93f92887 rename 2025-10-14 14:26:57 -04:00
Jason Wen 9117f6c071 Merge branch 'e2e-alerts-cooldown' into hkg-angle-steering-2025 2025-10-14 14:23:46 -04:00
Jason Wen 3567ff9691 introduce recent moving check 2025-10-14 14:22:41 -04:00
Jason Wen f44ae2ced9 too complicated 2025-10-14 14:08:40 -04:00
Jason Wen 376e0ca615 only allow one trigger per standstill session 2025-10-14 14:07:16 -04:00
Jason Wen 32b7686468 Merge branch 'master' into e2e-alerts-cooldown 2025-10-14 11:40:58 -04:00
nayan b9e0f52ea9 E2E Alert Cooldown 2025-10-14 07:43:15 -04:00
Jason Wen a3163b680f Merge branch 'sla-chimes' into hkg-angle-steering-2025 2025-10-14 02:01:05 -04:00
Jason Wen 8a927d808f Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2025-10-14 02:00:36 -04:00
Jason Wen bc7d5e474d Speed Limit Assist: audible alerts for certain states 2025-10-14 01:45:13 -04:00
Jason Wen 36f192b5fe Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2025-10-13 03:10:47 -04:00
Jason Wen ffd5cd4ac2 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2025-10-11 02:29:47 -04:00
Jason Wen e4a00fcd6c Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025 2025-10-10 17:28:16 -04:00
Jason Wen 0bdcb41103 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into hkg-angle-steering-2025
# Conflicts:
#	common/params_keys.h
#	opendbc_repo
#	selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc
2025-10-10 17:02:14 -04:00
DevTekVE 78051085ca Merge branch 'master' into hkg-angle-steering-2025 2025-09-23 07:54:32 +02:00
DevTekVE 4910d5809a bump opendbc 2025-09-23 07:45:39 +02:00
DevTekVE 8dd862ff28 yikes, becoming picky huh? 2025-09-14 22:50:21 +02:00
DevTekVE 1b57497da9 Merge remote-tracking branch 'origin/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2025-09-14 22:48:00 +02:00
DevTekVE 8d8d1ffc7a bump opendbc again 2025-09-14 22:46:20 +02:00
DevTekVE c5919d5495 wrong dbc lol 2025-09-14 22:42:19 +02:00
DevTekVE da71951c95 This is no longer in use nor needed. Bai! 2025-09-14 12:56:28 +02:00
DevTekVE 25a152cd8b Merge remote-tracking branch 'origin/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2025-09-14 12:44:23 +02:00
DevTekVE 9077a1082a Sorry for C3 :( but moving you out to a last working branch before I sync it up 2025-09-14 06:52:24 +02:00
DevTekVE 7ae7000254 Rework override behavior and feeling 2025-09-14 06:38:00 +02:00
DevTekVE 7029455706 better juggle 2025-09-13 08:32:27 +02:00
DevTekVE 3a71a62215 Merge branch 'master' into hkg-angle-steering-2025 2025-09-12 10:14:52 +02:00
DevTekVE 00622e8c33 Merge remote-tracking branch 'origin/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2025-09-05 09:46:41 +02:00
DevTekVE 549da3ee92 Merge branch 'master' into hkg-angle-steering-2025 2025-09-04 20:20:37 +02:00
DevTekVE e038a65ef8 Merge branch 'master' into hkg-angle-steering-2025 2025-08-31 13:14:33 +02:00
DevTekVE 8d0513c657 dbc: update CHECKSUM format for multiple messages to improve data integrity 2025-08-30 19:56:21 +02:00
DevTekVE 2cea48f4cd Merge remote-tracking branch 'origin/master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2025-08-30 15:27:45 +02:00
DevTekVE 8855a9ab65 Improve surprise jerk by safety blocks 2025-08-26 14:47:26 +02:00
DevTekVE 32321c01cc A bit more helpful safety block investigation help 2025-08-26 10:09:11 +02:00
DevTekVE 52cd65fefb lint dont bother me 2025-08-25 08:24:24 +02:00
DevTekVE 1fcdeccd40 Merge branch 'master' into hkg-angle-steering-2025
# Conflicts:
#	common/params_keys.h
#	opendbc_repo
2025-08-25 08:01:51 +02:00
DevTekVE 3517c36978 Revert "Add HkgAngleDebug structure and enhance angle debugging in car controller" 2025-08-24 19:02:48 +02:00
DevTekVE fb30c3c1e8 cleanup and honour params 2025-08-23 15:42:47 +02:00
DevTekVE 1c25e568d5 Update steering pressed logic to include hands-on-wheel detection for improved safety 2025-08-23 15:11:07 +02:00
DevTekVE f172122b7c Update steering pressed logic to include hands-on-wheel detection for improved safety 2025-08-22 19:55:16 +02:00
Jason Wen 67d6cdc7cd Merge remote-tracking branch 'sunnypilot/sunnypilot/hkg-angle-steering-2025' into hkg-angle-steering-2025 2025-08-21 16:39:36 -04:00
DevTekVE 6724085cfd Refactor angle limit calculations and adjust average road roll for improved steering dynamics 2025-08-21 20:09:57 +02:00
DevTekVE 6774f34eee Refine non-linear mapping in torque reduction gain calculation for improved steering response 2025-08-21 00:21:46 +02:00
DevTekVE 6d0402896d Adjust STEER_THRESHOLD and refine non-linear mapping in torque reduction gain calculation for improved steering response 2025-08-20 23:16:04 +02:00
DevTekVE 344021a3d9 Adjust non-linear mapping in torque reduction gain calculation for improved response 2025-08-20 19:54:31 +02:00
DevTekVE a9b85ab27d Refactor HkgAngleDebug structure to include current and baseline limits for angle parameters 2025-08-20 19:54:10 +02:00
DevTekVE 17204a46e4 Add HkgAngleDebug structure and enhance angle debugging in car controller 2025-08-20 18:39:53 +02:00
DevTekVE 7c4d415462 Enhance torque reduction gain calculation with non-linear mapping and smoothing 2025-08-20 00:12:43 +02:00
DevTekVE b8985b6d72 Enhance torque reduction gain calculation with non-linear mapping and smoothing 2025-08-19 19:50:13 +02:00
DevTekVE c669473f88 Refine torque reduction parameters and update UI for angle error analysis 2025-08-19 19:35:34 +02:00
DevTekVE 8751435bf5 Improving tq redc gain and override behavior 2025-08-19 10:08:51 +02:00
DevTekVE 30ae210761 Merge branch 'master' into hkg-angle-steering-2025 2025-08-19 10:07:42 +02:00
Jason Wen 3eb693f58b Merge remote-tracking branch 'sunnypilot/sunnypilot/hkg-angle-steering-2025' into hkg-angle-steering-2025 2025-08-18 12:30:29 -04:00
DevTekVE 7b4a31c5ac bugfix 2025-08-17 16:07:45 +02:00
DevTekVE 1527c8cf88 bump opendbc 2025-08-17 15:31:31 +02:00
DevTekVE ab0a7ae666 no joystick on this branch, causing issues 2025-08-17 15:25:40 +02:00
DevTekVE 11ec2f1f21 Apply suggestions from code review 2025-08-17 15:10:17 +02:00
DevTekVE 2c6808d37e Merge branch 'master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2025-08-17 15:02:28 +02:00
Jason Wen 2e96382c49 notebook init 2025-08-17 00:34:01 -04:00
DevTekVE 0239e440ca Adjust replay 2025-08-15 10:32:07 +02:00
Jason Wen 76b972daff Hyundai angle steering: STEERING_ANGLE_2 available on all cars 2025-08-14 01:56:46 -04:00
Jason Wen bc3ef3e7dd Hyundai angle steering: hugging no more - use the true steering angle signal from MDPS 2025-08-13 23:08:32 -04:00
DevTekVE 9839291dd0 Merge remote-tracking branch 'origin/master' into hkg-angle-steering-2025 2025-08-13 20:13:42 +02:00
DevTekVE 17cba328d6 refactor: update torque tuning configuration for angle steering
- Adjusted torque tuning configuration to avoid reliance on torque controller for Hyundai angle steering.
- Simplified control logic by removing unnecessary checks for torque control type.
refactor: clean up code formatting and improve test structure for torque reduction gain
2025-08-13 20:03:51 +02:00
DevTekVE 86093765d8 Merge branch 'master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2025-08-13 07:55:46 +02:00
DevTekVE 05fa1c8ae8 Adjust default params and cleanup 2025-08-12 21:36:22 +02:00
DevTekVE e8a40d6b85 Merge branch 'master' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
#	system/manager/process_config.py
2025-08-10 14:24:10 +02:00
DevTekVE c265e0bb85 Merge remote-tracking branch 'origin/master' into hkg-angle-steering-2025
# Conflicts:
#	common/params_keys.h
#	opendbc_repo
#	system/manager/manager.py
2025-08-02 09:56:58 +02:00
DevTekVE c6b118788b Merge branch 'master-new' into hkg-angle-steering-2025 2025-07-31 18:25:08 +02:00
DevTekVE b004f6dbdc Merge branch 'master-new' into hkg-angle-steering-2025 2025-07-31 18:17:34 +02:00
DevTekVE cd4930b680 Bump opendbc 2025-07-26 10:56:49 +02:00
DevTekVE f861aca628 Refactor vehicle model initialization and adjust angle limits for baseline model 2025-07-26 08:12:06 +02:00
DevTekVE 1ff0d8e2ee please don't bother me anymore! 2025-07-26 08:09:51 +02:00
DevTekVE d76d70764e Update slip factor precision for Hyundai steering parameters
- Adjusted `slip_factor` in Hyundai CANFD safety modes for improved consistency and accuracy.
- Ensured proper representation of `slip_factor` output in test logs.
2025-07-25 20:17:19 +02:00
DevTekVE dc73ce0b71 save tools replay 2025-07-25 19:47:31 +02:00
DevTekVE 3f666748af bump opendbc 2025-07-25 14:16:02 +02:00
DevTekVE 8de8a8838c bumo opendbc 2025-07-25 14:08:53 +02:00
DevTekVE f563b7eb71 Refactor steering angle limit application for improved safety and model compliance 2025-07-25 12:12:42 +02:00
DevTekVE 3c18b83708 save temp 2025-07-25 09:56:30 +02:00
DevTekVE bd35f5904b Enhance steering angle rate limiting and safety enforcement logic
- Introduced explicit post-rate limiting using model-specific dynamics.
- Improved low-speed smoothing and precision of applied angles.
2025-07-25 08:12:41 +02:00
DevTekVE 474f2737f6 Refactor steering angle limit logic for conservative seleion
- Removed unused lateral accel/jerk logic for simplicity.
- Updated angle limit calculations to choose the smallest delta for safer control.
2025-07-24 21:48:24 +02:00
DevTekVE ea1ac4a212 Revert "Add configurable max lateral accel and jerk parameters for Hyundai vehicles"
This reverts commit b95f8c5929.

Revert "Add baseline safety model and improve steering angle limiting logic"

This reverts commit b53cbb2e18.

Revert "Disable lateral accel/jerk params and ensure float consistency in angle limits"

This reverts commit 165d7c7b36.
2025-07-24 10:02:48 +02:00
DevTekVE 165d7c7b36 Disable lateral accel/jerk params and ensure float consistency in angle limits
- Commented out unused lateral accel/jerk parameters for clarity.
- Ensured `np.clip` always returns a float for precision.
2025-07-24 09:41:52 +02:00
DevTekVE b53cbb2e18 Add baseline safety model and improve steering angle limiting logic
- Introduced a baseline safety model (`GENESIS_GV80_2025`) for comparison.
- Enhanced steer angle limit calculation using both baseline and current limits for improved safety and precision.
2025-07-24 09:38:03 +02:00
DevTekVE b95f8c5929 Add configurable max lateral accel and jerk parameters for Hyundai vehicles
- Introduced user-configurable options for max lateral acceleration and jerk.
- Enables fine-tuning of vehicle handling for smoother control.
2025-07-24 08:39:10 +02:00
DevTekVE e564bb0b85 bumo openbc 2025-07-23 18:46:52 +02:00
DevTekVE e2ec8a7b13 Refine lateral control limits and simplify safety model handling
- Reduced max lateral acceleration and jerk by 20% for smoother handling.
- Removed unused `get_safety_CP` function, simplifying `VehicleModel` initialization.
2025-07-22 08:07:51 +02:00
DevTekVE 75c6f0f10e Test with gv80 as baseline for limits 2025-07-20 22:14:18 +02:00
DevTekVE af38044b42 Merge branch 'master-new' into hkg-angle-steering-2025 2025-07-20 10:14:46 +02:00
DevTekVE 684fa846d8 Merge branch 'master-new' into hkg-angle-steering-2025
# Conflicts:
#	.codespellignore
#	opendbc_repo
#	system/manager/manager.py
2025-07-19 21:51:46 +02:00
DevTekVE 416e722855 Update baseline model to IONIQ 5 PE for improved angle safety tuning
- Replaced conservative GENESIS_GV80_2025 model with IONIQ 5 PE parameters.
- Adjusted steering parameters (ratio, slip factor, wheelbase) for better lateral control performance.
2025-07-19 21:46:17 +02:00
DevTekVE 9fd4613bbb Add 2025 Kia EV6 support with updated radar and camera fingerprints 2025-07-09 09:47:31 +02:00
DevTekVE a0362e3c5f "Refined UI labels and tooltips for HKG tuning options to improve clarity and user understanding." 2025-06-29 16:48:53 +02:00
DevTekVE e32ef1cdc0 Fix incorrect torque sign usage in torque reduction calculation
Ensure `actuators.torque` uses its absolute value in the `calculate_angle_torque_reduction_gain` method to prevent sign-related issues during Hyundai steering angle control.
2025-06-29 14:33:30 +02:00
DevTekVE 20673ec8a6 Adjust warning font size in angle tuning settings panel. 2025-06-29 13:35:09 +02:00
DevTekVE 53fbdf7329 Rename "IdleTorque" to "ActiveTorque" for clarity.
The parameter name "HkgTuningAngleIdleTorqueReductionGain" was updated to "HkgTuningAngleActiveTorqueReductionGain" across multiple files for better clarity and alignment with its functionality. This change ensures consistency in naming conventions and improves code readability.
2025-06-29 13:23:56 +02:00
DevTekVE 6f72b74fac Add idle torque reduction for Hyundai lateral control
Introduced `ANGLE_IDLE_TORQUE_REDUCTION_GAIN` to manage torque when the vehicle is stationary, ensuring smoother handling and better lane centering. Updated parsing, parameters, and UI settings to support this new idle torque parameter. Adjusted torque calculation logic and smoothing factor behavior for enhanced control flexibility.
2025-06-29 13:22:18 +02:00
DevTekVE e83705a32e Merge branch 'master-new' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2025-06-29 09:44:01 +02:00
DevTekVE 95d36b9ba2 Refactor and enhance HKG angle tuning logic.
Introduced a toggle for angle smoothing factor and renamed related parameters for clarity. Refactored backend settings to use new parameter names and expanded smoothing matrices for better tuning granularity. Updated UI elements to reflect these changes, emphasizing usability and consistency.
2025-06-28 21:25:44 +02:00
DevTekVE b9e74254bd Merge branch 'master-new' into hkg-angle-steering-2025 2025-06-27 10:46:22 +02:00
DevTekVE b4405b200d Merge remote-tracking branch 'origin/master-new' into hkg-angle-steering-2025 2025-06-25 09:27:21 +02:00
DevTekVE a8a3fdac54 Rename parameter in calculate_target_torque for clarity. 2025-06-23 22:53:45 +02:00
DevTekVE 4eddc622a7 Refactor torque calculations in Hyundai controller
Rename methods and variables for clarity in torque reduction and override calculations. Adjust logic to streamline handling of steering inputs and improve maintainability.
2025-06-23 22:51:40 +02:00
DevTekVE 4e42ada240 Refactor torque management in Hyundai controller for cleaner override and ramp logic
Extract torque ramping and override functionality into dedicated methods within `LkasTorqueManager` to improve maintainability and reduce redundancy. Simplify `update` logic by delegating state-specific operations to new methods.
2025-06-23 22:47:51 +02:00
DevTekVE 6266217655 Introduce LkasTorqueManager for LKAS torque handling in Hyundai controller
Encapsulate LKAS torque calculations, ramping, and override logic into the new `LkasTorqueManager` class to improve modularity and maintainability. Replace existing torque logic with calls to the manager.
2025-06-23 22:20:10 +02:00
DevTekVE ea09d32e98 Remove lateral acceleration logic from Hyundai steering controller 2025-06-23 21:55:17 +02:00
DevTekVE 15958c88d3 Refactor lateral acceleration scaling logic in Hyundai controller
Move scaling of `max_angle_delta` under high lateral acceleration to improve clarity and prevent redundant operations.
2025-06-23 20:22:03 +02:00
DevTekVE b80d7fb5ea Adding GV70 electrified 2026 2025-06-22 14:30:59 +02:00
DevTekVE 1c3d25c6ff Refine lateral acceleration handling in Hyundai steering logic
Enforce absolute check for `real_a_lat` against `MAX_LATERAL_ACCEL` to improve angle scaling under high lateral acceleration conditions.
2025-06-22 11:03:35 +02:00
rav4kumar c9f22b32c7 Revert "Incorporate lateral acceleration in Hyundai angle steering logic"
This reverts commit c0524985bb.
2025-06-21 11:40:58 -07:00
DevTekVE c0524985bb Incorporate lateral acceleration in Hyundai angle steering logic
Add handling for IMU lateral acceleration to refine steering angle limits in CAN FD configurations. Parse and utilize `IMU_LatAccelVal` signal for enhanced lateral control accuracy.
2025-06-21 17:34:13 +02:00
DevTekVE 83839c7ea7 Add angle steering support and refactor related logic for Hyundai CAN FD.
Introduced support for CAN FD angle steering, including updated parameters, signal parsing, and new tests. Refactored related steering logic for clarity, reducing unused code and enhancing maintainability.
2025-06-21 13:38:50 +02:00
DevTekVE c1a1d4b4c3 Update lint script to exclude .xml files in layouts directory
Added `layouts/.*\.xml` to `IGNORED_FILES` in `lint.sh` to prevent linting of layout XML files.
2025-06-20 10:50:45 +02:00
DevTekVE b5af7a905a Merge branch 'master-new' into hkg-angle-steering-2025 2025-06-18 20:13:45 +02:00
DevTekVE 96b1b2f55f Update steering request logic in Hyundai controller
Ensure steering request activation depends on lateral control being active. This adds clarity and aligns better with control logic requirements.
2025-06-17 18:51:54 +02:00
DevTekVE 9361ba5d70 Refactor Hyundai steering angle handling logic
Streamline steering angle calculations and fault avoidance logic by removing redundant comments and unused code. Simplified `round_angle` implementation for clarity and consistency.
2025-06-17 12:08:06 +02:00
DevTekVE 4e9014311e Refactor steeringPressed logic in Hyundai carstate.py.
Revised the determination of `steeringPressed` to account for both hands-on-wheel detection and torque overriding in CAN FD setups. Simplified fallback logic for non-CAN FD configurations for better code clarity and maintainability.
2025-06-12 00:31:07 +02:00
DevTekVE 232873fc70 Refine steering press detection logic.
Adjusted the sensitivity and threshold values for `HOD_Dir_Status` in steering press updates, improving accuracy in detecting steering input. This change aligns with updated parameter requirements for better responsiveness.
2025-06-12 00:14:05 +02:00
DevTekVE 0a61fca9c9 Fix steering press detection for Hyundai models.
Updated the condition to detect steering press by changing HOD_Dir_Status threshold from `> 2` to `>= 2`. This ensures the detection logic aligns correctly with expected behavior.
2025-06-12 00:06:52 +02:00
DevTekVE 480bdc34dc Add support for CANFD angle steering in Hyundai cars
Introduced handling for the `HOD_FD_01_100ms` message when the CANFD angle steering flag is enabled. This ensures proper message parsing and extends compatibility for specific Hyundai vehicle configurations.
2025-06-12 00:04:44 +02:00
DevTekVE 716b475a13 Update Hyundai controls for HOD status and steer limits
Adjusted the steering override frame window and incorporated new HOD_Dir_Status to improve hands-on detection. Added parsing for new signals in Hyundai CAN FD, enhancing steering override responsiveness and reliability.
2025-06-12 00:01:31 +02:00
DevTekVE b1ec5ec034 Adjust override angle cap in Hyundai car controller
Increased the minimum override angle cap from 0.01 to 0.1 and explicitly cast the maximum cap to a float. This change improves consistency and ensures proper handling of steering limits.
2025-06-11 23:20:19 +02:00
DevTekVE 470613c2b7 Adjust Hyundai steer override parameters for improved control.
Reduced the override frame window and updated the angle cap logic to use MAX_ANGLE_RATE. These changes aim to enhance steering responsiveness and safety by fine-tuning steer angle limits.
2025-06-11 23:07:49 +02:00
DevTekVE 336c5b4154 Remove smoothing_factor from Hyundai car controller logic
The `smoothing_factor` parameter and related logic have been removed to simplify the steering angle smoothing approach. All references and usage of this parameter have been eliminated, relying solely on speed-based dynamic interpolation. This change streamlines the code while maintaining functionality.
2025-06-11 23:04:32 +02:00
DevTekVE abdb9dc750 Adjust Hyundai steering override frame logic
Reduced `OVERRIDE_FRAME_WINDOW` and updated condition to properly respect override frame limits. This ensures smoother handling and more precise steering adjustments under certain driving scenarios.
2025-06-11 22:49:04 +02:00
DevTekVE ab98683973 Refactor steering override logic in Hyundai carcontroller
Replaced `recently_overridden` with `frames_since_override` for better granularity and added dynamic override angle limits using interpolation. These changes enhance steering control accuracy during user overrides and improve overall code readability.
2025-06-11 22:42:05 +02:00
DevTekVE 186c24dbe6 Refine Hyundai steering override handling logic
Adjusted logic for recently overridden steering to improve angle limits and torque smoothing. Removed unused or redundant code, optimizing the functionality and maintaining cleaner readability.
2025-06-11 21:49:18 +02:00
DevTekVE 9cdf6340a1 Refactor steering angle smoothing for clarity and reuse.
Extracted the steering angle smoothing logic into a standalone function `sp_smooth_angle` to enhance readability and reusability. Adjusted angle smoothing parameters and introduced a maximum vehicle speed threshold for applying smoothing. Minor updates improve maintainability and ensure consistent behavior across speed ranges.
2025-06-11 10:07:44 +02:00
DevTekVE 2855b1341c Adjust steering thresholds for Hyundai CAN FD vehicles
Updated `STEER_THRESHOLD` to 350 and `NO_LONGER_OVERRIDING_THRESHOLD` to 150 for better alignment with Hyundai CAN FD steering behavior. These changes ensure improved compatibility and more accurate steering response.
2025-06-10 09:53:08 +02:00
DevTekVE cf28f99976 Revert "Add twilsonco's LKAS torque calculator for improved lateral control"
This reverts commit b1770fb0e7aece0e160b1b083cb260edbbdc53dd.
2025-06-10 09:40:59 +02:00
DevTekVE a39d67dc47 Fix apply_angle_last reset logic in Hyundai carcontroller
Re-enables resetting `apply_angle_last` to `steering_angle` when steering is recently overridden. This ensures proper handling of steering angle limits during transitions.
2025-06-08 19:04:02 +02:00
DevTekVE 7e75257f12 Refine Hyundai steering control logic.
Simplified torque ramp-up logic by combining conditions and adjusted `STEER_THRESHOLD` for CANFD angle steering. These changes aim to enhance control precision and maintain consistency in overrides.
2025-06-08 19:02:49 +02:00
DevTekVE df38449553 Reduce override timeout for Hyundai carcontroller
Decrease the override timeout from 100 to 50 frames, ensuring quicker recognition of driver input override. This improves responsiveness and aligns with refined control behavior.
2025-06-08 18:22:44 +02:00
DevTekVE 1385ef3bc5 Fix steering control behavior during user override
Removed restrictive rate limiting during recent user overrides to improve steering response. Adjusted logic to ensure correct handling of steering angle when lateral control is inactive or overridden.
2025-06-08 18:01:47 +02:00
DevTekVE 7c23c11c51 Refine steering logic with override detection.
Adjust steering behavior to account for recent user overrides, improving safety and control. Introduced a "recently_overridden" check to limit angle rates and torque adjustments when user intervention is detected.
2025-06-08 17:52:16 +02:00
DevTekVE aeff2e12ec Refine steering logic with user override handling.
Added logic to use the current steering angle when the steering wheel is pressed, ensuring smoother transitions during user overrides. Updated function parameters and implementation to reflect this enhancement.
2025-06-08 17:38:34 +02:00
DevTekVE 7274899671 Refactor Hyundai override logic for steering thresholds
Removed redundant `recently_overridden` logic and introduced a more robust approach for tracking user steering overrides. Added `NO_LONGER_OVERRIDING_THRESHOLD` and updated conditions to improve steer override handling. Adjustments ensure smoother torque transitions and more accurate steering state detection.
2025-06-08 17:00:44 +02:00
DevTekVE 6c00fd608f pass tests? 2025-06-08 12:28:11 +02:00
DevTekVE df6a034c11 Bump opendbc 2025-06-08 12:26:09 +02:00
DevTekVE acb109c290 adding plotjuggler stuff 2025-06-08 10:02:44 +02:00
DevTekVE b227b00249 Update torque clamping to use parameterized min torque
Replaced hardcoded `angle_min_active_torque` with `ANGLE_MIN_TORQUE` from params for better configurability and consistency. This ensures the torque clamping logic aligns with defined parameters.
2025-06-07 19:43:01 +02:00
DevTekVE 3ec9d6c18a Merge branch 'master-new' into hkg-angle-steering-2025
# Conflicts:
#	common/params_keys.h
2025-06-07 15:04:16 +02:00
DevTekVE 86db8b95f0 Refactor torque calculation and deactivate live tuning.
Updated torque calculation logic with a new optional parameter for minimum active torque, streamlining control behavior. Deactivated and cleaned up references to HkgAngleLiveTuning, simplifying configuration and reducing runtime complexities. Updated relevant UI and parameter descriptions for clarity.
2025-06-07 11:55:57 +02:00
DevTekVE 4cfff8a35f Merge branch 'master-new' into hkg-angle-steering-2025 2025-06-06 23:08:37 +02:00
DevTekVE 962fedf48c Merge branch 'master-new' into hkg-angle-steering-2025
# Conflicts:
#	opendbc/car/tests/routes.py
2025-06-06 20:49:06 +02:00
DevTekVE 04494414d1 Merge branch 'master-new' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2025-06-05 09:18:53 +02:00
DevTekVE 0b83576e9b Adjust torque ramping logic and update steering thresholds
Increase the override window and refine torque ramp-up behavior to avoid conflicts during recent overrides. Updated steering driver allowance and threshold values for CANFD angle steering to improve compatibility and performance.
2025-06-02 09:49:31 +02:00
DevTekVE ce4ef0f817 Refine steering override logic in Hyundai car controller
Added logic to track recent steering overrides and adjust LKAS torque behavior accordingly. This ensures smoother transitions when the steering is overridden and reduces potential conflicts with driver input. Updated CANFD-specific steering thresholds for enhanced compatibility.
2025-06-02 09:12:03 +02:00
DevTekVE f0b15c1c56 Adding twil's torque calculation 2025-06-01 19:04:34 +02:00
DevTekVE f898e9fdfe Merge branch 'master-new' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2025-06-01 09:46:45 +02:00
DevTekVE 8ee7804b0e Bump opendbc (no tesla controls, no twil yet) 2025-05-29 16:31:42 +02:00
DevTekVE 923228194e bump opendbc to prior tesla changes until i can pass safety validations 2025-05-28 12:44:44 +02:00
DevTekVE 8837b2e3f6 Merge branch 'master-new' into hkg-angle-steering-2025
# Conflicts:
#	opendbc_repo
2025-05-28 12:36:02 +02:00
DevTekVE f48c9dc1c2 bump opendbc 2025-05-25 17:34:24 +02:00
DevTekVE 74aa07a8cd Ingore something i dont control thx 2025-05-25 17:31:37 +02:00
DevTekVE 5236e4860f Make lint happy, maybe 2025-05-25 17:31:37 +02:00
DevTekVE 3d174da1c3 adding some of my tests and validaitons 2025-05-25 17:31:25 +02:00
DevTekVE 8faa40f3a3 clean 2025-05-25 17:31:25 +02:00
DevTekVE 3e03275f28 Add PlotJuggler layout for analyzing torque and angle data
This new layout visualizes actuator data, CAN steering messages, and car state variables. It provides multiple time-series plots to aid in debugging and analysis. Plugin configurations are also included for extended functionality.
2025-05-25 17:31:22 +02:00
DevTekVE 7595cf8a25 Refine Hyundai angle and torque control logic.
Simplified control flag handling for angle steering, adjusted torque calculations for smoother ramp rates, and updated tuning parameters for the Hyundai Ioniq 5 PE. Minor adjustment to return value handling in lateral control functions.
2025-05-25 17:31:22 +02:00
DevTekVE 2675d43adb bump opendbc
Remove duplicate STEER_ANGLE_SATURATION_THRESHOLD import

Cleaned up an unnecessary duplicate import of STEER_ANGLE_SATURATION_THRESHOLD from latcontrol_angle_torque. This simplifies the module imports and prevents potential redundancy or confusion.

Refactor lateral control to combine torque and angle logic

Merged functionalities of LatControlTorque and LatControlAngle into a single LatControlAngleTorque class. Refactored code to utilize methods from both parent classes, reducing duplication and improving maintainability.

Add angle-torque hybrid lateral control for Hyundai CAN FD

Introduces `LatControlAngleTorque` to enable hybrid angle and torque-based steering for specific Hyundai models. Updates related logic in carcontroller, interface, and controlsd to accommodate this new lateral control method. Adjusts torque parameters for enhanced control in supported models.
2025-05-25 17:31:21 +02:00
DevTekVE d9f4ce82e6 clean 2025-05-25 17:31:21 +02:00
DevTekVE 648a1845d8 cleanup the mess 2025-05-25 17:31:21 +02:00
DevTekVE a87eff6d1c Add HKG Angle Live Tuning parameter and update related handling 2025-05-25 17:31:21 +02:00
DevTekVE dd6ad37e23 Absolutely zero clue on this, I did it with AI and it's for me to play. Don't take this notebook seriously please 2025-05-25 17:31:21 +02:00
DevTekVE c5e778b939 How annoying the linter on a comment lol 2025-05-25 17:31:21 +02:00
DevTekVE a9ab81a77a useless but should keep linter happy 2025-05-25 17:31:21 +02:00
DevTekVE 2d40e1d8e5 Refactor torque parameter handling in Hyundai carcontroller
Replaced direct access to `params` with instance variables for torque parameters to improve code clarity and maintainability. Updated smoothing factor description in angle tuning settings to include speed-related behavior. This enhances readability and prepares for further tuning adjustments.
2025-05-25 17:31:20 +02:00
DevTekVE 7c8f367a5d Fix data type for HkgTuningOverridingCycles value
Updated the value of HkgTuningOverridingCycles to a string for consistency with other parameters in the tuning configuration. This ensures proper handling and avoids potential issues with type mismatches.

Add overriding cycles parameter for torque adjustment

Introduced "HkgTuningOverridingCycles" for configurable user override torque ramp-down cycles. Updated relevant logic in torque control and UI settings to handle the new parameter. This improves flexibility in adjusting steering torque override behavior.
2025-05-25 17:31:20 +02:00
DevTekVE ff4cf558aa Add HKG angle tuning settings with min/max torque parameters
Introduce separate angle tuning controls for HKG vehicles, including smoothing factor, min torque, and max torque parameters. Refactor developer panel to integrate the new settings into a dedicated UI panel, enhancing modularity and customization capabilities.
2025-05-25 17:31:20 +02:00
DevTekVE 0e151e51bc Update HKG Angle Smoothing Factor description in Developer Panel
Enhanced the description to clarify its effect on steering behavior. Included details on how the smoothing factor impacts steering smoothness using EMA, aiding user understanding.
2025-05-25 17:31:20 +02:00
DevTekVE 60cc0031b0 Revert "Revert "Revert the EMA calculation on the curvature to test another approach""
This reverts commit 58fcda8c
2025-05-25 17:31:20 +02:00
DevTekVE d24cac0998 Refactor steering angle logic for smoother control adjustments
Refactored the calculation and application of the steering angle to improve code clarity and ensure smoother transitions. Removed unused parameter update logic in `latcontrol_angle.py` and enhanced handling of driver overrides in `carcontroller.py`.
2025-05-25 17:31:20 +02:00
DevTekVE 45d110830c Fix typo in parameter access method.
Replaced `self._params` with `self.params` to correctly access the parameter `HkgTuningAngleSmoothingFactor`. This ensures the smoothing factor is updated as intended during the control loop.
2025-05-25 17:31:20 +02:00
DevTekVE ea3a9ae911 Improve angle smoothing by integrating dynamic parameter tuning
Introduced a dynamic smoothing factor using the `HkgTuningAngleSmoothingFactor` parameter. This allows more granular control over curvature smoothing based on customizable user input, enhancing driving smoothness. Added necessary logic to process and apply this parameter efficiently.
2025-05-25 17:31:19 +02:00
DevTekVE 6bff8c0e7c Revert "Revert the EMA calculation on the curvature to test another approach"
This reverts commit bd471b3498.
2025-05-25 17:31:19 +02:00
DevTekVE bc6b8802b8 Add HKG angle smoothing factor for steering adjustments
Introduced a new parameter, `HkgTuningAngleSmoothingFactor`, to apply exponential moving average (EMA) smoothing to steering angle changes, reducing sudden adjustments. Added associated UI controls, parameter persistence, and integration into Hyundai carcontroller logic for improved steering stability.
2025-05-25 17:31:19 +02:00
DevTekVE 252ef572d3 Revert the EMA calculation on the curvature to test another approach 2025-05-25 17:31:19 +02:00
DevTekVE 414d397e3f Handle missing pygame import gracefully
Wrap the pygame import in a try-except block to catch ImportError. This prevents the script from crashing and provides a clear message prompting the user to install pygame if it's missing.

Remove "inputs" package and update "pygame" dependency

The "inputs" package has been removed from the lockfile and dependency list, while "pygame" is now included universally without the "dev" extra marker. This change simplifies dependencies and ensures consistency across environments.

Update dependencies: replace 'inputs' with 'pygame'

Replaced the 'inputs' library with 'pygame' for joystickd dependencies in `pyproject.toml`. Additionally, removed a redundant 'pygame' entry from the general dependencies.

Ugly, I know, but soundd is unhappy with joystick

Allowing lat with mads

Invert steering input for joystick control

The steering axis input is now multiplied by -1 to reverse its direction. This ensures correct handling of the left stick's horizontal input, aligning behavior with expected control dynamics.

Refactor joystick control to use pygame for broader support

Replaced the `inputs` library with `pygame` for joystick handling, providing improved compatibility with Xbox and PlayStation controllers. Added initialization, adaptive mappings, deadzone handling, and enhanced event processing for robust joystick operation. Updated README with dependencies and usage information for Xbox controllers.
2025-05-25 17:31:19 +02:00
DevTekVE 11b7b3789d Adjust speed thresholds in filter_speed_matrox.
Updated the `filter_speed_matrox` values to improve curvature filtering behavior at different speeds. This change ensures better handling and stability across a wider range of driving conditions.
2025-05-25 17:31:19 +02:00
DevTekVE 871ac53717 Optimize curvature filtering by adding speed-dependent logic.
Introduced speed-based dynamic alpha adjustment using interpolation for smoother curvature filtering. This improves steering angle calculations by adapting filter sensitivity to vehicle speed, enhancing control performance.
2025-05-25 17:31:19 +02:00
DevTekVE 64ea66b6e6 chsnge alpha to nicer value 2025-05-25 17:31:19 +02:00
DevTekVE 6d7c6759b3 Adjust curvature handling and filtering parameters
Updated curvature breakpoints and torque scaling for improved control in sharp turns. Increased filter alpha for faster curvature response while maintaining system stability.
2025-05-25 17:31:18 +02:00
DevTekVE 4cea013570 Adjust curvature handling and filtering parameters
Updated curvature breakpoints in Hyundai carcontroller to improve torque scaling for curved driving. Slightly refined the filter coefficient in lateral control for smoother curvature filtering and more accurate steering adjustments.
2025-05-25 17:31:18 +02:00
DevTekVE eb375c0587 Refactor curvature-based steering angle and torque logic.
Introduced dynamic torque scaling based on curvature for smoother and more adaptive steering control. Replaced raw curvature inputs with filtered curvature for enhanced stability and reduced noise in steering angle calculations. Removed unused speed scaling logic to simplify the lateral control flow.
2025-05-25 17:31:18 +02:00
DevTekVE 7fd8a5a4bd Reapply "Significant improvement on the jerkiness"
This reverts commit 85ce84e7b7.
2025-05-25 17:31:18 +02:00
DevTekVE b3c90216bb Revert "Significant improvement on the jerkiness"
This reverts commit ea1af879ba2905b076ccfe65993a9db701d689dd.

Revert "More improvement but still not quite"

This reverts commit ad95493c5c61b2ace7c459d2ebc151ddaa80040f.

Revert "Adjust low-speed scaling for lateral control angle"

This reverts commit 6f789ac1ebb66b0239b4028303573c2d7d386b39.

Revert "Refactor speed-based steering scaling logic."

This reverts commit 1d40735ab8db8d470ff3b287a6b42847beffff7d.
2025-05-25 17:31:18 +02:00
DevTekVE 10f345f956 Refactor speed-based steering scaling logic.
Updated the steering angle computation to use a clearer and more descriptive speed-scaling configuration. Replaced low-speed-specific logic with a generalized approach based on speed breakpoints and corresponding influence factors. This improves maintainability and ensures smoother steering adjustments at varying speeds.
2025-05-25 17:31:18 +02:00
DevTekVE 956d2c36d0 Adjust low-speed scaling for lateral control angle
Refined the low-speed scaling parameters by modifying speed breakpoints and factors. This improves handling at lower speeds for smoother and more predictable behavior.
2025-05-25 17:31:18 +02:00
DevTekVE 55e688b6f2 More improvement but still not quite 2025-05-25 17:31:17 +02:00
DevTekVE f017954027 Significant improvement on the jerkiness 2025-05-25 17:31:17 +02:00
DevTekVE 7e992d11b1 bump panda and opendbc 2025-05-25 17:31:15 +02:00
203 changed files with 5051 additions and 6845 deletions
+1
View File
@@ -2,5 +2,6 @@ Wen
REGIST
PullRequest
cancelled
indeces
FOF
NoO
@@ -121,7 +121,7 @@ jobs:
if-no-files-found: error
build_model:
runs-on: [self-hosted, "${{ inputs.target_hardware == 'chestnut' && 'chestnut' || 'tici' }}"]
runs-on: [self-hosted, chestnut]
needs: get_model
env:
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
-1
View File
@@ -4,7 +4,6 @@
[submodule "opendbc"]
path = opendbc_repo
url = https://github.com/sunnypilot/opendbc.git
branch = tn
[submodule "msgq"]
path = msgq_repo
url = https://github.com/sunnypilot/msgq.git
+7
View File
@@ -21,5 +21,12 @@
</clean>
</configuration>
</target>
<target id="f2590b2b-9b93-49f9-8510-da3f3724a2ae" name="replay" defaultType="TOOL">
<configuration id="d475264f-6f4c-4092-9b4e-6773309f38b7" name="replay" toolchainName="Default">
<build type="TOOL">
<tool actionId="Tool_External Tools_uv build tools replay" />
</build>
</configuration>
</target>
</component>
</project>
+7
View File
@@ -20,4 +20,11 @@
<option name="WORKING_DIRECTORY" value="$ProjectFileDir$" />
</exec>
</tool>
<tool name="uv build tools replay" showInMainMenu="false" showInEditor="false" showInProject="false" showInSearchPopup="false" disabled="false" useConsole="true" showConsoleOnStdOut="false" showConsoleOnStdErr="false" synchronizeAfterRun="true">
<exec>
<option name="COMMAND" value="bash" />
<option name="PARAMETERS" value="-c &quot;source .venv/bin/activate &amp;&amp; scons -u -j$(nproc) tools/replay/&quot;" />
<option name="WORKING_DIRECTORY" value="$ProjectFileDir$" />
</exec>
</tool>
</toolSet>
+1 -1
View File
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build Debug" type="CLionExternalRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$ProjectFileDir$/selfdrive/ui" PASS_PARENT_ENVS_2="true" PROJECT_NAME="sunnypilot" TARGET_NAME="uv Scons Build Debug" CONFIG_NAME="uv Scons Build Debug" RUN_PATH="ui">
<configuration default="false" name="Build Debug" type="CLionExternalRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$ProjectFileDir$/selfdrive/ui" PASS_PARENT_ENVS_2="true" PROJECT_NAME="openpilot-special" TARGET_NAME="uv Scons Build Debug" CONFIG_NAME="uv Scons Build Debug" RUN_PATH="ui">
<envs>
<env name="QT_DBL_CLICK_DIST" value="150" />
</envs>
+27
View File
@@ -0,0 +1,27 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Debug Route Controls" type="PythonConfigurationType" factoryName="Python">
<module name="openpilot-special" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="FINGERPRINT" value="KIA_EV9" />
<env name="SKIP_FW_QUERY" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/selfdrive/car" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/selfdrive/car/card.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="true" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>
+7
View File
@@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Replay for controls + ui" type="Multirun" separateTabs="false" reuseTabsWithFailures="false" startOneByOne="true" markFailedProcess="true" hideSuccessProcess="false" delayTime="0.0">
<runConfiguration name="replay for controls" type="Native Application" />
<runConfiguration name="Build Debug" type="Custom Build Application" />
<method v="2" />
</configuration>
</component>
+7
View File
@@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="replay for controls" type="CLionNativeAppRunConfigurationType" focusToolWindowBeforeRun="true" PROGRAM_PARAMS="&quot;$Prompt$&quot; --block &quot;sendcan,carState,carParams,carOutput,liveTracks,carParamsSP,carStateSP,bookmarkButton&quot;" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="true" WORKING_DIR="file://$ProjectFileDir$/tools/replay" PASS_PARENT_ENVS_2="true" PROJECT_NAME="openpilot-special" TARGET_NAME="replay" CONFIG_NAME="replay" version="1" RUN_PATH="replay">
<method v="2">
<option name="CLION.COMPOUND.BUILD" enabled="true" />
</method>
</configuration>
</component>
+1 -1
View File
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="19.7"
export AGNOS_VERSION="19.6"
fi
export STAGING_ROOT="/data/safe_staging"
-16
View File
@@ -204,16 +204,11 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
aTarget @5 :Float32;
events @6 :List(OnroadEventSP.Event);
e2eAlerts @7 :E2eAlerts;
accelController @8 :AccelController;
struct DynamicExperimentalControl {
state @0 :DynamicExperimentalControlState;
enabled @1 :Bool;
active @2 :Bool;
decelIntent @3 :Float32;
curveDetected @4 :Bool;
wantBlended @5 :Bool;
leadVeto @6 :Bool;
enum DynamicExperimentalControlState {
acc @0;
@@ -311,17 +306,6 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
greenLightAlert @0 :Bool;
leadDepartAlert @1 :Bool;
}
struct AccelController {
enabled @0 :Bool;
active @1 :Bool;
profile @2 :Profile;
enum Profile {
eco @0;
normal @1;
sport @2;
}
}
}
struct OnroadEventSP @0xda96579883444c35 {
-2
View File
@@ -725,7 +725,6 @@ struct ChestnutState {
pcieLtssm @7 :UInt8;
supplyVoltage @8 :UInt16; # mV
supplyCurrent @9 :Int16; # mA
supplyFault @10 :Bool;
}
struct RadarState @0x9a185389d6fdd05f {
@@ -1005,7 +1004,6 @@ struct DrivingModelData {
frameIdExtra @1 :UInt32;
frameDropPerc @6 :Float32;
modelExecutionTime @7 :Float32;
big @8 :Bool;
action @2 :ModelDataV2.Action;
+11 -11
View File
@@ -56,29 +56,29 @@
},
{
"name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d.img.xz",
"hash": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
"hash_raw": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
"url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz",
"hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
"hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
"size": 46897152,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "d12e1e5b9455b62a1464558716493b33e470d7a7e88da1c4105a3b21d0961808"
"ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e"
},
{
"name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img.xz",
"hash": "74ffc9c551e1f29cda897ace8a69080fe644f8039977c6885f2b48362e39b744",
"hash_raw": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz",
"hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b",
"hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
"size": 4718592000,
"sparse": true,
"full_check": false,
"has_ab": true,
"ondevice_hash": "6a992680183685eea9db99d915219a37935f45989330d9b619e880450257f448",
"ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468",
"alt": {
"hash": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img",
"hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img",
"size": 4718592000
}
}
]
]
+1 -2
View File
@@ -5,7 +5,6 @@ import logging
import os
import select
import signal
import string
import struct
import subprocess
import tempfile
@@ -355,7 +354,7 @@ class Modem:
imei = ""
iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F")
if not all(c in string.hexdigits for c in iccid):
if not iccid.isdigit():
iccid = ""
imsi = first_line("AT+CIMI")
+1 -7
View File
@@ -4,17 +4,11 @@ from pathlib import Path
CHESTNUT_FW_VERSION = "ed4e39b7"
CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
CHESTNUT_USB_PRODUCT = f"custom {CHESTNUT_FW_VERSION}-CLEAN"
USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
PRIMARY_USB_CONTROLLER = "a600000.ssusb"
def is_chestnut_usb_id(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool:
ids = CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS if include_bootloader else CHESTNUT_USB_IDS
return (vendor_id, product_id) in ids
def get_usb_topology() -> set[str]:
try:
return set(os.listdir(USB_DEVICES_PATH))
@@ -87,7 +81,7 @@ def set_usb_state(device_state, devices: list[dict]) -> None:
entry.linkErrorCount = device["linkErrorCount"]
entry.usb3Lane = device.get("usb3Lane", "unknown")
if is_chestnut_usb_id(entry.vendorId, entry.productId):
if (entry.vendorId, entry.productId) in CHESTNUT_USB_IDS:
chestnut_present = True
device_state.chestnutPresent = chestnut_present
-17
View File
@@ -92,12 +92,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ChestnutNotDetected", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutOverheated", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ChestnutPcieUnavailable", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ChestnutUncompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutUpdateFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutUsbSlow", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ExcessiveActuation", {PERSISTENT, JSON}},
@@ -138,7 +132,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
{"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"ChestnutModelError", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"Version", {PERSISTENT, STRING}},
// --- sunnypilot params --- //
@@ -194,12 +187,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
// toyota specific params
{"ToyotaAutoHold", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaEnhancedBsm", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaTSS2Long", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaDriveMode", {PERSISTENT | BACKUP, BOOL, "0"}},
// MADS params
{"Mads", {PERSISTENT | BACKUP, BOOL, "1"}},
{"MadsMainCruiseAllowed", {PERSISTENT | BACKUP, BOOL, "1"}},
@@ -249,10 +236,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
// Accel Controller profiles (Eco / Normal / Sport)
{"AccelPersonalityEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
{"AccelPersonality", {PERSISTENT | BACKUP, INT, "1"}},
// sunnypilot model params
{"CameraOffset", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
{"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
+4 -4
View File
@@ -27,14 +27,14 @@ public:
auto param_path = Params().getParamPath();
if (util::file_exists(param_path)) {
std::string real_path = util::readlink(param_path);
util::check_system(util::string_format("rm -rf %s", real_path.c_str()));
util::check_system(util::string_format("rm %s -rf", real_path.c_str()));
unlink(param_path.c_str());
}
if (getenv("COMMA_CACHE") == nullptr) {
util::check_system(util::string_format("rm -rf %s", Path::download_cache_root().c_str()));
util::check_system(util::string_format("rm %s -rf", Path::download_cache_root().c_str()));
}
util::check_system(util::string_format("rm -rf %s", Path::comma_home().c_str()));
util::check_system(util::string_format("rm -rf %s", msgq_path.c_str()));
util::check_system(util::string_format("rm %s -rf", Path::comma_home().c_str()));
util::check_system(util::string_format("rm %s -rf", msgq_path.c_str()));
unsetenv("OPENPILOT_PREFIX");
}
-4
View File
@@ -117,16 +117,12 @@ class TestParams(OpenpilotTestCase):
def test_params_default_value(self):
self.params.remove("LanguageSetting")
self.params.remove("LongitudinalPersonality")
self.params.remove("AccelPersonalityEnabled")
self.params.remove("AccelPersonality")
self.params.remove("LiveParametersV2")
assert self.params.get("LanguageSetting") is None
assert self.params.get("LanguageSetting", return_default=False) is None
assert isinstance(self.params.get("LanguageSetting", return_default=True), str)
assert isinstance(self.params.get("LongitudinalPersonality", return_default=True), int)
assert self.params.get("AccelPersonalityEnabled", return_default=True) is False
assert self.params.get("AccelPersonality", return_default=True) == 1
assert self.params.get("LiveParametersV2") is None
assert self.params.get("LiveParametersV2", return_default=True) is None
+2
View File
@@ -37,6 +37,8 @@ SP_BRANCH_MIGRATIONS = {
("tizi", "release3-staging"): "release-tizi-staging",
("mici", "release3"): "release-mici",
("mici", "release3-staging"): "release-mici-staging",
("tici", "hkg-angle-steering-2025"): "hkg-angle-steering-2025-tici",
("tici", "hkg-angle-steering-2025-prebuilt"): "hkg-angle-steering-2025-tici-prebuilt"
}
BUILD_METADATA_FILENAME = "build.json"
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:07bda2fe5d6be0b2854044053c384fe002e96406da119863a443b9344258b500
size 1544
+1 -4
View File
@@ -11,13 +11,13 @@ from opendbc.car.structs import car
from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper
from openpilot.common.swaglog import cloudlog, ForwardingHandler
from opendbc.car import DT_CTRL, structs
from opendbc.car.can_definitions import CanData, CanRecvCallable, CanSendCallable
from opendbc.car.carlog import carlog
from opendbc.car.fw_versions import ObdCallback
from opendbc.car.car_helpers import get_car, interfaces
from opendbc.car.interfaces import CarInterfaceBase, RadarInterfaceBase
from opendbc.safety import ALTERNATIVE_EXPERIENCE
from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp
from openpilot.selfdrive.car.cruise import VCruiseHelper
from openpilot.selfdrive.car.helpers import convert_carControlSP, convert_to_capnp
@@ -123,9 +123,6 @@ class Car:
self.RI = RI
self.CP.alternativeExperience = 0
if self.params.get_bool("ToyotaAutoHold"):
self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ALLOW_AEB
# mads
set_alternative_experience(self.CP, self.CP_SP, self.params)
set_car_specific_params(self.CP, self.CP_SP, self.params)
+8 -65
View File
@@ -13,13 +13,12 @@ V_CRUISE_MIN = 8
V_CRUISE_MAX = 145
V_CRUISE_UNSET = 255
V_CRUISE_INITIAL = 40
V_CRUISE_INITIAL_EXPERIMENTAL_MODE = 128
V_CRUISE_INITIAL_EXPERIMENTAL_MODE = 105
IMPERIAL_INCREMENT = round(CV.MPH_TO_KPH, 1) # round here to avoid rounding errors incrementing set speed
ButtonEvent = car.CarState.ButtonEvent
ButtonType = car.CarState.ButtonEvent.Type
CRUISE_LONG_PRESS = 50
TOYOTA_VIRTUAL_CRUISE_LONG_PRESS = 65
CRUISE_NEAREST_FUNC = {
ButtonType.accelCruise: math.ceil,
ButtonType.decelCruise: math.floor,
@@ -44,30 +43,6 @@ class VCruiseHelper(VCruiseHelperSP):
def v_cruise_initialized(self):
return self.v_cruise_kph != V_CRUISE_UNSET
@property
def software_pcm_cruise_speed(self) -> bool:
return self.CP.brand == "toyota" and self.CP.pcmCruise and self.CP.openpilotLongitudinalControl and not self.CP_SP.pcmCruiseSpeed
@property
def cruise_long_press_frames(self) -> int:
return TOYOTA_VIRTUAL_CRUISE_LONG_PRESS if self.software_pcm_cruise_speed else CRUISE_LONG_PRESS
@property
def software_pcm_cruise_initialized(self) -> bool:
return 0 < self.v_cruise_kph < V_CRUISE_UNSET and 0 < self.v_cruise_cluster_kph < V_CRUISE_UNSET
def _apply_software_pcm_cruise_delta(self, delta_kph: float, is_metric: bool) -> None:
"""Move Toyota's planner/display targets together while respecting both targets' bounds."""
cluster_min_kph = self.v_cruise_min if is_metric else self.v_cruise_min * CV.MPH_TO_KPH
min_delta = max(V_CRUISE_MIN - self.v_cruise_kph, cluster_min_kph - self.v_cruise_cluster_kph)
max_delta = min(V_CRUISE_MAX - self.v_cruise_kph, V_CRUISE_MAX - self.v_cruise_cluster_kph)
if delta_kph > 0:
applied_delta = min(delta_kph, max(0., max_delta))
else:
applied_delta = max(delta_kph, min(0., min_delta))
self.v_cruise_kph = round(self.v_cruise_kph + applied_delta, 1)
self.v_cruise_cluster_kph = round(self.v_cruise_cluster_kph + applied_delta, 1)
def update_v_cruise(self, CS, enabled, is_metric):
self.v_cruise_kph_last = self.v_cruise_kph
@@ -76,21 +51,11 @@ class VCruiseHelper(VCruiseHelperSP):
_enabled = self.update_enabled_state(CS, enabled)
if CS.cruiseState.available:
software_pcm_enabled = not self.CP_SP.pcmCruiseSpeed and _enabled
if self.software_pcm_cruise_speed:
software_pcm_enabled = software_pcm_enabled and self.software_pcm_cruise_initialized
if not self.CP.pcmCruise or software_pcm_enabled:
if not self.CP.pcmCruise or (not self.CP_SP.pcmCruiseSpeed and _enabled):
# if stock cruise is completely disabled, then we can use our own set speed logic
self._update_v_cruise_non_pcm(CS, _enabled, is_metric)
v_cruise_kph_before_sla = self.v_cruise_kph
self.update_speed_limit_assist_v_cruise_non_pcm()
if self.software_pcm_cruise_speed:
sla_delta_kph = self.v_cruise_kph - v_cruise_kph_before_sla
self.v_cruise_kph = v_cruise_kph_before_sla
self._apply_software_pcm_cruise_delta(sla_delta_kph, is_metric)
else:
self.v_cruise_cluster_kph = self.v_cruise_kph
self.v_cruise_cluster_kph = self.v_cruise_kph
else:
self.v_cruise_kph = CS.cruiseState.speed * CV.MS_TO_KPH
self.v_cruise_cluster_kph = CS.cruiseState.speedCluster * CV.MS_TO_KPH
@@ -120,13 +85,13 @@ class VCruiseHelper(VCruiseHelperSP):
for b in CS.buttonEvents:
if b.type.raw in self.button_timers and not b.pressed:
if self.button_timers[b.type.raw] > self.cruise_long_press_frames:
if self.button_timers[b.type.raw] > CRUISE_LONG_PRESS:
return # end long press
button_type = b.type.raw
break
else:
for k, timer in self.button_timers.items():
if timer and timer % self.cruise_long_press_frames == 0:
if timer and timer % CRUISE_LONG_PRESS == 0:
button_type = k
long_press = True
break
@@ -150,26 +115,10 @@ class VCruiseHelper(VCruiseHelperSP):
return
long_press, v_cruise_delta = VCruiseHelperSP.update_v_cruise_delta(self, long_press, v_cruise_delta)
# Toyota's canonical PCM set speed and displayed cluster set speed can differ. In
# software-owned PCM mode, round the value the driver sees and apply the same delta
# to both targets so the planner/cluster calibration offset remains intact.
v_cruise_reference = self.v_cruise_cluster_kph if self.software_pcm_cruise_speed else self.v_cruise_kph
if long_press and v_cruise_reference % v_cruise_delta != 0: # partial interval
v_cruise_reference_new = CRUISE_NEAREST_FUNC[button_type](v_cruise_reference / v_cruise_delta) * v_cruise_delta
if long_press and self.v_cruise_kph % v_cruise_delta != 0: # partial interval
self.v_cruise_kph = CRUISE_NEAREST_FUNC[button_type](self.v_cruise_kph / v_cruise_delta) * v_cruise_delta
else:
v_cruise_reference_new = v_cruise_reference + v_cruise_delta * CRUISE_INTERVAL_SIGN[button_type]
if self.software_pcm_cruise_speed:
delta_kph = v_cruise_reference_new - v_cruise_reference
# If SET is pressed while overriding, do not lower the target below the current speed.
if CS.gasPressed and button_type in (ButtonType.decelCruise, ButtonType.setCruise):
delta_kph = max(delta_kph, CS.vEgo * CV.MS_TO_KPH - self.v_cruise_kph)
self._apply_software_pcm_cruise_delta(delta_kph, is_metric)
return
self.v_cruise_kph += v_cruise_reference_new - v_cruise_reference
self.v_cruise_kph += v_cruise_delta * CRUISE_INTERVAL_SIGN[button_type]
# If set is pressed while overriding, clip cruise speed to minimum of vEgo
if CS.gasPressed and button_type in (ButtonType.decelCruise, ButtonType.setCruise):
@@ -178,12 +127,6 @@ class VCruiseHelper(VCruiseHelperSP):
self.v_cruise_kph = np.clip(round(self.v_cruise_kph, 1), self.v_cruise_min, V_CRUISE_MAX)
def update_button_timers(self, CS, enabled):
if self.software_pcm_cruise_speed and (not enabled or not CS.cruiseState.available or not self.software_pcm_cruise_initialized):
for k in self.button_timers:
self.button_timers[k] = 0
self.button_change_states[k] = {"standstill": False, "enabled": False}
return
# increment timer for buttons still pressed
for k in self.button_timers:
if self.button_timers[k] > 0:
@@ -14,11 +14,8 @@ MAX_LATERAL_JERK = 5.0 # m/s^3
MAX_LATERAL_ACCEL_NO_ROLL = 3.0 # m/s^2
STOPPING_SPEED = 0.25 # m/s, speed at which the car goes into the stopping state
def should_stop(v_ego: float, a_target: float) -> bool:
return bool(v_ego < STOPPING_SPEED and a_target < 0.1)
return bool(v_ego < 0.3 and a_target < 0.1)
def clamp(val, min_val, max_val):
clamped_val = float(np.clip(val, min_val, max_val))
@@ -7,8 +7,6 @@ from openpilot.selfdrive.modeld.constants import ModelConstants
CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N]
STOPPING_DECEL_RATE = 0.3 # m/s^2/s while trying to stop
LongCtrlState = car.CarControl.Actuators.LongControlState
@@ -70,7 +68,7 @@ class LongControl:
if output_accel > self.CP.stopAccel:
output_accel = min(output_accel, 0.0)
# TODO: can we just go straight to stopAccel?
output_accel -= STOPPING_DECEL_RATE * DT_CTRL
output_accel -= 1.0 * DT_CTRL # m/s^2/s while trying to stop
self.reset()
else: # LongCtrlState.pid
@@ -35,12 +35,9 @@ def get_max_accel(v_ego):
def get_coast_accel(pitch):
return np.sin(pitch) * -5.65 - 0.3 # fitted from data using xx/projects/allow_throttle/compute_coast_accel.py
def get_cruise_accel(e2e, v_cruise, v_ego, a_cruise_prev, angle_steers, CP, dt, accel_coast, allow_throttle,
max_accel_override=None):
if max_accel_override is not None:
max_accel = max_accel_override
else:
max_accel = ACCEL_MAX if e2e else get_max_accel(v_ego)
def get_cruise_accel(e2e, v_cruise, v_ego, a_cruise_prev, angle_steers, CP, dt, accel_coast, allow_throttle):
max_accel = ACCEL_MAX if e2e else get_max_accel(v_ego)
if not e2e:
a_total_max = np.interp(v_ego, _A_TOTAL_MAX_BP, _A_TOTAL_MAX_V)
a_y = v_ego ** 2 * angle_steers * CV.DEG_TO_RAD / (CP.steerRatio * CP.wheelbase)
@@ -87,8 +84,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
v_ego = sm['carState'].vEgo
v_cruise_kph = min(sm['carState'].vCruise, V_CRUISE_MAX)
v_cruise = v_cruise_kph * CV.KPH_TO_MS
force_decel = sm['controlsState'].forceDecel
if force_decel:
if sm['controlsState'].forceDecel:
v_cruise = 0.0
long_control_off = sm['controlsState'].longControlState == LongCtrlState.off
@@ -122,7 +118,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
self.mpc.set_cur_state(self.v_desired_filter.x, self.output_a_target)
self.mpc.update(sm['radarState'], personality=sm['selfdriveState'].personality)
self.update_dec(sm)
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
@@ -145,17 +140,9 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
is_e2e = self.is_e2e(sm)
max_accel_override = self.get_max_accel_override(v_ego)
v_cruise = self.get_cruise_target_override(v_ego, v_cruise, force_decel)
a_cruise_prev = self.a_cruise
gated_cruise = get_cruise_accel(is_e2e, v_cruise, v_ego, a_cruise_prev, steer_angle_without_offset,
self.CP, self.dt, accel_coast, self.allow_throttle, max_accel_override)
ungated_cruise = get_cruise_accel(is_e2e, v_cruise, v_ego, a_cruise_prev, steer_angle_without_offset,
self.CP, self.dt, accel_coast, True, max_accel_override)
self.a_cruise = self.arbitrate_cruise_candidate(
sm, gated_cruise, ungated_cruise, output_a_target_mpc, self.mpc.source,
allow_throttle=self.allow_throttle, e2e=is_e2e, force_decel=force_decel,
)
self.a_cruise = get_cruise_accel(is_e2e, v_cruise, v_ego,
self.a_cruise, steer_angle_without_offset, self.CP, self.dt,
accel_coast, self.allow_throttle)
cruise_should_stop = should_stop(v_ego, self.a_cruise)
candidates = [(output_a_target_mpc, self.mpc.source, output_should_stop_mpc),
@@ -166,7 +153,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
output_a_target, self.mpc.source, _ = min(candidates, key=lambda c: c[0])
self.output_should_stop = any(should_stop for _, _, should_stop in candidates)
self.output_a_target = np.clip(output_a_target, ACCEL_MIN, ACCEL_MAX)
self.accel_controller_active = self.is_accel_controller_active(force_decel)
self.v_desired_filter.x = self.v_desired_filter.x + self.dt * (self.output_a_target + a_prev) / 2.0
@@ -191,7 +177,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
longitudinalPlan.aTarget = float(self.output_a_target)
longitudinalPlan.shouldStop = bool(self.output_should_stop)
longitudinalPlan.allowBrake = True
# Raw model throttle intent used for path visualization; lead MPC can still request positive acceleration.
longitudinalPlan.allowThrottle = bool(self.allow_throttle)
pm.send('longitudinalPlan', plan_send)
@@ -1,7 +1,6 @@
from openpilot.common.test import OpenpilotTestCase
from openpilot.cereal import custom
from openpilot.selfdrive.controls.lib.drive_helpers import STOPPING_SPEED, should_stop
from openpilot.selfdrive.controls.lib.longcontrol import STOPPING_DECEL_RATE, LongCtrlState, long_control_state_trans
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState, long_control_state_trans
class TestLongControlStateTransition(OpenpilotTestCase):
@@ -43,13 +42,3 @@ class TestLongControlStateTransition(OpenpilotTestCase):
next_state = long_control_state_trans(CP_SP, active, current_state,
should_stop=False, brake_pressed=False, cruise_standstill=False)
assert next_state == LongCtrlState.pid
class TestTerminalStop(OpenpilotTestCase):
def test_stopping_tune_is_gentler_than_upstream_default(self):
# Upstream #38394 hardcoded a 1.0 m/s^2/s ramp and a 0.3 m/s latch. comma's own one-stopping-tune uses
# 0.3 / 0.25, and every stop recorded on this car was driven with that pair. Both must stay on the less
# braking side, or a future edit re-deepens the terminal brake unnoticed - which already happened once.
assert 0.0 < STOPPING_DECEL_RATE <= 1.0
assert 0.0 < STOPPING_SPEED <= 0.3
assert should_stop(STOPPING_SPEED - 0.01, 0.0)
assert not should_stop(0.29, 0.0) # the band upstream would latch in and we do not
+18 -13
View File
@@ -10,6 +10,11 @@ from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path
CAMERA_CONFIGS = [
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
]
Import('env', 'arch')
chunker_file = File("#openpilot/common/file_chunker.py")
lenv = env.Clone()
@@ -19,22 +24,20 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "
if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))]
def estimate_pickle_max_size(onnx_size):
# QCOM programs for models with spatial recurrent features can approach 2x
# the ONNX size. Overestimating only adds an empty trailing chunk.
return 2.0 * onnx_size + 10 * 1024 * 1024
return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty
if arch == 'comma_arm64':
from openpilot.common.hardware import HARDWARE
camera = _os_fisheye if HARDWARE.get_device_type() == "mici" else _ar_ox_fisheye
camera_configs = [(camera.width, camera.height)]
tg_backend = 'QCOM'
tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
else:
camera_configs = [(c.width, c.height) for c in (_ar_ox_fisheye, _os_fisheye)]
tg_backend = 'CPU'
tg_flags = f'DEV=CPU' if arch == 'Darwin' else 'DEV=CPU:LLVM'
tg_devices = { # which device to put jit inputs to at runtime
'openpilot.selfdrive.modeld.modeld': {
'default': {'WARP_DEV': tg_backend, 'QUEUE_DEV': tg_backend},
'chestnut': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'}
},
'openpilot.selfdrive.modeld.dmonitoringmodeld': {
'default': {'DEV': tg_backend}
},
@@ -42,7 +45,7 @@ tg_devices = { # which device to put jit inputs to at runtime
CHESTNUT = chestnut_present()
if CHESTNUT:
chestnut_tg_flags = 'DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_OCCUPANCY_OPT=1'
chestnut_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2'
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
chestnut_lock = File("models/.chestnut.lock").abspath
@@ -73,9 +76,10 @@ frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
if not os.getenv('SKIP_TINYGRAD_COMPILE'):
for chestnut in [False, True] if CHESTNUT else [False]:
target_pkl_path = File(modeld_pkl_path(chestnut)).abspath
file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('', tg_flags)
# BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a chestnut
file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags)
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in camera_configs)
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS)
# CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it.
taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else ''
cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py '
@@ -103,7 +107,7 @@ if not os.getenv('SKIP_TINYGRAD_COMPILE'):
actions = Action(do_compile, " [CHESTNUT] $TARGET") if chestnut else [cmd, Action(do_chunk, " [CHUNK] $TARGET")]
node = lenv.Command(
chunk_targets,
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(camera_res_args), Value(chunk_targets), chunker_file],
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file],
actions,
)
if chestnut:
@@ -117,7 +121,7 @@ lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_file
dm_w, dm_h = DM_INPUT_SIZE
compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")]
for cam_w, cam_h in camera_configs:
for cam_w, cam_h in CAMERA_CONFIGS:
dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath
cmd = (f'{tg_flags} {mac_brew_string} python3 {modeld_dir}/compile_dm_warp.py '
f'--camera-resolution {cam_w}x{cam_h} --warp-to {dm_w}x{dm_h} '
@@ -139,4 +143,5 @@ def tg_compile(flags, model_name):
Action(do_chunk, " [CHUNK] $TARGET")],
)
tg_compile(tg_flags, 'dmonitoring_model')
if not os.getenv('SKIP_TINYGRAD_COMPILE'):
tg_compile(tg_flags, 'dmonitoring_model')
+73 -86
View File
@@ -37,12 +37,17 @@ from tinygrad.engine.jit import TinyJit
NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size'])
MODELD_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
WARP_INPUTS = ['tfm', 'big_tfm']
POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
WARP_DEV = os.getenv('WARP_DEV')
def nv12_copy_size(stride: int, y_height: int, uv_height: int) -> int:
# Retain the padded Y and UV plane storage, but skip the trailing kernel/guard allocation.
return stride * (y_height + uv_height)
def make_random_images(keys, shape, device=None):
return {k: Tensor.randint(shape, low=0, high=256, dtype='uint8', device=device).realize() for k in keys}
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None):
@@ -94,7 +99,7 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
def frame_prepare_tinygrad(input_frame, M_inv):
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=Device.DEFAULT)
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=WARP_DEV)
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
with Context(SPLIT_REDUCEOP=0):
@@ -113,43 +118,49 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
return frame_prepare_tinygrad
def make_warp_input_queues(vision_input_shapes, frame_skip, device):
img = vision_input_shapes['img'] # (1, 12, 128, 256)
n_frames = img[1] // 6
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
npy = {
'tfm': np.zeros((3, 3), dtype=np.float32),
'big_tfm': np.zeros((3, 3), dtype=np.float32),
}
input_queues = {
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
**{k: Tensor(v, device='NPY').realize() for k, v in npy.items()},
}
return input_queues, npy
def get_policy_npy_shapes(input_shapes):
dp = input_shapes['desire_pulse'] # (1, 25, 8)
tc = input_shapes['traffic_convention'] # (1, 2)
at = input_shapes['action_t'] # (1, 2)
fb = input_shapes['features_buffer'] # (1, T-1, ...) e.g. (1, 24, 32, 512) with spatial features
feat_dim = math.prod(fb[2:])
fb = input_shapes['features_buffer'] # (1, 24, 512)
# TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now
shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], feat_dim)}
shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], fb[2])}
return shapes, [math.prod(s) for s in shapes.values()]
def make_input_queues(input_shapes, frame_skip, device, frame_copy_size):
img = input_shapes['img'] # (1, 12, 128, 256)
fb = input_shapes['features_buffer'] # (1, T-1, ...), past features only; the model appends the current frame's feature
feat_dim = math.prod(fb[2:])
dp = input_shapes['desire_pulse'] # (1, 25, 8)
n_frames = img[1] // 6
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
def make_input_queues(input_shapes, frame_skip, device):
input_queues, npy = make_warp_input_queues(input_shapes, frame_skip, device)
policy_shapes, _ = get_policy_npy_shapes(input_shapes)
shapes = {'tfm': (3, 3), 'big_tfm': (3, 3)} | policy_shapes
sizes = [math.prod(s) for s in shapes.values()]
packed_npy_size = sum(sizes) * np.dtype(np.float32).itemsize
packed_input = np.zeros(packed_npy_size + 2 * frame_copy_size, dtype=np.uint8)
packed_npy_inputs = packed_input[:packed_npy_size].view(np.float32)
frames = packed_input[packed_npy_size:]
frame_views = {'img': frames[:frame_copy_size], 'big_img': frames[frame_copy_size:]}
fb = input_shapes['features_buffer'] # (1, 24, 512), past features only; the model appends the current frame's feature
dp = input_shapes['desire_pulse'] # (1, 25, 8)
shapes, sizes = get_policy_npy_shapes(input_shapes)
packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32)
# views into the packed inputs, to be refilled at runtime
npy = {k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}
input_queues = {
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], feat_dim), dtype=np.float32), device=device).contiguous().realize(),
npy.update({k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)})
input_queues.update({
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(),
'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(),
'packed_npy_inputs': Tensor(packed_input, device='NPY').realize(),
}
return input_queues, npy, frame_views
'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(),
})
return input_queues, npy
def shift_and_sample(buf, new_val, sample_fn):
@@ -165,15 +176,13 @@ def sample_desire(buf, frame_skip):
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
def make_warp(nv12, model_w, model_h):
def make_warp(nv12, model_w, model_h, frame_skip):
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
def warp(tfm, big_tfm, frame, big_frame):
tfm = tfm.to(Device.DEFAULT)
big_tfm = big_tfm.to(Device.DEFAULT)
frame = frame.to(Device.DEFAULT)
big_frame = big_frame.to(Device.DEFAULT)
Tensor.realize(tfm, big_tfm, frame, big_frame)
tfm = tfm.to(WARP_DEV)
big_tfm = big_tfm.to(WARP_DEV)
Tensor.realize(tfm, big_tfm)
warped_frame = frame_prepare(frame, tfm).unsqueeze(0)
warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0)
@@ -186,10 +195,10 @@ def make_run_policy(model_runner, model_metadata, frame_skip):
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
npy_shapes, npy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
model_input_dtypes = {name: spec.dtype for name, spec in model_runner.graph_inputs.items()}
def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT)
warped = warped.to(Device.DEFAULT)
Tensor.realize(packed_npy_inputs, warped)
img = shift_and_sample(img_q, warped[0:1], sample_skip_fn)
@@ -202,50 +211,33 @@ def make_run_policy(model_runner, model_metadata, frame_skip):
inputs = {
'img': img,
'big_img': big_img,
'features_buffer': feat_buf.reshape(model_metadata['input_shapes']['features_buffer']),
'features_buffer': feat_buf,
'desire_pulse': desire_buf,
'traffic_convention': traffic_convention,
'action_t': action_t,
}
inputs = {name: value.cast(model_input_dtypes[name]) for name, value in inputs.items()}
out = next(iter(model_runner(inputs).values())).cast('float32')
return out,
return run_policy
def make_run_model(warp, run_policy, model_metadata, frame_copy_size):
_, policy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
packed_npy_size = (18 + sum(policy_sizes)) * np.dtype(np.float32).itemsize
def run_model(img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
packed_input = packed_npy_inputs.to(Device.DEFAULT)
Tensor.realize(packed_input)
packed_npy_inputs = packed_input[:packed_npy_size].bitcast('float32')
frame = packed_input[packed_npy_size:packed_npy_size + frame_copy_size]
big_frame = packed_input[packed_npy_size + frame_copy_size:]
tfm, big_tfm, policy_inputs = packed_npy_inputs.split([9, 9, sum(policy_sizes)])
warped = warp(tfm.reshape(3, 3), big_tfm.reshape(3, 3), frame, big_frame)
return run_policy(warped, img_q, big_img_q, feat_q, desire_q, policy_inputs)
return run_model
def compile_jit(jit, input_keys, make_queues, benchmark_runs):
if benchmark_runs < 1:
raise ValueError("benchmark_runs must be at least 1")
def compile_jit(jit, make_random_inputs, input_keys, make_queues):
SEED = 42
def random_inputs_run(fn, seed, n_runs, test_val=None, test_buffers=None, expect_match=True):
input_queues, npy, frame_views = make_queues(Device.DEFAULT)
def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True):
input_queues, npy = make_queues(Device.DEFAULT)
rng = np.random.default_rng(seed)
Tensor.manual_seed(seed)
testing = test_val is not None or test_buffers is not None
n_runs = 1 if testing else 3
for i in range(n_runs):
for v in npy.values():
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
for v in frame_views.values():
v[:] = rng.integers(0, 256, size=v.shape, dtype=np.uint8)
Device.default.synchronize()
random_inputs = make_random_inputs()
st = time.perf_counter()
outs = fn(**{k: input_queues[k] for k in input_keys})
outs = fn(**{k: input_queues[k] for k in input_keys}, **random_inputs)
mt = time.perf_counter()
Device.default.synchronize()
et = time.perf_counter()
@@ -264,15 +256,14 @@ def compile_jit(jit, input_keys, make_queues, benchmark_runs):
return val, buffers
print('capture + replay')
test_val, test_buffers = random_inputs_run(jit, SEED, 3)
print(f'pickle round trip ({benchmark_runs} runs per seed)')
test_val, test_buffers = random_inputs_run(jit, SEED)
print('pickle round trip')
with tempfile.TemporaryFile(dir=".") as f:
dump_oob(jit, f)
f.seek(0)
loaded_jit = load_oob(f)
random_inputs_run(loaded_jit, SEED, benchmark_runs, test_val, test_buffers, expect_match=True)
random_inputs_run(loaded_jit, SEED+1, benchmark_runs, test_val, test_buffers, expect_match=False)
# Keep the original so per-resolution JITs share model weight buffers in the final pickle.
jit = load_oob(f)
random_inputs_run(jit, SEED, test_val, test_buffers, expect_match=True)
random_inputs_run(jit, SEED+1, test_val, test_buffers, expect_match=False)
return jit
@@ -301,31 +292,27 @@ if __name__ == "__main__":
p.add_argument('--onnx', required=True)
p.add_argument('--output', required=True)
p.add_argument('--frame-skip', type=int, required=True)
p.add_argument('--benchmark-runs', type=int, default=1,
help='timed loaded-JIT runs for each correctness seed')
args = p.parse_args()
model_path = read_file_chunked_to_disk(args.onnx)
model_w, model_h = args.model_size
model_runner = OnnxRunner(model_path)
out = {
'metadata': make_metadata_dict(model_path),
'input_devices': {'model': Device.DEFAULT},
'run_model': {},
}
out = {'metadata': make_metadata_dict(model_path)}
run_policy = make_run_policy(model_runner, out['metadata'], args.frame_skip)
run_policy_jit = TinyJit(make_run_policy(model_runner, out['metadata'], args.frame_skip), prune=True)
make_policy_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip)
make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, *out['metadata']['input_shapes']['img'][2:]), device=WARP_DEV)
out['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS,
make_policy_queues)
for cam_w, cam_h in args.camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
frame_copy_size = nv12_copy_size(nv12.stride, nv12.y_height, nv12.uv_height)
make_model_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip,
frame_copy_size=frame_copy_size)
warp = make_warp(nv12, model_w, model_h)
run_model_jit = TinyJit(make_run_model(warp, run_policy, out['metadata'], frame_copy_size), prune=True)
out['run_model'][(cam_w,cam_h)] = compile_jit(run_model_jit, MODELD_INPUTS, make_model_queues,
args.benchmark_runs)
make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=WARP_DEV)
warp = TinyJit(make_warp(nv12, model_w, model_h, args.frame_skip), prune=True)
make_warp_queues = partial(make_warp_input_queues, out['metadata']['input_shapes'], args.frame_skip)
out[(cam_w,cam_h)] = compile_jit(warp, make_random_warp_inputs, WARP_INPUTS, make_warp_queues)
with open(args.output, "wb") as f:
dump_oob(out, f)
@@ -64,7 +64,6 @@ def fill_driving_model_data(msg: capnp._DynamicStructBuilder, modelv2_send: capn
driving_model_data.frameIdExtra = modelV2.frameIdExtra
driving_model_data.frameDropPerc = modelV2.frameDropPerc
driving_model_data.modelExecutionTime = modelV2.modelExecutionTime
driving_model_data.big = modelV2.big
driving_model_data.action = modelV2.action
driving_model_data.meta.laneChangeState = modelV2.meta.laneChangeState
driving_model_data.meta.laneChangeDirection = modelV2.meta.laneChangeDirection
+2 -8
View File
@@ -7,12 +7,10 @@ import tempfile
from pathlib import Path
from openpilot.common.file_chunker import get_manifest_path
from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, USB_DEVICES_PATH, is_chestnut_usb_id
from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_USB_IDS, USB_DEVICES_PATH
MODELS_DIR = Path(__file__).resolve().parent / 'models'
TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json'
CHESTNUT_POWERED_VOLTAGE = 5000
CHESTNUT_PCIE_READY = 0x78
def get_tg_input_devices(process_name: str, chestnut: bool):
@@ -52,7 +50,7 @@ def chestnut_present() -> bool:
try:
usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16))
product = (d / "product").read_text().strip()
if is_chestnut_usb_id(*usb_id) and product == CHESTNUT_USB_PRODUCT:
if usb_id in CHESTNUT_USB_IDS and product == f"custom {CHESTNUT_FW_VERSION}-CLEAN":
return True
except Exception:
pass
@@ -60,7 +58,3 @@ def chestnut_present() -> bool:
def chestnut_compiled() -> bool:
return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file()
def chestnut_ready(state) -> bool:
return state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE and not state.supplyFault and state.pcieLtssm == CHESTNUT_PCIE_READY
+40 -77
View File
@@ -4,8 +4,8 @@ import ctypes
from functools import cached_property
import os
os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom
from tinygrad.tensor import Tensor
from tinygrad.device import Device
import usb1
import struct
import threading
import time
@@ -28,17 +28,17 @@ from openpilot.common.transformations.model import get_warp_matrix
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, nv12_copy_size, MODELD_INPUTS
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState
from openpilot.common.file_chunker import open_file_chunked
from openpilot.common.hardware.usb import CHESTNUT_USB_IDS
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, chestnut_ready, modeld_pkl_path, load_oob
from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, modeld_pkl_path, get_tg_input_devices, load_oob
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld"
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
LAT_SMOOTH_SECONDS = 0.0
@@ -83,37 +83,6 @@ class ChestnutState:
self.valid = True
self.sends = 0
self.metrics = {}
self._asm_usb = None
def _close_asm_usb(self) -> None:
if self._asm_usb is not None:
self._asm_usb.close()
self._asm_usb = None
def _open_asm_usb(self):
context = usb1.USBContext()
for vendor_id, product_id in CHESTNUT_USB_IDS:
if (handle := context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True)) is not None:
return handle
context.close()
def _read_ina(self) -> tuple[int, int, bool]:
if "AMD" in Device._opened_devices and self._asm_usb is None:
try:
raw = Device["AMD"].iface.pci_dev.usb.usb.control_read(0xC0, 5)
return struct.unpack('<Hh?', bytes(raw))
except Exception:
pass
if self._asm_usb is None:
self._asm_usb = self._open_asm_usb()
if self._asm_usb is None:
raise usb1.USBErrorNoDevice
try:
raw = self._asm_usb.controlRead(0xC0, 0xC0, 0, 0, 5, timeout=100)
except usb1.USBError:
self._close_asm_usb()
raise
return struct.unpack('<Hh?', bytes(raw))
@cached_property
def power_limit(self) -> int:
@@ -149,15 +118,13 @@ class ChestnutState:
setattr(state, k, v)
asm_valid = False
try:
# ASM runs on USB-C power, these still read without a gpu
state.supplyVoltage, state.supplyCurrent, state.supplyFault = self._read_ina()
asm_valid = True
except Exception:
pass
if "AMD" in Device._opened_devices:
try:
state.pcieLtssm = Device["AMD"].iface.pci_dev.usb.read(0xB450, 1)[0]
# ASM runs on USB-C power, these still read without a gpu
asm = Device["AMD"].iface.pci_dev.usb
state.pcieLtssm = asm.read(0xB450, 1)[0]
state.supplyVoltage, state.supplyCurrent = struct.unpack('<Hh', bytes(asm.usb.control_read(0xC0, 5))[:4])
asm_valid = True
except Exception:
pass
@@ -180,9 +147,9 @@ class ModelState(ModelStateBase):
def __init__(self, cam_w: int, cam_h: int, chestnut: bool):
ModelStateBase.__init__(self)
input_devices = get_tg_input_devices(PROCESS_NAME, chestnut)
self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV']
jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut)))
input_devices = jits['input_devices']
self.model_device = input_devices['model']
metadata = jits['metadata']
self.input_shapes = metadata['input_shapes']
self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
@@ -192,11 +159,13 @@ class ModelState(ModelStateBase):
self.chestnut = chestnut
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
self.frame_copy_size = nv12_copy_size(*get_nv12_info(cam_w, cam_h)[:3])
self.input_queues, self.npy, self.frame_views = make_input_queues(
self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size)
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
self.full_frames: dict[str, Tensor] = {}
self._blob_cache: dict[tuple[str, int], Tensor] = {}
self.parser = Parser()
self.run_model = jits['run_model'][(cam_w,cam_h)]
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
self.run_policy = jits['run_policy']
self.warp = jits[(cam_w,cam_h)]
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
@@ -204,8 +173,14 @@ class ModelState(ModelStateBase):
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]:
for key, buf in bufs.items():
np.copyto(self.frame_views[key], np.frombuffer(buf.data, dtype=np.uint8, count=self.frame_copy_size))
for key in bufs.keys():
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
yuv_size = self.frame_buf_params[key][3]
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
cache_key = (key, ptr)
if cache_key not in self._blob_cache:
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8', device=self.WARP_DEV)
self.full_frames[key] = self._blob_cache[cache_key]
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
inputs['desire_pulse'][0] = 0
@@ -216,7 +191,11 @@ class ModelState(ModelStateBase):
self.npy['tfm'][:,:] = transforms['img'][:,:]
self.npy['big_tfm'][:,:] = transforms['big_img'][:,:]
outs, = self.run_model(**{k: self.input_queues[k] for k in MODELD_INPUTS})
warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames['img'], big_frame=self.full_frames['big_img'])
outs, = self.run_policy(
**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped
)
if after_enqueue is not None:
after_enqueue()
model_output = outs.numpy()[0]
@@ -230,37 +209,25 @@ class ModelState(ModelStateBase):
return outputs_dict
def warmup(self) -> None:
dummy_frames = {k: np.zeros(self.frame_copy_size, dtype=np.uint8) for k in self.vision_input_names}
dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self.vision_input_names}
eye = np.eye(3, dtype=np.float32)
dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2}
self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()})
self.input_queues, self.npy, self.frame_views = make_input_queues(
self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size)
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
self.prev_desire[:] = 0
self.full_frames.clear()
self._blob_cache.clear()
def main(demo=False):
cloudlog.warning("modeld init")
chestnut_available = chestnut_present() and chestnut_compiled()
CHESTNUT = False
if chestnut_available:
poller = messaging.Poller()
sock = messaging.sub_sock("chestnutState", poller=poller, conflate=True)
deadline = time.monotonic() + 4. / SERVICE_LIST['deviceState'].frequency
while not CHESTNUT and (remaining := deadline - time.monotonic()) > 0.:
if not poller.poll(round(remaining * 1000)):
break
msg = messaging.recv_one_or_none(sock)
CHESTNUT = msg is not None and msg.valid and chestnut_ready(msg.chestnutState)
CHESTNUT = chestnut_present() and chestnut_compiled()
if CHESTNUT:
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
params = Params()
params.put_bool("ChestnutLoading", CHESTNUT)
if chestnut_available and not CHESTNUT:
params.put_bool("ChestnutActive", False)
else:
params.remove("ChestnutActive")
params.remove("ChestnutActive")
config_realtime_process(7, 54)
@@ -304,11 +271,7 @@ def main(demo=False):
loader.start()
loader.join(BIG_MODEL_TIMEOUT)
model = big_model
if model is None:
params.put_bool("ChestnutModelError", True)
params.put_bool("ChestnutActive", model is not None)
if model is not None:
params.remove("ChestnutModelError")
small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None
if model is None:
@@ -442,7 +405,6 @@ def main(demo=False):
raise
# fallback to small model
cloudlog.exception("big model failed, fall back to small")
params.put_bool("ChestnutModelError", True)
params.put_bool("ChestnutActive", False)
assert small_model is not None
model = small_model
@@ -469,11 +431,12 @@ def main(demo=False):
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
lane_change_prob = l_lane_change_prob + r_lane_change_prob
mdv2sp_send = messaging.new_message('modelDataV2SP')
left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge)
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send = messaging.new_message('modelDataV2SP')
left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
fill_driving_model_data(drivingdata_send, modelv2_send)
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1791d5940b2c048d0639813426dd2cf1d6f2a6727ed51e17c8bcea8bbe754123
size 765950064
oid sha256:a501760a9d1d5fef0eab2b8c5d122d06124fc26dc8e0782e0aa94b82a208f0ff
size 1757355221
+10 -10
View File
@@ -123,22 +123,22 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda
ps.setUptime(health.uptime_pkt);
ps.setSafetyTxBlocked(health.safety_tx_blocked_pkt);
ps.setSafetyRxInvalid(health.safety_rx_invalid_pkt);
ps.setIgnitionLine((health.flags_pkt & HEALTH_FLAG_IGNITION_LINE) != 0U);
ps.setIgnitionCan((health.flags_pkt & HEALTH_FLAG_IGNITION_CAN) != 0U);
ps.setControlsAllowed((health.flags_pkt & HEALTH_FLAG_CONTROLS_ALLOWED) != 0U);
ps.setIgnitionLine(health.ignition_line_pkt);
ps.setIgnitionCan(health.ignition_can_pkt);
ps.setControlsAllowed(health.controls_allowed_pkt);
ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt);
ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt);
ps.setPandaType(hw_type);
ps.setSafetyModel(cereal::CarParams::SafetyModel(health.safety_mode_pkt));
ps.setSafetyParam(health.safety_param_pkt);
ps.setFaultStatus(cereal::PandaState::FaultStatus(health.fault_status_pkt));
ps.setPowerSaveEnabled((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U);
ps.setHeartbeatLost((health.flags_pkt & HEALTH_FLAG_HEARTBEAT_LOST) != 0U);
ps.setPowerSaveEnabled((bool)(health.power_save_enabled_pkt));
ps.setHeartbeatLost((bool)(health.heartbeat_lost_pkt));
ps.setAlternativeExperience(health.alternative_experience_pkt);
ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt));
ps.setInterruptLoad(health.interrupt_load_pkt / 255.0f);
ps.setInterruptLoad(health.interrupt_load_pkt);
ps.setFanPower(health.fan_power);
ps.setSafetyRxChecksInvalid((health.flags_pkt & HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID) != 0U);
ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid_pkt));
ps.setSpiErrorCount(health.spi_error_count_pkt);
ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f);
ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f);
@@ -198,10 +198,10 @@ std::optional<bool> send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa
}
if (spoofing_started) {
health.flags_pkt |= HEALTH_FLAG_IGNITION_LINE;
health.ignition_line_pkt = 1;
}
bool ignition_local = ((health.flags_pkt & (HEALTH_FLAG_IGNITION_LINE | HEALTH_FLAG_IGNITION_CAN)) != 0U) && !always_offroad;
bool ignition_local = ((health.ignition_line_pkt != 0) || (health.ignition_can_pkt != 0)) && !always_offroad;
// Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node
if (health.safety_mode_pkt == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) {
@@ -209,7 +209,7 @@ std::optional<bool> send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa
}
bool power_save_desired = !ignition_local;
if (((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U) != power_save_desired) {
if (health.power_save_enabled_pkt != power_save_desired) {
panda->set_power_saving(power_save_desired);
}
+11
View File
@@ -29,6 +29,12 @@ enum SpiError {
const unsigned int SPI_ACK_TIMEOUT = 500; // milliseconds
const std::string SPI_DEVICE = "/dev/spidev0.0";
// TODO: fix SPI turnaround synchronization at the protocol level.
static uint64_t spi_last_bus_activity_ns = 0; // protected by hw_lock
static void wait_for_spi_turnaround(uint64_t start_ns) {
while ((nanos_since_boot() - start_ns) < 400000) {}
}
class LockEx {
public:
@@ -319,6 +325,8 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
assert(tx_len < SPI_BUF_SIZE);
assert(max_rx_len < SPI_BUF_SIZE);
wait_for_spi_turnaround(spi_last_bus_activity_ns);
xfer_count++;
header = {
.sync = SPI_SYNC,
@@ -347,6 +355,7 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
if (ret < 0) {
goto fail;
}
wait_for_spi_turnaround(nanos_since_boot());
// Send data
if (tx_data != NULL) {
@@ -389,6 +398,7 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
memcpy(rx_data, rx_buf + 3, rx_data_len);
}
spi_last_bus_activity_ns = nanos_since_boot();
return rx_data_len;
fail:
@@ -403,6 +413,7 @@ fail:
}
}
spi_last_bus_activity_ns = nanos_since_boot();
if (ret >= 0) ret = -1;
return ret;
}
@@ -19,30 +19,6 @@
},
"Offroad_ChestnutBranch": {
"text": "Chestnut detected! Switch to the %1 branch to use chestnut-class models.",
"severity": -1
},
"Offroad_ChestnutNotDetected": {
"text": "Chestnut not detected. Check USB and 12V connections.",
"severity": 0
},
"Offroad_ChestnutOverheated": {
"text": "Chestnut overheated. Ensure good airflow. Current GPU temperature is %1.",
"severity": 0
},
"Offroad_ChestnutPcieUnavailable": {
"text": "%1",
"severity": 0
},
"Offroad_ChestnutUncompiled": {
"text": "Chestnut model not compiled. Keep ignition on and reboot the comma.",
"severity": 0
},
"Offroad_ChestnutUpdateFailed": {
"text": "Chestnut update failed. Check the USB cable.",
"severity": 0
},
"Offroad_ChestnutUsbSlow": {
"text": "Chestnut USB link is slow. Check the USB cable. The current speed is %1.",
"severity": 0
},
"Offroad_UnregisteredHardware": {
@@ -11,15 +11,6 @@ from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPl
from openpilot.selfdrive.controls.radard import _LEAD_ACCEL_TAU
class PlannerSM(dict):
def __init__(self, radar_frame: int, services: dict):
super().__init__(services)
self.frame = radar_frame
self.logMonoTime = {"radarState": radar_frame}
self.valid = {"radarState": True}
self.alive = {"radarState": True}
class Plant:
messaging_initialized = False
@@ -141,7 +132,7 @@ class Plant:
car_control.carControl.orientationNED = [0., float(pitch), 0.]
# ******** get controlsState messages for plotting ***
sm = PlannerSM(self.rk.frame, {'radarState': radar.radarState,
sm = {'radarState': radar.radarState,
'carState': car_state.carState,
'carControl': car_control.carControl,
'controlsState': control.controlsState,
@@ -150,7 +141,7 @@ class Plant:
'modelV2': model.modelV2,
'carStateSP': car_state_sp.carStateSP,
'liveMapDataSP': live_map_data_sp.liveMapDataSP,
'gpsLocation': gps_data.gpsLocation})
'gpsLocation': gps_data.gpsLocation}
self.planner.update(sm)
self.acceleration = self.planner.output_a_target
if self.planner.output_should_stop:
@@ -152,7 +152,7 @@ def migrate_drivingModelData(msgs):
add_ops = []
for _, msg in msgs:
dmd = messaging.new_message('drivingModelData', valid=msg.valid, logMonoTime=msg.logMonoTime)
for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "big", "action"]:
for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "action"]:
setattr(dmd.drivingModelData, field, getattr(msg.modelV2, field))
for meta_field in ["laneChangeState", "laneChangeState"]:
setattr(dmd.drivingModelData.meta, meta_field, getattr(msg.modelV2.meta, meta_field))
@@ -33,9 +33,9 @@ MODEL_REPLAY_BUCKET="model_replay_master"
GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN)
EXEC_TIMINGS = [
# model, instant max, average max, chestnut average max
("modelV2", 0.05, 0.03, 0.05),
("driverStateV2", 0.05, 0.018, 0.018),
# model, instant max, average max
("modelV2", 0.05, 0.028),
("driverStateV2", 0.05, 0.018),
]
def get_log_fn(test_route, ref="master"):
@@ -169,13 +169,11 @@ def model_replay(lr, frs):
dmonitoringmodeld_msgs = replay_process(dmonitoringmodeld, dmodeld_logs, frs)
msgs = modeld_msgs + dmonitoringmodeld_msgs
chestnut = any(m.modelV2.big for m in modeld_msgs if m.which() == "modelV2")
header = ['model', 'max instant', 'max instant allowed', 'average', 'max average allowed', 'test result']
rows = []
timings_ok = True
for (s, instant_max, avg_max, chestnut_avg_max) in EXEC_TIMINGS:
avg_max = chestnut_avg_max if chestnut else avg_max
for (s, instant_max, avg_max) in EXEC_TIMINGS:
ts = [getattr(m, s).modelExecutionTime for m in msgs if m.which() == s]
# TODO some init can happen in first iteration
ts = ts[1:]
@@ -1,7 +1,7 @@
import time
import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.selfdrive.ui.ui_state import ui_state
@@ -26,8 +26,8 @@ class BodyLayout(Widget):
self._last_input_time = time.monotonic()
self._was_active = False
self._offroad_label = UnifiedLabel("turn on ignition to use", 95 if gui_app.big_ui() else 45, FontWeight.DISPLAY,
alignment=TextAlignment.CENTER,
alignment_vertical=TextAlignmentVertical.MIDDLE)
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
def draw_dot_grid(self, rect: rl.Rectangle, dots: list[tuple[int, int]], color: rl.Color):
spacing = min(rect.height / GRID_ROWS, rect.width / GRID_COLS)
+2 -2
View File
@@ -8,7 +8,7 @@ from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButto
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.multilang import tr, trn
from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.widgets import Widget
@@ -178,7 +178,7 @@ class HomeLayout(Widget):
version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y,
version_text_width, self.header_rect.height)
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=TextAlignment.RIGHT)
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
def _render_home_content(self):
self._render_left_column()
+4 -4
View File
@@ -5,7 +5,7 @@ from enum import IntEnum
import pyray as rl
from openpilot.common.basedir import BASEDIR
from openpilot.system.ui.lib.application import FontWeight, TextAlignment, gui_app
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle
@@ -115,9 +115,9 @@ class TermsPage(Widget):
self._on_accept = on_accept
self._on_decline = on_decline
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.LEFT)
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._desc = Label(tr("You must accept the Terms of Service to use sunnypilot. Read the latest terms at https://sunnypilot.ai/terms before continuing."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT)
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
@@ -150,7 +150,7 @@ class DeclinePage(Widget):
def __init__(self, back_callback=None):
super().__init__()
self._text = Label(tr("You must accept the Terms of Service in order to use sunnypilot."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT)
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._back_btn = Button(tr("Back"), click_callback=back_callback)
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
click_callback=self._on_uninstall_clicked)
@@ -199,9 +199,6 @@ class SoftwareLayout(Widget):
selection = self._branch_dialog.selection
ui_state.params.put("UpdaterTargetBranch", selection, block=True)
self._branch_btn.action_item.set_value(selection)
self._download_btn.action_item.set_enabled(False)
self._waiting_for_updater = True
self._waiting_start_ts = time.monotonic()
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
self._branch_dialog = None
@@ -27,12 +27,6 @@ DESCRIPTIONS = {
"In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
"your steering wheel distance button."
),
"AccelPersonalityEnabled": tr_noop(
"Lets you choose how sunnypilot starts, catches up, and settles at the cruise speed. Emergency braking and stopping are unchanged."
),
"AccelPersonality": tr_noop(
"Eco is gentlest, Normal balances a prompt start with smooth catch-up, and Sport is more responsive."
),
"IsLdwEnabled": tr_noop(
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
"without a turn signal activated while driving over 31 mph (50 km/h)."
@@ -112,24 +106,6 @@ class TogglesLayout(Widget):
icon="speed_limit.png"
)
self._accel_controller_enabled = toggle_item(
lambda: tr("Enable Accel Controller"),
lambda: tr(DESCRIPTIONS["AccelPersonalityEnabled"]),
self._params.get_bool("AccelPersonalityEnabled"),
callback=self._set_accel_controller_enabled,
icon="speed_limit.png",
)
self._accel_personality_setting = multiple_button_item(
lambda: tr("Acceleration Profile"),
lambda: tr(DESCRIPTIONS["AccelPersonality"]),
buttons=[lambda: tr("Eco"), lambda: tr("Normal"), lambda: tr("Sport")],
button_width=300,
callback=self._set_accel_personality,
selected_index=self._params.get("AccelPersonality", return_default=True),
icon="speed_limit.png"
)
self._toggles = {}
self._locked_toggles = set()
for param, (title, desc, icon, needs_restart) in self._toggle_defs.items():
@@ -159,11 +135,9 @@ class TogglesLayout(Widget):
self._toggles[param] = toggle
# insert longitudinal personality and Accel Controller settings after NDOG toggle
# insert longitudinal personality after NDOG toggle
if param == "DisengageOnAccelerator":
self._toggles["LongitudinalPersonality"] = self._long_personality_setting
self._toggles["AccelPersonalityEnabled"] = self._accel_controller_enabled
self._toggles["AccelPersonality"] = self._accel_personality_setting
self._update_experimental_mode_icon()
self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0)
@@ -184,7 +158,6 @@ class TogglesLayout(Widget):
def _update_toggles(self):
ui_state.update_params()
accel_controller_enabled = self._params.get_bool("AccelPersonalityEnabled")
e2e_description = tr(
"sunnypilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " +
@@ -203,15 +176,11 @@ class TogglesLayout(Widget):
self._toggles["ExperimentalMode"].action_item.set_enabled(True)
self._toggles["ExperimentalMode"].set_description(e2e_description)
self._long_personality_setting.action_item.set_enabled(True)
self._accel_controller_enabled.action_item.set_enabled(True)
self._accel_personality_setting.action_item.set_enabled(True)
else:
# no long for now
self._toggles["ExperimentalMode"].action_item.set_enabled(False)
self._toggles["ExperimentalMode"].action_item.set_state(False)
self._long_personality_setting.action_item.set_enabled(False)
self._accel_controller_enabled.action_item.set_enabled(False)
self._accel_personality_setting.action_item.set_enabled(False)
self._params.remove("ExperimentalMode")
unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control.")
@@ -234,8 +203,6 @@ class TogglesLayout(Widget):
# refresh toggles from params to mirror external changes
for param in self._toggle_defs:
self._toggles[param].action_item.set_state(self._params.get_bool(param))
self._accel_controller_enabled.action_item.set_state(accel_controller_enabled)
self._accel_personality_setting.action_item.set_selected_button(self._params.get("AccelPersonality", return_default=True))
# these toggles need restart, block while engaged
for toggle_def in self._toggle_defs:
@@ -280,9 +247,3 @@ class TogglesLayout(Widget):
def _set_longitudinal_personality(self, button_index: int):
self._params.put("LongitudinalPersonality", button_index, block=True)
def _set_accel_personality(self, button_index: int):
self._params.put("AccelPersonality", button_index, block=True)
def _set_accel_controller_enabled(self, state: bool):
self._params.put_bool("AccelPersonalityEnabled", state, block=True)
+6 -19
View File
@@ -1,5 +1,4 @@
import datetime
import math
import time
from openpilot.cereal import log
@@ -9,7 +8,7 @@ from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.layouts import HBoxLayout
from openpilot.system.ui.widgets.icon_widget import IconWidget
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
from openpilot.common.version import RELEASE_BRANCHES
@@ -70,8 +69,8 @@ class AlertsPill(Widget):
count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h)
gui_label(count_rect, str(alert_count), font_size=36,
alignment=TextAlignment.CENTER,
alignment_vertical=TextAlignmentVertical.MIDDLE)
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
class NetworkIcon(Widget):
@@ -140,9 +139,7 @@ class MiciHomeLayout(Widget):
self._version_text = self._get_version_text()
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
self._usb_icon = IconWidget("icons_mici/usb.png", (62, 40))
self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40))
self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40))
self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40))
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
self._body_icon = IconWidget("icons_mici/body.png", (54, 37))
@@ -153,15 +150,13 @@ class MiciHomeLayout(Widget):
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
NetworkIcon(),
self._experimental_icon,
self._usb_icon,
self._chestnut_icon,
self._chestnut_loading_icon,
self._chestnut_failed_icon,
self._body_icon,
self._mic_icon,
], spacing=18)
self._openpilot_label = UnifiedLabel("openpilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False)
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False)
self._version_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
self._large_version_label = UnifiedLabel("", font_size=64, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
self._date_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
@@ -252,20 +247,12 @@ class MiciHomeLayout(Widget):
self._version_commit_label.render()
# ***** Center-aligned bottom section icons *****
usb_connected = ui_state.usb_connected
usb_unknown = ui_state.usb_unknown
chestnut_state = ui_state.chestnut_state
self._experimental_icon.set_visible(ui_state.experimental_mode)
if gui_app.sunnypilot_ui():
self._set_chestnut_visibility()
else:
self._usb_icon.set_visible(usb_connected and usb_unknown)
self._chestnut_icon.set_visible(not usb_unknown and chestnut_state not in
(ChestnutState.LOADING, ChestnutState.UNCOMPILED, ChestnutState.FAILED) and
(usb_connected or chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE)))
self._chestnut_loading_icon.set_visible(not usb_unknown and chestnut_state == ChestnutState.LOADING)
self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)))
self._chestnut_failed_icon.set_visible(not usb_unknown and chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.LOADING, ChestnutState.ACTIVE))
self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
self._mic_icon.set_visible(ui_state.recording_audio)
self._body_icon.set_visible(bool(ui_state.is_body))
+2 -8
View File
@@ -14,7 +14,6 @@ from openpilot.system.ui.lib.application import gui_app
if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.home import MiciHomeLayoutSP as MiciHomeLayout
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onroad import OnroadViewContainerSP as AugmentedRoadView
ONROAD_DELAY = 2.5 # seconds
@@ -73,9 +72,6 @@ class MiciMainLayout(Scroller):
# For scroll_to
return self._body_onroad_layout if ui_state.is_body else self._car_onroad_layout
def _should_auto_scroll_to_onroad(self) -> bool:
return True
def _setup_callbacks(self):
self._home_layout.set_callbacks(
on_settings=lambda: gui_app.push_widget(self._settings_layout),
@@ -126,15 +122,13 @@ class MiciMainLayout(Scroller):
# FIXME: these two pops can interrupt user interacting in the settings
if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY:
if not gui_app.sunnypilot_ui() or self._should_auto_scroll_to_onroad():
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
self._onroad_time_delay = None
# When car leaves standstill, pop nav stack and scroll to onroad
CS = ui_state.sm["carState"]
if not CS.standstill and self._prev_standstill:
if not gui_app.sunnypilot_ui() or self._should_auto_scroll_to_onroad():
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
self._prev_standstill = CS.standstill
def _on_interactive_timeout(self):
@@ -11,7 +11,7 @@ from openpilot.common.hardware import HARDWARE
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
REFRESH_INTERVAL = 5.0 # seconds
@@ -62,12 +62,12 @@ class AlertItem(Widget):
self._icon_green = gui_app.texture("icons_mici/offroad_alerts/green_wheel.png", self.ICON_SIZE, self.ICON_SIZE)
self._title_label = UnifiedLabel(text="", font_size=32, font_weight=FontWeight.SEMI_BOLD, text_color=self.TEXT_COLOR,
alignment=TextAlignment.LEFT,
alignment_vertical=TextAlignmentVertical.TOP, line_height=0.95)
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, line_height=0.95)
self._body_label = UnifiedLabel(text="", font_size=28, font_weight=FontWeight.ROMAN, text_color=self.TEXT_COLOR,
alignment=TextAlignment.LEFT,
alignment_vertical=TextAlignmentVertical.BOTTOM, line_height=0.95)
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, line_height=0.95)
self._title_text = ""
self._body_text = ""
@@ -200,8 +200,8 @@ class MiciOffroadAlerts(Scroller):
# Create empty state label
self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE,
alignment=TextAlignment.CENTER,
alignment_vertical=TextAlignmentVertical.MIDDLE)
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
# Build initial alert list
self._build_alerts()
@@ -4,7 +4,7 @@ import pyray as rl
from collections.abc import Callable
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.qrcode import make_texture
from openpilot.system.ui.lib.application import FontWeight, gui_app, TextAlignment
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import SmallCircleIconButton
from openpilot.system.ui.widgets.scroller import NavScroller, Scroller
@@ -35,7 +35,7 @@ class DriverCameraSetupDialog(BaseCabinCameraDialog):
if not self._camera_view.frame:
gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD,
alignment=TextAlignment.CENTER)
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
rl.end_scissor_mode()
return
@@ -74,10 +74,6 @@ class SoftwareInfoLayoutMici(Widget):
class CheckUpdateButton(BigButton):
UPDATER_PROC = "openpilot.system.updated.updated"
CHECK_FOR_UPDATE = "SIGUSR1"
DOWNLOAD_UPDATE = "SIGHUP"
def __init__(self):
self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75)
self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
@@ -101,20 +97,15 @@ class CheckUpdateButton(BigButton):
gui_app.push_widget(dlg)
return
self._signal_updater(self.DOWNLOAD_UPDATE if self.get_value() == "download update" else self.CHECK_FOR_UPDATE)
def check_for_update(self):
self._signal_updater(self.CHECK_FOR_UPDATE)
def _signal_updater(self, sig: str):
self.set_enabled(False)
self._state = UpdaterState.WAITING_FOR_UPDATER
self._hide_value_t = None
self.set_value("")
self.set_icon(self._txt_update_icon)
def run():
subprocess.run(f"pkill -{sig} -f {self.UPDATER_PROC}", shell=True)
if self.get_value() == "download update":
subprocess.run("pkill -SIGHUP -f openpilot.system.updated.updated", shell=True)
else:
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
threading.Thread(target=run, daemon=True).start()
@@ -193,7 +184,7 @@ class CheckUpdateButton(BigButton):
class InstallUpdateButton(BigButton):
def __init__(self):
super().__init__("install now", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70))
super().__init__("install update", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70))
self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable"))
def _update_state(self):
@@ -241,9 +232,8 @@ class BranchSelectPage(NavScroller):
class TargetBranchButton(BigButton):
def __init__(self, check_update_btn: CheckUpdateButton):
def __init__(self):
super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "")
self._check_update_btn = check_update_btn
self.set_click_callback(self._on_click)
self.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
self.set_enabled(lambda: ui_state.is_offroad())
@@ -256,15 +246,12 @@ class TargetBranchButton(BigButton):
self.set_value(target)
def _on_click(self):
if not ui_state.params.get("UpdaterAvailableBranches"):
gui_app.push_widget(BigDialog("", tr("Failed to get available branches. Ensure you're connected to the internet and try again.")))
return
gui_app.push_widget(BranchSelectPage(self._on_select))
def _on_select(self, branch: str):
ui_state.params.put("UpdaterTargetBranch", branch, block=True)
self.set_value(branch)
self._check_update_btn.check_for_update()
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
class SoftwareLayoutMici(NavScroller):
@@ -278,11 +265,10 @@ class SoftwareLayoutMici(NavScroller):
gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64),
uninstall_openpilot_callback, exit_on_confirm=False)
check_update_btn = CheckUpdateButton()
self._scroller.add_widgets([
SoftwareInfoLayoutMici(),
check_update_btn,
CheckUpdateButton(),
InstallUpdateButton(),
TargetBranchButton(check_update_btn),
TargetBranchButton(),
uninstall_openpilot_btn,
])
@@ -42,8 +42,6 @@ class TogglesLayoutMici(NavScroller):
super().__init__()
self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"])
self._accel_controller_enabled = BigParamControl("enable accel controller", "AccelPersonalityEnabled")
self._accel_personality_toggle = BigMultiParamToggle("acceleration profile", "AccelPersonality", ["eco", "normal", "sport"])
self._experimental_btn = BigToggle("experimental mode", initial_state=ui_state.params.get_bool("ExperimentalMode"),
toggle_callback=self._on_experimental_mode)
is_metric_toggle = BigParamControl("use metric units", "IsMetric")
@@ -55,8 +53,6 @@ class TogglesLayoutMici(NavScroller):
self._scroller.add_widgets([
self._personality_toggle,
self._accel_controller_enabled,
self._accel_personality_toggle,
self._experimental_btn,
is_metric_toggle,
ldw_toggle,
@@ -69,7 +65,6 @@ class TogglesLayoutMici(NavScroller):
# Toggle lists
self._refresh_toggles = (
("ExperimentalMode", self._experimental_btn),
("AccelPersonalityEnabled", self._accel_controller_enabled),
("IsMetric", is_metric_toggle),
("IsLdwEnabled", ldw_toggle),
("AlwaysOnDM", always_on_dm_toggle),
@@ -109,23 +104,17 @@ class TogglesLayoutMici(NavScroller):
if ui_state.has_longitudinal_control:
self._experimental_btn.set_visible(True)
self._personality_toggle.set_visible(True)
self._accel_controller_enabled.set_visible(True)
self._accel_personality_toggle.set_visible(True)
else:
# no long for now
self._experimental_btn.set_visible(False)
self._experimental_btn.set_checked(False)
self._personality_toggle.set_visible(False)
self._accel_controller_enabled.set_visible(False)
self._accel_personality_toggle.set_visible(False)
ui_state.params.remove("ExperimentalMode")
# Refresh toggles from params to mirror external changes
for key, item in self._refresh_toggles:
item.set_checked(ui_state.params.get_bool(key))
self._accel_personality_toggle.refresh()
def _on_experimental_mode(self, state: bool):
if state and not ui_state.params.get_bool("ExperimentalModeConfirmed"):
# Don't show enabled state until confirm
@@ -10,7 +10,7 @@ from opendbc.car.structs import car
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter
from openpilot.common.hardware import COMMA_HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
@@ -333,7 +333,7 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
self._alert_text1_label.set_text(alert_text1)
self._alert_text1_label.set_text_color(color)
self._alert_text1_label.set_font_size(font_size)
self._alert_text1_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT)
self._alert_text1_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
self._alert_text1_label.render(text_rect1)
alert_text2 = alert.text2.lower()
@@ -365,5 +365,5 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
self._alert_text2_label.set_text(alert_text2)
self._alert_text2_label.set_text_color(color)
self._alert_text2_label.set_font_size(small_font_size)
self._alert_text2_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT)
self._alert_text2_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
self._alert_text2_label.render(text_rect2)
@@ -11,7 +11,7 @@ from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer
from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer
from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets import Widget
from openpilot.common.filter_simple import BounceFilter
@@ -158,8 +158,8 @@ class AugmentedRoadView(CameraView):
self._confidence_ball = ConfidenceBall()
self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY,
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
alignment=TextAlignment.CENTER,
alignment_vertical=TextAlignmentVertical.MIDDLE)
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
@@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.ui_state import ui_state, device
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.nav_widget import NavWidget
@@ -76,7 +76,7 @@ class BaseCabinCameraDialog(Widget):
if not self._camera_view.frame:
gui_label(rect, tr("camera starting"), font_size=54, font_weight=FontWeight.BOLD,
alignment=TextAlignment.CENTER)
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
rl.end_scissor_mode()
self._publish_alert_sound(None)
return
@@ -124,12 +124,12 @@ class BaseCabinCameraDialog(Widget):
awareness_pct = dm_state.visionPolicyState.awarenessPercent if is_vision else dm_state.wheeltouchPolicyState.awarenessPercent
gui_label(rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height),
f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
alignment=TextAlignment.RIGHT,
alignment_vertical=TextAlignmentVertical.TOP,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
color=rl.Color(0, 0, 0, 180))
gui_label(rect, f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
alignment=TextAlignment.RIGHT,
alignment_vertical=TextAlignmentVertical.TOP,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
color=rl.Color(255, 255, 255, int(255 * 0.9)))
if dm_state.alertLevel == log.DriverMonitoringState.AlertLevel.none:
@@ -137,16 +137,16 @@ class BaseCabinCameraDialog(Widget):
# Show alert level
alert_level_str = f"{'Pay Attention' if is_vision else 'Touch Wheel'} - level {dm_state.alertLevel}"
alignment = TextAlignment.RIGHT if self.driver_state_renderer.is_rhd else TextAlignment.LEFT
alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if self.driver_state_renderer.is_rhd else rl.GuiTextAlignment.TEXT_ALIGN_LEFT
shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height)
gui_label(shadow_rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
alignment=alignment,
alignment_vertical=TextAlignmentVertical.BOTTOM,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM,
color=rl.Color(0, 0, 0, 180))
gui_label(rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
alignment=alignment,
alignment_vertical=TextAlignmentVertical.BOTTOM,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM,
color=rl.Color(255, 255, 255, int(255 * 0.9)))
def _load_eye_textures(self):
@@ -154,8 +154,8 @@ class ModelRenderer(Widget, ModelRendererSP):
self._draw_lane_lines()
self._draw_path(sm)
if render_lead_indicator and radar_state:
self._draw_lead_indicator()
# if render_lead_indicator and radar_state:
# self._draw_lead_indicator()
def _update_raw_points(self, model):
"""Update raw 3D points from model data"""
+8 -13
View File
@@ -6,7 +6,7 @@ from collections.abc import Callable
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.scroller import DO_ZOOM
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignmentVertical
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.common.filter_simple import BounceFilter
if TYPE_CHECKING:
@@ -125,10 +125,10 @@ class BigButton(Widget):
self._rotate_icon_t: float | None = None
self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD,
text_color=LABEL_COLOR, alignment_vertical=TextAlignmentVertical.BOTTOM, scroll=scroll,
text_color=LABEL_COLOR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, scroll=scroll,
line_height=0.9)
self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN,
text_color=COMPLICATION_GREY, alignment_vertical=TextAlignmentVertical.BOTTOM)
text_color=COMPLICATION_GREY, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
self._update_label_layout()
self._load_images()
@@ -167,9 +167,9 @@ class BigButton(Widget):
def _update_label_layout(self):
self._label.set_font_size(self._get_label_font_size())
if self.value:
self._label.set_alignment_vertical(TextAlignmentVertical.TOP)
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
else:
self._label.set_alignment_vertical(TextAlignmentVertical.BOTTOM)
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
def set_text(self, text: str):
self.text = text
@@ -356,8 +356,8 @@ class GreyBigButton(BigButton):
self._sub_label.set_font_size(36)
self._sub_label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9)))
self._sub_label.set_font_weight(FontWeight.DISPLAY_REGULAR)
self._sub_label.set_alignment_vertical(TextAlignmentVertical.MIDDLE if not self._label.text else
TextAlignmentVertical.BOTTOM)
self._sub_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE if not self._label.text else
rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
self._sub_label.set_line_height(0.95)
@property
@@ -383,18 +383,13 @@ class BigMultiParamToggle(BigMultiToggle):
self._load_value()
def _load_value(self):
value = self._params.get(self._param, return_default=True)
index = value if isinstance(value, int) else 0
self.set_value(self._options[max(0, min(index, len(self._options) - 1))])
self.set_value(self._options[self._params.get(self._param) or 0])
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
new_idx = self._options.index(self.value)
self._params.put(self._param, new_idx)
def refresh(self):
self._load_value()
class BigParamControl(BigToggle):
def __init__(self, text: str, param: str, toggle_callback: Callable | None = None):
@@ -4,7 +4,7 @@ from dataclasses import dataclass
from openpilot.cereal import messaging, log
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.common.hardware import COMMA_HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
@@ -76,10 +76,10 @@ class AlertRenderer(Widget):
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
# font size is set dynamically
self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.CENTER,
text_alignment_vertical=TextAlignmentVertical.TOP)
self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=TextAlignment.CENTER,
text_alignment_vertical=TextAlignmentVertical.TOP)
self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
def get_alert(self, sm: messaging.SubMaster) -> Alert | None:
"""Generate the current alert based on selfdrive state."""
@@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.ui_state import ui_state, device
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.label import gui_label
@@ -38,7 +38,7 @@ class CabinCameraDialog(CameraView):
tr("camera starting"),
font_size=100,
font_weight=FontWeight.BOLD,
alignment=TextAlignment.CENTER,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
)
return -1
@@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.selfdrive.ui.layouts.home import HomeLayout, HomeLayoutState, HEAD_BUTTON_FONT_SIZE, SPACING
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.multilang import tr, trn
from openpilot.system.ui.widgets.label import gui_label
@@ -59,7 +59,7 @@ class HomeLayoutSP(HomeLayout):
desc_size = measure_text_cached(gui_app.font(FontWeight.NORMAL), description, BRAND_FONT_SIZE)
desc_width = desc_size.x
desc_rect = rl.Rectangle(version_right - desc_width, self.header_rect.y, desc_width, self.header_rect.height)
gui_label(desc_rect, description, BRAND_FONT_SIZE, rl.WHITE, alignment=TextAlignment.RIGHT)
gui_label(desc_rect, description, BRAND_FONT_SIZE, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
brand_size = measure_text_cached(gui_app.font(FontWeight.AUDIOWIDE), brand, BRAND_FONT_SIZE)
spacing = BRAND_DESC_SPACING if description else 0
@@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import FontWeight, TextAlignment
from openpilot.system.ui.lib.application import FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle
@@ -20,7 +20,7 @@ class SunnylinkConsentPage(Widget):
self._done_callback = done_callback
self._step = 0
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.AUDIOWIDE, text_alignment=TextAlignment.LEFT))
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.AUDIOWIDE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
self._content = [
{
@@ -43,7 +43,7 @@ class SunnylinkConsentPage(Widget):
self._primary_btn = self._child(Button("", button_style=ButtonStyle.PRIMARY, click_callback=lambda: self._handle_choice("enable")))
self._secondary_btn = self._child(Button("", button_style=ButtonStyle.NORMAL, click_callback=lambda: self._handle_choice("secondary")))
self._danger_btn = self._child(Button("", button_style=ButtonStyle.DANGER, click_callback=lambda: self._handle_choice("disable")))
self._desc = self._child(Label("", font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT))
self._desc = self._child(Label("", font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
def _handle_choice(self, choice):
if choice == "enable":
@@ -143,8 +143,7 @@ class CruiseLayout(Widget):
self.icbm_toggle.show_description(True)
if has_long or has_icbm:
software_cruise_speed = has_long and (not ui_state.CP.pcmCruise or not ui_state.CP_SP.pcmCruiseSpeed)
self.custom_acc_toggle.action_item.set_enabled((software_cruise_speed or has_icbm) and ui_state.is_offroad())
self.custom_acc_toggle.action_item.set_enabled(((has_long and not ui_state.CP.pcmCruise) or has_icbm) and ui_state.is_offroad())
self.dec_toggle.action_item.set_enabled(has_long)
self.scc_v_toggle.action_item.set_enabled(True)
self.scc_m_toggle.action_item.set_enabled(True)
@@ -170,7 +169,7 @@ class CruiseLayout(Widget):
show_custom_acc_desc = True
else:
if has_long or has_icbm:
if has_long and ui_state.CP.pcmCruise and ui_state.CP_SP.pcmCruiseSpeed:
if has_long and ui_state.CP.pcmCruise:
new_custom_acc_desc = tr(ACC_PCMCRUISE_DISABLED_DESCRIPTION)
show_custom_acc_desc = True
else:
@@ -9,7 +9,7 @@ from openpilot.cereal import custom
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkConsentPage
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
@@ -32,8 +32,8 @@ class SunnylinkHeader(Widget):
font_size=90,
font_weight=FontWeight.AUDIOWIDE,
text_color=rl.WHITE,
alignment=TextAlignment.CENTER,
alignment_vertical=TextAlignmentVertical.TOP,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
wrap_text=False,
elide=False
)
@@ -43,8 +43,8 @@ class SunnylinkHeader(Widget):
font_size=40,
font_weight=FontWeight.NORMAL,
text_color=rl.Color(0, 255, 0, 255), # Green
alignment=TextAlignment.CENTER,
alignment_vertical=TextAlignmentVertical.TOP,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
wrap_text=True,
elide=False
)
@@ -55,8 +55,8 @@ class SunnylinkHeader(Widget):
font_size=35,
font_weight=FontWeight.NORMAL,
text_color=rl.Color(255, 165, 0, 255), # Orange
alignment=TextAlignment.CENTER,
alignment_vertical=TextAlignmentVertical.TOP,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
wrap_text=True,
elide=False
)
@@ -109,8 +109,8 @@ class SunnylinkDescriptionItem(Widget):
font_size=40,
font_weight=FontWeight.NORMAL,
text_color=rl.WHITE,
alignment=TextAlignment.LEFT,
alignment_vertical=TextAlignmentVertical.TOP,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
wrap_text=True,
elide=False,
)
@@ -23,7 +23,7 @@ DESCRIPTIONS = {
'stop_and_go_hack': tr_noop(
'sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. ' +
'This feature is only applicable to certain models that are able to use longitudinal control. This is an alpha feature. Use at your own risk.'
),
)
}
@@ -11,6 +11,7 @@ import pyray as rl
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
from openpilot.system.ui.lib.application import FontWeight
from openpilot.system.ui.widgets.icon_widget import IconWidget
from openpilot.system.ui.widgets.label import UnifiedLabel
@@ -18,16 +19,16 @@ class MiciHomeLayoutSP(MiciHomeLayout):
def __init__(self):
super().__init__()
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False)
self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40))
self._chestnut_loading_icon.set_visible(False)
failed_idx = self._status_bar_layout.widgets.index(self._chestnut_failed_icon)
self._status_bar_layout.widgets.insert(failed_idx + 1, self._chestnut_loading_icon)
def _set_chestnut_visibility(self):
usb_connected = ui_state.usb_connected
usb_unknown = ui_state.usb_unknown
chestnut_state = ui_state.chestnut_state
loading = chestnut_state == ChestnutState.LOADING
self._usb_icon.set_visible(usb_connected and usb_unknown)
self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)))
self._chestnut_loading_icon.set_visible(not usb_unknown and loading)
self._chestnut_icon.set_visible(not usb_unknown and not loading and
chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))
self._chestnut_failed_icon.set_visible(not usb_unknown and chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
# stock has no loading tier: it shows green from the moment a big model is available. keep the
# pulse so the status bar and the onroad HUD agree on what loading looks like.
loading = ui_state.chestnut_state == ChestnutState.LOADING
self._chestnut_loading_icon._opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))
self._chestnut_loading_icon.set_visible(loading)
self._chestnut_icon.set_visible(not loading and ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))
self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
@@ -1,19 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroll_panel_sp import GuiScrollPanel2SP
class MiciMainLayoutSP(MiciMainLayout):
def __init__(self):
super().__init__()
scroller = self._scroller
scroller.scroll_panel = GuiScrollPanel2SP(scroller._horizontal, handle_out_of_bounds=not scroller._snap_items)
def _should_auto_scroll_to_onroad(self) -> bool:
return not self._onroad_layout.is_on_info_panel()
@@ -1,64 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from collections.abc import Callable
import pyray as rl
from openpilot.system.ui.lib.application import gui_app
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroller_sp import ScrollerSP
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.augmented_road_view import AugmentedRoadViewSP
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onroad_info_panel import OnroadInfoPanel
CONFIDENCE_BALL_VISIBLE_RATIO = 0.4
HORIZONTAL_SETTLE_PX = 5
HORIZONTAL_RESET_RATIO = 0.5
class OnroadViewContainerSP(ScrollerSP):
def __init__(self, bookmark_callback=None):
super().__init__(horizontal=False, snap_items=True, spacing=0, pad=0, scroll_indicator=False, edge_shadows=False)
self.road_view = AugmentedRoadViewSP(bookmark_callback=bookmark_callback)
self.onroad_info_panel = OnroadInfoPanel(bookmark_callback=bookmark_callback)
self._scroller.add_widgets([
self.road_view,
self.onroad_info_panel,
])
self._scroller.set_reset_scroll_at_show(False)
self._scroller.set_scrolling_enabled(lambda: abs(self.rect.x) < HORIZONTAL_SETTLE_PX)
for child in (self.road_view, self.onroad_info_panel):
inner_touch_valid = child._touch_valid_callback
child.set_touch_valid_callback(
lambda inner=inner_touch_valid: self._touch_valid() and (inner() if inner else True)
)
def set_rect(self, rect: rl.Rectangle):
super().set_rect(rect)
self.road_view.set_rect(rect)
self.onroad_info_panel.set_rect(rect)
return self
def is_swiping_left(self) -> bool:
return self.road_view.is_swiping_left() or self.onroad_info_panel.is_swiping_left()
def set_click_callback(self, click_callback: Callable[[], None] | None) -> None:
self.road_view.set_click_callback(click_callback)
self.onroad_info_panel.set_click_callback(click_callback)
def is_on_info_panel(self) -> bool:
"""True when scrolled past halfway toward onroad_info_panel (used by main layout
to skip auto-pop-back-to-camera while user is reading the info panel)."""
return abs(self._scroller.scroll_panel.get_offset()) > self._rect.height / 2
def _render(self, rect: rl.Rectangle):
if abs(self.rect.x) > gui_app.width * HORIZONTAL_RESET_RATIO:
self._scroller.scroll_panel.set_offset(0)
vertical_offset = self._scroller.scroll_panel.get_offset()
show_ball = abs(vertical_offset) < rect.height * CONFIDENCE_BALL_VISIBLE_RATIO
self.road_view.set_show_confidence_ball(show_ball)
super()._render(rect)
@@ -1,403 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from dataclasses import dataclass, field
from openpilot.common.constants import CV
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.mici.onroad.alert_renderer import AlertRenderer
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import BookmarkIcon
METER_TO_KM = 0.001
METER_TO_MILE = 0.000621371
CONTENT_MARGIN = 16
SPEED_LIMIT_SIGN_WIDTH = 146
VIENNA_SIGN_SIZE = 146
MUTCD_SIGN_HEIGHT = 178
OFFSET_BADGE_SIZE = 50
OFFSET_BADGE_PANEL_PADDING = 4
MUTCD_OFFSET_SIGN_Y_SHIFT = 6
VIENNA_BADGE_X_RATIO = 0.80
VIENNA_BADGE_UPCOMING_X_RATIO = 0.70
VIENNA_BADGE_Y_RATIO = -0.82
UPCOMING_SIGN_SIZE_RATIO = 0.76
UPCOMING_SIGN_OVERLAP_RATIO = 0.05
UNIT_FONT_SIZE = 40
SPEED_FONT_SIZE = 114
ROAD_FONT_SIZE = 32
SCC_TAG_WIDTH = 78
SCC_TAG_HEIGHT = 30
SCC_TAG_GAP = 5
COLUMN_GAP = 12
@dataclass(frozen=True)
class OnroadInfoPanelColors:
white: rl.Color = rl.WHITE
black: rl.Color = rl.BLACK
red: rl.Color = field(default_factory=lambda: rl.Color(255, 0, 0, 255))
green: rl.Color = field(default_factory=lambda: rl.Color(0, 255, 0, 255))
grey: rl.Color = field(default_factory=lambda: rl.Color(190, 195, 190, 255))
light_grey: rl.Color = field(default_factory=lambda: rl.Color(200, 200, 200, 255))
dark_grey: rl.Color = field(default_factory=lambda: rl.Color(100, 100, 100, 255))
bg_dark: rl.Color = field(default_factory=lambda: rl.Color(0, 0, 0, 255))
card_bg: rl.Color = field(default_factory=lambda: rl.Color(50, 50, 50, 200))
badge_bg: rl.Color = field(default_factory=lambda: rl.Color(60, 60, 60, 255))
COLORS = OnroadInfoPanelColors()
class OnroadInfoPanel(Widget):
def __init__(self, bookmark_callback=None):
super().__init__()
self.speed_limit: float = 0.0
self.speed_limit_valid: bool = False
self.speed_limit_offset: float = 0.0
self.next_speed_limit: float = 0.0
self.next_speed_limit_distance: float = 0.0
self.road_name: str = ""
self.current_speed: float = 0.0
self.set_speed: float = 0.0
self.cruise_enabled: bool = False
self._sign_slide: float = 0.0
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
self._marquee_offset: float = 0.0
self._marquee_direction: int = 1
self._marquee_pause_timer: float = 0.0
self._marquee_speed: float = 40.0
self._marquee_pause_duration: float = 1.5
self._alert_renderer = AlertRenderer()
self._alert_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
self._bookmark_icon = BookmarkIcon(bookmark_callback)
def is_swiping_left(self) -> bool:
return self._bookmark_icon.is_swiping_left()
def _handle_mouse_release(self, mouse_pos: MousePos) -> None:
# Mirror stock AugmentedRoadView: suppress click while bookmark gesture active
if not self._bookmark_icon.interacting():
super()._handle_mouse_release(mouse_pos)
def _update_state(self) -> None:
sm = ui_state.sm
speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
if sm.valid["longitudinalPlanSP"]:
lp_sp = sm["longitudinalPlanSP"]
resolver = lp_sp.speedLimit.resolver
self.speed_limit = resolver.speedLimit * speed_conv
self.speed_limit_valid = resolver.speedLimitValid
self.speed_limit_offset = resolver.speedLimitOffset * speed_conv
if sm.valid["liveMapDataSP"]:
lmd = sm["liveMapDataSP"]
self.next_speed_limit = lmd.speedLimitAhead * speed_conv
self.next_speed_limit_distance = lmd.speedLimitAheadDistance
self.road_name = lmd.roadName
if sm.updated["carState"]:
self.current_speed = sm["carState"].vEgo * speed_conv
if sm.valid["carState"] and sm.valid["controlsState"]:
self.cruise_enabled = sm["carState"].cruiseState.enabled
v_cruise_cluster = sm["carState"].vCruiseCluster
set_speed_kph = sm["controlsState"].vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster
self.set_speed = set_speed_kph * (METER_TO_MILE / METER_TO_KM) if not ui_state.is_metric else set_speed_kph
def _render(self, rect: rl.Rectangle) -> None:
self._update_state()
rl.draw_rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height), COLORS.bg_dark)
left_x = rect.x + CONTENT_MARGIN
if self.cruise_enabled:
unit = tr("MAX")
display_speed = self.set_speed
else:
unit = tr("km/h") if ui_state.is_metric else tr("MPH")
display_speed = self.current_speed
display_speed_text = str(round(display_speed))
if self.speed_limit_valid and display_speed > self.speed_limit:
speed_color = COLORS.red
else:
speed_color = COLORS.white
sign_width = min(SPEED_LIMIT_SIGN_WIDTH, rect.width * 0.30)
sign_height = VIENNA_SIGN_SIZE if ui_state.is_metric else MUTCD_SIGN_HEIGHT
has_upcoming_limit = self.next_speed_limit > 0 and self.next_speed_limit != self.speed_limit
target_sign_slide = 1.0 if has_upcoming_limit else 0.0
slide_speed = 3.0 * rl.get_frame_time()
if self._sign_slide < target_sign_slide:
self._sign_slide = min(self._sign_slide + slide_speed, target_sign_slide)
elif self._sign_slide > target_sign_slide:
self._sign_slide = max(self._sign_slide - slide_speed, target_sign_slide)
upcoming_width = int(sign_width * UPCOMING_SIGN_SIZE_RATIO)
upcoming_height = int(sign_height * UPCOMING_SIGN_SIZE_RATIO)
upcoming_reserved_width = int(upcoming_width * 0.85) + 5
sign_x_without_upcoming = rect.x + rect.width - sign_width - CONTENT_MARGIN
sign_x_with_upcoming = rect.x + rect.width - sign_width - CONTENT_MARGIN - upcoming_reserved_width
sign_x = sign_x_without_upcoming + (sign_x_with_upcoming - sign_x_without_upcoming) * self._sign_slide
sign_y = rect.y + (rect.height - sign_height) / 2
if not ui_state.is_metric and self.speed_limit_offset != 0 and self.speed_limit_valid:
sign_y += MUTCD_OFFSET_SIGN_Y_SHIFT
readout_right = sign_x - COLUMN_GAP
readout_width = max(1, readout_right - left_x)
road_y = rect.y + rect.height - 44
unit_font_size = self._fit_font_size(self._font_semi_bold, unit, readout_width, 46, UNIT_FONT_SIZE, 28)
speed_font_size = self._fit_font_size(self._font_bold, display_speed_text, readout_width, road_y - (rect.y + 54) - 8,
SPEED_FONT_SIZE, 76)
speed_size = measure_text_cached(self._font_bold, display_speed_text, speed_font_size)
speed_y = min(rect.y + 54, road_y - speed_size.y - 8)
unit_y = max(rect.y + 14, speed_y - unit_font_size - 6)
rl.draw_text_ex(self._font_semi_bold, unit, rl.Vector2(left_x, unit_y), unit_font_size, 0, COLORS.grey)
rl.draw_text_ex(self._font_bold, display_speed_text, rl.Vector2(left_x, speed_y), speed_font_size, 0, speed_color)
self._draw_road_name(left_x, road_y, readout_width)
if has_upcoming_limit and self._sign_slide > 0.01:
upcoming_speed_text = str(round(self.next_speed_limit))
distance_text = self._format_distance(self.next_speed_limit_distance)
upcoming_x = sign_x + sign_width - int(upcoming_width * UPCOMING_SIGN_OVERLAP_RATIO)
upcoming_y = sign_y + (sign_height - upcoming_height) / 2
upcoming_speed_color = COLORS.black
if ui_state.is_metric:
self._draw_vienna_sign(upcoming_x, upcoming_y, upcoming_width, upcoming_height, upcoming_speed_text, upcoming_speed_color, is_upcoming=True)
else:
self._draw_mutcd_sign(upcoming_x, upcoming_y, upcoming_width, upcoming_height, upcoming_speed_text, upcoming_speed_color, is_upcoming=True)
distance_font_size = self._fit_font_size(self._font_medium, distance_text, upcoming_width, 30, 24, 16)
distance_size = measure_text_cached(self._font_medium, distance_text, distance_font_size)
rl.draw_text_ex(self._font_medium, distance_text, rl.Vector2(upcoming_x + upcoming_width / 2 - distance_size.x / 2, upcoming_y + upcoming_height),
distance_font_size, 0, COLORS.grey)
self._draw_speed_limit_sign(sign_x, sign_y, sign_width, sign_height)
if self.speed_limit_offset != 0 and self.speed_limit_valid:
offset_text = str(abs(round(self.speed_limit_offset)))
badge_size = OFFSET_BADGE_SIZE
badge_rect = self._offset_badge_rect(rect, sign_x, sign_y, sign_width, sign_height, badge_size, has_upcoming_limit)
if ui_state.is_metric:
badge_radius = badge_size / 2
badge_center_x = badge_rect.x + badge_radius
badge_center_y = badge_rect.y + badge_radius
rl.draw_circle(int(badge_center_x), int(badge_center_y), badge_radius + 2, COLORS.dark_grey)
rl.draw_circle(int(badge_center_x), int(badge_center_y), badge_radius, COLORS.badge_bg)
self._draw_text_centered_fit(self._font_bold, offset_text, 32, rl.Vector2(badge_center_x, badge_center_y), COLORS.white,
badge_size - 10, badge_size - 8, min_size=24)
else:
rl.draw_rectangle_rounded(badge_rect, 0.25, 10, COLORS.badge_bg)
rl.draw_rectangle_rounded_lines_ex(badge_rect, 0.25, 10, 2, COLORS.dark_grey)
self._draw_text_centered_fit(self._font_bold, offset_text, 32, rl.Vector2(badge_rect.x + badge_size / 2, badge_rect.y + badge_size / 2),
COLORS.white, badge_size - 10, badge_size - 8, min_size=24)
scc_tag_x = min(left_x + speed_size.x + COLUMN_GAP, readout_right - SCC_TAG_WIDTH)
scc_tag_y = speed_y + (speed_size.y - (SCC_TAG_HEIGHT * 2 + SCC_TAG_GAP)) / 2
if scc_tag_x >= left_x + speed_size.x + 8:
self._draw_scc_icons(scc_tag_x, scc_tag_y, readout_right)
self._bookmark_icon.render(rect)
if ui_state.started:
alert_obj, no_alert = self._alert_renderer.will_render()
self._alert_alpha_filter.update(0 if no_alert else 1)
alpha = self._alert_alpha_filter.x
if alpha > 0.01:
rl.draw_rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height), rl.Color(0, 0, 0, int(150 * alpha)))
self._alert_renderer.render(rect)
def _draw_scc_icons(self, x: float, y: float, right_limit: float) -> None:
sm = ui_state.sm
if not sm.valid["longitudinalPlanSP"]:
return
scc = sm["longitudinalPlanSP"].smartCruiseControl
drawn = 0
for label, active in [("SCC-V", scc.vision.active), ("SCC-M", scc.map.active)]:
if not active:
continue
tag_x = x
if tag_x + SCC_TAG_WIDTH > right_limit:
return
tag_y = y + drawn * (SCC_TAG_HEIGHT + SCC_TAG_GAP)
rl.draw_rectangle_rounded(rl.Rectangle(tag_x, tag_y, SCC_TAG_WIDTH, SCC_TAG_HEIGHT), 0.3, 10, COLORS.green)
self._draw_text_centered_fit(self._font_bold, label, 18, rl.Vector2(tag_x + SCC_TAG_WIDTH / 2, tag_y + SCC_TAG_HEIGHT / 2), COLORS.black,
SCC_TAG_WIDTH - 10, SCC_TAG_HEIGHT - 4, min_size=14)
drawn += 1
def _draw_speed_limit_sign(self, x: float, y: float, sign_width: float, sign_height: float) -> None:
speed_str = str(round(self.speed_limit)) if self.speed_limit_valid and self.speed_limit > 0 else "--"
speed_color = COLORS.black if not self.speed_limit_valid or self.current_speed <= self.speed_limit else COLORS.red
if ui_state.is_metric:
self._draw_vienna_sign(x, y, sign_width, sign_height, speed_str, speed_color, is_upcoming=False)
else:
self._draw_mutcd_sign(x, y, sign_width, sign_height, speed_str, speed_color, is_upcoming=False)
def _draw_road_name(self, x: float, y: float, width: float) -> None:
if width <= 0:
return
road_display = self.road_name if self.road_name else "--"
font_size = self._fit_font_size(self._font_semi_bold, road_display, width, 38, ROAD_FONT_SIZE, 28)
road_size = measure_text_cached(self._font_semi_bold, road_display, font_size)
text_width = road_size.x
if text_width <= width:
self._marquee_offset = 0.0
self._marquee_direction = 1
self._marquee_pause_timer = 0.0
rl.draw_text_ex(self._font_semi_bold, road_display, rl.Vector2(x, y), font_size, 0, COLORS.white)
else:
overflow = text_width - width
dt = rl.get_frame_time()
if self._marquee_pause_timer > 0:
self._marquee_pause_timer -= dt
else:
self._marquee_offset += self._marquee_direction * self._marquee_speed * dt
if self._marquee_offset >= overflow:
self._marquee_offset = overflow
self._marquee_direction = -1
self._marquee_pause_timer = self._marquee_pause_duration
elif self._marquee_offset <= 0:
self._marquee_offset = 0
self._marquee_direction = 1
self._marquee_pause_timer = self._marquee_pause_duration
rl.begin_scissor_mode(int(x), int(y), int(width), int(road_size.y + 4))
text_pos = rl.Vector2(x - self._marquee_offset, y)
rl.draw_text_ex(self._font_semi_bold, road_display, text_pos, font_size, 0, COLORS.white)
rl.end_scissor_mode()
def _draw_vienna_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color, is_upcoming: bool = False) -> None:
center = rl.Vector2(x + width / 2, y + height / 2)
outer_radius = min(width, height) / 2
rl.draw_circle_v(center, outer_radius, COLORS.white)
ring_width = outer_radius * 0.18
rl.draw_ring(center, outer_radius - ring_width, outer_radius, 0, 360, 36, COLORS.red)
font_size = outer_radius * (0.7 if len(speed_str) >= 3 else 0.9)
self._draw_text_centered_fit(self._font_bold, speed_str, int(font_size), center, speed_color, width * 0.72, height * 0.50, min_size=24)
def _draw_mutcd_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color, is_upcoming: bool = False) -> None:
sign_rect = rl.Rectangle(x, y, width, height)
rl.draw_rectangle_rounded(sign_rect, 0.35, 10, COLORS.white)
inset = max(4, width * 0.05)
inner_rect = rl.Rectangle(x + inset, y + inset, width - inset * 2, height - inset * 2)
outer_radius = 0.35 * width / 2.0
inner_radius = outer_radius - inset
inner_roundness = inner_radius / (inner_rect.width / 2.0)
rl.draw_rectangle_rounded_lines_ex(inner_rect, inner_roundness, 10, 3, COLORS.black)
mid_x = x + width / 2
label_size = max(18, int(width * 0.26))
if is_upcoming:
self._draw_text_centered_fit(self._font_bold, tr("AHEAD"), int(width * 0.34), rl.Vector2(mid_x, y + height * 0.28), COLORS.black,
width * 0.94, height * 0.32, min_size=20)
else:
self._draw_text_centered_fit(self._font_bold, tr("SPEED"), label_size, rl.Vector2(mid_x, y + height * 0.20), COLORS.black,
width * 0.84, height * 0.24, min_size=16)
self._draw_text_centered_fit(self._font_bold, tr("LIMIT"), label_size, rl.Vector2(mid_x, y + height * 0.40), COLORS.black,
width * 0.84, height * 0.24, min_size=16)
speed_font_size = int(width * 0.60) if len(speed_str) >= 3 else int(width * 0.72)
self._draw_text_centered_fit(self._font_bold, speed_str, speed_font_size, rl.Vector2(mid_x, y + height * 0.72), speed_color,
width * 0.90, height * 0.52, min_size=32)
def _draw_text_centered(self, font, text, size, pos_center, color):
sz = measure_text_cached(font, text, size)
rl.draw_text_ex(font, text, rl.Vector2(pos_center.x - sz.x / 2, pos_center.y - sz.y / 2), size, 0, color)
def _draw_text_centered_fit(self, font, text, size, pos_center, color, max_width: float, max_height: float, min_size: int = 10):
size = self._fit_font_size(font, text, max_width, max_height, size, min_size)
self._draw_text_centered(font, text, size, pos_center, color)
def _fit_font_size(self, font, text: str, max_width: float, max_height: float, max_size: int | float, min_size: int) -> int:
size = int(max_size)
while size > min_size:
text_size = measure_text_cached(font, text, size)
if text_size.x <= max_width and text_size.y <= max_height:
return size
size -= 2
return min_size
def _offset_badge_rect(self, panel_rect: rl.Rectangle, sign_x: float, sign_y: float, sign_width: float, sign_height: float,
badge_size: float, has_upcoming_limit: bool) -> rl.Rectangle:
if ui_state.is_metric:
radius = min(sign_width, sign_height) / 2
center_x = sign_x + sign_width / 2
center_y = sign_y + sign_height / 2
badge_x_ratio = VIENNA_BADGE_UPCOMING_X_RATIO if has_upcoming_limit else VIENNA_BADGE_X_RATIO
badge_center_x = center_x + radius * badge_x_ratio
badge_center_y = center_y + radius * VIENNA_BADGE_Y_RATIO
badge_x = badge_center_x - badge_size / 2
badge_y = badge_center_y - badge_size / 2
else:
badge_x = sign_x + sign_width - badge_size * 0.45
badge_y = sign_y - badge_size * 0.75
return rl.Rectangle(
self._clamp(
badge_x,
panel_rect.x + OFFSET_BADGE_PANEL_PADDING,
panel_rect.x + panel_rect.width - badge_size - OFFSET_BADGE_PANEL_PADDING,
),
self._clamp(
badge_y,
panel_rect.y + OFFSET_BADGE_PANEL_PADDING,
panel_rect.y + panel_rect.height - badge_size - OFFSET_BADGE_PANEL_PADDING,
),
badge_size,
badge_size,
)
@staticmethod
def _clamp(value: float, min_value: float, max_value: float) -> float:
return max(min_value, min(max_value, value))
def _format_distance(self, distance: float) -> str:
if ui_state.is_metric:
if distance < 50:
return tr("Near")
if distance >= 1000:
return f"{distance * METER_TO_KM:.1f}" + tr("km")
if distance < 200:
rounded = max(10, int(distance / 10) * 10)
else:
rounded = int(distance / 100) * 100
return str(rounded) + tr("m")
else:
distance_mi = distance * METER_TO_MILE
if distance_mi < 0.1:
return tr("Near")
return f"{distance_mi:.1f}" + tr("mi")
@@ -1,29 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
class _SuppressedConfidenceBall:
def render(self, *_):
pass
class AugmentedRoadViewSP(AugmentedRoadView):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._show_confidence_ball: bool = True
self._real_confidence_ball = self._confidence_ball
self._confidence_ball = _SuppressedConfidenceBall()
def set_show_confidence_ball(self, show: bool) -> None:
self._show_confidence_ball = show
def _render(self, _) -> None:
super()._render(_)
if self._show_confidence_ball:
self._real_confidence_ball.render(self.rect)
@@ -1,83 +0,0 @@
import pyray as rl
from openpilot.common.test import OpenpilotTestCase
from openpilot.system.ui.lib.application import MouseEvent, MousePos, gui_app
from openpilot.system.ui.lib.scroll_panel2 import ScrollState
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets import scroller as scroller_mod
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroll_panel_sp import GuiScrollPanel2SP
class DummyScrollIndicator:
def update(self, *_) -> None:
pass
def render(self) -> None:
pass
class DummyWidget(Widget):
def __init__(self, rect: rl.Rectangle):
super().__init__()
self.set_rect(rect)
def _render(self, _) -> None:
pass
def _mouse_event(x: float, y: float, *, pressed: bool = False, released: bool = False,
down: bool = True, t: float = 0.0) -> MouseEvent:
return MouseEvent(MousePos(x, y), 0, pressed, released, down, t)
class TestScrollerSP(OpenpilotTestCase):
def test_vertical_snap_items_are_supported(self, monkeypatch):
monkeypatch.setattr(scroller_mod, "ScrollIndicator", DummyScrollIndicator)
scroller = scroller_mod._Scroller([], horizontal=False, snap_items=True, scroll_indicator=False)
scroller.set_rect(rl.Rectangle(0, 0, 100, 100))
scroller.scroll_panel.set_offset(-60)
captured_snap_target = None
def update(_, __, snap_target=None):
nonlocal captured_snap_target
captured_snap_target = snap_target
return scroller.scroll_panel.get_offset()
monkeypatch.setattr(scroller.scroll_panel, "update", update)
visible_items: list[Widget] = [
DummyWidget(rl.Rectangle(0, -60, 100, 100)),
DummyWidget(rl.Rectangle(0, 40, 100, 100)),
]
scroller._get_scroll(visible_items, 200)
assert captured_snap_target == -100
def test_scroll_panel_sp_rejects_orthogonal_drags(self, monkeypatch):
panel = GuiScrollPanel2SP(horizontal=True)
bounds = rl.Rectangle(0, 0, 100, 100)
monkeypatch.setattr(gui_app, "_mouse_events", [_mouse_event(10, 10, pressed=True, t=1.0)])
panel.update(bounds, 200)
assert panel.state == ScrollState.PRESSED
monkeypatch.setattr(gui_app, "_mouse_events", [_mouse_event(23, 60, t=1.1)])
panel.update(bounds, 200)
assert panel.state == ScrollState.STEADY
assert panel.get_offset() == 0
def test_scroll_panel_sp_can_disable_out_of_bounds_handling(self, monkeypatch):
panel = GuiScrollPanel2SP(horizontal=False, handle_out_of_bounds=False)
bounds = rl.Rectangle(0, 0, 100, 100)
monkeypatch.setattr(gui_app, "_mouse_events", [])
panel.set_offset(20)
panel.update(bounds, 200)
assert panel.get_offset() == 0
panel.set_offset(-150)
panel.update(bounds, 200)
assert panel.get_offset() == -100
@@ -1,33 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.system.ui.lib.application import MouseEvent
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2, ScrollState
class GuiScrollPanel2SP(GuiScrollPanel2):
"""Scroll panel behavior for nested Mici pagers."""
def __init__(self, horizontal: bool = True, handle_out_of_bounds: bool = True) -> None:
super().__init__(horizontal, handle_out_of_bounds=handle_out_of_bounds)
def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, bounds_size: float,
content_size: float) -> None:
state_before_update = self._state
super()._handle_mouse_event(mouse_event, bounds, bounds_size, content_size)
if self._state == ScrollState.MANUAL_SCROLL and state_before_update == ScrollState.PRESSED and \
self._initial_click_event is not None:
drag_x = abs(mouse_event.pos.x - self._initial_click_event.pos.x)
drag_y = abs(mouse_event.pos.y - self._initial_click_event.pos.y)
primary_drag = drag_x if self._horizontal else drag_y
cross_drag = drag_y if self._horizontal else drag_x
if cross_drag > primary_drag:
self._state = ScrollState.STEADY
self._velocity = 0.0
self._velocity_buffer.clear()
@@ -1,16 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroll_panel_sp import GuiScrollPanel2SP
class ScrollerSP(Scroller):
def __init__(self, **kwargs):
super().__init__(**kwargs)
inner = self._scroller
inner.scroll_panel = GuiScrollPanel2SP(inner._horizontal, handle_out_of_bounds=not inner._snap_items)
-3
View File
@@ -10,9 +10,6 @@ from openpilot.selfdrive.ui.layouts.main import MainLayout
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
from openpilot.selfdrive.ui.ui_state import ui_state
if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.main import MiciMainLayoutSP as MiciMainLayout
BIG_UI = gui_app.big_ui()
-22
View File
@@ -12,7 +12,6 @@ from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
from openpilot.system.ui.lib.application import gui_app
from openpilot.common.hardware import HARDWARE, PC
from openpilot.common.hardware.usb import TYPEC_CC_ORIENTATION_PATH, get_usb_state, is_chestnut_usb_id, read_int
from openpilot.selfdrive.modeld.helpers import chestnut_compiled
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP
@@ -96,10 +95,6 @@ class UIState(UIStateSP):
self.chestnut_compiled: bool = chestnut_compiled()
self.chestnut_active: bool | None = None
self.chestnut_loading: bool = False
self.usb_connected: bool = False
self.usb_connected_ts: float | None = None
self.usb_disconnected_ts: float | None = None
self.usb_unknown: bool = False
self.chestnut_state = ChestnutState.DISCONNECTED
self.started: bool = False
self.ignition: bool = False
@@ -259,23 +254,6 @@ class UIState(UIStateSP):
self.chestnut_compiled = chestnut_compiled()
self.chestnut_active = self.params.get("ChestnutActive")
self.chestnut_loading = self.params.get_bool("ChestnutLoading")
now = time.monotonic()
if read_int(TYPEC_CC_ORIENTATION_PATH) != 0:
self.usb_disconnected_ts = None
if not self.usb_connected:
self.usb_connected = True
self.usb_connected_ts = now
self.usb_unknown = False
elif self.usb_connected_ts is not None and now - self.usb_connected_ts > 10.:
self.usb_unknown = not any(is_chestnut_usb_id(d["vendorId"], d["productId"], True) for d in get_usb_state())
self.usb_connected_ts = None
elif self.usb_connected:
if self.usb_disconnected_ts is None:
self.usb_disconnected_ts = now
elif now - self.usb_disconnected_ts > PARAM_UPDATE_TIME:
self.usb_connected = False
self.usb_connected_ts = None
self.usb_unknown = False
UIStateSP.update_params(self)
@@ -75,11 +75,11 @@ class TestStockEquivalence(OpenpilotTestCase):
frame_skip = derive_frame_skip(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES)
stock_shapes = {**SPLIT_VISION_INPUT_SHAPES, **SPLIT_POLICY_INPUT_SHAPES, 'action_t': (1, 2)}
stock_queues, stock_npy, _frame_views = make_input_queues(stock_shapes, frame_skip, device='NPY', frame_copy_size=49152)
stock_queues, stock_npy = make_input_queues(stock_shapes, frame_skip, device='NPY')
# sunnypilot split pipeline has tfm/big_tfm as queues (stock has them in npy only)
assert set(stock_queues.keys()) <= set(state.input_queues.keys())
assert set(state.input_queues.keys()) == set(stock_queues.keys())
assert {'desire', 'traffic_convention'} <= set(state.numpy_inputs.keys())
assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - {'action_t', 'prev_feat'}
def test_split_queue_keys_work_with_desire_key(self, model_state_factory):
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
@@ -7,7 +7,6 @@ See the LICENSE.md file in the root directory for more details.
import os
import tempfile
import unittest
from pathlib import Path
import numpy as np
@@ -249,27 +248,23 @@ class TestStockCompileModeldEquivalence(OpenpilotTestCase):
input_shapes = {
'img': (1, 12, 128, 256),
'desire_pulse': (1, 25, 8),
'features_buffer': (1, 24, 32, 512),
'features_buffer': (1, 24, 512), # when https://github.com/commaai/openpilot/pull/38681 merges, update to 1,24,32,512
'traffic_convention': (1, 2),
'action_t': (1, 2),
}
frame_skip = 4
stock_queues, stock_npy, _frame_views = stock_make_input_queues(input_shapes, frame_skip, device='NPY', frame_copy_size=49152)
stock_queues, stock_npy = stock_make_input_queues(input_shapes, frame_skip, device='NPY')
sunny_queues, sunny_npy = sunny_make_supercombo_input_queues(input_shapes, frame_skip, device='NPY')
# sunnypilot split pipeline has tfm/big_tfm as queues; packed_npy_inputs size differs (different frame packing)
assert set(stock_queues.keys()) <= set(sunny_queues.keys())
assert set(sunny_queues.keys()) == set(stock_queues.keys())
for key in stock_queues:
if key == 'packed_npy_inputs':
continue
assert sunny_queues[key].shape == stock_queues[key].shape, \
f"Queue shape mismatch for {key}: sunny {sunny_queues[key].shape} != stock {stock_queues[key].shape}"
assert set(stock_npy.keys()) <= set(sunny_npy.keys())
assert set(sunny_npy.keys()) == set(stock_npy.keys())
for key in stock_npy:
assert sunny_npy[key].shape == stock_npy[key].shape, \
f"Numpy array shape mismatch for {key}: sunny {sunny_npy[key].shape} != stock {stock_npy[key].shape}"
@unittest.skip("upstream removed make_warp_input_queues — warp merged into run_model")
def test_make_warp_queues_stock_equivalence(self):
from openpilot.selfdrive.modeld.compile_modeld import make_warp_input_queues as stock_make_warp_queues
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_warp_queues as sunny_make_warp_queues
+1 -29
View File
@@ -20,46 +20,18 @@ def get_default_model() -> str:
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py")
MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash")
BIG_MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "big_model_hash")
SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "driving_supercombo.onnx")
BIG_SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "big_driving_supercombo.onnx")
def _read_model_name_fields():
with open(DEFAULT_MODEL_NAME_PATH) as f:
content = f.read()
fields = {}
for line in content.splitlines():
if "=" in line:
key, val = line.split("=", 1)
fields[key.strip()] = val.strip().strip('"')
return fields
def update_model_hash():
fields = _read_model_name_fields()
supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH)
fingerprint = f"{supercombo_hash}:{fields.get('DEFAULT_MODEL', '')}:{fields.get('DEFAULT_MODEL_REF', '')}"
combined_hash = hashlib.sha256(fingerprint.encode()).hexdigest()
combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest()
with open(MODEL_HASH_PATH, "w") as f:
f.write(combined_hash)
print(f"Generated and updated new combined model hash to {MODEL_HASH_PATH}")
if os.path.exists(BIG_SUPERCOMBO_ONNX_PATH):
import subprocess
rel = os.path.relpath(BIG_SUPERCOMBO_ONNX_PATH, os.getcwd())
pointer = subprocess.check_output(["git", "show", f"HEAD:{rel}"], text=True)
oid = next(l.split(":", 1)[1] for l in pointer.splitlines() if l.startswith("oid sha256:"))
big_fingerprint = f"{oid}:{fields.get('DEFAULT_BIG_MODEL', '')}:{fields.get('DEFAULT_BIG_MODEL_REF', '')}"
big_combined_hash = hashlib.sha256(big_fingerprint.encode()).hexdigest()
with open(BIG_MODEL_HASH_PATH, "w") as f:
f.write(big_combined_hash)
print(f"Generated and updated new big model hash to {BIG_MODEL_HASH_PATH}")
def get_ref_for_name(url: str, name: str) -> str:
response = requests.get(url, timeout=10)
+2 -2
View File
@@ -138,8 +138,8 @@ class ModelCache:
class ModelFetcher:
"""Handles fetching and caching of model data from remote source"""
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v22.json"
MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v23.json"
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v21.json"
MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v22.json"
MODEL_SOURCES = {
"qcom": (MODEL_URL, ""),
+1 -1
View File
@@ -19,7 +19,7 @@ from openpilot.common.hardware.hw import Paths
from openpilot.selfdrive.modeld.helpers import chestnut_present
# SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO
REQUIRED_JSON_VERSION = 19
REQUIRED_JSON_VERSION = 18
CUSTOM_MODEL_PATH = Paths.model_root()
METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl'
+2 -2
View File
@@ -1,4 +1,4 @@
DEFAULT_MODEL = "CD210"
DEFAULT_MODEL_REF = "5b6436a90cf6902b8aaa71c2b6f3d7164d8ae391"
DEFAULT_BIG_MODEL = "BMRLNAP Model v4"
DEFAULT_BIG_MODEL_REF = "f877d7a0ccc3cce943c76e285214c020cd65c899"
DEFAULT_BIG_MODEL = "Lebowski"
DEFAULT_BIG_MODEL_REF = "fa0c6876d3cf070e91e25e5353ceadc68a5b3285"
@@ -1 +0,0 @@
2c814f08a2c51323b87839fbf8d2c2a9853a2b5536271b3d67f7b7a2de7f9374
+1 -1
View File
@@ -1 +1 @@
c5be11d2fb1115be953c541f30c50f7c71a00bc4a0e128e19aa11b60689317fc
49133798d9cd9cacf47085c7ef8122bfee88cd9c6192a8314c81bfb1b37f5809
@@ -5,47 +5,19 @@ This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import os
import subprocess
from openpilot.sunnypilot import get_file_hash
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, SUPERCOMBO_ONNX_PATH, BIG_MODEL_HASH_PATH, \
BIG_SUPERCOMBO_ONNX_PATH, _read_model_name_fields
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, SUPERCOMBO_ONNX_PATH
import hashlib
from openpilot.common.test import OpenpilotTestCase
def _get_lfs_oid(path: str) -> str:
"""Extract the LFS OID (SHA256 of actual content) from git, works whether the file is smudged or not."""
pointer = subprocess.check_output(["git", "show", f"HEAD:{path}"], text=True)
for line in pointer.splitlines():
if line.startswith("oid sha256:"):
return line.split(":", 1)[1]
raise ValueError(f"No LFS OID found for {path}")
class TestDefaultModel(OpenpilotTestCase):
def test_compare_onnx_hashes(self):
fields = _read_model_name_fields()
supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH)
fingerprint = f"{supercombo_hash}:{fields.get('DEFAULT_MODEL', '')}:{fields.get('DEFAULT_MODEL_REF', '')}"
combined_hash = hashlib.sha256(fingerprint.encode()).hexdigest()
combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest()
with open(MODEL_HASH_PATH) as f:
current_hash = f.read().strip()
assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash"
def test_compare_big_onnx_hashes(self):
if not os.path.exists(BIG_SUPERCOMBO_ONNX_PATH):
self.skipTest("big_driving_supercombo.onnx not present")
fields = _read_model_name_fields()
oid = _get_lfs_oid(os.path.relpath(BIG_SUPERCOMBO_ONNX_PATH, os.getcwd()))
big_fingerprint = f"{oid}:{fields.get('DEFAULT_BIG_MODEL', '')}:{fields.get('DEFAULT_BIG_MODEL_REF', '')}"
combined_hash = hashlib.sha256(big_fingerprint.encode()).hexdigest()
with open(BIG_MODEL_HASH_PATH) as f:
current_hash = f.read().strip()
assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash"
@@ -386,7 +386,7 @@ class TestManagerDownload(ManagerDownloadTestBase):
def body():
artifact = self.make_artifact(chunked=True)
self._bundle.ref = "test-ref"
self._bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
self._bundle.minimumSelectorVersion = 18
params, store = self._make_params_with_store()
self.manager.params = params
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "qcom"))
@@ -406,7 +406,7 @@ class TestManagerDownload(ManagerDownloadTestBase):
def body():
self.make_artifact(chunked=True)
self._bundle.ref = "big-ref"
self._bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
self._bundle.minimumSelectorVersion = 18
params, store = self._make_params_with_store()
self.manager.params = params
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "chestnut"))
@@ -469,7 +469,7 @@ def manifest_bundle(short_name: str, ref: str, index: int = 0, is_big: bool = Fa
"environment": "release",
"runner": "tinygrad",
"is_big": is_big,
"minimum_selector_version": str(helpers.REQUIRED_JSON_VERSION),
"minimum_selector_version": "18",
"ref": ref,
"models": [{
"type": "supercombo",
@@ -655,7 +655,7 @@ class TestActiveBundleValidation(OpenpilotTestCase):
def _raw_bundle(ref: str, runner: int | None = None) -> dict:
bundle = custom.ModelManagerSP.ModelBundle.new_message()
bundle.ref = ref
bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
bundle.minimumSelectorVersion = 18
if runner is not None:
bundle.runner = runner
return bundle.to_dict()
@@ -697,7 +697,7 @@ class TestActiveBundleSelection(OpenpilotTestCase):
def _raw_bundle(ref: str) -> dict:
bundle = custom.ModelManagerSP.ModelBundle.new_message()
bundle.ref = ref
bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
bundle.minimumSelectorVersion = 18
return bundle.to_dict()
def _params(self, qcom=None, chestnut=None):
@@ -744,7 +744,7 @@ class TestEffectiveSource(OpenpilotTestCase):
def _raw_bundle(ref: str) -> dict:
bundle = custom.ModelManagerSP.ModelBundle.new_message()
bundle.ref = ref
bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
bundle.minimumSelectorVersion = 18
return bundle.to_dict()
def test_runtime_no_gpu(self):
@@ -115,7 +115,7 @@ class IntelligentCruiseButtonManagement:
self.is_ready = ready and not button_pressed
def run(self, CS: car.CarState, CC: car.CarControl, LP_SP: custom.LongitudinalPlanSP, is_metric: bool) -> None:
if self.CP_SP.pcmCruiseSpeed or not self.CP_SP.intelligentCruiseButtonManagementAvailable:
if self.CP_SP.pcmCruiseSpeed:
return
self.is_metric = is_metric
@@ -136,9 +136,6 @@ def initialize_params(params) -> list[dict[str, Any]]:
keys.extend([
"ToyotaEnforceStockLongitudinal",
"ToyotaStopAndGoHack",
"ToyotaTSS2Long",
"ToyotaEnhancedBsm",
"ToyotaAutoHold",
])
return [{k: params.get(k, return_default=True)} for k in keys]
@@ -1,26 +1,14 @@
from opendbc.can.parser import CANParser
from opendbc.car import create_button_events
from opendbc.car.structs import car
from opendbc.car.toyota.carstate import get_virtual_cruise_button, VIRTUAL_CRUISE_BUTTONS
from openpilot.cereal import custom
from openpilot.common.constants import CV
from openpilot.common.parameterized import parameterized, parameterized_class
from openpilot.common.params import Params
from openpilot.common.test import OpenpilotTestCase
from openpilot.selfdrive.car.cruise import TOYOTA_VIRTUAL_CRUISE_LONG_PRESS, VCruiseHelper, V_CRUISE_INITIAL, V_CRUISE_UNSET
from openpilot.selfdrive.car.cruise import V_CRUISE_INITIAL
from openpilot.selfdrive.car.tests.test_cruise_speed import TestVCruiseHelper
from openpilot.sunnypilot.selfdrive.car.interfaces import initialize_params
ButtonEvent = car.CarState.ButtonEvent
ButtonType = car.CarState.ButtonEvent.Type
class TestToyotaParamsHandoff(OpenpilotTestCase):
def test_tss2_long_tuning_param_is_forwarded_to_opendbc(self):
keys = {next(iter(entry)) for entry in initialize_params(Params())}
assert "ToyotaTSS2Long" in keys
# TODO: test pcmCruise and pcmCruiseSpeed
@parameterized_class(('pcm_cruise', 'pcm_cruise_speed'), [(False, True)])
class TestCustomAccIncrements(TestVCruiseHelper):
@@ -126,8 +114,8 @@ class TestCustomAccIncrements(TestVCruiseHelper):
def test_rounding_behavior(self):
"""Test rounding behavior for 5 and 10 increments"""
test_cases = [
(47, 5, 50), # 47 -> 50 (round up to next 5)
(45, 5, 50), # 45 -> 50 (already at 5, increment by 5)
(47, 5, 50), # 47 -> 50 (round up to next 5)
(45, 5, 50), # 45 -> 50 (already at 5, increment by 5)
(43, 10, 50), # 43 -> 50 (round up to next 10)
(40, 10, 50), # 40 -> 50 (already at 10, increment by 10)
]
@@ -158,302 +146,3 @@ class TestCustomAccIncrements(TestVCruiseHelper):
initial_speed = self.v_cruise_helper.v_cruise_kph
self.press_button_long(ButtonType.accelCruise)
assert self.v_cruise_helper.v_cruise_kph == initial_speed + 10 # Should fallback to 10
class TestToyotaVirtualCruiseSpeed(OpenpilotTestCase):
def setup_method(self):
self.params = Params()
self.params.put_bool("CustomAccIncrementsEnabled", True, block=True)
self.params.put("CustomAccShortPressIncrement", 5, block=True)
self.params.put("CustomAccLongPressIncrement", 5, block=True)
CP = car.CarParams(brand="toyota", pcmCruise=True, openpilotLongitudinalControl=True)
CP_SP = custom.CarParamsSP(pcmCruiseSpeed=False)
self.v_cruise_helper = VCruiseHelper(CP, CP_SP)
self.v_cruise_helper.read_custom_set_speed_params()
self.route_parser = CANParser("toyota_nodsu_pt_generated", [("CLUTCH", 16)], 0)
self.route_button = 0
@staticmethod
def car_state(canonical_kph, cluster_kph, *, available=True, standstill=False, gas_pressed=False, v_ego_kph=0.0, button_events=None):
CS = car.CarState(
gasPressed=gas_pressed,
vEgo=v_ego_kph * CV.KPH_TO_MS,
cruiseState={
"available": available,
"speed": canonical_kph * CV.KPH_TO_MS,
"speedCluster": cluster_kph * CV.KPH_TO_MS,
"standstill": standstill,
},
)
CS.buttonEvents = button_events or []
return CS
def seed_enabled(self, canonical_kph, cluster_kph, *, is_metric=True):
CS = self.car_state(canonical_kph, cluster_kph)
self.v_cruise_helper.update_v_cruise(CS, enabled=False, is_metric=is_metric)
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=is_metric)
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=is_metric)
assert self.v_cruise_helper.v_cruise_kph == canonical_kph
assert self.v_cruise_helper.v_cruise_cluster_kph == cluster_kph
def press(self, button_type, canonical_kph, cluster_kph, hold_frames=0, *, standstill=False, gas_pressed=False, v_ego_kph=0.0, is_metric=True):
pressed = [ButtonEvent(type=button_type, pressed=True)]
self.v_cruise_helper.update_v_cruise(
self.car_state(canonical_kph, cluster_kph, standstill=standstill, gas_pressed=gas_pressed, v_ego_kph=v_ego_kph, button_events=pressed),
enabled=True,
is_metric=is_metric,
)
for _ in range(hold_frames):
self.v_cruise_helper.update_v_cruise(
self.car_state(canonical_kph, cluster_kph, standstill=standstill, gas_pressed=gas_pressed, v_ego_kph=v_ego_kph),
enabled=True,
is_metric=is_metric,
)
released = [ButtonEvent(type=button_type, pressed=False)]
self.v_cruise_helper.update_v_cruise(
self.car_state(canonical_kph, cluster_kph, standstill=standstill, gas_pressed=gas_pressed, v_ego_kph=v_ego_kph, button_events=released),
enabled=True,
is_metric=is_metric,
)
def set_increments(self, short_increment, long_increment):
self.params.put("CustomAccShortPressIncrement", short_increment, block=True)
self.params.put("CustomAccLongPressIncrement", long_increment, block=True)
self.v_cruise_helper.read_custom_set_speed_params()
def assert_kph_almost_equal(self, actual, expected):
self.assertAlmostEqual(actual, expected, delta=abs(expected) * 1e-6)
def route_button_events(self, payload):
self.route_parser.update((1, [(0x361, bytes.fromhex(payload), 0)]))
current = get_virtual_cruise_button(
self.route_parser.vl["CLUTCH"]["CRUISE_RES"],
self.route_parser.vl["CLUTCH"]["CRUISE_SET"],
)
events = create_button_events(current, self.route_button, VIRTUAL_CRUISE_BUTTONS)
self.route_button = current
return events
def test_short_press_rounds_display_target_and_preserves_offset(self):
self.seed_enabled(27, 31)
self.press(ButtonType.accelCruise, 28, 32)
assert self.v_cruise_helper.v_cruise_kph == 31
assert self.v_cruise_helper.v_cruise_cluster_kph == 35
def test_decel_at_display_minimum_does_not_increase_target(self):
self.seed_enabled(26, 30)
self.press(ButtonType.decelCruise, 25, 29)
assert self.v_cruise_helper.v_cruise_kph == 26
assert self.v_cruise_helper.v_cruise_cluster_kph == 30
@parameterized.expand((52, TOYOTA_VIRTUAL_CRUISE_LONG_PRESS - 1))
def test_route_length_short_press_is_not_a_long_press(self, hold_frames):
self.set_increments(short_increment=2, long_increment=5)
self.seed_enabled(27, 31)
self.press(ButtonType.accelCruise, 28, 32, hold_frames=hold_frames)
assert self.v_cruise_helper.v_cruise_kph == 29
assert self.v_cruise_helper.v_cruise_cluster_kph == 33
def test_toyota_long_press_uses_route_validated_cadence_and_suppresses_release(self):
self.set_increments(short_increment=2, long_increment=5)
self.seed_enabled(27, 31)
pressed = [ButtonEvent(type=ButtonType.accelCruise, pressed=True)]
self.v_cruise_helper.update_v_cruise(self.car_state(31, 35, button_events=pressed), enabled=True, is_metric=True)
for _ in range(TOYOTA_VIRTUAL_CRUISE_LONG_PRESS):
self.v_cruise_helper.update_v_cruise(self.car_state(31, 35), enabled=True, is_metric=True)
assert self.v_cruise_helper.v_cruise_kph == 31
assert self.v_cruise_helper.v_cruise_cluster_kph == 35
released = [ButtonEvent(type=ButtonType.accelCruise, pressed=False)]
self.v_cruise_helper.update_v_cruise(self.car_state(31, 35, button_events=released), enabled=True, is_metric=True)
assert self.v_cruise_helper.v_cruise_kph == 31
assert self.v_cruise_helper.v_cruise_cluster_kph == 35
def test_route_4_32_second_hold_repeats_six_times(self):
self.seed_enabled(26, 30)
self.press(ButtonType.accelCruise, 30, 34, hold_frames=432)
assert self.v_cruise_helper.v_cruise_kph == 56
assert self.v_cruise_helper.v_cruise_cluster_kph == 60
def test_maximum_boundary_caps_pair_and_preserves_offset(self):
self.seed_enabled(141, 145)
self.press(ButtonType.accelCruise, 142, 146)
assert self.v_cruise_helper.v_cruise_kph == 141
assert self.v_cruise_helper.v_cruise_cluster_kph == 145
self.press(ButtonType.accelCruise, 143, 147)
assert self.v_cruise_helper.v_cruise_kph == 141
assert self.v_cruise_helper.v_cruise_cluster_kph == 145
@parameterized.expand(
(
(25, 29, ButtonType.decelCruise),
(141, 147, ButtonType.accelCruise),
)
)
def test_out_of_range_raw_pair_is_not_moved_in_opposite_direction(self, canonical_kph, cluster_kph, button_type):
self.seed_enabled(canonical_kph, cluster_kph)
self.press(button_type, canonical_kph, cluster_kph)
assert self.v_cruise_helper.v_cruise_kph == canonical_kph
assert self.v_cruise_helper.v_cruise_cluster_kph == cluster_kph
def test_imperial_increment_preserves_canonical_cluster_pair(self):
self.seed_enabled(45, 50, is_metric=False)
self.press(ButtonType.accelCruise, 46, 51, is_metric=False)
assert self.v_cruise_helper.v_cruise_kph == 51
assert self.v_cruise_helper.v_cruise_cluster_kph == 56
def test_engagement_button_held_does_not_change_target(self):
initial = self.car_state(27, 31)
self.v_cruise_helper.update_v_cruise(initial, enabled=False, is_metric=True)
pressed = [ButtonEvent(type=ButtonType.decelCruise, pressed=True)]
self.v_cruise_helper.update_v_cruise(self.car_state(27, 31, button_events=pressed), enabled=False, is_metric=True)
for _ in range(TOYOTA_VIRTUAL_CRUISE_LONG_PRESS + 10):
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=True, is_metric=True)
released = [ButtonEvent(type=ButtonType.decelCruise, pressed=False)]
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32, button_events=released), enabled=True, is_metric=True)
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=True, is_metric=True)
assert self.v_cruise_helper.v_cruise_kph == 28
assert self.v_cruise_helper.v_cruise_cluster_kph == 32
def test_delayed_pcm_target_seeds_before_software_ownership(self):
invalid = self.car_state(0, 0)
self.v_cruise_helper.update_v_cruise(invalid, enabled=False, is_metric=True)
release = [ButtonEvent(type=ButtonType.decelCruise, pressed=False)]
for _ in range(4):
self.v_cruise_helper.update_v_cruise(self.car_state(0, 0, button_events=release), enabled=True, is_metric=True)
assert self.v_cruise_helper.v_cruise_kph == V_CRUISE_UNSET
assert self.v_cruise_helper.v_cruise_cluster_kph == V_CRUISE_UNSET
self.v_cruise_helper.update_v_cruise(self.car_state(27, 31), enabled=True, is_metric=True)
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_kph, 27)
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_cluster_kph, 31)
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=True, is_metric=True)
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_kph, 27)
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_cluster_kph, 31)
def test_route_payload_short_press_drives_virtual_target(self):
self.seed_enabled(27, 31)
pressed = self.route_button_events("a61a0000561a1a81")
self.v_cruise_helper.update_v_cruise(self.car_state(27, 31, button_events=pressed), enabled=True, is_metric=True)
for _ in range(52):
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=True, is_metric=True)
released = self.route_button_events("861a0000561b1a81")
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32, button_events=released), enabled=True, is_metric=True)
assert self.v_cruise_helper.v_cruise_kph == 31
assert self.v_cruise_helper.v_cruise_cluster_kph == 35
def test_prius_route_payload_short_set_drives_virtual_target(self):
self.seed_enabled(31, 35)
pressed = self.route_button_events("965f000056666585")
self.v_cruise_helper.update_v_cruise(self.car_state(31, 35, button_events=pressed), enabled=True, is_metric=True)
for _ in range(45):
self.v_cruise_helper.update_v_cruise(self.car_state(30, 34), enabled=True, is_metric=True)
released = self.route_button_events("865f000056666585")
self.v_cruise_helper.update_v_cruise(self.car_state(30, 34, button_events=released), enabled=True, is_metric=True)
assert self.v_cruise_helper.v_cruise_kph == 26
assert self.v_cruise_helper.v_cruise_cluster_kph == 30
def test_prius_route_payload_standstill_res_does_not_change_target(self):
self.seed_enabled(27, 31)
pressed = self.route_button_events("a61b0000561c1c80")
self.v_cruise_helper.update_v_cruise(
self.car_state(27, 31, standstill=True, button_events=pressed),
enabled=True,
is_metric=True,
)
for _ in range(TOYOTA_VIRTUAL_CRUISE_LONG_PRESS):
self.v_cruise_helper.update_v_cruise(self.car_state(27, 31, standstill=True), enabled=True, is_metric=True)
released = self.route_button_events("865f000056666585")
self.v_cruise_helper.update_v_cruise(
self.car_state(27, 31, standstill=True, button_events=released),
enabled=True,
is_metric=True,
)
assert self.v_cruise_helper.v_cruise_kph == 27
assert self.v_cruise_helper.v_cruise_cluster_kph == 31
def test_route_payload_disengage_mid_hold_clears_pending_action(self):
self.seed_enabled(27, 31)
pressed = self.route_button_events("a61a0000561a1a81")
self.v_cruise_helper.update_v_cruise(self.car_state(27, 31, button_events=pressed), enabled=True, is_metric=True)
for _ in range(30):
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=True, is_metric=True)
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=False, is_metric=True)
released = self.route_button_events("861a0000561b1a81")
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32, available=False, button_events=released), enabled=False, is_metric=True)
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=False, is_metric=True)
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=True, is_metric=True)
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=True, is_metric=True)
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_kph, 28)
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_cluster_kph, 32)
def test_standstill_resume_does_not_change_target(self):
self.seed_enabled(27, 31)
self.press(ButtonType.accelCruise, 27, 31, standstill=True)
assert self.v_cruise_helper.v_cruise_kph == 27
assert self.v_cruise_helper.v_cruise_cluster_kph == 31
def test_disengagement_discards_virtual_target_and_reseeds_raw_pair(self):
self.seed_enabled(27, 31)
self.press(ButtonType.accelCruise, 28, 32)
assert self.v_cruise_helper.v_cruise_kph == 31
raw = self.car_state(28, 32)
self.v_cruise_helper.update_v_cruise(raw, enabled=False, is_metric=True)
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_kph, 28)
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_cluster_kph, 32)
self.v_cruise_helper.update_v_cruise(raw, enabled=True, is_metric=True)
self.v_cruise_helper.update_v_cruise(raw, enabled=True, is_metric=True)
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_kph, 28)
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_cluster_kph, 32)
def test_unavailable_and_mads_handback_discard_virtual_target(self):
self.seed_enabled(27, 31)
self.press(ButtonType.accelCruise, 28, 32)
assert self.v_cruise_helper.v_cruise_kph == 31
self.v_cruise_helper.update_v_cruise(self.car_state(28, 32), enabled=False, is_metric=True)
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_kph, 28)
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_cluster_kph, 32)
self.v_cruise_helper.update_v_cruise(self.car_state(0, 0, available=False), enabled=False, is_metric=True)
assert self.v_cruise_helper.v_cruise_kph == V_CRUISE_UNSET
assert self.v_cruise_helper.v_cruise_cluster_kph == V_CRUISE_UNSET
self.v_cruise_helper.update_v_cruise(self.car_state(29, 33), enabled=False, is_metric=True)
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_kph, 29)
self.assert_kph_almost_equal(self.v_cruise_helper.v_cruise_cluster_kph, 33)
def test_set_during_gas_override_clips_target_to_ego_speed(self):
self.seed_enabled(27, 31)
self.press(ButtonType.decelCruise, 26, 30, gas_pressed=True, v_ego_kph=50)
assert self.v_cruise_helper.v_cruise_kph == 50
assert self.v_cruise_helper.v_cruise_cluster_kph == 54
@@ -1,61 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import numpy as np
from openpilot.cereal import custom
from openpilot.common.params import Params
from openpilot.sunnypilot import get_sanitize_int_param
AccelProfile = custom.LongitudinalPlanSP.AccelController.Profile
MAX_ACCEL_BREAKPOINTS = [0., 3., 5., 10., 20., 25., 40.] # m/s
MAX_ACCEL_PROFILES = {
AccelProfile.eco: [1.60, 1.48, 1.22, 0.86, 0.66, 0.52, 0.40],
AccelProfile.normal: [1.90, 1.70, 1.42, 0.99, 0.80, 0.66, 0.52],
AccelProfile.sport: [2.00, 2.00, 1.86, 1.30, 1.02, 0.86, 0.72],
}
CRUISE_DECEL_RESPONSE_TIME = { # seconds
AccelProfile.eco: 4.0,
AccelProfile.normal: 3.5,
AccelProfile.sport: 3.0,
}
CRUISE_DECEL_ACCEL = { # m/s^2; comfort-first cruise deceleration target
AccelProfile.eco: -0.35,
AccelProfile.normal: -0.50,
AccelProfile.sport: -0.65,
}
class AccelController:
def __init__(self):
self.params = Params()
self.update()
def update(self) -> None:
self._profile = get_sanitize_int_param("AccelPersonality", AccelProfile.eco, AccelProfile.sport, self.params)
self._enabled = self.params.get_bool("AccelPersonalityEnabled")
@property
def profile(self) -> int:
return self._profile
def is_enabled(self) -> bool:
return self._enabled
def get_max_accel(self, v_ego: float) -> float:
return float(np.interp(max(0.0, v_ego), MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[self._profile]))
def get_cruise_target(self, v_ego: float, v_target: float) -> float:
if not np.isfinite(v_target) or v_target <= 0.0 or v_target >= v_ego:
return v_target
response_time = CRUISE_DECEL_RESPONSE_TIME[self._profile]
target_delta = v_target - v_ego
if target_delta < CRUISE_DECEL_ACCEL[self._profile] * response_time * 2.0:
return v_target
return float(v_ego + target_delta / response_time)
@@ -1,336 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import numpy as np
from opendbc.car.interfaces import ACCEL_MAX
from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL
from openpilot.common.test import OpenpilotTestCase
from openpilot.selfdrive.controls.lib.longitudinal_planner import (
A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS, A_CRUISE_MIN, J_CRUISE_VALS, get_cruise_accel,
)
from openpilot.sunnypilot.selfdrive.controls.lib.accel_controller.accel_controller import (
AccelController, AccelProfile, CRUISE_DECEL_ACCEL, CRUISE_DECEL_RESPONSE_TIME, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES,
)
class TestAccelController(OpenpilotTestCase):
def setUp(self):
self.params = Params()
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
self.params.put("AccelPersonality", AccelProfile.normal, block=True)
def set_profile(self, profile: int) -> AccelController:
self.params.put("AccelPersonality", profile, block=True)
return AccelController()
def test_table_breakpoints(self):
for profile, values in MAX_ACCEL_PROFILES.items():
controller = self.set_profile(profile)
for speed, expected in zip(MAX_ACCEL_BREAKPOINTS, values, strict=True):
assert controller.get_max_accel(speed) == expected
def test_profile_ordering_and_bounds(self):
controllers = {
AccelProfile.eco: self.set_profile(AccelProfile.eco),
AccelProfile.normal: self.set_profile(AccelProfile.normal),
AccelProfile.sport: self.set_profile(AccelProfile.sport),
}
previous = {profile: float("inf") for profile in controllers}
for speed in np.linspace(0.0, 55.0, 551):
values = {profile: controller.get_max_accel(speed) for profile, controller in controllers.items()}
assert 0.0 <= values[AccelProfile.eco] <= values[AccelProfile.normal] <= values[AccelProfile.sport] <= 2.0
for profile, value in values.items():
assert value <= previous[profile]
previous[profile] = value
def test_profiles_stay_within_openpilot_accel_max(self):
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport):
controller = self.set_profile(profile)
for speed in np.linspace(0.0, 55.0, 551):
assert controller.get_max_accel(speed) <= ACCEL_MAX
def test_profiles_have_material_separation(self):
controllers = [self.set_profile(profile) for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport)]
for speed in MAX_ACCEL_BREAKPOINTS:
eco, normal, sport = (controller.get_max_accel(speed) for controller in controllers)
assert normal - eco >= 0.1 - 1e-12
assert sport - normal >= 0.1 - 1e-12
for speed in MAX_ACCEL_BREAKPOINTS[1:-1]:
assert controllers[2].get_max_accel(speed) - controllers[0].get_max_accel(speed) >= 0.3 - 1e-12
def test_profiles_keep_usable_road_speed_acceleration(self):
# A previous revision had eco at 0.20 m/s^2 at 40 m/s. 1% of road grade costs 0.098 m/s^2 of gravity, so
# that profile cannot hold speed on anything steeper than ~2% and can never recover once it bleeds off.
# This is a LOWER bound on purpose: the tapered upper bounds it replaces let highway accel go to zero.
controllers = {profile: self.set_profile(profile) for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport)}
for speed in np.linspace(8.0, 40.0, 321):
stock = float(np.interp(speed, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS))
values = {profile: controller.get_max_accel(speed) for profile, controller in controllers.items()}
# 0.35 m/s^2 holds a 3% grade; the fractions keep merges and passes usable.
assert values[AccelProfile.eco] >= max(0.35, 0.60 * stock), speed
assert values[AccelProfile.normal] >= 0.80 * stock, speed
assert values[AccelProfile.sport] >= stock, speed
def test_eco_never_exceeds_stock(self):
controller = self.set_profile(AccelProfile.eco)
for speed in np.linspace(0.0, 55.0, 551):
assert controller.get_max_accel(speed) <= float(np.interp(speed, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS)) + 1e-12, speed
def test_comfort_profile_caps_taper_after_launch(self):
for profile in (AccelProfile.eco, AccelProfile.normal):
values = MAX_ACCEL_PROFILES[profile]
assert values[3] <= 0.55 * values[0]
def test_sport_uses_openpilot_accel_max_at_launch(self):
controller = self.set_profile(AccelProfile.sport)
assert controller.get_max_accel(0.0) == ACCEL_MAX
assert all(controller.get_max_accel(speed) <= ACCEL_MAX for speed in np.linspace(0.0, 55.0, 551))
def test_ceiling_is_continuous_in_speed(self):
# The ceiling is the only thing the controller sets, so a step in it is a step in the commanded
# acceleration. dt=10 makes the stock jerk limiter a no-op so nothing can hide a discontinuity.
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport):
controller = self.set_profile(profile)
speeds = np.linspace(0.0, 45.0, 451)
spacing = float(speeds[1] - speeds[0])
commands = np.asarray([
get_cruise_accel(False, 60.0, speed, 0.0, 0.0, _fake_cp(), 10.0, 0.0, True, controller.get_max_accel(speed))
for speed in speeds
])
assert np.all(np.isfinite(commands)), profile
assert np.all(np.abs(np.diff(commands)) <= spacing * 1.05 + 1e-9), profile
def test_negative_speed_uses_standstill_value(self):
controller = self.set_profile(AccelProfile.sport)
assert controller.get_max_accel(-1.0) == MAX_ACCEL_PROFILES[AccelProfile.sport][0]
def test_cruise_decel_response(self):
v_ego = 25.0
v_target = 22.5
targets = {}
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport):
controller = self.set_profile(profile)
targets[profile] = controller.get_cruise_target(v_ego, v_target)
assert np.isclose(targets[profile] - v_ego, (v_target - v_ego) / CRUISE_DECEL_RESPONSE_TIME[profile])
assert v_ego > targets[AccelProfile.eco] > targets[AccelProfile.normal] > targets[AccelProfile.sport] > v_target
assert all(CRUISE_DECEL_RESPONSE_TIME[profile] >= 3.0 for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport))
assert all(CRUISE_DECEL_ACCEL[profile] < 0.0 for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport))
def test_cruise_target_bypasses_non_decel_requests(self):
controller = self.set_profile(AccelProfile.eco)
assert controller.get_cruise_target(20.0, 25.0) == 25.0
assert controller.get_cruise_target(20.0, 20.0) == 20.0
assert controller.get_cruise_target(20.0, 0.0) == 0.0
assert np.isnan(controller.get_cruise_target(20.0, float("nan")))
def test_profile_change_refreshes_ceiling(self):
controller = self.set_profile(AccelProfile.normal)
self.params.put("AccelPersonality", AccelProfile.sport, block=True)
controller.update()
index = MAX_ACCEL_BREAKPOINTS.index(10.0)
assert controller.get_max_accel(10.0) == MAX_ACCEL_PROFILES[AccelProfile.sport][index]
def test_params_refresh_every_update(self):
controller = self.set_profile(AccelProfile.normal)
self.params.put("AccelPersonality", AccelProfile.sport, block=True)
controller.update()
assert controller.profile == AccelProfile.sport
def test_enabled_param_refresh(self):
controller = self.set_profile(AccelProfile.normal)
self.params.put_bool("AccelPersonalityEnabled", False, block=True)
controller.update()
assert not controller.is_enabled()
class TestPlannerIntegration(OpenpilotTestCase):
def setUp(self):
self.params = Params()
self.params.put_bool("AccelPersonalityEnabled", False, block=True)
def test_none_override_matches_stock(self):
for e2e in (False, True):
for allow_throttle in (False, True):
args = (e2e, 30.0, 12.0, 0.2, 4.0, _fake_cp(), DT_MDL, -0.3, allow_throttle)
assert get_cruise_accel(*args) == get_cruise_accel(*args, max_accel_override=None)
def test_profiles_do_not_change_far_braking(self):
# The ceiling is an upper bound only, so it can never participate in a deceleration. Braking authority
# stays with stock's clip to A_CRUISE_MIN for every profile.
args = (False, 0.0, 20.0, 0.0, 0.0, _fake_cp(), 10.0, -0.3, True)
stock = get_cruise_accel(*args)
assert stock == A_CRUISE_MIN
for profile_values in MAX_ACCEL_PROFILES.values():
assert get_cruise_accel(*args, max_accel_override=profile_values[0]) == stock
def test_stock_jerk_limit_still_owns_smoothing(self):
speed = 8.0
sport_limit = np.interp(speed, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[AccelProfile.sport])
target = get_cruise_accel(False, 30.0, speed, 0.0, 0.0, _fake_cp(), DT_MDL, 0.0, True, sport_limit)
jerk_limit = np.interp(speed, A_CRUISE_MAX_BP, J_CRUISE_VALS) * DT_MDL
assert np.isclose(target, jerk_limit)
def test_disabled_leaves_stock_limit_active(self):
planner = _bare_planner()
assert planner.get_max_accel_override(5.0) is None
assert planner.accel_controller_active is False
def test_enabled_profile_applies_to_cruise_candidate(self):
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
planner = _bare_planner()
expected = np.interp(5.0, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[AccelProfile.normal])
assert planner.get_max_accel_override(5.0) == expected
def test_enabled_acc_uses_python_native_telemetry_types(self):
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
self.params.put("AccelPersonality", AccelProfile.sport, block=True)
planner = _bare_planner()
expected = np.interp(5.0, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[AccelProfile.sport])
assert planner.get_max_accel_override(5.0) == expected
assert type(planner.accel_controller_active) is bool
assert type(planner.accel_controller.is_enabled()) is bool
assert type(planner.accel_controller.profile) is int
def test_normal_profile_uses_tuned_limit(self):
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
self.params.put("AccelPersonality", AccelProfile.normal, block=True)
planner = _bare_planner()
expected = np.interp(5.0, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[AccelProfile.normal])
assert planner.get_max_accel_override(5.0) == expected
def test_ceiling_applies_to_every_target_source(self):
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanSource
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
planner = _bare_planner()
speed = 29.0
expected = planner.accel_controller.get_max_accel(speed)
for source in (LongitudinalPlanSource.cruise, LongitudinalPlanSource.sccVision,
LongitudinalPlanSource.sccMap, LongitudinalPlanSource.speedLimitAssist):
planner.source = source
assert np.isclose(planner.get_max_accel_override(speed), expected), source
def test_ceiling_remains_active_without_throttle_intent(self):
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
self.params.put("AccelPersonality", AccelProfile.eco, block=True)
planner = _bare_planner()
planner.allow_throttle = False
assert planner.get_max_accel_override(12.0) == planner.accel_controller.get_max_accel(12.0)
def test_profile_switch_uses_stock_jerk_limit(self):
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
self.params.put("AccelPersonality", AccelProfile.sport, block=True)
planner = _bare_planner()
v_ego = 12.0
planner.a_cruise = planner.accel_controller.get_max_accel(v_ego)
self.params.put("AccelPersonality", AccelProfile.eco, block=True)
planner.accel_controller.update()
ceiling = planner.get_max_accel_override(v_ego)
previous = planner.a_cruise
accel = get_cruise_accel(False, 30.0, v_ego, previous, 0.0, _fake_cp(), DT_MDL, 0.0, True, ceiling)
jerk_step = float(np.interp(v_ego, A_CRUISE_MAX_BP, J_CRUISE_VALS)) * DT_MDL
assert previous > ceiling
assert np.isclose(previous - accel, jerk_step)
assert planner.a_cruise == previous
def test_cruise_target_shaping_is_source_and_force_decel_gated(self):
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanSource
self.params.put_bool("AccelPersonalityEnabled", True, block=True)
planner = _bare_planner()
shaped = planner.get_cruise_target_override(25.0, 22.5, force_decel=False)
assert 22.5 < shaped < 25.0
for source in (LongitudinalPlanSource.sccVision, LongitudinalPlanSource.sccMap, LongitudinalPlanSource.speedLimitAssist):
planner.source = source
assert planner.get_cruise_target_override(25.0, 22.5, force_decel=False) == 22.5
planner.source = LongitudinalPlanSource.cruise
assert planner.get_cruise_target_override(25.0, 0.0, force_decel=True) == 0.0
def test_e2e_candidate_is_held_through_a_brake_but_not_otherwise(self):
# Route 000005dd: e2e -> lead1 stepped +2.25 m/s^2 in one frame (45 m/s^3) and back the next, while the
# model held desiredAcceleration at -1.63 and never moved more than 0.024. Dropping a candidate the model
# still owns is what produced the brake/gas/brake flip.
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import E2E_BRAKE_HOLD_ACCEL, MpcPlanSource
planner = _bare_planner()
class _Mpc:
source = MpcPlanSource.cruise
class _Dec:
def __init__(self):
self._active = True
self._mode = "acc"
def active(self):
return self._active
def mode(self):
return self._mode
planner.mpc = _Mpc()
planner.dec = _Dec()
def sm(experimental: bool, model_accel: float):
return {
'selfdriveState': type("S", (), {"experimentalMode": experimental})(),
'modelV2': type("M", (), {"action": type("A", (), {"desiredAcceleration": model_accel})()})(),
}
braking = E2E_BRAKE_HOLD_ACCEL - 1.0
# Not experimental: never e2e, whatever the model wants.
assert planner.is_e2e(sm(False, braking)) is False
# DEC in acc, and the model was NOT the selected source: acc stands. This is the case that must stay
# untouched, or a phantom model brake could be pulled into the arbitration that never won it.
planner.mpc.source = MpcPlanSource.lead0
assert planner.is_e2e(sm(True, braking)) is False
# DEC in acc, model WAS selected and is still braking: hold it rather than release the brake.
planner.mpc.source = MpcPlanSource.e2e
assert planner.is_e2e(sm(True, braking)) is True
# Still selected but no longer braking: release, DEC's decision stands.
assert planner.is_e2e(sm(True, 0.0)) is False
assert planner.is_e2e(sm(True, E2E_BRAKE_HOLD_ACCEL + 0.01)) is False
# DEC blended, or DEC inactive, is unconditionally e2e as before.
planner.dec._mode = "blended"
assert planner.is_e2e(sm(True, 1.0)) is True
planner.dec._mode = "acc"
planner.dec._active = False
assert planner.is_e2e(sm(True, 1.0)) is True
def _fake_cp():
class CP:
steerRatio = 15.0
wheelbase = 2.7
return CP()
def _bare_planner():
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlannerSP, LongitudinalPlanSource
planner = LongitudinalPlannerSP.__new__(LongitudinalPlannerSP)
planner.accel_controller = AccelController()
planner.accel_controller_active = False
planner.allow_throttle = True
planner.a_cruise = 0.0
planner.source = LongitudinalPlanSource.cruise
return planner
@@ -1,329 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from collections.abc import Callable
import numpy as np
from openpilot.common.constants import CV
from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL
from openpilot.common.test import OpenpilotTestCase
from openpilot.selfdrive.controls.lib.drive_helpers import should_stop
from openpilot.selfdrive.controls.lib.longitudinal_planner import A_CRUISE_MAX_BP, A_CRUISE_MIN, J_CRUISE_VALS, get_cruise_accel
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalPlanSource, T_IDXS as T_IDXS_MPC
from openpilot.sunnypilot.selfdrive.controls.lib.accel_controller.accel_controller import (
AccelController, AccelProfile, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES,
)
from openpilot.sunnypilot.selfdrive.test.longitudinal_maneuvers.plant import PRIUS_TSS2_ROUTE_MODEL, PlantSP
class CarParams:
steerRatio = 15.0
wheelbase = 2.7
def _set_mpc_acceleration(plant: PlantSP, acceleration: float = 2.0) -> None:
def update(_radar_state, **_kwargs):
mpc = plant.planner.mpc
mpc.source = LongitudinalPlanSource.lead0
mpc.v_solution[:] = mpc.x0[1] + acceleration * T_IDXS_MPC
mpc.a_solution.fill(acceleration)
mpc.j_solution.fill(0.0)
plant.planner.mpc.update = update
def run_profile(profile: int, *, enabled: bool = True, speed: float = 0.0, v_cruise: float = 30.0,
v_cruise_fn: Callable[[int], float] | None = None, e2e: bool = False, steps: int = 120,
speed_noise: float = 0.0, seed: int = 0):
params = Params()
params.put_bool("AccelPersonalityEnabled", enabled, block=True)
params.put("AccelPersonality", profile, block=True)
controller = AccelController()
rng = np.random.default_rng(seed)
accel = 0.0
rows = []
for frame in range(steps):
target_speed = v_cruise if v_cruise_fn is None else v_cruise_fn(frame)
measured = speed + (float(rng.normal(0.0, speed_noise)) if speed_noise else 0.0)
max_accel_override = controller.get_max_accel(measured) if controller.is_enabled() else None
if controller.is_enabled():
target_speed = controller.get_cruise_target(measured, target_speed)
accel = get_cruise_accel(e2e, target_speed, measured, accel, 0.0, CarParams(), DT_MDL, 2.0, True, max_accel_override)
speed = max(0.0, speed + accel * DT_MDL)
rows.append((speed, accel, should_stop(speed, accel)))
return rows
def run_vehicle_profile(profile: int, duration: float = 80.0, enabled: bool = True, speed: float = 0.0,
v_cruise_fn: Callable[[float], float] | None = None):
params = Params()
params.put_bool("AccelPersonalityEnabled", enabled, block=True)
params.put("AccelPersonality", profile, block=True)
plant = PlantSP(speed=speed, actuator_model=PRIUS_TSS2_ROUTE_MODEL, run_long_control=True)
_set_mpc_acceleration(plant)
rows = []
while plant.current_time < duration:
v_cruise = 25.0 if v_cruise_fn is None else v_cruise_fn(plant.current_time)
result = plant.step(v_cruise=v_cruise)
rows.append((plant.current_time, result["speed"], result["a_target"], result["actuator_command"], result["acceleration"]))
return np.asarray(rows)
class TestAccelControllerClosedLoop(OpenpilotTestCase):
def test_profiles_are_immediate_smooth_and_clearly_distinct(self):
traces = {profile: run_vehicle_profile(profile) for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport)}
stock = run_vehicle_profile(AccelProfile.normal, enabled=False)
def crossing(trace, speed):
return float(trace[np.flatnonzero(trace[:, 1] >= speed)[0], 0])
time_to_20 = {profile: crossing(trace, 20.0 * CV.MPH_TO_MS) for profile, trace in traces.items()}
time_to_50 = {profile: crossing(trace, 50.0 * CV.MPH_TO_MS) for profile, trace in traces.items()}
first_motion = {profile: int(np.flatnonzero(trace[:, 1] > 0.01)[0]) for profile, trace in traces.items()}
self.assertEqual(len(set(first_motion.values())), 1)
self.assertTrue(all(trace[0, 2] > 0.0 and trace[1, 3] > 0.0 for trace in traces.values()))
self.assertLess(time_to_20[AccelProfile.eco], 8.0)
self.assertLess(time_to_50[AccelProfile.eco], 27.0)
self.assertGreaterEqual(time_to_20[AccelProfile.eco] - time_to_20[AccelProfile.normal], 0.5)
self.assertGreaterEqual(time_to_20[AccelProfile.normal] - time_to_20[AccelProfile.sport], 0.5)
self.assertGreaterEqual(time_to_50[AccelProfile.eco] - time_to_50[AccelProfile.normal], 2.0)
self.assertGreaterEqual(time_to_50[AccelProfile.normal] - time_to_50[AccelProfile.sport], 3.0)
# Asserted against stock rather than against the actuator's rate limit. The peak command jerk in this run
# is stock's stop-release ramp at launch (LongCtrlState.stopping -> pid), which on its own already exceeds
# PRIUS_TSS2_ROUTE_MODEL.command_rate_limit: measured 4.392 for stock and for all three profiles alike.
# An absolute bound here would only be testing that stock ramp, and would pass or fail on stock changes
# that have nothing to do with the profiles. What this test can honestly own is that the profiles add no
# command jerk of their own.
stock_peak_jerk = float(np.max(np.abs(np.diff(stock[:, 3])) / DT_MDL))
for profile, trace in traces.items():
command_jerk = np.abs(np.diff(trace[:, 3])) / DT_MDL
self.assertLessEqual(float(np.max(command_jerk)), stock_peak_jerk + 1e-9, profile)
settled = np.flatnonzero(trace[:, 1] >= 25.0 - 0.15)
self.assertGreater(len(settled), 0)
settled_trace = trace[settled[0]:]
self.assertGreaterEqual(float(np.min(settled_trace[:, 3])), -0.05)
self.assertLessEqual(float(np.max(trace[:, 1])), 25.0 + 1e-9)
def test_blended_positive_model_request_uses_profile_cruise_cap(self):
params = Params()
params.put_bool("DynamicExperimentalControl", False, block=True)
params.put_bool("AccelPersonalityEnabled", True, block=True)
params.put("AccelPersonality", AccelProfile.eco, block=True)
def request_acceleration(_current_time: float, _speed: float, _acceleration: float) -> tuple[float, bool]:
return 2.0, False
plant = PlantSP(speed=15.0, e2e=True, model_action_fn=request_acceleration)
_set_mpc_acceleration(plant)
results = [plant.step(v_cruise=35.0) for _ in range(20)]
settled = results[-1]
eco_limit = float(np.interp(settled["published_v_ego"], MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[AccelProfile.eco]))
self.assertTrue(settled["controller_active"])
self.assertEqual(settled["mpc_source"], LongitudinalPlanSource.cruise)
self.assertAlmostEqual(settled["a_target"], eco_limit, delta=0.01)
self.assertLess(settled["a_target"], settled["model_action"]["desiredAcceleration"])
def test_lower_cruise_target_does_not_soften_model_braking(self):
params = Params()
params.put_bool("DynamicExperimentalControl", False, block=True)
params.put("AccelPersonality", AccelProfile.eco, block=True)
def request_braking(_current_time: float, _speed: float, _acceleration: float) -> tuple[float, bool]:
return -0.8, False
traces = {}
for enabled in (False, True):
params.put_bool("AccelPersonalityEnabled", enabled, block=True)
plant = PlantSP(speed=20.0, e2e=True, model_action_fn=request_braking)
_set_mpc_acceleration(plant)
traces[enabled] = [plant.step(v_cruise=19.5) for _ in range(10)]
self.assertTrue(all(row["mpc_source"] == LongitudinalPlanSource.e2e for row in traces[True]))
self.assertTrue(all(not row["controller_active"] for trace in traces.values() for row in trace))
for key in ("a_target", "should_stop", "mpc_source"):
self.assertEqual([row[key] for row in traces[True]], [row[key] for row in traces[False]])
def test_profile_does_not_change_model_stop_request(self):
params = Params()
params.put_bool("DynamicExperimentalControl", False, block=True)
params.put("AccelPersonality", AccelProfile.eco, block=True)
def request_stop(_current_time: float, _speed: float, _acceleration: float) -> tuple[float, bool]:
return -0.8, True
traces = {}
for enabled in (False, True):
params.put_bool("AccelPersonalityEnabled", enabled, block=True)
plant = PlantSP(speed=1.0, e2e=True, model_action_fn=request_stop)
_set_mpc_acceleration(plant)
traces[enabled] = [plant.step(v_cruise=30.0) for _ in range(10)]
for key in ("a_target", "should_stop", "mpc_source"):
self.assertEqual([row[key] for row in traces[True]], [row[key] for row in traces[False]])
def test_lower_cruise_target_does_not_soften_lead_braking(self):
params = Params()
params.put_bool("DynamicExperimentalControl", False, block=True)
params.put("AccelPersonality", AccelProfile.eco, block=True)
traces = {}
for enabled in (False, True):
params.put_bool("AccelPersonalityEnabled", enabled, block=True)
plant = PlantSP(speed=20.0)
_set_mpc_acceleration(plant, -0.8)
traces[enabled] = [plant.step(v_cruise=19.5) for _ in range(10)]
self.assertTrue(all(row["mpc_source"] == LongitudinalPlanSource.lead0 for row in traces[True]))
for key in ("a_target", "should_stop", "mpc_source"):
self.assertEqual([row[key] for row in traces[True]], [row[key] for row in traces[False]])
def test_normal_launch_is_faster_than_eco(self):
eco = run_profile(AccelProfile.eco, speed=4.0, steps=120)
normal = run_profile(AccelProfile.normal, speed=4.0, steps=120)
self.assertGreater(normal[-1][0], eco[-1][0])
def test_zero_speed_stop_request_is_unchanged(self):
# Zero-speed stop requests bypass the small cruise-setpoint pre-shape.
for e2e in (False, True):
stock = run_profile(AccelProfile.normal, enabled=False, speed=20.0, v_cruise=0.0, e2e=e2e, steps=100)
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport):
self.assertEqual(run_profile(profile, speed=20.0, v_cruise=0.0, e2e=e2e, steps=100), stock)
def test_large_cruise_decel_retains_stock_authority(self):
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport):
trace = np.asarray(run_profile(profile, speed=25.0, v_cruise=20.0, steps=220))
self.assertAlmostEqual(float(np.min(trace[:, 1])), A_CRUISE_MIN, places=12)
self.assertGreaterEqual(float(np.min(trace[:, 0])), 20.0 - 1e-9)
def test_small_cruise_decel_is_profiled_and_smooth(self):
target = 25.0 - 5.0 * CV.MPH_TO_MS
stock = np.asarray(run_profile(AccelProfile.normal, enabled=False, speed=25.0, v_cruise=target, steps=300))
traces = {
profile: np.asarray(run_profile(profile, speed=25.0, v_cruise=target, steps=300))
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport)
}
peak_decel = {profile: float(np.min(trace[:, 1])) for profile, trace in traces.items()}
self.assertGreater(peak_decel[AccelProfile.eco], peak_decel[AccelProfile.normal])
self.assertGreater(peak_decel[AccelProfile.normal], peak_decel[AccelProfile.sport])
self.assertGreater(peak_decel[AccelProfile.sport], float(np.min(stock[:, 1])))
for trace in traces.values():
peak_frame = int(np.argmin(trace[:, 1]))
self.assertTrue(np.all(np.diff(trace[peak_frame:, 1]) >= -1e-12))
self.assertGreaterEqual(float(np.min(trace[:, 0])), target - 1e-9)
speeds = np.concatenate(([25.0], trace[:-1, 0]))
jerk_limit = np.interp(speeds, A_CRUISE_MAX_BP, J_CRUISE_VALS)
jerk = np.abs(np.diff(np.concatenate(([0.0], trace[:, 1])))) / DT_MDL
self.assertTrue(np.all(jerk <= jerk_limit + 1e-9))
def test_small_cruise_decel_stays_smooth_through_actuator(self):
target = 25.0 - 5.0 * CV.MPH_TO_MS
def cruise_target(current_time: float) -> float:
return 25.0 if current_time < 2.0 else target
stock = run_vehicle_profile(AccelProfile.normal, duration=12.0, enabled=False, speed=25.0, v_cruise_fn=cruise_target)
traces = [
run_vehicle_profile(profile, duration=12.0, speed=25.0, v_cruise_fn=cruise_target)
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport)
]
step_frame = int(np.flatnonzero(stock[:, 0] >= 2.0)[0])
stock_decel = stock[step_frame:]
for trace in traces:
decel = trace[step_frame:]
self.assertGreater(float(np.min(decel[:, 2])), float(np.min(stock_decel[:, 2])))
self.assertGreater(float(np.min(decel[:, 4])), float(np.min(stock_decel[:, 4])))
self.assertGreaterEqual(float(np.min(decel[:, 1])), target - 1e-9)
for column in (2, 3, 4):
jerk = np.max(np.abs(np.diff(trace[step_frame - 1:, column]))) / DT_MDL
stock_jerk = np.max(np.abs(np.diff(stock[step_frame - 1:, column]))) / DT_MDL
self.assertLessEqual(float(jerk), float(stock_jerk) + 1e-9)
def test_blended_launch_respects_profiles(self):
traces = {
profile: run_profile(profile, v_cruise=8.0, e2e=True, steps=180)
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport)
}
time_to_five = {
profile: next(frame for frame, row in enumerate(rows) if row[0] >= 5.0) * DT_MDL
for profile, rows in traces.items()
}
self.assertLess(time_to_five[AccelProfile.sport], time_to_five[AccelProfile.normal])
self.assertLess(time_to_five[AccelProfile.normal], time_to_five[AccelProfile.eco])
def test_launch_ordering_without_departure_delay(self):
stock = run_profile(AccelProfile.normal, enabled=False, v_cruise=8.0, steps=160)
traces = {
profile: run_profile(profile, v_cruise=8.0, steps=160)
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport)
}
first_motion = {
profile: next(frame for frame, row in enumerate(rows) if row[0] > 0.01)
for profile, rows in traces.items()
}
time_to_five = {
profile: next(frame for frame, row in enumerate(rows) if row[0] >= 5.0) * DT_MDL
for profile, rows in traces.items()
}
stock_first_motion = next(frame for frame, row in enumerate(stock) if row[0] > 0.01)
# No launch dead time: motion starts on the same frame as stock, because the ceiling only ever bounds the
# command from above and stock's own law owns the first frame.
self.assertEqual(len(set(first_motion.values())), 1)
self.assertTrue(all(frame == stock_first_motion for frame in first_motion.values()))
self.assertGreaterEqual(time_to_five[AccelProfile.eco] - time_to_five[AccelProfile.normal], 0.1)
self.assertGreaterEqual(time_to_five[AccelProfile.normal] - time_to_five[AccelProfile.sport], 0.1)
def test_road_speed_catchup_stays_useful(self):
traces = {
profile: run_profile(profile, speed=20.0, v_cruise=30.0, steps=100)
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport)
}
gains = {profile: rows[-1][0] - 20.0 for profile, rows in traces.items()}
self.assertGreater(gains[AccelProfile.normal] - gains[AccelProfile.eco], 0.05)
self.assertGreater(gains[AccelProfile.sport] - gains[AccelProfile.normal], 0.1)
def test_full_catchup_trace_respects_stock_jerk(self):
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport):
rows = run_profile(profile, v_cruise=30.0, steps=300)
previous_speed = 0.0
previous_accel = 0.0
for speed, accel, _should_stop in rows:
jerk_step = float(np.interp(previous_speed, A_CRUISE_MAX_BP, J_CRUISE_VALS)) * DT_MDL
self.assertLessEqual(abs(accel - previous_accel), jerk_step + 1e-12)
previous_speed = speed
previous_accel = accel
def test_stop_release_frame_is_profile_independent(self):
def target_speed(frame: int) -> float:
return 0.0 if frame < 20 else 8.0
traces = {
profile: run_profile(profile, v_cruise_fn=target_speed, steps=80)
for profile in (AccelProfile.eco, AccelProfile.normal, AccelProfile.sport)
}
release_frames = {
profile: next(frame for frame, row in enumerate(rows) if frame >= 20 and not row[2])
for profile, rows in traces.items()
}
stock = run_profile(AccelProfile.normal, enabled=False, v_cruise_fn=target_speed, steps=80)
stock_release_frame = next(frame for frame, row in enumerate(stock) if frame >= 20 and not row[2])
self.assertEqual(len(set(release_frames.values())), 1)
self.assertTrue(all(frame == stock_release_frame for frame in release_frames.values()))
@@ -0,0 +1,17 @@
class WMACConstants:
# Lead detection parameters
LEAD_WINDOW_SIZE = 6 # Stable detection window
LEAD_PROB = 0.45 # Balanced threshold for lead detection
# Slow down detection parameters
SLOW_DOWN_WINDOW_SIZE = 5 # Responsive but stable
SLOW_DOWN_PROB = 0.3 # Balanced threshold for slow down scenarios
# Optimized slow down distance curve - smooth and progressive
SLOW_DOWN_BP = [0., 10., 20., 30., 40., 50., 55., 60.]
SLOW_DOWN_DIST = [32., 46., 64., 86., 108., 130., 145., 165.]
# Slowness detection parameters
SLOWNESS_WINDOW_SIZE = 10 # Stable slowness detection
SLOWNESS_PROB = 0.55 # Clear threshold for slowness
SLOWNESS_CRUISE_OFFSET = 1.025 # Conservative cruise speed offset
@@ -4,120 +4,192 @@ Copyright (c) 2021-, rav4kumar, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from dataclasses import dataclass
from typing import Literal
import numpy as np
# Version = 2025-6-30
from openpilot.cereal import messaging
from opendbc.car import structs
from numpy import interp
from openpilot.common.params import Params
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.common.realtime import DT_MDL
from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants
from typing import Literal
# d-e2e, from modeldata.h
TRAJECTORY_SIZE = 33
SET_MODE_TIMEOUT = 15
# Define the valid mode types
ModeType = Literal['acc', 'blended']
_DECEL_LOOKAHEAD_MIN_T = 1.0
_DECEL_LOOKAHEAD_MAX_T = 6.0
_T_IDXS = np.array(ModelConstants.T_IDXS)
_DECEL_IDX = np.where((_T_IDXS >= _DECEL_LOOKAHEAD_MIN_T) & (_T_IDXS <= _DECEL_LOOKAHEAD_MAX_T))[0]
_DECEL_INV_T = 1.0 / _T_IDXS[_DECEL_IDX]
DECEL_INTENT_A_HINT = 0.35
DECEL_INTENT_A_FULL = 1.30
DECEL_INTENT_TRIGGER = 0.5
DECEL_INTENT_CURVE_OVERRIDE = 0.9
class SmoothKalmanFilter:
"""Enhanced Kalman filter with smoothing for stable decision making."""
CURVE_Y_MAX = 5.0
def __init__(self, initial_value=0, measurement_noise=0.1, process_noise=0.01,
alpha=1.0, smoothing_factor=0.85):
self.x = initial_value
self.P = 1.0
self.R = measurement_noise
self.Q = process_noise
self.alpha = alpha
self.smoothing_factor = smoothing_factor
self.initialized = False
self.history = []
self.max_history = 10
self.confidence = 0.0
LEAD_FUTURE_PROB_VANISH = 0.35
LEAD_VETO_CONFIRM_FRAMES = 4
def add_data(self, measurement):
if len(self.history) >= self.max_history:
self.history.pop(0)
self.history.append(measurement)
MODEL_DROP_TRUST_FULL = 5.0
MODEL_DROP_TRUST_NONE = 30.0
MODEL_TRUST_MIN = 0.5
if not self.initialized:
self.x = measurement
self.initialized = True
self.confidence = 0.1
return
CREEP_SPEED_ENTER = 2.0
CREEP_SPEED_EXIT = 3.0
self.P = self.alpha * self.P + self.Q
ENTER_FRAMES = 3
EXIT_FRAMES = 16
MIN_BLENDED_FRAMES = 20
K = self.P / (self.P + self.R)
effective_K = K * (1.0 - self.smoothing_factor) + self.smoothing_factor * 0.1
PARAM_READ_FRAMES = 5
innovation = measurement - self.x
self.x = self.x + effective_K * innovation
self.P = (1 - effective_K) * self.P
@dataclass
class DecSignals:
decel_intent: float = 0.0
curve_detected: bool = False
model_trust: float = 1.0
creeping: bool = False
def should_blend(s: DecSignals) -> bool:
degraded = s.model_trust < MODEL_TRUST_MIN
curve_gate = s.decel_intent >= DECEL_INTENT_CURVE_OVERRIDE or not s.curve_detected
slowdown_detected = not degraded and s.decel_intent >= DECEL_INTENT_TRIGGER and curve_gate
return slowdown_detected or s.creeping
class ModeHysteresis:
def __init__(self):
self.mode: ModeType = 'acc'
self.above = 0
self.below = 0
self.blended_frames = 0
def update(self, want_blended: bool, override: bool, veto: bool) -> ModeType:
self.above = self.above + 1 if want_blended else 0
self.below = 0 if want_blended else self.below + 1
if override:
self.mode, self.blended_frames = 'blended', 0
elif veto:
self.mode = 'acc'
elif self.mode == 'acc':
if self.above >= ENTER_FRAMES:
self.mode, self.blended_frames = 'blended', 0
if abs(innovation) < 0.1:
self.confidence = min(1.0, self.confidence + 0.05)
else:
self.blended_frames += 1
if self.blended_frames >= MIN_BLENDED_FRAMES and self.below >= EXIT_FRAMES:
self.mode = 'acc'
return self.mode
self.confidence = max(0.1, self.confidence - 0.02)
def reset(self) -> None:
self.mode = 'acc'
self.above = 0
self.below = 0
self.blended_frames = 0
def get_value(self):
return self.x if self.initialized else None
def get_confidence(self):
return self.confidence
def reset_data(self):
self.initialized = False
self.history = []
self.confidence = 0.0
class ModeTransitionManager:
"""Manages smooth transitions between driving modes with hysteresis."""
def __init__(self):
self.current_mode: ModeType = 'acc'
self.mode_confidence = {'acc': 1.0, 'blended': 0.0}
self.transition_timeout = 0
self.min_mode_duration = 10
self.mode_duration = 0
self.emergency_override = False
def request_mode(self, mode: ModeType, confidence: float = 1.0, emergency: bool = False):
# Emergency override for critical situations (stops, collisions)
if emergency:
self.emergency_override = True
self.current_mode = mode
self.transition_timeout = SET_MODE_TIMEOUT
self.mode_duration = 0
return
self.mode_confidence[mode] = min(1.0, self.mode_confidence[mode] + 0.1 * confidence)
for m in self.mode_confidence:
if m != mode:
self.mode_confidence[m] = max(0.0, self.mode_confidence[m] - 0.05)
# Require minimum duration in current mode (unless emergency)
if self.mode_duration < self.min_mode_duration and not self.emergency_override:
return
# Hysteresis: higher threshold for mode changes
confidence_threshold = 0.6 if mode != self.current_mode else 0.3 # Lower threshold for faster response
if self.mode_confidence[mode] > confidence_threshold:
if mode != self.current_mode and self.transition_timeout == 0:
self.transition_timeout = SET_MODE_TIMEOUT
self.current_mode = mode
self.mode_duration = 0
def update(self):
if self.transition_timeout > 0:
self.transition_timeout -= 1
self.mode_duration += 1
# Reset emergency override after some time
if self.emergency_override and self.mode_duration > 20:
self.emergency_override = False
# Gradual confidence decay
for mode in self.mode_confidence:
self.mode_confidence[mode] *= 0.98
def get_mode(self) -> ModeType:
return self.current_mode
class DynamicExperimentalController:
def __init__(self, CP: structs.CarParams, mpc, params=None):
self._CP = CP
self._mpc = mpc
self._params = params or Params()
self._enabled: bool = self._params.get_bool("DynamicExperimentalControl")
self._active: bool = False
self._frame: int = 0
self._urgency = 0.0
self._hysteresis = ModeHysteresis()
self._creeping = False
self._lead_veto_frames = 0
self._mode_manager = ModeTransitionManager()
self.signals = DecSignals()
self.want_blended = False
self.lead_veto = False
# Smooth filters for stable decision making with faster response for critical scenarios
self._lead_filter = SmoothKalmanFilter(
measurement_noise=0.15,
process_noise=0.05,
alpha=1.02,
smoothing_factor=0.8
)
def _update_creeping(self, v_ego: float) -> bool:
self._creeping = v_ego < CREEP_SPEED_EXIT if self._creeping else v_ego <= CREEP_SPEED_ENTER
return self._creeping
self._slow_down_filter = SmoothKalmanFilter(
measurement_noise=0.1,
process_noise=0.1,
alpha=1.05,
smoothing_factor=0.7
)
self._slowness_filter = SmoothKalmanFilter(
measurement_noise=0.1,
process_noise=0.06,
alpha=1.015,
smoothing_factor=0.92
)
self._mpc_fcw_filter = SmoothKalmanFilter(
measurement_noise=0.2,
process_noise=0.1,
alpha=1.1,
smoothing_factor=0.5
)
self._has_lead_filtered = False
self._has_slow_down = False
self._has_slowness = False
self._has_mpc_fcw = False
self._v_ego_kph = 0.0
self._v_cruise_kph = 0.0
self._has_standstill = False
self._mpc_fcw_crash_cnt = 0
self._standstill_count = 0
# debug
self._endpoint_x = float('inf')
self._expected_distance = 0.0
self._trajectory_valid = False
def _read_params(self) -> None:
if self._frame % PARAM_READ_FRAMES == 0:
if self._frame % int(1. / DT_MDL) == 0:
self._enabled = self._params.get_bool("DynamicExperimentalControl")
def mode(self) -> str:
return self._hysteresis.mode
return self._mode_manager.get_mode()
def enabled(self) -> bool:
return self._enabled
@@ -125,77 +197,192 @@ class DynamicExperimentalController:
def active(self) -> bool:
return self._active
@staticmethod
def _decel_intent(md) -> float:
v = np.asarray(md.velocity.x)
if len(v) != len(_T_IDXS):
return 0.0
a_req = float(np.min((v[_DECEL_IDX] - v[0]) * _DECEL_INV_T))
return float(np.interp(-a_req, [DECEL_INTENT_A_HINT, DECEL_INTENT_A_FULL], [0.0, 1.0]))
def set_mpc_fcw_crash_cnt(self) -> None:
"""Set MPC FCW crash count"""
self._mpc_fcw_crash_cnt = self._mpc.crash_cnt
@staticmethod
def _curve_detected(md) -> bool:
y = md.position.y
if len(y) < 1:
return False
return abs(y[-1]) >= CURVE_Y_MAX
def _update_calculations(self, sm: messaging.SubMaster) -> None:
car_state = sm['carState']
lead_one = sm['radarState'].leadOne
md = sm['modelV2']
@staticmethod
def _model_trust(md) -> float:
if len(md.velocity.x) != len(_T_IDXS):
return 0.0
return float(np.interp(md.frameDropPerc, [MODEL_DROP_TRUST_FULL, MODEL_DROP_TRUST_NONE], [1.0, 0.0]))
self._v_ego_kph = car_state.vEgo * 3.6
self._v_cruise_kph = car_state.vCruise
self._has_standstill = car_state.standstill
@staticmethod
def _lead_veto(radar_state, md) -> bool:
lead_one, lead_two = radar_state.leadOne, radar_state.leadTwo
lead_now = lead_one.present or lead_two.present
probs = md.leadsV3
future = min(probs[1].prob, probs[2].prob) if len(probs) >= 3 else 1.0
return bool(lead_now and future > LEAD_FUTURE_PROB_VANISH)
# standstill detection
if self._has_standstill:
self._standstill_count = min(20, self._standstill_count + 1)
else:
self._standstill_count = max(0, self._standstill_count - 1)
def _update_lead_veto(self, raw_veto: bool, lead_present: bool, urgent_override: bool) -> bool:
if raw_veto:
self._lead_veto_frames = min(self._lead_veto_frames + 1, LEAD_VETO_CONFIRM_FRAMES)
return self.lead_veto or self._lead_veto_frames >= LEAD_VETO_CONFIRM_FRAMES
# Lead detection
self._lead_filter.add_data(float(lead_one.present))
lead_value = self._lead_filter.get_value() or 0.0
self._has_lead_filtered = lead_value > WMACConstants.LEAD_PROB
if not lead_present or urgent_override or not self.lead_veto:
self._lead_veto_frames = 0
return False
# MPC FCW detection
fcw_filtered_value = self._mpc_fcw_filter.get_value() or 0.0
self._mpc_fcw_filter.add_data(float(self._mpc_fcw_crash_cnt > 0))
self._has_mpc_fcw = fcw_filtered_value > 0.5
self._lead_veto_frames = max(self._lead_veto_frames - 1, 0)
return self._lead_veto_frames > 0
# Slow down detection
self._calculate_slow_down(md)
# Slowness detection
if not (self._standstill_count > 5) and not self._has_slow_down:
current_slowness = float(self._v_ego_kph <= (self._v_cruise_kph * WMACConstants.SLOWNESS_CRUISE_OFFSET))
self._slowness_filter.add_data(current_slowness)
slowness_value = self._slowness_filter.get_value() or 0.0
# Hysteresis for slowness
threshold = WMACConstants.SLOWNESS_PROB * (0.8 if self._has_slowness else 1.1)
self._has_slowness = slowness_value > threshold
def _calculate_slow_down(self, md):
"""Calculate urgency based on trajectory endpoint vs expected distance."""
# Reset to safe defaults
urgency = 0.0
self._endpoint_x = float('inf')
self._trajectory_valid = False
#Require exact trajectory size
position_valid = len(md.position.x) == TRAJECTORY_SIZE
orientation_valid = len(md.orientation.x) == TRAJECTORY_SIZE
if not (position_valid and orientation_valid):
# Invalid trajectory - this itself might indicate a stop scenario
# Apply moderate urgency for incomplete trajectories at speed
if self._v_ego_kph > 20.0:
urgency = 0.3
self._slow_down_filter.add_data(urgency)
urgency_filtered = self._slow_down_filter.get_value() or 0.0
self._has_slow_down = urgency_filtered > WMACConstants.SLOW_DOWN_PROB
self._urgency = urgency_filtered
return
# We have a valid full trajectory
self._trajectory_valid = True
# Use the exact endpoint (33rd point, index 32)
endpoint_x = md.position.x[TRAJECTORY_SIZE - 1]
self._endpoint_x = endpoint_x
# Get expected distance based on current speed using tuned constants
expected_distance = interp(self._v_ego_kph,
WMACConstants.SLOW_DOWN_BP,
WMACConstants.SLOW_DOWN_DIST)
self._expected_distance = expected_distance
# Calculate urgency based on trajectory shortage
if endpoint_x < expected_distance:
shortage = expected_distance - endpoint_x
shortage_ratio = shortage / expected_distance
# Base urgency on shortage ratio
urgency = min(1.0, shortage_ratio * 2.0)
# Increase urgency for very short trajectories (imminent stops)
critical_distance = expected_distance * 0.3
if endpoint_x < critical_distance:
urgency = min(1.0, urgency * 2.0)
# Speed-based urgency adjustment
if self._v_ego_kph > 25.0:
speed_factor = 1.0 + (self._v_ego_kph - 25.0) / 80.0
urgency = min(1.0, urgency * speed_factor)
# Apply filtering but with less smoothing for stops
self._slow_down_filter.add_data(urgency)
urgency_filtered = self._slow_down_filter.get_value() or 0.0
# Update state with lower threshold for better stop detection
self._has_slow_down = urgency_filtered > (WMACConstants.SLOW_DOWN_PROB * 0.8)
self._urgency = urgency_filtered
def _radarless_mode(self) -> None:
"""Radarless mode decision logic with emergency handling."""
# EMERGENCY: MPC FCW - immediate blended mode
if self._has_mpc_fcw:
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
return
# Standstill: use blended
if self._standstill_count > 3:
self._mode_manager.request_mode('blended', confidence=0.9)
return
# Slow down scenarios: emergency for high urgency, normal for lower urgency
if self._has_slow_down:
if self._urgency > 0.7:
# Emergency: immediate blended mode for high urgency stops
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
else:
# Normal: blended with urgency-based confidence
confidence = min(1.0, self._urgency * 1.5)
self._mode_manager.request_mode('blended', confidence=confidence)
return
# Driving slow: use ACC (but not if actively slowing down)
if self._has_slowness and not self._has_slow_down:
self._mode_manager.request_mode('acc', confidence=0.8)
return
# Default: ACC
self._mode_manager.request_mode('acc', confidence=0.7)
def _radar_mode(self) -> None:
"""Radar mode with emergency handling."""
# EMERGENCY: MPC FCW - immediate blended mode
if self._has_mpc_fcw:
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
return
# If lead detected and not in standstill: always use ACC
if self._has_lead_filtered and not (self._standstill_count > 3):
self._mode_manager.request_mode('acc', confidence=1.0)
return
# Slow down scenarios: emergency for high urgency, normal for lower urgency
if self._has_slow_down:
if self._urgency > 0.7:
# Emergency: immediate blended mode for high urgency stops
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
else:
# Normal: blended with urgency-based confidence
confidence = min(1.0, self._urgency * 1.3)
self._mode_manager.request_mode('blended', confidence=confidence)
return
# Standstill: use blended
if self._standstill_count > 3:
self._mode_manager.request_mode('blended', confidence=0.9)
return
# Driving slow: use ACC (but not if actively slowing down)
if self._has_slowness and not self._has_slow_down:
self._mode_manager.request_mode('acc', confidence=0.8)
return
# Default: ACC
self._mode_manager.request_mode('acc', confidence=0.7)
def update(self, sm: messaging.SubMaster) -> None:
self._read_params()
car_state = sm['carState']
md = sm['modelV2']
radar_state = sm['radarState']
self.set_mpc_fcw_crash_cnt()
is_creeping = self._update_creeping(car_state.vEgo)
lead_present = radar_state.leadOne.present or radar_state.leadTwo.present
self.signals = DecSignals(
decel_intent=self._decel_intent(md),
curve_detected=self._curve_detected(md),
model_trust=self._model_trust(md),
creeping=is_creeping and not lead_present,
)
self.want_blended = should_blend(self.signals)
self._update_calculations(sm)
crash_override = self._mpc.crash_cnt >= 1
hard_brake_override = bool(md.meta.hardBrakePredicted)
strong_stop = self.signals.model_trust >= MODEL_TRUST_MIN and self.signals.decel_intent >= DECEL_INTENT_CURVE_OVERRIDE
raw_lead_veto = self._lead_veto(radar_state, md)
urgent_release = (crash_override or hard_brake_override or strong_stop) and not raw_lead_veto
self.lead_veto = self._update_lead_veto(raw_lead_veto, lead_present, urgent_release)
override = (crash_override or hard_brake_override) and not self.lead_veto
if self._enabled:
self._hysteresis.update(self.want_blended, override, self.lead_veto)
if self._CP.radarUnavailable:
self._radarless_mode()
else:
self._hysteresis.reset()
self._radar_mode()
self._mode_manager.update()
self._active = sm['selfdriveState'].experimentalMode and self._enabled
self._frame += 1
@@ -1,465 +1,91 @@
import numpy as np
from openpilot.cereal import messaging
from opendbc.car import structs
from openpilot.common.test import OpenpilotTestCase
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import (
DecSignals,
DynamicExperimentalController,
ModeHysteresis,
should_blend,
ENTER_FRAMES,
LEAD_VETO_CONFIRM_FRAMES,
MIN_BLENDED_FRAMES,
)
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController
T_IDXS = np.array(ModelConstants.T_IDXS)
class MockLeadOne:
def __init__(self, present=0.0):
self.present = present
class MockRadarState:
def __init__(self, present=0.0):
self.leadOne = MockLeadOne(present=present)
class MockCarState:
def __init__(self, vEgo=0.0, vCruise=0.0, standstill=False):
self.vEgo = vEgo
self.vCruise = vCruise
self.standstill = standstill
class MockModelData:
def __init__(self, valid=True):
size = 33 if valid else 10 # incomplete if invalid
self.position = type("Pos", (), {"x": [0.0] * size})()
self.orientation = type("Ori", (), {"x": [0.0] * size})()
class MockSelfDriveState:
def __init__(self, experimentalMode=False):
self.experimentalMode = experimentalMode
class MockParams:
def __init__(self, enabled=True):
self._enabled = enabled
def get_bool(self, name):
return self._enabled
return True
class MockMpc:
def __init__(self, crash_cnt=0):
self.crash_cnt = crash_cnt
def flat_velocity(v):
return [float(v)] * len(T_IDXS)
def decel_velocity(v0, a):
return [float(max(0.0, v0 + a * t)) for t in T_IDXS]
def make_car_state(v_ego=10.0, v_cruise=20.0):
msg = messaging.new_message('carState')
msg.carState.vEgo = v_ego
msg.carState.vCruise = v_cruise
return msg.carState.as_reader()
def make_selfdrive_state(experimental_mode=True):
msg = messaging.new_message('selfdriveState')
msg.selfdriveState.experimentalMode = experimental_mode
return msg.selfdriveState.as_reader()
def make_radar_state(lead_present=False, lead_radar=False, lead_two_present=False):
msg = messaging.new_message('radarState')
msg.radarState.leadOne.present = lead_present
msg.radarState.leadOne.radar = lead_radar
msg.radarState.leadTwo.present = lead_two_present
return msg.radarState.as_reader()
def make_model_v2(velocity=None, position_y=None, hard_brake=False, lead_probs=None, frame_drop_perc=0.0):
msg = messaging.new_message('modelV2')
msg.modelV2.velocity.x = velocity if velocity is not None else flat_velocity(0.0)
msg.modelV2.position.y = position_y if position_y is not None else [0.0] * len(T_IDXS)
msg.modelV2.frameDropPerc = frame_drop_perc
msg.modelV2.meta.hardBrakePredicted = hard_brake
if lead_probs is not None:
msg.modelV2.init('leadsV3', 3)
for i, (prob, prob_time) in enumerate(zip(lead_probs, (0.0, 2.0, 4.0), strict=True)):
msg.modelV2.leadsV3[i].prob = prob
msg.modelV2.leadsV3[i].probTime = prob_time
return msg.modelV2.as_reader()
def make_sm(v_ego=10.0, v_cruise=20.0, velocity=None, position_y=None, hard_brake=False,
lead_present=False, lead_radar=False, lead_two_present=False, lead_probs=None,
frame_drop_perc=0.0, experimental_mode=True):
return {
'carState': make_car_state(v_ego, v_cruise),
'radarState': make_radar_state(lead_present, lead_radar, lead_two_present),
'modelV2': make_model_v2(velocity, position_y, hard_brake, lead_probs, frame_drop_perc),
'selfdriveState': make_selfdrive_state(experimental_mode),
def default_sm():
sm = {
'carState': MockCarState(vEgo=10.0, vCruise=20.0),
'radarState': MockRadarState(present=1.0),
'modelV2': MockModelData(valid=True),
'selfdriveState': MockSelfDriveState(experimentalMode=True),
}
return sm
def mock_cp():
class CP:
radarUnavailable = False
return CP()
def make_controller(cp=None, mpc=None, enabled=True):
return DynamicExperimentalController(cp or structs.CarParams(), mpc or MockMpc(), params=MockParams(enabled))
def mock_mpc():
class MPC:
crash_cnt = 0
return MPC()
# Fake Kalman Filter that always returns a given value
class FakeKalman:
def __init__(self, value=1.0):
self.value = value
def add_data(self, v): pass
def get_value(self): return self.value
def get_confidence(self): return 1.0
def reset_data(self): pass
class TestDynamicExperimentalController(OpenpilotTestCase):
def test_initial_mode_is_acc(self):
controller = make_controller()
def test_initial_mode_is_acc(self, mock_cp, mock_mpc):
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
assert controller.mode() == "acc"
def test_flat_plan_never_blends_at_any_speed(self):
for v_ego in (2.5, 5.6, 8.3, 13.9, 22.2, 30.6):
controller = make_controller()
sm = make_sm(v_ego=v_ego, velocity=flat_velocity(v_ego))
for _ in range(100):
controller.update(sm)
assert controller.mode() == "acc", f"false blend on a flat plan at v_ego={v_ego}"
def test_highway_slowdown_without_lead_blends(self):
v0 = 110 / 3.6
a = (70 / 3.6 - v0) / 6.0
controller = make_controller()
sm = make_sm(v_ego=v0, velocity=decel_velocity(v0, a))
def test_standstill_triggers_blended(self, mock_cp, mock_mpc, default_sm):
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
default_sm['carState'].standstill = True
for _ in range(10):
controller.update(sm)
controller.update(default_sm)
assert controller.mode() == "blended"
def test_curve_exclusion_prevents_false_blend(self):
controller = make_controller()
sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -1.0), position_y=[6.0] * len(T_IDXS))
for _ in range(30):
controller.update(sm)
assert controller.mode() == "acc"
def test_curve_does_not_override_saturated_decel_intent(self):
controller = make_controller()
sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0), position_y=[6.0] * len(T_IDXS))
for _ in range(10):
controller.update(sm)
def test_emergency_blended_on_fcw(self, mock_cp, mock_mpc, default_sm):
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
mock_mpc.crash_cnt = 1 # simulate FCW
for _ in range(2):
controller.update(default_sm)
assert controller.mode() == "blended"
def test_persistent_lead_forces_acc_even_with_strong_model_signal(self):
for lead_radar in (True, False):
controller = make_controller()
sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
lead_present=True, lead_radar=lead_radar, lead_probs=[1.0, 1.0, 1.0])
for _ in range(LEAD_VETO_CONFIRM_FRAMES - 1):
controller.update(sm)
assert not controller.lead_veto
for _ in range(60):
controller.update(sm)
assert controller.lead_veto
assert controller.mode() == "acc"
def test_radarless_slowdown_triggers_blended(self, mock_cp, mock_mpc, default_sm):
mock_cp.radarUnavailable = True
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
# Force conditions to simulate slowdown
controller._slow_down_filter = FakeKalman(value=1.0) # ty: ignore[invalid-assignment]
controller._v_ego_kph = 35.0
default_sm['modelV2'] = MockModelData(valid=False) # Incomplete trajectory
for _ in range(3):
controller.update(default_sm)
def test_single_frame_lead_veto_pulse_does_not_leave_blended(self):
controller = make_controller()
no_lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0))
lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
for _ in range(ENTER_FRAMES):
controller.update(no_lead_sm)
assert controller.mode() == "blended"
controller.update(lead_sm)
assert controller.mode() == "blended"
assert not controller.lead_veto
controller.update(no_lead_sm)
assert controller.mode() == "blended"
assert not controller.lead_veto
def test_three_frame_lead_veto_pulse_does_not_leave_blended(self):
controller = make_controller()
no_lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0))
lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
for _ in range(ENTER_FRAMES):
controller.update(no_lead_sm)
assert controller.mode() == "blended"
for _ in range(LEAD_VETO_CONFIRM_FRAMES - 1):
controller.update(lead_sm)
assert controller.mode() == "blended"
assert not controller.lead_veto
controller.update(no_lead_sm)
assert controller.mode() == "blended"
assert not controller.lead_veto
def test_persistent_lead_veto_forces_acc_after_confirmation(self):
controller = make_controller()
no_lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0))
lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
for _ in range(ENTER_FRAMES):
controller.update(no_lead_sm)
assert controller.mode() == "blended"
for _ in range(LEAD_VETO_CONFIRM_FRAMES - 1):
controller.update(lead_sm)
assert controller.mode() == "blended"
assert not controller.lead_veto
controller.update(lead_sm)
assert controller.mode() == "acc"
assert controller.lead_veto
controller.update(no_lead_sm)
assert controller.mode() == "blended"
assert not controller.lead_veto
def test_veto_releases_without_rebuild_lag(self):
controller = make_controller()
lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
for _ in range(30):
controller.update(lead_sm)
assert controller.mode() == "acc"
assert controller.lead_veto
no_lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0), lead_present=False)
for _ in range(ENTER_FRAMES + 2):
controller.update(no_lead_sm)
if controller.mode() == "blended":
break
assert controller.mode() == "blended"
def test_lead_gone_with_no_underlying_slowdown_stays_acc(self):
controller = make_controller()
lead_sm = make_sm(v_ego=20.0, velocity=flat_velocity(20.0), lead_present=True, lead_probs=[1.0, 1.0, 1.0])
for _ in range(30):
controller.update(lead_sm)
assert controller.mode() == "acc"
no_lead_sm = make_sm(v_ego=20.0, velocity=flat_velocity(20.0), lead_present=False)
for _ in range(20):
controller.update(no_lead_sm)
assert controller.mode() == "acc"
def test_creep_does_not_release_lead_veto(self):
controller = make_controller()
sm = make_sm(v_ego=1.0, velocity=flat_velocity(1.0), lead_present=True, lead_probs=[1.0, 1.0, 1.0])
for _ in range(10):
controller.update(sm)
assert controller.mode() == "acc"
assert controller.lead_veto
def test_lead_prevents_creep_only_blending_when_model_probability_drops(self):
controller = make_controller()
confirmed_sm = make_sm(v_ego=1.0, velocity=flat_velocity(1.0), lead_present=True, lead_probs=[1.0, 1.0, 1.0])
for _ in range(LEAD_VETO_CONFIRM_FRAMES):
controller.update(confirmed_sm)
assert controller.lead_veto
low_probability_sm = make_sm(v_ego=1.0, velocity=flat_velocity(1.0), lead_present=True, lead_probs=[1.0, 0.1, 0.1])
for _ in range(LEAD_VETO_CONFIRM_FRAMES - 1):
controller.update(low_probability_sm)
assert controller.lead_veto
assert controller.mode() == "acc"
for _ in range(20):
controller.update(low_probability_sm)
assert not controller.lead_veto
assert not controller.signals.creeping
assert controller.mode() == "acc"
no_lead_sm = make_sm(v_ego=1.0, velocity=flat_velocity(1.0), lead_present=False, lead_probs=[1.0, 0.1, 0.1])
for _ in range(ENTER_FRAMES):
controller.update(no_lead_sm)
assert controller.mode() == "blended"
def test_confirmed_lead_veto_ignores_short_future_probability_dropout(self):
controller = make_controller()
confirmed_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -1.0),
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
for _ in range(LEAD_VETO_CONFIRM_FRAMES):
controller.update(confirmed_sm)
assert controller.lead_veto
dropout_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -1.0),
lead_present=True, lead_probs=[1.0, 0.1, 0.1])
for _ in range(LEAD_VETO_CONFIRM_FRAMES - 1):
controller.update(dropout_sm)
assert controller.lead_veto
assert controller.mode() == "acc"
controller.update(confirmed_sm)
assert controller.lead_veto
assert controller.mode() == "acc"
def test_urgent_override_bypasses_confirmed_veto_release(self):
controller = make_controller()
confirmed_sm = make_sm(v_ego=20.0, velocity=flat_velocity(20.0),
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
for _ in range(LEAD_VETO_CONFIRM_FRAMES):
controller.update(confirmed_sm)
assert controller.lead_veto
hard_brake_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0), hard_brake=True,
lead_present=True, lead_probs=[1.0, 0.1, 0.1])
controller.update(hard_brake_sm)
assert not controller.lead_veto
assert controller.mode() == "blended"
def test_trusted_strong_stop_bypasses_confirmed_veto_release(self):
controller = make_controller()
confirmed_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
for _ in range(LEAD_VETO_CONFIRM_FRAMES):
controller.update(confirmed_sm)
assert controller.lead_veto
assert controller.mode() == "acc"
departing_lead_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
lead_present=True, lead_probs=[1.0, 0.1, 0.1])
controller.update(departing_lead_sm)
assert not controller.lead_veto
assert controller.mode() == "blended"
def test_degraded_strong_stop_does_not_bypass_confirmed_veto_release(self):
controller = make_controller()
confirmed_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0),
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
for _ in range(LEAD_VETO_CONFIRM_FRAMES):
controller.update(confirmed_sm)
assert controller.lead_veto
degraded_sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -2.0), lead_present=True,
lead_probs=[1.0, 0.1, 0.1], frame_drop_perc=60.0)
for _ in range(LEAD_VETO_CONFIRM_FRAMES - 1):
controller.update(degraded_sm)
assert controller.lead_veto
assert controller.mode() == "acc"
def test_model_slowdown_still_blends_while_creeping_with_a_lead(self):
controller = make_controller()
sm = make_sm(v_ego=1.0, velocity=decel_velocity(1.0, -1.0), lead_present=True, lead_probs=[1.0, 0.1, 0.1])
for _ in range(ENTER_FRAMES):
controller.update(sm)
assert not controller.lead_veto
assert controller.mode() == "blended"
def test_hard_brake_still_blends_while_creeping_with_a_lead(self):
controller = make_controller()
sm = make_sm(v_ego=1.0, velocity=flat_velocity(1.0), hard_brake=True,
lead_present=True, lead_probs=[1.0, 0.1, 0.1])
controller.update(sm)
assert not controller.lead_veto
assert controller.mode() == "blended"
def test_creep_hysteresis_band_without_lead(self):
controller = make_controller()
controller.update(make_sm(v_ego=1.5, velocity=flat_velocity(1.5)))
assert controller.signals.creeping
controller.update(make_sm(v_ego=2.5, velocity=flat_velocity(2.5)))
assert controller.signals.creeping, "a small excursion above CREEP_SPEED_ENTER should not exit creeping"
controller.update(make_sm(v_ego=5.0, velocity=flat_velocity(5.0)))
assert not controller.signals.creeping, "should exit creeping once genuinely above CREEP_SPEED_EXIT"
def test_crash_cnt_override_inert_while_lead_present(self):
mpc = MockMpc(crash_cnt=0)
controller = make_controller(mpc=mpc)
sm = make_sm(v_ego=20.0, velocity=flat_velocity(20.0), lead_present=True, lead_probs=[1.0, 1.0, 1.0])
for _ in range(30):
controller.update(sm)
assert controller.mode() == "acc"
mpc.crash_cnt = 1
controller.update(sm)
assert controller.mode() == "acc"
def test_crash_cnt_blends_within_one_frame_without_lead(self):
mpc = MockMpc(crash_cnt=1)
controller = make_controller(mpc=mpc)
sm = make_sm(v_ego=20.0, velocity=flat_velocity(20.0), lead_present=False)
controller.update(sm)
assert controller.mode() == "blended"
def test_hard_brake_predicted_blends_within_one_frame_without_lead(self):
controller = make_controller()
sm = make_sm(v_ego=20.0, velocity=flat_velocity(20.0), hard_brake=True, lead_present=False)
controller.update(sm)
assert controller.mode() == "blended"
def test_confirmed_lead_veto_suppresses_hard_brake_override(self):
controller = make_controller()
sm = make_sm(v_ego=20.0, velocity=flat_velocity(20.0), hard_brake=True,
lead_present=True, lead_probs=[1.0, 1.0, 1.0])
for _ in range(LEAD_VETO_CONFIRM_FRAMES - 1):
controller.update(sm)
assert controller.mode() == "blended"
assert not controller.lead_veto
controller.update(sm)
assert controller.mode() == "acc"
assert controller.lead_veto
def test_degraded_model_does_not_blend(self):
controller = make_controller()
sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -3.0), frame_drop_perc=60.0)
for _ in range(30):
controller.update(sm)
assert controller.mode() == "acc"
def test_short_plan_arrays_do_not_blend(self):
controller = make_controller()
sm = make_sm(v_ego=20.0, velocity=[20.0] * 5)
for _ in range(30):
controller.update(sm)
assert controller.mode() == "acc"
def test_disabled_param_holds_acc(self):
controller = make_controller(enabled=False)
sm = make_sm(v_ego=20.0, velocity=decel_velocity(20.0, -3.0))
for _ in range(30):
controller.update(sm)
assert controller.mode() == "acc"
class TestModeHysteresis(OpenpilotTestCase):
def test_entry_requires_enter_frames(self):
h = ModeHysteresis()
for _ in range(ENTER_FRAMES - 1):
assert h.update(want_blended=True, override=False, veto=False) == "acc"
assert h.update(want_blended=True, override=False, veto=False) == "blended"
def test_override_beats_veto(self):
h = ModeHysteresis()
assert h.update(want_blended=False, override=True, veto=True) == "blended"
def test_veto_forces_acc_even_when_reason_active(self):
h = ModeHysteresis()
for _ in range(ENTER_FRAMES + 5):
assert h.update(want_blended=True, override=False, veto=True) == "acc"
def test_counter_accumulates_under_veto_then_releases_instantly(self):
h = ModeHysteresis()
for _ in range(ENTER_FRAMES + 5):
h.update(want_blended=True, override=False, veto=True)
assert h.mode == "acc"
assert h.update(want_blended=True, override=False, veto=False) == "blended"
def test_exit_requires_min_dwell_and_sustained_absence(self):
h = ModeHysteresis()
for _ in range(ENTER_FRAMES):
h.update(want_blended=True, override=False, veto=False)
assert h.mode == "blended"
for _ in range(MIN_BLENDED_FRAMES - 1):
assert h.update(want_blended=False, override=False, veto=False) == "blended"
assert h.update(want_blended=False, override=False, veto=False) == "acc"
def test_no_flapping_on_alternating_reason(self):
h = ModeHysteresis()
changes = 0
prev = h.mode
for i in range(200):
mode = h.update(want_blended=i % 2 == 0, override=False, veto=False)
changes += mode != prev
prev = mode
assert changes == 0
class TestShouldBlend(OpenpilotTestCase):
def test_slowdown_detected_triggers(self):
assert should_blend(DecSignals(decel_intent=1.0))
assert not should_blend(DecSignals(decel_intent=0.0))
def test_curve_exclusion_suppresses_slowdown(self):
assert not should_blend(DecSignals(decel_intent=0.7, curve_detected=True))
def test_curve_exclusion_does_not_override_saturated_decel_intent(self):
assert should_blend(DecSignals(decel_intent=1.0, curve_detected=True))
def test_degraded_model_suppresses_model_based_reasons(self):
s = DecSignals(decel_intent=1.0, model_trust=0.0)
assert not should_blend(s)
def test_creep_bypasses_everything(self):
assert should_blend(DecSignals(model_trust=0.0, creeping=True))
@@ -5,13 +5,10 @@ This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import math
from openpilot.cereal import messaging, custom, log
from openpilot.cereal import messaging, custom
from opendbc.car import structs
from openpilot.common.constants import CV
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX
from openpilot.sunnypilot.selfdrive.controls.lib.accel_controller.accel_controller import AccelController
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController
from openpilot.sunnypilot.selfdrive.controls.lib.e2e_alerts_helper import E2EAlertsHelper
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.smart_cruise_control import SmartCruiseControl
@@ -22,16 +19,12 @@ from openpilot.sunnypilot.models.helpers import get_active_bundle
DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState
LongitudinalPlanSource = custom.LongitudinalPlanSP.LongitudinalPlanSource
MpcPlanSource = log.LongitudinalPlan.LongitudinalPlanSource
E2E_BRAKE_HOLD_ACCEL = -0.2 # m/s^2
class LongitudinalPlannerSP:
def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParamsSP, mpc):
self.accel_controller = AccelController()
self.accel_controller_active = False
self.events_sp = EventsSP()
self.resolver = SpeedLimitResolver()
self.dec = DynamicExperimentalController(CP, mpc)
self.scc = SmartCruiseControl()
self.resolver = SpeedLimitResolver()
@@ -45,50 +38,10 @@ class LongitudinalPlannerSP:
def is_e2e(self, sm: messaging.SubMaster) -> bool:
experimental_mode = sm['selfdriveState'].experimentalMode
if not experimental_mode:
return False
if not self.dec.active():
return experimental_mode
if not self.dec.active() or self.dec.mode() == "blended":
return True
# hold a brake the model already owns rather than release it mid-brake; min() means this can only ever
# add deceleration, and never one that was not already the selected source
if self.mpc.source == MpcPlanSource.e2e and sm['modelV2'].action.desiredAcceleration < E2E_BRAKE_HOLD_ACCEL:
return True
return False
def get_max_accel_override(self, v_ego: float) -> float | None:
if not self.accel_controller.is_enabled():
return None
return self.accel_controller.get_max_accel(v_ego)
def get_cruise_target_override(self, v_ego: float, v_target: float, force_decel: bool) -> float:
if not self.accel_controller.is_enabled() or force_decel or self.source != LongitudinalPlanSource.cruise:
return v_target
return self.accel_controller.get_cruise_target(v_ego, v_target)
def is_accel_controller_active(self, force_decel: bool) -> bool:
return bool(self.accel_controller.is_enabled() and not force_decel and
self.mpc.source == MpcPlanSource.cruise)
def _has_valid_selected_lead(self, sm: messaging.SubMaster, source: MpcPlanSource) -> bool:
radar_valid = sm.valid.get('radarState', False) and getattr(sm, 'alive', {}).get('radarState', False)
return radar_valid and ((source == MpcPlanSource.lead0 and sm['radarState'].leadOne.present) or
(source == MpcPlanSource.lead1 and sm['radarState'].leadTwo.present))
def arbitrate_cruise_candidate(self, sm: messaging.SubMaster, gated: float, ungated: float,
mpc_accel: float, mpc_source: MpcPlanSource, *, allow_throttle: bool,
e2e: bool, force_decel: bool) -> float:
finite = all(math.isfinite(value) for value in (gated, ungated, mpc_accel))
coast_gate_changed_source = gated < mpc_accel <= ungated
if (finite and not allow_throttle and not e2e and not force_decel
and self._has_valid_selected_lead(sm, mpc_source) and coast_gate_changed_source):
return ungated
return gated
return experimental_mode and self.dec.mode() == "blended"
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
CS = sm['carState']
@@ -121,12 +74,9 @@ class LongitudinalPlannerSP:
return self.output_v_target, self.output_a_target
def update(self, sm: messaging.SubMaster) -> None:
self.accel_controller.update()
self.events_sp.clear()
self.e2e_alerts_helper.update(sm, self.events_sp)
def update_dec(self, sm: messaging.SubMaster) -> None:
self.dec.update(sm)
self.e2e_alerts_helper.update(sm, self.events_sp)
def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None:
plan_sp_send = messaging.new_message('longitudinalPlanSP')
@@ -144,15 +94,6 @@ class LongitudinalPlannerSP:
dec.state = DecState.blended if self.dec.mode() == 'blended' else DecState.acc
dec.enabled = self.dec.enabled()
dec.active = self.dec.active()
dec.decelIntent = float(self.dec.signals.decel_intent)
dec.curveDetected = bool(self.dec.signals.curve_detected)
dec.wantBlended = bool(self.dec.want_blended)
dec.leadVeto = bool(self.dec.lead_veto)
accel_controller = longitudinalPlanSP.accelController
accel_controller.enabled = bool(self.accel_controller.is_enabled())
accel_controller.active = bool(self.accel_controller_active)
accel_controller.profile = int(self.accel_controller.profile)
# Smart Cruise Control
smartCruiseControl = longitudinalPlanSP.smartCruiseControl
@@ -4,8 +4,6 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from types import SimpleNamespace
from typing import Any
import numpy as np
@@ -17,23 +15,8 @@ from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlannerSP, LongitudinalPlanSource
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control import MIN_V
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import (
_A_LAT_REG_MAX,
_BELOW_EGO_TARGET_RELEASE_RATE,
_ENTERING_PRED_LAT_ACC_TH,
_MIN_ACTIVATION_SPEED,
_RELIEF_CONFIRMATION_FRAMES,
_TARGET_RELEASE_CONFIRMATION_FRAMES,
_TARGET_RELEASE_RATE,
_TARGET_TIGHTEN_CONFIRMATION_FRAMES,
_TARGET_TIGHTEN_RATE,
_TURNING_LAT_ACC_TH,
_URGENT_PRED_LAT_ACC_TH,
SmartCruiseControlVision,
)
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision, _ENTERING_PRED_LAT_ACC_TH
from openpilot.common.test import OpenpilotTestCase
VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.VisionState
@@ -124,6 +107,7 @@ def generate_controlsState():
class TestSmartCruiseControlVision(OpenpilotTestCase):
def setup_method(self):
self.params = Params()
self.reset_params()
@@ -137,377 +121,36 @@ class TestSmartCruiseControlVision(OpenpilotTestCase):
def reset_params(self):
self.params.put_bool("SmartCruiseControlVision", True, block=True)
def assert_approx(self, actual, expected):
self.assertAlmostEqual(actual, expected, delta=max(1e-12, abs(expected) * 1e-6))
def set_lat_accels(self, current: float, predicted: float, v_ego: float = 20.0, model_speed: float = 20.0) -> None:
self.sm['controlsState'].curvature = current / v_ego**2
self.sm['modelV2'].velocity.x = [model_speed] * len(ModelConstants.T_IDXS)
self.sm['modelV2'].orientationRate.z = [predicted / model_speed] * len(ModelConstants.T_IDXS)
def update_lat_accels(
self, current: float, predicted: float, cruise: float = 30.0, a_ego: float = 0.0, v_ego: float = 20.0, model_speed: float = 20.0
) -> None:
self.set_lat_accels(current, predicted, v_ego, model_speed)
self.scc_v.update(self.sm, True, False, v_ego, a_ego, cruise)
def enter_curve(self, predicted: float = 2.2) -> None:
self.update_lat_accels(0.5, predicted)
self.update_lat_accels(0.5, predicted)
assert self.scc_v.state == VisionState.entering
def test_initial_state(self):
assert self.scc_v.state == VisionState.disabled
assert not self.scc_v.is_active
assert self.scc_v.output_v_target == V_CRUISE_UNSET
assert self.scc_v.output_a_target == 0.0
assert self.scc_v.output_a_target == 0.
def test_system_disabled(self):
self.params.put_bool("SmartCruiseControlVision", False, block=True)
self.scc_v.enabled = self.params.get_bool("SmartCruiseControlVision")
for _ in range(int(10.0 / DT_MDL)):
self.scc_v.update(self.sm, True, False, 0.0, 0.0, 0.0)
for _ in range(int(10. / DT_MDL)):
self.scc_v.update(self.sm, True, False, 0., 0., 0.)
assert self.scc_v.state == VisionState.disabled
assert not self.scc_v.is_active
def test_disabled(self):
for _ in range(int(10.0 / DT_MDL)):
self.scc_v.update(self.sm, False, False, 0.0, 0.0, 0.0)
for _ in range(int(10. / DT_MDL)):
self.scc_v.update(self.sm, False, False, 0., 0., 0.)
assert self.scc_v.state == VisionState.disabled
def test_transition_disabled_to_enabled(self):
for _ in range(int(10.0 / DT_MDL)):
self.scc_v.update(self.sm, True, False, 0.0, 0.0, 0.0)
for _ in range(int(10. / DT_MDL)):
self.scc_v.update(self.sm, True, False, 0., 0., 0.)
assert self.scc_v.state == VisionState.enabled
def test_unconfirmed_release_holds_but_urgent_reentry_tightens(self):
self.enter_curve()
targets = [self.scc_v.output_v_target]
self.update_lat_accels(2.0, 2.2, a_ego=-0.8)
assert self.scc_v.state == VisionState.turning
assert self.scc_v.output_a_target == -0.8
turning_demand = self.scc_v._v_demand()
targets.append(self.scc_v.output_v_target)
self.update_lat_accels(1.2, 1.2, a_ego=0.3)
assert self.scc_v.state == VisionState.leaving
assert self.scc_v.output_a_target == 0.3
targets.append(self.scc_v.output_v_target)
self.update_lat_accels(1.0, 3.0, a_ego=-1.2)
assert self.scc_v.state == VisionState.entering
assert self.scc_v.output_a_target == -1.2
reentry_demand = self.scc_v._v_demand()
targets.append(self.scc_v.output_v_target)
entering, turning, leaving, reentering = targets
assert turning < entering
self.assert_approx(turning, turning_demand)
self.assert_approx(leaving, turning)
assert reentering < leaving
self.assert_approx(reentering, reentry_demand)
def test_new_curve_interrupts_confirmed_release_immediately(self):
self.enter_curve()
for _ in range(_RELIEF_CONFIRMATION_FRAMES + 1):
self.update_lat_accels(0.8, 0.8)
releasing_v_target = self.scc_v.output_v_target
assert self.scc_v.state == VisionState.leaving
self.update_lat_accels(0.8, 3.0, a_ego=-0.7)
assert self.scc_v.state == VisionState.entering
assert self.scc_v.output_v_target < releasing_v_target
assert self.scc_v.output_a_target == -0.7
@parameterized.expand([(-2.0,), (-0.5,), (0.0,), (0.8,)])
def test_planner_acceleration_passes_through_exactly(self, planner_accel):
self.enter_curve()
self.update_lat_accels(0.5, 2.2, a_ego=planner_accel)
assert self.scc_v.output_a_target == planner_accel
def test_planner_acceleration_passes_through_all_states(self):
cases = (
(False, False, 0.5, 2.2, -0.2, VisionState.disabled),
(True, False, 0.5, 0.8, 0.1, VisionState.enabled),
(True, False, 0.5, 2.2, -0.4, VisionState.entering),
(True, False, 2.0, 2.2, -0.8, VisionState.turning),
(True, False, 1.2, 1.2, 0.3, VisionState.leaving),
(True, True, 1.2, 1.2, 0.6, VisionState.overriding),
)
for long_enabled, override, current, predicted, planner_accel, state in cases:
self.set_lat_accels(current, predicted)
self.scc_v.update(self.sm, long_enabled, override, 20.0, planner_accel, 30.0)
assert self.scc_v.state == state
assert self.scc_v.output_a_target == planner_accel
def test_jitter_requires_confirmed_relief_then_releases_smoothly(self):
self.enter_curve()
previous_v_target = self.scc_v.output_v_target
for frame in range(_RELIEF_CONFIRMATION_FRAMES * 2):
self.update_lat_accels(1.0, 1.05 if frame % 2 == 0 else 1.15)
assert self.scc_v.state == VisionState.entering
assert self.scc_v.output_v_target >= previous_v_target
assert self.scc_v.output_v_target - previous_v_target <= _BELOW_EGO_TARGET_RELEASE_RATE * DT_MDL + 1e-9
previous_v_target = self.scc_v.output_v_target
for _ in range(_RELIEF_CONFIRMATION_FRAMES):
self.update_lat_accels(1.15, 0.8)
assert self.scc_v.state == VisionState.entering
assert 0.0 <= self.scc_v.output_v_target - previous_v_target <= _BELOW_EGO_TARGET_RELEASE_RATE * DT_MDL + 1e-9
previous_v_target = self.scc_v.output_v_target
release_cruise = 30.0
for _ in range(_RELIEF_CONFIRMATION_FRAMES - 1):
self.update_lat_accels(0.8, 0.8, release_cruise)
assert self.scc_v.state == VisionState.entering
assert 0.0 <= self.scc_v.output_v_target - previous_v_target <= _BELOW_EGO_TARGET_RELEASE_RATE * DT_MDL + 1e-9
previous_v_target = self.scc_v.output_v_target
active_v_targets = [previous_v_target]
for _ in range(int((release_cruise - previous_v_target) / (_TARGET_RELEASE_RATE * DT_MDL)) + 10):
self.update_lat_accels(0.8, 0.8, release_cruise)
if not self.scc_v.is_active:
break
assert self.scc_v.state == VisionState.leaving
assert self.scc_v.output_v_target != V_CRUISE_UNSET
active_v_targets.append(self.scc_v.output_v_target)
assert self.scc_v.state == VisionState.enabled
assert self.scc_v.output_v_target == V_CRUISE_UNSET
self.assert_approx(active_v_targets[-1], release_cruise)
assert np.all((np.diff(active_v_targets) >= 0.0) & (np.diff(active_v_targets) <= _BELOW_EGO_TARGET_RELEASE_RATE * DT_MDL + 1e-9))
def test_target_release_waits_for_relief_above_ego_speed(self):
self.enter_curve()
held_v_target = self.scc_v.output_v_target
self.assert_approx(held_v_target, self.scc_v.v_ego)
for _ in range(_RELIEF_CONFIRMATION_FRAMES + _TARGET_RELEASE_CONFIRMATION_FRAMES - 2):
self.update_lat_accels(0.8, 0.8)
self.assert_approx(self.scc_v.output_v_target, held_v_target)
self.update_lat_accels(0.8, 0.8)
rise = self.scc_v.output_v_target - held_v_target
assert 0.0 < rise <= _TARGET_RELEASE_RATE * DT_MDL + 1e-9
def test_curve_target_is_independent_of_ego_speed(self):
model_speed = 24.0
predicted_yaw_rate = 0.12
predicted_lat_accel = model_speed * predicted_yaw_rate
expected_v_target = (_A_LAT_REG_MAX / (predicted_yaw_rate / model_speed)) ** 0.5
targets = []
for v_ego in (18.0, 28.0):
controller = SmartCruiseControlVision()
self.set_lat_accels(0.5, predicted_lat_accel, v_ego, model_speed)
controller.update(self.sm, True, False, v_ego, 0.0, 30.0)
controller.update(self.sm, True, False, v_ego, 0.0, 30.0)
assert controller.state == VisionState.entering
targets.append(controller.v_target)
self.assert_approx(targets[0], expected_v_target)
self.assert_approx(targets[1], expected_v_target)
def test_curve_target_respects_minimum_speed_floor(self):
model_speed = 10.0
predicted_yaw_rate = 2.0
self.set_lat_accels(0.5, model_speed * predicted_yaw_rate, model_speed=model_speed)
self.scc_v.update(self.sm, True, False, 20.0, 0.0, 30.0)
self.scc_v.update(self.sm, True, False, 20.0, 0.0, 30.0)
assert self.scc_v.state == VisionState.entering
assert self.scc_v.v_target < MIN_V
self.assert_approx(self.scc_v.output_v_target, MIN_V)
@parameterized.expand(
[([], []), ([np.nan] * len(ModelConstants.T_IDXS), [np.nan] * len(ModelConstants.T_IDXS)), ([20.0] * 5, [0.1] * 3)],
names=["velocities", "yaw_rates"],
)
def test_model_vector_edges_remain_finite(self, velocities, yaw_rates):
self.sm['modelV2'].velocity.x = velocities
self.sm['modelV2'].orientationRate.z = yaw_rates
self.scc_v.update(self.sm, True, False, 20.0, 0.0, 30.0)
self.scc_v.update(self.sm, True, False, 20.0, 0.0, 30.0)
assert all(
np.isfinite(value)
for value in (
self.scc_v.current_lat_acc,
self.scc_v.max_pred_lat_acc,
self.scc_v.v_target,
self.scc_v.output_v_target,
self.scc_v.output_a_target,
)
)
@parameterized.expand([(5.75,), (9.9,), (_MIN_ACTIVATION_SPEED,)])
def test_vision_control_does_not_steal_launch(self, launch_speed):
self.set_lat_accels(0.5, 3.0, launch_speed)
self.scc_v.update(self.sm, True, False, launch_speed, 0.0, 30.0)
self.scc_v.update(self.sm, True, False, launch_speed, 0.0, 30.0)
assert launch_speed <= _MIN_ACTIVATION_SPEED
assert self.scc_v.state == VisionState.enabled
assert not self.scc_v.is_active
assert self.scc_v.output_v_target == V_CRUISE_UNSET
def test_vision_control_can_activate_above_launch_range(self):
speed = _MIN_ACTIVATION_SPEED + 0.01
self.set_lat_accels(0.5, 3.0, speed)
self.scc_v.update(self.sm, True, False, speed, 0.0, 30.0)
self.scc_v.update(self.sm, True, False, speed, 0.0, 30.0)
assert self.scc_v.state == VisionState.entering
assert self.scc_v.is_active
def test_nonurgent_activation_has_no_target_cliff(self):
v_ego = _MIN_ACTIVATION_SPEED + 0.01
model_speed = 8.0
self.update_lat_accels(0.5, 2.0, v_ego=v_ego, model_speed=model_speed)
self.update_lat_accels(0.5, 2.0, v_ego=v_ego, model_speed=model_speed)
self.assert_approx(self.scc_v.v_target, 8.0)
self.assert_approx(self.scc_v.output_v_target, v_ego)
def test_nonurgent_tightening_is_confirmed_and_rate_limited(self):
self.enter_curve()
initial_v_target = self.scc_v.output_v_target
for _ in range(_TARGET_TIGHTEN_CONFIRMATION_FRAMES - 1):
self.update_lat_accels(0.5, 2.8)
self.assert_approx(self.scc_v.output_v_target, initial_v_target)
self.update_lat_accels(0.5, 2.8)
drop = initial_v_target - self.scc_v.output_v_target
assert 0.0 < drop <= _TARGET_TIGHTEN_RATE * DT_MDL + 1e-9
def test_one_frame_curve_prediction_does_not_pulse_target(self):
self.enter_curve()
for _ in range(10):
self.update_lat_accels(0.5, 2.2)
stable_v_target = self.scc_v.output_v_target
self.update_lat_accels(0.5, 2.8)
self.assert_approx(self.scc_v.output_v_target, stable_v_target)
self.update_lat_accels(0.5, 2.2)
self.assert_approx(self.scc_v.output_v_target, stable_v_target)
def test_one_frame_release_does_not_reverse_target(self):
self.enter_curve(_URGENT_PRED_LAT_ACC_TH)
stable_v_target = self.scc_v.output_v_target
self.update_lat_accels(0.5, 2.2)
self.assert_approx(self.scc_v.output_v_target, stable_v_target)
self.update_lat_accels(0.5, _URGENT_PRED_LAT_ACC_TH)
self.assert_approx(self.scc_v.output_v_target, stable_v_target)
def test_urgent_predicted_curve_is_not_delayed(self):
self.enter_curve()
self.update_lat_accels(0.5, _URGENT_PRED_LAT_ACC_TH)
self.assert_approx(self.scc_v.output_v_target, self.scc_v._v_demand())
def test_current_curve_is_not_delayed(self):
self.enter_curve()
self.update_lat_accels(_TURNING_LAT_ACC_TH, 2.8)
self.assert_approx(self.scc_v.output_v_target, self.scc_v._v_demand())
def test_sequential_curve_confirms_release_and_tightens_urgently(self):
self.enter_curve(3.0)
for _ in range(20):
self.update_lat_accels(0.5, 3.0)
restrictive_v_target = self.scc_v.output_v_target
self.update_lat_accels(0.5, 1.4, a_ego=0.4)
assert self.scc_v.state == VisionState.entering
self.assert_approx(self.scc_v.output_v_target, restrictive_v_target)
assert self.scc_v.output_a_target == 0.4
for _ in range(_TARGET_RELEASE_CONFIRMATION_FRAMES - 2):
self.update_lat_accels(0.5, 1.4)
self.assert_approx(self.scc_v.output_v_target, restrictive_v_target)
self.update_lat_accels(0.5, 1.4)
released_v_target = self.scc_v.output_v_target
assert 0.0 < released_v_target - restrictive_v_target <= _BELOW_EGO_TARGET_RELEASE_RATE * DT_MDL + 1e-9
self.update_lat_accels(0.5, 3.0, a_ego=-0.6)
assert self.scc_v.state == VisionState.entering
self.assert_approx(self.scc_v.output_v_target, restrictive_v_target)
assert self.scc_v.output_a_target == -0.6
for _ in range(4):
self.update_lat_accels(0.5, 1.4)
self.assert_approx(self.scc_v.output_v_target, restrictive_v_target)
self.update_lat_accels(0.5, 3.0)
self.assert_approx(self.scc_v.output_v_target, restrictive_v_target)
def test_acceleration_is_continuous_through_planner_arbitration(self):
car_control = messaging.new_message('carControl')
car_control.carControl.enabled = True
car_control.carControl.cruiseControl.override = False
self.sm['carControl'] = car_control.carControl
self.sm['carState'].vCruiseCluster = 108.0
planner: Any = LongitudinalPlannerSP.__new__(LongitudinalPlannerSP)
planner.scc = SimpleNamespace(
vision=self.scc_v,
map=SimpleNamespace(output_v_target=V_CRUISE_UNSET, output_a_target=0.0),
update=lambda sm, enabled, override, v_ego, a_ego, v_cruise: self.scc_v.update(sm, enabled, override, v_ego, a_ego, v_cruise),
)
planner.resolver = SimpleNamespace(
speed_limit_valid=False,
speed_limit_last_valid=False,
speed_limit=0.0,
speed_limit_final_last=0.0,
distance=0.0,
update=lambda _v_ego, _sm: None,
)
planner.sla = SimpleNamespace(
output_v_target=V_CRUISE_UNSET,
output_a_target=0.0,
update=lambda *_args: None,
)
planner.events_sp = SimpleNamespace()
self.set_lat_accels(0.5, 2.2)
planner.update_targets(self.sm, 20.0, -0.8, 30.0)
planner.update_targets(self.sm, 20.0, -0.8, 30.0)
assert planner.source == LongitudinalPlanSource.sccVision
assert planner.output_a_target == -0.8
for planner_accel in (-2.0, 0.5, -0.2):
planner.update_targets(self.sm, 20.0, planner_accel, 30.0)
assert planner.source == LongitudinalPlanSource.sccVision
assert planner.output_a_target == planner_accel
self.set_lat_accels(0.8, 0.8)
for _ in range(int(30.0 / (_TARGET_RELEASE_RATE * DT_MDL)) + 10):
planner.update_targets(self.sm, 20.0, 0.4, 30.0)
assert planner.output_a_target == 0.4
if planner.source == LongitudinalPlanSource.cruise:
break
else:
self.fail("SCC Vision did not release to cruise")
planner.update_targets(self.sm, 20.0, 0.4, 30.0)
assert self.scc_v.state == VisionState.enabled
assert planner.source == LongitudinalPlanSource.cruise
@parameterized.expand(
[
@parameterized.expand([
("p97_just_above_threshold", True),
("single_spike_filtered", False),
("persistent_high_values", True),
],
names=["case", "should_enter"],
)
], names=["case", "should_enter"])
def test_max_pred_lat_acc_uses_p97_and_threshold(self, case, should_enter):
n = len(ModelConstants.T_IDXS)
th = float(_ENTERING_PRED_LAT_ACC_TH)
@@ -1,110 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import gc
from contextlib import ExitStack
from unittest import mock
import numpy as np
from openpilot.common.test import OpenpilotTestCase
from openpilot.selfdrive.test.longitudinal_maneuvers.plant import Plant
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanSource
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import _A_LAT_REG_MAX
def _run_constant_curve(*, scc_enabled: bool, cruise: float, duration: float = 70.0) -> dict[str, np.ndarray]:
gc.collect()
curvature = 0.005
plant = Plant(lead_relevancy=False, speed=30.0)
planner = plant.planner
planner.dec._enabled = False
planner.scc.map.enabled = False
planner.scc.vision.enabled = scc_enabled
solver_failures = 0
with ExitStack() as patches:
patches.enter_context(mock.patch.object(planner.dec, "_read_params", return_value=None))
patches.enter_context(mock.patch.object(planner.scc.map, "update_params", return_value=None))
patches.enter_context(mock.patch.object(planner.scc.vision, "_update_params", return_value=None))
original_mpc_reset = planner.mpc.reset
def record_mpc_reset(*args, **kwargs):
nonlocal solver_failures
solver_failures += int(planner.mpc.solution_status != 0)
return original_mpc_reset(*args, **kwargs)
patches.enter_context(mock.patch.object(planner.mpc, "reset", side_effect=record_mpc_reset))
if scc_enabled:
original_update_calculations = planner.scc.vision._update_calculations
def inject_constant_curvature(sm):
velocities = np.asarray(sm['modelV2'].velocity.x, dtype=float)
sm['modelV2'].orientationRate.z = (curvature * velocities).tolist()
sm['controlsState'].curvature = curvature
original_update_calculations(sm)
patches.enter_context(mock.patch.object(planner.scc.vision, "_update_calculations", side_effect=inject_constant_curvature))
original_update = planner.update
def enable_longitudinal(sm):
sm['carControl'].enabled = True
sm['carControl'].longActive = True
original_update(sm)
patches.enter_context(mock.patch.object(planner, "update", side_effect=enable_longitudinal))
rows = []
while plant.current_time < duration:
output = plant.step(v_cruise=cruise)
rows.append(
(
plant.current_time,
output['speed'],
output['should_stop'],
planner.scc.vision.is_active,
planner.source == LongitudinalPlanSource.sccVision,
planner.scc.vision.output_v_target,
)
)
data = np.asarray(rows, dtype=float)
gc.collect()
return {
'time': data[:, 0],
'speed': data[:, 1],
'should_stop': data[:, 2],
'active': data[:, 3],
'scc_source': data[:, 4],
'target': data[:, 5],
'solver_failures': np.asarray(solver_failures),
}
class TestVisionControllerClosedLoop(OpenpilotTestCase):
def test_constant_curve_recovers_like_stock_speed_cap(self):
target = (_A_LAT_REG_MAX / 0.005) ** 0.5
scc = _run_constant_curve(scc_enabled=True, cruise=30.0)
stock = _run_constant_curve(scc_enabled=False, cruise=target)
scc_final = scc['speed'][scc['time'] >= 60.0]
stock_final = stock['speed'][stock['time'] >= 60.0]
# The generated solver can report platform-specific failures for the
# synthetic no-lead plant. The feature must not make that stock baseline
# worse; requiring an absolute zero would hide a harness difference as a
# controller regression.
assert scc['solver_failures'] <= stock['solver_failures']
assert not scc['should_stop'].any()
assert np.all(scc['active'][scc['time'] >= 60.0])
assert np.all(scc['scc_source'][scc['time'] >= 60.0])
assert np.allclose(scc['target'][scc['time'] >= 60.0], target)
assert scc_final.min() >= target - 1.0
assert abs(scc_final.mean() - stock_final.mean()) < 0.5
assert abs(scc_final.min() - stock_final.min()) < 1.0
assert abs(scc_final.max() - stock_final.max()) < 1.0
@@ -23,21 +23,25 @@ _ENTERING_PRED_LAT_ACC_TH = 1.3 # Predicted Lat Acc threshold to trigger enteri
_ABORT_ENTERING_PRED_LAT_ACC_TH = 1.1 # Predicted Lat Acc threshold to abort entering state if speed drops.
_TURNING_LAT_ACC_TH = 1.6 # Lat Acc threshold to trigger turning state.
_URGENT_PRED_LAT_ACC_TH = 3. # Predicted Lat Acc threshold that requires an immediate speed reduction.
_LEAVING_LAT_ACC_TH = 1.3 # Lat Acc threshold to trigger leaving turn state.
_FINISH_LAT_ACC_TH = 1.1 # Lat Acc threshold to trigger the end of the turn cycle.
_A_LAT_REG_MAX = 2. # Maximum lateral acceleration
_RELIEF_CONFIRMATION_FRAMES = max(1, int(round(0.5 / DT_MDL)))
_TARGET_TIGHTEN_CONFIRMATION_FRAMES = max(1, int(round(0.1 / DT_MDL)))
_TARGET_RELEASE_CONFIRMATION_FRAMES = max(1, int(round(0.15 / DT_MDL)))
_TARGET_TIGHTEN_RATE = 5. # m/s^2
_TARGET_RELEASE_RATE = 1. # m/s^2
_BELOW_EGO_TARGET_RELEASE_RATE = 3. # m/s^2
_MIN_PRED_SPEED = 1. # m/s
_MIN_ACTIVATION_SPEED = 10. # m/s
_NO_OVERSHOOT_TIME_HORIZON = 4. # s. Time to use for velocity desired based on a_target when not overshooting.
# Lookup table for the minimum smooth deceleration during the ENTERING state
# depending on the actual maximum absolute lateral acceleration predicted on the turn ahead.
_ENTERING_SMOOTH_DECEL_V = [-0.2, -1.] # min decel value allowed on ENTERING state
_ENTERING_SMOOTH_DECEL_BP = [1.3, 3.] # absolute value of lat acc ahead
# Lookup table for the acceleration for the TURNING state
# depending on the current lateral acceleration of the vehicle.
_TURNING_ACC_V = [0.5, 0., -0.4] # acc value
_TURNING_ACC_BP = [1.5, 2.3, 3.] # absolute value of current lat acc
_LEAVING_ACC = 0.5 # Conformable acceleration to regain speed while leaving a turn.
class SmartCruiseControlVision:
@@ -61,62 +65,14 @@ class SmartCruiseControlVision:
self.state = VisionState.disabled
self.current_lat_acc = 0.
self.max_pred_lat_acc = 0.
self.relief_frames = 0
self.tighten_frames = 0
self.release_frames = 0
def _v_demand(self) -> float:
return max(MIN_V, min(self.v_target, self.v_cruise_setpoint))
def _curve_is_urgent(self) -> bool:
return self.current_lat_acc >= _TURNING_LAT_ACC_TH or self.max_pred_lat_acc >= _URGENT_PRED_LAT_ACC_TH
def _filtered_v_target(self) -> float:
demand = self._v_demand()
if self.output_v_target == V_CRUISE_UNSET:
self.tighten_frames = 0
self.release_frames = 0
if self._curve_is_urgent():
return demand
return max(demand, min(self.v_ego, self.v_cruise_setpoint))
if demand < self.output_v_target:
self.release_frames = 0
if self._curve_is_urgent():
self.tighten_frames = 0
return demand
self.tighten_frames += 1
if self.tighten_frames < _TARGET_TIGHTEN_CONFIRMATION_FRAMES:
return self.output_v_target
return max(demand, self.output_v_target - _TARGET_TIGHTEN_RATE * DT_MDL)
self.tighten_frames = 0
releasing_brake = self.output_v_target < min(self.v_ego, demand)
if not releasing_brake and self.relief_frames < _RELIEF_CONFIRMATION_FRAMES:
self.release_frames = 0
return self.output_v_target
if demand > self.output_v_target:
self.release_frames += 1
if self.release_frames < _TARGET_RELEASE_CONFIRMATION_FRAMES:
return self.output_v_target
else:
self.release_frames = 0
release_rate = _BELOW_EGO_TARGET_RELEASE_RATE if releasing_brake else _TARGET_RELEASE_RATE
return min(demand, self.output_v_target + release_rate * DT_MDL)
def get_a_target_from_control(self) -> float:
return self.a_ego
return self.a_target
def get_v_target_from_control(self) -> float:
if self.is_active:
return self._filtered_v_target()
return max(self.v_target, MIN_V) + self.a_target * _NO_OVERSHOOT_TIME_HORIZON
self.tighten_frames = 0
self.release_frames = 0
return V_CRUISE_UNSET
def _update_params(self) -> None:
@@ -126,27 +82,25 @@ class SmartCruiseControlVision:
def _update_calculations(self, sm: messaging.SubMaster) -> None:
if not self.long_enabled:
return
else:
rate_plan = np.array(np.abs(sm['modelV2'].orientationRate.z))
vel_plan = np.array(sm['modelV2'].velocity.x)
rate_plan = np.asarray(np.abs(sm['modelV2'].orientationRate.z), dtype=float)
vel_plan = np.asarray(sm['modelV2'].velocity.x, dtype=float)
size = min(len(rate_plan), len(vel_plan))
rate_plan, vel_plan = rate_plan[:size], vel_plan[:size]
valid = np.isfinite(rate_plan) & np.isfinite(vel_plan) & (vel_plan >= _MIN_PRED_SPEED)
self.current_lat_acc = self.v_ego ** 2 * abs(sm['controlsState'].curvature)
self.current_lat_acc = self.v_ego ** 2 * abs(sm['controlsState'].curvature)
self.max_pred_lat_acc = 0.
self.v_target = V_CRUISE_UNSET
if np.any(valid):
self.max_pred_lat_acc = float(np.percentile(rate_plan[valid] * vel_plan[valid], 97))
max_pred_curvature = float(np.percentile(rate_plan[valid] / vel_plan[valid], 97))
if max_pred_curvature > 0.:
self.v_target = min(float((_A_LAT_REG_MAX / max_pred_curvature) ** 0.5), V_CRUISE_UNSET)
# get the maximum lat accel from the model
predicted_lat_accels = rate_plan * vel_plan
self.max_pred_lat_acc = np.percentile(predicted_lat_accels, 97)
# get the maximum curve based on the current velocity
v_ego = max(self.v_ego, 0.1) # ensure a value greater than 0 for calculations
max_curve = self.max_pred_lat_acc / (v_ego**2)
# Get the target velocity for the maximum curve
self.v_target = (_A_LAT_REG_MAX / max_curve) ** 0.5
def _update_state_machine(self) -> tuple[bool, bool]:
# ENABLED, ENTERING, TURNING, LEAVING, OVERRIDING
relief = self.current_lat_acc < _FINISH_LAT_ACC_TH and self.max_pred_lat_acc < _ABORT_ENTERING_PRED_LAT_ACC_TH
self.relief_frames = self.relief_frames + 1 if self.state in ACTIVE_STATES and relief else 0
if self.state != VisionState.disabled:
# longitudinal and feature disable always have priority in a non-disabled state
if not self.long_enabled or not self.enabled:
@@ -158,7 +112,7 @@ class SmartCruiseControlVision:
# ENABLED
if self.state == VisionState.enabled:
# Do not enter a turn control cycle if the speed is low.
if self.v_ego <= _MIN_ACTIVATION_SPEED:
if self.v_ego <= MIN_V:
pass
# If significant lateral acceleration is predicted ahead, then move to Entering turn state.
elif self.max_pred_lat_acc >= _ENTERING_PRED_LAT_ACC_TH:
@@ -174,26 +128,23 @@ class SmartCruiseControlVision:
# Transition to Turning if current lateral acceleration is over the threshold.
if self.current_lat_acc >= _TURNING_LAT_ACC_TH:
self.state = VisionState.turning
# Begin releasing only after both current and predicted lateral acceleration stay clear.
elif self.relief_frames >= _RELIEF_CONFIRMATION_FRAMES:
self.state = VisionState.leaving
# Abort if the predicted lateral acceleration drops
elif self.max_pred_lat_acc < _ABORT_ENTERING_PRED_LAT_ACC_TH:
self.state = VisionState.enabled
# TURNING
elif self.state == VisionState.turning:
# Transition out of Turning if current lateral acceleration drops below a threshold.
# Transition to Leaving if current lateral acceleration drops below a threshold.
if self.current_lat_acc <= _LEAVING_LAT_ACC_TH:
self.state = VisionState.entering if self.max_pred_lat_acc >= _ENTERING_PRED_LAT_ACC_TH else VisionState.leaving
self.state = VisionState.leaving
# LEAVING
elif self.state == VisionState.leaving:
# Transition back to Turning if current lateral acceleration goes back over the threshold.
if self.current_lat_acc >= _TURNING_LAT_ACC_TH:
self.state = VisionState.turning
# Start a new turn cycle immediately if another curve is predicted.
elif self.max_pred_lat_acc >= _ENTERING_PRED_LAT_ACC_TH:
self.state = VisionState.entering
# Finish after confirmed relief and a gradual release to the cruise setpoint.
elif self.relief_frames >= _RELIEF_CONFIRMATION_FRAMES and self.output_v_target >= self.v_cruise_setpoint:
# Finish if current lateral acceleration goes below a threshold.
elif self.current_lat_acc < _FINISH_LAT_ACC_TH:
self.state = VisionState.enabled
# DISABLED
@@ -206,11 +157,32 @@ class SmartCruiseControlVision:
enabled = self.state in ENABLED_STATES
active = self.state in ACTIVE_STATES
if not active:
self.relief_frames = 0
return enabled, active
def _update_solution(self) -> float:
# DISABLED, ENABLED, OVERRIDING
if self.state not in ACTIVE_STATES:
# when not overshooting, calculate v_turn as the speed at the prediction horizon when following
# the smooth deceleration.
a_target = self.a_ego
# ENTERING
elif self.state == VisionState.entering:
# when not overshooting, target a smooth deceleration in preparation for a sharp turn to come.
a_target = np.interp(self.max_pred_lat_acc, _ENTERING_SMOOTH_DECEL_BP, _ENTERING_SMOOTH_DECEL_V)
# TURNING
elif self.state == VisionState.turning:
# When turning, we provide a target acceleration that is comfortable for the lateral acceleration felt.
a_target = np.interp(self.current_lat_acc, _TURNING_ACC_BP, _TURNING_ACC_V)
# LEAVING
elif self.state == VisionState.leaving:
# When leaving, we provide a comfortable acceleration to regain speed.
a_target = _LEAVING_ACC
else:
raise NotImplementedError(f"SCC-V state not supported: {self.state}")
return a_target
def update(self, sm: messaging.SubMaster, long_enabled: bool, long_override: bool, v_ego: float, a_ego: float,
v_cruise_setpoint: float) -> None:
self.long_enabled = long_enabled
@@ -223,7 +195,7 @@ class SmartCruiseControlVision:
self._update_calculations(sm)
self.is_enabled, self.is_active = self._update_state_machine()
self.a_target = self.a_ego
self.a_target = self._update_solution()
self.output_v_target = self.get_v_target_from_control()
self.output_a_target = self.get_a_target_from_control()
@@ -1,433 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from collections import deque
from collections.abc import Callable
from dataclasses import asdict, dataclass
import math
import time
from typing import Any
import numpy as np
from openpilot.cereal import log, messaging
from opendbc.car.interfaces import ACCEL_MAX, ACCEL_MIN
from openpilot.common.realtime import DT_CTRL, DT_MDL, Ratekeeper
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.controls.lib.longcontrol import LongControl, LongCtrlState
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner
from openpilot.selfdrive.controls.radard import _LEAD_ACCEL_TAU
from openpilot.selfdrive.test.longitudinal_maneuvers.plant import Plant, PlannerSM
LeadObservation = dict[str, Any]
LeadObservationFn = Callable[[float, str, LeadObservation], LeadObservation | None]
ModelActionFn = Callable[[float, float, float], tuple[float, bool]]
EgoObservationFn = Callable[[float, float, float], tuple[float, float]]
ModelPlanFn = Callable[[float, float, float], list[float]]
ModelMetaFn = Callable[[float], tuple[list[float], bool, float]]
LeadFutureProbsFn = Callable[[float], tuple[float, float, float]]
PositionYFn = Callable[[float], list[float]]
ExperimentalModeFn = Callable[[float], bool]
@dataclass(frozen=True)
class ActuatorModel:
planner_delay: float
transport_delay: float
actuator_lag: float
command_rate_limit: float
stopping_acceleration: float
standstill_breakaway_acceleration: float
standstill_breakaway_time: float
def __post_init__(self):
nonnegative_fields = {
"planner_delay": self.planner_delay,
"transport_delay": self.transport_delay,
"actuator_lag": self.actuator_lag,
"standstill_breakaway_acceleration": self.standstill_breakaway_acceleration,
"standstill_breakaway_time": self.standstill_breakaway_time,
}
if any(not math.isfinite(value) or value < 0.0 for value in nonnegative_fields.values()):
raise ValueError(f"ActuatorModel fields must be finite and non-negative: {nonnegative_fields}")
if not math.isfinite(self.command_rate_limit) or self.command_rate_limit <= 0.0:
raise ValueError("command_rate_limit must be finite and positive")
if not math.isfinite(self.stopping_acceleration) or self.stopping_acceleration > 0.0:
raise ValueError("stopping_acceleration must be finite and non-positive")
# Conservative Prius TSS2 actuator model.
PRIUS_TSS2_ROUTE_MODEL = ActuatorModel(
planner_delay=0.05,
transport_delay=0.0,
actuator_lag=0.20,
command_rate_limit=4.0,
stopping_acceleration=-2.0,
standstill_breakaway_acceleration=1.0,
standstill_breakaway_time=0.05,
)
class PlantSP(Plant):
"""Closed-loop plant with configurable observations and actuator response."""
def __init__(
self,
lead_relevancy=False,
speed=0.0,
distance_lead=2.0,
enabled=True,
only_lead2=False,
only_radar=False,
e2e=False,
personality=0,
force_decel=False,
lead_observation_fn: LeadObservationFn | None = None,
model_action_fn: ModelActionFn | None = None,
ego_observation_fn: EgoObservationFn | None = None,
model_plan_fn: ModelPlanFn | None = None,
model_meta_fn: ModelMetaFn | None = None,
lead_future_probs_fn: LeadFutureProbsFn | None = None,
position_y_fn: PositionYFn | None = None,
experimental_mode_fn: ExperimentalModeFn | None = None,
actuator_delay: float | None = None,
actuator_lag: float = 0.0,
actuator_model: ActuatorModel | None = None,
run_long_control: bool = False,
):
if actuator_delay is not None and (not math.isfinite(actuator_delay) or actuator_delay < 0.0):
raise ValueError("actuator_delay must be finite and non-negative")
if not math.isfinite(actuator_lag) or actuator_lag < 0.0:
raise ValueError("actuator_lag must be finite and non-negative")
self.rate = 1.0 / DT_MDL
if not Plant.messaging_initialized:
Plant.radar = messaging.pub_sock('radarState')
Plant.controls_state = messaging.pub_sock('controlsState')
Plant.selfdrive_state = messaging.pub_sock('selfdriveState')
Plant.car_state = messaging.pub_sock('carState')
Plant.plan = messaging.sub_sock('longitudinalPlan')
Plant.messaging_initialized = True
self.v_lead_prev = 0.0
self.distance = 0.0
self.speed = speed
self.should_stop = False
self.acceleration = 0.0
self.a_target = 0.0
self.actuator_command = 0.0
self.applied_actuator_command = 0.0
self.breakaway_confirmed = False
self._breakaway_timer = 0.0
# lead car
self.lead_relevancy = lead_relevancy
self.distance_lead = distance_lead
self.enabled = enabled
self.only_lead2 = only_lead2
self.only_radar = only_radar
self.e2e = e2e
self.personality = personality
self.force_decel = force_decel
self.lead_observation_fn = lead_observation_fn
self.model_action_fn = model_action_fn
self.ego_observation_fn = ego_observation_fn
self.model_plan_fn = model_plan_fn
self.model_meta_fn = model_meta_fn
self.lead_future_probs_fn = lead_future_probs_fn
self.position_y_fn = position_y_fn
self.experimental_mode_fn = experimental_mode_fn
self.actuator_model = actuator_model
self.actuator_delay = actuator_model.planner_delay if actuator_model is not None else actuator_delay
self.transport_delay = actuator_model.transport_delay if actuator_model is not None else actuator_delay
self.actuator_lag = actuator_model.actuator_lag if actuator_model is not None else actuator_lag
self.publish_realized_a_ego = any((lead_observation_fn is not None, model_action_fn is not None, ego_observation_fn is not None,
actuator_delay is not None, actuator_lag > 0.0, actuator_model is not None, run_long_control))
self.rk = Ratekeeper(self.rate, print_delay_threshold=100.0)
self.ts = 1.0 / self.rate
time.sleep(0.1)
self.sm = messaging.SubMaster(['longitudinalPlan'])
from opendbc.car.honda.values import CAR
from opendbc.car.honda.interface import CarInterface
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
if self.actuator_delay is not None:
CP.longitudinalActuatorDelay = self.actuator_delay
CP_SP = CarInterface.get_non_essential_params_sp(CP, CAR.HONDA_CIVIC)
self.planner = LongitudinalPlanner(CP, CP_SP, init_v=self.speed)
self.long_control = LongControl(CP, CP_SP) if run_long_control else None
if self.actuator_model is not None and self.speed >= 0.01:
self.breakaway_confirmed = True
self.integration_dt = DT_CTRL if run_long_control else self.ts
delay_steps = 0 if self.transport_delay is None else round(self.transport_delay / self.integration_dt)
self._actuator_delay_queue = deque([self.acceleration] * delay_steps)
@staticmethod
def _lead_message(observation: LeadObservation):
lead = log.RadarState.LeadData.new_message()
for field, value in observation.items():
setattr(lead, field, value)
return lead
def _observe_lead(self, lead_name: str, truth: LeadObservation, present_by_default: bool) -> LeadObservation | None:
if self.lead_observation_fn is None:
return dict(truth) if present_by_default else None
observed = self.lead_observation_fn(self.current_time, lead_name, dict(truth))
if observed is None:
return None
complete_observation = dict(truth)
complete_observation.update(observed)
return complete_observation
def _update_actuator(self, command: float) -> tuple[float, float]:
if self._actuator_delay_queue:
self._actuator_delay_queue.append(command)
delayed_command = self._actuator_delay_queue.popleft()
else:
delayed_command = command
if self.actuator_model is not None:
max_command_delta = self.actuator_model.command_rate_limit * self.integration_dt
self.applied_actuator_command = float(np.clip(delayed_command,
self.applied_actuator_command - max_command_delta,
self.applied_actuator_command + max_command_delta))
if self.speed < 0.01:
if self.applied_actuator_command <= 0.0:
self.breakaway_confirmed = False
self._breakaway_timer = 0.0
elif not self.breakaway_confirmed:
breakaway_ready = self.applied_actuator_command + 1e-9 >= self.actuator_model.standstill_breakaway_acceleration
if breakaway_ready:
self._breakaway_timer += self.integration_dt
else:
self._breakaway_timer = 0.0
self.breakaway_confirmed = breakaway_ready and self._breakaway_timer + 1e-9 >= self.actuator_model.standstill_breakaway_time
if not self.breakaway_confirmed:
self.acceleration = 0.0
return delayed_command, self.acceleration
else:
self.breakaway_confirmed = True
response_command = self.applied_actuator_command
else:
self.applied_actuator_command = delayed_command
response_command = delayed_command
if self.actuator_lag > 0.0:
alpha = 1.0 - math.exp(-self.integration_dt / self.actuator_lag)
self.acceleration += alpha * (response_command - self.acceleration)
else:
self.acceleration = response_command
return delayed_command, self.acceleration
def _integrate_ego(self, dt: float, stop_at_standstill: bool = False) -> None:
self.speed += self.acceleration * dt
if self.speed <= 0.0 or stop_at_standstill and self.speed < 0.01 and self.actuator_command <= 0.0:
self.speed = self.acceleration = 0.0
self.distance += self.speed * dt
def step(self, v_lead=0.0, prob_lead=1.0, v_cruise=50.0, pitch=0.0, prob_throttle=1.0):
# ******** publish a fake model going straight and fake calibration ********
# note that this is worst case for MPC, since model will delay long mpc by one time step
radar = messaging.new_message('radarState')
control = messaging.new_message('controlsState')
ss = messaging.new_message('selfdriveState')
car_state = messaging.new_message('carState')
vehicle_parameters = messaging.new_message('vehicleParameters')
car_control = messaging.new_message('carControl')
model = messaging.new_message('modelV2')
car_state_sp = messaging.new_message('carStateSP')
live_map_data_sp = messaging.new_message('liveMapDataSP')
gps_data = messaging.new_message('gpsLocation')
a_lead = (v_lead - self.v_lead_prev) / self.ts
self.v_lead_prev = v_lead
if self.lead_relevancy:
d_rel = np.maximum(0.0, self.distance_lead - self.distance)
v_rel = v_lead - self.speed
if self.only_radar:
status = True
elif prob_lead > 0.5:
status = True
else:
status = False
else:
d_rel = 200.0
v_rel = 0.0
prob_lead = 0.0
status = False
truth_lead: LeadObservation = {
"dRel": float(d_rel),
"yRel": 0.0,
"vRel": float(v_rel),
"vLead": float(v_lead),
"vLeadK": float(v_lead),
"aLeadK": float(a_lead),
"present": bool(status),
# TODO use real radard logic for this
"aLeadTau": float(_LEAD_ACCEL_TAU),
"modelProb": float(prob_lead),
"radar": bool(self.only_radar),
"radarTrackId": -1,
}
lead_one_observation = self._observe_lead("leadOne", truth_lead, not self.only_lead2)
lead_two_observation = self._observe_lead("leadTwo", truth_lead, True)
if lead_one_observation is not None:
radar.radarState.leadOne = self._lead_message(lead_one_observation)
if lead_two_observation is not None:
radar.radarState.leadTwo = self._lead_message(lead_two_observation)
# Simulate model predicting slightly faster speed
# this is to ensure lead policy is effective when model
# does not predict slowdown in e2e mode
position = log.XYZTData.new_message()
position.x = [float(x) for x in (self.speed + 0.5) * np.array(ModelConstants.T_IDXS)]
if self.position_y_fn is None:
position.y = [0.0] * len(ModelConstants.T_IDXS)
else:
position.y = [float(y) for y in self.position_y_fn(self.current_time)]
model.modelV2.position = position
if self.model_action_fn is None:
model_acceleration, model_should_stop = self.acceleration + 0.5, False
else:
model_acceleration, model_should_stop = self.model_action_fn(self.current_time, self.speed, self.acceleration)
model.modelV2.action.desiredAcceleration = float(model_acceleration)
model.modelV2.action.shouldStop = bool(model_should_stop)
velocity = log.XYZTData.new_message()
if self.model_plan_fn is None:
velocity_plan = [float(x) for x in (self.speed + 0.5) * np.ones_like(ModelConstants.T_IDXS)]
velocity_plan[0] = float(self.speed) # always start at current speed
else:
velocity_plan = [float(x) for x in self.model_plan_fn(self.current_time, self.speed, self.acceleration)]
velocity.x = velocity_plan
model.modelV2.velocity = velocity
acceleration = log.XYZTData.new_message()
acceleration.x = [float(x) for x in np.zeros_like(ModelConstants.T_IDXS)]
model.modelV2.acceleration = acceleration
model.modelV2.meta.disengagePredictions.gasPressProbs = [float(prob_throttle) for _ in range(6)]
if self.model_meta_fn is None:
brake3_probs, hard_brake_predicted, frame_drop_perc = [0.0] * 5, False, 0.0
else:
brake3_probs, hard_brake_predicted, frame_drop_perc = self.model_meta_fn(self.current_time)
model.modelV2.meta.disengagePredictions.brake3MetersPerSecondSquaredProbs = [float(p) for p in brake3_probs]
model.modelV2.meta.hardBrakePredicted = bool(hard_brake_predicted)
model.modelV2.frameDropPerc = float(frame_drop_perc)
if self.lead_future_probs_fn is not None:
model.modelV2.init('leadsV3', 3)
lead_future_probs = self.lead_future_probs_fn(self.current_time)
for i, (prob, prob_time) in enumerate(zip(lead_future_probs, (0.0, 2.0, 4.0), strict=True)):
model.modelV2.leadsV3[i].prob = float(prob)
model.modelV2.leadsV3[i].probTime = prob_time
control.controlsState.longControlState = self.long_control.long_control_state if self.long_control is not None else (
LongCtrlState.pid if self.enabled else LongCtrlState.off)
ss.selfdriveState.experimentalMode = self.e2e if self.experimental_mode_fn is None else bool(self.experimental_mode_fn(self.current_time))
ss.selfdriveState.personality = self.personality
control.controlsState.forceDecel = self.force_decel
true_v_ego = self.speed
true_a_ego = self.acceleration
published_v_ego = true_v_ego
published_a_ego = true_a_ego if self.publish_realized_a_ego else 0.0
if self.ego_observation_fn is not None:
published_v_ego, published_a_ego = self.ego_observation_fn(self.current_time, true_v_ego, true_a_ego)
car_state.carState.vEgo = float(published_v_ego)
car_state.carState.aEgo = float(published_a_ego)
car_state.carState.standstill = bool(self.speed < 0.01)
car_state.carState.vCruise = float(v_cruise * 3.6)
car_control.carControl.orientationNED = [0.0, float(pitch), 0.0]
# ******** get controlsState messages for plotting ***
sm = PlannerSM(self.rk.frame, {
'radarState': radar.radarState,
'carState': car_state.carState,
'carControl': car_control.carControl,
'controlsState': control.controlsState,
'selfdriveState': ss.selfdriveState,
'vehicleParameters': vehicle_parameters.vehicleParameters,
'modelV2': model.modelV2,
'carStateSP': car_state_sp.carStateSP,
'liveMapDataSP': live_map_data_sp.liveMapDataSP,
'gpsLocation': gps_data.gpsLocation,
})
self.planner.update(sm)
self.a_target = self.planner.output_a_target
if self.long_control is None:
self.actuator_command = self.a_target
if self.planner.output_should_stop:
stopping_acceleration = -0.5 if self.actuator_model is None else self.actuator_model.stopping_acceleration
self.actuator_command = min(stopping_acceleration, self.actuator_command)
self._update_actuator(self.actuator_command)
self._integrate_ego(self.ts)
else:
for _ in range(round(self.ts / DT_CTRL)):
car_state.carState.vEgo = self.speed
car_state.carState.aEgo = self.acceleration
car_state.carState.standstill = self.speed < 0.01
self.actuator_command = self.long_control.update(
self.enabled, car_state.carState, self.a_target, self.planner.output_should_stop, (ACCEL_MIN, ACCEL_MAX),
)
self._update_actuator(self.actuator_command)
self._integrate_ego(DT_CTRL, stop_at_standstill=True)
self.should_stop = self.planner.output_should_stop
fcw = self.planner.fcw
self.distance_lead = self.distance_lead + v_lead * self.ts
# *** radar model ***
if self.lead_relevancy:
d_rel = np.maximum(0.0, self.distance_lead - self.distance)
v_rel = v_lead - self.speed
else:
d_rel = 200.0
v_rel = 0.0
# print at 5hz
# if (self.rk.frame % (self.rate // 5)) == 0:
# print("%2.2f sec %6.2f m %6.2f m/s %6.2f m/s2 lead_rel: %6.2f m %6.2f m/s"
# % (self.current_time, self.distance, self.speed, self.acceleration, d_rel, v_rel))
# ******** update prevs ********
self.rk.monitor_time()
return {
"distance": self.distance,
"speed": self.speed,
"acceleration": self.acceleration,
"realized_acceleration": self.acceleration,
"a_target": self.a_target,
"actuator_command": self.actuator_command,
"published_a_ego": published_a_ego,
"published_v_ego": published_v_ego,
"should_stop": self.should_stop,
"long_control_state": (int(self.long_control.long_control_state) if self.long_control is not None
else control.controlsState.longControlState.raw),
"distance_lead": self.distance_lead,
"fcw": fcw,
"mpc_source": self.planner.mpc.source,
"dec_mode": self.planner.dec.mode(),
"dec_want_blended": self.planner.dec.want_blended,
"dec_signals": asdict(self.planner.dec.signals),
"dec_lead_veto": self.planner.dec.lead_veto,
"controller_active": self.planner.accel_controller_active,
"model_action": {
"desiredAcceleration": float(model_acceleration),
"shouldStop": bool(model_should_stop),
},
"truth_lead": dict(truth_lead),
"lead_one_observation": None if lead_one_observation is None else dict(lead_one_observation),
"lead_two_observation": None if lead_two_observation is None else dict(lead_two_observation),
}

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