Setup: improvements (#37264)

* pressed state for larger sliders

* wifibutton

* fix

* clean up

* some work

* don't nee this now

* stash

* more

* new pressed bigcircle

* black

* interp

* just check position

* clean up and fix slider reset

* fix custom

* no speed

* stash

* even chatter couldn't figure this one out

* makes sense to combine together, less split mentality

* clean that up

* fix lag

* match ui.py prio to eliminate lag on wifiui show event. separately, why is this slow?

* night mode

* delay scroll over

* fix auto scrolling

* stash

* waiting looks disabled

* clean up and don't reset sliders until user goes back

* rm

* fix

* add termsheader back

* fix callbacks

* ctrl alt l

* fix text spacing

* clean up

* stash

* fix style

* i want to go back

* guard on exit

* kinda useless stuff

* Revert "kinda useless stuff"

This reverts commit a4acbac31523408f358c5f68262cb630aa13ad8e.

* Revert "guard on exit"

This reverts commit 63ccfbf64edfbe1a144a441681f5ec78d8021ff7.

* wide

* setup pressed!

* grow animation

* 10s after initial

* slow fast

* start onboarding (terms)

* rm duplicate page

* add qr code

* final grey

* fix visual lag on first start

* clean up dead code

* dont exit from cancel

* revert grey

* clean up, REVIEW ME

* Revert "clean up, REVIEW ME"

This reverts commit c66fa60947c5f922520e7cf58c630b4bbe2d0177.

* reboot slider

* kb fix

* Revert "kb fix"

This reverts commit 883039448e6c37ae1d25d4f75ada6e96b6736358.

* ./ goes to letters

* Revert "./ goes to letters"

This reverts commit 0d97442427edb1a000638863a3f2181204ddc160.

* clean up

* some more clean up

* more

* clean up

* rename block

* reset pending scroll so it can't use stale data in rare sequence

* remove unused assets

* clean up imports

* fix updater

* clean up

* fix double reboot

* demo time - reset to setup on reboot

* let manager restart

* Revert "demo time - reset to setup on reboot"

This reverts commit 9468657e8438a1ce8fcb5266403b7bb3539f131f.

* url... and no grow animation on start button

* one next button

* grow instead of shake wifi button

* 36 pt font size in setup

* touch up onboarding a lil

* Revert "rm cpp bz2 (#37332)"

This reverts commit f4a36f7f74.

* more onboarding and clean up

* clean up

* wow what an amazing future clean up

* back to software select

* fix

* copy

* fix dm confirmation dialog not disabling widget underneath, all fixed with real nav stack in here

* uploading

* lint

* add review terms to device w/ close button

* todo

* remove old Terms vertical scrolling classes

* use new Scroller!

* installer

* tweak to match figma exactly

* revert

* fixup updater

* demo day

* demo day v2

* ... for percent while finishing setup

* demo day v3

* demo day v4

* remove ...

* demo day v6 -- "why does it do that!!"

* demo day v7 -- no flash

* hmm

* demo day v7

* prebuilt

* revert demo day

* scroll after pop animation

* back -> retry

* stash fixes

* damn, need back_callback

* scroll over immediately if already in network setup

* tweaks

* going down is confusing

* more

* Revert "more"

This reverts commit 29ce75b1f81eb40e7527a71d27842d9a66802206.

* Revert "going down is confusing"

This reverts commit 0cd2ae30d4135db1ccba6478429b45e886714e9c.

* dupl

* nl

* sort functions

* more clean up from merge

* move

* more

* dismiss to download (hack)

* Revert "dismiss to download (hack)"

This reverts commit 53c45ed1f63db1f0cebbce0dfab1777c8658f505.

* onboarding work

* set brightness and timeout in root onboarding only

* clean up

* type

* keep 5m for settings preview

* switch back to letters on . or /

* reset first step scroller

* custom software warning goes down network comes up and back cb fix

* clean up

* smaller qr

* ReviewTermsPage just for device as NavWidget

* clean up

* installer: stay on 100%

* reset has internet while in wifiui

* try this

* try this

* see what error we get exactly

see what error we get exactly

* not final solution but see how good

* rm

* copy changes

* reset on disconnect

* for separate pr

* Revert "reset on disconnect"

This reverts commit 552372fa4d497ba7d9de7f2edb730ee63798ffa4.

* revert this, too buggy

* fix for updater

* sort

* fix test

* minor cleanup

* more leaks than this rn

* onboarding clean up

* clean up application

* click delay to small button

* clean up

* reset more state

* fix training guide not cleaning up driverview

* Revert "fix training guide not cleaning up driverview"

This reverts commit cac7c5f436056cc9e747f80905d390790fb83c22.

* simpler fix :(

* nice catch, if you go back to terms it will reset 300s timeout and brightness

* duplicate show

* unused
This commit is contained in:
Shane Smiskol
2026-03-03 01:06:51 -08:00
committed by GitHub
parent 2ebf09eb07
commit 91b7752268
16 changed files with 456 additions and 587 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -81,16 +81,14 @@ void run(const char* cmd) {
}
void finishInstall() {
BeginDrawing();
ClearBackground(BLACK);
if (tici_device) {
if (tici_device) {
BeginDrawing();
ClearBackground(BLACK);
const char *m = "Finishing install...";
int text_width = MeasureText(m, FONT_SIZE);
DrawTextEx(font_display, m, (Vector2){(float)(GetScreenWidth() - text_width)/2 + FONT_SIZE, (float)(GetScreenHeight() - FONT_SIZE)/2}, FONT_SIZE, 0, WHITE);
} else {
DrawTextEx(font_display, "finishing setup", (Vector2){12, 0}, 77, 0, (Color){255, 255, 255, (unsigned char)(255 * 0.9)});
}
EndDrawing();
EndDrawing();
}
util::sleep_for(60 * 1000);
}

View File

@@ -53,7 +53,7 @@ class MiciMainLayout(Scroller):
gui_app.push_widget(self)
# Start onboarding if terms or training not completed, make sure to push after self
self._onboarding_window = OnboardingWindow()
self._onboarding_window = OnboardingWindow(lambda: gui_app.pop_widgets_to(self))
if not self._onboarding_window.completed:
gui_app.push_widget(self._onboarding_window)
@@ -79,7 +79,7 @@ class MiciMainLayout(Scroller):
def _handle_transitions(self):
# Don't pop if onboarding
if gui_app.get_active_widget() == self._onboarding_window:
if gui_app.widget_in_stack(self._onboarding_window):
return
if ui_state.started != self._prev_onroad:
@@ -105,7 +105,7 @@ class MiciMainLayout(Scroller):
def _on_interactive_timeout(self):
# Don't pop if onboarding
if gui_app.get_active_widget() == self._onboarding_window:
if gui_app.widget_in_stack(self._onboarding_window):
return
if ui_state.started:

View File

@@ -1,29 +1,23 @@
from enum import IntEnum
import weakref
import math
import numpy as np
import qrcode
import pyray as rl
from collections.abc import Callable
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.button import SmallButton, SmallCircleIconButton
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.slider import SmallSlider
from openpilot.system.ui.mici_setup import TermsHeader, TermsPage as SetupTermsPage
from openpilot.selfdrive.ui.ui_state import ui_state, device
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import BaseDriverCameraDialog
from openpilot.system.ui.widgets.button import SmallCircleIconButton
from openpilot.system.ui.widgets.scroller import NavScroller, Scroller
from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.system.ui.mici_setup import GreyBigButton, BigPillButton
from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.version import terms_version, training_version
class OnboardingState(IntEnum):
TERMS = 0
ONBOARDING = 1
DECLINE = 2
from openpilot.selfdrive.ui.ui_state import ui_state, device
from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialogV2
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import BaseDriverCameraDialog
class DriverCameraSetupDialog(BaseDriverCameraDialog):
@@ -57,91 +51,62 @@ class DriverCameraSetupDialog(BaseDriverCameraDialog):
rl.end_scissor_mode()
class TrainingGuidePreDMTutorial(SetupTermsPage):
def __init__(self, continue_callback):
super().__init__(continue_callback, continue_text="continue")
self._title_header = TermsHeader("driver monitoring setup", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
class TrainingGuidePreDMTutorial(NavScroller):
def __init__(self, continue_callback: Callable[[], None]):
super().__init__()
self._dm_label = UnifiedLabel("Next, we'll ensure comma four is mounted properly.\n\nIf it does not have a clear view of the driver, " +
"unplug and remount before continuing.", 42,
FontWeight.ROMAN)
continue_button = BigPillButton("next")
continue_button.set_click_callback(continue_callback)
self._scroller.add_widgets([
GreyBigButton("driver monitoring\ncheck", "scroll to continue",
gui_app.texture("icons_mici/setup/green_dm.png", 64, 64)),
GreyBigButton("", "Next, we'll check if comma four can detect the driver properly."),
GreyBigButton("", "openpilot uses the cabin camera to check if the driver is distracted."),
GreyBigButton("", "If it does not have a clear view of the driver, unplug and remount before continuing."),
continue_button,
])
def show_event(self):
super().show_event()
# Get driver monitoring model ready for next step
ui_state.params.put_bool("IsDriverViewEnabled", True)
@property
def _content_height(self):
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
def _render_content(self, scroll_offset):
self._title_header.render(rl.Rectangle(
self._rect.x + 16,
self._rect.y + 16 + scroll_offset,
self._title_header.rect.width,
self._title_header.rect.height,
))
self._dm_label.render(rl.Rectangle(
self._rect.x + 16,
self._title_header.rect.y + self._title_header.rect.height + 16,
self._rect.width - 32,
self._dm_label.get_content_height(int(self._rect.width - 32)),
))
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", True)
class DMBadFaceDetected(SetupTermsPage):
def __init__(self, continue_callback, back_callback):
super().__init__(continue_callback, back_callback, continue_text="power off")
self._title_header = TermsHeader("make sure comma four can see your face", gui_app.texture("icons_mici/setup/orange_dm.png", 60, 60))
self._dm_label = UnifiedLabel("Re-mount if your face is occluded or driver monitoring has difficulty tracking your face.", 42, FontWeight.ROMAN)
class DMBadFaceDetected(NavScroller):
def __init__(self):
super().__init__()
@property
def _content_height(self):
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
back_button = BigPillButton("back")
back_button.set_click_callback(self.dismiss)
def _render_content(self, scroll_offset):
self._title_header.render(rl.Rectangle(
self._rect.x + 16,
self._rect.y + 16 + scroll_offset,
self._title_header.rect.width,
self._title_header.rect.height,
))
self._dm_label.render(rl.Rectangle(
self._rect.x + 16,
self._title_header.rect.y + self._title_header.rect.height + 16,
self._rect.width - 32,
self._dm_label.get_content_height(int(self._rect.width - 32)),
))
self._scroller.add_widgets([
GreyBigButton("looking for driver", "make sure comma\nfour can see your face",
gui_app.texture("icons_mici/setup/orange_dm.png", 64, 64)),
GreyBigButton("", "Remount if your face is blocked, or driver monitoring has difficulty tracking your face."),
back_button,
])
class TrainingGuideDMTutorial(Widget):
class TrainingGuideDMTutorial(NavWidget):
PROGRESS_DURATION = 4
LOOKING_THRESHOLD_DEG = 30.0
def __init__(self, continue_callback):
def __init__(self, continue_callback: Callable[[], None]):
super().__init__()
self_ref = weakref.ref(self)
self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 28, 48))
self._back_button.set_click_callback(lambda: self_ref() and self_ref()._show_bad_face_page())
self._back_button.set_click_callback(lambda: gui_app.push_widget(self._bad_face_page))
self._back_button.set_touch_valid_callback(lambda: self.enabled and not self.is_dismissing) # for nav stack
self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 42, 42))
self._good_button.set_touch_valid_callback(lambda: self.enabled and not self.is_dismissing) # for nav stack
# Wrap the continue callback to restore settings
def wrapped_continue_callback():
device.set_offroad_brightness(None)
continue_callback()
self._good_button.set_click_callback(wrapped_continue_callback)
self._good_button.set_click_callback(continue_callback)
self._good_button.set_enabled(False)
self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps)
self._dialog = DriverCameraSetupDialog()
self._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, lambda: self_ref() and self_ref()._hide_bad_face_page())
self._should_show_bad_face_page = False
self._bad_face_page = DMBadFaceDetected()
# Disable driver monitoring model when device times out for inactivity
def inactivity_callback():
@@ -149,23 +114,11 @@ class TrainingGuideDMTutorial(Widget):
device.add_interactive_timeout_callback(inactivity_callback)
def _show_bad_face_page(self):
self._bad_face_page.show_event()
self.hide_event()
self._should_show_bad_face_page = True
def _hide_bad_face_page(self):
self._bad_face_page.hide_event()
self.show_event()
self._should_show_bad_face_page = False
def show_event(self):
super().show_event()
self._dialog.show_event()
self._progress.x = 0.0
device.set_offroad_brightness(100)
def _update_state(self):
super()._update_state()
if device.awake and not ui_state.params.get_bool("IsDriverViewEnabled"):
@@ -185,7 +138,8 @@ class TrainingGuideDMTutorial(Widget):
looking_center = False
# stay at 100% once reached
if (dm_state.faceDetected and looking_center) or self._progress.x > 0.99:
in_bad_face = gui_app.get_active_widget() == self._bad_face_page
if ((dm_state.faceDetected and looking_center) or self._progress.x > 0.99) and not in_bad_face:
slow = self._progress.x < 0.25
duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION
self._progress.x += 1.0 / (duration * gui_app.target_fps)
@@ -196,9 +150,6 @@ class TrainingGuideDMTutorial(Widget):
self._good_button.set_enabled(self._progress.x >= 0.999)
def _render(self, _):
if self._should_show_bad_face_page:
return self._bad_face_page.render(self._rect)
self._dialog.render(self._rect)
rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 80),
@@ -255,228 +206,193 @@ class TrainingGuideDMTutorial(Widget):
))
# rounded border
rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height))
rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK)
rl.end_scissor_mode()
class TrainingGuideRecordFront(SetupTermsPage):
def __init__(self, continue_callback):
def on_back():
ui_state.params.put_bool("RecordFront", False)
continue_callback()
def on_continue():
ui_state.params.put_bool("RecordFront", True)
continue_callback()
super().__init__(on_continue, back_callback=on_back, back_text="no", continue_text="yes")
self._title_header = TermsHeader("improve driver monitoring", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
self._dm_label = UnifiedLabel("Do you want to upload driver camera data?", 42,
FontWeight.ROMAN)
def show_event(self):
super().show_event()
# Disable driver monitoring model after last step
ui_state.params.put_bool("IsDriverViewEnabled", False)
@property
def _content_height(self):
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
def _render_content(self, scroll_offset):
self._title_header.render(rl.Rectangle(
self._rect.x + 16,
self._rect.y + 16 + scroll_offset,
self._title_header.rect.width,
self._title_header.rect.height,
))
self._dm_label.render(rl.Rectangle(
self._rect.x + 16,
self._title_header.rect.y + self._title_header.rect.height + 16,
self._rect.width - 32,
self._dm_label.get_content_height(int(self._rect.width - 32)),
))
class TrainingGuideAttentionNotice(SetupTermsPage):
def __init__(self, continue_callback):
super().__init__(continue_callback, continue_text="continue")
self._title_header = TermsHeader("driver assistance", gui_app.texture("icons_mici/setup/warning.png", 60, 60))
self._warning_label = UnifiedLabel("1. openpilot is a driver assistance system.\n\n" +
"2. You must pay attention at all times.\n\n" +
"3. You must be ready to take over at any time.\n\n" +
"4. You are fully responsible for driving the car.", 42,
FontWeight.ROMAN)
@property
def _content_height(self):
return self._warning_label.rect.y + self._warning_label.rect.height - self._scroll_panel.get_offset()
def _render_content(self, scroll_offset):
self._title_header.render(rl.Rectangle(
self._rect.x + 16,
self._rect.y + 16 + scroll_offset,
self._title_header.rect.width,
self._title_header.rect.height,
))
self._warning_label.render(rl.Rectangle(
self._rect.x + 16,
self._title_header.rect.y + self._title_header.rect.height + 16,
self._rect.width - 32,
self._warning_label.get_content_height(int(self._rect.width - 32)),
))
class TrainingGuide(Widget):
def __init__(self, completed_callback=None):
class TrainingGuideRecordFront(NavScroller):
def __init__(self, continue_callback: Callable[[], None]):
super().__init__()
self._completed_callback = completed_callback
self._step = 0
self_ref = weakref.ref(self)
def show_accept_dialog():
def on_accept():
ui_state.params.put_bool_nonblocking("RecordFront", True)
continue_callback()
def on_continue():
if obj := self_ref():
obj._advance_step()
gui_app.push_widget(BigConfirmationDialogV2("allow data uploading", "icons_mici/setup/driver_monitoring/dm_check.png", exit_on_confirm=False,
confirm_callback=on_accept))
def show_decline_dialog():
def on_decline():
ui_state.params.put_bool_nonblocking("RecordFront", False)
continue_callback()
gui_app.push_widget(BigConfirmationDialogV2("no, don't upload", "icons_mici/setup/cancel.png", exit_on_confirm=False, confirm_callback=on_decline))
self._accept_button = BigCircleButton("icons_mici/setup/driver_monitoring/dm_check.png")
self._accept_button.set_click_callback(show_accept_dialog)
self._decline_button = BigCircleButton("icons_mici/setup/cancel.png")
self._decline_button.set_click_callback(show_decline_dialog)
self._scroller.add_widgets([
GreyBigButton("driver camera data", "do you want to share video data for training?",
gui_app.texture("icons_mici/setup/green_dm.png", 64, 64)),
GreyBigButton("", "Sharing your data with comma helps improve openpilot for everyone."),
self._accept_button,
self._decline_button,
])
class TrainingGuideAttentionNotice(Scroller):
def __init__(self, continue_callback: Callable[[], None]):
super().__init__()
continue_button = BigPillButton("next")
continue_button.set_click_callback(continue_callback)
self._scroller.add_widgets([
GreyBigButton("what is openpilot?", "scroll to continue",
gui_app.texture("icons_mici/setup/green_info.png", 64, 64)),
GreyBigButton("", "1. openpilot is a driver assistance system."),
GreyBigButton("", "2. You must pay attention at all times."),
GreyBigButton("", "3. You must be ready to take over at any time."),
GreyBigButton("", "4. You are fully responsible for driving the car."),
continue_button,
])
class TrainingGuide(NavWidget):
def __init__(self, completed_callback: Callable[[], None]):
super().__init__()
self._steps = [
TrainingGuideAttentionNotice(continue_callback=on_continue),
TrainingGuidePreDMTutorial(continue_callback=on_continue),
TrainingGuideDMTutorial(continue_callback=on_continue),
TrainingGuideRecordFront(continue_callback=on_continue),
TrainingGuideAttentionNotice(continue_callback=lambda: gui_app.push_widget(self._steps[1])),
TrainingGuidePreDMTutorial(continue_callback=lambda: gui_app.push_widget(self._steps[2])),
TrainingGuideDMTutorial(continue_callback=lambda: gui_app.push_widget(self._steps[3])),
TrainingGuideRecordFront(continue_callback=completed_callback),
]
self._steps[0].set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack
def show_event(self):
super().show_event()
device.set_override_interactive_timeout(300)
self._steps[0].show_event()
def hide_event(self):
super().hide_event()
device.set_override_interactive_timeout(None)
def _render(self, _):
self._steps[0].render(self._rect)
def _advance_step(self):
if self._step < len(self._steps) - 1:
self._step += 1
self._steps[self._step].show_event()
else:
self._step = 0
if self._completed_callback:
self._completed_callback()
class QRCodeWidget(Widget):
def __init__(self, url: str, size: int = 170):
super().__init__()
self.set_rect(rl.Rectangle(0, 0, size, size))
self._size = size
self._qr_texture: rl.Texture | None = None
self._generate_qr(url)
def _generate_qr(self, url: str):
qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=0)
qr.add_data(url)
qr.make(fit=True)
pil_img = qr.make_image(fill_color="white", back_color="black").convert('RGBA')
img_array = np.array(pil_img, dtype=np.uint8)
rl_image = rl.Image()
rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data)
rl_image.width = pil_img.width
rl_image.height = pil_img.height
rl_image.mipmaps = 1
rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
self._qr_texture = rl.load_texture_from_image(rl_image)
def _render(self, _):
if self._qr_texture:
scale = self._size / self._qr_texture.height
rl.draw_texture_ex(self._qr_texture, rl.Vector2(self._rect.x, self._rect.y), 0.0, scale, rl.WHITE)
def __del__(self):
if self._qr_texture and self._qr_texture.id != 0:
rl.unload_texture(self._qr_texture)
class TermsPage(Scroller):
def __init__(self, on_accept, on_decline):
super().__init__()
def show_accept_dialog():
gui_app.push_widget(BigConfirmationDialogV2("accept\nterms", "icons_mici/setup/driver_monitoring/dm_check.png",
confirm_callback=on_accept))
def show_decline_dialog():
gui_app.push_widget(BigConfirmationDialogV2("decline &\nuninstall", "icons_mici/setup/cancel.png",
red=True, exit_on_confirm=False, confirm_callback=on_decline))
self._accept_button = BigCircleButton("icons_mici/setup/driver_monitoring/dm_check.png")
self._accept_button.set_click_callback(show_accept_dialog)
self._decline_button = BigCircleButton("icons_mici/setup/cancel.png", red=True)
self._decline_button.set_click_callback(show_decline_dialog)
self._scroller.add_widgets([
GreyBigButton("terms and\nconditions", "scroll to continue",
gui_app.texture("icons_mici/setup/green_info.png", 64, 64)),
GreyBigButton("swipe for QR code", "or go to https://comma.ai/terms",
gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 64, 56, flip_x=True)),
QRCodeWidget("https://comma.ai/terms"),
GreyBigButton("", "You must accept the Terms & Conditions to use openpilot."),
self._accept_button,
self._decline_button,
])
def _render(self, _):
rl.draw_rectangle_rec(self._rect, rl.BLACK)
if self._step < len(self._steps):
self._steps[self._step].render(self._rect)
class DeclinePage(Widget):
def __init__(self, back_callback=None):
super().__init__()
self._uninstall_slider = SmallSlider("uninstall openpilot", self._on_uninstall)
self._back_button = SmallButton("back")
self._back_button.set_click_callback(back_callback)
self._warning_header = TermsHeader("you must accept the\nterms to use openpilot",
gui_app.texture("icons_mici/setup/red_warning.png", 66, 60))
def _on_uninstall(self):
ui_state.params.put_bool("DoUninstall", True)
gui_app.request_close()
def _render(self, _):
self._warning_header.render(rl.Rectangle(
self._rect.x + 16,
self._rect.y + 16,
self._warning_header.rect.width,
self._warning_header.rect.height,
))
self._back_button.set_opacity(1 - self._uninstall_slider.slider_percentage)
self._back_button.render(rl.Rectangle(
self._rect.x + 8,
self._rect.y + self._rect.height - self._back_button.rect.height,
self._back_button.rect.width,
self._back_button.rect.height,
))
self._uninstall_slider.render(rl.Rectangle(
self._rect.x + self._rect.width - self._uninstall_slider.rect.width,
self._rect.y + self._rect.height - self._uninstall_slider.rect.height,
self._uninstall_slider.rect.width,
self._uninstall_slider.rect.height,
))
class TermsPage(SetupTermsPage):
def __init__(self, on_accept=None, on_decline=None):
super().__init__(on_accept, on_decline, "decline")
info_txt = gui_app.texture("icons_mici/setup/green_info.png", 60, 60)
self._title_header = TermsHeader("terms & conditions", info_txt)
self._terms_label = UnifiedLabel("You must accept the Terms and Conditions to use openpilot. " +
"Read the latest terms at https://comma.ai/terms before continuing.", 36,
FontWeight.ROMAN)
@property
def _content_height(self):
return self._terms_label.rect.y + self._terms_label.rect.height - self._scroll_panel.get_offset()
def _render_content(self, scroll_offset):
self._title_header.set_position(self._rect.x + 16, self._rect.y + 12 + scroll_offset)
self._title_header.render()
self._terms_label.render(rl.Rectangle(
self._rect.x + 16,
self._title_header.rect.y + self._title_header.rect.height + self.ITEM_SPACING,
self._rect.width - 100,
self._terms_label.get_content_height(int(self._rect.width - 100)),
))
super()._render(_)
class OnboardingWindow(Widget):
def __init__(self):
def __init__(self, completed_callback: Callable[[], None]):
super().__init__()
self._completed_callback = completed_callback
self._accepted_terms: bool = ui_state.params.get("HasAcceptedTerms") == terms_version
self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == training_version
self._state = OnboardingState.TERMS if not self._accepted_terms else OnboardingState.ONBOARDING
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
# Windows
self._terms = TermsPage(on_accept=self._on_terms_accepted, on_decline=self._on_terms_declined)
self._terms = TermsPage(on_accept=self._on_terms_accepted, on_decline=self._on_uninstall)
self._terms.set_enabled(lambda: self.enabled) # for nav stack
self._training_guide = TrainingGuide(completed_callback=self._on_completed_training)
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
self._training_guide.set_enabled(lambda: self.enabled) # for nav stack
def _on_uninstall(self):
ui_state.params.put_bool("DoUninstall", True)
def show_event(self):
super().show_event()
device.set_override_interactive_timeout(300)
device.set_offroad_brightness(100)
def hide_event(self):
super().hide_event()
# FIXME: when nav stack sends hide event to widget 2 below on push, this needs to be moved
device.set_override_interactive_timeout(None)
device.set_offroad_brightness(None)
@property
def completed(self) -> bool:
return self._accepted_terms and self._training_done
def _on_terms_declined(self):
self._state = OnboardingState.DECLINE
def _on_decline_back(self):
self._state = OnboardingState.TERMS
def close(self):
ui_state.params.put_bool("IsDriverViewEnabled", False)
gui_app.pop_widget()
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", False)
self._completed_callback()
def _on_terms_accepted(self):
ui_state.params.put("HasAcceptedTerms", terms_version)
self._state = OnboardingState.ONBOARDING
gui_app.push_widget(self._training_guide)
def _on_completed_training(self):
ui_state.params.put("CompletedTrainingVersion", training_version)
@@ -484,9 +400,4 @@ class OnboardingWindow(Widget):
def _render(self, _):
rl.draw_rectangle_rec(self._rect, rl.BLACK)
if self._state == OnboardingState.TERMS:
self._terms.render(self._rect)
elif self._state == OnboardingState.ONBOARDING:
self._training_guide.render(self._rect)
elif self._state == OnboardingState.DECLINE:
self._decline_page.render(self._rect)
self._terms.render(self._rect)

