#!/usr/bin/env python3
import sys
import subprocess
import threading
import pyray as rl
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 . button import gui_button , ButtonStyle
from openpilot . system . ui . lib . label import gui_text_box , gui_label
# Constants
MARGIN = 50
BUTTON_HEIGHT = 160
BUTTON_WIDTH = 400
PROGRESS_BAR_HEIGHT = 72
TITLE_FONT_SIZE = 80
BODY_FONT_SIZE = 65
BACKGROUND_COLOR = rl . BLACK
PROGRESS_BG_COLOR = rl . Color ( 41 , 41 , 41 , 255 )
PROGRESS_COLOR = rl . Color ( 54 , 77 , 239 , 255 )
class Screen ( IntEnum ) :
PROMPT = 0
WIFI = 1
PROGRESS = 2
class Updater :
def __init__ ( self , updater_path , manifest_path ) :
self . updater = updater_path
self . manifest = manifest_path
self . current_screen = Screen . PROMPT
self . progress_value = 0
self . progress_text = " Loading... "
self . show_reboot_button = False
self . process = None
self . update_thread = None
def install_update ( self ) :
self . current_screen = Screen . PROGRESS
self . progress_value = 0
self . progress_text = " Downloading... "
self . show_reboot_button = False
# Start the update process in a separate thread
self . update_thread = threading . Thread ( target = self . _run_update_process )
self . update_thread . daemon = True
self . update_thread . start ( )
def _run_update_process ( self ) :
# TODO: just import it and run in a thread without a subprocess
cmd = [ self . updater , " --swap " , self . manifest ]
self . process = subprocess . Popen ( cmd , stdout = subprocess . PIPE , stderr = subprocess . PIPE ,
text = True , bufsize = 1 , universal_newlines = True )
for line in self . process . stdout :
parts = line . strip ( ) . split ( " : " )
if len ( parts ) == 2 :
self . progress_text = parts [ 0 ]
try :
self . progress_value = int ( float ( parts [ 1 ] ) )
except ValueError :
pass
exit_code = self . process . wait ( )
if exit_code == 0 :
HARDWARE . reboot ( )
else :
self . progress_text = " Update failed "
self . show_reboot_button = True
def render_prompt_screen ( self ) :
# Title
title_rect = rl . Rectangle ( MARGIN + 50 , 250 , gui_app . width - MARGIN * 2 - 100 , TITLE_FONT_SIZE )
gui_label ( title_rect , " Update Required " , TITLE_FONT_SIZE , font_weight = FontWeight . BOLD )
# Description
desc_text = " An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. \
The download size is approximately 1 GB . "
desc_rect = rl . Rectangle ( MARGIN + 50 , 250 + TITLE_FONT_SIZE + 75 , gui_app . width - MARGIN * 2 - 100 , BODY_FONT_SIZE * 3 )
gui_text_box ( desc_rect , desc_text , BODY_FONT_SIZE )
# Buttons at the bottom
button_y = gui_app . height - MARGIN - BUTTON_HEIGHT
button_width = ( gui_app . width - MARGIN * 3 ) / / 2
# WiFi button
wifi_button_rect = rl . Rectangle ( MARGIN , button_y , button_width , BUTTON_HEIGHT )
if gui_button ( wifi_button_rect , " Connect to Wi-Fi " ) :
self . current_screen = Screen . WIFI
return # Return to avoid processing other buttons after screen change
# Install button
install_button_rect = rl . Rectangle ( MARGIN * 2 + button_width , button_y , button_width , BUTTON_HEIGHT )
if gui_button ( install_button_rect , " Install " , button_style = ButtonStyle . PRIMARY ) :
self . install_update ( )
return # Return to avoid further processing after action
def render_wifi_screen ( self ) :
# Title and back button
title_rect = rl . Rectangle ( MARGIN + 50 , MARGIN , gui_app . width - MARGIN * 2 - 100 , 60 )
gui_label ( title_rect , " Wi-Fi Networks " , 60 , font_weight = FontWeight . BOLD )
back_button_rect = rl . Rectangle ( MARGIN , gui_app . height - MARGIN - BUTTON_HEIGHT , BUTTON_WIDTH , BUTTON_HEIGHT )
if gui_button ( back_button_rect , " Back " ) :
self . current_screen = Screen . PROMPT
return # Return to avoid processing other interactions after screen change
# Draw placeholder for WiFi implementation
placeholder_rect = rl . Rectangle (
MARGIN ,
title_rect . y + title_rect . height + MARGIN ,
gui_app . width - MARGIN * 2 ,
gui_app . height - title_rect . height - MARGIN * 3 - BUTTON_HEIGHT
)
# Draw rounded rectangle background
rl . draw_rectangle_rounded (
placeholder_rect ,
0.1 ,
10 ,
rl . Color ( 41 , 41 , 41 , 255 )
)
# Draw placeholder text
placeholder_text = " WiFi Implementation Placeholder "
text_size = rl . measure_text_ex ( gui_app . font ( ) , placeholder_text , 80 , 1 )
text_pos = rl . Vector2 (
placeholder_rect . x + ( placeholder_rect . width - text_size . x ) / 2 ,
placeholder_rect . y + ( placeholder_rect . height - text_size . y ) / 2
)
rl . draw_text_ex ( gui_app . font ( ) , placeholder_text , text_pos , 80 , 1 , rl . WHITE )
# Draw instructions
instructions_text = " Real WiFi functionality would be implemented here "
instructions_size = rl . measure_text_ex ( gui_app . font ( ) , instructions_text , 40 , 1 )
instructions_pos = rl . Vector2 (
placeholder_rect . x + ( placeholder_rect . width - instructions_size . x ) / 2 ,
text_pos . y + text_size . y + 20
)
rl . draw_text_ex ( gui_app . font ( ) , instructions_text , instructions_pos , 40 , 1 , rl . GRAY )
def render_progress_screen ( self ) :
title_rect = rl . Rectangle ( MARGIN + 100 , 330 , gui_app . width - MARGIN * 2 - 200 , 100 )
gui_label ( title_rect , self . progress_text , 90 , font_weight = FontWeight . SEMI_BOLD )
# Progress bar
bar_rect = rl . Rectangle ( MARGIN + 100 , 330 + 100 + 100 , gui_app . width - MARGIN * 2 - 200 , PROGRESS_BAR_HEIGHT )
rl . draw_rectangle_rounded ( bar_rect , 0.5 , 10 , PROGRESS_BG_COLOR )
# Calculate the width of the progress chunk
progress_width = ( bar_rect . width * self . progress_value ) / 100
if progress_width > 0 :
progress_rect = rl . Rectangle ( bar_rect . x , bar_rect . y , progress_width , bar_rect . height )
rl . draw_rectangle_rounded ( progress_rect , 0.5 , 10 , PROGRESS_COLOR )
# Show reboot button if needed
if self . show_reboot_button :
reboot_rect = rl . Rectangle ( MARGIN + 100 , gui_app . height - MARGIN - BUTTON_HEIGHT , BUTTON_WIDTH , BUTTON_HEIGHT )
if gui_button ( reboot_rect , " Reboot " ) :
# Return True to signal main loop to exit before rebooting
HARDWARE . reboot ( )
return
def render ( self ) :
if self . current_screen == Screen . PROMPT :
self . render_prompt_screen ( )
elif self . current_screen == Screen . WIFI :
self . render_wifi_screen ( )
elif self . current_screen == Screen . PROGRESS :
self . render_progress_screen ( )
def main ( ) :
if len ( sys . argv ) < 3 :
print ( " Usage: updater.py <updater_path> <manifest_path> " )
sys . exit ( 1 )
updater_path = sys . argv [ 1 ]
manifest_path = sys . argv [ 2 ]
try :
gui_app . init_window ( " System Update " )
updater = Updater ( updater_path , manifest_path )
for _ in gui_app . render ( ) :
updater . render ( )
finally :
# Make sure we clean up even if there's an error
gui_app . close ( )
if __name__ == " __main__ " :
main ( )