Compare commits

...

360 Commits

Author SHA1 Message Date
Jason Wen
2a16b0d0cc Merge branch '2.1' into hkg-angle-steering-2025-sl-2.1
# Conflicts:
#	opendbc_repo
#	system/version.py
2026-04-23 01:26:05 -04:00
Jason Wen
c9d0f0af8c tests 2026-04-22 20:51:57 -04:00
Jason Wen
b81b5e51e7 new 2026-04-22 20:39:36 -04:00
Jason Wen
461c134a23 no 2026-04-22 20:38:44 -04:00
Jason Wen
83d405dc2b legacy code 2026-04-22 14:45:52 -04:00
Jason Wen
3bc818a206 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into 2.1 2026-04-22 02:02:48 -04:00
Jason Wen
b57c593d92 bump 2026-04-20 23:31:05 -04:00
Jason Wen
7bee798c28 sunnylink: route resume_queued to athena direct
Backend is retiring the CloudFront proxy on stg.api.sunnypilot.ai that
currently forwards ws|settings|navigation to athena.sunnylink.ai. Once
retired, /ws/{id}/resume_queued only resolves via athena.sunnylink.ai.

SunnylinkApi.resume_queued now temporarily swaps self.api_host to
ATHENA_HOST while re-entering api_get (preserves the SunnylinkEnabled
gate and existing JWT/UA header handling). Other device calls
(device/{id}/roles, device/{id}/users, v2/pilotauth/, backups) continue
to hit the sunnylink main API host unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:10:16 -04:00
Jason Wen
5790ba37d9 schema: convert ShowAdvancedControls visibility to enablement
Items previously hidden when ShowAdvancedControls is off now render
as disabled with an ADVANCED tooltip. Same treatment for dependency-
based hides (LagdToggleDelay, LaneTurnValue) so users see what's
available and why it's locked. CameraOffset gets the Advanced gate it
was missing.

Affected: DisableUpdates, EnableGithubRunner, EnableCopyparty,
QuickBootToggle, LaneTurnValue, LagdToggleDelay, CameraOffset, and the
test_maneuvers section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 05:02:23 -04:00
nayan
0674c42866 good bot
fix state
2026-04-19 08:52:19 +02:00
Jason Wen
63c6b4fea2 sunnylink: protocol_version sentinel + opaque-flag tests
Pin PROTOCOL_VERSION to KNOWN_PROTOCOL_VERSIONS so a bump shows up in
review, and assert subaru_has_sng / hyundai_alpha_long_available are
declared with safe False defaults.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:38:06 -04:00
Jason Wen
20842be3db sunnylink: per-bug regression tests for settings_ui parity
Cover the audit findings end-to-end: MADS brand gates, Test Maneuvers
section + attestation, validator accepts the JSON, dynamic torque
options match the versions JSON, SP dev items gate on is_sp_release as
well as is_release, spurious offroad_only dropped from
DisengageOnAccelerator and DynamicExperimentalControl, and offroad_only
replaced with not_engaged on AlphaLongitudinalEnabled and the Toyota
long toggles. Validator test skips when jsonschema is unavailable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:37:32 -04:00
Jason Wen
ab0061eac1 sunnylink: inject torque tune options from versions JSON
Read latcontrol_torque_versions.json at schema generation time and
override TorqueControlTune options so adding a version to the JSON
flows to the dashboard without editing settings_ui.json by hand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:31:42 -04:00
Jason Wen
cc196b20b5 sunnylink: extend settings_ui validator for new fields
Add SchemaItem fields title_param_suffix (object form), needs_onroad_cycle,
blocked, requires_attestation; add option-level enablement to SchemaOption;
add visibility/enablement/attestation_required to PanelSection; add
not_engaged Rule type; replace vehicle_settings inline shape with
VehicleBrandSettings def (title/description/items).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:30:35 -04:00
Jason Wen
e537a28512 sunnylink: tighten settings_ui gates against Raylib parity
Brand gates on MadsMainCruiseAllowed/MadsUnifiedEngagementMode (rivian +
tesla-no-bus); convert enablement->visibility on items Raylib hides
(DisableUpdates, EnableGithubRunner, EnableCopyparty, QuickBootToggle,
HyundaiLongitudinalTuning, LaneTurnValue, LagdToggleDelay); drop spurious
offroad_only on DynamicExperimentalControl + DisengageOnAccelerator and
SpeedLimit policy/offset over-gates; replace offroad_only with not_engaged
on AlphaLongitudinalEnabled/Toyota long toggles; extend release-branch
NOTs with is_sp_release; add Test Maneuvers section with attestation,
move LongitudinalManeuverMode in alongside new LateralManeuverMode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:29:11 -04:00
Jason Wen
c35b73ee90 sunnylink: bundle-first capabilities + protocol_version
Bundle-first brand resolution, fix steer_control_type to use
CP.steerControlType (physical) not lateralTuning.which() (tuning class),
add Subaru/Hyundai opaque per-platform flags from CarPlatformBundle, and
embed PROTOCOL_VERSION in the capabilities payload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:22:29 -04:00
Jason Wen
dc410c57fa Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into 2.1 2026-04-18 22:41:54 -04:00
Jason Wen
dcf932e212 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into 2.1 2026-04-18 15:39:09 -04:00
nayan
1f7bcf246a Bringing sl change to validate 2026-04-18 09:25:12 +02:00
Jason Wen
cffa673ca8 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into 2.1 2026-04-17 13:57:14 -04:00
Jason Wen
609d66e151 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into 2.1 2026-04-17 02:05:35 -04: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
Jason Wen
f3dc00eb55 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into 2.1
# Conflicts:
#	sunnypilot/sunnylink/params_metadata.json
2026-04-12 00:32:17 -04: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
e3c4db68d1 Revert "fixes"
This reverts commit 53a2adb45a.
2026-03-26 21:26:20 -07:00
Jason Wen
bcdf26258b ew 2026-03-26 06:00:31 -04:00
Jason Wen
53a2adb45a fixes 2026-03-26 03:25:04 -04:00
Jason Wen
aa91030972 order 2026-03-25 18:36:40 -04:00
Jason Wen
dcfc48cba5 order 2026-03-25 18:15:52 -04:00
Jason Wen
44a5c1c5d0 order 2026-03-25 18:14:19 -04:00
Jason Wen
5ab792c5b7 Merge branch 'master' into 2.1 2026-03-25 16:52:15 -04:00
Jason Wen
a3a3478c62 device type 2026-03-25 14:58:29 -04:00
Jason Wen
50504246e7 move and fix 2026-03-25 14:33:41 -04:00
Jason Wen
9f68541a77 more 2026-03-25 10:39:31 -04:00
Jason Wen
52465215c1 in another pr 2026-03-25 10:16:38 -04:00
Jason Wen
de5f22f659 less 2026-03-25 10:02:39 -04:00
Jason Wen
2563dca0eb update 2026-03-25 04:45:24 -04:00
Jason Wen
5af3b9c1a1 update 2026-03-25 03:30:08 -04:00
Jason Wen
1a8ccf46fd slightly more 2026-03-25 03:16:09 -04:00
Jason Wen
cadb0b1868 dynamic 2026-03-25 02:58:53 -04:00
Jason Wen
371a58a9b7 sync 2026-03-25 02:53:48 -04:00
Jason Wen
23d0f391f8 set 2026-03-25 02:22:35 -04:00
Jason Wen
08bc66f6db new 2026-03-25 01:58:29 -04:00
Jason Wen
a2e137b54e more 2026-03-25 01:48:52 -04:00
Jason Wen
08ad38cfdf revert 2026-03-25 01:04:31 -04:00
Jason Wen
a0069e5966 move 2026-03-25 01:00:02 -04:00
Jason Wen
49bdd832ee hide 2026-03-25 00:44:34 -04:00
Jason Wen
cd0563622a more 2026-03-24 23:52:23 -04:00
Jason Wen
80bf7ad066 more 2026-03-24 23:41:44 -04:00
Jason Wen
4124053716 move 2026-03-24 23:08:05 -04:00
Jason Wen
b0b2c7becf partial availability 2026-03-24 12:30:10 -04:00
Jason Wen
c53e8aec3c rearrange 2026-03-24 12:02:31 -04:00
Jason Wen
1c1af6133f boom shakalaka 2026-03-24 11:53:42 -04:00
Jason Wen
c2fb074a90 more 2026-03-24 11:51:07 -04:00
Jason Wen
550c3b3a69 syncing more 2026-03-24 11:39:45 -04:00
Jason Wen
454d69f58d move it 2026-03-24 10:42:50 -04:00
Jason Wen
1cd1dc3f2c default 2026-03-24 09:22:08 -04:00
Jason Wen
45ca9f39b8 dynamic unit 2026-03-24 07:29:33 -04:00
Jason Wen
5b88ec1e5c new 2026-03-23 23:08:01 -04:00
Jason Wen
48f003ee6c combine 2026-03-23 03:24:01 -04:00
Jason Wen
614f2de8ae new 2026-03-23 02:48:15 -04:00
Jason Wen
bec33e52ce options 2026-03-23 01:41:51 -04:00
Jason Wen
058b03bcdd cycle init 2026-03-22 04:04:31 -04:00
Jason Wen
574c0379ed vehicle 2026-03-22 00:49:22 -04:00
Jason Wen
8a873d9107 readme 2026-03-21 23:50:45 -04:00
Jason Wen
cf8d001612 more subs 2026-03-20 11:07:12 -04:00
Jason Wen
a610ff250f update 2026-03-20 02:56:02 -04:00
Jason Wen
9f6369d610 v2 2026-03-20 00:49:18 -04:00
Jason Wen
068baca464 for now 2026-03-19 23:28:53 -04:00
Jason Wen
8dcdee5fa0 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into 2.1 2026-03-18 03:53:30 -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
3ce5835c0d more 2026-03-17 03:41:32 -04:00
Jason Wen
e380c0b606 uh 2026-03-16 13:09:33 -04:00
Jason Wen
556009edfe fix 2026-03-15 18:53:22 -04:00
Jason Wen
69b098c1f2 1 2026-03-15 18:08:58 -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
31 changed files with 8553 additions and 47 deletions

View File

@@ -2,5 +2,6 @@ Wen
REGIST
PullRequest
cancelled
indeces
FOF
NoO

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>

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>

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>

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>

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>

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>

View File

@@ -204,6 +204,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
// sunnylink params
{"EnableSunnylinkUploader", {PERSISTENT | BACKUP, BOOL}},
{"LastSunnylinkPingTime", {CLEAR_ON_MANAGER_START, INT}},
{"ParamsVersion", {PERSISTENT, INT}},
{"SunnylinkCache_Roles", {PERSISTENT, STRING}},
{"SunnylinkCache_Users", {PERSISTENT, STRING}},
{"SunnylinkDongleId", {PERSISTENT, STRING}},

View File

@@ -227,6 +227,7 @@ exclude = [
"teleoprtc/",
"teleoprtc_repo/",
"third_party/",
"**/*.ipynb",
]
[tool.ty.rules]

View File

