Add export screen

This commit is contained in:
Alexander Rosenberg 2024-04-04 00:53:29 -07:00
parent 961179db27
commit 6ae67c4732
Signed by: school-rpi4
GPG Key ID: 5CCFC80B0B47B04B
11 changed files with 977 additions and 24 deletions

View File

@ -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:

View File

@ -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

View File

@ -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
}

228
src/drive.c Normal file
View File

@ -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 <ctype.h>
#include <err.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <dirent.h>
#include <sys/param.h>
#include <sys/mount.h>
#include <pthread.h>
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;
}

60
src/drive.h Normal file
View File

@ -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 <stddef.h>
#include <stdbool.h>
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

View File

@ -18,6 +18,7 @@
#include "ui/datapoints.h"
#include "ui/statrange.h"
#include "ui/blankscreen.h"
#include "ui/exportscreen.h"
#include <unistd.h>
#include <err.h>
@ -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());
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() {

403
src/ui/exportscreen.c Normal file
View File

@ -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 <err.h>
#include <errno.h>
#include <sys/types.h>
#include <signal.h>
#include <stdarg.h>
#include <pthread.h>
#include <pthread_np.h>
#include <unistd.h>
#include <sys/stat.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) {
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;
}

61
src/ui/exportscreen.h Normal file
View File

@ -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 <pthread.h>
#include <time.h>
#include <stdatomic.h>
#include <sqlite3.h>
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

View File

@ -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),

View File

@ -12,6 +12,12 @@
#include <err.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
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;
}

View File

@ -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