Files
rpi4b-temp-humidity/src/ui/exportscreen.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;
}