@@ -13,7 +13,7 @@ cd $ROOT
FAILED=0
IGNORED_FILES="uv\.lock|docs\/CARS.md|LICENSE\.md"
IGNORED_FILES="uv\.lock|docs\/CARS.md|LICENSE\.md|layouts\/.*\.xml|.*\.ipynb"
IGNORED_DIRS="^third_party.*|^msgq.*|^msgq_repo.*|^opendbc.*|^opendbc_repo.*|^cereal.*|^panda.*|^rednose.*|^rednose_repo.*|^tinygrad.*|^tinygrad_repo.*|^teleoprtc.*|^teleoprtc_repo.*"
function run() {

View File

@@ -120,20 +120,12 @@ class SteeringLayout(Widget):
def _update_state(self):
super()._update_state()
torque_allowed = True
torque_allowed = ui_state.CP is not None and ui_state.CP.steerControlType != car.CarParams.SteerControlType.angle
if ui_state.CP is not None:
mads_main_desc = self._mads_limited_desc if self._mads_settings_layout._mads_limited_settings() else self._mads_full_desc
self._mads_toggle.set_description(f"<b>{mads_main_desc}</b><br><br>{self._mads_base_desc}")
if ui_state.CP.steerControlType == car.CarParams.SteerControlType.angle:
ui_state.params.remove("EnforceTorqueControl")
ui_state.params.remove("NeuralNetworkLateralControl")
torque_allowed = False
else:
self._mads_toggle.set_description(f"<b>{self._mads_check_compat_desc}</b><br><br>{self._mads_base_desc}")
ui_state.params.remove("EnforceTorqueControl")
ui_state.params.remove("NeuralNetworkLateralControl")
torque_allowed = False
self._mads_toggle.action_item.set_enabled(ui_state.is_offroad())
self._mads_settings_button.action_item.set_enabled(ui_state.is_offroad() and self._mads_toggle.action_item.get_state())

View File

@@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details.
"""
from enum import Enum
from cereal import messaging, log, custom
from cereal import messaging, log, car, custom
from openpilot.common.params import Params
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
@@ -26,22 +26,22 @@ class OnroadTimerStatus(Enum):
class UIStateSP:
def __init__(self):
self.CP_SP: custom.CarParamsSP | None = None
self.params = Params()
self.CP_SP: custom.CarParamsSP | None = None
self.has_icbm: bool = False
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
self.sm_services_ext = [
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
"gpsLocation", "liveTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "liveDelay"
]
self.sunnylink_state = SunnylinkState()
self.update_params()
self.onroad_brightness_timer: int = 0
self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True)
self.update_params()
self.reset_onroad_sleep_timer()
self.CP_SP: custom.CarParamsSP | None = None
self.has_icbm: bool = False
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
def update(self) -> None:
if self.sunnylink_enabled:
@@ -128,6 +128,8 @@ class UIStateSP:
if CP_SP_bytes is not None:
self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement")
self._enforce_sp_constraints()
self.active_bundle = self.params.get("ModelManager_ActiveBundle")
self.blindspot = self.params.get_bool("BlindSpot")
self.chevron_metrics = self.params.get("ChevronInfo")
@@ -148,6 +150,48 @@ class UIStateSP:
self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True)
self.always_offroad = self.params.get_bool("OffroadMode")
def _enforce_sp_constraints(self) -> None:
has_long = getattr(self, 'has_longitudinal_control', False)
has_icbm = self.has_icbm
CP = getattr(self, 'CP', None)
if CP is not None:
# Angle steering: no torque-based lateral controls
if CP.steerControlType == car.CarParams.SteerControlType.angle:
self.params.remove("EnforceTorqueControl")
self.params.remove("NeuralNetworkLateralControl")
# Alpha longitudinal: clear if not available or on release branch
if not CP.alphaLongitudinalAvailable or self.params.get_bool("IsReleaseBranch"):
self.params.remove("AlphaLongitudinalEnabled")
# BSM not available: clear BSM-dependent settings
if not CP.enableBsm:
self.params.remove("AutoLaneChangeBsmDelay")
else:
# No CarParams: clear all car-dependent params as safety default
self.params.remove("EnforceTorqueControl")
self.params.remove("NeuralNetworkLateralControl")
self.params.remove("AlphaLongitudinalEnabled")
# No longitudinal control: no experimental mode
if not has_long:
self.params.remove("ExperimentalMode")
# ICBM: clear if not available or if full longitudinal control is active
if self.CP_SP is not None:
if not self.CP_SP.intelligentCruiseButtonManagementAvailable or has_long:
self.params.remove("IntelligentCruiseButtonManagement")
else:
self.params.remove("IntelligentCruiseButtonManagement")
# Cruise features requiring longitudinal or ICBM
if not (has_long or has_icbm):
self.params.remove("CustomAccIncrementsEnabled")
self.params.remove("DynamicExperimentalControl")
self.params.remove("SmartCruiseControlVision")
self.params.remove("SmartCruiseControlMap")
class DeviceSP:
@staticmethod

View File

@@ -12,6 +12,9 @@ from openpilot.system.hardware import HARDWARE
from openpilot.system.hardware.hw import Paths
API_HOST = os.getenv('SUNNYLINK_API_HOST', 'https://stg.api.sunnypilot.ai')
# Athena HTTP gateway (serves ws/settings/navigation paths). CloudFront proxy
# from stg.api.sunnypilot.ai is being retired; clients must call athena direct.
ATHENA_HOST = 'https://athena.sunnylink.ai'
UNREGISTERED_SUNNYLINK_DONGLE_ID = "UnregisteredDevice"
MAX_RETRIES = 6
CRASH_LOG_DIR = Paths.crash_log_root()
@@ -32,7 +35,12 @@ class SunnylinkApi(BaseApi):
def resume_queued(self, timeout=10, **kwargs):
sunnylinkId, commaId = self._resolve_dongle_ids()
return self.api_get(f"ws/{sunnylinkId}/resume_queued", "POST", timeout, access_token=self.get_token(), **kwargs)
saved_host = self.api_host
self.api_host = ATHENA_HOST
try:
return self.api_get(f"ws/{sunnylinkId}/resume_queued", "POST", timeout, access_token=self.get_token(), **kwargs)
finally:
self.api_host = saved_host
def get_token(self, payload_extra=None, expiry_hours=1):
# Add your additional data here

View File

@@ -18,7 +18,7 @@ import time
from jsonrpc import dispatcher
from functools import partial
from openpilot.common.params import Params
from openpilot.common.params import Params, ParamKeyType
from openpilot.common.realtime import set_core_affinity
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware.hw import Paths
@@ -31,6 +31,8 @@ import cereal.messaging as messaging
from openpilot.sunnypilot.selfdrive.car.sync_car_list_param import update_car_list_param
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi
from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string
from openpilot.sunnypilot.sunnylink.capabilities import generate_capabilities, CAPABILITY_LABELS
from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import generate_schema
SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://ws.stg.api.sunnypilot.ai')
HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4"))
@@ -44,12 +46,15 @@ params = Params()
# Parameters that should never be remotely modified
BLOCKED_PARAMS = {
"AdbEnabled",
"CompletedSunnylinkConsentVersion",
"CompletedTrainingVersion",
"GithubUsername", # Could grant SSH access
"GithubSshKeys", # Direct SSH key injection
"HasAcceptedTerms",
"HasAcceptedTermsSP",
"OnroadCycleRequested", # Prevent remote cycle trigger
"ParamsVersion", # Device-managed version counter
}
@@ -231,34 +236,18 @@ def getParamsAllKeysV1() -> dict[str, str]:
@dispatcher.add_method
def getParamsMetadata() -> str:
"""Compressed equivalent of getParamsAllKeysV1 — same struct, gzipped + base64."""
"""Return settings_ui.json + live capabilities as gzip-compressed, base64-encoded string.
Reads settings_ui.json, injects live capabilities derived from CarParams,
compresses, and returns. This is the single RPC for the frontend to get
the complete settings UI definition + runtime capabilities.
"""
try:
with open(METADATA_PATH) as f:
metadata = json.load(f)
except Exception:
cloudlog.exception("sunnylinkd.getParamsMetadata.exception")
metadata = {}
try:
available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()]
params_list: list[dict] = []
for key in available_keys:
value = get_param_as_byte(key, get_default=True)
param_entry: dict = {
"key": key,
"type": int(params.get_type(key).value),
"default_value": base64.b64encode(value).decode('utf-8') if value else None,
}
if key in metadata:
param_entry["_extra"] = metadata[key]
params_list.append(param_entry)
raw = json.dumps(params_list, separators=(',', ':')).encode('utf-8')
return base64.b64encode(gzip.compress(raw)).decode('utf-8')
schema = generate_schema()
schema["capabilities"] = generate_capabilities()
schema["capability_labels"] = CAPABILITY_LABELS
raw = json.dumps(schema, separators=(",", ":")).encode("utf-8")
return base64.b64encode(gzip.compress(raw)).decode("utf-8")
except Exception:
cloudlog.exception("sunnylinkd.getParamsMetadata.exception")
raise
@@ -270,12 +259,25 @@ def getParams(params_keys: list[str], compression: bool = False) -> str | dict[s
available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()]
try:
zero_values: dict[int, bytes] = {
ParamKeyType.STRING.value: b"",
ParamKeyType.BOOL.value: b"0",
ParamKeyType.INT.value: b"0",
ParamKeyType.FLOAT.value: b"0.0",
ParamKeyType.TIME.value: b"",
ParamKeyType.JSON.value: b"{}",
ParamKeyType.BYTES.value: b"",
}
param_keys_validated = [key for key in params_keys if key in available_keys]
params_dict: dict[str, list[dict[str, str | bool | int]]] = {"params": []}
for key in param_keys_validated:
value = get_param_as_byte(key)
if value is None:
continue
value = get_param_as_byte(key, get_default=True)
if value is None:
param_type = params.get_type(key)
value = zero_values.get(param_type.value, b"")
params_dict["params"].append({
"key": key,
@@ -306,6 +308,13 @@ def saveParams(params_to_update: dict[str, str], compression: bool = False) -> N
except Exception as e:
cloudlog.error(f"sunnylinkd.saveParams.exception {e}")
# Increment version counter for frontend change detection
try:
current = int(params.get("ParamsVersion") or "0")
params.put("ParamsVersion", str(current + 1))
except Exception:
pass
def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]:
sunnylink_dongle_id = params.get("SunnylinkDongleId")

View File

@@ -0,0 +1,169 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import json
from cereal import car, custom, messaging
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, UNSUPPORTED_LONGITUDINAL_CAR
from opendbc.car.subaru.values import CAR as SUBARU_CAR, SubaruFlags
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware import HARDWARE
# Wire-protocol version for the capabilities payload. Bump on breaking changes
# only; additive fields are backward-compatible and do not require a bump.
PROTOCOL_VERSION = 1
# All capability fields that rules may reference.
# Non-boolean fields must have defaults in CAPABILITY_DEFAULTS.
CAPABILITY_FIELDS = (
"protocol_version",
"has_longitudinal_control",
"has_icbm",
"icbm_available",
"torque_allowed",
"brand",
"pcm_cruise",
"alpha_long_available",
"steer_control_type",
"enable_bsm",
"is_release",
"is_sp_release",
"is_development",
"tesla_has_vehicle_bus",
"has_stop_and_go",
"stock_longitudinal",
"device_type",
"subaru_has_sng",
"hyundai_alpha_long_available",
)
CAPABILITY_LABELS: dict[str, str] = {
"protocol_version": "Capabilities protocol version",
"has_longitudinal_control": "sunnypilot longitudinal control",
"has_icbm": "ICBM enabled",
"icbm_available": "ICBM available",
"torque_allowed": "torque steering (not available for angle steering vehicles)",
"brand": "Vehicle brand",
"pcm_cruise": "PCM cruise",
"alpha_long_available": "Alpha Longitudinal available",
"steer_control_type": "Steer control type",
"enable_bsm": "BSM available",
"is_release": "Release branch",
"is_sp_release": "SP release branch",
"is_development": "Development branch",
"tesla_has_vehicle_bus": "Tesla vehicle bus",
"has_stop_and_go": "Stop and Go",
"stock_longitudinal": "stock longitudinal",
"device_type": "Device type",
"subaru_has_sng": "Subaru Stop-and-Go available",
"hyundai_alpha_long_available": "Hyundai Alpha Longitudinal available",
}
# Explicit defaults for non-boolean capability fields
CAPABILITY_DEFAULTS: dict[str, bool | str | int] = {
"brand": "",
"steer_control_type": "",
"device_type": "",
"protocol_version": PROTOCOL_VERSION,
}
def _bundle_field(bundle: dict | None, key: str) -> str:
return bundle.get(key, "") if isinstance(bundle, dict) else ""
def generate_capabilities(params: Params | None = None) -> dict:
"""Generate a SettingsCapabilities dict from CarParams + boolean params.
Bundle-first source of truth: when CarPlatformBundle is present, brand and
platform derive from the bundle (mirrors Raylib settings code paths). The
CarParams* deserialization is the fallback when the bundle has not yet been
written (very early after first pairing).
"""
params = params or Params()
caps: dict = {field: CAPABILITY_DEFAULTS.get(field, False) for field in CAPABILITY_FIELDS}
# Wire-protocol version is always set explicitly.
caps["protocol_version"] = PROTOCOL_VERSION
# Hardware + boolean params (no CarParams dependency)
caps["device_type"] = HARDWARE.get_device_type()
caps["is_release"] = params.get_bool("IsReleaseBranch")
caps["is_sp_release"] = params.get_bool("IsReleaseSpBranch")
caps["is_development"] = params.get_bool("IsDevelopmentBranch")
caps["stock_longitudinal"] = params.get_bool("ToyotaEnforceStockLongitudinal")
bundle = params.get("CarPlatformBundle")
bundle_brand = _bundle_field(bundle, "brand")
bundle_platform = _bundle_field(bundle, "platform")
# Bundle-first brand resolution; CP is fallback only.
if bundle_brand:
caps["brand"] = bundle_brand
# CarParams-derived capabilities
CP_bytes = params.get("CarParamsPersistent")
if CP_bytes is not None:
try:
CP = messaging.log_from_bytes(CP_bytes, car.CarParams)
caps["alpha_long_available"] = bool(CP.alphaLongitudinalAvailable)
if CP.alphaLongitudinalAvailable:
caps["has_longitudinal_control"] = params.get_bool("AlphaLongitudinalEnabled")
else:
caps["has_longitudinal_control"] = bool(CP.openpilotLongitudinalControl)
# CP.steerControlType is the physical control mode (angle / torque).
# CP.lateralTuning.which() returns the tuning class (pid / torque / indi)
# which is a separate concept and is not interchangeable.
caps["steer_control_type"] = str(CP.steerControlType)
caps["torque_allowed"] = CP.steerControlType != car.CarParams.SteerControlType.angle
if not caps["brand"] and CP.brand:
caps["brand"] = str(CP.brand)
caps["pcm_cruise"] = bool(CP.pcmCruise)
caps["enable_bsm"] = bool(CP.enableBsm)
# Generic SnG fallback. Brand-specific opaque flags below override.
caps["has_stop_and_go"] = bool(CP.openpilotLongitudinalControl)
except Exception:
cloudlog.exception("capabilities: failed to deserialize CarParamsPersistent")
# CarParamsSP-derived capabilities
CP_SP_bytes = params.get("CarParamsSPPersistent")
if CP_SP_bytes is not None:
try:
CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
caps["icbm_available"] = bool(CP_SP.intelligentCruiseButtonManagementAvailable)
caps["has_icbm"] = bool(CP_SP.intelligentCruiseButtonManagementAvailable) and params.get_bool("IntelligentCruiseButtonManagement")
caps["tesla_has_vehicle_bus"] = bool(CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS)
except Exception:
cloudlog.exception("capabilities: failed to deserialize CarParamsSPPersistent")
# Brand-specific opaque flags. Mirror Raylib brand-settings logic so the
# device and the dashboard agree on per-platform availability without
# leaking the platform identifier over the wire.
if caps["brand"] == "subaru" and bundle_platform:
try:
flags = SUBARU_CAR[bundle_platform].config.flags
caps["subaru_has_sng"] = not bool(flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
caps["has_stop_and_go"] = caps["subaru_has_sng"]
except KeyError:
cloudlog.exception(f"capabilities: unknown subaru platform {bundle_platform!r}")
if caps["brand"] == "hyundai" and bundle_platform:
try:
unsupported = set().union(*UNSUPPORTED_LONGITUDINAL_CAR.values())
caps["hyundai_alpha_long_available"] = HYUNDAI_CAR[bundle_platform] not in unsupported
except KeyError:
cloudlog.exception(f"capabilities: unknown hyundai platform {bundle_platform!r}")
return caps
def generate_capabilities_json(params: Params | None = None) -> str:
"""Generate SettingsCapabilities as a JSON string."""
return json.dumps(generate_capabilities(params), separators=(",", ":"))

View File

@@ -0,0 +1,353 @@
# sunnylink Settings UI Guide
> Edit one JSON file, run the validator, commit. The sunnylink frontend updates automatically.
For detailed architecture, capability fields, parity analysis, and dialog mappings, see [REFERENCE.md](REFERENCE.md).
## The File You Edit
| File | What | When to edit |
|------|------|-------------|
| `settings_ui.json` | Structure, widget types, display text, options, rules - everything | Adding/moving/removing/renaming a setting |
All metadata (titles, descriptions, options, min/max/step/unit) lives **inline on each item**. There is no separate metadata file.
## Quick Reference: Widget Types
| Widget | Use for | Fields needed |
|--------|---------|---------------|
| `toggle` | On/off boolean | `title` |
| `multiple_button` | 2-4 discrete options | `title` + `options` array |
| `option` | Numeric range or dropdown | `title` + `min/max/step` or `options` |
| `info` | Read-only display | `title` |
## Quick Reference: Item Fields
| Field | Required | Description |
|-------|----------|-------------|
| `key` | Yes | Param key name (must exist in `params_keys.h`) |
| `widget` | Yes | `toggle`, `option`, `multiple_button`, `button`, `info` |
| `title` | Yes | Display name shown to the user |
| `description` | No | Explanatory text below the title |
| `options` | For selectors | Array of `{"value": 0, "label": "Off"}` objects (see per-option enablement below) |
| `min`, `max`, `step` | For sliders | Numeric range constraints |
| `unit` | No | Unit label. Static: `"seconds"`. Dynamic: `{"metric": "km/h", "imperial": "mph"}` (resolved by IsMetric) |
| `visibility` | No | Rules for show/hide. Settings are **never hidden**, always dimmed with UNAVAILABLE badge when rules fail |
| `enablement` | No | Rules for enabled/disabled (all must pass). Dimmed with badge when rules fail |
| `blocked` | No | `true` for device-only settings that cannot be modified remotely. Frontend shows as read-only |
| `title_param_suffix` | No | Dynamic title suffix. Example: `{"param": "IsMetric", "values": {"0": "mph", "1": "km/h"}}` |
| `sub_items` | No | Nested child items |
| `needs_onroad_cycle` | No | `true` if changing this param triggers a system restart. Frontend shows a "Restart" badge. See [REFERENCE.md - Remote Onroad Cycle](REFERENCE.md#remote-onroad-cycle) |
## Quick Reference: Rule Types
| Rule | Example | Use for |
|------|---------|---------|
| `offroad_only` | `{"type": "offroad_only"}` | Grey out while driving |
| `capability` | `{"type": "capability", "field": "has_longitudinal_control", "equals": true}` | Car-dependent visibility |
| `param` | `{"type": "param", "key": "Mads", "equals": true}` | Show/enable based on another setting |
| `param_compare` | `{"type": "param_compare", "key": "SpeedLimitMode", "op": ">", "value": 0}` | Numeric comparison |
| `not` | `{"type": "not", "condition": {...}}` | Negate a rule |
| `any` | `{"type": "any", "conditions": [...]}` | OR logic |
| `all` | `{"type": "all", "conditions": [...]}` | AND logic (for nesting inside `any`/`not`) |
**Visibility design**: Settings are always visible. When visibility rules fail, the setting is dimmed with an UNAVAILABLE badge, so users know it exists but is not applicable.
**Enablement rules**: Grayed out (disabled) when rules fail. Frontend shows a contextual badge explaining why.
**Capability fields** (referenced in rules): `has_longitudinal_control`, `has_icbm`, `icbm_available`, `torque_allowed`, `brand`, `pcm_cruise`, `alpha_long_available`, `steer_control_type`, `enable_bsm`, `is_release`, `is_sp_release`, `is_development`, `tesla_has_vehicle_bus`, `has_stop_and_go`, `stock_longitudinal`
---
## How To
### Add a new toggle
1. Register in `common/params_keys.h`:
```cpp
{"MyToggle", {PERSISTENT | BACKUP, BOOL}},
```
2. Add to `settings_ui.json` in the right panel/section `items` array:
```json
{
"key": "MyToggle",
"widget": "toggle",
"title": "My Feature",
"description": "What this feature does.",
"enablement": [{"type": "offroad_only"}]
}
```
If the toggle requires an onroad cycle (system restart) to take effect:
```json
{
"key": "MyToggle",
"widget": "toggle",
"title": "My Feature",
"description": "What this feature does.",
"needs_onroad_cycle": true,
"enablement": [{"type": "offroad_only"}]
}
```
3. Validate: `python sunnypilot/sunnylink/tools/validate_settings_ui.py`
### Add a multi-button selector
```json
{
"key": "MySelector",
"widget": "multiple_button",
"title": "Mode",
"options": [
{"value": 0, "label": "Off"},
{"value": 1, "label": "On"},
{"value": 2, "label": "Auto"}
]
}
```
### Add a slider/range
```json
{
"key": "MyRange",
"widget": "option",
"title": "Follow Distance",
"description": "Time gap to lead vehicle.",
"min": 0.5,
"max": 3.0,
"step": 0.1,
"unit": "seconds"
}
```
### Add a slider with metric/imperial unit
For speed or distance values that change based on the user's `IsMetric` preference:
```json
{
"key": "MinSpeed",
"widget": "option",
"title": "Minimum Speed",
"min": 0,
"max": 100,
"step": 5,
"unit": {"metric": "km/h", "imperial": "mph"}
}
```
The frontend resolves the correct unit string based on the device's `IsMetric` param value. Static units (like `"seconds"`, `"m/s²"`) remain plain strings.
### Add a title with dynamic suffix
Use `title_param_suffix` to append a param value to the title:
```json
{
"key": "FollowDistance",
"widget": "option",
"title": "Follow Distance",
"title_param_suffix": {
"param": "IsMetric",
"values": {"0": "mph", "1": "km/h"}
},
"min": 0.5,
"max": 3.0,
"step": 0.1
}
```
The title will display as "Follow Distance: mph" or "Follow Distance: km/h" based on the `IsMetric` param value.
### Add a device-only (read-only) setting
Use `blocked: true` for settings that cannot be modified remotely:
```json
{
"key": "OnroadCyclePendingRemote",
"widget": "info",
"title": "Pending Remote Cycle",
"blocked": true
}
```
The frontend will display this as read-only and prevent any changes.
### Add a dropdown
```json
{
"key": "MyDropdown",
"widget": "option",
"title": "Recording Quality",
"options": [
{"value": 0, "label": "Low (720p)"},
{"value": 1, "label": "Medium (1080p)"},
{"value": 2, "label": "High (4K)"}
]
}
```
### Per-option enablement
Individual options within `multiple_button` or `option` widgets can have their own enablement rules:
```json
{
"key": "MadsSteeringMode",
"widget": "multiple_button",
"title": "Steering Mode on Brake Pedal",
"options": [
{
"value": 0,
"label": "Remain Active",
"enablement": [{"type": "capability", "field": "brand", "equals": "tesla"}]
},
{
"value": 1,
"label": "Pause",
"enablement": [{"type": "offroad_only"}]
}
]
}
```
When an option's enablement fails, that option is grayed out (disabled) but still visible.
### Show only when another setting is on
```json
{
"key": "ChildSetting",
"widget": "toggle",
"title": "Child Feature",
"visibility": [{"type": "param", "key": "ParentToggle", "equals": true}]
}
```
Note: Due to the "dim instead of hide" design, this setting will be dimmed (not hidden) when the rule fails.
### Show only for certain cars
```json
{
"key": "LongFeature",
"widget": "toggle",
"title": "Longitudinal Feature",
"visibility": [{"type": "capability", "field": "has_longitudinal_control", "equals": true}]
}
```
### Mutual exclusion (only one can be on)
```json
{
"key": "FeatureAlpha",
"widget": "toggle",
"title": "Feature Alpha",
"enablement": [{"type": "param", "key": "FeatureBeta", "equals": false}]
},
{
"key": "FeatureBeta",
"widget": "toggle",
"title": "Feature Beta",
"enablement": [{"type": "param", "key": "FeatureAlpha", "equals": false}]
}
```
### Add a new section
```json
{
"id": "my_section",
"title": "My Section",
"description": "Optional subtitle",
"items": [...],
"enablement": [{"type": "capability", "field": "has_longitudinal_control", "equals": true}]
}
```
Sections can have visibility and enablement rules (optional). When section-level rules fail, all items within are dimmed.
### Add section-level enablement
Sections can be conditionally available or enabled via `visibility` or `enablement`:
```json
{
"id": "longitudinal_tuning",
"title": "Longitudinal Tuning",
"description": "Advanced control parameters",
"visibility": [{"type": "capability", "field": "has_longitudinal_control", "equals": true}],
"items": [...]
}
```
### Add a sub-panel (drill-down page)
```json
{
"id": "my_sub",
"label": "Advanced Settings",
"trigger_key": "ParentParam",
"trigger_condition": {"type": "param", "key": "ParentParam", "equals": true},
"items": [...]
}
```
### Add vehicle-specific settings
Add to `vehicle_settings` in `settings_ui.json`:
```json
"rivian": {
"title": "Rivian Settings",
"items": [
{
"key": "RivianFeature",
"widget": "toggle",
"title": "Rivian One Pedal",
"enablement": [{"type": "offroad_only"}]
}
]
}
```
### Change display text
Edit the `title` or `description` on the item in `settings_ui.json`.
### Move a setting between panels
Remove from source panel, add to target panel. Validator catches duplicates.
### Reorder sections
Set `order` field on sections, or reorder the JSON array.
---
### Capability labels and tooltips
The schema response includes `capability_labels` which map capability field names to human-readable descriptions. These are used by the frontend to show contextual tooltips when a capability rule prevents a setting from being used.
The device defines these labels in `capabilities.py:CAPABILITY_LABELS`. Examples:
- `has_longitudinal_control` → "sunnypilot longitudinal control"
- `torque_allowed` → "torque steering (not available for angle steering vehicles)"
- `brand` → "Vehicle brand"
### Centralized param enforcement
The device-side UI enforces capability constraints in `selfdrive/ui/sunnypilot/ui_state.py:_enforce_sp_constraints()`. This method removes incompatible params based on car capabilities, and should be the single source of truth for such constraints.
**Settings layouts should NOT duplicate these params.remove() calls.** Instead, they should rely on schema rules and the centralized enforcement. This prevents duplicate logic and ensures consistency.
Example constraints in `_enforce_sp_constraints()`:
- Angle steering cars: remove `EnforceTorqueControl` and `NeuralNetworkLateralControl`
- No CarParams: remove all car-dependent params
- No longitudinal: remove `ExperimentalMode`
- No ICBM: remove `IntelligentCruiseButtonManagement`

View File

@@ -1071,6 +1071,10 @@
"title": "Panda Som Reset Triggered",
"description": ""
},
"ParamsVersion": {
"title": "Params Version",
"description": ""
},
"PlanplusControl": {
"title": "Plan Plus Controls",
"description": "Adjust planplus model recentering strength. The higher this number the more aggressively the model will recover to lanecenter, too high and it will ping-pong",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,512 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://sunnypilot.com/schemas/settings_ui.schema.json",
"title": "sunnypilot Settings UI Schema",
"description": "Defines the structure of the sunnypilot settings UI panels, items, rules, and vehicle-specific settings.",
"type": "object",
"required": ["schema_version", "panels", "vehicle_settings"],
"additionalProperties": false,
"properties": {
"$schema": {
"type": "string",
"description": "JSON Schema reference for editor support."
},
"schema_version": {
"type": "string",
"description": "Version of the settings UI schema format.",
"examples": ["1.0"]
},
"panels": {
"type": "array",
"description": "Top-level settings panels displayed in the UI.",
"items": {
"$ref": "#/$defs/Panel"
}
},
"vehicle_settings": {
"type": "object",
"description": "Brand-keyed vehicle-specific settings. Each key is a car brand (e.g. 'hyundai', 'toyota').",
"additionalProperties": {
"$ref": "#/$defs/VehicleBrandSettings"
}
}
},
"$defs": {
"Panel": {
"type": "object",
"description": "A top-level settings panel (tab) in the UI.",
"required": ["id", "label", "icon", "order"],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for this panel."
},
"label": {
"type": "string",
"description": "Display label shown in the UI."
},
"icon": {
"type": "string",
"description": "Icon identifier for this panel."
},
"order": {
"type": "integer",
"description": "Sort order for panel display.",
"minimum": 0
},
"description": {
"type": "string",
"description": "Optional description shown below the panel label."
},
"remote_configurable": {
"type": "boolean",
"description": "Whether this panel's settings can be changed remotely via sunnylink.",
"default": false
},
"sections": {
"type": "array",
"description": "Grouped sections within this panel.",
"items": {
"$ref": "#/$defs/PanelSection"
}
},
"items": {
"type": "array",
"description": "Settings items directly in this panel (no section grouping).",
"items": {
"$ref": "#/$defs/SchemaItem"
}
},
"sub_panels": {
"type": "array",
"description": "Nested sub-panels triggered by a setting.",
"items": {
"$ref": "#/$defs/SubPanel"
}
}
}
},
"PanelSection": {
"type": "object",
"description": "A grouped section within a panel.",
"required": ["id", "title"],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for this section."
},
"title": {
"type": "string",
"description": "Display title for this section."
},
"description": {
"type": "string",
"description": "Optional description shown below the section title."
},
"order": {
"type": "integer",
"description": "Sort order within the parent panel.",
"minimum": 0
},
"visibility": {
"type": "array",
"description": "Rules that determine whether this section is visible. All rules must pass.",
"items": {
"$ref": "#/$defs/Rule"
}
},
"enablement": {
"type": "array",
"description": "Rules that determine whether items in this section are enabled. All rules must pass.",
"items": {
"$ref": "#/$defs/Rule"
}
},
"attestation_required": {
"type": "boolean",
"description": "When true, the UI must show an attestation modal before any write to items in this section.",
"default": false
},
"items": {
"type": "array",
"description": "Settings items within this section.",
"items": {
"$ref": "#/$defs/SchemaItem"
}
},
"sub_panels": {
"type": "array",
"description": "Nested sub-panels within this section.",
"items": {
"$ref": "#/$defs/SubPanel"
}
}
}
},
"VehicleBrandSettings": {
"type": "object",
"description": "Brand-specific settings group inside vehicle_settings.",
"required": ["items"],
"additionalProperties": false,
"properties": {
"title": {
"type": "string",
"description": "Display title for this brand's settings group."
},
"description": {
"type": "string",
"description": "Optional description shown below the brand title."
},
"items": {
"type": "array",
"description": "Settings items for this brand.",
"items": {
"$ref": "#/$defs/SchemaItem"
}
}
}
},
"SchemaItem": {
"type": "object",
"description": "A single settings item (toggle, option selector, button group, etc.).",
"required": ["key", "widget"],
"additionalProperties": false,
"properties": {
"key": {
"type": "string",
"description": "The param key this item reads/writes."
},
"widget": {
"type": "string",
"description": "The UI widget type to render.",
"enum": ["toggle", "option", "multiple_button", "button", "info"]
},
"title": {
"type": "string",
"description": "Override display title (defaults to metadata lookup by key)."
},
"description": {
"type": "string",
"description": "Override description text."
},
"options": {
"type": "array",
"description": "Available options for 'option' or 'multiple_button' widgets.",
"items": {
"$ref": "#/$defs/SchemaOption"
}
},
"min": {
"type": "number",
"description": "Minimum value for numeric option widgets."
},
"max": {
"type": "number",
"description": "Maximum value for numeric option widgets."
},
"step": {
"type": "number",
"description": "Step increment for numeric option widgets."
},
"unit": {
"oneOf": [
{
"type": "string",
"description": "Static unit label (e.g. 'seconds', 'm/s²')."
},
{
"type": "object",
"description": "Dynamic unit that changes based on IsMetric param.",
"required": ["metric", "imperial"],
"additionalProperties": false,
"properties": {
"metric": {
"type": "string",
"description": "Unit label when IsMetric is true (e.g. 'km/h')."
},
"imperial": {
"type": "string",
"description": "Unit label when IsMetric is false (e.g. 'mph')."
}
}
}
],
"description": "Unit label for numeric values. Use a string for static units or an object with metric/imperial variants for units that depend on the IsMetric param."
},
"value_map": {
"type": "object",
"description": "Maps stored values to display labels.",
"additionalProperties": {
"type": "string"
}
},
"visibility": {
"type": "array",
"description": "Rules that determine whether this item is visible. All rules must pass.",
"items": {
"$ref": "#/$defs/Rule"
}
},
"enablement": {
"type": "array",
"description": "Rules that determine whether this item is enabled/interactive. All rules must pass.",
"items": {
"$ref": "#/$defs/Rule"
}
},
"sub_items": {
"type": "array",
"description": "Child items nested under this item (e.g. options revealed by a toggle).",
"items": {
"$ref": "#/$defs/SchemaItem"
}
},
"action": {
"type": "string",
"description": "Action identifier for button widgets."
},
"title_param_suffix": {
"type": "object",
"description": "Renders an extra suffix in the item title chosen by the value of another param.",
"required": ["param", "values"],
"additionalProperties": false,
"properties": {
"param": {
"type": "string",
"description": "Param key whose value selects the suffix label."
},
"values": {
"type": "object",
"description": "Map from stringified param value to suffix label.",
"additionalProperties": {
"type": "string"
}
}
}
},
"needs_onroad_cycle": {
"type": "boolean",
"description": "When true, the device must cycle onroad/offroad for the new value to take effect.",
"default": false
},
"blocked": {
"type": "boolean",
"description": "When true, this item is treated as DEVICE_ONLY and the dashboard must not write it remotely.",
"default": false
},
"requires_attestation": {
"type": "boolean",
"description": "When true, writes to this item require an explicit per-write confirmation modal.",
"default": false
}
}
},
"SubPanel": {
"type": "object",
"description": "A nested panel that opens when triggered by a parent item.",
"required": ["id", "label", "trigger_key"],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for this sub-panel."
},
"label": {
"type": "string",
"description": "Display label for the sub-panel header."
},
"trigger_key": {
"type": "string",
"description": "The param key that triggers opening this sub-panel."
},
"trigger_condition": {
"$ref": "#/$defs/Rule",
"description": "Optional rule that must evaluate to true for the sub-panel trigger to be active."
},
"items": {
"type": "array",
"description": "Settings items within this sub-panel.",
"items": {
"$ref": "#/$defs/SchemaItem"
}
}
}
},
"SchemaOption": {
"type": "object",
"description": "A selectable option for option/multiple_button widgets.",
"required": ["value", "label"],
"additionalProperties": false,
"properties": {
"value": {
"oneOf": [
{ "type": "number" },
{ "type": "string" }
],
"description": "The stored value when this option is selected."
},
"label": {
"type": "string",
"description": "The display label for this option."
},
"enablement": {
"type": "array",
"description": "Rules that determine whether this option is selectable. All rules must pass.",
"items": {
"$ref": "#/$defs/Rule"
}
}
}
},
"Rule": {
"description": "A visibility or enablement rule. Discriminated union on the 'type' field.",
"oneOf": [
{ "$ref": "#/$defs/RuleOffroadOnly" },
{ "$ref": "#/$defs/RuleNotEngaged" },
{ "$ref": "#/$defs/RuleCapability" },
{ "$ref": "#/$defs/RuleParam" },
{ "$ref": "#/$defs/RuleParamCompare" },
{ "$ref": "#/$defs/RuleNot" },
{ "$ref": "#/$defs/RuleAny" },
{ "$ref": "#/$defs/RuleAll" }
]
},
"RuleOffroadOnly": {
"type": "object",
"description": "Rule that passes only when the device is offroad.",
"required": ["type"],
"additionalProperties": false,
"properties": {
"type": {
"const": "offroad_only"
}
}
},
"RuleNotEngaged": {
"type": "object",
"description": "Rule that passes when the vehicle is not engaged (matches Raylib `engaged = started AND (selfdriveState.enabled OR selfdriveStateSP.mads.enabled)`).",
"required": ["type"],
"additionalProperties": false,
"properties": {
"type": {
"const": "not_engaged"
}
}
},
"RuleCapability": {
"type": "object",
"description": "Rule that checks a vehicle capability field against an expected value.",
"required": ["type", "field", "equals"],
"additionalProperties": false,
"properties": {
"type": {
"const": "capability"
},
"field": {
"type": "string",
"description": "The capability field name to check."
},
"equals": {
"description": "The expected value to match against."
}
}
},
"RuleParam": {
"type": "object",
"description": "Rule that checks a param value against an expected value.",
"required": ["type", "key", "equals"],
"additionalProperties": false,
"properties": {
"type": {
"const": "param"
},
"key": {
"type": "string",
"description": "The param key to read."
},
"equals": {
"description": "The expected value to match against."
}
}
},
"RuleParamCompare": {
"type": "object",
"description": "Rule that compares a numeric param value using a comparison operator.",
"required": ["type", "key", "op", "value"],
"additionalProperties": false,
"properties": {
"type": {
"const": "param_compare"
},
"key": {
"type": "string",
"description": "The param key to read."
},
"op": {
"type": "string",
"description": "Comparison operator.",
"enum": [">", "<", ">=", "<="]
},
"value": {
"type": "number",
"description": "The numeric value to compare against."
}
}
},
"RuleNot": {
"type": "object",
"description": "Rule that negates a single child condition.",
"required": ["type", "condition"],
"additionalProperties": false,
"properties": {
"type": {
"const": "not"
},
"condition": {
"$ref": "#/$defs/Rule",
"description": "The rule to negate."
}
}
},
"RuleAny": {
"type": "object",
"description": "Rule that passes if ANY of the child conditions pass (logical OR).",
"required": ["type", "conditions"],
"additionalProperties": false,
"properties": {
"type": {
"const": "any"
},
"conditions": {
"type": "array",
"description": "Child rules; at least one must pass.",
"items": {
"$ref": "#/$defs/Rule"
},
"minItems": 1
}
}
},
"RuleAll": {
"type": "object",
"description": "Rule that passes only if ALL child conditions pass (logical AND).",
"required": ["type", "conditions"],
"additionalProperties": false,
"properties": {
"type": {
"const": "all"
},
"conditions": {
"type": "array",
"description": "Child rules; all must pass.",
"items": {
"$ref": "#/$defs/Rule"
},
"minItems": 1
}
}
}
}
}

View File

@@ -0,0 +1,93 @@
"""
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.
Sentinel tests for the capabilities payload contract. PROTOCOL_VERSION is the
wire-protocol version observable by the dashboard; bumping it is a breaking
change and must be intentional. KNOWN_PROTOCOL_VERSIONS pins the set we
explicitly support — when the constant is bumped, this list must be edited in
the same commit so the bump shows up in code review.
"""
from __future__ import annotations
import pytest
from openpilot.sunnypilot.sunnylink.capabilities import (
CAPABILITY_DEFAULTS,
CAPABILITY_FIELDS,
CAPABILITY_LABELS,
PROTOCOL_VERSION,
generate_capabilities,
)
KNOWN_PROTOCOL_VERSIONS = (1,)
LATEST_KNOWN = max(KNOWN_PROTOCOL_VERSIONS)
@pytest.fixture(scope="module")
def caps():
return generate_capabilities()
class TestProtocolVersion:
def test_protocol_version_in_capability_fields(self):
assert "protocol_version" in CAPABILITY_FIELDS
def test_protocol_version_has_label(self):
assert "protocol_version" in CAPABILITY_LABELS
def test_protocol_version_default_is_set(self):
assert CAPABILITY_DEFAULTS.get("protocol_version") == PROTOCOL_VERSION
def test_protocol_version_emitted(self, caps):
assert "protocol_version" in caps
assert isinstance(caps["protocol_version"], int)
assert caps["protocol_version"] >= 1
def test_protocol_version_matches_constant(self, caps):
assert caps["protocol_version"] == PROTOCOL_VERSION
def test_protocol_version_is_known(self):
"""Sentinel against accidental bumps. If you intentionally bumped the
protocol, edit KNOWN_PROTOCOL_VERSIONS in the same commit."""
assert PROTOCOL_VERSION in KNOWN_PROTOCOL_VERSIONS, (
f"PROTOCOL_VERSION={PROTOCOL_VERSION} is not in KNOWN_PROTOCOL_VERSIONS={KNOWN_PROTOCOL_VERSIONS}. "
"If this bump is intentional, add it to KNOWN_PROTOCOL_VERSIONS."
)
def test_protocol_version_matches_latest_known(self):
assert PROTOCOL_VERSION == LATEST_KNOWN, (
"Test invariant: PROTOCOL_VERSION must equal max(KNOWN_PROTOCOL_VERSIONS)."
)
class TestOpaquePerBrandFlags:
def test_subaru_has_sng_field_present(self):
assert "subaru_has_sng" in CAPABILITY_FIELDS
def test_hyundai_alpha_long_available_field_present(self):
assert "hyundai_alpha_long_available" in CAPABILITY_FIELDS
def test_subaru_has_sng_default_false(self, caps):
assert caps["subaru_has_sng"] is False
def test_hyundai_alpha_long_available_default_false(self, caps):
assert caps["hyundai_alpha_long_available"] is False
class TestCapabilitiesShape:
def test_all_fields_present(self, caps):
for field in CAPABILITY_FIELDS:
assert field in caps, f"capabilities missing {field}"
def test_all_fields_have_labels(self):
for field in CAPABILITY_FIELDS:
assert field in CAPABILITY_LABELS, f"CAPABILITY_LABELS missing {field}"
def test_string_defaults_are_strings(self, caps):
assert isinstance(caps["brand"], str)
assert isinstance(caps["steer_control_type"], str)
assert isinstance(caps["device_type"], str)

View File

@@ -0,0 +1,222 @@
"""
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.
Per-bug regression tests for the Raylib-vs-schema parity audit. Each test
isolates one of the gating bugs that the design-overhaul branch fixes so a
future regression is loud and obvious. These tests are intentionally narrow
and additive — they do not replace the broader test_settings_schema.py.
"""
from __future__ import annotations
import json
import os
from typing import Any
import pytest
from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import (
DEFINITION_PATH,
TORQUE_VERSIONS_PATH,
_build_torque_options,
_load_torque_versions,
generate_schema,
)
SCHEMA_VALIDATOR_PATH = os.path.join(os.path.dirname(DEFINITION_PATH), "settings_ui.schema.json")
def _walk_items(schema: dict[str, Any]):
"""Yield every item dict reachable from the schema (panels, sections, sub_panels, sub_items, vehicle_settings)."""
def _yield(item: dict[str, Any]):
yield item
for sub in item.get("sub_items", []):
yield from _yield(sub)
for panel in schema.get("panels", []):
for section in panel.get("sections", []):
for item in section.get("items", []):
yield from _yield(item)
for sp in section.get("sub_panels", []):
for item in sp.get("items", []):
yield from _yield(item)
for item in panel.get("items", []):
yield from _yield(item)
for sp in panel.get("sub_panels", []):
for item in sp.get("items", []):
yield from _yield(item)
for brand in schema.get("vehicle_settings", {}).values():
items = brand.get("items", []) if isinstance(brand, dict) else brand
for item in items:
yield from _yield(item)
def _find_item(schema: dict[str, Any], key: str) -> dict[str, Any] | None:
for item in _walk_items(schema):
if item.get("key") == key:
return item
return None
def _find_section(schema: dict[str, Any], panel_id: str, section_id: str) -> dict[str, Any] | None:
for panel in schema.get("panels", []):
if panel.get("id") != panel_id:
continue
for section in panel.get("sections", []):
if section.get("id") == section_id:
return section
return None
def _flatten_rule_types(rules: list[dict[str, Any]] | None) -> set[str]:
out: set[str] = set()
def _walk(rule: dict[str, Any]) -> None:
out.add(rule.get("type", ""))
if rule.get("type") == "not" and "condition" in rule:
_walk(rule["condition"])
elif rule.get("type") in ("any", "all"):
for c in rule.get("conditions", []):
_walk(c)
for rule in rules or []:
_walk(rule)
return out
def _references_capability_field(rules: list[dict[str, Any]] | None, field: str) -> bool:
found = False
def _walk(rule: dict[str, Any]) -> None:
nonlocal found
if rule.get("type") == "capability" and rule.get("field") == field:
found = True
elif rule.get("type") == "not" and "condition" in rule:
_walk(rule["condition"])
elif rule.get("type") in ("any", "all"):
for c in rule.get("conditions", []):
_walk(c)
for rule in rules or []:
_walk(rule)
return found
@pytest.fixture(scope="module")
def schema():
return generate_schema()
class TestMadsBrandGates:
def test_mads_main_cruise_has_brand_gate(self, schema):
"""MadsMainCruiseAllowed must be disabled on rivian + tesla-no-bus to match Raylib _mads_limited_settings."""
item = _find_item(schema, "MadsMainCruiseAllowed")
assert item is not None
assert _references_capability_field(item.get("enablement"), "brand")
assert _references_capability_field(item.get("enablement"), "tesla_has_vehicle_bus")
def test_mads_unified_engagement_has_brand_gate(self, schema):
"""MadsUnifiedEngagementMode must mirror MadsMainCruiseAllowed brand-gate."""
item = _find_item(schema, "MadsUnifiedEngagementMode")
assert item is not None
assert _references_capability_field(item.get("enablement"), "brand")
assert _references_capability_field(item.get("enablement"), "tesla_has_vehicle_bus")
class TestTestManeuversSection:
def test_lateral_maneuver_mode_in_test_maneuvers(self, schema):
section = _find_section(schema, "developer", "test_maneuvers")
assert section is not None, "developer.test_maneuvers section missing"
keys = {item["key"] for item in section.get("items", [])}
assert "LateralManeuverMode" in keys
assert "LongitudinalManeuverMode" in keys
def test_test_maneuvers_section_requires_attestation(self, schema):
section = _find_section(schema, "developer", "test_maneuvers")
assert section is not None
assert section.get("attestation_required") is True
def test_test_maneuvers_section_visibility_gate(self, schema):
section = _find_section(schema, "developer", "test_maneuvers")
assert section is not None
visibility = section.get("visibility")
assert visibility, "test_maneuvers must have visibility gate"
vis_refs = json.dumps(visibility)
assert "is_development" in vis_refs
assert "is_sp_release" in vis_refs
enablement = section.get("enablement") or []
enable_refs = json.dumps(enablement)
assert "ShowAdvancedControls" in enable_refs, \
"test_maneuvers must gate ShowAdvancedControls via enablement (disabled, not hidden)"
class TestValidator:
def test_validator_accepts_real_json(self):
"""settings_ui.json must validate against settings_ui.schema.json."""
jsonschema = pytest.importorskip("jsonschema")
with open(DEFINITION_PATH) as f:
data = json.load(f)
with open(SCHEMA_VALIDATOR_PATH) as f:
validator = json.load(f)
jsonschema.validate(instance=data, schema=validator)
class TestTorqueOptionGeneration:
def test_torque_versions_match_generated_options(self, schema):
versions = _load_torque_versions()
assert versions, "latcontrol_torque_versions.json must have at least one version"
expected = _build_torque_options(versions)
item = _find_item(schema, "TorqueControlTune")
assert item is not None, "TorqueControlTune item must be present"
assert item.get("options") == expected
def test_torque_versions_path_resolves(self):
assert os.path.exists(TORQUE_VERSIONS_PATH), (
f"latcontrol_torque_versions.json not found at {TORQUE_VERSIONS_PATH}"
)
class TestReleaseBranchGates:
@pytest.mark.parametrize("key", [
"JoystickDebugMode",
"AlphaLongitudinalEnabled",
"EnableGithubRunner",
"QuickBootToggle",
])
def test_sp_dev_items_gate_on_is_sp_release(self, schema, key):
"""SP-side dev items must hide on EITHER release branch (matches Raylib _is_release_branch = is_release OR IsReleaseSpBranch)."""
item = _find_item(schema, key)
assert item is not None, f"{key} not found in schema"
rules = (item.get("visibility") or []) + (item.get("enablement") or [])
assert _references_capability_field(rules, "is_release"), f"{key} missing is_release gate"
assert _references_capability_field(rules, "is_sp_release"), f"{key} missing is_sp_release gate"
class TestSpuriousOffroadGatesDropped:
def test_disengage_on_accelerator_has_no_offroad_only(self, schema):
item = _find_item(schema, "DisengageOnAccelerator")
assert item is not None
assert "offroad_only" not in _flatten_rule_types(item.get("enablement"))
def test_dynamic_experimental_has_no_offroad_only(self, schema):
item = _find_item(schema, "DynamicExperimentalControl")
assert item is not None
assert "offroad_only" not in _flatten_rule_types(item.get("enablement"))
class TestNotEngagedReplacement:
@pytest.mark.parametrize("key", [
"AlphaLongitudinalEnabled",
"ToyotaEnforceStockLongitudinal",
"ToyotaStopAndGoHack",
])
def test_offroad_only_replaced_with_not_engaged(self, schema, key):
"""These items can be changed safely when not engaged; must not require full offroad."""
item = _find_item(schema, key)
assert item is not None, f"{key} not found"
rule_types = _flatten_rule_types(item.get("enablement"))
assert "offroad_only" not in rule_types, f"{key} still uses offroad_only"
assert "not_engaged" in rule_types, f"{key} missing not_engaged"

View File

@@ -0,0 +1,354 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import json
import pytest
from openpilot.common.params import Params
from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import (
SCHEMA_VERSION,
generate_schema,
generate_schema_json,
collect_all_keys,
collect_capability_refs,
)
from openpilot.sunnypilot.sunnylink.capabilities import CAPABILITY_FIELDS
VALID_WIDGET_TYPES = {"toggle", "option", "multiple_button", "button", "info"}
VALID_RULE_TYPES = {"offroad_only", "not_engaged", "capability", "param", "param_compare", "not", "any", "all"}
VALID_COMPARE_OPS = {">", "<", ">=", "<="}
MAX_ALLOWED_MISSING_TITLES = 0 # All items must have titles (metadata is inline in settings_ui.json)
def _iter_panel_items(panel: dict):
"""Yield top-level items reachable from a panel: panel.items, panel.sub_panels.items,
panel.sections.items, panel.sections.sub_panels.items. Does not recurse sub_items."""
for item in panel.get("items", []):
yield item
for sp in panel.get("sub_panels", []):
for item in sp.get("items", []):
yield item
for section in panel.get("sections", []):
for item in section.get("items", []):
yield item
for sp in section.get("sub_panels", []):
for item in sp.get("items", []):
yield item
def _iter_all_sub_panels(panel: dict):
"""Yield every sub_panel (panel-level + section-nested)."""
yield from panel.get("sub_panels", [])
for section in panel.get("sections", []):
yield from section.get("sub_panels", [])
def _brand_items(brand_data) -> list[dict]:
"""vehicle_settings[brand] is dict {title, description?, items}; tolerate raw list."""
if isinstance(brand_data, dict):
return brand_data.get("items", [])
if isinstance(brand_data, list):
return brand_data
return []
@pytest.fixture(scope="module")
def schema():
return generate_schema()
@pytest.fixture(scope="module")
def all_param_keys():
"""All keys registered in the device param store."""
return {k.decode("utf-8") for k in Params().all_keys()}
class TestSchemaStructure:
def test_schema_is_valid_json(self):
"""Schema can be serialized to valid JSON."""
raw = generate_schema_json()
parsed = json.loads(raw)
assert isinstance(parsed, dict)
def test_has_required_top_level_fields(self, schema):
assert "schema_version" in schema
assert schema["schema_version"] == SCHEMA_VERSION
assert "generated_at" in schema
assert "panels" in schema
assert "vehicle_settings" in schema
assert "capability_fields" in schema
def test_panels_are_list(self, schema):
assert isinstance(schema["panels"], list)
assert len(schema["panels"]) > 0
def test_all_panels_have_required_fields(self, schema):
for panel in schema["panels"]:
assert "id" in panel, f"Panel missing 'id': {panel}"
assert "label" in panel, f"Panel {panel.get('id')} missing 'label'"
assert "order" in panel, f"Panel {panel.get('id')} missing 'order'"
has_sections = "sections" in panel
has_items = "items" in panel
assert has_sections or has_items, \
f"Panel {panel['id']} must have 'sections' or 'items'"
if has_sections:
assert isinstance(panel["sections"], list)
for sec in panel["sections"]:
assert "id" in sec, f"Section in panel {panel['id']} missing 'id'"
assert "items" in sec, f"Section {panel['id']}.{sec.get('id')} missing 'items'"
assert isinstance(sec["items"], list)
if has_items:
assert isinstance(panel["items"], list)
def test_all_items_have_key_and_widget(self, schema):
for panel in schema["panels"]:
for item in _iter_panel_items(panel):
assert "key" in item, f"Item in panel {panel['id']} missing 'key'"
assert "widget" in item, f"Item {item.get('key')} missing 'widget'"
assert item["widget"] in VALID_WIDGET_TYPES, \
f"Item {item['key']} has invalid widget type: {item['widget']}"
def test_sub_panel_items_have_key_and_widget(self, schema):
for panel in schema["panels"]:
for sp in _iter_all_sub_panels(panel):
assert "id" in sp
assert "items" in sp
for item in sp["items"]:
assert "key" in item
assert "widget" in item
assert item["widget"] in VALID_WIDGET_TYPES
def test_vehicle_settings_structure(self, schema):
vs = schema["vehicle_settings"]
assert isinstance(vs, dict)
for brand, data in vs.items():
assert isinstance(brand, str)
assert isinstance(data, dict), \
f"vehicle_settings[{brand}] must be a dict {{title, items, ...}}"
assert "items" in data, f"vehicle_settings[{brand}] missing 'items'"
assert isinstance(data["items"], list)
for item in data["items"]:
assert "key" in item, f"Vehicle item for {brand} missing 'key'"
assert "widget" in item, f"Vehicle item {item.get('key')} missing 'widget'"
def test_no_duplicate_keys_across_panels(self, schema):
"""Each param key should appear in at most one panel to avoid double-rendering."""
seen: dict[str, str] = {} # key -> panel_id
for panel in schema["panels"]:
for item in _iter_panel_items(panel):
key = item["key"]
if key in seen:
pytest.fail(f"Key '{key}' appears in both panel '{seen[key]}' and '{panel['id']}'")
seen[key] = panel["id"]
for sub in item.get("sub_items", []):
sub_key = sub["key"]
if sub_key in seen:
pytest.fail(f"Sub-item key '{sub_key}' appears in both '{seen[sub_key]}' and '{panel['id']}'")
seen[sub_key] = panel["id"]
class TestSchemaCoverage:
def test_all_schema_keys_exist_in_params(self, schema, all_param_keys):
"""Every key referenced in the schema must exist in Params().all_keys()."""
schema_keys = collect_all_keys(schema)
missing = schema_keys - all_param_keys
assert not missing, f"Schema references keys not in Params: {missing}"
def test_all_capability_fields_are_declared(self, schema):
"""Every capability field used in rules must be in capability_fields."""
declared = set(schema["capability_fields"])
referenced = collect_capability_refs(schema)
undeclared = referenced - declared
assert not undeclared, f"Rules reference undeclared capability fields: {undeclared}"
def test_capability_fields_match_constant(self, schema):
"""Schema capability_fields must match the CAPABILITY_FIELDS constant."""
assert set(schema["capability_fields"]) == set(CAPABILITY_FIELDS)
class TestRuleWellFormedness:
def _validate_rule(self, rule: dict, context: str = ""):
"""Recursively validate a single rule dict."""
assert "type" in rule, f"Rule missing 'type' in {context}"
rtype = rule["type"]
assert rtype in VALID_RULE_TYPES, f"Invalid rule type '{rtype}' in {context}"
if rtype == "capability":
assert "field" in rule, f"Capability rule missing 'field' in {context}"
assert "equals" in rule, f"Capability rule missing 'equals' in {context}"
elif rtype == "param":
assert "key" in rule, f"Param rule missing 'key' in {context}"
assert "equals" in rule, f"Param rule missing 'equals' in {context}"
elif rtype == "param_compare":
assert "key" in rule, f"Param compare rule missing 'key' in {context}"
assert "op" in rule, f"Param compare rule missing 'op' in {context}"
assert rule["op"] in VALID_COMPARE_OPS, f"Invalid op '{rule['op']}' in {context}"
assert "value" in rule, f"Param compare rule missing 'value' in {context}"
elif rtype == "not":
assert "condition" in rule, f"Not rule missing 'condition' in {context}"
self._validate_rule(rule["condition"], context=f"{context} > not")
elif rtype in ("any", "all"):
assert "conditions" in rule, f"{rtype} rule missing 'conditions' in {context}"
assert isinstance(rule["conditions"], list)
for c in rule["conditions"]:
self._validate_rule(c, context=f"{context} > {rtype}")
def _validate_items(self, items: list[dict], context: str):
for item in items:
key = item.get("key", "unknown")
for rules_field in ("visibility", "enablement"):
rules = item.get(rules_field)
if rules:
assert isinstance(rules, list), f"{key}.{rules_field} must be a list"
for rule in rules:
self._validate_rule(rule, context=f"{context}.{key}.{rules_field}")
for sub in item.get("sub_items", []):
self._validate_items([sub], context=f"{context}.{key}")
def _validate_section_rules(self, section: dict, context: str):
for rules_field in ("visibility", "enablement"):
rules = section.get(rules_field) or []
for rule in rules:
self._validate_rule(rule, context=f"{context}.{rules_field}")
def test_all_panel_rules_well_formed(self, schema):
for panel in schema["panels"]:
self._validate_items(list(_iter_panel_items(panel)), context=f"panel:{panel['id']}")
for sp in _iter_all_sub_panels(panel):
self._validate_items(sp["items"], context=f"subpanel:{sp['id']}")
for section in panel.get("sections", []):
self._validate_section_rules(section, context=f"section:{panel['id']}.{section['id']}")
def test_all_vehicle_rules_well_formed(self, schema):
for brand, data in schema["vehicle_settings"].items():
self._validate_items(_brand_items(data), context=f"vehicle:{brand}")
def test_no_self_referencing_visibility(self, schema):
"""An item's visibility/enablement rules should not depend on its own key."""
def _check_self_ref(item: dict, rules_field: str):
key = item.get("key")
for rule in item.get(rules_field, []):
if rule.get("type") == "param" and rule.get("key") == key:
pytest.fail(f"Item {key} has self-referencing {rules_field} rule")
for panel in schema["panels"]:
for item in _iter_panel_items(panel):
_check_self_ref(item, "visibility")
_check_self_ref(item, "enablement")
for brand_data in schema.get("vehicle_settings", {}).values():
for item in _brand_items(brand_data):
_check_self_ref(item, "visibility")
_check_self_ref(item, "enablement")
class TestKnownPanels:
def test_expected_panels_exist(self, schema):
panel_ids = {p["id"] for p in schema["panels"]}
expected = {"steering", "cruise", "display", "visuals", "device", "software", "developer"}
assert expected.issubset(panel_ids), f"Missing panels: {expected - panel_ids}"
def test_mads_sub_panel_exists(self, schema):
steering = next(p for p in schema["panels"] if p["id"] == "steering")
sub_ids = {sp["id"] for sp in _iter_all_sub_panels(steering)}
assert "mads_settings" in sub_ids
def test_mutual_exclusion_torque_nnlc(self, schema):
"""EnforceTorqueControl and NNLC should have cross-param rules."""
torque = nnlc = None
for panel in schema["panels"]:
for item in _iter_panel_items(panel):
if item["key"] == "EnforceTorqueControl":
torque = item
elif item["key"] == "NeuralNetworkLateralControl":
nnlc = item
assert torque is not None, "EnforceTorqueControl item missing"
assert nnlc is not None, "NeuralNetworkLateralControl item missing"
torque_enable_keys = {r.get("key") for r in torque.get("enablement", []) if r.get("type") == "param"}
assert "NeuralNetworkLateralControl" in torque_enable_keys
nnlc_enable_keys = {r.get("key") for r in nnlc.get("enablement", []) if r.get("type") == "param"}
assert "EnforceTorqueControl" in nnlc_enable_keys
class TestKnownVehicleSettings:
def test_hyundai_has_longitudinal_tuning(self, schema):
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("hyundai"))}
assert "HyundaiLongitudinalTuning" in keys
def test_toyota_has_enforce_stock_and_stop_go(self, schema):
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("toyota"))}
assert "ToyotaEnforceStockLongitudinal" in keys
assert "ToyotaStopAndGoHack" in keys
def test_tesla_has_coop_steering(self, schema):
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("tesla"))}
assert "TeslaCoopSteering" in keys
def test_subaru_has_stop_and_go(self, schema):
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("subaru"))}
assert "SubaruStopAndGo" in keys
assert "SubaruStopAndGoManualParkingBrake" in keys
class TestItemCompleteness:
def _collect_all_items(self, schema):
"""Collect every item, including sub_items, across panels (sections + flat) + vehicle_settings."""
items = []
for panel in schema["panels"]:
for item in _iter_panel_items(panel):
items.append(item)
for sub in item.get("sub_items", []):
items.append(sub)
for brand_data in schema.get("vehicle_settings", {}).values():
for item in _brand_items(brand_data):
items.append(item)
for sub in item.get("sub_items", []):
items.append(sub)
return items
def test_all_items_have_titles(self, schema):
"""Every item must have a title (metadata is inline, no enrichment fallback)."""
missing = [i["key"] for i in self._collect_all_items(schema) if "title" not in i]
if len(missing) > MAX_ALLOWED_MISSING_TITLES:
pytest.fail(f"Items without titles ({len(missing)}): {missing[:10]}")
def test_no_default_titles(self, schema):
"""No item should have title == key (forces human-readable titles)."""
defaults = [i["key"] for i in self._collect_all_items(schema) if i.get("title") == i["key"]]
assert not defaults, f"Items with default titles (title == key): {defaults}"
def test_options_structure(self, schema):
"""Options must be list of {value, label} dicts."""
for item in self._collect_all_items(schema):
opts = item.get("options")
if opts is None:
continue
assert isinstance(opts, list), f"{item['key']}: options must be a list"
for opt in opts:
assert isinstance(opt, dict), f"{item['key']}: each option must be a dict"
assert "value" in opt, f"{item['key']}: option missing 'value': {opt}"
assert "label" in opt, f"{item['key']}: option missing 'label': {opt}"
def test_numeric_constraints(self, schema):
"""If min/max/step present, all three must be present and min < max."""
for item in self._collect_all_items(schema):
has_min = "min" in item
has_max = "max" in item
has_step = "step" in item
if has_min or has_max or has_step:
assert has_min and has_max and has_step, \
f"{item['key']}: must have all of min/max/step or none"
assert item["min"] < item["max"], \
f"{item['key']}: min ({item['min']}) must be < max ({item['max']})"
def test_known_param_has_options(self, schema):
"""LongitudinalPersonality should have 3 options."""
cruise = next(p for p in schema["panels"] if p["id"] == "cruise")
lp = next((i for i in _iter_panel_items(cruise) if i["key"] == "LongitudinalPersonality"), None)
assert lp is not None
assert "options" in lp
assert len(lp["options"]) == 3

