From 3d7509c4ca3ec33168bb41b29b2a87ed3f41e5aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Wed, 7 Oct 2026 07:59:43 -0700 Subject: [PATCH] Enable Experimental Mode by default without confirmation (#39043) * Enable Experimental Mode by default without confirmation * Update toggles.py * Fix formatting in End-to-End Longitudinal Control description Original-upstream-commit: f21bfc36be5f423f31b2c2a948d1e2eff91b02e8 Upstream: https://github.com/commaai/openpilot/commit/f21bfc36be5f423f31b2c2a948d1e2eff91b02e8 --- openpilot/common/params_keys.h | 1 - .../selfdrive/ui/layouts/settings/toggles.py | 38 ++------------ openpilot/selfdrive/ui/mici/layouts/home.py | 2 +- .../ui/mici/layouts/settings/toggles.py | 52 +++---------------- openpilot/selfdrive/ui/onroad/exp_button.py | 9 +--- openpilot/selfdrive/ui/ui_state.py | 2 - .../conditional_mode/tests/test_ui_action.py | 9 ++-- .../starpilot/conditional_mode/ui_action.py | 3 +- openpilot/starpilot/ui/runtime_app.py | 5 +- .../ui/tests/test_native_settings_pages.py | 18 +++---- .../ui/tests/test_onroad_favorites.py | 8 +-- 11 files changed, 31 insertions(+), 116 deletions(-) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index cd00235c45..42a03f1f34 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -183,7 +183,6 @@ inline static std::unordered_map keys = { {"DriverLockoutCount", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, INT, "0"}}, {"AlphaLongitudinalEnabled", {PERSISTENT, BOOL}}, {"ExperimentalMode", {PERSISTENT, BOOL}}, - {"ExperimentalModeConfirmed", {PERSISTENT, BOOL}}, {"FirmwareQueryDone", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"ForcePowerDown", {PERSISTENT, BOOL}}, {"GitBranch", {PERSISTENT, STRING}}, diff --git a/openpilot/selfdrive/ui/layouts/settings/toggles.py b/openpilot/selfdrive/ui/layouts/settings/toggles.py index 7e9e078c61..9fe0fd7486 100644 --- a/openpilot/selfdrive/ui/layouts/settings/toggles.py +++ b/openpilot/selfdrive/ui/layouts/settings/toggles.py @@ -3,10 +3,7 @@ from openpilot.common.params import Params, UnknownKeyName from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item from openpilot.system.ui.widgets.scroller_tici import Scroller -from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog -from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr, tr_noop -from openpilot.system.ui.widgets import DialogResult from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.starpilot.ui.slc_offset_feature import SlcOffsetOwner, native_parked @@ -159,15 +156,9 @@ class TogglesLayout(Widget): ui_state.update_params() e2e_description = tr( - "openpilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " + - "Experimental features are listed below:
" + "

End-to-End Longitudinal Control


" + "Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. " + - "Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; " + - "mistakes should be expected.
" + - "

New Driving Visualization


" + - "The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. " + - "The Experimental mode logo will also be shown in the top right corner." + "Since the driving model decides the speed to drive, the set speed will only act as an upper bound. Mistakes should be expected.
" ) if ui_state.CP is not None: @@ -216,27 +207,6 @@ class TogglesLayout(Widget): icon = "experimental.png" if self._toggles["ExperimentalMode"].action_item.get_state() else "experimental_white.png" self._toggles["ExperimentalMode"].set_icon(icon) - def _handle_experimental_mode_toggle(self, state: bool): - confirmed = self._params.get_bool("ExperimentalModeConfirmed") - if state and not confirmed: - def confirm_callback(result: DialogResult): - if result == DialogResult.CONFIRM: - self._params.put_bool("ExperimentalMode", True, block=True) - self._params.put_bool("ExperimentalModeConfirmed", True, block=True) - self._toggles["ExperimentalMode"].action_item.set_state(True) - else: - self._toggles["ExperimentalMode"].action_item.set_state(False) - self._update_experimental_mode_icon() - - # show confirmation dialog - content = (f"

{self._toggles['ExperimentalMode'].title}


" + - f"

{self._toggles['ExperimentalMode'].description}

") - dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback) - gui_app.push_widget(dlg) - else: - self._params.put_bool("ExperimentalMode", state, block=True) - self._toggles["ExperimentalMode"].action_item.set_state(state) - self._update_experimental_mode_icon() def _toggle_callback(self, state: bool, param: str): if param == "IsMetric": @@ -244,12 +214,10 @@ class TogglesLayout(Widget): self._toggles[param].action_item.set_state(self._params.get_bool(param)) self._metric_source = self._slc_offsets._raw("IsMetric") return ok - if param == "ExperimentalMode": - self._handle_experimental_mode_toggle(state) - return True - self._params.put_bool(param, state, block=True) self._toggles[param].action_item.set_state(state) + if param == "ExperimentalMode": + self._update_experimental_mode_icon() if self._toggle_defs[param][3]: self._params.put_bool("OnroadCycleRequested", True, block=True) return True diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index b852bf9ea6..14370d67c4 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -173,7 +173,7 @@ class MiciHomeLayout(Widget): if self._mouse_down_t is not None: if time.monotonic() - self._mouse_down_t > 0.5: # long gating for experimental mode - only allow toggle if longitudinal control is available - if ui_state.has_longitudinal_control and ui_state.experimental_mode_confirmed: + if ui_state.has_longitudinal_control: ui_state.experimental_mode = not ui_state.experimental_mode ui_state.params.put("ExperimentalMode", ui_state.experimental_mode, block=True) self._mouse_down_t = None diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py b/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py index 9ecaec7823..8b42742802 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py @@ -1,10 +1,7 @@ -from collections.abc import Callable - from openpilot.cereal import log from openpilot.system.ui.widgets.scroller import NavScroller -from openpilot.selfdrive.ui.mici.widgets.button import BigParamControl, BigMultiParamToggle, BigToggle, GreyBigButton -from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationCircleButton +from openpilot.selfdrive.ui.mici.widgets.button import BigParamControl, BigMultiParamToggle, BigToggle from openpilot.system.ui.lib.application import gui_app from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback from openpilot.selfdrive.ui.ui_state import ui_state @@ -13,29 +10,6 @@ from openpilot.starpilot.ui.slc_offset_feature import SlcOffsetOwner, native_par PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants -class ExperimentalModeConfirmPage(NavScroller): - def __init__(self, on_confirm: Callable[[], None]): - super().__init__() - - accept = BigConfirmationCircleButton("enable\nexperimental mode", - gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64), - lambda: self.dismiss(on_confirm)) - - self._scroller.add_widgets([ - GreyBigButton("enabling\nexperimental mode", "scroll to continue", - gui_app.texture("icons_mici/setup/warning.png", 64, 64)), - GreyBigButton("", "openpilot defaults to driving in chill mode."), - GreyBigButton("", "Experimental mode enables alpha-level features that aren't ready for chill mode."), - GreyBigButton("End-to-End Longitudinal Control"), - GreyBigButton("", "Let the driving model control the gas and brakes."), - GreyBigButton("", "openpilot will drive as it thinks a human would, including stopping for red lights and stop signs."), - GreyBigButton("", "The set speed will only act as an upper bound."), - GreyBigButton("", "This is an alpha quality feature; mistakes should be expected."), - GreyBigButton("New Driving Visualization"), - GreyBigButton("", "The path will change colors to communicate acceleration intent."), - GreyBigButton("", "Red for braking, green for acceleration, and gray for coasting."), - accept, - ]) class TogglesLayoutMici(NavScroller): @@ -50,8 +24,8 @@ class TogglesLayoutMici(NavScroller): "Relaxed leaves more space.\n" + "Use the steering wheel distance button on supported cars.") self._safe_mode_btn = BigParamControl("safe mode", "SafeMode", toggle_callback=restart_needed_callback) - self._experimental_btn = BigToggle("experimental mode", description_icon=gui_app.texture("icons_mici/experimental_mode.png", 64, 64), - initial_state=ui_state.params.get_bool("ExperimentalMode"), toggle_callback=self._on_experimental_mode, + self._experimental_btn = BigParamControl("experimental mode", "ExperimentalMode", + description_icon=gui_app.texture("icons_mici/experimental_mode.png", 64, 64), description="Let the driving model control gas and brakes.\n" + "Includes stopping for red lights and stop signs.\n" + "Set speed is a maximum, not a target.\n" + @@ -164,19 +138,6 @@ class TogglesLayoutMici(NavScroller): ui_state.params.put_bool("RecordAudio", desired) ui_state.params.put_bool("OnroadCycleRequested", True) - def _on_experimental_mode(self, state: bool): - if state and not ui_state.params.get_bool("ExperimentalModeConfirmed"): - # Don't show enabled state until confirm - self._experimental_btn.set_checked(False) - - def on_confirm(): - ui_state.params.put_bool("ExperimentalModeConfirmed", True) - ui_state.params.put_bool("ExperimentalMode", True) - self._experimental_btn.set_checked(True) - - gui_app.push_widget(ExperimentalModeConfirmPage(on_confirm)) - else: - ui_state.params.put_bool("ExperimentalMode", state) def request_personality(self, index: int) -> bool: self._update_toggles() @@ -186,8 +147,9 @@ class TogglesLayoutMici(NavScroller): def request_experimental(self) -> bool: self._update_toggles() - if (ui_state.CP is None or not ui_state.has_longitudinal_control or not self._experimental_btn.enabled or - not ui_state.params.get_bool("ExperimentalModeConfirmed")): + if ui_state.CP is None or not ui_state.has_longitudinal_control or not self._experimental_btn.enabled: return False - self._on_experimental_mode(not ui_state.sm["selfdriveState"].experimentalMode) + desired = not ui_state.sm["selfdriveState"].experimentalMode + ui_state.params.put_bool("ExperimentalMode", desired) + self._experimental_btn.set_checked(desired) return True diff --git a/openpilot/selfdrive/ui/onroad/exp_button.py b/openpilot/selfdrive/ui/onroad/exp_button.py index 7d9eacce6e..328116e976 100644 --- a/openpilot/selfdrive/ui/onroad/exp_button.py +++ b/openpilot/selfdrive/ui/onroad/exp_button.py @@ -38,7 +38,7 @@ class ExpButton(Widget): def request_toggle(self) -> bool: self._update_state() - if self._is_toggle_allowed(): + if ui_state.has_longitudinal_control: new_mode = not self._experimental_mode self._params.put_bool("ExperimentalMode", new_mode) @@ -67,10 +67,3 @@ class ExpButton(Widget): self._hold_end_time = self._held_mode = None return self._experimental_mode - - def _is_toggle_allowed(self): - if not self._params.get_bool("ExperimentalModeConfirmed"): - return False - - # Mirror exp mode toggle using persistent car params - return ui_state.has_longitudinal_control diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index 60a154bef9..98b5f514e0 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -138,7 +138,6 @@ class UIState: self.is_release = self.params.get_bool("IsReleaseBranch") self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM") self.experimental_mode: bool = self.params.get_bool("ExperimentalMode") - self.experimental_mode_confirmed: bool = self.params.get_bool("ExperimentalModeConfirmed") self.chestnut_present: bool = False self._gpu_artifacts = ModelManager() self._gpu_artifacts_at: float | None = None @@ -322,7 +321,6 @@ class UIState: self.is_metric = self.params.get_bool("IsMetric") self.always_on_dm = self.params.get_bool("AlwaysOnDM") self.experimental_mode = self.params.get_bool("ExperimentalMode") - self.experimental_mode_confirmed = self.params.get_bool("ExperimentalModeConfirmed") raw_chestnut = self.params.get("ChestnutActive") self.chestnut_active = raw_chestnut if type(raw_chestnut) is bool else ( True if raw_chestnut in (b"1", "1") else False if raw_chestnut in (b"0", "0") else None) diff --git a/openpilot/starpilot/conditional_mode/tests/test_ui_action.py b/openpilot/starpilot/conditional_mode/tests/test_ui_action.py index 4e8ce88a82..69c7c0e7c6 100644 --- a/openpilot/starpilot/conditional_mode/tests/test_ui_action.py +++ b/openpilot/starpilot/conditional_mode/tests/test_ui_action.py @@ -48,7 +48,6 @@ class TestUiAction(unittest.TestCase): directory = tempfile.TemporaryDirectory() self.addCleanup(directory.cleanup) self.params = Params(directory.name) - self.params.put_bool('ExperimentalModeConfirmed', True, block=True) self.params.put('LKASButtonControl', 5, block=True) self.cp = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC) self.planner = LongitudinalPlanner(self.cp, init_v=15.0) @@ -226,7 +225,7 @@ class TestUiAction(unittest.TestCase): setattr(changed.slcAction, field, value) self.assertIsNone(observation(changed, observed)) - def test_release_checks_exact_settings_confirmation_and_changed_context(self): + def test_release_checks_exact_settings_and_changed_context(self): stamp = MONO + 50_000_000 self.ack(stamp) context = self.ui.context(self.sm, self.cp, now_ns=stamp + 1000) @@ -237,9 +236,11 @@ class TestUiAction(unittest.TestCase): self.params.put_bool('SafeMode', True, block=True) self.assertFalse(self.ui.dispatch(context, self.sm, self.cp, self.publisher, now_ns=stamp + 10_002_000)) self.params.remove('SafeMode') - self.params.put_bool('ExperimentalModeConfirmed', False, block=True) + original = self.params.get('ConditionalModeConfig') + self.configure(choice=ModeChoice.STOCK) self.assertFalse(self.ui.dispatch(context, self.sm, self.cp, self.publisher, now_ns=stamp + 10_003_000)) - self.params.put_bool('ExperimentalModeConfirmed', True, block=True) + self.params.put('ConditionalModeConfig', original, block=True) + self.choice = ModeChoice.CEM self.ack(stamp + 20_000_000, effective=True) self.assertFalse(self.ui.dispatch(context, self.sm, self.cp, self.publisher, now_ns=stamp + 20_001_000)) self.assertEqual(self.publisher.events, []) diff --git a/openpilot/starpilot/conditional_mode/ui_action.py b/openpilot/starpilot/conditional_mode/ui_action.py index 890976e931..6d7ecf976c 100644 --- a/openpilot/starpilot/conditional_mode/ui_action.py +++ b/openpilot/starpilot/conditional_mode/ui_action.py @@ -51,8 +51,7 @@ def current_authority(sm, cp, drive_id: int, now_ns: int) -> bool: def exact_settings(params, snapshot: SettingsSnapshot) -> bool: try: return (read_saved(params, 'ConditionalModeConfig', MAX_DOCUMENT_BYTES) == (snapshot.document_raw, True) and - read_saved(params, 'SafeMode', 8) == (snapshot.safe_mode_raw, True) and - read_saved(params, 'ExperimentalModeConfirmed', 8) == (b'1', True)) + read_saved(params, 'SafeMode', 8) == (snapshot.safe_mode_raw, True)) except (AttributeError, OSError, TypeError, ValueError): return False diff --git a/openpilot/starpilot/ui/runtime_app.py b/openpilot/starpilot/ui/runtime_app.py index 057cc5faf4..ba771ac0f5 100644 --- a/openpilot/starpilot/ui/runtime_app.py +++ b/openpilot/starpilot/ui/runtime_app.py @@ -315,10 +315,9 @@ class StarShellSession: return bool(self._favorite_authority() and ui_state.CP is not None and ui_state.has_longitudinal_control and not ui_state.params.get_bool("SafeMode") and callback()) - confirmed = ui_state.params.get_bool("ExperimentalModeConfirmed") context = (self._conditional_action_owner().context(ui_state.sm, ui_state.CP, now_ns=time.monotonic_ns()) - if long_available and confirmed and conditional else None) - exp_available = bool(long_available and confirmed and (not conditional or context is not None)) + if long_available and conditional else None) + exp_available = bool(long_available and (not conditional or context is not None)) def toggle_experimental(): if self._conditional_favorite_active() != conditional: return False diff --git a/openpilot/starpilot/ui/tests/test_native_settings_pages.py b/openpilot/starpilot/ui/tests/test_native_settings_pages.py index 202dc41ddc..d4db63db8d 100644 --- a/openpilot/starpilot/ui/tests/test_native_settings_pages.py +++ b/openpilot/starpilot/ui/tests/test_native_settings_pages.py @@ -310,19 +310,18 @@ class NativeSettingsPageTests(unittest.TestCase): personality.callback.assert_not_called() self.assertEqual(personality.selected_button, 1) - def test_toggles_saved_state_matches_display_after_changes_and_confirmations(self): + def test_toggles_saved_state_matches_display_after_changes(self): # Run the real controller methods with only device services isolated. source = Path('openpilot/selfdrive/ui/layouts/settings/toggles.py') layout = next(node for node in ast.parse(source.read_text()).body if isinstance(node, ast.ClassDef) and node.name == 'TogglesLayout') - methods = {'_update_toggles', '_update_state', '_toggle_callback', '_handle_experimental_mode_toggle', + methods = {'_update_toggles', '_update_state', '_toggle_callback', 'request_toggle', 'request_personality', '_set_longitudinal_personality'} layout.bases = [] layout.body = [node for node in layout.body if isinstance(node, ast.FunctionDef) and node.name in methods] ui = NS(CP=None, engaged=False, update_params=Mock(), sm=NS(updated={'selfdriveState': False})) app = NS(push_widget=Mock()) namespace = ModuleType('toggles_controller_test') - namespace.__dict__.update(tr=lambda text: text, ui_state=ui, gui_app=app, DialogResult=DialogResult, - ConfirmDialog=lambda *args, **kwargs: NS(callback=kwargs['callback'])) + namespace.__dict__.update(tr=lambda text: text, ui_state=ui) exec(compile(ast.Module(body=[layout], type_ignores=[]), str(source), 'exec'), namespace.__dict__) owner = namespace.TogglesLayout() saved = {'IsLdwEnabled': False, 'LongitudinalPersonality': 1} @@ -358,15 +357,12 @@ class NativeSettingsPageTests(unittest.TestCase): self.click(pane, 2000) self.assertFalse(saved['IsLdwEnabled']) self.assertEqual(pane.snapshot().rows[0].value, 'Off') - for result in (DialogResult.CANCEL, DialogResult.CONFIRM): + for desired in (True, False): self.click(pane, 2000, index=2) - self.assertFalse(saved.get('ExperimentalMode', False)) - app.push_widget.call_args.args[0].callback(result) pane._update_state() - self.assertEqual(pane.snapshot().rows[2].value, 'On' if result == DialogResult.CONFIRM else 'Off') - self.click(pane, 2000, index=2) - self.assertFalse(saved['ExperimentalMode']) - self.assertEqual(pane.snapshot().rows[2].value, 'Off') + self.assertEqual(saved['ExperimentalMode'], desired) + self.assertEqual(pane.snapshot().rows[2].value, 'On' if desired else 'Off') + app.push_widget.assert_not_called() def network_panel(self): manager = Mock(spec=WifiManager, is_connection_saved=lambda ssid: ssid == "Saved", connected_ssid="Saved", diff --git a/openpilot/starpilot/ui/tests/test_onroad_favorites.py b/openpilot/starpilot/ui/tests/test_onroad_favorites.py index 937e2c3dec..f538362a61 100644 --- a/openpilot/starpilot/ui/tests/test_onroad_favorites.py +++ b/openpilot/starpilot/ui/tests/test_onroad_favorites.py @@ -307,7 +307,7 @@ class TestOnroadFavorites(unittest.TestCase): bookmark, personality, experimental = Mock(return_value=True), Mock(return_value=True), Mock(return_value=True) native = NS(CP=NS(carFingerprint='car', flags=0, pcmCruise=True), has_longitudinal_control=True, started_frame=42, personality=1, sm={"deviceState": NS(startedMonoTime=42)}, - params=NS(get_bool=lambda key: key == 'ExperimentalModeConfirmed')) + params=NS(get_bool=lambda key: False)) with patch.object(runtime_app, 'ui_state', native): actions = session.native_favorite_actions(bookmark, personality, experimental) assert actions[BOOKMARK].invoke() @@ -444,7 +444,7 @@ class TestOnroadFavorites(unittest.TestCase): self.enterContext(patch.object(session, 'conditional_actions', NS(context=Mock(return_value=context), dispatch=Mock(return_value=True)), create=True)) experimental = Mock(return_value=True) native = NS(CP=NS(carFingerprint='car', flags=0, pcmCruise=True), has_longitudinal_control=True, started_frame=42, - personality=1, sm={"deviceState": NS(startedMonoTime=42)}, params=NS(get_bool=lambda key: key == 'ExperimentalModeConfirmed')) + personality=1, sm={"deviceState": NS(startedMonoTime=42)}, params=NS(get_bool=lambda key: False)) with patch.object(runtime_app, 'ui_state', native), patch.object(runtime_app, '_slc_action_publisher') as publisher: action = session.native_favorite_actions(Mock(), Mock(), experimental)[EXPERIMENTAL] assert action.available and action.invoke() @@ -457,9 +457,9 @@ class TestOnroadFavorites(unittest.TestCase): session._favorite_authority.return_value = False assert not action.invoke() session._favorite_authority.return_value = True - native.params.get_bool = lambda key: key in ('SafeMode', 'ExperimentalModeConfirmed') + native.params.get_bool = lambda key: key == 'SafeMode' assert not action.invoke() - native.params.get_bool = lambda key: key == 'ExperimentalModeConfirmed' + native.params.get_bool = lambda key: False session._conditional_favorite_active.return_value = False assert not action.invoke() assert session.conditional_actions.dispatch.call_count == 1