/* * exportscreen.c - Screen for exporting to USB drives * Copyright (C) 2024 Alexander Rosenberg * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. See the LICENSE file for more information. */ #include "exportscreen.h" #include #include #include #include #include #include static void export_screen_reset(ExportScreen *screen, SensorState *state) { screen->format = EXPORT_FORMAT_SQLITE; screen->stage = EXPORT_SCREEN_FORMAT; screen->need_redraw = true; drive_free_drives(screen->drives, screen->ndrive); screen->read_drives = true; screen->confirm_state = false; screen->need_overwrite = false; lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, LCD_DISPLAY_ON); } /* * Retrun: true if task is done, false otherwise */ static bool export_check_bg_task(ExportScreen *screen) { switch (async_operation_poll(screen->current_op, false)) { case ASYNC_OPERATION_DONE: if (async_operation_failed(screen->current_op)) { goto failed; } screen->need_redraw = true; screen->current_op = NULL; return true; case ASYNC_OPERATION_FAILED: goto failed; case ASYNC_OPERATION_WORKING: return false; } failed: screen->stage = EXPORT_SCREEN_FAILED; screen->need_redraw = true; screen->current_op = NULL; return true; } static const char *FORMAT_STRINGS[] = { "SQLITE", "CSV" }; static const char *EXTENSION_STRINGS[] = { "sqlite3", "csv" }; static char *export_build_cur_path(ExportScreen *screen) { char *path = NULL; pthread_cleanup_push(free, path); asprintf_checked(&path, "%s/%s.%s", screen->drives[screen->cur_drive].mnt, GLOBAL_OPTS.export_file_name, EXTENSION_STRINGS[screen->format]); pthread_cleanup_pop(false); return path; } static bool export_select_format(ExportScreen *screen, SensorState *state) { if (state->back_down) { export_screen_reset(screen, state); return true; } if (state->up_down || state->down_down) { screen->need_redraw = true; screen->format = abs((screen->format + state->up_down - state->down_down) % EXPORT_NFORMAT); } if (screen->need_redraw) { screen->need_redraw = false; lcd_display_control(state->lcd, LCD_CURSOR_BLINK, LCD_CURSOR_ON, LCD_DISPLAY_ON); lcd_clear(state->lcd); lcd_move_to(state->lcd, 0, 0); lcd_write_string(state->lcd, "Format:"); lcd_move_to(state->lcd, 1, 0); lcd_write_char(state->lcd, '>'); lcd_write_string(state->lcd, FORMAT_STRINGS[screen->format]); lcd_move_to(state->lcd, 1, 0); } if (state->sel_down) { ++screen->stage; screen->need_redraw = true; } return false; } static void export_select_drive(ExportScreen *screen, SensorState *state) { if (state->back_down) { --screen->stage; screen->need_redraw = true; return; } if (state->down_down && screen->cur_drive > 0) { --screen->cur_drive; screen->need_redraw = true; } if (state->up_down && screen->cur_drive < screen->ndrive - 1) { ++screen->cur_drive; screen->need_redraw = true; } Drive *drive = &screen->drives[screen->cur_drive]; if (screen->need_redraw) { screen->need_redraw = false; lcd_clear(state->lcd); lcd_move_to(state->lcd, 0, 0); lcd_write_string(state->lcd, "Drive:"); lcd_move_to(state->lcd, 1, 0); lcd_write_char(state->lcd, '>'); lcd_write_string(state->lcd, drive->name); lcd_write_char(state->lcd, ' '); if (drive->fs) { lcd_write_string(state->lcd, drive->fs); } else { lcd_write_string(state->lcd, "no FS"); } lcd_move_to(state->lcd, 1, 0); } if (state->sel_down) { screen->need_redraw = true; if (!drive->fs) { screen->stage = EXPORT_SCREEN_CONFIRM_NEWFS; } else { screen->stage = EXPORT_SCREEN_MOUNTING; } } } __attribute__((format(printf, 3, 4))) static enum { EXPORT_CONFIRM_CONTINUE = 0, EXPORT_CONFIRM_NO, EXPORT_CONFIRM_YES, } export_confirm_action(ExportScreen *screen, SensorState *state, const char *fmt, ...) { int done_status; if (state->back_down || (state->sel_down && !screen->confirm_state)) { done_status = EXPORT_CONFIRM_NO; goto confirm_done; } if (state->sel_down) { done_status = EXPORT_CONFIRM_YES; goto confirm_done; } if (state->up_down || state->down_down) { screen->confirm_state = abs((screen->confirm_state + state->up_down - state->down_down) % 2); screen->need_redraw = true; } if (screen->need_redraw) { screen->need_redraw = false; char message[17]; va_list args; va_start(args, fmt); vsnprintf(message, 17, fmt, args); va_end(args); lcd_clear(state->lcd); lcd_move_to(state->lcd, 0, 0); lcd_write_string(state->lcd, message); lcd_move_to(state->lcd, 1, 0); lcd_write_string(state->lcd, ">No >Yes"); lcd_display_control(state->lcd, LCD_CURSOR_BLINK, LCD_CURSOR_ON, LCD_DISPLAY_ON); lcd_move_to(state->lcd, 1, 4 * screen->confirm_state); } return EXPORT_CONFIRM_CONTINUE; confirm_done: lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, LCD_DISPLAY_ON); screen->confirm_state = false; screen->need_redraw = true; return done_status; } static bool export_show_message(ExportScreen *screen, SensorState *state, const char *message) { if (state->back_down || state->up_down || state->down_down || state->sel_down) { export_screen_reset(screen, state); return true; } if (screen->need_redraw) { lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, LCD_DISPLAY_ON); lcd_clear(state->lcd); lcd_move_to(state->lcd, 0, 0); lcd_write_string(state->lcd, message); screen->need_redraw = false; } return false; } static const char BACKGROUND_CHARS[] = { '|', '-' }; #define NBACKGROUND_CHAR sizeof(BACKGROUND_CHARS) /* * True if done, false otherwise */ static bool export_do_background_action(ExportScreen *screen, SensorState *state) { if (!screen->show_cancel_screen && (state->down_down || state->up_down || state->back_down || state->sel_down)) { screen->show_cancel_screen = true; screen->need_redraw = true; // we need to make sure that the confirm action screen ignores actions // for this iteration of the UI loop state->back_down = state->up_down = state->sel_down = state->down_down = false; } if (!screen->current_op) { screen->stage = EXPORT_SCREEN_FAILED; screen->need_redraw = true; return false; } if (export_check_bg_task(screen)) { return true; } if (screen->show_cancel_screen) { int confirm_status = export_confirm_action(screen, state, "Really cancel?"); if (confirm_status == EXPORT_CONFIRM_YES) { async_operation_free(screen->current_op); screen->current_op = NULL; AsyncOperation *op = drive_unmount(&screen->drives[screen->cur_drive], true); async_operation_poll(op, true); async_operation_free(op); screen->stage = EXPORT_SCREEN_CANCELED; screen->need_redraw = true; } else if (confirm_status == EXPORT_CONFIRM_NO) { screen->need_redraw = true; screen->show_cancel_screen = false; } } else { time_t now = time(NULL); if (screen->need_redraw || now != screen->last_char_change) { lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, LCD_DISPLAY_ON); screen->last_char_change = now; screen->need_redraw = false; lcd_clear(state->lcd); lcd_move_to(state->lcd, 0, 0); lcd_write_string(state->lcd, "Working"); lcd_write_char(state->lcd, BACKGROUND_CHARS[screen->working_char++ % NBACKGROUND_CHAR]); } } return false; } static void export_async_write_action(ExportScreen *screen) { signal(SIGINT, SIG_DFL); signal(SIGTERM, SIG_DFL); char *path = export_build_cur_path(screen); if (screen->need_overwrite && !recursive_rm(path)) { exit(1); } LOG_VERBOSE("Exporting to \"%s\"\n", path); switch (screen->format) { case EXPORT_FORMAT_SQLITE: if (!export_database(screen->db_cache, path)) { errx(1, "export to sqlite3 db failed: \"%s\"", path); } break; case EXPORT_FORMAT_CSV: if (!export_database_as_csv(screen->db_cache, path)) { errx(1, "export to CSV failed: \"%s\"", path); } break; default: errx(1, "unknown export format"); break; } free(path); LOG_VERBOSE("Export done\n"); exit(0); } static void export_handle_write(ExportScreen *screen, SensorState *state) { if (screen->current_op) { if (export_do_background_action(screen, state)) { screen->need_redraw = true; screen->stage = EXPORT_SCREEN_UNMOUNT; } return; } // if we have no current op, start a new one screen->show_cancel_screen = false; pid_t pid = fork(); if (pid < 0) { warn("failed to start write background task"); screen->need_redraw = true; screen->stage = EXPORT_SCREEN_FAILED; return; } else if (pid == 0) { // child export_async_write_action(screen); abort(); } else { // parent screen->current_op = async_operation_take(NULL, NULL, NULL, NULL, pid); } } static bool export_screen_dispatch(ExportScreen *screen, SensorState *state) { screen->db_cache = state->db; if (state->force_draw) { screen->need_redraw = true; } if (screen->read_drives) { screen->drives = drive_list_usb_drives(&screen->ndrive); screen->read_drives = false; if (screen->ndrive == 0) { screen->stage = EXPORT_SCREEN_NO_DRIVES; } else { screen->cur_drive = 0; } } Drive *cur_drive = &screen->drives[screen->cur_drive]; int confirm_status; switch (screen->stage) { case EXPORT_SCREEN_FORMAT: return export_select_format(screen, state); case EXPORT_SCREEN_DRIVE: export_select_drive(screen, state); break; case EXPORT_SCREEN_CONFIRM_NEWFS: confirm_status = export_confirm_action(screen, state, "Newfs? (%s)", cur_drive->name); if (confirm_status == EXPORT_CONFIRM_YES) { screen->stage = EXPORT_SCREEN_WRITE_NEWFS; screen->need_redraw = true; } else if (confirm_status == EXPORT_CONFIRM_NO) { screen->stage = EXPORT_SCREEN_DRIVE; screen->need_redraw = true; } break; case EXPORT_SCREEN_WRITE_NEWFS: if (!screen->current_op) { screen->current_op = drive_newfs_msdos(cur_drive); screen->show_cancel_screen = false; } if (export_do_background_action(screen, state)) { screen->stage = EXPORT_SCREEN_MOUNTING; screen->need_redraw = true; } break; case EXPORT_SCREEN_MOUNTING: if (!screen->current_op) { screen->current_op = drive_mount(cur_drive); screen->show_cancel_screen = false; } if (export_do_background_action(screen, state)) { char *path = export_build_cur_path(screen); struct stat statbuf; if (stat(path, &statbuf) == 0) { screen->stage = EXPORT_SCREEN_CONFIRM_OVERWRITE; } else { screen->stage = EXPORT_SCREEN_WRITING; } screen->need_redraw = true; free(path); } break; case EXPORT_SCREEN_CONFIRM_OVERWRITE: confirm_status = export_confirm_action(screen, state, "Overwrite?"); if (confirm_status == EXPORT_CONFIRM_YES) { screen->stage = EXPORT_SCREEN_WRITING; screen->need_redraw = true; } else if (confirm_status == EXPORT_CONFIRM_NO) { screen->stage = EXPORT_SCREEN_DRIVE; screen->need_redraw = true; // on the main thread so force unmount // also, nothing should be writing, so kill it if it is AsyncOperation *op = drive_unmount(cur_drive, true); async_operation_poll(op, true); async_operation_free(op); } break; case EXPORT_SCREEN_WRITING: export_handle_write(screen, state); break; case EXPORT_SCREEN_UNMOUNT: if (!screen->current_op) { screen->current_op = drive_unmount(cur_drive, false); screen->show_cancel_screen = false; } if (export_do_background_action(screen, state)) { screen->stage = EXPORT_SCREEN_DONE; screen->need_redraw = true; } break; case EXPORT_SCREEN_CANCELED: return export_show_message(screen, state, "Cancelled!"); case EXPORT_SCREEN_FAILED: return export_show_message(screen, state, "Failed!"); case EXPORT_SCREEN_DONE: return export_show_message(screen, state, "Export Done!"); case EXPORT_SCREEN_NO_DRIVES: return export_show_message(screen, state, "No drives found!"); } return false; } static void export_screen_cleanup(ExportScreen *screen) { async_operation_free(screen->current_op); } ExportScreen *export_screen_new() { ExportScreen *s = malloc_checked(sizeof(ExportScreen)); screen_init(&s->parent, "Export", (ScreenDispatchFunc) export_screen_dispatch, (ScreenCleanupFunc) export_screen_cleanup); s->read_drives = true; s->stage = EXPORT_SCREEN_FORMAT; s->format = EXPORT_FORMAT_SQLITE; s->current_op = NULL; return s; }