View File

@@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""
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 __future__ import annotations
import base64
import gzip
import json
import os
from collections.abc import Callable
from datetime import datetime, timezone
from openpilot.sunnypilot.sunnylink.capabilities import CAPABILITY_FIELDS, CAPABILITY_LABELS
SCHEMA_VERSION = "1.0"
_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEFINITION_PATH = os.path.join(_DIR, "settings_ui.json")
TORQUE_VERSIONS_PATH = os.path.normpath(
os.path.join(_DIR, "..", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json")
)
def _load_torque_versions() -> dict:
"""Load latcontrol_torque_versions.json so TorqueControlTune options stay in sync."""
try:
with open(TORQUE_VERSIONS_PATH) as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {}
def _build_torque_options(versions: dict) -> list[dict]:
options: list[dict] = [{"value": "", "label": "Default"}]
parsed: list[tuple[float, str]] = []
for label, info in versions.items():
try:
parsed.append((float(info["version"]), label))
except (KeyError, TypeError, ValueError):
continue
for version, label in sorted(parsed, key=lambda kv: kv[0], reverse=True):
options.append({"value": version, "label": label})
return options
def _inject_dynamic_options(schema: dict) -> None:
versions = _load_torque_versions()
if not versions:
return
options = _build_torque_options(versions)
def visitor(item: dict) -> None:
if item.get("key") == "TorqueControlTune":
item["options"] = options
_walk_all_items(schema, visitor)
def _load_definition() -> dict:
"""Load settings_ui.json and inject dynamic options sourced from runtime data files."""
with open(DEFINITION_PATH) as f:
schema = json.load(f)
_inject_dynamic_options(schema)
return schema
# Public API
def generate_schema() -> dict:
"""Return the settings_ui.json content augmented with runtime metadata.
Adds three top-level fields the frontend consumes:
- generated_at: ISO timestamp (drives schema-cache freshness checks)
- capability_fields: declared CAPABILITY_FIELDS, used for rule validation
- capability_labels: human-readable labels for capability_fields
"""
schema = _load_definition()
schema["generated_at"] = datetime.now(timezone.utc).isoformat()
schema["capability_fields"] = list(CAPABILITY_FIELDS)
schema["capability_labels"] = dict(CAPABILITY_LABELS)
return schema
def generate_schema_json() -> str:
"""Generate SettingsSchema as a compact JSON string."""
return json.dumps(generate_schema(), separators=(",", ":"))
def generate_schema_compressed() -> str:
"""Generate SettingsSchema as gzip-compressed, base64-encoded string.
Compression pipeline:
1. JSON serialize (compact, no whitespace)
2. UTF-8 encode
3. gzip compress
4. base64 encode
"""
raw = json.dumps(generate_schema(), separators=(",", ":")).encode("utf-8")
return base64.b64encode(gzip.compress(raw)).decode("utf-8")
# Schema introspection utilities
def _walk_rules(rules: list[dict] | None, visitor: Callable[[dict], None]) -> None:
"""Recursively walk all rules, calling visitor on each leaf rule."""
if not rules:
return
for rule in rules:
visitor(rule)
if rule.get("type") == "not" and "condition" in rule:
_walk_rules([rule["condition"]], visitor)
elif rule.get("type") in ("any", "all") and "conditions" in rule:
_walk_rules(rule["conditions"], visitor)
def _walk_all_items(schema: dict, visitor: Callable[[dict], None]) -> None:
"""Walk every item in the schema (panels, sections, sub_panels, sub_items, vehicle_settings)."""
def _visit_item(item: dict) -> None:
visitor(item)
for sub in item.get("sub_items", []):
_visit_item(sub)
for panel in schema.get("panels", []):
# Walk section items (V2)
for section in panel.get("sections", []):
for item in section.get("items", []):
_visit_item(item)
for sp in section.get("sub_panels", []):
for item in sp.get("items", []):
_visit_item(item)
# Walk flat items (V1)
for item in panel.get("items", []):
_visit_item(item)
for sp in panel.get("sub_panels", []):
for item in sp.get("items", []):
_visit_item(item)
for brand_data in schema.get("vehicle_settings", {}).values():
items = brand_data.get("items", []) if isinstance(brand_data, dict) else brand_data
for item in items:
_visit_item(item)
def collect_all_keys(schema: dict) -> set[str]:
"""Collect all param keys referenced in the schema (items + rules)."""
keys: set[str] = set()
def _visit_rule(rule: dict) -> None:
if rule.get("type") in ("param", "param_compare") and "key" in rule:
keys.add(rule["key"])
def _visit_item(item: dict) -> None:
if "key" in item:
keys.add(item["key"])
_walk_rules(item.get("visibility"), _visit_rule)
_walk_rules(item.get("enablement"), _visit_rule)
_walk_all_items(schema, _visit_item)
return keys
def collect_capability_refs(schema: dict) -> set[str]:
"""Collect all capability field names referenced in rules."""
refs: set[str] = set()
def _visit_rule(rule: dict) -> None:
if rule.get("type") == "capability" and "field" in rule:
refs.add(rule["field"])
def _visit_item(item: dict) -> None:
_walk_rules(item.get("visibility"), _visit_rule)
_walk_rules(item.get("enablement"), _visit_rule)
_walk_all_items(schema, _visit_item)
return refs
if __name__ == "__main__":
# CLI: print schema for inspection
schema = generate_schema()
print(json.dumps(schema, indent=2))
print(f"\nTotal panels: {len(schema.get('panels', []))}")
print(f"Total vehicle brands: {len(schema.get('vehicle_settings', {}))}")
keys = collect_all_keys(schema)
print(f"Total unique param keys: {len(keys)}")
# Show compression stats
raw_json = json.dumps(schema, separators=(",", ":")).encode("utf-8")
compressed = gzip.compress(raw_json)
print(f"\nRaw JSON size: {len(raw_json):,} bytes")
print(f"Compressed size: {len(compressed):,} bytes")
print(f"Compression ratio: {len(compressed)/len(raw_json):.1%}")

View File

@@ -0,0 +1,525 @@
#!/usr/bin/env python3
"""
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.
Settings UI Validator
=====================
Validates settings_ui.json against structural, semantic, and referential
integrity constraints. Ensures the file is well-formed for consumption by
the sunnylink frontend and the device-side schema generator.
Usage:
python validate_settings_ui.py
python validate_settings_ui.py /path/to/settings_ui.json
"""
from __future__ import annotations
import json
import os
import sys
# Add repo root to path for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))
from openpilot.sunnypilot.sunnylink.capabilities import CAPABILITY_FIELDS
VALID_WIDGETS = {"toggle", "option", "multiple_button", "button", "info"}
VALID_COMPARE_OPS = {">", "<", ">=", "<="}
DEFAULT_PATH = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"settings_ui.json",
)
class ValidationResult:
"""Tracks pass/fail for each named check."""
def __init__(self) -> None:
self.passed: list[str] = []
self.failed: list[tuple[str, str]] = []
self.warnings: list[str] = []
def ok(self, name: str) -> None:
self.passed.append(name)
print(f"OK: {name}")
def error(self, name: str, details: str) -> None:
self.failed.append((name, details))
print(f"ERROR: {name}: {details}")
def warn(self, msg: str) -> None:
self.warnings.append(msg)
print(f"WARNING: {msg}")
@property
def success(self) -> bool:
return len(self.failed) == 0
def summary(self) -> None:
total_passed = len(self.passed)
total_failed = len(self.failed)
print(f"\n{'='*60}")
print(f"Summary: {total_passed} checks passed, {total_failed} checks failed")
if self.warnings:
print(f" {len(self.warnings)} warnings")
if self.success:
print("Result: PASS")
else:
print("Result: FAIL")
def validate_rule(rule: dict, path: str, result: ValidationResult,
capability_fields: tuple[str, ...]) -> bool:
"""Validate a single rule dict. Returns True if valid."""
if not isinstance(rule, dict):
result.error("rule well-formedness", f"{path}: rule is not a dict: {rule!r}")
return False
rule_type = rule.get("type")
if rule_type is None:
result.error("rule well-formedness", f"{path}: rule missing 'type' field")
return False
if rule_type == "offroad_only":
# Only type required
return True
if rule_type == "capability":
valid = True
if "field" not in rule or not isinstance(rule["field"], str):
result.error("rule well-formedness", f"{path}: capability rule missing/invalid 'field'")
valid = False
if "equals" not in rule:
result.error("rule well-formedness", f"{path}: capability rule missing 'equals'")
valid = False
return valid
if rule_type == "param":
valid = True
if "key" not in rule or not isinstance(rule["key"], str):
result.error("rule well-formedness", f"{path}: param rule missing/invalid 'key'")
valid = False
if "equals" not in rule:
result.error("rule well-formedness", f"{path}: param rule missing 'equals'")
valid = False
return valid
if rule_type == "param_compare":
valid = True
if "key" not in rule or not isinstance(rule["key"], str):
result.error("rule well-formedness", f"{path}: param_compare rule missing/invalid 'key'")
valid = False
if "op" not in rule or rule["op"] not in VALID_COMPARE_OPS:
result.error("rule well-formedness",
f"{path}: param_compare rule missing/invalid 'op' (must be one of {VALID_COMPARE_OPS})")
valid = False
if "value" not in rule or not isinstance(rule["value"], (int, float)):
result.error("rule well-formedness", f"{path}: param_compare rule missing/invalid 'value' (must be number)")
valid = False
return valid
if rule_type == "not":
if "condition" not in rule or not isinstance(rule["condition"], dict):
result.error("rule well-formedness", f"{path}: 'not' rule missing/invalid 'condition'")
return False
return validate_rule(rule["condition"], f"{path}.not", result, capability_fields)
if rule_type in ("any", "all"):
if "conditions" not in rule or not isinstance(rule["conditions"], list):
result.error("rule well-formedness", f"{path}: '{rule_type}' rule missing/invalid 'conditions' array")
return False
valid = True
for i, cond in enumerate(rule["conditions"]):
if not validate_rule(cond, f"{path}.{rule_type}[{i}]", result, capability_fields):
valid = False
return valid
result.error("rule well-formedness", f"{path}: unknown rule type '{rule_type}'")
return False
def collect_rules_from_item(item: dict) -> list[tuple[str, list[dict]]]:
"""Return list of (context, rules_list) for an item's visibility + enablement."""
result = []
key = item.get("key", "?")
if "visibility" in item:
result.append((f"item '{key}' visibility", item["visibility"]))
if "enablement" in item:
result.append((f"item '{key}' enablement", item["enablement"]))
return result
def walk_rules_flat(rules: list[dict]) -> list[dict]:
"""Flatten all rules recursively into a single list."""
flat: list[dict] = []
for rule in rules:
flat.append(rule)
if rule.get("type") == "not" and "condition" in rule:
flat.extend(walk_rules_flat([rule["condition"]]))
elif rule.get("type") in ("any", "all") and "conditions" in rule:
flat.extend(walk_rules_flat(rule["conditions"]))
return flat
def collect_all_items(data: dict) -> list[tuple[str, dict]]:
"""Collect all items with their location path from the schema.
Returns (path, item_dict) tuples. Traverses sections, sub_panels, sub_items,
and vehicle_settings.
"""
items: list[tuple[str, dict]] = []
for panel in data.get("panels", []):
pid = panel.get("id", "?")
# Flat items on panel
for item in panel.get("items", []):
_collect_item(f"panel '{pid}'", item, items)
# Sections
for section in panel.get("sections", []):
sid = section.get("id", "?")
for item in section.get("items", []):
_collect_item(f"panel '{pid}' > section '{sid}'", item, items)
for sp in section.get("sub_panels", []):
spid = sp.get("id", "?")
for item in sp.get("items", []):
_collect_item(f"panel '{pid}' > section '{sid}' > sub_panel '{spid}'", item, items)
# Top-level sub_panels on panel (no section)
for sp in panel.get("sub_panels", []):
spid = sp.get("id", "?")
for item in sp.get("items", []):
_collect_item(f"panel '{pid}' > sub_panel '{spid}'", item, items)
# Vehicle settings (supports both flat list and { title, items } structure)
for brand, brand_data in data.get("vehicle_settings", {}).items():
brand_items = brand_data.get("items", []) if isinstance(brand_data, dict) else brand_data
for item in brand_items:
_collect_item(f"vehicle_settings '{brand}'", item, items)
return items
def _collect_item(path: str, item: dict, items: list[tuple[str, dict]]) -> None:
"""Recursively collect an item and its sub_items."""
items.append((path, item))
for sub in item.get("sub_items", []):
_collect_item(f"{path} > sub_item", sub, items)
def collect_panel_keys(panel: dict) -> set[str]:
"""Collect all item keys within a single panel (sections, sub_panels, sub_items)."""
keys: set[str] = set()
def _add(item: dict) -> None:
if "key" in item:
keys.add(item["key"])
for sub in item.get("sub_items", []):
_add(sub)
for item in panel.get("items", []):
_add(item)
for section in panel.get("sections", []):
for item in section.get("items", []):
_add(item)
for sp in section.get("sub_panels", []):
for item in sp.get("items", []):
_add(item)
for sp in panel.get("sub_panels", []):
for item in sp.get("items", []):
_add(item)
return keys
def check_json_parseable(path: str, result: ValidationResult) -> dict | None:
"""Check 1: JSON parseable."""
try:
with open(path) as f:
data = json.load(f)
result.ok("JSON parseable")
return data
except json.JSONDecodeError as e:
result.error("JSON parseable", str(e))
return None
except FileNotFoundError:
result.error("JSON parseable", f"file not found: {path}")
return None
def check_structural(data: dict, result: ValidationResult) -> None:
"""Check 2: Required fields on panels, sections, items, sub_panels."""
errors: list[str] = []
for i, panel in enumerate(data.get("panels", [])):
for field in ("id", "label", "icon", "order"):
if field not in panel:
errors.append(f"panels[{i}]: missing required field '{field}'")
for j, section in enumerate(panel.get("sections", [])):
for field in ("id", "title"):
if field not in section:
errors.append(f"panels[{i}].sections[{j}]: missing required field '{field}'")
for k, sp in enumerate(section.get("sub_panels", [])):
for field in ("id", "label", "trigger_key"):
if field not in sp:
errors.append(f"panels[{i}].sections[{j}].sub_panels[{k}]: missing required field '{field}'")
for k, sp in enumerate(panel.get("sub_panels", [])):
for field in ("id", "label", "trigger_key"):
if field not in sp:
errors.append(f"panels[{i}].sub_panels[{k}]: missing required field '{field}'")
# Validate items
all_items = collect_all_items(data)
for path, item in all_items:
if "key" not in item:
errors.append(f"{path}: item missing required field 'key'")
if "widget" not in item:
errors.append(f"{path}: item missing required field 'widget'")
elif item["widget"] not in VALID_WIDGETS:
errors.append(
f"{path}: item '{item.get('key', '?')}' has invalid widget '{item['widget']}'"
+ f" (must be one of {VALID_WIDGETS})"
)
if errors:
result.error("structural", "; ".join(errors))
else:
result.ok("structural")
def check_item_completeness(data: dict, result: ValidationResult) -> None:
"""Check 3: All items have required metadata (title, options for dropdowns)."""
all_items = collect_all_items(data)
issues: list[str] = []
for _path, item in all_items:
key = item.get("key", "unknown")
if "title" not in item:
issues.append(f"{key}: missing 'title'")
elif item["title"] == key:
issues.append(f"{key}: title must not equal key (use a human-readable title)")
widget = item.get("widget")
if widget in ("multiple_button", "option") and "options" in item:
opts = item["options"]
if not isinstance(opts, list):
issues.append(f"{key}: options must be a list")
else:
for opt in opts:
if not isinstance(opt, dict) or "value" not in opt or "label" not in opt:
issues.append(f"{key}: each option must have 'value' and 'label'")
break
if issues:
for issue in issues:
result.error("item completeness", issue)
else:
result.ok("item completeness")
def check_no_duplicate_keys(data: dict, result: ValidationResult) -> None:
"""Check 4: No param key appears in more than one panel."""
panel_keys: dict[str, list[str]] = {} # key -> list of panel ids
for panel in data.get("panels", []):
pid = panel.get("id", "?")
keys = collect_panel_keys(panel)
for key in keys:
panel_keys.setdefault(key, []).append(pid)
# Also check vehicle_settings keys don't collide with panel keys
for brand, brand_data in data.get("vehicle_settings", {}).items():
brand_items = brand_data.get("items", []) if isinstance(brand_data, dict) else brand_data
for item in brand_items:
key = item.get("key")
if key:
panel_keys.setdefault(key, []).append(f"vehicle_settings.{brand}")
duplicates = {k: v for k, v in panel_keys.items() if len(v) > 1}
if duplicates:
details = "; ".join(f"'{k}' in [{', '.join(v)}]" for k, v in duplicates.items())
result.error("no duplicate keys", details)
else:
result.ok("no duplicate keys")
def check_rule_wellformedness(data: dict, result: ValidationResult) -> None:
"""Check 5: All rules have valid structure."""
all_items = collect_all_items(data)
# Save current error count to detect new errors
error_count_before = len(result.failed)
for path, item in all_items:
for ctx, rules in collect_rules_from_item(item):
for i, rule in enumerate(rules):
validate_rule(rule, f"{path} > {ctx}[{i}]", result, CAPABILITY_FIELDS)
# Also validate trigger_condition rules on sub_panels
for panel in data.get("panels", []):
pid = panel.get("id", "?")
for section in panel.get("sections", []):
for sp in section.get("sub_panels", []):
if "trigger_condition" in sp:
validate_rule(sp["trigger_condition"], f"panel '{pid}' > sub_panel '{sp.get('id', '?')}' trigger_condition",
result, CAPABILITY_FIELDS)
if len(result.failed) == error_count_before:
result.ok("rule well-formedness")
def check_capability_refs(data: dict, result: ValidationResult) -> None:
"""Check 6: All capability rule field values are in CAPABILITY_FIELDS."""
all_items = collect_all_items(data)
invalid_refs: list[str] = []
cap_set = set(CAPABILITY_FIELDS)
for _path, item in all_items:
for _ctx, rules in collect_rules_from_item(item):
for rule in walk_rules_flat(rules):
if rule.get("type") == "capability":
field = rule.get("field")
if field and field not in cap_set:
invalid_refs.append(f"'{field}' in item '{item.get('key', '?')}'")
# Also check trigger_conditions
for panel in data.get("panels", []):
for section in panel.get("sections", []):
for sp in section.get("sub_panels", []):
if "trigger_condition" in sp:
for rule in walk_rules_flat([sp["trigger_condition"]]):
if rule.get("type") == "capability":
field = rule.get("field")
if field and field not in cap_set:
invalid_refs.append(f"'{field}' in sub_panel '{sp.get('id', '?')}' trigger_condition")
if invalid_refs:
result.error("capability refs", f"unknown capability fields: {', '.join(invalid_refs)}")
else:
result.ok("capability refs")
def check_no_self_reference(data: dict, result: ValidationResult) -> None:
"""Check 7: Item's rules must not reference the item's own key."""
all_items = collect_all_items(data)
self_refs: list[str] = []
for path, item in all_items:
key = item.get("key")
if not key:
continue
for _ctx, rules in collect_rules_from_item(item):
for rule in walk_rules_flat(rules):
if rule.get("type") in ("param", "param_compare") and rule.get("key") == key:
self_refs.append(f"'{key}' at {path}")
if self_refs:
result.error("no self-reference", f"items reference their own key: {', '.join(self_refs)}")
else:
result.ok("no self-reference")
def check_sub_panel_triggers(data: dict, result: ValidationResult) -> None:
"""Check 8: Sub-panel trigger_key must reference a key in the same panel."""
errors: list[str] = []
for panel in data.get("panels", []):
pid = panel.get("id", "?")
panel_keys = collect_panel_keys(panel)
# Check sub_panels at section level
for section in panel.get("sections", []):
for sp in section.get("sub_panels", []):
trigger = sp.get("trigger_key")
if trigger and trigger not in panel_keys:
errors.append(
f"sub_panel '{sp.get('id', '?')}' trigger_key '{trigger}'"
+ f" not found in panel '{pid}'"
)
# Check top-level sub_panels
for sp in panel.get("sub_panels", []):
trigger = sp.get("trigger_key")
if trigger and trigger not in panel_keys:
errors.append(
f"sub_panel '{sp.get('id', '?')}' trigger_key '{trigger}'"
+ f" not found in panel '{pid}'"
)
if errors:
result.error("sub-panel triggers", "; ".join(errors))
else:
result.ok("sub-panel triggers")
def check_ordering(data: dict, result: ValidationResult) -> None:
"""Check 9: Panel order values must be unique."""
orders: dict[int, list[str]] = {}
for panel in data.get("panels", []):
order = panel.get("order")
if order is not None:
orders.setdefault(order, []).append(panel.get("id", "?"))
duplicates = {o: ids for o, ids in orders.items() if len(ids) > 1}
if duplicates:
details = "; ".join(f"order {o}: [{', '.join(ids)}]" for o, ids in duplicates.items())
result.error("ordering", f"duplicate order values: {details}")
else:
result.ok("ordering")
def check_vehicle_brands(data: dict, result: ValidationResult) -> None:
"""Check 10: Vehicle settings keys should be lowercase strings."""
vehicle = data.get("vehicle_settings", {})
bad_brands: list[str] = []
for brand in vehicle:
if not isinstance(brand, str) or brand != brand.lower():
bad_brands.append(brand)
if bad_brands:
result.error("vehicle brands", f"non-lowercase brand keys: {', '.join(bad_brands)}")
else:
result.ok("vehicle brands")
def validate(path: str) -> bool:
"""Run all validation checks on the given settings_ui.json file.
Returns True if all checks pass.
"""
result = ValidationResult()
# Check 1: JSON parseable
data = check_json_parseable(path, result)
if data is None:
result.summary()
return False
# Checks 2-10
check_structural(data, result)
check_item_completeness(data, result)
check_no_duplicate_keys(data, result)
check_rule_wellformedness(data, result)
check_capability_refs(data, result)
check_no_self_reference(data, result)
check_sub_panel_triggers(data, result)
check_ordering(data, result)
check_vehicle_brands(data, result)
result.summary()
return result.success
if __name__ == "__main__":
target = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_PATH
success = validate(target)
sys.exit(0 if success else 1)

