Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d36d52d
initial
sshane Feb 6, 2026
6d9fb09
start to support nav stack in settings panels + fix some navwidget bugs
sshane Feb 6, 2026
8ecc21f
add deprecation warning and move more to new nav stack
sshane Feb 6, 2026
6ec007e
fix overriding NavWidget enabled and do developer panel
sshane Feb 6, 2026
7dfb849
fix interactive timeout and do main
sshane Feb 6, 2026
0cdf32c
more device, not done yet
sshane Feb 6, 2026
fb26395
minor network fixes
sshane Feb 6, 2026
952d406
dcam dialog
sshane Feb 6, 2026
ebe52da
start onboarding
sshane Feb 6, 2026
3925b49
fix onboarding
sshane Feb 6, 2026
afed1e8
do mici setup
sshane Feb 6, 2026
3966feb
Merge remote-tracking branch 'upstream/master' into nav-stack
sshane Feb 6, 2026
150630f
Merge remote-tracking branch 'upstream/master' into nav-stack
sshane Feb 6, 2026
c13f4f0
remove now useless CUSTOM_SOFTWARE
sshane Feb 6, 2026
96a2544
support big ui with old modal overlay
sshane Feb 6, 2026
391c911
reset can be old modal overlay, but updater needs new since it uses w…
sshane Feb 6, 2026
14962b5
flip name truthiness to inspire excitement
sshane Feb 6, 2026
a0a9057
all *should* work, but will do pass later
sshane Feb 6, 2026
6cac211
clean up main
sshane Feb 6, 2026
0d383ed
clean up settiings
sshane Feb 6, 2026
62d89a6
clean up dialog and developer
sshane Feb 6, 2026
ca97c35
cleanup mici setup some
sshane Feb 6, 2026
78a81c8
rm one more
sshane Feb 6, 2026
9ef2255
fix keyboard
sshane Feb 6, 2026
02b3ddc
revert
sshane Feb 6, 2026
96c3c25
might as well but clarify
sshane Feb 6, 2026
da637a5
fix networkinfopage buttons
sshane Feb 6, 2026
67424e2
lint
sshane Feb 6, 2026
e77f5ce
nice clean up from cursor
sshane Feb 6, 2026
589f816
animate background fade with position
sshane Feb 6, 2026
7e2fd37
Merge remote-tracking branch 'upstream/master' into nav-stack
sshane Feb 7, 2026
a963af2
Merge remote-tracking branch 'upstream/master' into nav-stack
sshane Feb 7, 2026
f59d35a
fix device overlays
sshane Feb 7, 2026
3e9ee7a
cursor fix pt1
sshane Feb 7, 2026
46accb9
rm print
sshane Feb 7, 2026
09a2271
capital
sshane Feb 7, 2026
516a944
temp fix from cursor for onboarding not freeing space after reviewing…
sshane Feb 7, 2026
56a3e34
Merge remote-tracking branch 'upstream/master' into nav-stack
sshane Feb 19, 2026
ad144eb
fix home screen scroller snap not resetting
sshane Feb 20, 2026
488d651
stash
sshane Feb 20, 2026
61551a6
nice gradient on top
sshane Feb 20, 2026
8a14fe0
40
sshane Feb 20, 2026
12896b5
20
sshane Feb 20, 2026
e1d0ad8
no gradient
sshane Feb 20, 2026
6a691f3
return unused returns and always show regulatory btn
sshane Feb 20, 2026
188a587
Merge remote-tracking branch 'upstream/master' into nav-stack
sshane Feb 20, 2026
3b856a6
nice!
sshane Feb 20, 2026
3adf39f
Initial plan
Copilot Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions selfdrive/ui/layouts/settings/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,10 @@ def _initialize_items(self):
self._reset_calib_btn,
button_item(lambda: tr("Review Training Guide"), lambda: tr("REVIEW"), lambda: tr(DESCRIPTIONS['review_guide']),
self._on_review_training_guide, enabled=ui_state.is_offroad),
regulatory_btn := button_item(lambda: tr("Regulatory"), lambda: tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad),
button_item(lambda: tr("Regulatory"), lambda: tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad),
button_item(lambda: tr("Change Language"), lambda: tr("CHANGE"), callback=self._show_language_dialog),
self._power_off_btn,
]
regulatory_btn.set_visible(TICI)
return items

def _offroad_transition(self):
Expand Down
81 changes: 29 additions & 52 deletions selfdrive/ui/mici/layouts/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import pyray as rl
from enum import IntEnum
import cereal.messaging as messaging
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout
Expand All @@ -15,18 +14,12 @@
ONROAD_DELAY = 2.5 # seconds


