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 , 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 . _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 = [
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 ) ,
]
return items
def _render ( self , rect ) :
self . _scroller . render ( rect )
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 :
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