View File

@@ -28,6 +28,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"

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,183 @@
<?xml version='1.0' encoding='UTF-8'?>
<root>
<tabbed_widget parent="main_window" name="Main Window">
<Tab containers="1" tab_name="actuator data">
<Container>
<DockSplitter orientation="-" sizes="0.25;0.25;0.25;0.25" count="4">
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range bottom="-0.100000" left="301.990881" top="0.100000" right="462.962504"/>
<limitY/>
<curve name="/carControl/actuators/torque" color="#17becf"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range bottom="-130.920132" left="301.990881" top="103.993176" right="462.962504"/>
<limitY/>
<curve name="/carControl/actuators/steeringAngleDeg" color="#1f77b4"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range bottom="-0.604818" left="301.990881" top="24.797527" right="462.962504"/>
<limitY/>
<curve name="/carState/vEgoRaw" color="#ff7f0e"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range bottom="-0.037812" left="301.990881" top="0.047261" right="462.962504"/>
<limitY/>
<curve name="/carControl/actuators/curvature" color="#f14cc1"/>
</plot>
</DockArea>
</DockSplitter>
</Container>
</Tab>
<Tab containers="1" tab_name="steering messages (CAN)">
<Container>
<DockSplitter orientation="-" sizes="0.142857;0.142857;0.142857;0.142857;0.142857;0.142857;0.142857" count="7">
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range bottom="-0.025000" left="301.990881" top="1.025000" right="462.962504"/>
<limitY/>
<curve name="/can/128/LKAS_ALT/ADAS_ACIAnglTqRedcGainVal" color="#1f77b4"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range bottom="-128.740000" left="301.990881" top="103.940000" right="462.962504"/>
<limitY/>
<curve name="/can/128/LKAS_ALT/ADAS_StrAnglReqVal" color="#29d627"/>
<curve name="/can/192/LKAS_ALT/ADAS_StrAnglReqVal" color="#b41f32"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range bottom="-0.025000" left="301.990881" top="1.025000" right="462.962504"/>
<limitY/>
<curve name="/carState/steeringPressed" color="#d62728"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range bottom="0.975000" left="301.990881" top="2.025000" right="462.962504"/>
<limitY/>
<curve name="/sendcan/0/LKAS_ALT/LKAS_ANGLE_ACTIVE" color="#1f77b4"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range bottom="-0.025000" left="301.990881" top="1.025000" right="462.962504"/>
<limitY/>
<curve name="/carState/steerFaultTemporary" color="#d62728"/>
<curve name="/carControl/latActive" color="#1ac938"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range bottom="-0.025000" left="301.990881" top="1.025000" right="462.962504"/>
<limitY/>
<curve name="/can/1/CCNC_0x161/DAW_ICON" color="#f14cc1"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range bottom="-14.375000" left="301.990881" top="589.375000" right="462.962504"/>
<limitY/>
<curve name="/pandaStates/0/safetyTxBlocked" color="#1f77b4"/>
</plot>
</DockArea>
</DockSplitter>
</Container>
</Tab>
<Tab containers="1" tab_name="Understanding Torque">
<Container>
<DockSplitter orientation="-" sizes="0.5;0.5" count="2">
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range bottom="-6.175000" left="429.901019" top="253.175000" right="590.696728"/>
<limitY/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range bottom="-24.190000" left="301.990881" top="21.590000" right="462.962504"/>
<limitY/>
<curve name="/carState/steeringTorqueEps" color="#1ac938"/>
</plot>
</DockArea>
</DockSplitter>
</Container>
</Tab>
<Tab containers="1" tab_name="tab2">
<Container>
<DockSplitter orientation="-" sizes="0.25;0.25;0.25;0.25" count="4">
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Dots">
<range bottom="-0.025000" left="301.990881" top="1.025000" right="462.962504"/>
<limitY/>
<curve name="/sendcan/0/LKAS_ALT/ADAS_ACIAnglTqRedcGainVal" color="#9467bd"/>
<curve name="/can/1/LFA_ALT/ADAS_ACIAnglTqRedcGainVal" color="#17becf"/>
<curve name="/can/128/LKAS_ALT/ADAS_ACIAnglTqRedcGainVal" color="#1f77b4"/>
<curve name="/can/192/LKAS_ALT/ADAS_ACIAnglTqRedcGainVal" color="#d62728"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Dots">
<range bottom="0.975000" left="301.990881" top="2.025000" right="462.962504"/>
<limitY/>
<curve name="/can/128/LKAS_ALT/LKAS_ANGLE_ACTIVE" color="#b8c91a"/>
<curve name="/can/192/LKAS_ALT/LKAS_ANGLE_ACTIVE" color="#ff7f0e"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range bottom="-0.025000" left="301.990881" top="1.025000" right="462.962504"/>
<limitY/>
<curve name="/can/1/CCNC_0x161/DAW_ICON" color="#f14cc1"/>
<curve name="/carState/steerFaultTemporary" color="#1f77b4"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Dots">
<range bottom="-14.375000" left="301.990881" top="589.375000" right="462.962504"/>
<limitY/>
<curve name="/pandaStates/0/safetyTxBlocked" color="#bcbd22"/>
</plot>
</DockArea>
</DockSplitter>
</Container>
</Tab>
<currentTabIndex index="3"/>
</tabbed_widget>
<use_relative_time_offset enabled="1"/>
<!-- - - - - - - - - - - - - - - -->
<!-- - - - - - - - - - - - - - - -->
<Plugins>
<plugin ID="DataLoad CSV">
<default time_axis="" delimiter="0"/>
</plugin>
<plugin ID="DataLoad MCAP"/>
<plugin ID="DataLoad Rlog"/>
<plugin ID="DataLoad ULog"/>
<plugin ID="Cereal Subscriber"/>
<plugin ID="UDP Server"/>
<plugin ID="WebSocket Server"/>
<plugin ID="ZMQ Subscriber"/>
<plugin ID="Fast Fourier Transform"/>
<plugin ID="Quaternion to RPY"/>
<plugin ID="Reactive Script Editor">
<library code="--[[ Helper function to create a series from arrays&#xa;&#xa; new_series: a series previously created with ScatterXY.new(name)&#xa; prefix: prefix of the timeseries, before the index of the array&#xa; suffix_X: suffix to complete the name of the series containing the X value. If [nil], use the index of the array.&#xa; suffix_Y: suffix to complete the name of the series containing the Y value&#xa; timestamp: usually the tracker_time variable&#xa; &#xa; Example:&#xa; &#xa; Assuming we have multiple series in the form:&#xa; &#xa; /trajectory/node.{X}/position/x&#xa; /trajectory/node.{X}/position/y&#xa; &#xa; where {N} is the index of the array (integer). We can create a reactive series from the array with:&#xa; &#xa; new_series = ScatterXY.new(&quot;my_trajectory&quot;) &#xa; CreateSeriesFromArray( new_series, &quot;/trajectory/node&quot;, &quot;position/x&quot;, &quot;position/y&quot;, tracker_time );&#xa;--]]&#xa;&#xa;function CreateSeriesFromArray( new_series, prefix, suffix_X, suffix_Y, timestamp )&#xa; &#xa; --- clear previous values&#xa; new_series:clear()&#xa; &#xa; --- Append points to new_series&#xa; index = 0&#xa; while(true) do&#xa;&#xa; x = index;&#xa; -- if not nil, get the X coordinate from a series&#xa; if suffix_X ~= nil then &#xa; series_x = TimeseriesView.find( string.format( &quot;%s.%d/%s&quot;, prefix, index, suffix_X) )&#xa; if series_x == nil then break end&#xa; x = series_x:atTime(timestamp)&#x9; &#xa; end&#xa; &#xa; series_y = TimeseriesView.find( string.format( &quot;%s.%d/%s&quot;, prefix, index, suffix_Y) )&#xa; if series_y == nil then break end &#xa; y = series_y:atTime(timestamp)&#xa; &#xa; new_series:push_back(x,y)&#xa; index = index+1&#xa; end&#xa;end&#xa;&#xa;--[[ Similar to the built-in function GetSeriesNames(), but select only the names with a give prefix. --]]&#xa;&#xa;function GetSeriesNamesByPrefix(prefix)&#xa; -- GetSeriesNames(9 is a built-in function&#xa; all_names = GetSeriesNames()&#xa; filtered_names = {}&#xa; for i, name in ipairs(all_names) do&#xa; -- check the prefix&#xa; if name:find(prefix, 1, #prefix) then&#xa; table.insert(filtered_names, name);&#xa; end&#xa; end&#xa; return filtered_names&#xa;end&#xa;&#xa;--[[ Modify an existing series, applying offsets to all their X and Y values&#xa;&#xa; series: an existing timeseries, obtained with TimeseriesView.find(name)&#xa; delta_x: offset to apply to each x value&#xa; delta_y: offset to apply to each y value &#xa; &#xa;--]]&#xa;&#xa;function ApplyOffsetInPlace(series, delta_x, delta_y)&#xa; -- use C++ indeces, not Lua indeces&#xa; for index=0, series:size()-1 do&#xa; x,y = series:at(index)&#xa; series:set(index, x + delta_x, y + delta_y)&#xa; end&#xa;end&#xa;"/>
<scripts/>
</plugin>
<plugin ID="CSV Exporter"/>
</Plugins>
<!-- - - - - - - - - - - - - - - -->
<!-- - - - - - - - - - - - - - - -->
<customMathEquations/>
<snippets/>
<!-- - - - - - - - - - - - - - - -->
</root>