class MainState(IntEnum):
MAIN = 0
SETTINGS = 1


class MiciMainLayout(Widget):
def __init__(self):
super().__init__()

self._pm = messaging.PubMaster(['bookmarkButton'])

self._current_mode: MainState | None = None
self._prev_onroad = False
self._prev_standstill = False
self._onroad_time_delay: float | None = None
Expand All @@ -49,38 +42,31 @@ def __init__(self):
self._onroad_layout,
], spacing=0, pad_start=0, pad_end=0, scroll_indicator=False, edge_shadows=False)
self._scroller.set_reset_scroll_at_show(False)
self._scroller.set_enabled(lambda: self.enabled) # for nav stack

# Disable scrolling when onroad is interacting with bookmark
self._scroller.set_scrolling_enabled(lambda: not self._onroad_layout.is_swiping_left())

self._layouts = {
MainState.MAIN: self._scroller,
MainState.SETTINGS: self._settings_layout,
}

# Set callbacks
self._setup_callbacks()

# Start onboarding if terms or training not completed
gui_app.push_widget(self)

# Start onboarding if terms or training not completed, make sure to push after self
self._onboarding_window = OnboardingWindow()
if not self._onboarding_window.completed:
gui_app.set_modal_overlay(self._onboarding_window)
gui_app.push_widget(self._onboarding_window)

def _setup_callbacks(self):
self._home_layout.set_callbacks(on_settings=self._on_settings_clicked)
self._settings_layout.set_callbacks(on_close=self._on_settings_closed)
self._home_layout.set_callbacks(on_settings=lambda: gui_app.push_widget(self._settings_layout))
self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
device.add_interactive_timeout_callback(self._set_mode_for_started)
device.add_interactive_timeout_callback(self._on_interactive_timeout)

def _scroll_to(self, layout: Widget):
layout_x = int(layout.rect.x)
self._scroller.scroll_to(layout_x, smooth=True)

def _render(self, _):
# Initial show event
if self._current_mode is None:
self._set_mode(MainState.MAIN)

if not self._setup:
if self._alerts_layout.active_alerts() > 0:
self._scroller.scroll_to(self._alerts_layout.rect.x)
Expand All @@ -89,59 +75,50 @@ def _render(self, _):
self._setup = True

# Render
if self._current_mode == MainState.MAIN:
self._scroller.render(self._rect)

elif self._current_mode == MainState.SETTINGS:
self._settings_layout.render(self._rect)
self._scroller.render(self._rect)

self._handle_transitions()

def _set_mode(self, mode: MainState):
if mode != self._current_mode:
if self._current_mode is not None:
self._layouts[self._current_mode].hide_event()
self._layouts[mode].show_event()
self._current_mode = mode

def _handle_transitions(self):
# Don't pop if onboarding
if gui_app.get_active_widget() == self._onboarding_window:
return

if ui_state.started != self._prev_onroad:
self._prev_onroad = ui_state.started

# onroad: after delay, pop nav stack and scroll to onroad
# offroad: immediately scroll to home, but don't pop nav stack (can stay in settings)
if ui_state.started:
self._onroad_time_delay = rl.get_time()
else:
self._set_mode_for_started(True)
self._scroll_to(self._home_layout)

# delay so we show home for a bit after starting
if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY:
self._set_mode_for_started(True)
gui_app.pop_widgets_to(self)
self._scroll_to(self._onroad_layout)
self._onroad_time_delay = None

# When car leaves standstill, pop nav stack and scroll to onroad
CS = ui_state.sm["carState"]
if not CS.standstill and self._prev_standstill:
self._set_mode(MainState.MAIN)
gui_app.pop_widgets_to(self)
self._scroll_to(self._onroad_layout)
self._prev_standstill = CS.standstill

def _set_mode_for_started(self, onroad_transition: bool = False):
def _on_interactive_timeout(self):
# Don't pop if onboarding
if gui_app.get_active_widget() == self._onboarding_window:
return

if ui_state.started:
CS = ui_state.sm["carState"]
# Only go onroad if car starts or is not at a standstill
if not CS.standstill or onroad_transition:
self._set_mode(MainState.MAIN)
# Don't pop if at standstill
if not ui_state.sm["carState"].standstill:
gui_app.pop_widgets_to(self)
self._scroll_to(self._onroad_layout)
else:
# Stay in settings if car turns off while in settings
if not onroad_transition or self._current_mode != MainState.SETTINGS:
self._set_mode(MainState.MAIN)
self._scroll_to(self._home_layout)

