From 2563dca0eb52597645775ae7aeaae7e8ddd34bb4 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 25 Mar 2026 04:45:24 -0400 Subject: [PATCH] update --- sunnypilot/sunnylink/docs/README.md | 16 ++++++------- sunnypilot/sunnylink/docs/REFERENCE.md | 10 ++++---- .../tools/generate_settings_schema.py | 0 .../sunnylink/tools/validate_settings_ui.py | 24 +++++++++++-------- 4 files changed, 27 insertions(+), 23 deletions(-) mode change 100644 => 100755 sunnypilot/sunnylink/tools/generate_settings_schema.py mode change 100644 => 100755 sunnypilot/sunnylink/tools/validate_settings_ui.py diff --git a/sunnypilot/sunnylink/docs/README.md b/sunnypilot/sunnylink/docs/README.md index 9d7e02c20f..3ab7f29fcc 100644 --- a/sunnypilot/sunnylink/docs/README.md +++ b/sunnypilot/sunnylink/docs/README.md @@ -53,7 +53,7 @@ All metadata (titles, descriptions, options, min/max/step/unit) lives **inline o **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**: Greyed out (disabled) when rules fail. Frontend shows a contextual badge explaining why. +**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` @@ -216,7 +216,7 @@ Individual options within `multiple_button` or `option` widgets can have their o } ``` -When an option's enablement fails, that option is greyed out (disabled) but still visible. +When an option's enablement fails, that option is grayed out (disabled) but still visible. ### Show only when another setting is on @@ -246,16 +246,16 @@ Note: Due to the "dim instead of hide" design, this setting will be dimmed (not ```json { - "key": "OptionA", + "key": "FeatureAlpha", "widget": "toggle", - "title": "Option A", - "enablement": [{"type": "param", "key": "OptionB", "equals": false}] + "title": "Feature Alpha", + "enablement": [{"type": "param", "key": "FeatureBeta", "equals": false}] }, { - "key": "OptionB", + "key": "FeatureBeta", "widget": "toggle", - "title": "Option B", - "enablement": [{"type": "param", "key": "OptionA", "equals": false}] + "title": "Feature Beta", + "enablement": [{"type": "param", "key": "FeatureAlpha", "equals": false}] } ``` diff --git a/sunnypilot/sunnylink/docs/REFERENCE.md b/sunnypilot/sunnylink/docs/REFERENCE.md index fbf5363ed2..c18a266bbd 100644 --- a/sunnypilot/sunnylink/docs/REFERENCE.md +++ b/sunnypilot/sunnylink/docs/REFERENCE.md @@ -176,7 +176,7 @@ Root | `unit` | No | Unit label. Static: `"seconds"`. Dynamic: `{"metric": "km/h", "imperial": "mph"}` (frontend resolves based on `IsMetric` param). See [Dynamic Units](#dynamic-units). | | `value_map` | No | Maps stored values to display labels | | `visibility` | No | Rules controlling visibility. Settings are **never hidden**, always dimmed with UNAVAILABLE badge when rules fail. See [Visibility vs Enablement](#visibility-vs-enablement). | -| `enablement` | No | Rules controlling enabled/disabled state. Greyed out with contextual badge when rules fail. | +| `enablement` | No | Rules controlling enabled/disabled state. Grayed out with contextual badge when rules fail. | | `blocked` | No | When `true`, this param cannot be modified remotely (device-only). Frontend shows as read-only. | | `title_param_suffix` | No | Dynamic title suffix. Object with `param` (param key) and `values` (mapping of param values to suffix strings). | | `sub_items` | No | Child items that appear indented below this item | @@ -235,13 +235,13 @@ Root - **`visibility` rules** (NEW): Settings are **never hidden**. When rules fail, the setting is **dimmed with an UNAVAILABLE badge** so users know it exists but isn't applicable. This prevents confusion and preserves UI stability. -- **`enablement` rules**: When rules fail, the setting is **greyed out** with a contextual badge explaining why (e.g., "Requires longitudinal control"). User can still see it exists. +- **`enablement` rules**: When rules fail, the setting is **grayed out** with a contextual badge explaining why (e.g., "Requires longitudinal control"). User can still see it exists. The "dim instead of hide" approach (visibility-based dimming instead of hiding) provides better UX: settings remain discoverable, and users understand why a setting is unavailable. ## Rules Reference -Rules control **visibility** (dimmed) and **enablement** (greyed out). +Rules control **visibility** (dimmed) and **enablement** (grayed out). - All rules in an array use **AND** logic (all must pass for the rule to pass) - If ANY rule fails, the condition is unsatisfied @@ -660,7 +660,7 @@ Individual options within `multiple_button` or `option` widgets can have their o } ``` -When an option's enablement fails, that option is **greyed out** (disabled but still visible and selectable). This prevents users from changing to an unavailable option but keeps the UI stable. +When an option's enablement fails, that option is **grayed out** (disabled but still visible and selectable). This prevents users from changing to an unavailable option but keeps the UI stable. --- @@ -1057,7 +1057,7 @@ At device boot, the generator reads `settings_ui.json`, compresses it, and write **Q: What's the difference between `visibility` and `enablement`?** - `visibility`: hidden entirely when rules fail (user doesn't know it exists) -- `enablement`: visible but greyed out when rules fail (user sees it but can't change it) +- `enablement`: visible but grayed out when rules fail (user sees it but can't change it) **Q: How do I test my changes locally?** Run the generator directly to see the full output: diff --git a/sunnypilot/sunnylink/tools/generate_settings_schema.py b/sunnypilot/sunnylink/tools/generate_settings_schema.py old mode 100644 new mode 100755 diff --git a/sunnypilot/sunnylink/tools/validate_settings_ui.py b/sunnypilot/sunnylink/tools/validate_settings_ui.py old mode 100644 new mode 100755 index 2e179efb69..b04b0906be --- a/sunnypilot/sunnylink/tools/validate_settings_ui.py +++ b/sunnypilot/sunnylink/tools/validate_settings_ui.py @@ -284,8 +284,10 @@ def check_structural(data: dict, result: ValidationResult) -> None: 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})") + 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)) @@ -298,7 +300,7 @@ def check_item_completeness(data: dict, result: ValidationResult) -> None: all_items = collect_all_items(data) issues: list[str] = [] - for path, item in all_items: + for _path, item in all_items: key = item.get("key", "unknown") if "title" not in item: issues.append(f"{key}: missing 'title'") @@ -350,7 +352,6 @@ def check_no_duplicate_keys(data: dict, result: ValidationResult) -> None: def check_rule_wellformedness(data: dict, result: ValidationResult) -> None: """Check 5: All rules have valid structure.""" all_items = collect_all_items(data) - has_errors = False # Save current error count to detect new errors error_count_before = len(result.failed) @@ -358,8 +359,7 @@ def check_rule_wellformedness(data: dict, result: ValidationResult) -> None: for path, item in all_items: for ctx, rules in collect_rules_from_item(item): for i, rule in enumerate(rules): - if not validate_rule(rule, f"{path} > {ctx}[{i}]", result, CAPABILITY_FIELDS): - has_errors = True + validate_rule(rule, f"{path} > {ctx}[{i}]", result, CAPABILITY_FIELDS) # Also validate trigger_condition rules on sub_panels for panel in data.get("panels", []): @@ -438,15 +438,19 @@ def check_sub_panel_triggers(data: dict, result: ValidationResult) -> None: 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}'") + 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}'") + 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))