diff --git a/Makefile b/Makefile index e69d3f0..f0f3ca0 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ .include "config.mk" CC=clang -CFLAGS=-std=c11 -Wall ${SQLITE3_CFLAGS} +CFLAGS=-g -std=c11 -Wall ${SQLITE3_CFLAGS} 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/screen.c diff --git a/src/main.c b/src/main.c index 0edd289..1d47a8f 100644 --- a/src/main.c +++ b/src/main.c @@ -91,14 +91,17 @@ int main(int argc, char *const *argv) { GLOBAL_OPTS.data_pins[7]); setup_signals(); open_database(); + initialize_util_queries(DATABASE); pthread_t bg_update; start_update_thread(&bg_update); - ScreenManager *screen_manager = screen_manager_new(lcd, handle, + ScreenManager *screen_manager = screen_manager_new(DATABASE, + lcd, handle, GLOBAL_OPTS.back_pin, GLOBAL_OPTS.up_pin, GLOBAL_OPTS.down_pin, GLOBAL_OPTS.sel_pin); screen_manager_add(screen_manager, (Screen *) stats_screen_new()); + screen_manager_add(screen_manager, (Screen *) stats_by_screen_new()); while (RUNNING) { lock_stat_globals(); uint32_t temp = LAST_TEMP; @@ -115,6 +118,7 @@ int main(int argc, char *const *argv) { err(1, "join of background thread failed"); } // this needs to be done after bg_update exits + cleanup_util_queries(); sqlite3_close(DATABASE); pthread_mutex_destroy(&STAT_MUTEX); gpio_close(handle); @@ -386,12 +390,12 @@ void parse_config_file(const char *path) { .action = parse_uint_callback, }, { - .directive = "down_pin", + .directive = "up_pin", .type = FIGPAR_TYPE_NONE, .action = parse_uint_callback, }, { - .directive = "up_pin", + .directive = "down_pin", .type = FIGPAR_TYPE_NONE, .action = parse_uint_callback, }, @@ -430,6 +434,9 @@ void parse_config_file(const char *path) { static void exit_signal_callback(int sig) { LOG_VERBOSE("Caught signal %d. Exiting...\n", sig); RUNNING = false; + if (signal(sig, SIG_DFL) == SIG_ERR) { + err(1, "resetting signal handler failed"); + } } #define SIGNAL_SETUP_CHECKED(sig) \ @@ -497,13 +504,13 @@ static void create_db_table() { LOG_VERBOSE("Ensured env_data table existance\n"); } -static const char *UPDATE_STATES_QUERY = +static const char *UPDATE_STATS_QUERY = "INSERT INTO env_data (time, temp, humid) " "VALUES(?, ?, ?);"; static void *update_thread_action(void *_ignored) { create_db_table(); sqlite3_stmt *insert_statement = NULL; - int status = sqlite3_prepare_v2(DATABASE, UPDATE_STATES_QUERY, -1, + int status = sqlite3_prepare_v2(DATABASE, UPDATE_STATS_QUERY, -1, &insert_statement, NULL); if (status != SQLITE_OK) { errx(1, "could not compile SQL query. sqlite3 error follows:\n%s", diff --git a/src/screen.c b/src/screen.c index f6e4b3d..7a08c21 100644 --- a/src/screen.c +++ b/src/screen.c @@ -8,11 +8,12 @@ * version. See the LICENSE file for more information. */ #include "screen.h" -#include "util.h" #include "ths.h" #include #include +#include +#include void screen_init(Screen *screen, const char *name, @@ -30,10 +31,11 @@ void screen_delete(Screen *screen) { } } -ScreenManager *screen_manager_new(LCD *lcd, gpio_handle_t handle, +ScreenManager *screen_manager_new(sqlite3 *db, LCD *lcd, gpio_handle_t handle, gpio_pin_t back_pin, gpio_pin_t up_pin, gpio_pin_t down_pin, gpio_pin_t sel_pin) { ScreenManager *sm = malloc_checked(sizeof(ScreenManager)); + sm->db = db; sm->lcd = lcd; sm->screens = NULL; sm->screen_count = 0; @@ -133,7 +135,7 @@ void screen_manager_dispatch(ScreenManager *sm, uint32_t temp, uint32_t humid) { } } -bool stats_screen_dispatch(StatsScreen *screen, SensorState *state) { +static bool stats_screen_dispatch(StatsScreen *screen, SensorState *state) { if (state->back_down) { return true; } @@ -168,3 +170,292 @@ StatsScreen *stats_screen_new() { s->last_temp = 0; return s; } + +static const char *PERIOD_LABELS[] = { + "HOUR", + "DAY", + "WEEK", + "MONTH", + "YEAR", +}; +static const size_t NPERIOD = sizeof(PERIOD_LABELS) / sizeof(char *); + +static void stats_by_select_period(StatsByScreen *screen, SensorState *state) { + if (state->up_down) { + screen->period = (screen->period + 1) % NPERIOD; + screen->need_redraw = true; + } + if (state->down_down) { + if (--screen->period < 0) { + screen->period = NPERIOD - 1; + } + screen->need_redraw = true; + } + if (screen->need_redraw) { + lcd_clear(state->lcd); + lcd_move_to(state->lcd, 0, 0); + lcd_write_string(state->lcd, "Period:"); + lcd_move_to(state->lcd, 1, 0); + lcd_write_string(state->lcd, ">"); + lcd_write_string(state->lcd, PERIOD_LABELS[screen->period]); + lcd_move_to(state->lcd, 1, 0); + lcd_display_control(state->lcd, LCD_CURSOR_BLINK, LCD_CURSOR_ON, + LCD_DISPLAY_ON); + screen->need_redraw = false; + } + if (state->sel_down) { + ++screen->stage; + switch (screen->period) { + case 0: + case 1: + case 2: + screen->ds.max_stage = DATE_SEL_DAY; + break; + case 3: + screen->ds.max_stage = DATE_SEL_MONTH; + break; + case 4: + screen->ds.max_stage = DATE_SEL_YEAR; + break; + } + screen->need_redraw = true; + } +} + +void date_sel_init(DateSelection *ds, int start_line, int start_col, + DateSelectionStage max_stage) { + ds->max_stage = max_stage; + ds->start_line = start_line; + ds->start_col = start_col; + memset(&ds->start_time, 0, sizeof(UtilDate)); // min date + memset(&ds->end_time, 0xff, sizeof(UtilDate)); // max date + date_sel_reset(ds); +} + +void date_sel_reset(DateSelection *ds) { + ds->year = -1; + ds->stage = DATE_SEL_YEAR; +} + +static void date_sel_clamp_time(DateSelection *ds) { + if (ds->year > ds->end_time.local_year) { + ds->year = ds->end_time.local_year; + } + if (ds->year == ds->end_time.local_year && + ds->month > ds->end_time.local_month) { + ds->month = ds->end_time.local_month; + } + if (ds->month == ds->end_time.local_year && + ds->month == ds->end_time.local_month && + ds->day > ds->end_time.local_day) { + printf("Clamp Day High!\n"); + ds->day = ds->end_time.local_day; + } + if (ds->year < ds->start_time.local_year) { + ds->year = ds->start_time.local_year; + } + if (ds->year == ds->start_time.local_year && + ds->month < ds->start_time.local_month) { + ds->month = ds->start_time.local_month; + } + if (ds->year == ds->start_time.local_year && + ds->month == ds->start_time.local_month && + ds->day < ds->start_time.local_day) { + printf("Clamp Day Low!\n"); + ds->day = ds->start_time.local_day; + } + printf("Max: %d %d %d\n", ds->end_time.local_year, ds->end_time.local_month, ds->end_time.local_day); + printf("Min: %d %d %d\n", ds->start_time.local_year, ds->start_time.local_month, ds->start_time.local_day); + printf("Cur: %d %d %d\n", ds->year, ds->month, ds->day); +} + +static void date_sel_cleanup(DateSelection *ds) { + if (ds->month < 1) { + ds->month = 12 - ds->month; + --ds->year; + } else if (ds->month > 12) { + ds->month = (ds->month % 13) + 1; + ++ds->year; + } + int ndays = days_in_month(ds->month, ds->year); + if (ds->day == 33) { + ds->day = ndays; + } else if (ds->day < 1) { + ds->day = 33; // this means last day of month + --ds->month; + date_sel_cleanup(ds); + } else if (ds->day > ndays) { + ds->day = 1; + ++ds->month; + date_sel_cleanup(ds); + } + date_sel_clamp_time(ds); +} + +static void date_sel_add_units(DateSelection *ds, int n) { + switch (ds->stage) { + case DATE_SEL_YEAR: + ds->year += n; + break; + case DATE_SEL_MONTH: + ds->month += n; + break; + case DATE_SEL_DAY: + ds->day += n; + break; + } + date_sel_cleanup(ds); +} + +DateSelectionState date_sel_dispatch(DateSelection *ds, SensorState *state) { + if (ds->year == -1) { + time_t utc_time = time(NULL); + struct tm local_time; + localtime_r(&utc_time, &local_time); + ds->year = local_time.tm_year + 1900; + if (ds->max_stage > DATE_SEL_YEAR) { + ds->month = local_time.tm_mon + 1; + } + if (ds->max_stage > DATE_SEL_MONTH) { + ds->day = local_time.tm_mday; + } + date_sel_cleanup(ds); + } + if (ds->max_stage < DATE_SEL_MONTH) { + ds->month = 1; + } + if (ds->max_stage < DATE_SEL_DAY) { + ds->day = 1; + } + if (state->back_down) { + if (ds->stage == DATE_SEL_YEAR) { + return DATE_SEL_BACK; + } + --ds->stage; + } + if (state->up_down || state->down_down) { + date_sel_add_units(ds, state->up_down - state->down_down); + } + if (state->sel_down) { + if (ds->stage == ds->max_stage) { + return DATE_SEL_DONE; + } + ++ds->stage; + } + int buf_size = 17 - ds->start_col; + char buff[buf_size]; + int cursor_pos; + const char *format; + switch (ds->stage) { + case DATE_SEL_YEAR: + cursor_pos = 0; + format = ">%04d/%02d/%02d"; + break; + case DATE_SEL_MONTH: + cursor_pos = 5; + format = "%04d/>%02d/%02d"; + break; + case DATE_SEL_DAY: + cursor_pos = 8; + format = "%04d/%02d/>%02d"; + break; + default: + LOG_VERBOSE("Date selector tried to select bad field\n"); + return DATE_SEL_BACK; + } + snprintf(buff, buf_size, format, ds->year, ds->month, ds->day); + cursor_pos += ds->start_col; + lcd_move_to(state->lcd, ds->start_line, ds->start_col); + lcd_write_string(state->lcd, buff); + lcd_display_control(state->lcd, LCD_CURSOR_BLINK, LCD_CURSOR_ON, + LCD_DISPLAY_ON); + lcd_move_to(state->lcd, ds->start_line, cursor_pos); + return DATE_SEL_CONTINUE; +} + +static void stats_by_select_start(StatsByScreen *screen, SensorState *state) { + if (state->up_down || state->down_down || state->sel_down || state->back_down) { + screen->need_redraw = true; + UtilDate start, end; + if (!get_database_limits(state->db, PERIOD_LABELS[screen->period], + &start, &end)) { + warnx("failed to query database limits"); + --screen->stage; + return; + } + screen->ds.start_time = start; + screen->ds.end_time = end; + } + if (screen->need_redraw) { + lcd_clear(state->lcd); + lcd_move_to(state->lcd, 0, 0); + lcd_write_string(state->lcd, "Start: "); + lcd_move_to(state->lcd, 1, 0); + screen->need_redraw = false; + DateSelectionState dss = date_sel_dispatch(&screen->ds, state); + switch (dss) { + case DATE_SEL_BACK: + screen->need_redraw = true; + lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, + LCD_DISPLAY_ON); + --screen->stage; + return; + case DATE_SEL_CONTINUE: + break; // ignore + case DATE_SEL_DONE: + ++screen->stage; + screen->need_redraw = true; + break; + } + } +} + +static bool stats_by_screen_dispatch(StatsByScreen *screen, + SensorState *state) { + if (state->force_draw) { + screen->need_redraw = true; + } + if (state->back_down) { + if (screen->stage == STATS_BY_SELECT_PERIOD) { + screen->need_redraw = true; + lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, + LCD_DISPLAY_ON); + date_sel_reset(&screen->ds); + return true; + } + } + switch (screen->stage) { + case STATS_BY_SELECT_PERIOD: + stats_by_select_period(screen, state); + break; + case STATS_BY_SELECT_START: + stats_by_select_start(screen, state); + break; + case STATS_BY_SHOWING: + if (state->back_down) { + screen->stage = STATS_BY_SELECT_PERIOD; + screen->need_redraw = true; + screen->ds.stage = DATE_SEL_YEAR; + } else if (screen->need_redraw) { + lcd_clear(state->lcd); + lcd_move_to(state->lcd, 0, 0); + lcd_write_string(state->lcd, "Some data!"); + screen->need_redraw = false; + } + break; + default: + LOG_VERBOSE("Attempt to show bad stats by screen\n"); + return true; + } + return false; +} + +StatsByScreen *stats_by_screen_new() { + StatsByScreen *s = malloc_checked(sizeof(StatsByScreen)); + screen_init(&s->parent, "Stats by...", + (ScreenDispatchFunc) stats_by_screen_dispatch, + (ScreenCleanupFunc) free); + s->need_redraw = true; + date_sel_init(&s->ds, 1, 0, DATE_SEL_DAY); + return s; +} diff --git a/src/screen.h b/src/screen.h index adb934d..81bac00 100644 --- a/src/screen.h +++ b/src/screen.h @@ -12,11 +12,14 @@ #include "button.h" #include "lcd.h" +#include "util.h" #include #include +#include typedef struct { + sqlite3 *db; LCD *lcd; uint32_t temp; uint32_t humid; @@ -52,6 +55,7 @@ void screen_init(Screen *screen, const char *name, void screen_delete(Screen *screen); typedef struct { + sqlite3 *db; LCD *lcd; Button *back_btn; Button *up_btn; @@ -68,10 +72,11 @@ typedef struct { } ScreenManager; /* - * Crate a new ScreenManager with the given LCD and buttons + * Crate a new ScreenManager with the given DB, LCD and buttons. DB and LCD + * still owned by the caller! * Return: the new ScreenManager */ -ScreenManager *screen_manager_new(LCD *lcd, gpio_handle_t handle, +ScreenManager *screen_manager_new(sqlite3 *db, LCD *lcd, gpio_handle_t handle, gpio_pin_t back_pin, gpio_pin_t up_pin, gpio_pin_t down_pin, gpio_pin_t sel_pin); @@ -99,6 +104,71 @@ typedef struct { time_t last_min; } StatsScreen; +/* + * Create a new stats screen. This screen will display the current temp. and + * humidity data from a THS sensor. + */ StatsScreen *stats_screen_new(void); +typedef enum { + DATE_SEL_YEAR, + DATE_SEL_MONTH, + DATE_SEL_DAY, +} DateSelectionStage; + +typedef struct { + int year; + int month; + int day; + DateSelectionStage stage; + DateSelectionStage max_stage; + int start_line; + int start_col; + UtilDate start_time; + UtilDate end_time; +} DateSelection; + +typedef enum { + DATE_SEL_BACK, + DATE_SEL_CONTINUE, + DATE_SEL_DONE +} DateSelectionState; + +/* + * Initialize a DateSelection. START_LINE and START_COL are the first line and + * column where it should draw the widget. MAX_STAGE is the most specific field + * to select. + */ +void date_sel_init(DateSelection *ds, int start_line, int start_col, + DateSelectionStage max_stage); + +/* + * Reset the date on DS. + */ +void date_sel_reset(DateSelection *ds); + +/* + * Dispatch a DateSelection by processing STATE to continue the selection. + * Return: weather the user backed out, is still working, or is done + */ +DateSelectionState date_sel_dispatch(DateSelection *ds, SensorState *state); + +typedef struct { + Screen parent; + enum { + STATS_BY_SELECT_PERIOD = 0, + STATS_BY_SELECT_START, + STATS_BY_SHOWING, + } stage; + bool need_redraw; + int period; + DateSelection ds; +} StatsByScreen; + +/* + * Create a new stats by screen. This screen will display data from the database + * by hour, day, week, month, and year. + */ +StatsByScreen *stats_by_screen_new(void); + #endif diff --git a/src/util.c b/src/util.c index f291604..cc0ff18 100644 --- a/src/util.c +++ b/src/util.c @@ -45,3 +45,103 @@ void *strdup_checked(const char *str) { } return new_str; } + +int days_in_month(int m, int y) { + switch (m) { + case 2: + if (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) { + return 29; + } else { + return 28; + } + break; + case 1: + case 3: + case 5: + case 7: + case 8: + case 10: + case 12: + return 31; + default: + return 30; + } +} + +// is this even a C program anymore? +static const char *DB_LIMITS_QUERY_STR = + "SELECT\n" + "MinUTC,\n" + // labels below are for debugging + "strftime('%Y', MinUTC, 'unixepoch') as MinUTCYear,\n" + "strftime('%m', MinUTC, 'unixepoch') as MinUTCMonth,\n" + "strftime('%e', MinUTC, 'unixepoch') as MinUTCDay,\n" + "MinLocal,\n" + "strftime('%Y', MinLocal, 'unixepoch') as MinLocalYear,\n" + "strftime('%m', MinLocal, 'unixepoch') as MinLocalMonth,\n" + "strftime('%e', MinLocal, 'unixepoch') as MinLocalDay,\n" + "MaxUTC,\n" + "strftime('%Y', MaxUTC, 'unixepoch') as MaxUTCYear,\n" + "strftime('%m', MaxUTC, 'unixepoch') as MaxUTCMonth,\n" + "strftime('%e', MaxUTC, 'unixepoch') as MaxUTCDay,\n" + "MaxLocal,\n" + "strftime('%Y', MaxLocal, 'unixepoch') as MaxLocalYear,\n" + "strftime('%m', MaxLocal, 'unixepoch') as MaxLocalMonth,\n" + "strftime('%e', MaxLocal, 'unixepoch') as MaxLocalDay\n" + "FROM\n" + "(SELECT\n" + "unixepoch(min(time), 'unixepoch', 'start of ' || ?1) as MinUTC,\n" + "unixepoch(max(time), 'unixepoch', '+1 ' || ?1, 'start of ' || ?1, '-1 second') as MaxUTC,\n" + "unixepoch(min(time), 'unixepoch', 'localtime', 'start of ' || ?1) as MinLocal,\n" + "unixepoch(max(time), 'unixepoch', 'localtime', '+1 ' || ?1, 'start of ' || ?1, '-1 second') as MaxLocal\n" + "FROM env_data);"; +static sqlite3_stmt *DB_LIMITS_QUERY; +void initialize_util_queries(sqlite3 *db) { + int status = sqlite3_prepare_v2(db, DB_LIMITS_QUERY_STR, -1, + &DB_LIMITS_QUERY, NULL); + if (status != SQLITE_OK) { + errx(1, "failed to compile limits query: %s", sqlite3_errstr(status)); + } +} + +void cleanup_util_queries() { + sqlite3_finalize(DB_LIMITS_QUERY); +} + +bool get_database_limits(sqlite3 *db, const char *period, UtilDate *start, + UtilDate *end) { + if (strcasecmp(period, "week") == 0 || strcasecmp(period, "hour") == 0) { + period = "day"; + } + bool success = true; + sqlite3_bind_text(DB_LIMITS_QUERY, 1, period, -1, SQLITE_TRANSIENT); + int status = sqlite3_step(DB_LIMITS_QUERY); + if (status == SQLITE_ROW) { + if (start) { + start->utc = sqlite3_column_int64(DB_LIMITS_QUERY, 0); + start->utc_year = sqlite3_column_int64(DB_LIMITS_QUERY, 1); + start->utc_month = sqlite3_column_int64(DB_LIMITS_QUERY, 2); + start->utc_day = sqlite3_column_int64(DB_LIMITS_QUERY, 3); + start->local = sqlite3_column_int64(DB_LIMITS_QUERY, 4); + start->local_year = sqlite3_column_int64(DB_LIMITS_QUERY, 5); + start->local_month = sqlite3_column_int64(DB_LIMITS_QUERY, 6); + start->local_day = sqlite3_column_int64(DB_LIMITS_QUERY, 7); + } + if (end) { + end->utc = sqlite3_column_int64(DB_LIMITS_QUERY, 8); + end->utc_year = sqlite3_column_int64(DB_LIMITS_QUERY, 9); + end->utc_month = sqlite3_column_int64(DB_LIMITS_QUERY, 10); + end->utc_day = sqlite3_column_int64(DB_LIMITS_QUERY, 11); + end->local = sqlite3_column_int64(DB_LIMITS_QUERY, 12); + end->local_year = sqlite3_column_int64(DB_LIMITS_QUERY, 13); + end->local_month = sqlite3_column_int64(DB_LIMITS_QUERY, 14); + end->local_day = sqlite3_column_int64(DB_LIMITS_QUERY, 15); + } + } else { + success = false; + } + // unbind the string so it can be freed by the caller + sqlite3_bind_null(DB_LIMITS_QUERY, 1); + sqlite3_reset(DB_LIMITS_QUERY); + return success; +} diff --git a/src/util.h b/src/util.h index b1ba2e6..fd32dc7 100644 --- a/src/util.h +++ b/src/util.h @@ -17,6 +17,7 @@ #include #include #include +#include typedef struct { char *config_path; // path to config file @@ -76,4 +77,37 @@ void *strdup_checked(const char *str); */ #define LOG_VERBOSE(...) if (GLOBAL_OPTS.verbose) {fprintf(stderr, __VA_ARGS__);} +/* + * Return: the number of days in month M. 1 is January. Y is the year. + */ +int days_in_month(int m, int y); + +/* + * Initialize SQL queries used by this file. + */ +void initialize_util_queries(sqlite3 *db); + +/* + * Cleanup SQL queries used by this file. + */ +void cleanup_util_queries(void); + +typedef struct { + int64_t utc; + int utc_year; + int utc_month; + int utc_day; + int64_t local; + int local_year; + int local_month; + int local_day; +} UtilDate; + +/* + * Return the START of the first and END of the last PERIOD (ex. week) of DB. + * Return: false if an error occurred, true otherwise. + */ +bool get_database_limits(sqlite3 *db, const char *period, UtilDate *start, + UtilDate *end); + #endif