mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-06-30 02:52:04 +08:00
raylib: updater UI (#36235)
* auto attempt * gpt5 * Revert "gpt5" This reverts commit 556d6d9ee4d53aca0f4612023db6cfb2bed7ce29. * clean up * fixes * use raylib * fixes * debug * test update * more * rm * add value to button like qt * bump * bump * fixes * bump * fix * bump * clean up * time ago like qt rm * bump * clean up * updated can fail to respond on boot leading to stuck state * fix color fix * bump * bump * add back * test update * no unknown just '' * ffix
This commit is contained in:
@@ -1,24 +1,69 @@
|
||||
from openpilot.common.params import Params
|
||||
import os
|
||||
import time
|
||||
import datetime
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.widgets.list_view import button_item, text_item
|
||||
from openpilot.system.ui.widgets.list_view import button_item, text_item, ListItem
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
|
||||
# TODO: remove this. updater fails to respond on startup if time is not correct
|
||||
UPDATED_TIMEOUT = 10 # seconds to wait for updated to respond
|
||||
|
||||
|
||||
def time_ago(date: datetime.datetime | None) -> str:
|
||||
if not date:
|
||||
return "never"
|
||||
|
||||
if not system_time_valid():
|
||||
return date.strftime("%a %b %d %Y")
|
||||
|
||||
now = datetime.datetime.now(datetime.UTC)
|
||||
if date.tzinfo is None:
|
||||
date = date.replace(tzinfo=datetime.UTC)
|
||||
|
||||
diff_seconds = int((now - date).total_seconds())
|
||||
if diff_seconds < 60:
|
||||
return "now"
|
||||
if diff_seconds < 3600:
|
||||
m = diff_seconds // 60
|
||||
return f"{m} minute{'s' if m != 1 else ''} ago"
|
||||
if diff_seconds < 86400:
|
||||
h = diff_seconds // 3600
|
||||
return f"{h} hour{'s' if h != 1 else ''} ago"
|
||||
if diff_seconds < 604800:
|
||||
d = diff_seconds // 86400
|
||||
return f"{d} day{'s' if d != 1 else ''} ago"
|
||||
return date.strftime("%a %b %d %Y")
|
||||
|
||||
|
||||
class SoftwareLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
self._onroad_label = ListItem(title="Updates are only downloaded while the car is off.")
|
||||
self._version_item = text_item("Current Version", ui_state.params.get("UpdaterCurrentDescription") or "")
|
||||
self._download_btn = button_item("Download", "CHECK", callback=self._on_download_update)
|
||||
|
||||
# Install button is initially hidden
|
||||
self._install_btn = button_item("Install Update", "INSTALL", callback=self._on_install_update)
|
||||
self._install_btn.set_visible(False)
|
||||
|
||||
# Track waiting-for-updater transition to avoid brief re-enable while still idle
|
||||
self._waiting_for_updater = False
|
||||
self._waiting_start_ts: float = 0.0
|
||||
|
||||
items = self._init_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _init_items(self):
|
||||
items = [
|
||||
text_item("Current Version", ""),
|
||||
button_item("Download", "CHECK", callback=self._on_download_update),
|
||||
button_item("Install Update", "INSTALL", callback=self._on_install_update),
|
||||
self._onroad_label,
|
||||
self._version_item,
|
||||
self._download_btn,
|
||||
self._install_btn,
|
||||
button_item("Target Branch", "SELECT", callback=self._on_select_branch),
|
||||
button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall),
|
||||
]
|
||||
@@ -27,14 +72,90 @@ class SoftwareLayout(Widget):
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _on_download_update(self): pass
|
||||
def _on_install_update(self): pass
|
||||
def _on_select_branch(self): pass
|
||||
def _update_state(self):
|
||||
# Show/hide onroad warning
|
||||
self._onroad_label.set_visible(ui_state.is_onroad())
|
||||
|
||||
# Update current version and release notes
|
||||
current_desc = ui_state.params.get("UpdaterCurrentDescription") or ""
|
||||
current_release_notes = (ui_state.params.get("UpdaterCurrentReleaseNotes") or b"").decode("utf-8", "replace")
|
||||
self._version_item.action_item.set_text(current_desc)
|
||||
self._version_item.description = current_release_notes
|
||||
|
||||
# Update download button visibility and state
|
||||
self._download_btn.set_visible(ui_state.is_offroad())
|
||||
|
||||
updater_state = ui_state.params.get("UpdaterState") or "idle"
|
||||
failed_count = ui_state.params.get("UpdateFailedCount") or 0
|
||||
fetch_available = ui_state.params.get_bool("UpdaterFetchAvailable")
|
||||
update_available = ui_state.params.get_bool("UpdateAvailable")
|
||||
|
||||
if updater_state != "idle":
|
||||
# Updater responded
|
||||
self._waiting_for_updater = False
|
||||
self._download_btn.action_item.set_enabled(False)
|
||||
self._download_btn.action_item.set_value(updater_state)
|
||||
else:
|
||||
if failed_count > 0:
|
||||
self._download_btn.action_item.set_value("failed to check for update")
|
||||
self._download_btn.action_item.set_text("CHECK")
|
||||
elif fetch_available:
|
||||
self._download_btn.action_item.set_value("update available")
|
||||
self._download_btn.action_item.set_text("DOWNLOAD")
|
||||
else:
|
||||
last_update = ui_state.params.get("LastUpdateTime")
|
||||
if last_update:
|
||||
formatted = time_ago(last_update)
|
||||
self._download_btn.action_item.set_value(f"up to date, last checked {formatted}")
|
||||
else:
|
||||
self._download_btn.action_item.set_value("up to date, last checked never")
|
||||
self._download_btn.action_item.set_text("CHECK")
|
||||
|
||||
# If we've been waiting too long without a state change, reset state
|
||||
if self._waiting_for_updater and (time.monotonic() - self._waiting_start_ts > UPDATED_TIMEOUT):
|
||||
self._waiting_for_updater = False
|
||||
|
||||
# Only enable if we're not waiting for updater to flip out of idle
|
||||
self._download_btn.action_item.set_enabled(not self._waiting_for_updater)
|
||||
|
||||
# Update install button
|
||||
self._install_btn.set_visible(ui_state.is_offroad() and update_available)
|
||||
if update_available:
|
||||
new_desc = ui_state.params.get("UpdaterNewDescription") or ""
|
||||
new_release_notes = (ui_state.params.get("UpdaterNewReleaseNotes") or b"").decode("utf-8", "replace")
|
||||
self._install_btn.action_item.set_text("INSTALL")
|
||||
self._install_btn.action_item.set_value(new_desc)
|
||||
self._install_btn.description = new_release_notes
|
||||
# Enable install button for testing (like Qt showEvent)
|
||||
self._install_btn.action_item.set_enabled(True)
|
||||
else:
|
||||
self._install_btn.set_visible(False)
|
||||
|
||||
def _on_download_update(self):
|
||||
# Check if we should start checking or start downloading
|
||||
self._download_btn.action_item.set_enabled(False)
|
||||
if self._download_btn.action_item.text == "CHECK":
|
||||
# Start checking for updates
|
||||
self._waiting_for_updater = True
|
||||
self._waiting_start_ts = time.monotonic()
|
||||
os.system("pkill -SIGUSR1 -f system.updated.updated")
|
||||
else:
|
||||
# Start downloading
|
||||
self._waiting_for_updater = True
|
||||
self._waiting_start_ts = time.monotonic()
|
||||
os.system("pkill -SIGHUP -f system.updated.updated")
|
||||
|
||||
def _on_uninstall(self):
|
||||
def handle_uninstall_confirmation(result):
|
||||
if result == DialogResult.CONFIRM:
|
||||
self._params.put_bool("DoUninstall", True)
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
|
||||
dialog = ConfirmDialog("Are you sure you want to uninstall?", "Uninstall")
|
||||
gui_app.set_modal_overlay(dialog, callback=handle_uninstall_confirmation)
|
||||
|
||||
def _on_install_update(self):
|
||||
# Trigger reboot to install update
|
||||
self._install_btn.action_item.set_enabled(False)
|
||||
ui_state.params.put_bool("DoReboot", True)
|
||||
|
||||
def _on_select_branch(self): pass
|
||||
|
||||
@@ -80,8 +80,8 @@ procs = [
|
||||
PythonProcess("dmonitoringmodeld", "selfdrive.modeld.dmonitoringmodeld", driverview, enabled=(WEBCAM or not PC)),
|
||||
|
||||
PythonProcess("sensord", "system.sensord.sensord", only_onroad, enabled=not PC),
|
||||
NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None)),
|
||||
PythonProcess("raylib_ui", "selfdrive.ui.ui", always_run, enabled=False, watchdog_max_dt=(5 if not PC else None)),
|
||||
NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, enabled=False, watchdog_max_dt=(5 if not PC else None)),
|
||||
PythonProcess("raylib_ui", "selfdrive.ui.ui", always_run, watchdog_max_dt=(5 if not PC else None)),
|
||||
PythonProcess("soundd", "selfdrive.ui.soundd", only_onroad),
|
||||
PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad),
|
||||
NativeProcess("_pandad", "selfdrive/pandad", ["./pandad"], always_run, enabled=False),
|
||||
|
||||
@@ -14,6 +14,7 @@ ITEM_BASE_HEIGHT = 170
|
||||
ITEM_PADDING = 20
|
||||
ITEM_TEXT_FONT_SIZE = 50
|
||||
ITEM_TEXT_COLOR = rl.WHITE
|
||||
ITEM_TEXT_VALUE_COLOR = rl.Color(170, 170, 170, 255)
|
||||
ITEM_DESC_TEXT_COLOR = rl.Color(128, 128, 128, 255)
|
||||
ITEM_DESC_FONT_SIZE = 40
|
||||
ITEM_DESC_V_OFFSET = 140
|
||||
@@ -77,7 +78,9 @@ class ButtonAction(ItemAction):
|
||||
def __init__(self, text: str | Callable[[], str], width: int = BUTTON_WIDTH, enabled: bool | Callable[[], bool] = True):
|
||||
super().__init__(width, enabled)
|
||||
self._text_source = text
|
||||
self._value_source: str | Callable[[], str] | None = None
|
||||
self._pressed = False
|
||||
self._font = gui_app.font(FontWeight.NORMAL)
|
||||
|
||||
def pressed():
|
||||
self._pressed = True
|
||||
@@ -96,16 +99,34 @@ class ButtonAction(ItemAction):
|
||||
super().set_touch_valid_callback(touch_callback)
|
||||
self._button.set_touch_valid_callback(touch_callback)
|
||||
|
||||
def set_text(self, text: str | Callable[[], str]):
|
||||
self._text_source = text
|
||||
|
||||
def set_value(self, value: str | Callable[[], str]):
|
||||
self._value_source = value
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return _resolve_value(self._text_source, "Error")
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return _resolve_value(self._value_source, "")
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> bool:
|
||||
self._button.set_text(self.text)
|
||||
self._button.set_enabled(_resolve_value(self.enabled))
|
||||
button_rect = rl.Rectangle(rect.x, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT)
|
||||
self._button.render(button_rect)
|
||||
|
||||
value_text = self.value
|
||||
if value_text:
|
||||
spacing = 20
|
||||
text_size = measure_text_cached(self._font, value_text, ITEM_TEXT_FONT_SIZE)
|
||||
text_x = button_rect.x - spacing - text_size.x
|
||||
text_y = rect.y + (rect.height - text_size.y) / 2
|
||||
rl.draw_text_ex(self._font, value_text, rl.Vector2(text_x, text_y), ITEM_TEXT_FONT_SIZE, 0, ITEM_TEXT_VALUE_COLOR)
|
||||
|
||||
# TODO: just use the generic Widget click callbacks everywhere, no returning from render
|
||||
pressed = self._pressed
|
||||
self._pressed = False
|
||||
@@ -139,6 +160,9 @@ class TextAction(ItemAction):
|
||||
rl.draw_text_ex(self._font, current_text, rl.Vector2(text_x, text_y), ITEM_TEXT_FONT_SIZE, 0, self.color)
|
||||
return False
|
||||
|
||||
def set_text(self, text: str | Callable[[], str]):
|
||||
self._text_source = text
|
||||
|
||||
def get_width(self) -> int:
|
||||
text_width = measure_text_cached(self._font, self.text, ITEM_TEXT_FONT_SIZE).x
|
||||
return int(text_width + TEXT_PADDING)
|
||||
@@ -382,7 +406,7 @@ def button_item(title: str, button_text: str | Callable[[], str], description: s
|
||||
|
||||
def text_item(title: str, value: str | Callable[[], str], description: str | Callable[[], str] | None = None,
|
||||
callback: Callable | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem:
|
||||
action = TextAction(text=value, color=rl.Color(170, 170, 170, 255), enabled=enabled)
|
||||
action = TextAction(text=value, color=ITEM_TEXT_VALUE_COLOR, enabled=enabled)
|
||||
return ListItem(title=title, description=description, action_item=action, callback=callback)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user