ui(raylib): setup.py (#35140)
* setup.py * better font * use gui_button * btn * fix button and triangle * low voltage text color * fix network page * HARDWARE.get_os_version() * typing * white title * update default text color * use default font color * fix software screen * fix software screen * radio font size * line length * fix regex * draw svgs * comment is out of date * add cairosvg * use cairosvg * remove unused import * support other image types * revert origin * fix setup warning icon * fix * remove cairosvg * use pngs * wrap * fix disabled style * TODO * revert uv.lock * use new file paths (not rasterized yet) * oops * fixes * params not used * network check thread * oops * fix custom URL and download failed screens * clear keyboard * rm * fixes * show full error message * check network typepull/35247/head
parent
83679bd856
commit
512d83cc36
2 changed files with 345 additions and 1 deletions
@ -0,0 +1,344 @@ |
||||
#!/usr/bin/env python3 |
||||
import os |
||||
import re |
||||
import threading |
||||
import time |
||||
import urllib.request |
||||
from enum import IntEnum |
||||
import pyray as rl |
||||
|
||||
from cereal import log |
||||
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_label, gui_text_box |
||||
from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManagerWrapper |
||||
from openpilot.system.ui.widgets.keyboard import Keyboard |
||||
|
||||
NetworkType = log.DeviceState.NetworkType |
||||
|
||||
MARGIN = 50 |
||||
TITLE_FONT_SIZE = 116 |
||||
TITLE_FONT_WEIGHT = FontWeight.MEDIUM |
||||
NEXT_BUTTON_WIDTH = 310 |
||||
BODY_FONT_SIZE = 96 |
||||
BUTTON_HEIGHT = 160 |
||||
BUTTON_SPACING = 50 |
||||
|
||||
OPENPILOT_URL = "https://openpilot.comma.ai" |
||||
USER_AGENT = f"AGNOSSetup-{HARDWARE.get_os_version()}" |
||||
|
||||
|
||||
class SetupState(IntEnum): |
||||
LOW_VOLTAGE = 0 |
||||
GETTING_STARTED = 1 |
||||
NETWORK_SETUP = 2 |
||||
SOFTWARE_SELECTION = 3 |
||||
CUSTOM_URL = 4 |
||||
DOWNLOADING = 5 |
||||
DOWNLOAD_FAILED = 6 |
||||
|
||||
|
||||
class Setup: |
||||
def __init__(self): |
||||
self.state = SetupState.GETTING_STARTED |
||||
self.network_check_thread = None |
||||
self.network_connected = threading.Event() |
||||
self.wifi_connected = threading.Event() |
||||
self.stop_network_check_thread = threading.Event() |
||||
self.failed_url = "" |
||||
self.failed_reason = "" |
||||
self.download_url = "" |
||||
self.download_progress = 0 |
||||
self.download_thread = None |
||||
self.wifi_manager = WifiManagerWrapper() |
||||
self.wifi_ui = WifiManagerUI(self.wifi_manager) |
||||
self.keyboard = Keyboard() |
||||
self.selected_radio = None |
||||
|
||||
self.warning = gui_app.texture("icons/warning.png", 150, 150) |
||||
self.checkmark = gui_app.texture("icons/circled_check.png", 100, 100) |
||||
|
||||
try: |
||||
with open("/sys/class/hwmon/hwmon1/in1_input") as f: |
||||
voltage = float(f.read().strip()) / 1000.0 |
||||
if voltage < 7: |
||||
self.state = SetupState.LOW_VOLTAGE |
||||
except (FileNotFoundError, ValueError): |
||||
self.state = SetupState.LOW_VOLTAGE |
||||
|
||||
def render(self, rect: rl.Rectangle): |
||||
if self.state == SetupState.LOW_VOLTAGE: |
||||
self.render_low_voltage(rect) |
||||
elif self.state == SetupState.GETTING_STARTED: |
||||
self.render_getting_started(rect) |
||||
elif self.state == SetupState.NETWORK_SETUP: |
||||
self.render_network_setup(rect) |
||||
elif self.state == SetupState.SOFTWARE_SELECTION: |
||||
self.render_software_selection(rect) |
||||
elif self.state == SetupState.CUSTOM_URL: |
||||
self.render_custom_url() |
||||
elif self.state == SetupState.DOWNLOADING: |
||||
self.render_downloading(rect) |
||||
elif self.state == SetupState.DOWNLOAD_FAILED: |
||||
self.render_download_failed(rect) |
||||
|
||||
def render_low_voltage(self, rect: rl.Rectangle): |
||||
rl.draw_texture(self.warning, int(rect.x + 150), int(rect.y + 110), rl.WHITE) |
||||
|
||||
title_rect = rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 100, rect.width - 500 - 150, TITLE_FONT_SIZE) |
||||
gui_label(title_rect, "WARNING: Low Voltage", TITLE_FONT_SIZE, rl.Color(255, 89, 79, 255), FontWeight.MEDIUM) |
||||
|
||||
body_rect = rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 100 + TITLE_FONT_SIZE + 25, rect.width - 500 - 150, BODY_FONT_SIZE * 3) |
||||
gui_text_box(body_rect, "Power your device in a car with a harness or proceed at your own risk.", BODY_FONT_SIZE) |
||||
|
||||
button_width = (rect.width - MARGIN * 3) / 2 |
||||
button_y = rect.height - MARGIN - BUTTON_HEIGHT |
||||
|
||||
if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Power off"): |
||||
HARDWARE.shutdown() |
||||
|
||||
if gui_button(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT), "Continue"): |
||||
self.state = SetupState.GETTING_STARTED |
||||
|
||||
def render_getting_started(self, rect: rl.Rectangle): |
||||
title_rect = rl.Rectangle(rect.x + 165, rect.y + 280, rect.width - 265, TITLE_FONT_SIZE) |
||||
gui_label(title_rect, "Getting Started", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) |
||||
|
||||
desc_rect = rl.Rectangle(rect.x + 165, rect.y + 280 + TITLE_FONT_SIZE + 90, rect.width - 500, BODY_FONT_SIZE * 3) |
||||
gui_text_box(desc_rect, "Before we get on the road, let's finish installation and cover some details.", BODY_FONT_SIZE) |
||||
|
||||
btn_rect = rl.Rectangle(rect.width - NEXT_BUTTON_WIDTH, 0, NEXT_BUTTON_WIDTH, rect.height) |
||||
|
||||
ret = gui_button(btn_rect, "", button_style=ButtonStyle.PRIMARY, border_radius=0) |
||||
triangle = gui_app.texture("images/button_continue_triangle.png", 54, int(btn_rect.height)) |
||||
rl.draw_texture_v(triangle, rl.Vector2(btn_rect.x + btn_rect.width / 2 - triangle.width / 2, btn_rect.height / 2 - triangle.height / 2), rl.WHITE) |
||||
|
||||
if ret: |
||||
self.state = SetupState.NETWORK_SETUP |
||||
self.wifi_manager.request_scan() |
||||
self.start_network_check() |
||||
|
||||
def check_network_connectivity(self): |
||||
while not self.stop_network_check_thread.is_set(): |
||||
if self.state == SetupState.NETWORK_SETUP: |
||||
try: |
||||
urllib.request.urlopen("https://google.com", timeout=2) |
||||
self.network_connected.set() |
||||
if HARDWARE.get_network_type() == NetworkType.wifi: |
||||
self.wifi_connected.set() |
||||
else: |
||||
self.wifi_connected.clear() |
||||
except Exception: |
||||
self.network_connected.clear() |
||||
time.sleep(1) |
||||
|
||||
def start_network_check(self): |
||||
if self.network_check_thread is None or not self.network_check_thread.is_alive(): |
||||
self.network_check_thread = threading.Thread(target=self.check_network_connectivity, daemon=True) |
||||
self.network_check_thread.start() |
||||
|
||||
def close(self): |
||||
if self.network_check_thread is not None: |
||||
self.stop_network_check_thread.set() |
||||
self.network_check_thread.join() |
||||
|
||||
def render_network_setup(self, rect: rl.Rectangle): |
||||
title_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE) |
||||
gui_label(title_rect, "Connect to Wi-Fi", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) |
||||
|
||||
wifi_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN + 25, rect.width - MARGIN * 2, |
||||
rect.height - TITLE_FONT_SIZE - 25 - BUTTON_HEIGHT - MARGIN * 3) |
||||
rl.draw_rectangle_rounded(wifi_rect, 0.05, 10, rl.Color(51, 51, 51, 255)) |
||||
wifi_content_rect = rl.Rectangle(wifi_rect.x + MARGIN, wifi_rect.y, wifi_rect.width - MARGIN * 2, wifi_rect.height) |
||||
self.wifi_ui.render(wifi_content_rect) |
||||
|
||||
button_width = (rect.width - BUTTON_SPACING - MARGIN * 2) / 2 |
||||
button_y = rect.height - BUTTON_HEIGHT - MARGIN |
||||
|
||||
if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Back"): |
||||
self.state = SetupState.GETTING_STARTED |
||||
|
||||
# Check network connectivity status |
||||
continue_enabled = self.network_connected.is_set() |
||||
continue_text = ("Continue" if self.wifi_connected.is_set() else "Continue without Wi-Fi") if continue_enabled else "Waiting for internet" |
||||
|
||||
if gui_button( |
||||
rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT), |
||||
continue_text, |
||||
button_style=ButtonStyle.PRIMARY if continue_enabled else ButtonStyle.NORMAL, |
||||
): |
||||
self.state = SetupState.SOFTWARE_SELECTION |
||||
self.stop_network_check_thread.set() |
||||
|
||||
def render_software_selection(self, rect: rl.Rectangle): |
||||
title_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE) |
||||
gui_label(title_rect, "Choose Software to Install", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) |
||||
|
||||
radio_height = 230 |
||||
radio_spacing = 30 |
||||
|
||||
openpilot_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN * 2, rect.width - MARGIN * 2, radio_height) |
||||
openpilot_selected = self.selected_radio == "openpilot" |
||||
|
||||
rl.draw_rectangle_rounded(openpilot_rect, 0.1, 10, rl.Color(70, 91, 234, 255) if openpilot_selected else rl.Color(79, 79, 79, 255)) |
||||
gui_label(rl.Rectangle(openpilot_rect.x + 100, openpilot_rect.y, openpilot_rect.width - 200, radio_height), "openpilot", BODY_FONT_SIZE) |
||||
|
||||
if openpilot_selected: |
||||
checkmark_pos = rl.Vector2(openpilot_rect.x + openpilot_rect.width - 100 - self.checkmark.width, |
||||
openpilot_rect.y + radio_height / 2 - self.checkmark.height / 2) |
||||
rl.draw_texture_v(self.checkmark, checkmark_pos, rl.WHITE) |
||||
|
||||
custom_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN * 2 + radio_height + radio_spacing, rect.width - MARGIN * 2, radio_height) |
||||
custom_selected = self.selected_radio == "custom" |
||||
|
||||
rl.draw_rectangle_rounded(custom_rect, 0.1, 10, rl.Color(70, 91, 234, 255) if custom_selected else rl.Color(79, 79, 79, 255)) |
||||
gui_label(rl.Rectangle(custom_rect.x + 100, custom_rect.y, custom_rect.width - 200, radio_height), "Custom Software", BODY_FONT_SIZE) |
||||
|
||||
if custom_selected: |
||||
checkmark_pos = rl.Vector2(custom_rect.x + custom_rect.width - 100 - self.checkmark.width, custom_rect.y + radio_height / 2 - self.checkmark.height / 2) |
||||
rl.draw_texture_v(self.checkmark, checkmark_pos, rl.WHITE) |
||||
|
||||
mouse_pos = rl.get_mouse_position() |
||||
if rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT): |
||||
if rl.check_collision_point_rec(mouse_pos, openpilot_rect): |
||||
self.selected_radio = "openpilot" |
||||
elif rl.check_collision_point_rec(mouse_pos, custom_rect): |
||||
self.selected_radio = "custom" |
||||
|
||||
button_width = (rect.width - BUTTON_SPACING - MARGIN * 2) / 2 |
||||
button_y = rect.height - BUTTON_HEIGHT - MARGIN |
||||
|
||||
if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Back"): |
||||
self.state = SetupState.NETWORK_SETUP |
||||
|
||||
continue_enabled = self.selected_radio is not None |
||||
if gui_button( |
||||
rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT), |
||||
"Continue", |
||||
button_style=ButtonStyle.PRIMARY, |
||||
is_enabled=continue_enabled, |
||||
): |
||||
if continue_enabled: |
||||
if self.selected_radio == "openpilot": |
||||
self.download(OPENPILOT_URL) |
||||
else: |
||||
self.state = SetupState.CUSTOM_URL |
||||
|
||||
def render_downloading(self, rect: rl.Rectangle): |
||||
title_rect = rl.Rectangle(rect.x, rect.y + rect.height / 2 - TITLE_FONT_SIZE / 2, rect.width, TITLE_FONT_SIZE) |
||||
gui_label(title_rect, "Downloading...", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) |
||||
|
||||
def render_download_failed(self, rect: rl.Rectangle): |
||||
title_rect = rl.Rectangle(rect.x + 117, rect.y + 185, rect.width - 117, TITLE_FONT_SIZE) |
||||
gui_label(title_rect, "Download Failed", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) |
||||
|
||||
url_rect = rl.Rectangle(rect.x + 117, rect.y + 185 + TITLE_FONT_SIZE + 67, rect.width - 117 - 100, 64) |
||||
gui_label(url_rect, self.failed_url, 64, font_weight=FontWeight.NORMAL) |
||||
|
||||
error_rect = rl.Rectangle(rect.x + 117, rect.y + 185 + TITLE_FONT_SIZE + 67 + 64 + 48, |
||||
rect.width - 117 - 100, rect.height - 185 + TITLE_FONT_SIZE + 67 + 64 + 48 - BUTTON_HEIGHT - MARGIN * 2) |
||||
gui_text_box(error_rect, self.failed_reason, BODY_FONT_SIZE) |
||||
|
||||
button_width = (rect.width - BUTTON_SPACING - MARGIN * 2) / 2 |
||||
button_y = rect.height - BUTTON_HEIGHT - MARGIN |
||||
|
||||
if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Reboot device"): |
||||
HARDWARE.reboot() |
||||
|
||||
if gui_button(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT), "Start over", |
||||
button_style=ButtonStyle.PRIMARY): |
||||
self.state = SetupState.GETTING_STARTED |
||||
|
||||
def render_custom_url(self): |
||||
result = self.keyboard.render("Enter URL", "for Custom Software") |
||||
|
||||
# Enter pressed |
||||
if result == 1: |
||||
url = self.keyboard.text |
||||
self.keyboard.clear() |
||||
if url: |
||||
self.download(url) |
||||
|
||||
# Cancel pressed |
||||
elif result == 0: |
||||
self.state = SetupState.SOFTWARE_SELECTION |
||||
|
||||
def download(self, url: str): |
||||
# autocomplete incomplete URLs |
||||
if re.match("^([^/.]+)/([^/]+)$", url): |
||||
url = f"https://installer.comma.ai/{url}" |
||||
|
||||
self.download_url = url |
||||
self.state = SetupState.DOWNLOADING |
||||
|
||||
self.download_thread = threading.Thread(target=self._download_thread, daemon=True) |
||||
self.download_thread.start() |
||||
|
||||
def _download_thread(self): |
||||
try: |
||||
import tempfile |
||||
|
||||
_, tmpfile = tempfile.mkstemp(prefix="installer_") |
||||
|
||||
headers = {"User-Agent": USER_AGENT, "X-openpilot-serial": HARDWARE.get_serial()} |
||||
req = urllib.request.Request(self.download_url, headers=headers) |
||||
|
||||
with open(tmpfile, 'wb') as f, urllib.request.urlopen(req, timeout=30) as response: |
||||
total_size = int(response.headers.get('content-length', 0)) |
||||
downloaded = 0 |
||||
block_size = 8192 |
||||
|
||||
while True: |
||||
buffer = response.read(block_size) |
||||
if not buffer: |
||||
break |
||||
|
||||
downloaded += len(buffer) |
||||
f.write(buffer) |
||||
|
||||
if total_size: |
||||
self.download_progress = int(downloaded * 100 / total_size) |
||||
|
||||
is_elf = False |
||||
with open(tmpfile, 'rb') as f: |
||||
header = f.read(4) |
||||
is_elf = header == b'\x7fELF' |
||||
|
||||
if not is_elf: |
||||
self.download_failed(self.download_url, "No custom software found at this URL.") |
||||
return |
||||
|
||||
os.rename(tmpfile, "/tmp/installer") |
||||
os.chmod("/tmp/installer", 0o755) |
||||
|
||||
with open("/tmp/installer_url", "w") as f: |
||||
f.write(self.download_url) |
||||
|
||||
gui_app.request_close() |
||||
|
||||
except Exception: |
||||
error_msg = "Ensure the entered URL is valid, and the device's internet connection is good." |
||||
self.download_failed(self.download_url, error_msg) |
||||
|
||||
def download_failed(self, url: str, reason: str): |
||||
self.failed_url = url |
||||
self.failed_reason = reason |
||||
self.state = SetupState.DOWNLOAD_FAILED |
||||
|
||||
|
||||
def main(): |
||||
try: |
||||
gui_app.init_window("Setup") |
||||
setup = Setup() |
||||
for _ in gui_app.render(): |
||||
setup.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) |
||||
setup.close() |
||||
except Exception as e: |
||||
print(f"Setup error: {e}") |
||||
finally: |
||||
gui_app.close() |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
main() |
Loading…
Reference in new issue