def _on_settings_clicked(self):
self._set_mode(MainState.SETTINGS)

def _on_settings_closed(self):
self._set_mode(MainState.MAIN)
gui_app.pop_widgets_to(self)
self._scroll_to(self._home_layout)

def _on_bookmark_clicked(self):
user_bookmark = messaging.new_message('bookmarkButton')
Expand Down
14 changes: 7 additions & 7 deletions selfdrive/ui/mici/layouts/onboarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets import Widget, NavWidget
from openpilot.system.ui.widgets.button import SmallButton, SmallCircleIconButton
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.slider import SmallSlider
Expand Down Expand Up @@ -42,7 +42,7 @@ def _render(self, rect):
gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
rl.end_scissor_mode()
return -1
return

# Position dmoji on opposite side from driver
is_rhd = self.driver_state_renderer.is_rhd
Expand All @@ -55,7 +55,6 @@ def _render(self, rect):
self._draw_face_detection(rect)

rl.end_scissor_mode()
return -1


class TrainingGuidePreDMTutorial(SetupTermsPage):
Expand Down Expand Up @@ -367,9 +366,9 @@ def _advance_step(self):
self._completed_callback()

def _render(self, _):
rl.draw_rectangle_rec(self._rect, rl.BLACK)
if self._step < len(self._steps):
self._steps[self._step].render(self._rect)
return -1


class DeclinePage(Widget):
Expand Down Expand Up @@ -438,9 +437,11 @@ def _render_content(self, scroll_offset):
))


class OnboardingWindow(Widget):
class OnboardingWindow(NavWidget):
def __init__(self):
super().__init__()
self.set_back_enabled(False)

self._accepted_terms: bool = ui_state.params.get("HasAcceptedTerms") == terms_version
self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == training_version

Expand Down Expand Up @@ -473,7 +474,7 @@ def _on_decline_back(self):

def close(self):
ui_state.params.put_bool("IsDriverViewEnabled", False)
gui_app.set_modal_overlay(None)
gui_app.pop_widget()

def _on_terms_accepted(self):
ui_state.params.put("HasAcceptedTerms", terms_version)
Expand All @@ -490,4 +491,3 @@ def _render(self, _):
self._training_guide.render(self._rect)
elif self._state == OnboardingState.DECLINE:
self._decline_page.render(self._rect)
return -1
11 changes: 5 additions & 6 deletions selfdrive/ui/mici/layouts/settings/developer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import pyray as rl
from collections.abc import Callable

from openpilot.common.time_helpers import system_time_valid
from openpilot.system.ui.widgets.scroller import Scroller
Expand All @@ -13,9 +12,9 @@


class DeveloperLayoutMici(NavWidget):
def __init__(self, back_callback: Callable):
def __init__(self):
super().__init__()
self.set_back_callback(back_callback)
self.set_back_callback(gui_app.pop_widget)

def github_username_callback(username: str):
if username:
Expand All @@ -25,16 +24,16 @@ def github_username_callback(username: str):
self._ssh_keys_btn.set_value(username)
else:
dlg = BigDialog("", ssh_keys._error_message)
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)

def ssh_keys_callback():
github_username = ui_state.params.get("GithubUsername") or ""
dlg = BigInputDialog("enter GitHub username...", github_username, confirm_callback=github_username_callback)
if not system_time_valid():
dlg = BigDialog("Please connect to Wi-Fi to fetch your key", "")
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)
return
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)

txt_ssh = gui_app.texture("icons_mici/settings/developer/ssh.png", 56, 64)
github_username = ui_state.params.get("GithubUsername") or ""
Expand Down
38 changes: 11 additions & 27 deletions selfdrive/ui/mici/layouts/settings/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class MiciFccModal(NavWidget):

def __init__(self, file_path: str | None = None, text: str | None = None):
super().__init__()
self.set_back_callback(lambda: gui_app.set_modal_overlay(None))
self.set_back_callback(gui_app.pop_widget)
self._content = HtmlRenderer(file_path=file_path, text=text)
self._scroll_panel = GuiScrollPanel2(horizontal=False)
self._fcc_logo = gui_app.texture("icons_mici/settings/device/fcc_logo.png", 76, 64)
Expand All @@ -47,8 +47,6 @@ def _render(self, rect: rl.Rectangle):

rl.draw_texture_ex(self._fcc_logo, fcc_pos, 0.0, 1.0, rl.WHITE)

return -1


