440 lines
15 KiB
C
440 lines
15 KiB
C
/*
|
|
* 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 <err.h>
|
|
#include <sys/types.h>
|
|
#include <stdarg.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
#include <signal.h>
|
|
|
|
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;
|
|
}
|