View File

@@ -13,15 +13,39 @@ from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmatio
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage
from openpilot.system.ui.mici_setup import BigPillButton
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.system.ui.widgets.label import MiciLabel
from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
class ReviewTermsPage(TermsPage, NavScroller):
"""TermsPage with NavWidget swipe-to-dismiss for reviewing in device settings."""
def __init__(self):
super().__init__(on_accept=self.dismiss, on_decline=self.dismiss)
self._accept_button.set_visible(False)
self._decline_button.set_visible(False)
close_button = BigPillButton("close")
close_button.set_click_callback(self.dismiss)
self._scroller.add_widget(close_button)
class ReviewTrainingGuide(TrainingGuide):
def show_event(self):
super().show_event()
device.set_override_interactive_timeout(300)
def hide_event(self):
super().hide_event()
device.set_override_interactive_timeout(None)
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", False)
class MiciFccModal(NavRawScrollPanel):
def __init__(self, file_path: str | None = None, text: str | None = None):
super().__init__()
@@ -311,11 +335,11 @@ class DeviceLayoutMici(NavScroller):
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(lambda: gui_app.push_widget(TrainingGuide(completed_callback=gui_app.pop_widget)))
review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTrainingGuide(completed_callback=lambda: gui_app.pop_widgets_to(self))))
review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad())
terms_btn = BigButton("terms &\nconditions", "", "icons_mici/settings/device/info.png")
terms_btn.set_click_callback(lambda: gui_app.push_widget(TermsPage(on_accept=gui_app.pop_widget)))
terms_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTermsPage()))
terms_btn.set_enabled(lambda: ui_state.is_offroad())
self._scroller.add_widgets([

View File

@@ -68,7 +68,9 @@ def test_dialogs_do_not_leak():
for ctor in (
# mici
MiciDriverCameraDialog, MiciTrainingGuide, MiciOnboardingWindow, MiciPairingDialog,
MiciDriverCameraDialog, MiciPairingDialog,
lambda: MiciTrainingGuide(lambda: None),
lambda: MiciOnboardingWindow(lambda: None),
lambda: BigDialog("test", "test"),
lambda: BigConfirmationDialogV2("test", "icons_mici/settings/network/new/trash.png"),
lambda: BigInputDialog("test"),

View File

@@ -120,6 +120,7 @@ class BigButton(Widget):
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
self._click_delay = 0.075
self._shake_start: float | None = None
self._grow_animation_until: float | None = None
self._rotate_icon_t: float | None = None
@@ -145,6 +146,9 @@ class BigButton(Widget):
self._txt_pressed_bg = gui_app.texture("icons_mici/buttons/button_rectangle_pressed.png", 402, 180)
self._txt_disabled_bg = gui_app.texture("icons_mici/buttons/button_rectangle_disabled.png", 402, 180)
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
super().set_touch_valid_callback(lambda: touch_callback() and self._grow_animation_until is None)
def _width_hint(self) -> int:
# Single line if scrolling, so hide behind icon if exists
icon_size = self._icon_size[0] if self._txt_icon and self._scroll and self.value else 0
@@ -182,6 +186,9 @@ class BigButton(Widget):
def trigger_shake(self):
self._shake_start = rl.get_time()
def trigger_grow_animation(self, duration: float = 0.65):
self._grow_animation_until = rl.get_time() + duration
@property
def _shake_offset(self) -> float:
SHAKE_DURATION = 0.5
@@ -197,6 +204,10 @@ class BigButton(Widget):
super().set_position(x + self._shake_offset, y)
def _handle_background(self) -> tuple[rl.Texture, float, float, float]:
if self._grow_animation_until is not None:
if rl.get_time() >= self._grow_animation_until:
self._grow_animation_until = None
# draw _txt_default_bg
txt_bg = self._txt_default_bg
if not self.enabled:
@@ -204,7 +215,7 @@ class BigButton(Widget):
elif self.is_pressed:
txt_bg = self._txt_pressed_bg
scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed else 1.0)
scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed or self._grow_animation_until is not None else 1.0)
btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2
btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
return txt_bg, btn_x, btn_y, scale

View File

@@ -431,6 +431,9 @@ class GuiApplication:
return self._nav_stack[-1]
return None
def widget_in_stack(self, widget: object) -> bool:
return widget in self._nav_stack
def add_nav_stack_tick(self, tick_function: Callable[[], None]):
if tick_function not in self._nav_stack_ticks:
self._nav_stack_ticks.append(tick_function)

View File

@@ -1,5 +1,4 @@
#!/usr/bin/env python3
from abc import abstractmethod
import os
import re
import threading
@@ -14,21 +13,21 @@ import pyray as rl
from cereal import log
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.system.hardware import HARDWARE, TICI
from openpilot.common.realtime import config_realtime_process, set_core_affinity
from openpilot.common.swaglog import cloudlog
from openpilot.common.utils import run_cmd
from openpilot.system.hardware import HARDWARE, TICI
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.wifi_manager import WifiManager
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.system.ui.widgets.button import (IconButton, SmallButton, WideRoundedButton, SmallerRoundedButton,
SmallCircleIconButton, WidishRoundedButton, FullRoundedButton)
from openpilot.system.ui.widgets.button import SmallButton
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.scroller import Scroller, NavScroller, ITEM_SPACING
from openpilot.system.ui.widgets.slider import LargerSlider, SmallSlider
from openpilot.selfdrive.ui.mici.layouts.settings.network import WifiUIMici
from openpilot.selfdrive.ui.mici.layouts.settings.network import WifiNetworkButton, WifiUIMici
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
NetworkType = log.DeviceState.NetworkType
@@ -122,9 +121,9 @@ class SoftwareSelectionPage(NavWidget):
use_custom_software_callback: Callable):
super().__init__()
self._openpilot_slider = LargerSlider("slide to use\nopenpilot", use_openpilot_callback)
self._openpilot_slider = LargerSlider("slide to install\nopenpilot", use_openpilot_callback)
self._openpilot_slider.set_enabled(lambda: self.enabled and not self.is_dismissing)
self._custom_software_slider = LargerSlider("slide to use\ncustom software", use_custom_software_callback, green=False)
self._custom_software_slider = LargerSlider("slide to install\nother software", use_custom_software_callback, green=False)
self._custom_software_slider.set_enabled(lambda: self.enabled and not self.is_dismissing)
def show_event(self):
@@ -161,199 +160,24 @@ class SoftwareSelectionPage(NavWidget):
self._custom_software_slider.render(custom_software_rect)
class TermsHeader(Widget):
def __init__(self, text: str, icon_texture: rl.Texture):
super().__init__()
self._title = UnifiedLabel(text, 36, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
font_weight=FontWeight.BOLD, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
line_height=0.8)
self._icon_texture = icon_texture
self.set_rect(rl.Rectangle(0, 0, gui_app.width - 16 * 2, self._icon_texture.height))
def set_title(self, text: str):
self._title.set_text(text)
def set_icon(self, icon_texture: rl.Texture):
self._icon_texture = icon_texture
def _render(self, _):
rl.draw_texture_ex(self._icon_texture, rl.Vector2(self._rect.x, self._rect.y),
0.0, 1.0, rl.WHITE)
# May expand outside parent rect
title_content_height = self._title.get_content_height(int(self._rect.width - self._icon_texture.width - 16))
title_rect = rl.Rectangle(
self._rect.x + self._icon_texture.width + 16,
self._rect.y + (self._rect.height - title_content_height) / 2,
self._rect.width - self._icon_texture.width - 16,
title_content_height,
)
self._title.render(title_rect)
class TermsPage(Widget):
ITEM_SPACING = 20
def __init__(self, continue_callback: Callable, back_callback: Callable | None = None,
back_text: str = "back", continue_text: str = "accept"):
super().__init__()
# TODO: use Scroller
self._scroll_panel = GuiScrollPanel2(horizontal=False)
self._continue_text = continue_text
self._continue_slider: bool = continue_text in ("reboot", "power off")
self._continue_button: WideRoundedButton | FullRoundedButton | SmallSlider
if self._continue_slider:
self._continue_button = SmallSlider(continue_text, confirm_callback=continue_callback)
self._scroll_panel.set_enabled(lambda: not self._continue_button.is_pressed)
elif back_callback is not None:
self._continue_button = WideRoundedButton(continue_text)
else:
self._continue_button = FullRoundedButton(continue_text)
self._continue_button.set_enabled(False)
self._continue_button.set_opacity(0.0)
self._continue_button.set_touch_valid_callback(self._scroll_panel.is_touch_valid)
if not self._continue_slider:
self._continue_button.set_click_callback(continue_callback)
self._enable_back = back_callback is not None
self._back_button = SmallButton(back_text)
self._back_button.set_opacity(0.0)
self._back_button.set_touch_valid_callback(self._scroll_panel.is_touch_valid)
self._back_button.set_click_callback(back_callback)
self._scroll_down_indicator = IconButton(gui_app.texture("icons_mici/setup/scroll_down_indicator.png", 64, 78))
self._scroll_down_indicator.set_enabled(False)
def reset(self):
self._scroll_panel.set_offset(0)
self._continue_button.set_enabled(False)
self._continue_button.set_opacity(0.0)
self._back_button.set_enabled(False)
self._back_button.set_opacity(0.0)
self._scroll_down_indicator.set_opacity(1.0)
def show_event(self):
super().show_event()
self.reset()
@property
@abstractmethod
def _content_height(self):
pass
@property
def _scrolled_down_offset(self):
return -self._content_height + (self._continue_button.rect.height + 16 + 30)
@abstractmethod
def _render_content(self, scroll_offset):
pass
def _render(self, _):
rl.draw_rectangle_rec(self._rect, rl.BLACK)
scroll_offset = round(self._scroll_panel.update(self._rect, self._content_height + self._continue_button.rect.height + 16))
if scroll_offset <= self._scrolled_down_offset:
# don't show back if not enabled
if self._enable_back:
self._back_button.set_enabled(True)
self._back_button.set_opacity(1.0, smooth=True)
self._continue_button.set_enabled(True)
self._continue_button.set_opacity(1.0, smooth=True)
self._scroll_down_indicator.set_opacity(0.0, smooth=True)
else:
self._back_button.set_enabled(False)
self._back_button.set_opacity(0.0, smooth=True)
self._continue_button.set_enabled(False)
self._continue_button.set_opacity(0.0, smooth=True)
self._scroll_down_indicator.set_opacity(1.0, smooth=True)
# Render content
self._render_content(scroll_offset)
# black gradient at top and bottom for scrolling content
rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y),
int(self._rect.width), 20, rl.BLACK, rl.BLANK)
rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 20),
int(self._rect.width), 20, rl.BLANK, rl.BLACK)
# fade out back button as slider is moved
if self._continue_slider and scroll_offset <= self._scrolled_down_offset:
self._back_button.set_opacity(1.0 - self._continue_button.slider_percentage)
self._back_button.set_visible(self._continue_button.slider_percentage < 0.99)
self._back_button.render(rl.Rectangle(
self._rect.x + 8,
self._rect.y + self._rect.height - self._back_button.rect.height,
self._back_button.rect.width,
self._back_button.rect.height,
))
continue_x = self._rect.x + 8
if self._enable_back:
continue_x = self._rect.x + self._rect.width - self._continue_button.rect.width - 8
if self._continue_slider:
continue_x += 8
self._continue_button.render(rl.Rectangle(
continue_x,
self._rect.y + self._rect.height - self._continue_button.rect.height,
self._continue_button.rect.width,
self._continue_button.rect.height,
))
self._scroll_down_indicator.render(rl.Rectangle(
self._rect.x + self._rect.width - self._scroll_down_indicator.rect.width - 8,
self._rect.y + self._rect.height - self._scroll_down_indicator.rect.height - 8,
self._scroll_down_indicator.rect.width,
self._scroll_down_indicator.rect.height,
))
class CustomSoftwareWarningPage(TermsPage):
class CustomSoftwareWarningPage(NavScroller):
def __init__(self, continue_callback: Callable, back_callback: Callable):
super().__init__(continue_callback, back_callback)
super().__init__()
self.set_back_callback(back_callback)
self._title_header = TermsHeader("use caution installing\n3rd party software",
gui_app.texture("icons_mici/setup/warning.png", 66, 60))
self._body = UnifiedLabel("• It has not been tested by comma.\n" +
"• It may not comply with relevant safety standards.\n" +
"• It may cause damage to your device and/or vehicle.\n", 36, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
font_weight=FontWeight.ROMAN)
self._continue_button = BigPillButton("next")
self._continue_button.set_click_callback(continue_callback)
self._restore_header = TermsHeader("how to backup &\nrestore", gui_app.texture("icons_mici/setup/restore.png", 60, 60))
self._restore_body = UnifiedLabel("To restore your device to a factory state later, use https://flash.comma.ai",
36, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
font_weight=FontWeight.ROMAN)
@property
def _content_height(self):
return self._restore_body.rect.y + self._restore_body.rect.height - self._scroll_panel.get_offset()
def _render_content(self, scroll_offset):
self._title_header.set_position(self._rect.x + 16, self._rect.y + 8 + scroll_offset)
self._title_header.render()
body_rect = rl.Rectangle(
self._rect.x + 8,
self._title_header.rect.y + self._title_header.rect.height + self.ITEM_SPACING,
self._rect.width - 50,
self._body.get_content_height(int(self._rect.width - 50)),
)
self._body.render(body_rect)
self._restore_header.set_position(self._rect.x + 16, self._body.rect.y + self._body.rect.height + self.ITEM_SPACING)
self._restore_header.render()
self._restore_body.render(rl.Rectangle(
self._rect.x + 8,
self._restore_header.rect.y + self._restore_header.rect.height + self.ITEM_SPACING,
self._rect.width - 50,
self._restore_body.get_content_height(int(self._rect.width - 50)),
))
self._scroller.add_widgets([
GreyBigButton("use caution", "when installing\n3rd party software",
gui_app.texture("icons_mici/setup/warning.png", 64, 58)),
GreyBigButton("", "• It has not been tested by comma"),
GreyBigButton("", "• It may not comply with relevant safety standards."),
GreyBigButton("", "• It may cause damage to your device and/or vehicle."),
GreyBigButton("how to restore to a\nfactory state later", "https://flash.comma.ai",
gui_app.texture("icons_mici/setup/restore.png", 64, 64)),
self._continue_button,
])
class DownloadingPage(Widget):
@@ -391,11 +215,9 @@ class DownloadingPage(Widget):
))
class FailedPage(NavWidget):
class FailedPageBase(Widget):
def __init__(self, reboot_callback: Callable, retry_callback: Callable, title: str = "download failed"):
super().__init__()
self.set_back_callback(retry_callback)
self._title_label = UnifiedLabel(title, 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
font_weight=FontWeight.DISPLAY)
self._reason_label = UnifiedLabel("", 36, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
@@ -446,11 +268,86 @@ class FailedPage(NavWidget):
))
class NetworkSetupPage(NavWidget):
class FailedPage(FailedPageBase, NavWidget):
def __init__(self, reboot_callback: Callable, retry_callback: Callable, title: str = "download failed"):
super().__init__(reboot_callback, retry_callback, title)
self.set_back_callback(retry_callback)
class GreyBigButton(BigButton):
"""Users should manage newlines with this class themselves"""
LABEL_HORIZONTAL_PADDING = 30
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_touch_valid_callback(lambda: False)
self._rect.width = 476
self._label.set_font_size(36)
self._label.set_font_weight(FontWeight.BOLD)
self._label.set_line_height(1.0)
self._sub_label.set_font_size(36)
self._sub_label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9)))
self._sub_label.set_font_weight(FontWeight.DISPLAY_REGULAR)
self._sub_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE if not self._label.text else
rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
self._sub_label.set_line_height(0.95)
@property
def LABEL_VERTICAL_PADDING(self):
return BigButton.LABEL_VERTICAL_PADDING if self._label.text else 18
def _width_hint(self) -> int:
return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2)
def _render(self, _):
rl.draw_rectangle_rounded(self._rect, 0.4, 10, rl.Color(255, 255, 255, int(255 * 0.15)))
self._draw_content(self._rect.y)
class BigPillButton(BigButton):
def __init__(self, *args, green: bool = False, disabled_background: bool = False, **kwargs):
self._green = green
self._disabled_background = disabled_background
super().__init__(*args, **kwargs)
self._label.set_font_size(48)
self._label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
def _load_images(self):
if self._green:
self._txt_default_bg = gui_app.texture("icons_mici/setup/start_button.png", 402, 180)
self._txt_pressed_bg = gui_app.texture("icons_mici/setup/start_button_pressed.png", 402, 180)
else:
self._txt_default_bg = gui_app.texture("icons_mici/setup/continue.png", 402, 180)
self._txt_pressed_bg = gui_app.texture("icons_mici/setup/continue_pressed.png", 402, 180)
self._txt_disabled_bg = gui_app.texture("icons_mici/setup/continue_disabled.png", 402, 180)
def set_green(self, green: bool):
if self._green != green:
self._green = green
self._load_images()
def _update_label_layout(self):
# Don't change label text size
pass
def _handle_background(self) -> tuple[rl.Texture, float, float, float]:
txt_bg, btn_x, btn_y, scale = super()._handle_background()
if self._disabled_background:
txt_bg = self._txt_disabled_bg
return txt_bg, btn_x, btn_y, scale
class NetworkSetupPageBase(Scroller):
def __init__(self, network_monitor: NetworkConnectivityMonitor, continue_callback: Callable[[bool], None],
back_callback: Callable[[], None] | None):
disable_connect_hint: bool = False):
super().__init__()
self.set_back_callback(back_callback)
self._wifi_manager = WifiManager()
self._wifi_manager.set_active(True)
@@ -459,83 +356,106 @@ class NetworkSetupPage(NavWidget):
self._prev_has_internet = False
self._wifi_ui = WifiUIMici(self._wifi_manager)
self._no_wifi_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 58, 50)
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 58, 50)
self._waiting_text = "waiting for internet..."
self._network_header = TermsHeader(self._waiting_text, self._no_wifi_txt)
self._connect_button = GreyBigButton("connect to\ninternet", "swipe down to go back",
gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 64, 56, flip_x=True))
self._connect_button.set_visible(not disable_connect_hint)
back_txt = gui_app.texture("icons_mici/setup/back_new.png", 37, 32)
self._back_button = SmallCircleIconButton(back_txt)
self._back_button.set_click_callback(back_callback)
self._back_button.set_enabled(lambda: self.enabled) # for nav stack
self._wifi_button = SmallerRoundedButton("wifi")
self._wifi_button = WifiNetworkButton(self._wifi_manager)
self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui))
self._wifi_button.set_enabled(lambda: self.enabled)
self._continue_button = WidishRoundedButton("continue")
self._continue_button.set_enabled(False)
self._show_time = 0.0
self._pending_has_internet_scroll = False
self._pending_continue_grow_animation = False
self._pending_wifi_grow_animation = False
def on_waiting_click():
offset = (self._wifi_button.rect.x + self._wifi_button.rect.width / 2) - (self._rect.x + self._rect.width / 2)
self._scroller.scroll_to(offset, smooth=True, block_interaction=True)
# trigger grow when wifi button in view
self._pending_wifi_grow_animation = True
self._waiting_button = BigPillButton("waiting for\ninternet...", disabled_background=True)
self._waiting_button.set_click_callback(on_waiting_click)
self._continue_button = BigPillButton("install openpilot", green=True)
self._continue_button.set_click_callback(lambda: continue_callback(self._custom_software))
self._scroller.add_widgets([
self._connect_button,
self._wifi_button,
self._continue_button,
self._waiting_button,
])
gui_app.add_nav_stack_tick(self._nav_stack_tick)
def show_event(self):
super().show_event()
self._show_time = rl.get_time()
self._prev_has_internet = False
self._network_monitor.reset()
self._set_has_internet(False)
self._pending_has_internet_scroll = False
self._pending_continue_grow_animation = False
self._pending_wifi_grow_animation = False
def _nav_stack_tick(self):
self._wifi_manager.process_callbacks()
has_internet = self._network_monitor.network_connected.is_set()
if has_internet != self._prev_has_internet:
self._set_has_internet(has_internet)
if has_internet:
gui_app.pop_widgets_to(self)
self._prev_has_internet = has_internet
if has_internet and not self._prev_has_internet:
self._pending_has_internet_scroll = True
self._prev_has_internet = has_internet
def _set_has_internet(self, has_internet: bool):
if has_internet:
self._network_header.set_title("connected to internet")
self._network_header.set_icon(self._wifi_full_txt)
self._continue_button.set_enabled(lambda: self.enabled)
else:
self._network_header.set_title(self._waiting_text)
self._network_header.set_icon(self._no_wifi_txt)
self._continue_button.set_enabled(False)
if self._pending_has_internet_scroll:
# Scrolls over to continue button, then grows once in view
elapsed = rl.get_time() - self._show_time
if elapsed > 0.5:
self._pending_has_internet_scroll = False
def scroll_to_download():
self._scroller._layout()
end_offset = -(self._scroller.content_size - self._rect.width)
remaining = self._scroller.scroll_panel.get_offset() - end_offset
self._scroller.scroll_to(remaining, smooth=True, block_interaction=True)
self._pending_continue_grow_animation = True
# Animate WifiUi down first before scroll
gui_app.pop_widgets_to(self, scroll_to_download)
def set_custom_software(self, custom_software: bool):
self._custom_software = custom_software
self._continue_button.set_text("install openpilot" if not custom_software else "choose software")
self._continue_button.set_green(not custom_software)
def _render(self, _):
self._network_header.render(rl.Rectangle(
self._rect.x + 16,
self._rect.y + 16,
self._rect.width - 32,
self._network_header.rect.height,
))
def set_is_updater(self):
self._continue_button.set_text("download\n& install")
self._continue_button.set_green(False)
self._back_button.render(rl.Rectangle(
self._rect.x + 8,
self._rect.y + self._rect.height - self._back_button.rect.height,
self._back_button.rect.width,
self._back_button.rect.height,
))
def _update_state(self):
super()._update_state()
self._wifi_button.render(rl.Rectangle(
self._rect.x + 8 + self._back_button.rect.width + 10,
self._rect.y + self._rect.height - self._wifi_button.rect.height,
self._wifi_button.rect.width,
self._wifi_button.rect.height,
))
if self._pending_continue_grow_animation:
btn_right = self._continue_button.rect.x + self._continue_button.rect.width
visible_right = self._rect.x + self._rect.width
if btn_right < visible_right + 50:
self._pending_continue_grow_animation = False
self._continue_button.trigger_grow_animation()
self._continue_button.render(rl.Rectangle(
self._rect.x + self._rect.width - self._continue_button.rect.width - 8,
self._rect.y + self._rect.height - self._continue_button.rect.height,
self._continue_button.rect.width,
self._continue_button.rect.height,
))
if self._pending_wifi_grow_animation and abs(self._wifi_button.rect.x - ITEM_SPACING) < 50:
self._pending_wifi_grow_animation = False
self._wifi_button.trigger_grow_animation()
if self._network_monitor.network_connected.is_set():
self._continue_button.set_visible(True)
self._waiting_button.set_visible(False)
else:
self._continue_button.set_visible(False)
self._waiting_button.set_visible(True)
class NetworkSetupPage(NetworkSetupPageBase, NavScroller):
def __init__(self, network_monitor: NetworkConnectivityMonitor, continue_callback: Callable[[bool], None],
back_callback: Callable[[], None] | None):
super().__init__(network_monitor, continue_callback)
self.set_back_callback(back_callback)
class Setup(Widget):
@@ -557,13 +477,13 @@ class Setup(Widget):
self._start_page.set_click_callback(getting_started_button_callback)
self._start_page.set_enabled(lambda: self.enabled) # for nav stack
self._network_setup_page = NetworkSetupPage(self._network_monitor, self._network_setup_continue_button_callback,
self._pop_to_software_selection)
self._network_setup_page = NetworkSetupPage(self._network_monitor, self._network_setup_continue_callback, self._pop_to_software_selection)
self._software_selection_page = SoftwareSelectionPage(self._use_openpilot, lambda: gui_app.push_widget(self._custom_software_warning_page))
self._download_failed_page = FailedPage(HARDWARE.reboot, self._pop_to_software_selection)
self._custom_software_warning_page = CustomSoftwareWarningPage(self._software_selection_custom_software_continue, self._pop_to_software_selection)
self._custom_software_warning_page = CustomSoftwareWarningPage(lambda: self._push_network_setup(True), self._pop_to_software_selection)
self._downloading_page = DownloadingPage()
@@ -602,17 +522,14 @@ class Setup(Widget):
time.sleep(0.1)
gui_app.request_close()
else:
self._push_network_setup(custom_software=False)
self._push_network_setup()
def _push_network_setup(self, custom_software: bool):
def _push_network_setup(self, custom_software: bool = False):
# to fire the correct continue callback later
self._network_setup_page.set_custom_software(custom_software)
gui_app.push_widget(self._network_setup_page)
gui_app.pop_widgets_to(self._software_selection_page, lambda: gui_app.push_widget(self._network_setup_page))
def _software_selection_custom_software_continue(self):
gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders
self._push_network_setup(custom_software=True)
def _network_setup_continue_button_callback(self, custom_software):
def _network_setup_continue_callback(self, custom_software: bool):
if not custom_software:
gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders
self._download(OPENPILOT_URL)

