#!/usr/bin/env python3 import os import sys import threading import time from enum import IntEnum import pyray as rl from openpilot.system.hardware import PC from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.slider import SmallSlider from openpilot.system.ui.widgets.button import SmallButton, FullRoundedButton from openpilot.system.ui.widgets.label import gui_label, gui_text_box USERDATA = "/dev/disk/by-partlabel/userdata" TIMEOUT = 3*60 class ResetMode(IntEnum): USER_RESET = 0 # user initiated a factory reset from openpilot RECOVER = 1 # userdata is corrupt for some reason, give a chance to recover FORMAT = 2 # finish up a factory reset from a tool that doesn't flash an empty partition to userdata class ResetState(IntEnum): NONE = 0 RESETTING = 1 FAILED = 2 class Reset(Widget): def __init__(self, mode): super().__init__() self._mode = mode self._previous_reset_state = None self._reset_state = ResetState.NONE self._cancel_button = SmallButton("cancel") self._cancel_button.set_click_callback(self._cancel_callback) self._reboot_button = FullRoundedButton("reboot") self._reboot_button.set_click_callback(self._do_reboot) self._confirm_slider = SmallSlider("reset", self._confirm) self._render_status = True def _cancel_callback(self): self._render_status = False def _do_reboot(self): if PC: return os.system("sudo reboot") def _do_erase(self): if PC: return # Removing data and formatting rm = os.system("sudo rm -rf /data/*") os.system(f"sudo umount {USERDATA}") fmt = os.system(f"yes | sudo mkfs.ext4 {USERDATA}") if rm == 0 or fmt == 0: os.system("sudo reboot") else: self._reset_state = ResetState.FAILED def start_reset(self): self._reset_state = ResetState.RESETTING threading.Timer(0.1, self._do_erase).start() def _update_state(self): if self._reset_state != self._previous_reset_state: self._previous_reset_state = self._reset_state self._timeout_st = time.monotonic() elif self._reset_state != ResetState.RESETTING and (time.monotonic() - self._timeout_st) > TIMEOUT: exit(0) def _render(self, rect: rl.Rectangle): label_rect = rl.Rectangle(rect.x + 8, rect.y + 8, rect.width, 50) gui_label(label_rect, "factory reset", 48, font_weight=FontWeight.BOLD, color=rl.Color(255, 255, 255, int(255 * 0.9))) text_rect = rl.Rectangle(rect.x + 8, rect.y + 56, rect.width - 8 * 2, rect.height - 80) gui_text_box(text_rect, self._get_body_text(), 36, font_weight=FontWeight.ROMAN, line_scale=0.9) if self._reset_state != ResetState.RESETTING: # fade out cancel button as slider is moved, set visible to prevent pressing invisible cancel self._cancel_button.set_opacity(1.0 - self._confirm_slider.slider_percentage) self._cancel_button.set_visible(self._confirm_slider.slider_percentage < 0.8) if self._mode == ResetMode.RECOVER: self._cancel_button.set_text("reboot") self._cancel_button.render(rl.Rectangle( rect.x + 8, rect.y + rect.height - self._cancel_button.rect.height, self._cancel_button.rect.width, self._cancel_button.rect.height)) elif self._mode == ResetMode.USER_RESET and self._reset_state != ResetState.FAILED: self._cancel_button.render(rl.Rectangle( rect.x + 8, rect.y + rect.height - self._cancel_button.rect.height, self._cancel_button.rect.width, self._cancel_button.rect.height)) if self._reset_state != ResetState.FAILED: self._confirm_slider.render(rl.Rectangle( rect.x + rect.width - self._confirm_slider.rect.width, rect.y + rect.height - self._confirm_slider.rect.height, self._confirm_slider.rect.width, self._confirm_slider.rect.height)) else: self._reboot_button.render(rl.Rectangle( rect.x + 8, rect.y + rect.height - self._reboot_button.rect.height, self._reboot_button.rect.width, self._reboot_button.rect.height)) return self._render_status def _confirm(self): self.start_reset() def _get_body_text(self): if self._reset_state == ResetState.RESETTING: return "Resetting device... This may take up to a minute." if self._reset_state == ResetState.FAILED: return "Reset failed. Reboot to try again." if self._mode == ResetMode.RECOVER: return "Unable to mount data partition. Partition may be corrupted." return "All content and settings will be erased." def main(): mode = ResetMode.USER_RESET if len(sys.argv) > 1: if sys.argv[1] == '--recover': mode = ResetMode.RECOVER elif sys.argv[1] == "--format": mode = ResetMode.FORMAT gui_app.init_window("System Reset") reset = Reset(mode) if mode == ResetMode.FORMAT: reset.start_reset() for should_render in gui_app.render(): if should_render: if not reset.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)): break if __name__ == "__main__": main()