/* * reset.c -- System Tools (Aigo Eros Q platform) * * SPDX-License-Identifier: GPL-2.0-or-later * * Multi-function system tools module for the Aigo Eros Q hardware * family (HiFiWalker H2, Surfans F20, Phinistec Z6, Agptek H3). * * OreBolt OS, Hiby Player (stock firmware), and Rockbox all boot * from the FAT32 MicroSD card -- the SPI flash bootloader is never * modified. Switching between them is a matter of which files live * on the SD card. * * Menu options: * 1. PURGE -- Operational cleanup. Kills child processes * (dosbox, emulators), brings usb0 down, wipes * /data/loot_drop/, calls sync(). Does NOT * touch any firmware files. * 2. HIBY -- Restore stock firmware (Hiby Player). Removes * all OreBolt OS files from the SD card and * restores the stock backup from /data/stock_backup/. * Reboots into the original music player. * 3. ROCKBOX -- If the user has separately installed Rockbox on * the SD card (detected by presence of .rockbox/ * or rockbox/ directory), reboots the device. * OreBolt OS does NOT ship, bundle, or install * Rockbox. This is detection-only. * 4. REBOOT -- Warm reboot the device. * * STOCK BACKUP CONVENTION: * When OreBolt OS is first deployed (build.sh --deploy), the * deploy script snapshots the stock firmware files into * /data/stock_backup/ before writing any OreBolt files. The * expected layout is: * * /data/stock_backup/ * usr/bin/hiby_player (stock music player binary) * etc/inittab (stock init configuration) * etc/init.d/S* (stock init scripts) * ...any other stock files... * * If /data/stock_backup/ does not exist, the HIBY option shows * a warning that no backup is available. * * INPUT: * ROTATE = scroll menu * PLAY = select / confirm * BACK = cancel / exit */ #include #include #include #include #include #include #include #include #include #include #include #include #include "h2_ui.h" #include "log_manager.h" /* ------------------------------------------------------------------ */ /* Menu items */ /* ------------------------------------------------------------------ */ typedef enum { OPT_PURGE = 0, OPT_HIBY, OPT_ROCKBOX, OPT_REBOOT, OPT_COUNT } menu_opt_t; static const char *opt_labels[] = { "PURGE - Clear data & connections", "HIBY PLAYER - Restore stock FW", "ROCKBOX - Reboot to Rockbox", "REBOOT - Restart device", }; #define NUM_OPTS ((int)(sizeof(opt_labels) / sizeof(opt_labels[0]))) static const char *opt_descs[] = { "Kills child procs, wipes loot_drop,\nbrings usb0 down. Safe, non-destructive.", "Removes OreBolt OS files, restores\nstock Hiby Player from backup, reboots.", "Reboots so the Rockbox bootloader\ncan chainload .rockbox/ on the SD card.", "Standard warm reboot via kernel.", }; /* ------------------------------------------------------------------ */ /* State */ /* ------------------------------------------------------------------ */ static int input_fd = -1; static int selection = 0; static lv_obj_t *menu_list = NULL; static lv_obj_t *desc_label = NULL; static lv_obj_t *status_lbl = NULL; static lv_obj_t *list_btns[OPT_COUNT]; /* ------------------------------------------------------------------ */ /* Filesystem helpers */ /* ------------------------------------------------------------------ */ /* * Recursively remove all files and directories under `path`. * Does not follow symlinks. Skips `.` and `..`. * Returns 0 on success, -1 on error. */ static int rm_rf(const char *path) { DIR *d = opendir(path); if (!d) return -1; struct dirent *ent; int ret = 0; while ((ent = readdir(d)) != NULL) { if (ent->d_name[0] == '.') continue; char fullpath[512]; snprintf(fullpath, sizeof(fullpath), "%s/%s", path, ent->d_name); struct stat st; if (lstat(fullpath, &st) != 0) continue; if (S_ISDIR(st.st_mode)) { ret |= rm_rf(fullpath); rmdir(fullpath); } else { ret |= unlink(fullpath); } } closedir(d); return ret; } /* * Check if a directory exists and is non-empty. * Returns 1 if directory exists with at least one non-dot entry. */ static int dir_exists(const char *path) { DIR *d = opendir(path); if (!d) return 0; struct dirent *ent; int found = 0; while ((ent = readdir(d)) != NULL) { if (ent->d_name[0] == '.') continue; found = 1; break; } closedir(d); return found; } /* * Check if a file exists and is executable. */ static int bin_exists(const char *path) { return access(path, X_OK) == 0; } /* ------------------------------------------------------------------ */ /* Actions */ /* ------------------------------------------------------------------ */ static void action_purge(void) { LOG_INF("executing operational purge"); /* Kill known child processes */ const char *procs[] = { "dosbox", "snes9x", "fceux", "mednafen", "dgen", "gambatte", "gpsp", "stella", NULL }; for (int i = 0; procs[i]; i++) { char cmd[128]; snprintf(cmd, sizeof(cmd), "pidof %s 2>/dev/null", procs[i]); FILE *pgrep = popen(cmd, "r"); if (pgrep) { char pidbuf[16]; if (fgets(pidbuf, sizeof(pidbuf), pgrep)) { long pid = atol(pidbuf); if (pid > 0) { kill(pid, SIGTERM); LOG_INF("killed %s (pid %ld)", procs[i], pid); } } pclose(pgrep); } } /* Bring usb0 down */ int fd = open("/sys/class/net/usb0/operstate", O_WRONLY); if (fd >= 0) { write(fd, "down", 4); close(fd); } /* Wipe loot_drop */ rm_rf("/data/loot_drop"); mkdir("/data/loot_drop", 0755); sync(); LOG_INF("operational purge complete"); } static void action_hiby(void) { LOG_INF("initiating stock firmware restore (Hiby Player)"); if (!dir_exists("/data/stock_backup")) { LOG_ERR("no stock backup found at /data/stock_backup/"); return; } /* Step 1: Stop OreBolt init scripts */ system("/etc/init.d/S98retro-input stop 2>/dev/null"); system("/etc/init.d/S99broker stop 2>/dev/null"); system("/etc/init.d/bt_input_daemon.sh stop 2>/dev/null"); /* Step 2: Remove all OreBolt module binaries */ rm_rf("/apps"); mkdir("/apps", 0755); /* Step 3: Remove OreBolt-specific binaries */ unlink("/usr/bin/h2_test"); unlink("/usr/bin/retro_input_mapper"); /* Step 4: Remove OreBolt init scripts */ unlink("/etc/init.d/S98retro-input"); unlink("/etc/init.d/S99broker"); unlink("/etc/init.d/bt_input_daemon.sh"); /* Step 5: Remove OreBolt shared libraries */ unlink("/usr/lib/liblvgl.so"); /* Step 6: Restore stock files from backup */ if (dir_exists("/data/stock_backup/usr")) { system("cp -a /data/stock_backup/usr/bin/* /usr/bin/ 2>/dev/null"); system("cp -a /data/stock_backup/usr/lib/* /usr/lib/ 2>/dev/null"); } if (dir_exists("/data/stock_backup/etc")) { system("cp -a /data/stock_backup/etc/inittab /etc/inittab 2>/dev/null"); system("cp -a /data/stock_backup/etc/init.d/* /etc/init.d/ 2>/dev/null"); } sync(); LOG_INF("stock firmware restored, rebooting to Hiby Player"); sleep(1); sync(); reboot(RB_AUTOBOOT); } static void action_rockbox(void) { if (!dir_exists("/.rockbox") && !dir_exists("/rockbox")) { LOG_ERR("no .rockbox/ or rockbox/ directory found on SD card"); return; } /* * OreBolt OS does NOT ship or install Rockbox. If the user * has installed Rockbox on their SD card independently, a * reboot is sufficient for the Rockbox bootloader (or the * stock bootloader with Rockbox boot files) to load it. * * We do NOT modify any files -- Rockbox manages its own boot * presence. A reboot is sufficient. */ LOG_INF("Rockbox detected, rebooting to Rockbox"); sleep(1); sync(); reboot(RB_AUTOBOOT); } /* ------------------------------------------------------------------ */ /* UI */ /* ------------------------------------------------------------------ */ static void build_menu(void) { lv_obj_clean(menu_list); for (int i = 0; i < NUM_OPTS; i++) { list_btns[i] = lv_list_add_btn(menu_list, LV_SYMBOL_LIST, opt_labels[i]); lv_obj_set_style_text_color(list_btns[i], COLOR_TEXT, LV_PART_MAIN); lv_obj_set_style_text_font(list_btns[i], &lv_font_montserrat_10, LV_PART_MAIN); } } static void update_desc(void) { lv_label_set_text(desc_label, opt_descs[selection]); /* Color-code destructive options */ lv_color_t desc_color = COLOR_TEXT; if (selection == OPT_HIBY) desc_color = COLOR_PRIMARY; /* red-orange: destructive */ else if (selection == OPT_ROCKBOX) desc_color = lv_color_make(100, 180, 255); /* blue: external */ lv_obj_set_style_text_color(desc_label, desc_color, LV_PART_MAIN); } static void scroll_menu(int delta) { selection += delta; if (selection < 0) selection = NUM_OPTS - 1; if (selection >= NUM_OPTS) selection = 0; if (selection < NUM_OPTS && list_btns[selection]) { lv_obj_scroll_to_view(list_btns[selection], LV_ANIM_ON); } update_desc(); } /* ------------------------------------------------------------------ */ /* Confirmation overlay */ /* ------------------------------------------------------------------ */ static int show_confirm(const char *warning, const char *action_name) { lv_obj_clean(lv_scr_act()); lv_obj_t *scr = lv_scr_act(); lv_obj_t *box = lv_obj_create(scr); lv_obj_set_size(box, 296, 180); lv_obj_align(box, LV_ALIGN_CENTER, 0, 0); lv_obj_set_style_bg_color(box, lv_color_make(24, 18, 18), LV_PART_MAIN); lv_obj_set_style_border_color(box, COLOR_PRIMARY, LV_PART_MAIN); lv_obj_set_style_border_width(box, 2, LV_PART_MAIN); lv_obj_t *title = lv_label_create(box); lv_label_set_text(title, action_name); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 12); lv_obj_set_style_text_color(title, COLOR_PRIMARY, LV_PART_MAIN); lv_obj_set_style_text_font(title, &lv_font_montserrat_14, LV_PART_MAIN); lv_obj_t *warn = lv_label_create(box); lv_label_set_text(warn, warning); lv_obj_align(warn, LV_ALIGN_TOP_MID, 0, 38); lv_obj_set_style_text_color(warn, COLOR_TEXT, LV_PART_MAIN); lv_obj_set_style_text_font(warn, &lv_font_montserrat_10, LV_PART_MAIN); lv_obj_set_style_text_align(warn, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN); lv_obj_t *prompt = lv_label_create(box); lv_label_set_text(prompt, "[PLAY] CONFIRM [BACK] CANCEL"); lv_obj_align(prompt, LV_ALIGN_BOTTOM_MID, 0, -16); lv_obj_set_style_text_color(prompt, lv_color_make(120, 120, 140), LV_PART_MAIN); lv_obj_set_style_text_font(prompt, &lv_font_montserrat_10, LV_PART_MAIN); /* Wait for PLAY or BACK */ struct input_event ev; while (1) { lv_timer_handler(); if (input_fd >= 0 && read(input_fd, &ev, sizeof(struct input_event)) > 0) { if (ev.type == EV_KEY && ev.value == 1) { if (ev.code == H2_KEY_PLAY) { lv_label_set_text(prompt, "EXECUTING..."); lv_obj_set_style_text_color(prompt, COLOR_ACCENT, LV_PART_MAIN); lv_timer_handler(); return 1; /* confirmed */ } if (ev.code == H2_KEY_BACK) return 0; /* cancelled */ } } usleep(15000); } } /* ------------------------------------------------------------------ */ /* Status overlay (result message + BACK to dismiss) */ /* ------------------------------------------------------------------ */ static void show_status(const char *msg, int is_error) { lv_obj_clean(lv_scr_act()); lv_obj_t *scr = lv_scr_act(); lv_obj_t *box = lv_obj_create(scr); lv_obj_set_size(box, 296, 120); lv_obj_align(box, LV_ALIGN_CENTER, 0, 0); lv_obj_t *lbl = lv_label_create(box); lv_label_set_text(lbl, msg); lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0); lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN); lv_obj_set_style_text_font(lbl, &lv_font_montserrat_10, LV_PART_MAIN); if (is_error) lv_obj_set_style_text_color(lbl, COLOR_PRIMARY, LV_PART_MAIN); else lv_obj_set_style_text_color(lbl, COLOR_ACCENT, LV_PART_MAIN); lv_obj_t *hint = lv_label_create(scr); lv_label_set_text(hint, "[BACK] Return to menu"); lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -4); lv_obj_set_style_text_color(hint, lv_color_make(80, 85, 100), LV_PART_MAIN); lv_obj_set_style_text_font(hint, &lv_font_montserrat_10, LV_PART_MAIN); /* Wait for BACK */ struct input_event ev; while (1) { lv_timer_handler(); if (input_fd >= 0 && read(input_fd, &ev, sizeof(struct input_event)) > 0) { if (ev.type == EV_KEY && ev.value == 1 && ev.code == H2_KEY_BACK) break; } usleep(15000); } } /* ------------------------------------------------------------------ */ /* Rebuild the main menu after a confirmation/status overlay */ /* ------------------------------------------------------------------ */ static void rebuild_main_ui(void) { lv_obj_clean(lv_scr_act()); lv_obj_t *scr = lv_scr_act(); lv_obj_t *title = lv_label_create(scr); lv_label_set_text(title, "SYSTEM TOOLS"); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 8); lv_obj_set_style_text_color(title, COLOR_PRIMARY, LV_PART_MAIN); lv_obj_set_style_text_font(title, &lv_font_montserrat_14, LV_PART_MAIN); lv_obj_t *sub = lv_label_create(scr); lv_label_set_text(sub, "Aigo Eros Q | X1000E"); lv_obj_align(sub, LV_ALIGN_TOP_MID, 0, 26); lv_obj_set_style_text_color(sub, COLOR_MUTED, LV_PART_MAIN); lv_obj_set_style_text_font(sub, &lv_font_montserrat_10, LV_PART_MAIN); menu_list = lv_list_create(scr); lv_obj_set_size(menu_list, 296, 140); lv_obj_align(menu_list, LV_ALIGN_TOP_MID, 0, 42); lv_obj_set_style_bg_color(menu_list, lv_color_make(18, 22, 32), LV_PART_MAIN); lv_obj_set_style_border_width(menu_list, 1, LV_PART_MAIN); lv_obj_set_style_border_color(menu_list, lv_color_make(50, 55, 70), LV_PART_MAIN); desc_label = lv_label_create(scr); lv_obj_set_size(desc_label, 296, 36); lv_obj_align(desc_label, LV_ALIGN_TOP_MID, 0, 186); lv_obj_set_style_text_color(desc_label, COLOR_TEXT, LV_PART_MAIN); lv_obj_set_style_text_font(desc_label, &lv_font_montserrat_10, LV_PART_MAIN); lv_obj_set_style_text_align(desc_label, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN); status_lbl = lv_label_create(scr); lv_label_set_text(status_lbl, "ROTATE=scroll PLAY=select BACK=exit"); lv_obj_align(status_lbl, LV_ALIGN_BOTTOM_MID, 0, -4); lv_obj_set_style_text_color(status_lbl, lv_color_make(80, 85, 100), LV_PART_MAIN); lv_obj_set_style_text_font(status_lbl, &lv_font_montserrat_10, LV_PART_MAIN); build_menu(); update_desc(); } /* ------------------------------------------------------------------ */ /* main */ /* ------------------------------------------------------------------ */ int main(void) { init_h2_graphics_runtime("SYSTEM TOOLS"); LOG_INF("reset/system-tools module started"); input_fd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK); /* Build initial UI */ rebuild_main_ui(); /* Detect available options */ int has_stock_backup = dir_exists("/data/stock_backup"); int has_rockbox = dir_exists("/.rockbox") || dir_exists("/rockbox"); if (has_stock_backup) LOG_INF("stock backup detected: /data/stock_backup/"); if (has_rockbox) LOG_INF("Rockbox detected on SD card"); /* Input loop */ struct input_event ev; while (1) { lv_timer_handler(); if (input_fd >= 0 && read(input_fd, &ev, sizeof(struct input_event)) > 0) { if (ev.type == EV_REL && ev.code == 0 && ev.value != 0) { scroll_menu(ev.value > 0 ? 1 : -1); } if (ev.type == EV_KEY && ev.value == 1) { if (ev.code == H2_KEY_BACK) break; if (ev.code == H2_KEY_PLAY) { switch ((menu_opt_t)selection) { case OPT_PURGE: if (show_confirm( "Clears all captured data, kills\n" "child processes, drops network.\n" "OreBolt OS files are NOT removed.", "OPERATIONAL PURGE")) { action_purge(); show_status("PURGE COMPLETE.\n\n" "All captured data cleared.", 0); } rebuild_main_ui(); break; case OPT_HIBY: if (!has_stock_backup) { show_status( "No stock backup found.\n\n" "/data/stock_backup/ does not\n" "exist. Was OreBolt OS deployed\n" "with --backup-stock?", 1); rebuild_main_ui(); break; } if (show_confirm( "This will REMOVE OreBolt OS from\n" "the SD card and restore the stock\n" "Hiby Player firmware. All OreBolt\n" "modules, data, and configs will be\n" "permanently deleted.\n\n" "The device will reboot into the\n" "original Hiby music player.", "RESTORE HIBY PLAYER")) { /* This reboots -- does not return */ action_hiby(); } rebuild_main_ui(); break; case OPT_ROCKBOX: if (!has_rockbox) { show_status( "No Rockbox installation found.\n\n" "Install .rockbox/ on the SD card\n" "or use a Rockboot-flashed SD card.", 1); rebuild_main_ui(); break; } if (show_confirm( "Reboot to Rockbox.\n\n" "The Rockbox bootloader will\n" "chainload from the SD card.\n" "OreBolt OS files are NOT removed.", "BOOT ROCKBOX")) { /* This reboots -- does not return */ action_rockbox(); } rebuild_main_ui(); break; case OPT_REBOOT: if (show_confirm( "Standard warm reboot.\n" "Device will restart normally.", "REBOOT DEVICE")) { LOG_INF("user-initiated reboot"); sync(); reboot(RB_AUTOBOOT); } rebuild_main_ui(); break; default: break; } } } } usleep(15000); } if (input_fd >= 0) close(input_fd); LOG_INF("system-tools module exited"); return 0; }