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
This commit is contained in:
Harald Schäfer
2026-10-07 07:59:43 -07:00
committed by GitHub
parent 5d16c4c9af
commit 3d7509c4ca
11 changed files with 31 additions and 116 deletions
-1
View File
@@ -183,7 +183,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> 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}},
@@ -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:<br>" +
"<h4>End-to-End Longitudinal Control</h4><br>" +
"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.<br>" +
"<h4>New Driving Visualization</h4><br>" +
"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.<br>"
)
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"<h1>{self._toggles['ExperimentalMode'].title}</h1><br>" +
f"<p>{self._toggles['ExperimentalMode'].description}</p>")
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
+1 -1
View File
@@ -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
@@ -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
+1 -8
View File
@@ -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
-2
View File
@@ -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)
@@ -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, [])
@@ -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
+2 -3
View File
@@ -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
@@ -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",
@@ -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