From 6ae67c4732f48a32f53f58395d2d7420743d0544 Mon Sep 17 00:00:00 2001 From: Alexander Rosenberg Date: Thu, 4 Apr 2024 00:53:29 -0700 Subject: [PATCH] Add export screen --- Makefile | 6 +- config.mk | 1 + src/config.c | 25 ++- src/drive.c | 228 ++++++++++++++++++++++++ src/drive.h | 60 +++++++ src/main.c | 19 +- src/ui/exportscreen.c | 403 ++++++++++++++++++++++++++++++++++++++++++ src/ui/exportscreen.h | 61 +++++++ src/ui/screen.c | 5 +- src/util.c | 160 ++++++++++++++++- src/util.h | 33 +++- 11 files changed, 977 insertions(+), 24 deletions(-) create mode 100644 src/drive.c create mode 100644 src/drive.h create mode 100644 src/ui/exportscreen.c create mode 100644 src/ui/exportscreen.h diff --git a/Makefile b/Makefile index f4f0d4d..3618790 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,8 @@ LD=clang LDFLAGS=-lgpio -lfigpar -lpthread ${SQLITE3_LDFLAGS} SRCS=src/main.c src/util.c src/lcd.c src/ths.c src/button.c src/ui/screen.c\ src/ui/datesel.c src/ui/statsby.c src/ui/datapoints.c src/ui/timesel.c\ - src/ui/statrange.c src/config.c src/ui/blankscreen.c + src/ui/statrange.c src/config.c src/ui/blankscreen.c src/drive.c\ + src/ui/exportscreen.c PROG=rpi4b-temp-humidity OBJS=${SRCS:C/^src/bin/:C/.c$/.o/} @@ -40,6 +41,8 @@ bin/main.o bin/ui/statsby.o: src/ui/statsby.h bin/main.o bin/ui/datapoints.o: src/ui/datapoints.h bin/main.o bin/ui/blankscreen.o: src/ui/blankscreen.h bin/main.o bin/ui/statrange.o: src/ui/statrange.h +vin/main.o bin/ui/exportscreen.o: src/ui/exportscreen.h +bin/main.o bin/drive.o bin/ui/exportscreen.o: src/drive.h .if "${DEFAULT_TEMP_UNIT}" != "F" && "${DEFAULT_TEMP_UNIT}" != "C" &&\ "${DEFAULT_TEMP_UNIT}" != "c" && "${DEFAULT_TEMP_UNIT}" != "c" @@ -58,6 +61,7 @@ ${OBJS}: ${.TARGET:C/^bin/src/:C/.o$/.c/} -DDEFAULT_DATABASE_LOCATION="\"${DEFAULT_DATABASE_LOCATION}\""\ -DDEFAULT_REFRESH_TIME="${DEFAULT_REFRESH_TIME}"\ -DDEFAULT_TEMP_UNIT="'${DEFAULT_TEMP_UNIT}'"\ +-DDEFAULT_EXPORT_FILE_NAME="\"${DEFAULT_EXPORT_FILE_NAME}\""\ -o ${@} ${.TARGET:C/^bin/src/:C/.o$/.c/} clean: diff --git a/config.mk b/config.mk index e860b82..9efa167 100644 --- a/config.mk +++ b/config.mk @@ -12,3 +12,4 @@ DEFAULT_FAIL_LIMIT=5 DEFAULT_DATABASE_LOCATION=/var/db/rpi4-temp-humidity/db.sqlite DEFAULT_REFRESH_TIME=5000 DEFAULT_TEMP_UNIT=F +DEFAULT_EXPORT_FILE_NAME=env_data diff --git a/src/config.c b/src/config.c index 0ef5540..d4ca93a 100644 --- a/src/config.c +++ b/src/config.c @@ -93,7 +93,7 @@ static int parse_uint_arr_callback(struct figpar_config *opt, uint32_t line, if (last_char && !isdigit(last_char)) { warnx("line %" PRIu32 ": not a valid number array \"%s\"", line, value); - FREE_CHECKED(arr->arr); + free(arr->arr); free(arr); return 1; } @@ -118,7 +118,7 @@ static int parse_uint_arr_callback(struct figpar_config *opt, uint32_t line, static int parse_str_callback(struct figpar_config *opt, uint32_t line, char *directive, char *value) { - FREE_CHECKED(opt->value.str); + free(opt->value.str); if (!value[0]) { opt->type = FIGPAR_TYPE_STR; opt->value.str = NULL; @@ -200,6 +200,11 @@ static void set_options_from_entries(struct figpar_config *entries, GLOBAL_OPTS.temp_unit = entries[15].value.num; GLOBAL_OPTS.bl_pin = entries[16].value.num; + + entries[17].type = FIGPAR_TYPE_NONE; + GLOBAL_OPTS.export_file_name = steal_opt_if_set(entries[17].value.str); + LOG_VERBOSE("Using export_file_name: \"%s\"\n", + GLOBAL_OPTS.export_file_name); } static char *strdup_default_opt(const char *def) { @@ -298,13 +303,19 @@ void parse_config_file(const char *path) { .directive = "temp_unit", .type = FIGPAR_TYPE_INT, .action = parse_temp_unit_callback, - .value = {.num = DEFAULT_TEMP_UNIT} + .value = {.num = DEFAULT_TEMP_UNIT}, }, { .directive = "bl_pin", .type = FIGPAR_TYPE_INT, .action = parse_int_callback, - .value = {.num = -1} + .value = {.num = -1}, + }, + { + .directive = "export_file_name", + .type = FIGPAR_TYPE_STR, + .action = parse_str_callback, + .value = {.str = strdup_default_opt(DEFAULT_EXPORT_FILE_NAME)}, }, { .directive = NULL }, }; @@ -321,11 +332,11 @@ void parse_config_file(const char *path) { for (size_t i = 0; i < entry_count; ++i) { switch (entries[i].type) { case FIGPAR_TYPE_STR: - FREE_CHECKED(entries[i].value.str); + free(entries[i].value.str); break; case CONFIG_UINT_ARR_TYPE: - FREE_CHECKED(((struct UIntArr *) entries[i].value.data)->arr); - FREE_CHECKED(entries[i].value.data); + free(((struct UIntArr *) entries[i].value.data)->arr); + free(entries[i].value.data); break; default: ; // ignore } diff --git a/src/drive.c b/src/drive.c new file mode 100644 index 0000000..91d3353 --- /dev/null +++ b/src/drive.c @@ -0,0 +1,228 @@ +/* + * drive.c - Simple abstractions for working with file systems and 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 "drive.h" +#include "util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int compare_drives(const Drive *d1, const Drive *d2) { + return strcmp(d1->name, d2->name); +} + +Drive *drive_list_usb_drives(size_t *count) { + DIR *dir = opendir("/dev"); + if (!dir) { + warn("failed to open /dev"); + return NULL; + } + Drive *drives = NULL; + *count = 0; + struct dirent *sd; + while ((sd = readdir(dir))) { + if (strncmp(sd->d_name, "da", 2) == 0 && isdigit(sd->d_name[2])) { + drives = realloc_checked(drives, sizeof(Drive) * ++(*count)); + Drive *cur = &drives[*count - 1]; + cur->name = strdup_checked(sd->d_name); + asprintf_checked(&cur->path, "/dev/%s", sd->d_name); + cur->fs = drive_guess_fs(cur->path); + cur->mnt = NULL; + } + } + closedir(dir); + if (*count) { + qsort(drives, *count, sizeof(Drive), + (int(*)(const void *, const void *))compare_drives); + } + return drives; +} + +void drive_free_drives(Drive *drives, size_t count) { + for (size_t i = 0; i < count; ++i) { + free(drives[i].name); + free(drives[i].path); + free(drives[i].fs); + free(drives[i].mnt); + } + free(drives); +} + +static void close_pipe(void *ptr) { + int *pipe = ptr; + if (pipe[0] >= 0) { + close(pipe[0]); + } + if (pipe[1] >= 0) { + close(pipe[1]); + } +} + +char *drive_guess_fs(const char *dev) { + char *result_fs = NULL; + int ctop[2] = {0, 0}; + pthread_cleanup_push(close_pipe, ctop); + pthread_cleanup_push(free, result_fs); + if (pipe(ctop) < 0) { + warn("failed to create pipe"); + goto cleanup; + } + pid_t pid = fork(); + if (pid < 0) { + warn("fork(2) failed"); + goto cleanup; + } else if (pid == 0) { + // child + close(ctop[0]); // close read end + if (dup2(ctop[1], 1) < 0) { + warn("dup2(2) failed"); + close(ctop[1]); + exit(1); + } + if (!GLOBAL_OPTS.verbose) { + freopen("/dev/null", "w", stderr); // prevent warnings for unknown fs + } + execl("/usr/sbin/fstyp", "/usr/sbin/fstyp", dev, NULL); + close(ctop[1]); + exit(1); + } + // parent + int status; + if (waitpid(pid, &status, 0) < 0) { + warn("waiting for child failed"); + goto cleanup; + } + if (WEXITSTATUS(status) != 0) { + // no error, we probably just don't know the FS type + goto cleanup; + } + char read_buf[32]; + ssize_t read_count = read(ctop[0], read_buf, 32); + if (read_count < 0) { + warn("reading from pipe failed"); + goto cleanup; + } + read_buf[read_count - 1] = '\0'; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + result_fs = strdup_checked(read_buf); + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + cleanup: + pthread_cleanup_pop(false); // dont free result + pthread_cleanup_pop(true); + return result_fs; +} + +bool drive_newfs_msdos(Drive *drive) { + pid_t pid = fork(); + if (pid < 0) { + warn("fork(2) failed"); + return false; + } else if (pid == 0) { + // child + if (!GLOBAL_OPTS.verbose) { + freopen("/dev/null", "w", stdout); + } + execl("/sbin/newfs_msdos", "/sbin/newfs_msdos", "-F32", drive->path, NULL); + warn("execl(2) failed"); + exit(1); + } else { + // parent + int status; + if (waitpid(pid, &status, 0) < 0) { + warn("waiting for child failed"); + return false; + } + if (WEXITSTATUS(status) != 0) { + warnx("creating FAT32 FS failed"); + return false; + } + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + free(drive->fs); + drive->fs = strdup_checked("msdosfs"); + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + return true; + } +} + +bool drive_mount(Drive *drive) { + bool status = true; + char *name = NULL; + pthread_cleanup_push(free, name); + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + name = strdup_checked("/tmp/mntXXXXXX"); + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + if (!mkdtemp(name)) { + warn("failed to create temp mount point"); + status = false; + goto cleanup; + } + pid_t pid = fork(); + if (pid < 0) { + warn("fork(2) failed"); + status = false; + goto cleanup; + } else if (pid == 0) { + // child + if (!GLOBAL_OPTS.verbose) { + freopen("/dev/null", "w", stdout); + } + execl("/sbin/mount", "/sbin/mount", "-t", drive->fs, drive->path, name, NULL); + warn("execl(2) failed"); + abort(); + } + // parent + int exit_code; + if (waitpid(pid, &exit_code, 0) < 0) { + warn("waiting for child failed"); + rmdir(name); + status = false; + goto cleanup; + } + if (WEXITSTATUS(exit_code) != 0) { + warnx("mount(8) failed: \"%s\"", drive->path); + rmdir(name); + status = false; + goto cleanup; + } + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + drive->mnt = strdup_checked(name); + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + LOG_VERBOSE("Mounted \"%s\" with fs %s to \"%s\"\n", drive->path, drive->fs, + drive->mnt); + cleanup: + pthread_cleanup_pop(true); + return status; +} + +bool drive_unmount(Drive *drive, bool force) { + if (!drive->mnt) { + warnx("\"%s\" is not mounted", drive->path); + return false; + } + int flags = force ? MNT_FORCE : 0; + if (unmount(drive->mnt, flags) < 0) { + warn("unmount(2) failed for \"%s\"", drive->path); + return false; + } + rmdir(drive->mnt); + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + free(drive->mnt); + drive->mnt = NULL; + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + LOG_VERBOSE("Unmounted \"%s\"\n", drive->path); + return true; +} diff --git a/src/drive.h b/src/drive.h new file mode 100644 index 0000000..117ad81 --- /dev/null +++ b/src/drive.h @@ -0,0 +1,60 @@ +/* + * drive.h - Simple abstractions for working with file systems and 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. + */ +#ifndef INCLUDED_DRIVE_H +#define INCLUDED_DRIVE_H + +#include +#include + +typedef struct { + char *name; + char *path; + char *fs; + char *mnt; +} Drive; + +/* + * Attempt to list all connected USB drives and their file systems. The list is + * sorted alphabetically. + * Return: all connected USB (actually any serial drive) and their file systems. + */ +Drive *drive_list_usb_drives(size_t *count); + +/* + * Free the array of drives DRIVES. + */ +void drive_free_drives(Drive *drives, size_t count); + +/* + * Use the fstyp(8) utility to try to find the FS for device file DEV. + * Return: The FS name allocated with malloc(3), or NULL if it could not be + * found. + */ +char *drive_guess_fs(const char *dev); + +/* + * Create an MSDOS (FAT32) FS on DRIVE. DRIVE MUST NOT BE MOUNTED! + * Return: true on success, false otherwise + */ +bool drive_newfs_msdos(Drive *drive); + +/* + * Mount DRIVE onto a new temp. dir in /tmp. DRIVE MUST NOT BE MOUNTED! + * Return: true on success, false otherwise + */ +bool drive_mount(Drive *drive); + +/* + * Unmount DRIVE. Force the operation if FORCE. Also rmdir(2) the mount point. + * Return: true of success, false on failure. + */ +bool drive_unmount(Drive *drive, bool force); + +#endif diff --git a/src/main.c b/src/main.c index c3b2c03..0da045b 100644 --- a/src/main.c +++ b/src/main.c @@ -18,6 +18,7 @@ #include "ui/datapoints.h" #include "ui/statrange.h" #include "ui/blankscreen.h" +#include "ui/exportscreen.h" #include #include @@ -105,7 +106,12 @@ int main(int argc, char *const *argv) { screen_manager_add(screen_manager, (Screen *) stats_by_screen_new()); screen_manager_add(screen_manager, (Screen *) data_points_screen_new()); screen_manager_add(screen_manager, (Screen *) stat_range_screen_new()); - screen_manager_add(screen_manager, blank_screen_new()); + if (GLOBAL_OPTS.export_file_name) { + screen_manager_add(screen_manager, (Screen *) export_screen_new()); + } + if (GLOBAL_OPTS.bl_pin >= 0) { + screen_manager_add(screen_manager, blank_screen_new()); + } while (RUNNING) { lock_stat_globals(); uint32_t temp = LAST_TEMP; @@ -142,7 +148,7 @@ void parse_arguments(int argc, char *const *argv) { print_help(argv[0]); exit(0); case 'f': - FREE_CHECKED(GLOBAL_OPTS.config_path); + free(GLOBAL_OPTS.config_path); GLOBAL_OPTS.config_path = optarg; LOG_VERBOSE("Config file path set: \"%s\"\n", optarg); break; @@ -174,13 +180,14 @@ static void exit_signal_callback(int sig) { } } -#define SIGNAL_SETUP_CHECKED(sig) \ -if (signal(sig, exit_signal_callback) == SIG_ERR) {\ +#define SIGNAL_SETUP_CHECKED(sig, func) \ +if (signal(sig, func) == SIG_ERR) {\ err(1, "failed to setup signal %s", #sig);\ } void setup_signals() { - SIGNAL_SETUP_CHECKED(SIGTERM); - SIGNAL_SETUP_CHECKED(SIGINT); + SIGNAL_SETUP_CHECKED(SIGTERM, exit_signal_callback); + SIGNAL_SETUP_CHECKED(SIGINT, exit_signal_callback); + SIGNAL_SETUP_CHECKED(SIGUSR1, SIG_IGN); // used to kill export operations } void open_database() { diff --git a/src/ui/exportscreen.c b/src/ui/exportscreen.c new file mode 100644 index 0000000..83d9ff1 --- /dev/null +++ b/src/ui/exportscreen.c @@ -0,0 +1,403 @@ +/* + * 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 +#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) { + void *status; + if (pthread_peekjoin_np(screen->backgroud_oper, &status) != EBUSY) { + // done + screen->bg_inited = false; + if ((intptr_t) status == EXPORT_SCREEN_FAILED) { + // error happened + warnx("background export operation failed"); + } + screen->stage = (intptr_t) status; + screen->need_redraw = true; + return true; + } + return false; +} + +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; + } + 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, screen->drives[screen->cur_drive].name); + lcd_write_char(state->lcd, ' '); + lcd_write_string(state->lcd, screen->drives[screen->cur_drive].fs); + lcd_move_to(state->lcd, 1, 0); + } + if (state->sel_down) { + screen->need_redraw = true; + if (!screen->drives[screen->cur_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 = EXPORT_CONFIRM_CONTINUE; + if (screen->bg_inited && export_check_bg_task(screen)) { + // background task done + done_status = EXPORT_CONFIRM_CONTINUE; + goto confirm_done; + } + if (state->back_down || (state->sel_down && !screen->confirm_state)) { + done_status = EXPORT_CONFIRM_NO; + screen->need_redraw = true; + 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; + 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_clear(state->lcd); + lcd_move_to(state->lcd, 0, 0); + lcd_write_string(state->lcd, message); + screen->need_redraw = false; + } + return false; +} + +static intptr_t export_newfs_action(ExportScreen *screen) { + if (drive_newfs_msdos(&screen->drives[screen->cur_drive])) { + return EXPORT_SCREEN_MOUNTING; + } else { + return EXPORT_SCREEN_FAILED; + } +} + +static intptr_t export_mount_action(ExportScreen *screen) { + Drive *drive = &screen->drives[screen->cur_drive]; + if (drive_mount(drive)) { + struct stat statbuf; + char *path = NULL; + int exist; + pthread_cleanup_push(free, path); + path = export_build_cur_path(screen); + exist = stat(path, &statbuf); + screen->need_overwrite = exist == 0; + pthread_cleanup_pop(true); // free(path) + return screen->need_overwrite ? EXPORT_SCREEN_CONFIRM_OVERWRITE : + EXPORT_SCREEN_WRITING; + } else { + return EXPORT_SCREEN_FAILED; + } +} + +static intptr_t export_write_action(ExportScreen *screen) { + char *path = NULL; + intptr_t next_target = EXPORT_SCREEN_DONE; + pthread_cleanup_push(free, path); + path = export_build_cur_path(screen); + if (screen->need_overwrite && !recursive_rm(path)) { + next_target = EXPORT_SCREEN_FAILED; + goto cleanup; + } + LOG_VERBOSE("Exporting to \"%s\"\n", path); + switch (screen->format) { + case EXPORT_FORMAT_SQLITE: + if (!export_database(screen->db_cache, path)) { + warnx("export to sqlite3 db failed: \"%s\"", path); + next_target = EXPORT_SCREEN_FAILED; + } + break; + case EXPORT_FORMAT_CSV: + if (!export_database_as_csv(screen->db_cache, path)) { + warnx("export to CSV failed: \"%s\"", path); + next_target = EXPORT_SCREEN_FAILED; + } + break; + default: + warnx("unknown export format"); + next_target = EXPORT_SCREEN_FAILED; + break; + } + LOG_VERBOSE("Export done\n"); + cleanup: + pthread_cleanup_pop(true); // free(path) + drive_unmount(&screen->drives[screen->cur_drive], true); + return next_target; +} + +static const char BACKGROUND_CHARS[] = { + '|', + '-' +}; +#define NBACKGROUND_CHAR sizeof(BACKGROUND_CHARS) + +static void export_do_background_action(ExportScreen *screen, + SensorState *state, + intptr_t(*action)(ExportScreen *screen)) { + if (!screen->bg_inited) { + if (pthread_create(&screen->backgroud_oper, NULL, + (void*(*)(void *)) action, screen)) { + warnx("could not start background export operation"); + screen->stage = EXPORT_SCREEN_FAILED; + } + screen->bg_inited = true; + screen->working_char = 0; + } + if (state->down_down || state->up_down || state->back_down || + state->sel_down) { + screen->last_cancel_stage = screen->stage; + screen->stage = EXPORT_SCREEN_CONFIRM_CANCEL; + screen->need_redraw = true; + return; + } + time_t now = time(NULL); + if (export_check_bg_task(screen)) { + // bg task done + return; + } + 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]); + } +} + +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); + if (screen->ndrive == 0) { + screen->stage = EXPORT_SCREEN_NO_DRIVES; + screen->need_redraw =true; + } else { + screen->cur_drive = 0; + screen->read_drives = false; + } + } + 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: + export_do_background_action(screen, state, export_newfs_action); + break; + case EXPORT_SCREEN_MOUNTING: + export_do_background_action(screen, state, export_mount_action); + 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 + drive_unmount(cur_drive, true); + } + break; + case EXPORT_SCREEN_WRITING: + export_do_background_action(screen, state, export_write_action); + break; + case EXPORT_SCREEN_CONFIRM_CANCEL: + confirm_status = export_confirm_action(screen, state, "Really cancel?"); + if (confirm_status == EXPORT_CONFIRM_YES) { + kill(0, SIGUSR1); + pthread_cancel(screen->backgroud_oper); + drive_unmount(cur_drive, true); + screen->stage = EXPORT_SCREEN_CANCELED; + screen->need_redraw = true; + } else if (confirm_status == EXPORT_CONFIRM_NO) { + screen->stage = screen->last_cancel_stage; + 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; +} + +ExportScreen *export_screen_new() { + ExportScreen *s = malloc_checked(sizeof(ExportScreen)); + screen_init(&s->parent, "Export", + (ScreenDispatchFunc) export_screen_dispatch, + (ScreenCleanupFunc) free); + s->read_drives = true; + s->stage = EXPORT_SCREEN_FORMAT; + s->format = EXPORT_FORMAT_SQLITE; + return s; +} diff --git a/src/ui/exportscreen.h b/src/ui/exportscreen.h new file mode 100644 index 0000000..5a7cd40 --- /dev/null +++ b/src/ui/exportscreen.h @@ -0,0 +1,61 @@ +/* + * exportscreen.h - 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. + */ +#ifndef INCLUDED_EXPORTSCREEN_H +#define INCLUDED_EXPORTSCREEN_H + +#include "screen.h" +#include "../drive.h" + +#include +#include +#include +#include + +typedef struct { + Screen parent; + enum { + EXPORT_SCREEN_FORMAT = 0, + EXPORT_SCREEN_DRIVE, + EXPORT_SCREEN_CONFIRM_NEWFS, + EXPORT_SCREEN_WRITE_NEWFS, + EXPORT_SCREEN_MOUNTING, + EXPORT_SCREEN_CONFIRM_OVERWRITE, + EXPORT_SCREEN_WRITING, + EXPORT_SCREEN_DONE, + EXPORT_SCREEN_CONFIRM_CANCEL, + EXPORT_SCREEN_CANCELED, + EXPORT_SCREEN_FAILED, + EXPORT_SCREEN_NO_DRIVES, + } stage; + enum { + EXPORT_FORMAT_SQLITE = 0, + EXPORT_FORMAT_CSV, + EXPORT_NFORMAT, + }; + int format; + bool need_redraw; + + bool read_drives; + Drive *drives; + size_t ndrive; + size_t cur_drive; + int confirm_state; + bool bg_inited; + pthread_t backgroud_oper; + int working_char; + time_t last_char_change; + bool need_overwrite; + sqlite3 *db_cache; + int last_cancel_stage; +} ExportScreen; + +ExportScreen *export_screen_new(void); + +#endif diff --git a/src/ui/screen.c b/src/ui/screen.c index 1fb50f3..bb3dd2a 100644 --- a/src/ui/screen.c +++ b/src/ui/screen.c @@ -24,7 +24,7 @@ void screen_init(Screen *screen, const char *name, } void screen_delete(Screen *screen) { - FREE_CHECKED(screen->name); + free(screen->name); if (screen->cleanup_func) { screen->cleanup_func(screen); } @@ -53,7 +53,7 @@ void screen_manager_delete(ScreenManager *sm) { for (size_t i = 0; i < sm->screen_count; ++i) { screen_delete(sm->screens[i]); } - FREE_CHECKED(sm->screens); + free(sm->screens); button_delete(sm->up_btn); button_delete(sm->down_btn); button_delete(sm->back_btn); @@ -115,6 +115,7 @@ void screen_manager_dispatch(ScreenManager *sm, uint32_t temp, uint32_t humid) { if (cs->dispatch_func) { SensorState state = { .lcd = sm->lcd, + .db = sm->db, .temp = temp, .humid = humid, .back_down = button_pressed(sm->back_btn), diff --git a/src/util.c b/src/util.c index e1d5cc4..ef47e61 100644 --- a/src/util.c +++ b/src/util.c @@ -12,6 +12,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include const char *PERIOD_LABELS[] = { "HOUR", @@ -25,12 +31,13 @@ const size_t NPERIOD = sizeof(PERIOD_LABELS) / sizeof(char *); Options GLOBAL_OPTS; void cleanup_options(Options *opts) { - FREE_CHECKED(opts->config_path); - FREE_CHECKED(opts->gpio_path); - FREE_CHECKED(opts->temp_key); - FREE_CHECKED(opts->humid_key); - FREE_CHECKED(opts->fail_key); - FREE_CHECKED(opts->database_location); + free(opts->config_path); + free(opts->gpio_path); + free(opts->temp_key); + free(opts->humid_key); + free(opts->fail_key); + free(opts->database_location); + free(opts->export_file_name); } void *malloc_checked(size_t n) { @@ -56,6 +63,17 @@ void *strdup_checked(const char *str) { return new_str; } +int asprintf_checked(char **str, const char *format, ...) { + va_list args; + va_start(args, format); + int retval = vasprintf(str, format, args); + if (!*str) { + errx(1, "out of memory!"); + } + va_end(args); + return retval; +} + int days_in_month(int m, int y) { switch (m) { case 2: @@ -160,6 +178,9 @@ static const char *AVG_FOR_RANGE_QUERY_STR = " max(temp), min(temp), max(humid), min(humid) FROM env_data\n" "WHERE time >= ?1 AND time <= ?2;"; static sqlite3_stmt *AVG_FOR_RANGE_QUERY; +static const char *EXPORT_CSV_QUERY_STR = + "select printf('%d,%d,%d', time, temp, humid) from env_data;"; +static sqlite3_stmt *EXPORT_CSV_QUERY; void initialize_util_queries(sqlite3 *db) { int status = sqlite3_prepare_v2(db, DB_LIMITS_QUERY_STR, -1, &DB_LIMITS_QUERY, NULL); @@ -181,6 +202,11 @@ void initialize_util_queries(sqlite3 *db) { if (status != SQLITE_OK) { errx(1, "failed to compile range average query: %s", sqlite3_errstr(status)); } + status = sqlite3_prepare_v2(db, EXPORT_CSV_QUERY_STR, -1, + &EXPORT_CSV_QUERY, NULL); + if (status != SQLITE_OK) { + errx(1, "failed to compile CSV export query: %s", sqlite3_errstr(status)); + } } void cleanup_util_queries() { @@ -188,6 +214,7 @@ void cleanup_util_queries() { sqlite3_finalize(AVG_FOR_PERIOD_QUERY); sqlite3_finalize(DATA_POINT_QUERY); sqlite3_finalize(AVG_FOR_RANGE_QUERY); + sqlite3_finalize(EXPORT_CSV_QUERY); } bool get_database_limits(sqlite3 *db, UtilPeriod period, UtilDate *start, @@ -379,3 +406,124 @@ char *pad_humid_str(int humid, char *buf, size_t buf_size) { } return buf; } + +static void close_dir_if_set(void *dir) { + if (dir) { + closedir(dir); + } +} + +bool recursive_rm_at(int dfd, const char *path) { + if (dfd != AT_FDCWD) { + LOG_VERBOSE("Recursive delete: \"%s\"\n", path); + } + struct stat statbuf; + if (fstatat(dfd, path, &statbuf, 0) < 0) { + warn("could not stat \"%s\" for removal", path); + return false; + } + int flag = 0; + if (S_ISDIR(statbuf.st_mode)) { + flag = AT_REMOVEDIR; + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + int fd = openat(dfd, path, O_DIRECTORY | O_RDONLY); + if (fd < 0) { + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + warn("open \"%s\"", path); + return false; + } + DIR *dir = NULL; + bool error = false; + pthread_cleanup_push(close_dir_if_set, dir); + dir = fdopendir(fd); + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + if (!dir) { + close(fd); + warn("fdopendir \"%s\"", path); + return false; + } + struct dirent *sd; + while ((sd = readdir(dir))) { + if (strcmp(sd->d_name, ".") != 0 && strcmp(sd->d_name, "..") != 0 && + !recursive_rm_at(dirfd(dir), sd->d_name)) { + error = true; + break; + } + } + pthread_cleanup_pop(true); + if (error) { + return false; + } + } + if (unlinkat(dfd, path, flag)< 0) { + warn("remove \"%s\"", path); + return false; + } + return true; +} + +bool recursive_rm(const char *path) { + return recursive_rm_at(AT_FDCWD, path); +} + +bool export_database(sqlite3 *db, const char *dest) { + bool status = true; + sqlite3 *new_db = NULL; + sqlite3_backup *oper = NULL; + pthread_cleanup_push((void(*)(void*))sqlite3_close, new_db); + pthread_cleanup_push((void(*)(void*))sqlite3_backup_finish, oper); + int res = sqlite3_open(dest, &new_db); + if (res != SQLITE_OK) { + warnx("could not open new db: \"%s\": %s", dest, + sqlite3_errstr(res)); + status = false; + goto cleanup; + } + oper = sqlite3_backup_init(new_db, "main", db, "main"); + if (!oper) { + warnx("could not start backup: \"%s\": %s", dest, + sqlite3_errstr(res)); + status = false; + goto cleanup; + } + sqlite3_backup_step(oper, -1); + cleanup: + pthread_cleanup_pop(true); // cleanup oper + pthread_cleanup_pop(true); // cleanup new_db + return status; +} + +static void cleanup_file_obj(void *obj) { + if (obj) { + fclose(obj); + } +} + +bool export_database_as_csv(sqlite3 *db, const char *dest) { + bool status = true; + FILE *outfile = NULL; + pthread_cleanup_push((void(*)(void *))sqlite3_reset, EXPORT_CSV_QUERY); + pthread_cleanup_push(cleanup_file_obj, outfile); + outfile = fopen(dest, "w"); + if (!outfile) { + warn("fopen \"%s\"", dest); + status = false; + goto cleanup; + } + int res; + while ((res = sqlite3_step(EXPORT_CSV_QUERY)) == SQLITE_ROW) { + const unsigned char *test = sqlite3_column_text(EXPORT_CSV_QUERY, 0); + fputs((const char *) test, outfile); + fputc('\n', outfile); // avoid flush + } + if (res != SQLITE_DONE) { + warnx("CSV export to \"%s\" faield: %s\n", dest, + sqlite3_errstr(res)); + status = false; + goto cleanup; // readability + } + cleanup: + pthread_cleanup_pop(true); // reset query + pthread_cleanup_pop(true); // close file + return status; +} diff --git a/src/util.h b/src/util.h index 1f78ad6..f9bb137 100644 --- a/src/util.h +++ b/src/util.h @@ -49,6 +49,8 @@ typedef struct { gpio_pin_t sel_pin; TemperatureUnit temp_unit; + + char *export_file_name; // file to export to } Options; extern Options GLOBAL_OPTS; @@ -76,9 +78,11 @@ void *realloc_checked(void *old_ptr, size_t n); void *strdup_checked(const char *str); /* - * Like free(3), but do nothing if P is NULL. + * Like asprintf(3), except that if allocation fails, an error will be written to + * standard error and abort(3) called */ -#define FREE_CHECKED(p) if (p) {free(p);} +int asprintf_checked(char **str, const char *format, ...) +__attribute__((format(printf, 2, 3))); /* * Call fprintf(3) to stderr only if verbose mode was enabled. @@ -210,4 +214,29 @@ float convert_temperature(int dk); */ char *pad_humid_str(int humid, char *buf, size_t buf_size); +/* + * Like recursive_rm (see below), except that PATH is resolved relative to DFD. + */ +bool recursive_rm_at(int dfd, const char *path); + +/* + * Call recursive_rm on all of FILE's children, the remove FILE + * Return: false on error, true otherwise + */ +bool recursive_rm(const char *path); + +/* + * Use the sqlite3 backup API to export DB to dest. This function is safe to + * call in a multi-threaded environment where pthread_cancel may be used. + * Return: true on success, false on error + */ +bool export_database(sqlite3 *db, const char *dest); + +/* + * Export DB as a CSV file to DEST. This function is safe to call in a + * multi-threaded environment where pthread_cancel may be used. + * Return: true on success, false on error + */ +bool export_database_as_csv(sqlite3 *db, const char *dest); + #endif