View File

@@ -0,0 +1,702 @@
<?xml version='1.0' encoding='UTF-8'?>
<root>
<tabbed_widget name="Main Window" parent="main_window">
<Tab containers="1" tab_name="actuator data">
<Container>
<DockSplitter sizes="0.25;0.25;0.25;0.25" count="4" orientation="-">
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="0.100000" bottom="-0.100000" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="/carControl/actuators/torque" color="#17becf"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="21.512506" bottom="-143.133163" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="/carControl/actuators/steeringAngleDeg" color="#1f77b4"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="17.919758" bottom="7.125814" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="/carState/vEgoRaw" color="#ff7f0e"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="0.055486" bottom="-0.008455" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="/carControl/actuators/curvature" color="#f14cc1"/>
</plot>
</DockArea>
</DockSplitter>
</Container>
</Tab>
<Tab containers="1" tab_name="steering messages (CAN)">
<Container>
<DockSplitter sizes="0.25;0.25;0.25;0.25" count="4" orientation="-">
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="1.020200" bottom="0.171800" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="/sendcan/0/LKAS_ALT/ADAS_ACIAnglTqRedcGainVal" color="#1f77b4"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="3.417986" bottom="-1.676466" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="IMU_LatAccelVal_ms^2_roll_compensated" color="#ff7f0e"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="1.025000" bottom="-0.025000" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="/carState/steeringPressed" color="#d62728"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="0.100000" bottom="-0.100000" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="/can/1/CCNC_0x161/DAW_ICON" color="#f14cc1"/>
</plot>
</DockArea>
</DockSplitter>
</Container>
</Tab>
<Tab containers="1" tab_name="Understanding Torque">
<Container>
<DockSplitter sizes="0.5;0.5" count="2" orientation="-">
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="378.825000" bottom="-1125.825000" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="/carState/steeringTorque" color="#1f77b4"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="17.092500" bottom="-32.992499" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="/carState/steeringTorqueEps" color="#1ac938"/>
</plot>
</DockArea>
</DockSplitter>
</Container>
</Tab>
<Tab containers="1" tab_name="Angle Error and Saturation TQ">
<Container>
<DockSplitter sizes="0.166667;0.166667;0.166667;0.166667;0.166667;0.166667" count="6" orientation="-">
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="21.512506" bottom="-143.133163" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="/carControl/actuators/steeringAngleDeg" color="#d62728"/>
<curve name="/carState/steeringAngleDeg" color="#1ac938"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="48.052530" bottom="-34.508142" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="Angle Error" color="#ff7f0e"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="1.020200" bottom="0.171800" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="/sendcan/0/LKAS_ALT/ADAS_ACIAnglTqRedcGainVal" color="#1f77b4"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="1.025000" bottom="-0.025000" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="Angle Staturation" color="#f14cc1"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="1.025000" bottom="-0.025000" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="/carControl/latActive" color="#9467bd"/>
<curve name="/carState/steeringPressed" color="#17becf"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="64.446359" bottom="25.681587" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="carState.vEgo kmh" color="#1f77b4"/>
</plot>
</DockArea>
</DockSplitter>
</Container>
</Tab>
<Tab containers="1" tab_name="Smoothing and Torque celings">
<Container>
<DockSplitter sizes="0.5;0.5" count="2" orientation="-">
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="4.075000" bottom="0.925000" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="subject_to_angle_smoothing" color="#d62728"/>
</plot>
</DockArea>
<DockArea name="...">
<plot mode="TimeSeries" flip_x="false" flip_y="false" style="Lines">
<range top="3.025000" bottom="1.975000" left="256.448992" right="302.818689"/>
<limitY/>
<curve name="base_ceiling_bracket" color="#1f77b4"/>
</plot>
</DockArea>
</DockSplitter>
</Container>
</Tab>
<currentTabIndex index="4"/>
</tabbed_widget>
<use_relative_time_offset enabled="1"/>
<!-- - - - - - - - - - - - - - - -->
<!-- - - - - - - - - - - - - - - -->
<Plugins>
<plugin ID="DataLoad CSV">
<default time_axis="" delimiter="0"/>
</plugin>
<plugin ID="DataLoad MCAP"/>
<plugin ID="DataLoad Rlog"/>
<plugin ID="DataLoad ULog"/>
<plugin ID="Cereal Subscriber"/>
<plugin ID="UDP Server"/>
<plugin ID="WebSocket Server"/>
<plugin ID="ZMQ Subscriber"/>
<plugin ID="Fast Fourier Transform"/>
<plugin ID="Quaternion to RPY"/>
<plugin ID="Reactive Script Editor">
<library code="--[[ Helper function to create a series from arrays&#xa;&#xa; new_series: a series previously created with ScatterXY.new(name)&#xa; prefix: prefix of the timeseries, before the index of the array&#xa; suffix_X: suffix to complete the name of the series containing the X value. If [nil], use the index of the array.&#xa; suffix_Y: suffix to complete the name of the series containing the Y value&#xa; timestamp: usually the tracker_time variable&#xa; &#xa; Example:&#xa; &#xa; Assuming we have multiple series in the form:&#xa; &#xa; /trajectory/node.{X}/position/x&#xa; /trajectory/node.{X}/position/y&#xa; &#xa; where {N} is the index of the array (integer). We can create a reactive series from the array with:&#xa; &#xa; new_series = ScatterXY.new(&quot;my_trajectory&quot;) &#xa; CreateSeriesFromArray( new_series, &quot;/trajectory/node&quot;, &quot;position/x&quot;, &quot;position/y&quot;, tracker_time );&#xa;--]]&#xa;&#xa;function CreateSeriesFromArray( new_series, prefix, suffix_X, suffix_Y, timestamp )&#xa; &#xa; --- clear previous values&#xa; new_series:clear()&#xa; &#xa; --- Append points to new_series&#xa; index = 0&#xa; while(true) do&#xa;&#xa; x = index;&#xa; -- if not nil, get the X coordinate from a series&#xa; if suffix_X ~= nil then &#xa; series_x = TimeseriesView.find( string.format( &quot;%s.%d/%s&quot;, prefix, index, suffix_X) )&#xa; if series_x == nil then break end&#xa; x = series_x:atTime(timestamp)&#x9; &#xa; end&#xa; &#xa; series_y = TimeseriesView.find( string.format( &quot;%s.%d/%s&quot;, prefix, index, suffix_Y) )&#xa; if series_y == nil then break end &#xa; y = series_y:atTime(timestamp)&#xa; &#xa; new_series:push_back(x,y)&#xa; index = index+1&#xa; end&#xa;end&#xa;&#xa;--[[ Similar to the built-in function GetSeriesNames(), but select only the names with a give prefix. --]]&#xa;&#xa;function GetSeriesNamesByPrefix(prefix)&#xa; -- GetSeriesNames(9 is a built-in function&#xa; all_names = GetSeriesNames()&#xa; filtered_names = {}&#xa; for i, name in ipairs(all_names) do&#xa; -- check the prefix&#xa; if name:find(prefix, 1, #prefix) then&#xa; table.insert(filtered_names, name);&#xa; end&#xa; end&#xa; return filtered_names&#xa;end&#xa;&#xa;--[[ Modify an existing series, applying offsets to all their X and Y values&#xa;&#xa; series: an existing timeseries, obtained with TimeseriesView.find(name)&#xa; delta_x: offset to apply to each x value&#xa; delta_y: offset to apply to each y value &#xa; &#xa;--]]&#xa;&#xa;function ApplyOffsetInPlace(series, delta_x, delta_y)&#xa; -- use C++ indeces, not Lua indeces&#xa; for index=0, series:size()-1 do&#xa; x,y = series:at(index)&#xa; series:set(index, x + delta_x, y + delta_y)&#xa; end&#xa;end&#xa;"/>
<scripts/>
</plugin>
<plugin ID="CSV Exporter"/>
</Plugins>
<!-- - - - - - - - - - - - - - - -->
<!-- - - - - - - - - - - - - - - -->
<customMathEquations>
<snippet name="subject_to_angle_smoothing">
<global></global>
<function>if v1 == 0 then
return 0
end
if value &lt; 8.5 then
return 1
elseif value &lt; 11 then
return 2
elseif value &lt;= 13.8 then
return 3
elseif value &lt;= 18 then
return 4
end
return 0</function>
<linked_source>/carState/vEgo</linked_source>
<additional_sources>
<v1>/carControl/latActive</v1>
</additional_sources>
</snippet>
<snippet name="base_ceiling_bracket">
<global></global>
<function>if v1 == 0 then
return 0
end
if value &lt; 20 then
return 1
elseif value &lt; 40 then
return 2
elseif value &lt;= 120 then
return 3
end
return 0</function>
<linked_source>carState.vEgo kmh</linked_source>
<additional_sources>
<v1>/carControl/latActive</v1>
</additional_sources>
</snippet>
<snippet name="max torque lj adj">
<global>min=0
max=250
max_from_speed=96
k1=200
k2=30
k3=1
k4=1
k5=10
function sign(number)
return number > 0 and 1 or (number == 0 and 0 or -1)
end</global>
<function>return 250 - value * 20</function>
<linked_source>desired lateral jark</linked_source>
</snippet>
<snippet name="Angle Staturation">
<global></global>
<function>if value > .3 then
return 1
end
return 0</function>
<linked_source>Angle Error</linked_source>
</snippet>
<snippet name="ang_cmd rate">
<global>firstX = 0
firstY = 0
is_first = true
secondX = 0
secondY = 0
is_second = false</global>
<function>-- Wait for initial values
if (is_first) then
is_first = false
is_second = true
firstX = time
firstY = value
end
if (is_second) then
is_second = false
secondX = time
secondY = value
end
-- Central derivative: dy/dx ~= f(x+delta_x)-f(x-delta_x)/(2*delta_x)
dx = time - firstX
dy = value - firstY
-- Increment
firstX = secondX
firstY = secondY
secondX = time
secondY = value
return dy/dx</function>
<linked_source>/can/1/LFA_ALT/LKAS_ANGLE_CMD</linked_source>
</snippet>
<snippet name="IMU_LatAccelVal_Ms^3">
<global></global>
<function>return value * -9.8</function>
<linked_source>/can/1/IMU_01_10ms/IMU_LatAccelVal</linked_source>
<additional_sources>
<v1>/carState/steeringPressed</v1>
<v2>/carControl/latActive</v2>
</additional_sources>
</snippet>
<snippet name="IMU_LatAccelVal_Ms^2">
<global></global>
<function>if (v1 == 0 and v2 == 1) then
return value * -9.8
end
return 0</function>
<linked_source>/can/1/IMU_01_10ms/IMU_LatAccelVal</linked_source>
<additional_sources>
<v1>/carState/steeringPressed</v1>
<v2>/carControl/latActive</v2>
</additional_sources>
</snippet>
<snippet name="IMU_LatAccelVal_ms^2_roll_compensated">
<global></global>
<function>if (v1 == 0 and v2 == 1) then
return (value * -9.8) - (v3 * 9.81)
end
--return 0
return (value * -9.8) - (v3 * 9.81)</function>
<linked_source>/can/1/IMU_01_10ms/IMU_LatAccelVal</linked_source>
<additional_sources>
<v1>/carState/steeringPressed</v1>
<v2>/carControl/latActive</v2>
<v3>/liveParameters/roll</v3>
</additional_sources>
</snippet>
<snippet name="abs(ang_cmd)">
<global></global>
<function>return math.abs(value)</function>
<linked_source>/can/1/LFA_ALT/LKAS_ANGLE_CMD</linked_source>
</snippet>
<snippet name="zero">
<global>min=0
max=250
max_from_speed=96
rate_lim = 500
la_deadzone = 0.38
k1=200
k2=20
k3=1.0
k4=1
k5=10
old = 0
function sign(number)
return number > 0 and 1 or (number == 0 and 0 or -1)
end
function apply_rate_limit(old, new, limit)
return math.min(math.max(new, old - limit), old + limit)
end
function apply_deadzone(val, deadzone)
if math.abs(val) &lt;= deadzone then
return 0.0
elseif val &lt; 0.0 then
return val + deadzone
else
return val - deadzone
end
end</global>
<function>return 0</function>
<linked_source>/carState/aEgo</linked_source>
</snippet>
<snippet name="engaged curvature vehicle model">
<global>engage_delay = 5
last_bad_time = -engage_delay</global>
<function>curvature = value
pressed = v1
enabled = v2
if (pressed == 1 or enabled == 0) then
last_bad_time = time
end
if (time > last_bad_time + engage_delay) then
return value
else
return 0
end</function>
<linked_source>/controlsState/curvature</linked_source>
<additional_sources>
<v1>/carState/steeringPressed</v1>
<v2>/carControl/enabled</v2>
</additional_sources>
</snippet>
<snippet name="engaged curvature plan">
<global>engage_delay = 5
last_bad_time = -engage_delay</global>
<function>curvature = value
pressed = v1
enabled = v2
if (pressed == 1 or enabled == 0) then
last_bad_time = time
end
if (time > last_bad_time + engage_delay) then
return value
else
return 0
end</function>
<linked_source>/lateralPlan/curvatures/0</linked_source>
<additional_sources>
<v1>/carState/steeringPressed</v1>
<v2>/carControl/enabled</v2>
</additional_sources>
</snippet>
<snippet name="desired lateral jark">
<global>firstX = 0
firstY = 0
is_first = true
secondX = 0
secondY = 0
is_second = false</global>
<function>-- Wait for initial values
if (is_first) then
is_first = false
is_second = true
firstX = time
firstY = value
end
if (is_second) then
is_second = false
secondX = time
secondY = value
end
-- Central derivative: dy/dx ~= f(x+delta_x)-f(x-delta_x)/(2*delta_x)
dx = time - firstX
dy = value - firstY
-- Increment
firstX = secondX
firstY = secondY
secondX = time
secondY = value
return dy/dx</function>
<linked_source>desired lat accel</linked_source>
</snippet>
<snippet name="carState.vEgo kmh">
<global></global>
<function>return value * 3.6</function>
<linked_source>/carState/vEgo</linked_source>
</snippet>
<snippet name="engaged_accel_plan">
<global>engage_delay = 5
last_bad_time = -engage_delay</global>
<function>accel = value
brake = v1
gas = v2
enabled = v3
if (brake ~= 0 or gas ~= 0 or enabled == 0) then
last_bad_time = time
end
if (time > last_bad_time + engage_delay) then
return value
else
return 0
end</function>
<linked_source>/longitudinalPlan/accels/0</linked_source>
<additional_sources>
<v1>/carState/brakePressed</v1>
<v2>/carState/gasPressed</v2>
<v3>/carControl/enabled</v3>
</additional_sources>
</snippet>
<snippet name="Angle Error">
<global>last_angle_requested = 0</global>
<function>angle_error = last_angle_requested - v1
last_angle_requested = value
return angle_error</function>
<linked_source>/carControl/actuators/steeringAngleDeg</linked_source>
<additional_sources>
<v1>/carState/steeringAngleDeg</v1>
</additional_sources>
</snippet>
<snippet name="IMU_LatAccelVal_ms^2">
<global></global>
<function>return value * -9.8</function>
<linked_source>/can/1/IMU_01_10ms/IMU_LatAccelVal</linked_source>
<additional_sources>
<v1>/carState/steeringPressed</v1>
<v2>/carControl/latActive</v2>
</additional_sources>
</snippet>
<snippet name="desired lat accel">
<global></global>
<function>return value * v1^2</function>
<linked_source>/controlsState/desiredCurvature</linked_source>
<additional_sources>
<v1>/carState/vEgo</v1>
</additional_sources>
</snippet>
<snippet name="engaged_accel_actuator">
<global>engage_delay = 5
last_bad_time = -engage_delay</global>
<function>accel = value
brake = v1
gas = v2
enabled = v3
if (brake ~= 0 or gas ~= 0 or enabled == 0) then
last_bad_time = time
end
if (time > last_bad_time + engage_delay) then
return value
else
return 0
end</function>
<linked_source>/carControl/actuators/accel</linked_source>
<additional_sources>
<v1>/carState/brakePressed</v1>
<v2>/carState/gasPressed</v2>
<v3>/carControl/enabled</v3>
</additional_sources>
</snippet>
<snippet name="engaged_accel_actual">
<global>engage_delay = 5
last_bad_time = -engage_delay</global>
<function>accel = value
brake = v1
gas = v2
enabled = v3
if (brake ~= 0 or gas ~= 0 or enabled == 0) then
last_bad_time = time
end
if (time > last_bad_time + engage_delay) then
return value
else
return 0
end</function>
<linked_source>/carState/aEgo</linked_source>
<additional_sources>
<v1>/carState/brakePressed</v1>
<v2>/carState/gasPressed</v2>
<v3>/carControl/enabled</v3>
</additional_sources>
</snippet>
<snippet name="roll compensated lateral acceleration">
<global></global>
<function>if (v3 == 0 and v4 == 1) then
return (value * v1 ^ 2) - (v2 * 9.81)
end
return 0</function>
<linked_source>/controlsState/curvature</linked_source>
<additional_sources>
<v1>/carState/vEgo</v1>
<v2>/liveParameters/roll</v2>
<v3>/carState/steeringPressed</v3>
<v4>/carControl/latActive</v4>
</additional_sources>
</snippet>
<snippet name="Zero">
<global></global>
<function>return (0)</function>
<linked_source>/carState/canValid</linked_source>
</snippet>
<snippet name="abs(des la accel)">
<global></global>
<function>return math.abs(value)</function>
<linked_source>desired lat accel</linked_source>
</snippet>
<snippet name="Desired lateral accel (roll compensated)">
<global></global>
<function>return (value * v1 ^ 2) - (v2 * 9.81)</function>
<linked_source>/controlsState/desiredCurvature</linked_source>
<additional_sources>
<v1>/carState/vEgo</v1>
<v2>/liveParameters/roll</v2>
</additional_sources>
</snippet>
<snippet name="Actual lateral accel (roll compensated)">
<global></global>
<function>return (value * v1 ^ 2) - (v2 * 9.81)</function>
<linked_source>/controlsState/curvature</linked_source>
<additional_sources>
<v1>/carState/vEgo</v1>
<v2>/liveParameters/roll</v2>
</additional_sources>
</snippet>
<snippet name="max torque(calc)">
<global>min=0
max=250
max_from_speed=96
rate_lim = 500
la_deadzone = 0.38
k1=200
k2=20
k3=1.0
k4=1
k5=10
old = 0
function sign(number)
return number > 0 and 1 or (number == 0 and 0 or -1)
end
function apply_rate_limit(old, new, limit)
return math.min(math.max(new, old - limit), old + limit)
end
function apply_deadzone(val, deadzone)
if math.abs(val) &lt;= deadzone then
return 0.0
elseif val &lt; 0.0 then
return val + deadzone
else
return val - deadzone
end
end</global>
<function>la = apply_deadzone(v2, la_deadzone)
lj = v3
if la == 0.0 then
lj = 0.0
end
fla = math.min(math.abs(k1 * la)^k3, max)
flj = math.min(math.abs(k2 * lj)^k4, max)
out = fla
flv = math.min(max_from_speed, k5 * v4)
out = out + flv
out = math.max(math.min(out, max), min)
if sign(la) == sign(lj) then
out = out - flj
else
out = out + flj
end
if v5 == 1.0 then
out = 0.0
end
out = math.max(math.min(out, max), min)
out = apply_rate_limit(old, out, rate_lim)
old = out
return out</function>
<linked_source>/can/1/LFA_ALT/LKAS_ANGLE_CMD</linked_source>
<additional_sources>
<v1>ang_cmd rate</v1>
<v2>desired lat accel</v2>
<v3>desired lateral jark</v3>
<v4>/carState/vEgo</v4>
<v5>/can/1/LFA_ALT/LKAS_ANGLE_ACTIVE</v5>
</additional_sources>
</snippet>
<snippet name="carState.vEgo mph">
<global></global>
<function>return value * 2.23694</function>
<linked_source>/carState/vEgo</linked_source>
</snippet>
<snippet name="engaged curvature yaw">
<global>engage_delay = 5
last_bad_time = -engage_delay</global>
<function>curvature = value / v3
pressed = v1
enabled = v2
if (pressed == 1 or enabled == 0) then
last_bad_time = time
end
if (time > last_bad_time + engage_delay) then
return curvature
else
return 0
end</function>
<linked_source>/liveLocationKalman/angularVelocityCalibrated/value/2</linked_source>
<additional_sources>
<v1>/carState/steeringPressed</v1>
<v2>/carControl/enabled</v2>
<v3>/liveLocationKalman/velocityCalibrated/value/0</v3>
</additional_sources>
</snippet>
</customMathEquations>
<snippets/>
<!-- - - - - - - - - - - - - - - -->
</root>

