diff --git a/Makefile b/Makefile index 44a5cbb..b7b3cda 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ 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/drive.c\ src/ui/exportscreen.c src/ui/setdatescreen.c src/ui/viewdatescreen.c\ - src/ui/powerscreen.c + src/ui/powerscreen.c src/ui/settzscreen.c src/ui/cleardatascreen.c PROG=rpi4b-temp-humidity all: config.conf bin/${PROG} @@ -24,16 +24,19 @@ bin/main.o bin/util.o bin/lcd.o bin/ths.o bin/button.o: src/util.h bin/ui/screen.o bin/ui/statsby.o bin/ui/datesel.o: src/util.h bin/ui/datapoints.o bin/ui/timesel.o bin/ui/statrange.o: src/util.h bin/ui/viewdatescreen.o bin/ui/setdatescreen.o bin/ui/powerscreen.o: src/util.h +bin/ui/settzscreen.o bin/ui/cleardatascreen.o: src/util.h bin/main.o bin/lcd.o bin/screen.o bin/ui/datesel.o: src/lcd.h bin/ui/statsby.o bin/ui/timesel.o bin/ui/statrange.o: src/lcd.h bin/ui/datapoints.o bin/ui/viewdatescreen.o: src/lcd.h bin/ui/setdatescreen.o bin/ui/powerscreen.o: src/lcd.h +bin/ui/settzscreen.o bin/ui/cleardatascreen.o: src/lcd.h bin/main.o bin/ui/screen.o bin/ui/statsby.o: src/ui/screen.h bin/ui/datesel.o bin/ui/datapoints.o bin/ui/timesel.o: src/ui/screen.h bin/ui/statrange.o bin/ui/setdatescreen.o: src/ui/screen.h bin/ui/viewdatescreen.o bin/ui/powerscreen.o: src/ui/screen.h +bin/ui/settzscreen.o bin/ui/cleardatascreen.o: src/ui/screen.h bin/main.o bin/ui/datesel.o bin/ui/statsby.o: src/ui/datesel.h bin/ui/datapoints.o bin/ui/timesel.o bin/ui/statrange.o: src/ui/datesel.h @@ -53,6 +56,8 @@ bin/main.o bin/ui/exportscreen.o: src/ui/exportscreen.h bin/main.o bin/ui/setdatescreen.o: src/ui/setdatescreen.h bin/main.o bin/ui/viewdatescreen.o: src/ui/viewdatescreen.h bin/main.o bin/ui/powerscreen.o: src/ui/powerscreen.h +bin/main.o bin/ui/settzscreen.o: src/ui/settzscreen.h +bin/main.o bin/ui/cleardatascreen.o: src/ui/cleardatascreen.h bin/main.o bin/drive.o bin/ui/exportscreen.o: src/drive.h .if "${DEFAULT_TEMP_UNIT}" != "F" && "${DEFAULT_TEMP_UNIT}" != "C" &&\ @@ -71,6 +76,7 @@ DEFINES:=\ -DDEFAULT_REFRESH_TIME="${DEFAULT_REFRESH_TIME}"\ -DDEFAULT_TEMP_UNIT="${DEFAULT_TEMP_UNIT}"\ -DDEFAULT_EXPORT_FILE_NAME="${DEFAULT_EXPORT_FILE_NAME}"\ +-DDEFAULT_TIMEZONE="${DEFAULT_TIMEZONE}"\ ${OBJS}: ${.TARGET:C/^bin/src/:C/.o$/.c/} @mkdir -p ${.TARGET:H} diff --git a/config.conf.m4 b/config.conf.m4 index d6aee03..fb00193 100644 --- a/config.conf.m4 +++ b/config.conf.m4 @@ -47,3 +47,8 @@ down_pin = 23 # The base name for export, .sqlite3 will be appended for SQLite exports and # .csv will be appended for CSV exports #export_file_name = DEFAULT_EXPORT_FILE_NAME + +# The timezone used. This is overridden by the environment variable TZ. See the +# tzset(3) manual page for more information about the format of this. Leave +# blank for system default +#timezone = DEFAULT_TIMEZONE diff --git a/config.mk b/config.mk index f770f2c..0f51d36 100644 --- a/config.mk +++ b/config.mk @@ -34,3 +34,8 @@ DEFAULT_TEMP_UNIT=F # The base name for export, .sqlite3 will be appended for SQLite exports and # .csv will be appended for CSV exports DEFAULT_EXPORT_FILE_NAME=env_data + +# The timezone used. This is overridden by the environment variable TZ. See the +# tzset(3) manual page for more information about the format of this. Leave +# blank for system default +DEFAULT_TIMEZONE= diff --git a/src/config.c b/src/config.c index 6768da9..02eddef 100644 --- a/src/config.c +++ b/src/config.c @@ -205,6 +205,10 @@ static void set_options_from_entries(struct figpar_config *entries, 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); + + entries[18].type = FIGPAR_TYPE_NONE; + GLOBAL_OPTS.timezone = steal_opt_if_set(entries[18].value.str); + LOG_VERBOSE("Using timezone: \"%s\"\n", GLOBAL_OPTS.timezone); } static char *strdup_default_opt(const char *def) { @@ -317,6 +321,12 @@ void parse_config_file(const char *path) { .action = parse_str_callback, .value = {.str = strdup_default_opt(STRINGIFY(DEFAULT_EXPORT_FILE_NAME))}, }, + { + .directive = "timezone", + .type = FIGPAR_TYPE_STR, + .action = parse_str_callback, + .value = {.str = strdup_default_opt(STRINGIFY(DEFAULT_TIMEZONE))}, + }, { .directive = NULL }, }; size_t entry_count = sizeof(entries) / sizeof(struct figpar_config); diff --git a/src/main.c b/src/main.c index 74fd4dd..7bb50ce 100644 --- a/src/main.c +++ b/src/main.c @@ -22,6 +22,8 @@ #include "ui/viewdatescreen.h" #include "ui/setdatescreen.h" #include "ui/powerscreen.h" +#include "ui/settzscreen.h" +#include "ui/cleardatascreen.h" #include #include @@ -84,9 +86,22 @@ void lock_stat_globals(void); */ #define unlock_stat_globals() pthread_mutex_unlock(&STAT_MUTEX) +// should we restart instead of exiting +bool SHOULD_RESTART = false; +bool NEED_CLEAR_TZ = false; + int main(int argc, char *const *argv) { parse_arguments(argc, argv); parse_config_file(GLOBAL_OPTS.config_path); + if (GLOBAL_OPTS.timezone) { + if (!getenv("TZ")) { + setenv("TZ", GLOBAL_OPTS.timezone, true); + NEED_CLEAR_TZ = true; + } else { + LOG_VERBOSE("Config timezone option shadowed by local environment variable\n"); + } + } + tzset(); gpio_handle_t handle = gpio_open_device(GLOBAL_OPTS.gpio_path); if (handle == GPIO_INVALID_HANDLE) { @@ -121,9 +136,11 @@ int main(int argc, char *const *argv) { if (GLOBAL_OPTS.bl_pin >= 0) { screen_manager_add(screen_manager, blank_screen_new()); } + screen_manager_add(screen_manager, (Screen *) power_screen_new()); screen_manager_add(screen_manager, (Screen *) view_date_screen_new()); screen_manager_add(screen_manager, (Screen *) set_date_screen_new()); - screen_manager_add(screen_manager, (Screen *) power_screen_new()); + screen_manager_add(screen_manager, (Screen *) set_tz_screen_new()); + screen_manager_add(screen_manager, (Screen *) clear_data_screen_new()); while (RUNNING) { lock_stat_globals(); uint32_t temp = LAST_TEMP; @@ -133,8 +150,6 @@ int main(int argc, char *const *argv) { // 10ms is probably faster than anyone will press a button usleep(10 * 1000); } - // this is mostly about LCD and DB cleanup, not freeing memory, this leaks a - // lot when caused from something other than the main screen screen_manager_delete(screen_manager); lcd_close(lcd); if (pthread_join(bg_update, NULL) != 0) { @@ -147,6 +162,14 @@ int main(int argc, char *const *argv) { pthread_mutex_destroy(&STAT_MUTEX); gpio_close(handle); cleanup_options(&GLOBAL_OPTS); + if (SHOULD_RESTART) { + LOG_VERBOSE("Re-invoking...\n"); + if (NEED_CLEAR_TZ) { + unsetenv("TZ"); + } + execv(argv[0], argv); + err(1, "re-invoke with execv(2) failed"); + } return 0; } @@ -263,8 +286,13 @@ void update_stats(THS *ths, sqlite3_stmt *insert_statement) { sqlite3_bind_int(insert_statement, 3, humid); int status = sqlite3_step(insert_statement); if (status != SQLITE_DONE) { - errx(1, "failed to insert temp. and humid. data into database: %s", - sqlite3_errstr(status)); + warnx("failed to insert temp. and humid. data into database: %s", + sqlite3_errstr(status)); + lock_stat_globals(); + // this means error + LAST_TEMP = UINT32_MAX; + LAST_HUMID = UINT32_MAX; + unlock_stat_globals(); } } diff --git a/src/ui/cleardatascreen.c b/src/ui/cleardatascreen.c new file mode 100644 index 0000000..6c7bdf5 --- /dev/null +++ b/src/ui/cleardatascreen.c @@ -0,0 +1,72 @@ +/* + * cleardatascreen.c - Screen for rebooting or halting + * 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 "cleardatascreen.h" + +#include + +static bool clear_data_screen_display(ClearDataScreen *screen, + SensorState *state) { + if (state->back_down || (!screen->choice && state->sel_down) || + (screen->done && (state->sel_down || state->up_down || + state->down_down))) { + screen->done = false; + screen->error = false; + lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, + LCD_DISPLAY_ON); + return true; + } + if (state->sel_down) { + // TODO clear data + char *errmsg; + sqlite3_exec(state->db, "DELETE FROM env_data;", NULL, NULL, &errmsg); + if (errmsg) { + warnx("sqlite3 error when clearing database: \"%s\"\n", errmsg); + sqlite3_free(errmsg); + screen->error = true; + } else { + LOG_VERBOSE("Database cleared!\n"); + } + screen->need_redraw = true; + screen->done = true; + return false; + } + if (screen->need_redraw || state->force_draw || state->up_down || + state->down_down) { + screen->need_redraw = false; + screen->choice = abs((screen->choice + state->up_down - + state->down_down) % 2); + lcd_clear(state->lcd); + lcd_move_to(state->lcd, 0, 0); + if (screen->done) { + lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, + LCD_DISPLAY_ON); + lcd_write_string(state->lcd, screen->error ? "Error!" : "Done!"); + } else { + lcd_write_string(state->lcd, "Really clear?"); + lcd_move_to(state->lcd, 1, 0); + lcd_write_string(state->lcd, ">No >Yes"); + lcd_move_to(state->lcd, 1, 4 * screen->choice); + lcd_display_control(state->lcd, LCD_CURSOR_BLINK, LCD_CURSOR_ON, + LCD_DISPLAY_ON); + } + } + return false; +} + +ClearDataScreen *clear_data_screen_new() { + ClearDataScreen *s = malloc_checked(sizeof(ClearDataScreen)); + screen_init(&s->parent, "Clear data", + (ScreenDispatchFunc) clear_data_screen_display, + (ScreenCleanupFunc) free); + s->choice = 0; + s->need_redraw = false; + s->done = false; + return s; +} diff --git a/src/ui/cleardatascreen.h b/src/ui/cleardatascreen.h new file mode 100644 index 0000000..df68e53 --- /dev/null +++ b/src/ui/cleardatascreen.h @@ -0,0 +1,29 @@ +/* + * cleardatascreen.c - Screen for rebooting or halting + * 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_CLEARDATASCREEN_H +#define INCLUDED_CLEARDATASCREEN_H + +#include "screen.h" + +typedef struct { + Screen parent; + int choice; + bool done; + bool need_redraw; + bool error; +} ClearDataScreen; + +/* + * Create a new ClearDataScreen that will allow the user to wipe data collected + * by the device. + */ +ClearDataScreen *clear_data_screen_new(void); + +#endif diff --git a/src/ui/exportscreen.c b/src/ui/exportscreen.c index c008a19..6deba6f 100644 --- a/src/ui/exportscreen.c +++ b/src/ui/exportscreen.c @@ -375,7 +375,9 @@ static bool export_screen_dispatch(ExportScreen *screen, if (confirm_status == EXPORT_CONFIRM_YES) { kill(0, SIGUSR1); pthread_cancel(screen->backgroud_oper); + pthread_detach(screen->backgroud_oper); drive_unmount(cur_drive, true); + screen->bg_inited = false; screen->stage = EXPORT_SCREEN_CANCELED; screen->need_redraw = true; } else if (confirm_status == EXPORT_CONFIRM_NO) { @@ -395,11 +397,18 @@ static bool export_screen_dispatch(ExportScreen *screen, return false; } +static void export_screen_cleanup(ExportScreen *screen) { + if (screen->bg_inited) { + pthread_cancel(screen->backgroud_oper); + pthread_detach(screen->backgroud_oper); + } +} + ExportScreen *export_screen_new() { ExportScreen *s = malloc_checked(sizeof(ExportScreen)); screen_init(&s->parent, "Export", (ScreenDispatchFunc) export_screen_dispatch, - (ScreenCleanupFunc) free); + (ScreenCleanupFunc) export_screen_cleanup); s->read_drives = true; s->stage = EXPORT_SCREEN_FORMAT; s->format = EXPORT_FORMAT_SQLITE; diff --git a/src/ui/powerscreen.c b/src/ui/powerscreen.c index deb2fd7..ee5b7cf 100644 --- a/src/ui/powerscreen.c +++ b/src/ui/powerscreen.c @@ -18,11 +18,14 @@ static bool power_screen_dispatch(PowerScreen *screen, if (state->back_down) { lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, LCD_DISPLAY_ON); - screen->choice = POWER_SCREEN_REBOOT; + screen->choice = POWER_SCREEN_RESTART_PROGRAM; return true; } if (state->sel_down) { switch (screen->choice) { + case POWER_SCREEN_RESTART_PROGRAM: + request_restart(); + break; case POWER_SCREEN_REBOOT: LOG_VERBOSE("Rebooting..."); reboot(RB_AUTOBOOT); @@ -51,6 +54,9 @@ static bool power_screen_dispatch(PowerScreen *screen, lcd_move_to(state->lcd, 1, 0); lcd_write_char(state->lcd, '>'); switch (screen->choice) { + case POWER_SCREEN_RESTART_PROGRAM: + lcd_write_string(state->lcd, "REINIT"); + break; case POWER_SCREEN_REBOOT: lcd_write_string(state->lcd, "REBOOT"); break; @@ -70,6 +76,6 @@ PowerScreen *power_screen_new(void) { screen_init(&s->parent, "Power options", (ScreenDispatchFunc) power_screen_dispatch, (ScreenCleanupFunc) free); - s->choice = POWER_SCREEN_REBOOT; + s->choice = POWER_SCREEN_RESTART_PROGRAM; return s; } diff --git a/src/ui/powerscreen.h b/src/ui/powerscreen.h index 4bfcc72..f4a1ee3 100644 --- a/src/ui/powerscreen.h +++ b/src/ui/powerscreen.h @@ -15,7 +15,8 @@ typedef struct { Screen parent; enum { - POWER_SCREEN_REBOOT = 0, + POWER_SCREEN_RESTART_PROGRAM = 0, + POWER_SCREEN_REBOOT, POWER_SCREEN_POWEROFF, POWER_SCREEN_NCHOICE, }; diff --git a/src/ui/screen.c b/src/ui/screen.c index bb3dd2a..e8ce3d0 100644 --- a/src/ui/screen.c +++ b/src/ui/screen.c @@ -149,9 +149,14 @@ static bool stats_screen_dispatch(StatsScreen *screen, SensorState *state) { lcd_clear(state->lcd); lcd_write_string(state->lcd, "temp humi time"); char buff[17]; - int cur_len = snprintf(buff, sizeof(buff), "%-4.1f%c %3" PRIu32 "%% ", + int cur_len; + if (state->temp == UINT32_MAX || state->humid == UINT32_MAX) { + cur_len = snprintf(buff, sizeof(buff), "ERROR! "); + } else { + cur_len = snprintf(buff, sizeof(buff), "%-4.1f%c %3" PRIu32 "%% ", convert_temperature(state->temp), GLOBAL_OPTS.temp_unit, state->humid); + } struct tm lt; localtime_r(&cur_time, <); strftime(buff + cur_len, sizeof(buff) - cur_len, diff --git a/src/ui/settzscreen.c b/src/ui/settzscreen.c new file mode 100644 index 0000000..d742295 --- /dev/null +++ b/src/ui/settzscreen.c @@ -0,0 +1,585 @@ +/* + * settzscreen.c - Screen for setting the timezone + * 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 "settzscreen.h" + +#include +#include +#include + +extern char **environ; + +static void set_tz_screen_reset(SetTZScreen *screen, SensorState *state) { + screen->has_dst = false; + screen->stage = SET_TZ_OFFSET; + screen->off_sel_stage = TZ_OFFSET_SEL_HOUR; + screen->bound_sel_stage = DST_BOUND_SEL_TYPE; + tz_offset_reset(&screen->offset); + tz_offset_reset(&screen->dst_offset); + screen->did_set = false; + if (state) { + lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, + LCD_DISPLAY_ON); + } +} + +static void set_tz_offset_normalize(TZOffset *offset, bool allow_negative) { + if (offset->second < 0) { + offset->second = 59; + --offset->minute; + } else if (offset->second > 59) { + offset->second = 0; + ++offset->minute; + } + if (offset->minute < 0) { + offset->minute = 59; + --offset->hour; + } else if (offset->minute > 59) { + offset->minute = 0; + ++offset->hour; + } + if (allow_negative) { + if (offset->hour < -24) { + offset->hour = 24; + } else if (offset->hour > 24) { + offset->hour = -24; + } + } else { + if (offset->hour < 0) { + offset->hour = 24; + } else if (offset->hour > 24) { + offset->hour = 0; + } + } +} + +static SetTZScreenAction set_tz_dispatch_select_offset(TZOffsetSelectStage *stage, + SensorState *state, + TZOffset *target, + const char *label, + bool force_redraw, + bool allow_negative) { + if (state->back_down) { + if (*stage == TZ_OFFSET_SEL_HOUR) { + *stage = TZ_OFFSET_SEL_SECOND; + return SET_TZ_BACK; + } + --*stage; + force_redraw = true; + } + if (state->up_down || state->down_down) { + force_redraw = true; + int change = state->up_down - state->down_down; + switch (*stage) { + case TZ_OFFSET_SEL_HOUR: + target->hour += change; + break; + case TZ_OFFSET_SEL_MINUTE: + target->minute += change; + break; + case TZ_OFFSET_SEL_SECOND: + target->second += change; + break; + } + set_tz_offset_normalize(target, allow_negative); + } + if (state->sel_down) { + if (*stage == TZ_OFFSET_SEL_SECOND) { + *stage = TZ_OFFSET_SEL_HOUR; + return SET_TZ_NEXT; + } + ++*stage; + force_redraw = true; + } + if (force_redraw) { + const char *format; + int cursor_pos; + switch (*stage) { + case TZ_OFFSET_SEL_HOUR: + format = ">%0*d:%02d:%02d"; + cursor_pos = 0; + break; + case TZ_OFFSET_SEL_MINUTE: + format = "%0*d:>%02d:%02d"; + cursor_pos = target->hour < 0 ? 4 : 3; + break; + case TZ_OFFSET_SEL_SECOND: + format = "%0*d:%02d:>%02d"; + cursor_pos = target->hour < 0 ? 7 : 6; + break; + default: + format = ""; + cursor_pos = 0; + break; + } + char time_str[17]; + snprintf(time_str, 17, format, target->hour < 0 ? 3 : 2, target->hour, + target->minute, target->second); + lcd_clear(state->lcd); + lcd_move_to(state->lcd, 0, 0); + lcd_write_string(state->lcd, label); + lcd_move_to(state->lcd, 1, 0); + lcd_write_string(state->lcd, time_str); + lcd_move_to(state->lcd, 1, cursor_pos); + lcd_display_control(state->lcd, LCD_CURSOR_BLINK, LCD_CURSOR_ON, + LCD_DISPLAY_ON); + } + return SET_TZ_CONTINUE; +} + +static void set_tz_prompt_has_dst(SetTZScreen *screen, + SensorState *state) { + if (state->back_down) { + --screen->stage; + screen->need_redraw = true; + screen->off_sel_stage = TZ_OFFSET_SEL_SECOND; + return; + } + if (state->sel_down) { + if (screen->has_dst) { + screen->stage = SET_TZ_DST_OFFSET; + } else { + screen->stage = SET_TZ_FINISH; + } + screen->need_redraw = true; + screen->off_sel_stage = TZ_OFFSET_SEL_HOUR; + return; + } + if (state->up_down || state->down_down) { + screen->has_dst = abs((((int) screen->has_dst) + state->up_down - + state->down_down) % 2); + 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, "Has DST?"); + lcd_move_to(state->lcd, 1, 0); + lcd_write_string(state->lcd, ">No >Yes"); + lcd_move_to(state->lcd, 1, screen->has_dst ? 4 : 0); + } +} + +static const char *DST_BOUND_TYPE_LABELS[] = { + "Exact Julian", + "Leap Julian", + "Month Day Week" +}; +static SetTZScreenAction set_tz_bound_select_type(SetTZScreen *screen, + SensorState *state, + DSTBound *target, + const char *label) { + if (state->back_down) { + screen->off_sel_stage = TZ_OFFSET_SEL_SECOND; + screen->bound_sel_stage = DST_BOUND_SEL_TIME; + return SET_TZ_BACK; + } + if (state->sel_down) { + switch (target->type) { + case DST_BOUND_EXACT_JULIAN: + screen->bound_sel_stage = DST_BOUND_SEL_JULIAN; + target->julian_day = 1; + break; + case DST_BOUND_LEAP_JULAIN: + screen->bound_sel_stage = DST_BOUND_SEL_JULIAN; + target->julian_day = 0; + break; + case DST_BOUND_MND: + screen->bound_sel_stage = DST_BOUND_SEL_MONTH; + target->month = 1; + target->week = 1; + target->day = 0; + break; + } + target->time.hour = 2; + target->time.minute = 0; + target->time.second = 0; + screen->julian_didgit = 3; + screen->need_redraw = true; + return SET_TZ_CONTINUE; + } + if (state->up_down || state->down_down) { + int new_type = (((int) target->type) + state->up_down - state->down_down) % 3; + if (new_type < 0) { + target->type = 2; + } else { + target->type = new_type; + } + 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, label); + lcd_write_string(state->lcd, " Type:"); + lcd_move_to(state->lcd, 1, 0); + lcd_write_char(state->lcd, '>'); + lcd_write_string(state->lcd, DST_BOUND_TYPE_LABELS[target->type]); + lcd_move_to(state->lcd, 1, 0); + } + return SET_TZ_CONTINUE; +} + +static const char *MONTH_ABBREVS[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", + "Dec", +}; +static const char *WEEKDAY_ABBREVS[] = { + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", +}; +static SetTZScreenAction set_tz_bound_select_mnd(SetTZScreen *screen, + SensorState *state, + DSTBound *target, + const char *label) { + if (state->back_down) { + --screen->bound_sel_stage; + screen->need_redraw = true; + return SET_TZ_CONTINUE; + } + if (state->sel_down) { + screen->need_redraw = true; + if (screen->bound_sel_stage == DST_BOUND_SEL_DAY) { + screen->off_sel_stage = TZ_OFFSET_SEL_HOUR; + screen->bound_sel_stage = DST_BOUND_SEL_TIME; + return SET_TZ_CONTINUE; + } + ++screen->bound_sel_stage; + } + if (state->up_down || state->down_down) { + int change = state->up_down - state->down_down; + switch (screen->bound_sel_stage) { + case DST_BOUND_SEL_MONTH: + target->month += change; + if (target->month < 1) { + target->month = 1; + } else if (target->month > 12) { + target->month = 12; + } + break; + case DST_BOUND_SEL_WEEK: + target->week += change; + if (target->week < 1) { + target->week = 1; + } else if (target->week > 5) { + target->week = 5; + } + break; + case DST_BOUND_SEL_DAY: + target->day += change; + if (target->day < 0) { + target->day = 0; + } else if (target->day > 6) { + target->day = 6; + } + break; + default: + break; // should not happen + } + screen->need_redraw = true; + } + if (screen->need_redraw) { + screen->need_redraw = false; + const char *format; + int cursor_pos; + switch(screen->bound_sel_stage) { + case DST_BOUND_SEL_MONTH: + format = ">%s-%d-%s"; + cursor_pos = 0; + break; + case DST_BOUND_SEL_WEEK: + format = "%s->%d-%s"; + cursor_pos = 4; + break; + case DST_BOUND_SEL_DAY: + format = "%s-%d->%s"; + cursor_pos = 7; + break; + default: + // should not happen + format = ""; + cursor_pos = 0; + break; + } + char sel_str[17]; + snprintf(sel_str, 17, format, MONTH_ABBREVS[target->month - 1], + target->week, WEEKDAY_ABBREVS[target->day]); + lcd_clear(state->lcd); + lcd_move_to(state->lcd, 0, 0); + lcd_write_string(state->lcd, "Mon-Week-Day"); + lcd_move_to(state->lcd, 1, 0); + lcd_write_string(state->lcd, sel_str); + lcd_move_to(state->lcd, 1, cursor_pos); + } + return SET_TZ_CONTINUE; +} + +static int set_tz_normalize_julian(int julian, bool allow_zero) { + if (julian <= 0) { + return allow_zero ? 0 : 1; + } else if (julian > 365) { + return 365; + } + return julian; +} + +static SetTZScreenAction set_tz_dispatch_select_julian(SetTZScreen *screen, + SensorState *state, + DSTBound *target, + const char *label) { + if (state->back_down) { + screen->need_redraw = true; + if (screen->julian_didgit == 3) { + screen->bound_sel_stage = DST_BOUND_SEL_TYPE; + return SET_TZ_CONTINUE; + } + ++screen->julian_didgit; + } + if (state->sel_down) { + screen->need_redraw = true; + if (screen->julian_didgit == 1) { + screen->off_sel_stage = TZ_OFFSET_SEL_HOUR; + screen->bound_sel_stage = DST_BOUND_SEL_TIME; + return SET_TZ_CONTINUE; + } + --screen->julian_didgit; + } + if (state->up_down || state->down_down) { + screen->need_redraw = true; + int change = state->up_down - state->down_down; + int scale = 0; + switch (screen->julian_didgit) { + case 1: // ones + scale = 1; + break; + case 2: // tens + scale = 10; + break; + case 3: // hundreds + scale = 100; + break; + } + bool allow_zero = screen->off_sel_stage == DST_BOUND_LEAP_JULAIN; + int new_day = target->julian_day + scale * change; + target->julian_day = set_tz_normalize_julian(new_day, allow_zero); + } + if (screen->need_redraw) { + screen->need_redraw = false; + char jul_str[4]; + snprintf(jul_str, 4, "%03d", target->julian_day); + lcd_clear(state->lcd); + lcd_move_to(state->lcd, 0, 0); + lcd_write_string(state->lcd, "Julian Day"); + lcd_move_to(state->lcd, 1, 0); + lcd_write_string(state->lcd, jul_str); + lcd_move_to(state->lcd, 1, 3 - screen->julian_didgit); + } + return SET_TZ_CONTINUE; +} + +static SetTZScreenAction set_tz_dispatch_select_bound(SetTZScreen *screen, + SensorState *state, + DSTBound *target, + const char *label) { + SetTZScreenAction action; + switch (screen->bound_sel_stage) { + case DST_BOUND_SEL_TYPE: + return set_tz_bound_select_type(screen, state, target, label); + case DST_BOUND_SEL_MONTH: + case DST_BOUND_SEL_WEEK: + case DST_BOUND_SEL_DAY: + return set_tz_bound_select_mnd(screen, state, target, label); + case DST_BOUND_SEL_JULIAN: + return set_tz_dispatch_select_julian(screen, state, target, label); + case DST_BOUND_SEL_TIME: + action = set_tz_dispatch_select_offset(&screen->off_sel_stage, state, + &target->time, "Effect Time:", + screen->need_redraw, false); + switch (action) { + case SET_TZ_NEXT: + screen->bound_sel_stage = DST_BOUND_SEL_TYPE; + case SET_TZ_CONTINUE: + screen->need_redraw = false; + return action; + case SET_TZ_BACK: + if (target->type == DST_BOUND_MND) { + screen->bound_sel_stage = DST_BOUND_SEL_DAY; + } else { + screen->julian_didgit = 1; + screen->bound_sel_stage = DST_BOUND_SEL_JULIAN; + } + screen->need_redraw = true; + return SET_TZ_CONTINUE; + } + default: + return SET_TZ_BACK; + } +} + +static void set_tz_write_dst_bound(FILE *tz, DSTBound *bound) { + fputc(',', tz); + switch (bound->type) { + case DST_BOUND_EXACT_JULIAN: + fprintf(tz, "J%d", bound->julian_day); + break; + case DST_BOUND_LEAP_JULAIN: + fprintf(tz, "%d", bound->julian_day); + break; + case DST_BOUND_MND: + fprintf(tz, "M%d.%d.%d", bound->month, bound->week, bound->day); + break; + } + fprintf(tz, "/%02d:%02d:%02d", bound->time.hour, bound->time.minute, + bound->time.second); +} + +static bool set_tz_screen_finish(SetTZScreen *screen, + SensorState *state) { + if (!screen->did_set) { + screen->did_set = true; + size_t len; + char *buf; + FILE *tz = open_memstream(&buf, &len); + TZOffset *off = &screen->offset; + fprintf(tz, "timezone=CTZ%02d:%02d:%02d", off->hour, off->minute, off->second); + if (screen->has_dst) { + TZOffset *dst_off = &screen->dst_offset; + fprintf(tz, "CDST%02d:%02d:%02d", dst_off->hour, dst_off->minute, + dst_off->second); + set_tz_write_dst_bound(tz, &screen->start_bound); + set_tz_write_dst_bound(tz, &screen->end_bound); + } + fclose(tz); + char *const args[] = { + "sysrc", + "-f", + GLOBAL_OPTS.config_path, + buf, + NULL + }; + pid_t pid; + int exit_status; + if (posix_spawnp(&pid, "sysrc", NULL, NULL, args, environ) != 0) { + warnx("failed to start sysrc(8)"); + } else if (waitpid(pid, &exit_status, 0) < 0) { + warn("waitpid(2) for sysrc(8) failed"); + } else if (exit_status != 0) { + warnx("sysrc(8) failed to set timezone"); + } else { + LOG_VERBOSE("Set timezone to: \"%s\"\n", buf); + } + free(buf); + request_restart(); + } + if (state->up_down || state->back_down || + state->sel_down || state->down_down) { + set_tz_screen_reset(screen, state); + return true; + } + if (screen->need_redraw) { + screen->need_redraw = false; + 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, "Done!"); + } + return false; +} + +static bool set_tz_screen_dispatch(SetTZScreen *screen, + SensorState *state) { + if (state->force_draw) { + screen->need_redraw = true; + } + SetTZScreenAction action; + switch (screen->stage) { + case SET_TZ_OFFSET: + action = set_tz_dispatch_select_offset(&screen->off_sel_stage, state, + &screen->offset, "Offset", + screen->need_redraw, true); + screen->need_redraw = false; + switch (action) { + case SET_TZ_BACK: + set_tz_screen_reset(screen, state); + return true; + case SET_TZ_CONTINUE: + break; // ignore + case SET_TZ_NEXT: + screen->need_redraw = true; + ++screen->stage; + break; + } + break; + case SET_TZ_HAS_DST: + set_tz_prompt_has_dst(screen, state); + break; + case SET_TZ_DST_OFFSET: + action = set_tz_dispatch_select_offset(&screen->off_sel_stage, state, + &screen->dst_offset, + "DST Offset", + screen->need_redraw, true); + screen->need_redraw = false; + switch (action) { + case SET_TZ_BACK: + --screen->stage; + screen->need_redraw = true; + case SET_TZ_CONTINUE: + break; // ignore + case SET_TZ_NEXT: + screen->need_redraw = true; + ++screen->stage; + break; + } + break; + case SET_TZ_DST_START: + action = set_tz_dispatch_select_bound(screen, state, + &screen->start_bound, "Start"); + switch (action) { + case SET_TZ_BACK: + --screen->stage; + screen->need_redraw = true; + case SET_TZ_CONTINUE: + break; // ignore + case SET_TZ_NEXT: + screen->need_redraw = true; + ++screen->stage; + break; + } + break; + case SET_TZ_DST_END: + action = set_tz_dispatch_select_bound(screen, state, + &screen->end_bound, "End"); + switch (action) { + case SET_TZ_BACK: + --screen->stage; + screen->need_redraw = true; + case SET_TZ_CONTINUE: + break; // ignore + case SET_TZ_NEXT: + screen->need_redraw = true; + ++screen->stage; + break; + } + break; + case SET_TZ_FINISH: + return set_tz_screen_finish(screen, state); + } + return false; +} + +SetTZScreen *set_tz_screen_new() { + SetTZScreen *s = malloc_checked(sizeof(SetTZScreen)); + screen_init(&s->parent, "Set timezone", + (ScreenDispatchFunc) set_tz_screen_dispatch, + (ScreenCleanupFunc) free); + set_tz_screen_reset(s, NULL); + return s; +} diff --git a/src/ui/settzscreen.h b/src/ui/settzscreen.h new file mode 100644 index 0000000..cf0f896 --- /dev/null +++ b/src/ui/settzscreen.h @@ -0,0 +1,88 @@ +/* + * settzscreen.h - Screen for setting the timezone + * 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_SETTZSCREEN_H +#define INCLUDED_SETTZSCREEN_H + +#include "screen.h" + +typedef struct { + int hour; + int minute; + int second; +} TZOffset; + +#define tz_offset_reset(o) ((o)->hour = (o)->minute = (o)->second = 0) + +typedef struct { + enum { + DST_BOUND_EXACT_JULIAN = 0, + DST_BOUND_LEAP_JULAIN, + DST_BOUND_MND, + } type; + union { + struct { + int month; + int week; + int day; + }; + int julian_day; + }; + TZOffset time; +} DSTBound; + +typedef enum { + TZ_OFFSET_SEL_HOUR, + TZ_OFFSET_SEL_MINUTE, + TZ_OFFSET_SEL_SECOND, +} TZOffsetSelectStage; + +typedef enum { + DST_BOUND_SEL_TYPE, + DST_BOUND_SEL_MONTH, + DST_BOUND_SEL_WEEK, + DST_BOUND_SEL_DAY, + DST_BOUND_SEL_JULIAN, + DST_BOUND_SEL_TIME, +} DSTBoundSelectStage; + +typedef enum { + SET_TZ_BACK, + SET_TZ_CONTINUE, + SET_TZ_NEXT, +} SetTZScreenAction; + +typedef struct { + Screen parent; + enum { + SET_TZ_OFFSET, + SET_TZ_HAS_DST, + SET_TZ_DST_OFFSET, + SET_TZ_DST_START, + SET_TZ_DST_END, + SET_TZ_FINISH, + } stage; + bool need_redraw; + TZOffsetSelectStage off_sel_stage; + DSTBoundSelectStage bound_sel_stage; + int julian_didgit; + TZOffset offset; + bool has_dst; + TZOffset dst_offset; + DSTBound start_bound; + DSTBound end_bound; + bool did_set; +} SetTZScreen; + +/* + * Create a new SetTZScreen that will allow the user the set the time zone. + */ +SetTZScreen *set_tz_screen_new(void); + +#endif diff --git a/src/util.c b/src/util.c index 60bf00e..933969a 100644 --- a/src/util.c +++ b/src/util.c @@ -19,6 +19,7 @@ #include #include #include +#include const char *PERIOD_LABELS[] = { "HOUR", @@ -29,6 +30,10 @@ const char *PERIOD_LABELS[] = { }; const size_t NPERIOD = sizeof(PERIOD_LABELS) / sizeof(char *); +// these are defined in main.c +extern _Atomic bool RUNNING; +extern bool SHOULD_RESTART; + Options GLOBAL_OPTS; void cleanup_options(Options *opts) { @@ -39,6 +44,7 @@ void cleanup_options(Options *opts) { free(opts->fail_key); free(opts->database_location); free(opts->export_file_name); + free(opts->timezone); } void *malloc_checked(size_t n) { @@ -565,3 +571,8 @@ bool mkdirs(const char *path, mode_t mode) { free(copy); return true; } + +void request_restart() { + RUNNING = false; + SHOULD_RESTART = true; +} diff --git a/src/util.h b/src/util.h index dfcbbff..2a1bc4b 100644 --- a/src/util.h +++ b/src/util.h @@ -54,6 +54,7 @@ typedef struct { TemperatureUnit temp_unit; char *export_file_name; // file to export to + char *timezone; // default timezone } Options; extern Options GLOBAL_OPTS; @@ -248,4 +249,10 @@ bool export_database_as_csv(sqlite3 *db, const char *dest); */ bool mkdirs(const char *path, mode_t mode); +/* + * Request that this program re-invoke itself with the same arguments. Used to + * re-initialize configuration values. + */ +void request_restart(void); + #endif