View File

@@ -7,11 +7,10 @@ from enum import IntEnum
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.wifi_manager import WifiManager
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.button import FullRoundedButton
from openpilot.system.ui.mici_setup import NetworkSetupPage, FailedPage, NetworkConnectivityMonitor
from openpilot.system.ui.mici_setup import NetworkSetupPageBase, FailedPageBase, NetworkConnectivityMonitor
class Screen(IntEnum):
@@ -32,16 +31,14 @@ class Updater(Widget):
self.progress_text = "loading"
self.process = None
self.update_thread = None
self._wifi_manager = WifiManager()
self._wifi_manager.set_active(True)
self._network_setup_page = NetworkSetupPage(self._wifi_manager, self._network_setup_continue_callback,
self._network_setup_back_callback)
self._network_setup_page.set_enabled(lambda: self.enabled) # for nav stack
self._network_monitor = NetworkConnectivityMonitor()
self._network_monitor.start()
self._network_setup_page = NetworkSetupPageBase(self._network_monitor, self._network_setup_continue_callback,
disable_connect_hint=True)
self._network_setup_page.set_is_updater()
self._network_setup_page.set_enabled(lambda: self.enabled) # for nav stack
# Buttons
self._continue_button = FullRoundedButton("continue")
self._continue_button.set_click_callback(lambda: self.set_current_screen(Screen.WIFI))
@@ -52,8 +49,8 @@ class Updater(Widget):
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
font_weight=FontWeight.ROMAN)
self._update_failed_page = FailedPage(HARDWARE.reboot, self._update_failed_retry_callback,
title="update failed")
self._update_failed_page = FailedPageBase(HARDWARE.reboot, self._update_failed_retry_callback,
title="update failed")
self._progress_title_label = UnifiedLabel("", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
font_weight=FontWeight.DISPLAY, line_height=0.8)
@@ -61,10 +58,7 @@ class Updater(Widget):
font_weight=FontWeight.ROMAN,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
def _network_setup_back_callback(self):
self.set_current_screen(Screen.PROMPT)
def _network_setup_continue_callback(self):
def _network_setup_continue_callback(self, _):
self.install_update()
def _update_failed_retry_callback(self):
@@ -160,14 +154,10 @@ class Updater(Widget):
rect.height,
))
def _update_state(self):
self._wifi_manager.process_callbacks()
def _render(self, rect: rl.Rectangle):
if self.current_screen == Screen.PROMPT:
self.render_prompt_screen(rect)
elif self.current_screen == Screen.WIFI:
self._network_setup_page.set_has_internet(self._network_monitor.network_connected.is_set())
self._network_setup_page.render(rect)
elif self.current_screen == Screen.PROGRESS:
self.render_progress_screen(rect)