View File

@@ -0,0 +1,274 @@
<?xml version='1.0' encoding='UTF-8'?>
<root>
<tabbed_widget name="Main Window" parent="main_window">
<Tab containers="1" tab_name="tab1">
<Container>
<DockSplitter sizes="0.500397;0.499603" count="2" orientation="-">
<DockArea name="...">
<plot flip_x="false" flip_y="false" mode="TimeSeries" style="Lines">
<range top="256.250000" left="0.000000" right="421.102293" bottom="-6.250000"/>
<limitY/>
<curve color="#1ac938" name="/can/1/LFA_ALT/LKAS_ANGLE_MAX_TORQUE"/>
<curve color="#17becf" name="max torque(calc)">
<transform name="Moving Average" alias="max torque(calc)[Moving Average]">
<options compensate_offset="true" value="10"/>
</transform>
</curve>
</plot>
</DockArea>
<DockArea name="...">
<plot flip_x="false" flip_y="false" mode="TimeSeries" style="Lines">
<range top="2.776497" left="0.000000" right="421.102293" bottom="-2.918548"/>
<limitY/>
<curve color="#f14cc1" name="desired lat accel"/>
<curve color="#888888" name="zero"/>
</plot>
</DockArea>
</DockSplitter>
</Container>
</Tab>
<currentTabIndex index="0"/>
</tabbed_widget>
<use_relative_time_offset enabled="1"/>
<!-- - - - - - - - - - - - - - - -->
<!-- - - - - - - - - - - - - - - -->
<Plugins>
<plugin ID="DataLoad CSV">
<default time_axis="" delimiter="0"/>
</plugin>
<plugin ID="DataLoad MCAP"/>
<plugin ID="DataLoad Rlog"/>
<plugin ID="DataLoad ULog"/>
<plugin ID="Cereal Subscriber"/>
<plugin ID="UDP Server"/>
<plugin ID="WebSocket Server"/>
<plugin ID="ZMQ Subscriber"/>
<plugin ID="Fast Fourier Transform"/>
<plugin ID="Quaternion to RPY"/>
<plugin ID="Reactive Script Editor">
<library code="--[[ Helper function to create a series from arrays&#xa;&#xa; new_series: a series previously created with ScatterXY.new(name)&#xa; prefix: prefix of the timeseries, before the index of the array&#xa; suffix_X: suffix to complete the name of the series containing the X value. If [nil], use the index of the array.&#xa; suffix_Y: suffix to complete the name of the series containing the Y value&#xa; timestamp: usually the tracker_time variable&#xa; &#xa; Example:&#xa; &#xa; Assuming we have multiple series in the form:&#xa; &#xa; /trajectory/node.{X}/position/x&#xa; /trajectory/node.{X}/position/y&#xa; &#xa; where {N} is the index of the array (integer). We can create a reactive series from the array with:&#xa; &#xa; new_series = ScatterXY.new(&quot;my_trajectory&quot;) &#xa; CreateSeriesFromArray( new_series, &quot;/trajectory/node&quot;, &quot;position/x&quot;, &quot;position/y&quot;, tracker_time );&#xa;--]]&#xa;&#xa;function CreateSeriesFromArray( new_series, prefix, suffix_X, suffix_Y, timestamp )&#xa; &#xa; --- clear previous values&#xa; new_series:clear()&#xa; &#xa; --- Append points to new_series&#xa; index = 0&#xa; while(true) do&#xa;&#xa; x = index;&#xa; -- if not nil, get the X coordinate from a series&#xa; if suffix_X ~= nil then &#xa; series_x = TimeseriesView.find( string.format( &quot;%s.%d/%s&quot;, prefix, index, suffix_X) )&#xa; if series_x == nil then break end&#xa; x = series_x:atTime(timestamp)&#x9; &#xa; end&#xa; &#xa; series_y = TimeseriesView.find( string.format( &quot;%s.%d/%s&quot;, prefix, index, suffix_Y) )&#xa; if series_y == nil then break end &#xa; y = series_y:atTime(timestamp)&#xa; &#xa; new_series:push_back(x,y)&#xa; index = index+1&#xa; end&#xa;end&#xa;&#xa;--[[ Similar to the built-in function GetSeriesNames(), but select only the names with a give prefix. --]]&#xa;&#xa;function GetSeriesNamesByPrefix(prefix)&#xa; -- GetSeriesNames(9 is a built-in function&#xa; all_names = GetSeriesNames()&#xa; filtered_names = {}&#xa; for i, name in ipairs(all_names) do&#xa; -- check the prefix&#xa; if name:find(prefix, 1, #prefix) then&#xa; table.insert(filtered_names, name);&#xa; end&#xa; end&#xa; return filtered_names&#xa;end&#xa;&#xa;--[[ Modify an existing series, applying offsets to all their X and Y values&#xa;&#xa; series: an existing timeseries, obtained with TimeseriesView.find(name)&#xa; delta_x: offset to apply to each x value&#xa; delta_y: offset to apply to each y value &#xa; &#xa;--]]&#xa;&#xa;function ApplyOffsetInPlace(series, delta_x, delta_y)&#xa; -- use C++ indeces, not Lua indeces&#xa; for index=0, series:size()-1 do&#xa; x,y = series:at(index)&#xa; series:set(index, x + delta_x, y + delta_y)&#xa; end&#xa;end&#xa;"/>
<scripts/>
</plugin>
<plugin ID="CSV Exporter"/>
</Plugins>
<customMathEquations>
<snippet name="zero">
<global>min=0
max=250
max_from_speed=96
rate_lim = 500
la_deadzone = 0.38
k1=200
k2=20
k3=1.0
k4=1
k5=10
old = 0
function sign(number)
return number > 0 and 1 or (number == 0 and 0 or -1)
end
function apply_rate_limit(old, new, limit)
return math.min(math.max(new, old - limit), old + limit)
end
function apply_deadzone(val, deadzone)
if math.abs(val) &lt;= deadzone then
return 0.0
elseif val &lt; 0.0 then
return val + deadzone
else
return val - deadzone
end
end</global>
<function>return 0</function>
<linked_source>/carState/aEgo</linked_source>
</snippet>
<snippet name="max torque lj adj">
<global>min=0
max=250
max_from_speed=96
k1=200
k2=30
k3=1
k4=1
k5=10
function sign(number)
return number > 0 and 1 or (number == 0 and 0 or -1)
end</global>
<function>return 250 - value * 20</function>
<linked_source>desired lateral jark</linked_source>
</snippet>
<snippet name="ang_cmd rate">
<global>firstX = 0
firstY = 0
is_first = true
secondX = 0
secondY = 0
is_second = false</global>
<function>-- Wait for initial values
if (is_first) then
is_first = false
is_second = true
firstX = time
firstY = value
end
if (is_second) then
is_second = false
secondX = time
secondY = value
end
-- Central derivative: dy/dx ~= f(x+delta_x)-f(x-delta_x)/(2*delta_x)
dx = time - firstX
dy = value - firstY
-- Increment
firstX = secondX
firstY = secondY
secondX = time
secondY = value
return dy/dx</function>
<linked_source>/can/1/LFA_ALT/LKAS_ANGLE_CMD</linked_source>
</snippet>
<snippet name="max torque(calc)">
<global>min=0
max=250
max_from_speed=96
rate_lim = 500
la_deadzone = 0.38
k1=200
k2=20
k3=1.0
k4=1
k5=10
old = 0
function sign(number)
return number > 0 and 1 or (number == 0 and 0 or -1)
end
function apply_rate_limit(old, new, limit)
return math.min(math.max(new, old - limit), old + limit)
end
function apply_deadzone(val, deadzone)
if math.abs(val) &lt;= deadzone then
return 0.0
elseif val &lt; 0.0 then
return val + deadzone
else
return val - deadzone
end
end</global>
<function>la = apply_deadzone(v2, la_deadzone)
lj = v3
if la == 0.0 then
lj = 0.0
end
fla = math.min(math.abs(k1 * la)^k3, max)
flj = math.min(math.abs(k2 * lj)^k4, max)
out = fla
flv = math.min(max_from_speed, k5 * v4)
out = out + flv
out = math.max(math.min(out, max), min)
if sign(la) == sign(lj) then
out = out - flj
else
out = out + flj
end
if v5 == 1.0 then
out = 0.0
end
out = math.max(math.min(out, max), min)
out = apply_rate_limit(old, out, rate_lim)
old = out
return out</function>
<linked_source>/can/1/LFA_ALT/LKAS_ANGLE_CMD</linked_source>
<additional_sources>
<v1>ang_cmd rate</v1>
<v2>desired lat accel</v2>
<v3>desired lateral jark</v3>
<v4>/carState/vEgo</v4>
<v5>/can/1/LFA_ALT/LKAS_ANGLE_ACTIVE</v5>
</additional_sources>
</snippet>
<snippet name="desired lateral jark">
<global>firstX = 0
firstY = 0
is_first = true
secondX = 0
secondY = 0
is_second = false</global>
<function>-- Wait for initial values
if (is_first) then
is_first = false
is_second = true
firstX = time
firstY = value
end
if (is_second) then
is_second = false
secondX = time
secondY = value
end
-- Central derivative: dy/dx ~= f(x+delta_x)-f(x-delta_x)/(2*delta_x)
dx = time - firstX
dy = value - firstY
-- Increment
firstX = secondX
firstY = secondY
secondX = time
secondY = value
return dy/dx</function>
<linked_source>desired lat accel</linked_source>
</snippet>
<snippet name="abs(des la accel)">
<global></global>
<function>return math.abs(value)</function>
<linked_source>desired lat accel</linked_source>
</snippet>
<snippet name="desired lat accel">
<global></global>
<function>return value * v1^2</function>
<linked_source>/controlsState/desiredCurvature</linked_source>
<additional_sources>
<v1>/carState/vEgo</v1>
</additional_sources>
</snippet>
<snippet name="abs(ang_cmd)">
<global></global>
<function>return math.abs(value)</function>
<linked_source>/can/1/LFA_ALT/LKAS_ANGLE_CMD</linked_source>
</snippet>
</customMathEquations>
<snippets/>
<!-- - - - - - - - - - - - - - - -->
</root>