def _engaged_confirmation_callback(callback: Callable, action_text: str):
if not ui_state.engaged:
Expand All @@ -74,10 +72,10 @@ def confirm_callback():
dlg: BigConfirmationDialogV2 | BigDialog = BigConfirmationDialogV2(f"slide to\n{action_text.lower()}", icon, red=red,
exit_on_confirm=action_text == "reset",
confirm_callback=confirm_callback)
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)
else:
dlg = BigDialog(f"Disengage to {action_text}", "")
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)


class DeviceInfoLayoutMici(Widget):
Expand Down Expand Up @@ -147,7 +145,7 @@ def _handle_mouse_release(self, mouse_pos: MousePos):
dlg = BigDialog(tr("Device must be registered with the comma.ai backend to pair"), "")
else:
dlg = PairingDialog()
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)


UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond
Expand All @@ -173,7 +171,7 @@ def offroad_transition(self):
def _handle_mouse_release(self, mouse_pos: MousePos):
if not system_time_valid():
dlg = BigDialog(tr("Please connect to Wi-Fi to update"), "")
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)
return

self.set_enabled(False)
Expand Down Expand Up @@ -268,12 +266,10 @@ def _update_state(self):


class DeviceLayoutMici(NavWidget):
def __init__(self, back_callback: Callable):
def __init__(self):
super().__init__()

self._fcc_dialog: HtmlModal | None = None
self._driver_camera: DriverCameraDialog | None = None
self._training_guide: TrainingGuide | None = None

def power_off_callback():
ui_state.params.put_bool("DoShutdown", True)
Expand Down Expand Up @@ -309,11 +305,11 @@ def uninstall_openpilot_callback():
regulatory_btn.set_click_callback(self._on_regulatory)

driver_cam_btn = BigButton("driver\ncamera preview", "", "icons_mici/settings/device/cameras.png")
driver_cam_btn.set_click_callback(self._show_driver_camera)
driver_cam_btn.set_click_callback(lambda: gui_app.push_widget(DriverCameraDialog()))
driver_cam_btn.set_enabled(lambda: ui_state.is_offroad())

review_training_guide_btn = BigButton("review\ntraining guide", "", "icons_mici/settings/device/info.png")
review_training_guide_btn.set_click_callback(self._on_review_training_guide)
review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(TrainingGuide(completed_callback=gui_app.pop_widget)))
review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad())

self._scroller = Scroller([
Expand All @@ -330,32 +326,20 @@ def uninstall_openpilot_callback():
], snap_items=False)

# Set up back navigation
self.set_back_callback(back_callback)
# TODO: can this somehow be generic in widgets/__init__.py or application.py?
self.set_back_callback(gui_app.pop_widget)

# Hide power off button when onroad
ui_state.add_offroad_transition_callback(self._offroad_transition)

def _on_regulatory(self):
if not self._fcc_dialog:
self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/mici_fcc.html"))
gui_app.set_modal_overlay(self._fcc_dialog)
gui_app.push_widget(self._fcc_dialog)

def _offroad_transition(self):
self._power_off_btn.set_visible(ui_state.is_offroad())

def _show_driver_camera(self):
if not self._driver_camera:
self._driver_camera = DriverCameraDialog()
gui_app.set_modal_overlay(self._driver_camera, callback=lambda result: setattr(self, '_driver_camera', None))

def _on_review_training_guide(self):
if not self._training_guide:
def completed_callback():
gui_app.set_modal_overlay(None)

self._training_guide = TrainingGuide(completed_callback=completed_callback)
gui_app.set_modal_overlay(self._training_guide, callback=lambda result: setattr(self, '_training_guide', None))

def show_event(self):
super().show_event()
self._scroller.show_event()
Expand Down
7 changes: 2 additions & 5 deletions selfdrive/ui/mici/layouts/settings/firehose.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,6 @@ def _render(self, rect: rl.Rectangle):
y = self._draw_wrapped_text(x, y, w, tr(answer), gui_app.font(FontWeight.ROMAN), 32, self.LIGHT_GRAY)
y += 20

# return value not used by NavWidget
return -1

def _draw_wrapped_text(self, x, y, width, text, font, font_size, color):
wrapped = wrap_text(font, text, font_size, width)
for line in wrapped:
Expand Down Expand Up @@ -223,6 +220,6 @@ def _update_loop(self):
class FirehoseLayout(FirehoseLayoutBase, NavWidget):
BACK_TOUCH_AREA_PERCENTAGE = 0.1

def __init__(self, back_callback):
def __init__(self):
super().__init__()
self.set_back_callback(back_callback)
self.set_back_callback(gui_app.pop_widget)
Loading
Loading