Make standby button wake optional and retain ignition wake

This commit is contained in:
AngusBell97
2026-09-15 17:28:32 +01:00
committed by firestar5683
parent 20f38f3d8e
commit 91052ea0e1
14 changed files with 75 additions and 34 deletions
+1
View File
@@ -700,6 +700,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"StandardJerkSpeed", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
{"StandardJerkSpeedDecrease", {PERSISTENT, FLOAT, "100.0", "100.0", 3}},
{"StandbyMode", {PERSISTENT, BOOL, "0", "0", 1, SETTINGS_SIMPLE}},
{"StandbyWakeButton", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"StandbyButtonPressTime", {CLEAR_ON_MANAGER_START | DONT_LOG, INT, "0", "0"}},
{"StandbyWakeEngage", {PERSISTENT, BOOL, "1", "1", 2, SETTINGS_SIMPLE}},
{"StandbyWakeDisengage", {PERSISTENT, BOOL, "1", "1", 2, SETTINGS_SIMPLE}},
@@ -257,7 +257,7 @@ class SystemSettingsManagerView(PanelManagerView):
self._toggle_defs = [
{
"title": tr("Standby Mode"),
"subtitle": tr("Touch and button presses always wake the screen. Choose additional wake events below."),
"subtitle": tr("Touch and ignition changes always wake the screen. Choose additional wake events below."),
"get_state": lambda: self._controller._params.get_bool("StandbyMode"),
"set_state": lambda v: self._controller._params.put_bool("StandbyMode", v),
},
+1 -1
View File
@@ -300,7 +300,7 @@ class ScreenSettingsLayoutMici(NavScroller):
self._onroad_timeout = self._timeout_button("ScreenTimeoutOnroad", "onroad timeout")
standby = ScreenToggleMici("standby mode", self._params, "StandbyMode")
self._wake_controls = [ScreenWakeToggleMici("wake: " + label.lower(), self._params, key, default) for key, label, default in SCREEN_WAKE_OPTIONS]
explanation = GreyBigButton("", "Standby sleeps the screen onroad. Touch and button presses always wake it.")
explanation = GreyBigButton("", "Standby sleeps the screen onroad. Touch and ignition changes always wake it.")
self._refresh_controls = [management, offroad_timeout, self._onroad_timeout, standby, *self._wake_controls]
for control in [offroad, onroad, offroad_timeout, self._onroad_timeout, standby, *self._wake_controls]:
control.set_enabled(lambda: self._params.get_bool("ScreenManagement", default=True))
+2 -1
View File
@@ -474,7 +474,8 @@ class Device:
input_events = self._wake_input_events()
selected_status_change = status_changed and status_key in self._wake_keys
selected_turn_signal = bool(input_events & self._wake_keys)
button_pressed = self._standby_mode and (ui_state.started or ui_state.ignition) and "button" in input_events
button_pressed = (self._standby_mode and (ui_state.started or ui_state.ignition) and
"StandbyWakeButton" in self._wake_keys and "button" in input_events)
wake_for_onroad_event = (ui_state.started and self._standby_mode and self._screen_brightness_onroad != 0 and
(selected_status_change or self._visible_onroad_alert() or selected_turn_signal))
@@ -4751,7 +4751,7 @@
{
"key": "StandbyMode",
"label": "Standby Mode",
"description": "Turn off the screen after the onroad timeout. Touch and recognised Bluetooth or steering wheel button presses always wake it. Choose additional wake events below.",
"description": "Turn off the screen after the onroad timeout. Touch and ignition changes always wake it. Choose additional wake events below.",
"picker_description": "Turn off the driving screen after inactivity and choose additional wake events.",
"data_type": "bool",
"ui_type": "toggle",
@@ -4842,6 +4842,20 @@
"StandbyMode"
],
"settings_tier": "simple"
},
{
"key": "StandbyWakeButton",
"label": "Bluetooth or steering wheel button",
"description": "Wake the screen from Standby when a recognised Bluetooth, controller or steering wheel button is pressed, including buttons without an assigned action.",
"data_type": "bool",
"ui_type": "toggle",
"default": false,
"parent_key": "StandbyMode",
"visible_when_all_true": [
"ScreenManagement",
"StandbyMode"
],
"settings_tier": "simple"
}
]
},
+3
View File
@@ -22,8 +22,11 @@ SCREEN_WAKE_OPTIONS = (
('StandbyWakeWarningAlert', 'Warning alerts', True),
('StandbyWakeCriticalAlert', 'Critical / takeover alerts', True),
('StandbyWakeTurnSignal', 'Turn signals', False),
('StandbyWakeButton', 'Bluetooth or steering wheel button', False),
)
SCREEN_WAKE_DESCRIPTIONS = {
'StandbyWakeButton':
'Wake the screen from Standby when a recognised Bluetooth, controller or steering wheel button is pressed, including buttons without an assigned action.',
'StandbyWakeEngage': 'Wake the screen from Standby when StarPilot engages.',
'StandbyWakeDisengage': 'Wake the screen from Standby when StarPilot disengages.',
'StandbyWakeInfoAlert': 'Wake the screen from Standby and keep it awake while an informational alert is displayed.',
@@ -148,6 +148,8 @@ def test_every_wake_choice_controls_its_own_event(key, selected):
alert = state.sm['selfdriveState']
alert.alertSize = 'small'
alert.alertStatus = {'StandbyWakeInfoAlert': 'normal', 'StandbyWakeWarningAlert': 'userPrompt', 'StandbyWakeCriticalAlert': 'critical'}[key]
elif key == 'StandbyWakeButton':
state.params_memory.values['StandbyButtonPressTime'] = 99_500_000_000
elif key == 'StandbyWakeTurnSignal':
state.sm['carState'].leftBlinker = True
else:
@@ -159,6 +161,7 @@ def test_every_wake_choice_controls_its_own_event(key, selected):
def test_external_button_press_is_fresh_and_consumed_once():
settings = dict.fromkeys(screen.SCREEN_WAKE_KEYS, False)
settings['StandbyWakeButton'] = True
device, state, _ = make_device(**settings)
device._update_wakefulness()
state.params_memory.values['StandbyButtonPressTime'] = 99_500_000_000
@@ -173,7 +176,7 @@ def test_external_button_press_is_fresh_and_consumed_once():
def test_consumed_button_press_is_not_replayed_between_ui_frames():
device, state, _ = make_device()
device, state, _ = make_device(StandbyWakeButton=True)
device._update_wakefulness()
state.params_memory.values['StandbyButtonPressTime'] = 99_500_000_000
device._update_wakefulness()
@@ -206,6 +209,7 @@ def test_dom_alert_predicate_does_not_depend_on_renderer_hide_setting(device_typ
def test_bluetooth_wake_during_ignition_only_standby():
settings = dict.fromkeys(screen.SCREEN_WAKE_KEYS, False)
settings["StandbyWakeButton"] = True
device, state, _ = make_device(**settings)
state.started = False
state.ignition = device._ignition = True
@@ -11,7 +11,7 @@ from test_screen_device_runtime import make_device
EXPECTED = {
'StandbyWakeEngage', 'StandbyWakeDisengage', 'StandbyWakeInfoAlert',
'StandbyWakeWarningAlert', 'StandbyWakeCriticalAlert', 'StandbyWakeTurnSignal',
'StandbyWakeWarningAlert', 'StandbyWakeCriticalAlert', 'StandbyWakeTurnSignal', 'StandbyWakeButton',
}
@@ -27,19 +27,26 @@ def test_only_requested_wake_options_are_exposed():
assert keys == EXPECTED
@pytest.mark.parametrize('source', ['touch', 'button'])
def test_touch_and_unassigned_button_always_wake_even_with_old_disabled_settings(source):
device, state, app = make_device(**disabled(), StandbyWakeTouch=False, StandbyWakeButton=False)
def test_touch_always_wakes_with_all_options_disabled():
device, state, app = make_device(**disabled(), StandbyWakeTouch=False)
device._update_wakefulness()
if source == 'touch':
app.mouse_events = [SimpleNamespace(left_down=True)]
else:
state.params_memory.values['StandbyButtonPressTime'] = 99_500_000_000
app.mouse_events = [SimpleNamespace(left_down=True)]
device._update_wakefulness()
assert device.awake
assert device._calculate_brightness() > 0
@pytest.mark.parametrize('enabled', [False, True])
@pytest.mark.parametrize('brightness', [0, 101])
def test_unassigned_buttons_only_wake_when_button_toggle_enabled(enabled, brightness):
device, state, _ = make_device(**{**disabled(), 'StandbyWakeButton': enabled}, ScreenBrightnessOnroad=brightness)
device._update_wakefulness()
state.params_memory.values['StandbyButtonPressTime'] = 99_500_000_000
device._update_wakefulness()
assert device.awake is enabled
assert (device._calculate_brightness() > 0) is enabled
@pytest.mark.parametrize('field,value,old_key', [
('brakePressed', True, 'StandbyWakeBrake'),
('gasPressed', True, 'StandbyWakeAccelerator'),
@@ -98,8 +105,16 @@ def test_dom_manual_zero_suppresses_automatic_status_and_alert_wakes(event):
assert device._calculate_brightness() == 5
def test_dom_ignition_transition_remains_unconditional():
device, state, _ = make_device(**disabled(), StandbyWakeDriveState=False)
state.ignition = False
@pytest.mark.parametrize('ignition', [False, True])
@pytest.mark.parametrize('started', [False, True])
@pytest.mark.parametrize('brightness', [0, 101])
def test_dom_ignition_transition_remains_unconditional(ignition, started, brightness):
device, state, _ = make_device(**disabled(), StandbyWakeDriveState=False,
ScreenBrightness=brightness, ScreenBrightnessOnroad=brightness)
state.started = started
state.ignition = device._ignition = not ignition
device._interaction_time = 90
state.ignition = ignition
device._update_wakefulness()
assert device.awake
assert device._calculate_brightness() > 0
+5 -4
View File
@@ -8,7 +8,7 @@ Auto remains the default. It uses the existing automatic brightness calculation
The existing onroad calculation follows camera exposure and filters changes. Parked Auto retains the existing base level: 50% on comma 3/3X and 65% on comma 4, with existing screen overrides still applied. This feature does not add an offroad ambient-light sensor.
Manual allows 0100% and starts at 100% when there is no previous manual choice. Each context remembers its own manual value when switched to Auto. Existing manual selections remain selected. Touch and recognised button presses temporarily make manual 0% visible at 5%. As in Dom, manual onroad 0% suppresses automatic engagement and alert wakes; the same rule applies to the optional turn-signal wake.
Manual allows 0100% and starts at 100% when there is no previous manual choice. Each context remembers its own manual value when switched to Auto. Existing manual selections remain selected. Touch and ignition changes temporarily make manual 0% visible at 5%; recognised button presses do so when their wake toggle is enabled. As in Dom, manual onroad 0% suppresses automatic engagement and alert wakes; the same rule applies to the optional turn-signal wake.
New Galaxy shows a mode selector and the slider for that mode. Standby uses the normal Galaxy toggle styling and an enabled-only Manage/Close submenu containing the onroad timeout and wake choices. Native settings provide the same preferences using their existing screen sizes and navigation patterns.
@@ -16,7 +16,7 @@ New Galaxy shows a mode selector and the slider for that mode. Standby uses the
Both timeout readouts use seconds, with a 560 second range and 5 second steps. The parked timeout controls normal offroad sleep. The onroad timeout and wake choices are visible when Standby is enabled; the onroad timeout also remains the internal temporary-visibility duration for manual 0%.
Touch and recognised Bluetooth/USB or steering-wheel button presses always wake Standby, including buttons without an assigned action. There are no touch or button wake toggles and no additional controller actions to configure.
Touch and ignition changes always wake Standby, as in Dom. The **Bluetooth or steering wheel button** toggle enables waking from recognised Bluetooth/USB/controller and steering-wheel button presses, including buttons without an assigned action. This toggle defaults to off. There are no touch or ignition wake toggles and no additional controller actions to configure.
| Wake choice | Default | Trigger |
| --- | --- | --- |
@@ -26,16 +26,17 @@ Touch and recognised Bluetooth/USB or steering-wheel button presses always wake
| Warning alerts | On | Dom reports a warning onroad alert |
| Critical / takeover alerts | On | Dom reports a critical or takeover onroad alert |
| Turn signals | Off | Signal activation or direction change |
| Bluetooth or steering wheel button | Off | Recognised button press, including unassigned buttons |
Engagement and alert detection use Dom's existing status and alert predicates. Entering override alone does not wake. A selected alert keeps resetting the timer while it remains reported. Primary alerts take precedence over the secondary StarPilot alert state, as in Dom; this code does not duplicate renderer-generated alerts or add a separate freshness policy. Wake preferences only affect the display, not the alert or its sound.
Dom's ignition transitions, screen-setting changes and page timeout handling are retained. There are no gear, brake-pedal or accelerator wake triggers. Standby powers the display down after the timeout using Dom's existing display-power path.
Vehicle buttons use the car interface's existing decoded `carState.buttonEvents`. The listener drains every message so short presses survive between UI refreshes. Controller buttons use the existing input-device reader. Fresh presses wake once; releases, key repeat and held buttons do not keep extending the timer. Mapped actions retain their separate enable setting and continue to work normally. Button coverage depends on what the existing vehicle interface and supported input devices expose; this PR introduces no manufacturer-specific CAN decoding.
Vehicle buttons use the car interface's existing decoded `carState.buttonEvents`. The listener drains every message so short presses survive between UI refreshes. Controller buttons use the existing input-device reader. When button wake is enabled, fresh presses wake once; releases, key repeat and held buttons do not keep extending the timer. Mapped actions retain their separate enable setting and continue to work normally. Button coverage depends on what the existing vehicle interface and supported input devices expose; this PR introduces no manufacturer-specific CAN decoding.
## Persistence and compatibility
The existing brightness keys retain 101 as Auto and 0100 as Manual. Four additional persistent integers store manual memory and relative offsets. Six persistent booleans store wake selections. StandbyButtonPressTime carries fresh button timestamps in RAM, clears on manager start, and is excluded from logging.
The existing brightness keys retain 101 as Auto and 0100 as Manual. Four additional persistent integers store manual memory and relative offsets. Seven persistent booleans store wake selections. StandbyButtonPressTime carries fresh button timestamps in RAM, clears on manager start, and is excluded from logging.
Native UI and Galaxy writes use one shared validator and an advisory nonblocking file lock outside the Params key directory. Snapshot, write, readback and rollback run within that transaction; UI caches invalidate inside and after it. A busy or failed save is reported and can be retried. Other direct Params writers must use the shared helper to participate in this transaction contract.
@@ -10,7 +10,7 @@ const layout = JSON.parse(fs.readFileSync(path.join(repo, 'starpilot/common/asse
const section = layout.find(s => s.params.some(p => p.key === 'ScreenManagement'))
const wakeDefaults = {
StandbyWakeEngage:true, StandbyWakeDisengage:true, StandbyWakeInfoAlert:true, StandbyWakeWarningAlert:true,
StandbyWakeCriticalAlert:true, StandbyWakeTurnSignal:false,
StandbyWakeCriticalAlert:true, StandbyWakeTurnSignal:false,StandbyWakeButton:false,
}
const wakes = Object.keys(wakeDefaults)
const fixture = `
@@ -154,7 +154,7 @@ createApp({components:{SettingTree},setup:()=>({values}),
assert.equal(await page.locator('.gx-wake-choice, [data-wake-choice]').count(),0,'wake events use ordinary Galaxy switches')
for(const key of wakes) {
const param=section.params.find(p=>p.key===key)
assert.ok(param && param.ui_type==='toggle','all six wake events use standard toggles')
assert.ok(param && param.ui_type==='toggle','all seven wake events use standard toggles')
assert.equal(param.default,wakeDefaults[key],key+' retains its required default')
assert.match(param.description,/wake.*standby|standby.*wake/i,key+' explains waking Standby')
assert.equal(await wakeRow(key).getByText(param.description,{exact:true}).count(),1,key+' description is visible')
@@ -163,7 +163,7 @@ createApp({components:{SettingTree},setup:()=>({values}),
await checkbox.setChecked(!wakeDefaults[key])
await page.waitForFunction(({key,expected})=>window.values[key]===expected,{key,expected:!wakeDefaults[key]})
}
assert.match(section.params.find(p=>p.key==='StandbyMode').description,/Touch.*Bluetooth.*steering wheel.*always wake/i)
assert.match(section.params.find(p=>p.key==='StandbyMode').description,/Touch and ignition changes always wake/i)
assert.deepEqual(section.params.filter(p=>p.key.startsWith('StandbyWake')).map(p=>p.key).sort(), [...wakes].sort())
await page.waitForFunction(()=>!Array.from(document.querySelectorAll('.gx-switch input')).some(input=>input.disabled))
await page.evaluate(()=>{window.values.ScreenBrightnessOffset=80})
@@ -192,6 +192,6 @@ createApp({components:{SettingTree},setup:()=>({values}),
await page.evaluate(()=>{window.values.StandbyMode=true;window.values.ScreenManagement=false})
assert.equal(await wakeRow(wakes[0]).count(),0,'disabled Screen Settings hides wake choices')
assert.deepEqual(errors,[])
console.log('PASS: real Vue Auto/Manual controls, manual100 default/reset, +/-30% offsets, memory/zero, independent contexts, drag/pending stability, save rollback, six described standard wake toggles, Manage/Close submenu, seconds readouts, mobile layout, zero page errors')
console.log('PASS: real Vue Auto/Manual controls, manual100 default/reset, +/-30% offsets, memory/zero, independent contexts, drag/pending stability, save rollback, seven described standard wake toggles, Manage/Close submenu, seconds readouts, mobile layout, zero page errors')
} finally {await browser.close()}
})().catch(error=>{console.error(error);process.exitCode=1})
@@ -24,7 +24,7 @@ def packet(timestamp, pressed=None, *, valid=True, button_type='accelCruise'):
@pytest.fixture
def car_buttons(monkeypatch):
params = FakeParams({'ScreenManagement': True, 'StandbyMode': True, 'IsOnroad': True})
params = FakeParams({'ScreenManagement': True, 'StandbyMode': True, 'StandbyWakeButton': True, 'IsOnroad': True})
memory = FakeParams()
daemon = wheel_controlsd.WheelControlsDaemon(params, memory)
queued, subscriptions = [], []
@@ -65,7 +65,7 @@ def test_short_button_edge_survives_later_empty_frame_and_is_consumed_once(car_b
assert len(subscriptions) == 1
@pytest.mark.parametrize('disabled', ['ScreenManagement', 'StandbyMode', 'IsOnroad'])
@pytest.mark.parametrize('disabled', ['ScreenManagement', 'StandbyMode', 'StandbyWakeButton', 'IsOnroad'])
def test_subscription_only_runs_when_needed_and_reopens_cleanly(car_buttons, disabled):
daemon, params, memory, queued, subscriptions, _boot, now = car_buttons
params.put_bool(disabled, False)
@@ -15,7 +15,7 @@ PRESS_PARAM = "StandbyButtonPressTime"
@pytest.fixture
def input_pipe():
params = FakeParams({"ScreenManagement": True, "StandbyMode": True})
params = FakeParams({"ScreenManagement": True, "StandbyMode": True, "StandbyWakeButton": True})
memory = FakeParams()
daemon = wheel_controlsd.WheelControlsDaemon(params, memory)
read_fd, write_fd = os.pipe()
@@ -60,7 +60,7 @@ def test_selected_joystick_buttons_wake_without_executing_mappings(input_pipe, m
assert actions == []
@pytest.mark.parametrize("disabled_key", ["ScreenManagement", "StandbyMode"])
@pytest.mark.parametrize("disabled_key", ["ScreenManagement", "StandbyMode", "StandbyWakeButton"])
def test_enabling_standby_while_held_does_not_create_a_press(input_pipe, monkeypatch, disabled_key):
_daemon, params, memory, _fd, send = input_pipe
params.put_bool(disabled_key, False)
@@ -111,17 +111,19 @@ def test_wake_only_listener_does_not_reactivate_disabled_mappings(input_pipe, mo
assert memory.get_int(PRESS_PARAM) == 999
def test_enabled_mapping_still_executes_once_on_press_with_wake_timestamp(input_pipe, monkeypatch):
@pytest.mark.parametrize("wake_enabled", [False, True])
def test_enabled_mapping_still_executes_once_on_press_with_wake_timestamp(input_pipe, monkeypatch, wake_enabled):
_daemon, params, memory, _fd, send = input_pipe
wheel_controlsd.upsert_mapping(source(), 30, 2, params)
actions = []
monkeypatch.setattr(wheel_controlsd, "execute_mapping_slot", lambda slot, *_args: actions.append(slot))
monkeypatch.setattr(wheel_controlsd.time, "monotonic_ns", lambda: 777)
params.put_bool("StandbyWakeButton", wake_enabled)
for value in (1, 2, 0):
send(wheel_controlsd.EV_KEY, 30, value)
assert actions == [2]
assert memory.get_int(PRESS_PARAM) == 777
assert memory.get(PRESS_PARAM) == (777 if wake_enabled else None)
def test_wake_timestamp_failure_does_not_interrupt_mapped_button_actions(input_pipe, monkeypatch):
@@ -162,7 +164,7 @@ def test_disconnected_device_does_not_suppress_next_press_on_reused_descriptor(i
(False, True, True, True, True),
(False, False, True, True, False),
(False, True, False, True, False),
(False, True, True, False, True),
(False, True, True, False, False),
(True, False, False, False, True),
])
def test_manager_runs_listener_for_enabled_mappings_or_standby(started, mapping, management, standby, button, expected):
@@ -664,7 +664,7 @@ class WheelControlsDaemon:
def _publish_button_press(self, timestamp: int) -> None:
try:
if not all(self.params.get_bool(key) for key in ("ScreenManagement", "StandbyMode")):
if not all(self.params.get_bool(key) for key in ("ScreenManagement", "StandbyMode", "StandbyWakeButton")):
return
self.params_memory.put_int(STANDBY_BUTTON_PRESS_PARAM, timestamp)
except Exception:
@@ -677,7 +677,7 @@ class WheelControlsDaemon:
self._last_car_button_frame = 0
def _configure_car_buttons(self) -> None:
if not all(self.params.get_bool(key) for key in ("ScreenManagement", "StandbyMode", "IsOnroad")):
if not all(self.params.get_bool(key) for key in ("ScreenManagement", "StandbyMode", "StandbyWakeButton", "IsOnroad")):
self._close_car_buttons()
return
if self._car_state_sock is not None:
+1 -1
View File
@@ -135,7 +135,7 @@ def soundd_run(started: bool, params: Params, CP: car.CarParams, starpilot_toggl
def wheel_controls_enabled(started: bool, params: Params, CP: car.CarParams, starpilot_toggles: SimpleNamespace) -> bool:
return params.get_bool("WheelControlsEnabled") or (
params.get_bool("ScreenManagement") and params.get_bool("StandbyMode")
params.get_bool("ScreenManagement") and params.get_bool("StandbyMode") and params.get_bool("StandbyWakeButton")
)