From 16f31abd759a0f80a97d70b216ee88ddcae33dbb Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:13:23 -0500 Subject: [PATCH] RHD Fix --- .../selfdrive/ui/layouts/settings/toggles.py | 16 +++++++++++++++ .../ui/mici/layouts/settings/toggles.py | 16 +++++++++++++++ openpilot/starpilot/ui/runtime_app.py | 3 +-- openpilot/starpilot/ui/runtime_snapshot.py | 7 +++++-- .../starpilot/ui/tests/test_c4_toggles.py | 15 ++++++++++++++ .../ui/tests/test_native_settings.py | 3 ++- .../ui/tests/test_runtime_navigation.py | 20 +++++++++++++++++++ 7 files changed, 75 insertions(+), 5 deletions(-) diff --git a/openpilot/selfdrive/ui/layouts/settings/toggles.py b/openpilot/selfdrive/ui/layouts/settings/toggles.py index 8c54cc6f82..1d294b08c1 100644 --- a/openpilot/selfdrive/ui/layouts/settings/toggles.py +++ b/openpilot/selfdrive/ui/layouts/settings/toggles.py @@ -8,6 +8,8 @@ from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.layouts.settings.initial_cruise_speed import InitialCruiseSpeedControl from openpilot.starpilot.ui.slc_offset_feature import SlcOffsetOwner, native_parked +from openpilot.starpilot.galaxy.localization_settings import LocalizationSettingsOwner +from openpilot.starpilot.ui.feature_settings_state import FeatureSettingsRequest PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants @@ -38,6 +40,7 @@ class TogglesLayout(Widget): def __init__(self): super().__init__() self._params = Params() + self._localization = LocalizationSettingsOwner(self._params, lambda: True, lambda: native_parked(ui_state)) self._slc_offsets = SlcOffsetOwner(self._params, lambda: native_parked(ui_state), lambda: ui_state.CP) self._metric_source = self._slc_offsets._raw("IsMetric") self._is_release = self._params.get_bool("IsReleaseBranch") @@ -74,6 +77,12 @@ class TogglesLayout(Widget): "monitoring.png", False, ), + "IsRhdDetected": ( + lambda: tr("Right Hand Driving"), + "Select the right-hand driver-monitoring side for the next drive. Off selects left-hand. Automatic side detection continues. Park and close driver camera preview before changing.", + "monitoring.png", + False, + ), "RecordFront": ( lambda: tr("Record and Upload Cabin Camera"), DESCRIPTIONS["RecordFront"], @@ -211,6 +220,9 @@ class TogglesLayout(Widget): for toggle_def in self._toggle_defs: if self._toggle_defs[toggle_def][3] and toggle_def not in self._locked_toggles: self._toggles[toggle_def].action_item.set_enabled(not ui_state.engaged) + row = self._localization.snapshot().rows[1] + self._rhd_source = row.source + self._toggles["IsRhdDetected"].action_item.set_enabled(row.available and "IsRhdDetected" not in self._locked_toggles) def _render(self, rect): self._scroller.render(rect) @@ -221,6 +233,10 @@ class TogglesLayout(Widget): def _toggle_callback(self, state: bool, param: str): + if param == "IsRhdDetected": + ok = self._localization.apply(FeatureSettingsRequest(param, self._rhd_source, "On" if state else "Off")) + self._update_toggles() + return ok if param == "IsMetric": ok = self._slc_offsets.change_units(state, self._metric_source) self._toggles[param].action_item.set_state(self._params.get_bool(param)) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py b/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py index 6fb13aae8d..5ad548b163 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py @@ -7,6 +7,8 @@ from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callba from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.mici.layouts.settings.initial_cruise_speed import InitialCruiseSpeedButton from openpilot.starpilot.ui.slc_offset_feature import SlcOffsetOwner, native_parked +from openpilot.starpilot.galaxy.localization_settings import LocalizationSettingsOwner +from openpilot.starpilot.ui.feature_settings_state import FeatureSettingsRequest PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants @@ -18,6 +20,12 @@ class TogglesLayoutMici(NavScroller): super().__init__() self._slc_offsets = SlcOffsetOwner(ui_state.params, lambda: native_parked(ui_state), lambda: ui_state.CP) self._metric_source = self._slc_offsets._raw("IsMetric") + self._localization = LocalizationSettingsOwner(ui_state.params, lambda: True, lambda: native_parked(ui_state)) + self._rhd_source = self._slc_offsets._raw("IsRhdDetected") + self._rhd_toggle = BigToggle("right hand driving", initial_state=ui_state.params.get_bool("IsRhdDetected"), + toggle_callback=self._on_rhd, + description="On selects right-hand; Off selects left-hand for the next drive. Automatic side detection continues.") + self._rhd_toggle.set_enabled(lambda: self._localization.snapshot().rows[1].available) self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"], description="Standard is recommended.\n" + @@ -60,6 +68,7 @@ class TogglesLayoutMici(NavScroller): is_metric_toggle, ldw_toggle, always_on_dm_toggle, + self._rhd_toggle, record_front, record_mic, enable_openpilot, @@ -72,6 +81,7 @@ class TogglesLayoutMici(NavScroller): ("IsMetric", is_metric_toggle), ("IsLdwEnabled", ldw_toggle), ("AlwaysOnDM", always_on_dm_toggle), + ("IsRhdDetected", self._rhd_toggle), ("RecordFront", record_front), ("RecordAudio", record_mic), ("OpenpilotEnabledToggle", enable_openpilot), @@ -131,6 +141,7 @@ class TogglesLayoutMici(NavScroller): for key, item in self._refresh_toggles: item.set_checked(ui_state.params.get_bool(key)) self._metric_source = self._slc_offsets._raw("IsMetric") + self._rhd_source = self._slc_offsets._raw("IsRhdDetected") def _on_metric(self, desired: bool) -> None: self._slc_offsets.change_units(desired, self._metric_source) @@ -138,6 +149,11 @@ class TogglesLayoutMici(NavScroller): self._metric_toggle.set_checked(ui_state.params.get_bool("IsMetric")) self._update_toggles() + def _on_rhd(self, desired: bool) -> None: + self._localization.apply(FeatureSettingsRequest("IsRhdDetected", self._rhd_source, "On" if desired else "Off")) + self._rhd_source = self._localization.snapshot().rows[1].source + self._rhd_toggle.set_checked(ui_state.params.get_bool("IsRhdDetected")) + def _on_record_audio(self, desired: bool) -> None: # Keep filesystem waits off the UI thread, and persist the preference before # publishing the restart request through the same Params writer queue. diff --git a/openpilot/starpilot/ui/runtime_app.py b/openpilot/starpilot/ui/runtime_app.py index 95ee71f84d..9bb266c203 100644 --- a/openpilot/starpilot/ui/runtime_app.py +++ b/openpilot/starpilot/ui/runtime_app.py @@ -1829,7 +1829,6 @@ class StarMainLayout(MainLayout): device_scroller._items.insert(1, device_scroller._items.pop()) toggle_items = self._large_panels[Destination.TOGGLES]._scroller._items toggle_items.insert(2, text_item("Safe Mode", "N/A")) - toggle_items.insert(7, text_item("Right Hand Driving", "N/A")) from openpilot.common.hardware import HARDWARE from openpilot.starpilot.ui.starpilot_settings_adapter_large import StarPilotSettingsAdapterLarge for destination, owner in self._large_panels.items(): @@ -1941,7 +1940,7 @@ class StarMainLayout(MainLayout): return False param = {ToggleKey.ENABLED: "OpenpilotEnabledToggle", ToggleKey.EXPERIMENTAL: "ExperimentalMode", ToggleKey.DISENGAGE_ACCELERATOR: "DisengageOnAccelerator", ToggleKey.LANE_DEPARTURE: "IsLdwEnabled", - ToggleKey.ALWAYS_ON_DM: "AlwaysOnDM", ToggleKey.RECORD_FRONT: "RecordFront", + ToggleKey.ALWAYS_ON_DM: "AlwaysOnDM", ToggleKey.RIGHT_HAND_DRIVING: "IsRhdDetected", ToggleKey.RECORD_FRONT: "RecordFront", ToggleKey.RECORD_AUDIO: "RecordAudio", ToggleKey.METRIC: "IsMetric"}.get(request.key) return bool(param is not None and owner.request_toggle(param, request.value)) diff --git a/openpilot/starpilot/ui/runtime_snapshot.py b/openpilot/starpilot/ui/runtime_snapshot.py index 2aef8a2744..1cd82bc5cc 100644 --- a/openpilot/starpilot/ui/runtime_snapshot.py +++ b/openpilot/starpilot/ui/runtime_snapshot.py @@ -822,9 +822,12 @@ class RuntimeSnapshotAdapter: toggles = TogglesState(enabled=_flag(params, "OpenpilotEnabledToggle"), experimental=experimental, disengage_accelerator=_flag(params, "DisengageOnAccelerator"), lane_departure=_flag(params, "IsLdwEnabled"), - always_on_dm=_flag(params, "AlwaysOnDM"), record_front=_flag(params, "RecordFront"), + always_on_dm=_flag(params, "AlwaysOnDM"), right_hand_driving=_flag(params, "IsRhdDetected"), + record_front=_flag(params, "RecordFront"), record_audio=_flag(params, "RecordAudio"), metric=bool(ui.is_metric), personality=persona, - unavailable=frozenset((ToggleKey.SAFE_MODE, ToggleKey.RIGHT_HAND_DRIVING))) + unavailable=frozenset((ToggleKey.SAFE_MODE,)) | ( + frozenset() if confirmed_offroad and not _flag(params, "IsDriverViewEnabled") else + frozenset((ToggleKey.RIGHT_HAND_DRIVING,)))) snapshot = ShellSnapshot(mode, home, settings, onroad, device_state, software, toggles, selected) if mode == ShellMode.ONROAD: self._onroad_ancillary = (now_ns, ancillary_key, snapshot) diff --git a/openpilot/starpilot/ui/tests/test_c4_toggles.py b/openpilot/starpilot/ui/tests/test_c4_toggles.py index abacf11020..b95521d6ab 100644 --- a/openpilot/starpilot/ui/tests/test_c4_toggles.py +++ b/openpilot/starpilot/ui/tests/test_c4_toggles.py @@ -62,6 +62,21 @@ class TestC4AudioToggle(unittest.TestCase): self.panel = toggles.TogglesLayoutMici() self.audio = next(item for key, item in self.panel._refresh_toggles if key == "RecordAudio") + def test_rhd_control_saves_shared_param_and_rejects_open_preview(self): + from openpilot.starpilot.galaxy.localization_settings import LocalizationSettingsOwner + self.panel._localization = LocalizationSettingsOwner(self.params, lambda: True, lambda: True) + for desired in (True, False): + self.panel._rhd_source = self.panel._localization.snapshot().rows[1].source + self.panel._on_rhd(desired) + self.assertEqual(self.params.get_bool('IsRhdDetected'), desired) + self.assertTrue(self.params.get_bool('OnroadCycleRequested')) + self.assertEqual(self.panel._rhd_toggle._checked, desired) + self.params.put_bool('OnroadCycleRequested', False, block=True) + self.params.put_bool('IsDriverViewEnabled', True, block=True) + self.panel._on_rhd(True) + self.assertFalse(self.params.get_bool('IsRhdDetected')) + self.assertFalse(self.params.get_bool('OnroadCycleRequested')) + def test_release_queues_audio_before_restart_without_blocking_writes(self): with patch.object(self.params, "put_bool") as save: self.audio._handle_mouse_release(MousePos(0, 0)) diff --git a/openpilot/starpilot/ui/tests/test_native_settings.py b/openpilot/starpilot/ui/tests/test_native_settings.py index de860df35b..3c18ea875f 100644 --- a/openpilot/starpilot/ui/tests/test_native_settings.py +++ b/openpilot/starpilot/ui/tests/test_native_settings.py @@ -222,7 +222,8 @@ class NativeSettingsTests(unittest.TestCase): self.assertIs(layout._large_panels[Destination.SOFTWARE].panel, self.software) self.assertIs(layout._large_panels[Destination.TOGGLES].panel, toggles) self.assertEqual(toggles._scroller._items[2].title, 'Safe Mode') - self.assertEqual(toggles._scroller._items[7].title, 'Right Hand Driving') + self.assertFalse(any(item.title == 'Right Hand Driving' for item in toggles._scroller._items), + 'RHD belongs to the native toggle owner, not an inserted N/A placeholder') self.assertEqual(self.device._scroller._items[0].title, 'Galaxy') self.assertNotIn(self.device._power_off_btn, self.device._scroller._items) self.assertEqual(session.system_power_action, layout._system_power_action) diff --git a/openpilot/starpilot/ui/tests/test_runtime_navigation.py b/openpilot/starpilot/ui/tests/test_runtime_navigation.py index 0b96380afa..4fe7e2cda3 100644 --- a/openpilot/starpilot/ui/tests/test_runtime_navigation.py +++ b/openpilot/starpilot/ui/tests/test_runtime_navigation.py @@ -71,6 +71,26 @@ class TestCompactSettingsNavigation(unittest.TestCase): class TestExistingToggleOwner(unittest.TestCase): + def test_rhd_toggle_saves_through_shared_owner_and_requests_next_drive(self): + from openpilot.starpilot.galaxy.localization_settings import LocalizationSettingsOwner + with tempfile.TemporaryDirectory() as directory: + params = Params(directory) + owner = TogglesLayout.__new__(TogglesLayout) + owner._params = params + owner._localization = LocalizationSettingsOwner(params, lambda: True, lambda: True) + with patch.object(owner, '_update_toggles'): + for enabled in (True, False): + owner._rhd_source = owner._localization.snapshot().rows[1].source + self.assertTrue(owner._toggle_callback(enabled, 'IsRhdDetected')) + self.assertEqual(params.get_bool('IsRhdDetected'), enabled) + self.assertTrue(params.get_bool('OnroadCycleRequested')) + params.put_bool('OnroadCycleRequested', False, block=True) + params.put_bool('IsDriverViewEnabled', True, block=True) + owner._rhd_source = b'0' + self.assertFalse(owner._toggle_callback(True, 'IsRhdDetected')) + self.assertFalse(params.get_bool('IsRhdDetected')) + self.assertFalse(params.get_bool('OnroadCycleRequested')) + def test_personality_no_op_uses_typed_integer_value(self): owner = TogglesLayout.__new__(TogglesLayout) owner._long_personality_setting = NS(action_item=NS(enabled=True))