From dbb1fe654738f6b59b410d18f78a0890e39d241b Mon Sep 17 00:00:00 2001 From: dirwin31 <83434411+dirwin31@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:46:51 -0800 Subject: [PATCH] Add a StarPort section to the Developer page with the new pairing switch and an H.265 setting The Developer page gets a heading row, StarPort, that groups the StarPort settings: the New pairing experience switch (on unless turned off), Stay on Wi-Fi (moved under the heading, same key, label and default) and Allow H.265 video (off by default, daemon setting allow_hevc). The heading is a row whose key starts with heading:, projected with heading=true. It has no choices, is never available and cannot be changed, and Galaxy draws it as a label. The new pairing switch is a plain file next to the other StarPort data rather than a Params key, so it reads and changes with the StarPort service down and needs no device rebuild; absent or unreadable means on. --- .../starpilot/galaxy/developer_settings.py | 67 ++++++++++++++++--- .../starpilot/galaxy/setting_descriptions.py | 7 ++ openpilot/starpilot/galaxy/settings.py | 5 +- .../tests/description-key-inventory.json | 20 +++--- .../galaxy/tests/test_developer_settings.py | 64 +++++++++++++++++- .../galaxy/tests/test_settings_controls.mjs | 12 ++++ openpilot/starpilot/galaxy/web/css/galaxy.css | 3 + .../galaxy/web/js/galaxy-setting-row.js | 8 +-- openpilot/starpilot/system/starport/daemon.py | 2 + .../starpilot/system/starport/identity.py | 2 + .../starpilot/system/starport/new_setup.py | 30 +++++++++ .../starpilot/system/starport/supervisor.py | 10 ++- .../starpilot/ui/feature_settings_state.py | 8 +++ 13 files changed, 212 insertions(+), 26 deletions(-) create mode 100644 openpilot/starpilot/system/starport/new_setup.py diff --git a/openpilot/starpilot/galaxy/developer_settings.py b/openpilot/starpilot/galaxy/developer_settings.py index a81232caff..a04fa617db 100644 --- a/openpilot/starpilot/galaxy/developer_settings.py +++ b/openpilot/starpilot/galaxy/developer_settings.py @@ -6,7 +6,8 @@ from openpilot.starpilot.longitudinal.voacc import KEY, DESCRIPTION from openpilot.starpilot.ui.calibration_reset import calibration_reset from openpilot.starpilot.saved_document import commit_exact from openpilot.starpilot.saved_source import read_saved -from openpilot.starpilot.ui.feature_settings_state import FeatureRow, FeatureSettingsState +from openpilot.starpilot.system.starport import new_setup +from openpilot.starpilot.ui.feature_settings_state import HEADING_PREFIX, FeatureRow, FeatureSettingsState STARPORT_WIFI_KEY = "StarPortDedicatedWifi" @@ -14,6 +15,13 @@ STARPORT_WIFI_DESCRIPTION = ("Experimental. Join the car's Wi-Fi on a second int "phone hotspot during wireless StarPort. Applies from the next StarPort connection.") CALIBRATION_RESET = "reset_calibration" CALIBRATION_RESET_DESCRIPTION = "Clear camera and steering calibration. The device recalibrates on the next drive." +STARPORT_HEADING_KEY = HEADING_PREFIX + "starport" +STARPORT_NEW_SETUP_KEY = "StarPortNewSetup" +STARPORT_NEW_SETUP_DESCRIPTION = ("Guided StarPort setup with a live checklist of what is being tried. Turn off to use the classic " + + "setup screens. Applies the next time the setup screen opens.") +STARPORT_HEVC_KEY = "StarPortAllowHevc" +STARPORT_HEVC_DESCRIPTION = ("Experimental. Only for a car that offers H.265 and no H.264 video. Cars that offer H.264 are not " + + "affected. Applies from the next StarPort connection.") DRIVER_MONITORING_RESET = "reset_driver_monitoring" DRIVER_MONITORING_RESET_DESCRIPTION = "Clear saved wheel-side detection. Automatic detection restarts on the next drive." @@ -25,14 +33,40 @@ class DeveloperSettingsOwner: self.panda_wake = panda_wake self.starport = starport # returns the StarPort daemon client - def _starport_enabled(self): + def _starport_status(self): if self.starport is None: return None try: - return bool(self.starport().call("status")["status"].get("dedicated_wifi_interface")) + status = self.starport().call("status")["status"] + return status if isinstance(status, dict) else None except (OSError, RuntimeError, ValueError, KeyError, TypeError): return None # the service is off; there is nothing to configure + def _starport_enabled(self): + status = self._starport_status() + return None if status is None else bool(status.get("dedicated_wifi_interface")) + + def _starport_hevc(self): + status = self._starport_status() + return None if status is None else bool(status.get("allow_hevc")) + + def _starport_rows(self, parked): + """The StarPort section. The switch back to classic setup is always listed; the daemon's settings only while it runs.""" + new_setup_on = new_setup.enabled() + held = "Park the vehicle to change this setting." + rows = [FeatureRow(STARPORT_HEADING_KEY, "StarPort", "", available=False), + FeatureRow(STARPORT_NEW_SETUP_KEY, "StarPort: New pairing experience", "On" if new_setup_on else "Off", + b"1" if new_setup_on else b"0", ("Off", "On"), available=parked, + reason=STARPORT_NEW_SETUP_DESCRIPTION if parked else held, default_value="On")] + status = self._starport_status() + if status is not None: + enabled, hevc = bool(status.get("dedicated_wifi_interface")), bool(status.get("allow_hevc")) + rows.append(FeatureRow(STARPORT_WIFI_KEY, "StarPort: Stay on Wi-Fi", "On" if enabled else "Off", b"1" if enabled else b"0", + ("Off", "On"), available=parked, reason=STARPORT_WIFI_DESCRIPTION if parked else held, default_value="On")) + rows.append(FeatureRow(STARPORT_HEVC_KEY, "StarPort: Allow H.265 video", "On" if hevc else "Off", b"1" if hevc else b"0", + ("Off", "On"), available=parked, reason=STARPORT_HEVC_DESCRIPTION if parked else held, default_value="Off")) + return tuple(rows) + def _driver_monitoring_reset_allowed(self): return self.authorized() and not self.params.get_bool("IsDriverViewEnabled") @@ -62,11 +96,7 @@ class DeveloperSettingsOwner: firmware_rows = (() if self.panda_wake is None else tuple(replace(item, vehicle_fingerprint=item.capability[0]) for item in self.panda_wake.rows(parked) if item.key == "IgnoreIgnitionLine")) - enabled = self._starport_enabled() - starport_rows = () if enabled is None else ( - FeatureRow(STARPORT_WIFI_KEY, "StarPort: Stay on Wi-Fi", "On" if enabled else "Off", b"1" if enabled else b"0", ("Off", "On"), - available=parked, reason=STARPORT_WIFI_DESCRIPTION if parked else "Park the vehicle to change this setting.", - default_value="On"),) + starport_rows = self._starport_rows(parked) summary = "Changes apply next drive. Panda firmware changes require a device restart." if firmware_rows else "Changes apply next drive." return FeatureSettingsState("developer", "Developer driving options", summary, (row, reset_row, calibration_row, *firmware_rows, *starport_rows), parked) @@ -96,6 +126,10 @@ class DeveloperSettingsOwner: return self.panda_wake is not None and self.panda_wake.apply(request) if request.key == STARPORT_WIFI_KEY: return self._apply_starport(request) + if request.key == STARPORT_HEVC_KEY: + return self._apply_hevc(request) + if request.key == STARPORT_NEW_SETUP_KEY: + return self._apply_new_setup(request) if request.key != KEY or request.value not in ("Off", "On") or request.expected not in (None, b"0", b"1"): return False result = commit_exact(self.params, key=request.key, max_bytes=8, raw=b"1" if request.value == "On" else b"0", @@ -112,3 +146,20 @@ class DeveloperSettingsOwner: except (OSError, RuntimeError, ValueError): return False return self._starport_enabled() == (request.value == "On") + + def _apply_hevc(self, request): + enabled, client = self._starport_hevc(), self.starport + if (enabled is None or client is None or request.value not in ("Off", "On") or request.expected != (b"1" if enabled else b"0") or + not self.authorized()): + return False + try: + client().call("set_allow_hevc", enabled=request.value == "On") + except (OSError, RuntimeError, ValueError): + return False + return self._starport_hevc() == (request.value == "On") + + def _apply_new_setup(self, request): + current = new_setup.enabled() + if request.value not in ("Off", "On") or request.expected != (b"1" if current else b"0") or not self.authorized(): + return False + return new_setup.set_enabled(request.value == "On") diff --git a/openpilot/starpilot/galaxy/setting_descriptions.py b/openpilot/starpilot/galaxy/setting_descriptions.py index 28715a2be0..6e3f72b586 100644 --- a/openpilot/starpilot/galaxy/setting_descriptions.py +++ b/openpilot/starpilot/galaxy/setting_descriptions.py @@ -90,6 +90,10 @@ DESCRIPTIONS = { 'RemoteStartBootsComma': 'Wakes comma during GM remote start.', 'HKGRemoteStartBootsComma': 'Wakes comma during Hyundai, Kia or Genesis remote climate operation.', 'IgnoreIgnitionLine': 'Uses GM CAN ignition instead of the harness ignition signal.', + 'heading:starport': 'Settings for connecting Android Auto to the car through StarPort.', + 'StarPortNewSetup': 'Shows the guided StarPort setup with a live checklist instead of the classic setup screens.', + 'StarPortAllowHevc': 'Lets a car that offers only H.265 video receive H.265; a car with an H.264 mode keeps H.264.', + 'StarPortDedicatedWifi': "Joins the car's Wi-Fi on a second interface so the comma stays on its home Wi-Fi or phone hotspot.", 'TeslaWakeOnCAN': 'Wakes comma when the Tesla wakes up, before you press the brake.', 'RadarRecoveryAlert': 'Play a chime and show a brief message when a radar fault clears. You must re-engage manually.', 'CameraView': 'Chooses the camera view shown on the driving screen.', @@ -158,6 +162,9 @@ DESCRIPTIONS = { # These are initialization explanations, not a second defaults table. INITIALIZATION = { + 'StarPortNewSetup': 'Applies the next time the setup screen opens.', + 'StarPortAllowHevc': 'Applies from the next StarPort connection.', + 'StarPortDedicatedWifi': 'Applies from the next StarPort connection.', 'LateralControllerSelection': ('Applies when driving processes next start: the controller class, vehicle policy and ' + 'PID/filter state are constructed together.'), 'UseStarPilotLongitudinalPlanner': 'Applies when the planner next starts: each choice runs a different planner process with its own subscriptions and state.', diff --git a/openpilot/starpilot/galaxy/settings.py b/openpilot/starpilot/galaxy/settings.py index 627e035796..f2b6ae3f0a 100644 --- a/openpilot/starpilot/galaxy/settings.py +++ b/openpilot/starpilot/galaxy/settings.py @@ -27,7 +27,7 @@ from openpilot.starpilot.audio.alert_volume import SPECS as SOUND_SPECS from openpilot.starpilot.ui.presentation import Profile from openpilot.common.hardware import HARDWARE from openpilot.starpilot.ui.feature_settings_state import ( - FeatureRow, FeatureSettingsRequest, FEATURE_CONFIRM_ACTIONS, is_long_confirm_action, row_change, row_default, + FeatureRow, FeatureSettingsRequest, FEATURE_CONFIRM_ACTIONS, is_heading, is_long_confirm_action, row_change, row_default, ) from openpilot.starpilot.saved_source import read_saved from openpilot.starpilot.galaxy.developer_settings import CALIBRATION_RESET, DRIVER_MONITORING_RESET, DeveloperSettingsOwner @@ -392,7 +392,8 @@ def _projection(row: FeatureRow) -> dict: "confirm": bool(row.key in CONFIRM_ACTIONS or is_long_confirm_action(row.key) or row.key in (PIP_RESET, PIP_EDITOR, VASM_RESET, VASM_ANNOTATION) or row.key.startswith(PIP_FORMAT_PREFIX)), "repairValue": row.repair_value, "defaultValue": row.default_value, "resetAvailable": row_default(row) is not None, - "displayScale": "lane-strength" if row.key == "LaneCenteringStrength" else None} + "displayScale": "lane-strength" if row.key == "LaneCenteringStrength" else None, + "heading": is_heading(row.key)} def _question(row: FeatureRow, request: FeatureSettingsRequest) -> str: diff --git a/openpilot/starpilot/galaxy/tests/description-key-inventory.json b/openpilot/starpilot/galaxy/tests/description-key-inventory.json index 4451cdbba3..26a99b379d 100644 --- a/openpilot/starpilot/galaxy/tests/description-key-inventory.json +++ b/openpilot/starpilot/galaxy/tests/description-key-inventory.json @@ -57,13 +57,6 @@ "LoudBlindspotAlert", "LoudBlindspotAlertWhenDisengaged", "NostalgiaMode", - "slc:offset:1", - "slc:offset:2", - "slc:offset:3", - "slc:offset:4", - "slc:offset:5", - "slc:offset:6", - "slc:offset:7", "PIPPreviewEnabled", "PIPPreviewInvert", "PIPPreviewShowOnBSM", @@ -106,6 +99,9 @@ "StandardJerkSpeed", "StandardJerkSpeedDecrease", "StarPilotDisplayPreferencesEnabled", + "StarPortAllowHevc", + "StarPortDedicatedWifi", + "StarPortNewSetup", "SteerFriction", "SteerLatAccel", "StockConfidenceBallWidget", @@ -166,6 +162,7 @@ "display:auto:ScreenBrightness", "display:auto:ScreenBrightnessOnroad", "enable_human_turn_detection_curv", + "heading:starport", "lane_change:auto", "lane_change:close_gap", "lane_change:close_gap_seconds", @@ -249,7 +246,6 @@ "profile:aggressive:following:8", "profile:aggressive:following:9", "profile:global_acceleration", - "profile:stopped_gap", "profile:global_braking", "profile:relaxed:acceleration", "profile:relaxed:acceleration:0", @@ -317,6 +313,7 @@ "profile:standard:following:7", "profile:standard:following:8", "profile:standard:following:9", + "profile:stopped_gap", "profile:traffic:acceleration", "profile:traffic:acceleration:0", "profile:traffic:acceleration:1", @@ -357,6 +354,13 @@ "sentry:reset", "sentry:sensitivity", "sentry:warning", + "slc:offset:1", + "slc:offset:2", + "slc:offset:3", + "slc:offset:4", + "slc:offset:5", + "slc:offset:6", + "slc:offset:7", "slc_reset", "sounds:auto:DisengageVolume", "sounds:auto:EngageVolume", diff --git a/openpilot/starpilot/galaxy/tests/test_developer_settings.py b/openpilot/starpilot/galaxy/tests/test_developer_settings.py index e8d283de14..89fddb28c8 100644 --- a/openpilot/starpilot/galaxy/tests/test_developer_settings.py +++ b/openpilot/starpilot/galaxy/tests/test_developer_settings.py @@ -3,14 +3,18 @@ from dataclasses import replace from pathlib import Path from types import SimpleNamespace +from unittest import mock from typing import cast import tempfile import unittest from unittest.mock import patch -from openpilot.starpilot.galaxy.developer_settings import CALIBRATION_RESET, DRIVER_MONITORING_RESET, STARPORT_WIFI_KEY, DeveloperSettingsOwner +from openpilot.starpilot.galaxy.developer_settings import (CALIBRATION_RESET, DRIVER_MONITORING_RESET, STARPORT_HEADING_KEY, + STARPORT_HEVC_KEY, STARPORT_NEW_SETUP_KEY, STARPORT_WIFI_KEY, + DeveloperSettingsOwner) from openpilot.starpilot.galaxy.settings import AuthorityContext, ContextSource, SettingsChanged, SettingsGateway from openpilot.starpilot.longitudinal.voacc import KEY, selected +from openpilot.starpilot.system.starport import new_setup from openpilot.starpilot.ui.feature_settings_state import FeatureSettingsRequest @@ -40,7 +44,7 @@ class FileParams: class FakeStarPort: def __init__(self, enabled=False, running=True): - self.enabled, self.running = enabled, running + self.enabled, self.running, self.hevc = enabled, running, False def call(self, command, **payload): if not self.running: @@ -48,7 +52,10 @@ class FakeStarPort: if command == "set_dedicated_wifi": self.enabled = payload["enabled"] return {"ok": True} - return {"ok": True, "status": {"dedicated_wifi_interface": self.enabled}} + if command == "set_allow_hevc": + self.hevc = payload["enabled"] + return {"ok": True} + return {"ok": True, "status": {"dedicated_wifi_interface": self.enabled, "allow_hevc": self.hevc}} class DeveloperSettingsTest(unittest.TestCase): @@ -60,6 +67,11 @@ class DeveloperSettingsTest(unittest.TestCase): self.cp = SimpleNamespace(openpilotLongitudinalControl=True, notCar=False, dashcamOnly=False, passive=False) self.parked = True self.owner = DeveloperSettingsOwner(self.params, lambda: self.parked) + flag = Path(directory.name) / "new_setup" + patch = mock.patch.object(new_setup, "FLAG_PATH", flag) + patch.start() + self.addCleanup(patch.stop) + self.flag = flag def test_default_off_strict_selection_and_vehicle_admission(self): self.assertEqual(self.owner.snapshot().rows[0].value, "Off") @@ -172,6 +184,52 @@ class DeveloperSettingsTest(unittest.TestCase): self.assertFalse(self.owner.apply(FeatureSettingsRequest(DRIVER_MONITORING_RESET, b"1", "confirm", confirmation=True))) self.assertTrue(Path(self.params.get_param_path("IsRhdDetected")).exists()) + def test_starport_section_has_a_heading_and_the_escape_hatch_even_with_the_service_off(self): + rows = self.owner.snapshot().rows + keys = [row.key for row in rows] + heading = rows[keys.index(STARPORT_HEADING_KEY)] + self.assertEqual((heading.label, heading.value, heading.available, heading.choices), ("StarPort", "", False, ())) + self.assertEqual(keys[keys.index(STARPORT_HEADING_KEY) + 1], STARPORT_NEW_SETUP_KEY) # the heading groups what follows + self.assertNotIn(STARPORT_WIFI_KEY, keys) + self.assertNotIn(STARPORT_HEVC_KEY, keys) + self.assertFalse(self.owner.apply(FeatureSettingsRequest(STARPORT_HEADING_KEY, b"", "On"))) # a heading is never a control + + def test_new_pairing_experience_defaults_on_and_the_classic_screens_are_one_switch_away(self): + row = next(row for row in self.owner.snapshot().rows if row.key == STARPORT_NEW_SETUP_KEY) + self.assertEqual((row.value, row.source, row.choices, row.default_value, row.available), ("On", b"1", ("Off", "On"), "On", True)) + self.assertFalse(self.owner.apply(FeatureSettingsRequest(STARPORT_NEW_SETUP_KEY, b"0", "On"))) # stale view + self.assertFalse(self.owner.apply(FeatureSettingsRequest(STARPORT_NEW_SETUP_KEY, b"1", "maybe"))) + self.assertTrue(self.owner.apply(FeatureSettingsRequest(STARPORT_NEW_SETUP_KEY, b"1", "Off"))) + self.assertFalse(new_setup.enabled()) + self.assertEqual(next(r for r in self.owner.snapshot().rows if r.key == STARPORT_NEW_SETUP_KEY).value, "Off") + self.parked = False + self.assertFalse(self.owner.apply(FeatureSettingsRequest(STARPORT_NEW_SETUP_KEY, b"0", "On"))) + self.parked = True + self.assertTrue(self.owner.apply(FeatureSettingsRequest(STARPORT_NEW_SETUP_KEY, b"0", "On"))) + self.assertTrue(new_setup.enabled()) + + def test_unreadable_or_odd_flag_means_on(self): + self.flag.write_text("garbage") + self.assertTrue(new_setup.enabled()) + self.flag.unlink() + self.assertTrue(new_setup.enabled()) + + def test_allow_h265_toggle_defaults_off_and_follows_the_daemon(self): + service = FakeStarPort() + owner = DeveloperSettingsOwner(self.params, lambda: self.parked, starport=lambda: service) + rows = owner.snapshot().rows + keys = [row.key for row in rows] + self.assertEqual(keys[keys.index(STARPORT_HEADING_KEY):], [STARPORT_HEADING_KEY, STARPORT_NEW_SETUP_KEY, STARPORT_WIFI_KEY, STARPORT_HEVC_KEY]) + row = rows[keys.index(STARPORT_HEVC_KEY)] + self.assertEqual((row.value, row.default_value, row.available), ("Off", "Off", True)) + self.assertTrue(owner.apply(FeatureSettingsRequest(STARPORT_HEVC_KEY, row.source, "On"))) + self.assertTrue(service.hevc) + self.assertFalse(owner.apply(FeatureSettingsRequest(STARPORT_HEVC_KEY, row.source, "Off"))) # stale view + self.parked = False + self.assertFalse(owner.apply(FeatureSettingsRequest(STARPORT_HEVC_KEY, b"1", "Off"))) + self.parked = True + service.running = False + self.assertFalse(owner.apply(FeatureSettingsRequest(STARPORT_HEVC_KEY, b"1", "Off"))) class CalibrationResetSettingsTest(unittest.TestCase): setUp = DeveloperSettingsTest.setUp diff --git a/openpilot/starpilot/galaxy/tests/test_settings_controls.mjs b/openpilot/starpilot/galaxy/tests/test_settings_controls.mjs index 93da10fb80..f05c83dee6 100644 --- a/openpilot/starpilot/galaxy/tests/test_settings_controls.mjs +++ b/openpilot/starpilot/galaxy/tests/test_settings_controls.mjs @@ -227,3 +227,15 @@ for (const row of [{ value: "30", minimum: 1, maximum: 30, step: 1, unit: "h" }, { value: "11.8", minimum: 11.8, maximum: 12.5, step: .1, unit: "V" }]) { assert.equal(settingControl(row), "slider") } + +// A section heading (for example "StarPort" on the Developer page) is a label only: no control, no readout, no default button. +{ + const heading = { label: "StarPort", value: "", heading: true, available: false, choices: [], key: "heading:starport" } + assert.equal(settingControl(heading), "readout") + const template = GalaxySettingRow.template + assert.match(template, /'gx-row--heading': row\.heading/) + assert.match(template, /v-else-if="!row\.heading" class="gx-row__value gx-row__readout"/) + assert.match(template, /showDefault && !row\.heading/) + assert.match(template, /presentedRow\.reason && !row\.heading/) +} +console.log("Settings controls: section headings render as labels only") diff --git a/openpilot/starpilot/galaxy/web/css/galaxy.css b/openpilot/starpilot/galaxy/web/css/galaxy.css index b6347847cf..42561ebe3c 100644 --- a/openpilot/starpilot/galaxy/web/css/galaxy.css +++ b/openpilot/starpilot/galaxy/web/css/galaxy.css @@ -1711,6 +1711,9 @@ input[type="checkbox"]:disabled { opacity: .4; cursor: not-allowed; } .gx-inset { margin: var(--sp-4); max-width: calc(100% - 2 * var(--sp-4)); } .gx-text-danger { color: var(--error); } .gx-row.gx-row--borderless { border-top: 0; } +/* A section title inside a settings page: a label only, never dimmed like an unavailable control. */ +.gx-row.gx-row--heading, .gx-row.gx-row--heading.disabled { opacity: 1; padding-top: 1.25rem; } +.gx-row--heading .gx-row__label { font-weight: 700; letter-spacing: .04em; text-transform: uppercase; opacity: .75; } .gx-row--wrap { flex-wrap: wrap; } .gx-row__info.gx-row__info--wide { flex: 1 1 180px; } .gx-filter-field { flex: 1 1 14rem; min-width: 0; } diff --git a/openpilot/starpilot/galaxy/web/js/galaxy-setting-row.js b/openpilot/starpilot/galaxy/web/js/galaxy-setting-row.js index de6ee7539e..69c1b1b894 100644 --- a/openpilot/starpilot/galaxy/web/js/galaxy-setting-row.js +++ b/openpilot/starpilot/galaxy/web/js/galaxy-setting-row.js @@ -215,14 +215,14 @@ export const GalaxySettingRow = { }, beforeUnmount() { this.clearHoldTimer() }, template: ` -