View File

@@ -0,0 +1,175 @@
<?xml version='1.0' encoding='UTF-8'?>
<root>
<tabbed_widget parent="main_window" name="Main Window">
<Tab tab_name="tab1" containers="1">
<Container>
<DockSplitter sizes="0.25;0.25;0.25;0.25" count="4" orientation="-">
<DockArea name="...">
<plot flip_x="false" style="Lines" flip_y="false" mode="TimeSeries">
<range bottom="-3.582051" right="1431.113121" top="5.314632" left="5.322399"/>
<limitY/>
<curve name="Actual lateral accel (roll compensated)" color="#1ac938"/>
<curve name="Desired lateral accel (roll compensated)" color="#ff7f0e"/>
<curve name="IMU_LatAccelVal_ms^2" color="#f14cc1"/>
</plot>
</DockArea>
<DockArea name="...">
<plot flip_x="false" style="Lines" flip_y="false" mode="TimeSeries">
<range bottom="-3.741271" right="1431.113121" top="3.756006" left="5.322399"/>
<limitY/>
<curve name="roll compensated lateral acceleration" color="#ff7f0e"/>
<curve name="IMU_LatAccelVal_Ms^2" color="#1ac938"/>
<curve name="IMU_LatAccelVal_ms^2_roll_compensated" color="#9467bd"/>
</plot>
</DockArea>
<DockArea name="...">
<plot flip_x="false" style="Lines" flip_y="false" mode="TimeSeries">
<range bottom="-0.025000" right="1431.113121" top="1.025000" left="5.322399"/>
<limitY/>
<curve name="/carState/steeringPressed" color="#0097ff"/>
<curve name="/carOutput/actuatorsOutput/torque" color="#d62728"/>
</plot>
</DockArea>
<DockArea name="...">
<plot flip_x="false" style="Lines" flip_y="false" mode="TimeSeries">
<range bottom="-1.660728" right="1431.113121" top="67.942958" left="5.322399"/>
<limitY/>
<curve name="/carState/vEgo" color="#f14cc1">
<transform name="Scale/Offset" alias="/carState/vEgo[Scale/Offset]">
<options value_scale="2.23694" time_offset="0" value_offset="0"/>
</transform>
</curve>
</plot>
</DockArea>
</DockSplitter>
</Container>
</Tab>
<Tab tab_name="tab2" containers="1">
<Container>
<DockSplitter sizes="0.5;0.5" count="2" orientation="-">
<DockArea name="...">
<plot flip_x="false" style="Lines" flip_y="false" mode="TimeSeries">
<range bottom="30595.900000" right="1431.113121" top="34884.100000" left="5.322399"/>
<limitY/>
<curve name="/can/1/IMU_01_10ms/IMU_RollRtVal" color="#17becf"/>
</plot>
</DockArea>
<DockArea name="...">
<plot flip_x="false" style="Lines" flip_y="false" mode="TimeSeries">
<range bottom="-0.058795" right="1431.113121" top="0.072448" left="5.322399"/>
<limitY/>
<curve name="/liveParameters/roll" color="#bcbd22"/>
</plot>
</DockArea>
</DockSplitter>
</Container>
</Tab>
<currentTabIndex index="1"/>
</tabbed_widget>
<use_relative_time_offset enabled="1"/>
<!-- - - - - - - - - - - - - - - -->
<!-- - - - - - - - - - - - - - - -->
<Plugins>
<plugin ID="DataLoad CSV">
<default time_axis="" delimiter="0"/>
</plugin>
<plugin ID="DataLoad MCAP"/>
<plugin ID="DataLoad Rlog"/>
<plugin ID="DataLoad ULog"/>
<plugin ID="Cereal Subscriber"/>
<plugin ID="UDP Server"/>
<plugin ID="WebSocket Server"/>
<plugin ID="ZMQ Subscriber"/>
<plugin ID="Fast Fourier Transform"/>
<plugin ID="Quaternion to RPY"/>
<plugin ID="Reactive Script Editor">
<library code="--[[ Helper function to create a series from arrays&#xa;&#xa; new_series: a series previously created with ScatterXY.new(name)&#xa; prefix: prefix of the timeseries, before the index of the array&#xa; suffix_X: suffix to complete the name of the series containing the X value. If [nil], use the index of the array.&#xa; suffix_Y: suffix to complete the name of the series containing the Y value&#xa; timestamp: usually the tracker_time variable&#xa; &#xa; Example:&#xa; &#xa; Assuming we have multiple series in the form:&#xa; &#xa; /trajectory/node.{X}/position/x&#xa; /trajectory/node.{X}/position/y&#xa; &#xa; where {N} is the index of the array (integer). We can create a reactive series from the array with:&#xa; &#xa; new_series = ScatterXY.new(&quot;my_trajectory&quot;) &#xa; CreateSeriesFromArray( new_series, &quot;/trajectory/node&quot;, &quot;position/x&quot;, &quot;position/y&quot;, tracker_time );&#xa;--]]&#xa;&#xa;function CreateSeriesFromArray( new_series, prefix, suffix_X, suffix_Y, timestamp )&#xa; &#xa; --- clear previous values&#xa; new_series:clear()&#xa; &#xa; --- Append points to new_series&#xa; index = 0&#xa; while(true) do&#xa;&#xa; x = index;&#xa; -- if not nil, get the X coordinate from a series&#xa; if suffix_X ~= nil then &#xa; series_x = TimeseriesView.find( string.format( &quot;%s.%d/%s&quot;, prefix, index, suffix_X) )&#xa; if series_x == nil then break end&#xa; x = series_x:atTime(timestamp)&#x9; &#xa; end&#xa; &#xa; series_y = TimeseriesView.find( string.format( &quot;%s.%d/%s&quot;, prefix, index, suffix_Y) )&#xa; if series_y == nil then break end &#xa; y = series_y:atTime(timestamp)&#xa; &#xa; new_series:push_back(x,y)&#xa; index = index+1&#xa; end&#xa;end&#xa;&#xa;--[[ Similar to the built-in function GetSeriesNames(), but select only the names with a give prefix. --]]&#xa;&#xa;function GetSeriesNamesByPrefix(prefix)&#xa; -- GetSeriesNames(9 is a built-in function&#xa; all_names = GetSeriesNames()&#xa; filtered_names = {}&#xa; for i, name in ipairs(all_names) do&#xa; -- check the prefix&#xa; if name:find(prefix, 1, #prefix) then&#xa; table.insert(filtered_names, name);&#xa; end&#xa; end&#xa; return filtered_names&#xa;end&#xa;&#xa;--[[ Modify an existing series, applying offsets to all their X and Y values&#xa;&#xa; series: an existing timeseries, obtained with TimeseriesView.find(name)&#xa; delta_x: offset to apply to each x value&#xa; delta_y: offset to apply to each y value &#xa; &#xa;--]]&#xa;&#xa;function ApplyOffsetInPlace(series, delta_x, delta_y)&#xa; -- use C++ indeces, not Lua indeces&#xa; for index=0, series:size()-1 do&#xa; x,y = series:at(index)&#xa; series:set(index, x + delta_x, y + delta_y)&#xa; end&#xa;end&#xa;"/>
<scripts/>
</plugin>
<plugin ID="CSV Exporter"/>
</Plugins>
<!-- - - - - - - - - - - - - - - -->
<!-- - - - - - - - - - - - - - - -->
<customMathEquations>
<snippet name="IMU_LatAccelVal_ms^2">
<global></global>
<function>return value * -9.8</function>
<linked_source>/can/1/IMU_01_10ms/IMU_LatAccelVal</linked_source>
<additional_sources>
<v1>/carState/steeringPressed</v1>
<v2>/carControl/latActive</v2>
</additional_sources>
</snippet>
<snippet name="roll compensated lateral acceleration">
<global></global>
<function>if (v3 == 0 and v4 == 1) then
return (value * v1 ^ 2) - (v2 * 9.81)
end
return 0</function>
<linked_source>/controlsState/curvature</linked_source>
<additional_sources>
<v1>/carState/vEgo</v1>
<v2>/liveParameters/roll</v2>
<v3>/carState/steeringPressed</v3>
<v4>/carControl/latActive</v4>
</additional_sources>
</snippet>
<snippet name="IMU_LatAccelVal_Ms^2">
<global></global>
<function>if (v1 == 0 and v2 == 1) then
return value * -9.8
end
return 0</function>
<linked_source>/can/1/IMU_01_10ms/IMU_LatAccelVal</linked_source>
<additional_sources>
<v1>/carState/steeringPressed</v1>
<v2>/carControl/latActive</v2>
</additional_sources>
</snippet>
<snippet name="IMU_LatAccelVal_Ms^3">
<global></global>
<function>return value * -9.8</function>
<linked_source>/can/1/IMU_01_10ms/IMU_LatAccelVal</linked_source>
<additional_sources>
<v1>/carState/steeringPressed</v1>
<v2>/carControl/latActive</v2>
</additional_sources>
</snippet>
<snippet name="IMU_LatAccelVal_ms^2_roll_compensated">
<global></global>
<function>
if (v1 == 0 and v2 == 1) then
return (value * -9.8) - (v3 * 9.81)
end
return 0</function>
<linked_source>/can/1/IMU_01_10ms/IMU_LatAccelVal</linked_source>
<additional_sources>
<v1>/carState/steeringPressed</v1>
<v2>/carControl/latActive</v2>
<v3>/liveParameters/roll</v3>
</additional_sources>
</snippet>
<snippet name="Actual lateral accel (roll compensated)">
<global></global>
<function>return (value * v1 ^ 2) - (v2 * 9.81)</function>
<linked_source>/controlsState/curvature</linked_source>
<additional_sources>
<v1>/carState/vEgo</v1>
<v2>/liveParameters/roll</v2>
</additional_sources>
</snippet>
<snippet name="Desired lateral accel (roll compensated)">
<global></global>
<function>return (value * v1 ^ 2) - (v2 * 9.81)</function>
<linked_source>/controlsState/desiredCurvature</linked_source>
<additional_sources>
<v1>/carState/vEgo</v1>
<v2>/liveParameters/roll</v2>
</additional_sources>
</snippet>
</customMathEquations>
<snippets/>
<!-- - - - - - - - - - - - - - - -->
</root>