View File

@@ -228,6 +228,7 @@ class SmallCircleIconButton(Widget):
class SmallButton(Widget):
def __init__(self, text: str):
super().__init__()
self._click_delay = 0.075
self._opacity_filter = FirstOrderFilter(1.0, 0.1, 1 / gui_app.target_fps)
self._load_assets()

View File

@@ -63,7 +63,7 @@ class NavWidget(Widget, abc.ABC):
self._playing_dismiss_animation = False # released and animating away
self._y_pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1)
self._back_callback: Callable[[], None] | None = None # persistent callback for any back navigation
self._back_callback: Callable[[], None] | None = None # persistent callback for user-initiated back navigation
self._dismiss_callback: Callable[[], None] | None = None # transient callback for programmatic dismiss
# TODO: move this state into NavBar
@@ -150,12 +150,12 @@ class NavWidget(Widget, abc.ABC):
if new_y > self._rect.height + DISMISS_PUSH_OFFSET - 10:
gui_app.pop_widget()
if self._back_callback is not None:
self._back_callback()
# Only one callback should ever be fired
if self._dismiss_callback is not None:
self._dismiss_callback()
self._dismiss_callback = None
elif self._back_callback is not None:
self._back_callback()
self._playing_dismiss_animation = False
self._drag_start_pos = None