From 375f8494f45785578515da9815433d99de06260a Mon Sep 17 00:00:00 2001 From: Alexander Rosenberg Date: Mon, 1 Apr 2024 13:56:24 -0700 Subject: [PATCH] Refactor ui code --- Makefile | 23 +- src/main.c | 3 +- src/screen.c | 519 ------------------------------------------ src/ui/datesel.c | 166 ++++++++++++++ src/ui/datesel.h | 59 +++++ src/ui/screen.c | 172 ++++++++++++++ src/{ => ui}/screen.h | 73 +----- src/ui/statsby.c | 205 +++++++++++++++++ src/ui/statsby.h | 36 +++ 9 files changed, 660 insertions(+), 596 deletions(-) delete mode 100644 src/screen.c create mode 100644 src/ui/datesel.c create mode 100644 src/ui/datesel.h create mode 100644 src/ui/screen.c rename src/{ => ui}/screen.h (63%) create mode 100644 src/ui/statsby.c create mode 100644 src/ui/statsby.h diff --git a/Makefile b/Makefile index f0f3ca0..8251041 100644 --- a/Makefile +++ b/Makefile @@ -4,22 +4,29 @@ CC=clang 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 +LDFLAGS=-lgpio -lfigpar -lgeom -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 PROG=rpi4b-temp-humidity OBJS=${SRCS:C/^src/bin/:C/.c$/.o/} bin/${PROG}: ${OBJS} ${LD} ${LDFLAGS} -o ${@} ${OBJS} -bin/main.o bin/util.o bin/lcd.o bin/ths.o bin/button.o bin/screen.o: src/util.h -bin/main.o bin/lcd.o bin/screen.o: src/lcd.h -bin/main.o bin/ths.o bin/screen.o: src/ths.h -bin/main.o bin/button.o bin/screen.o: src/button.h -bin/main.o bin/screen.o: src/screen.h +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/main.o bin/lcd.o bin/screen.o bin/ui/datesel.o: src/lcd.h +bin/ui/statsby.o: src/lcd.h + +bin/main.o bin/ths.o bin/ui/screen.o bin/ui/statsby.o: src/ths.h +bin/main.o bin/button.o bin/ui/screen.o: src/button.h +bin/main.o bin/ui/screen.o bin/ui/statsby.o bin/ui/datesel.o: src/ui/screen.h +bin/main.o bin/ui/datesel.o bin/ui/statsby.o: src/ui/datesel.h +bin/main.o bin/ui/statsby.o: src/ui/statsby.h ${OBJS}: ${.TARGET:C/^bin/src/:C/.o$/.c/} - @mkdir -p bin/ + @mkdir -p ${.TARGET:H} ${CC} ${CFLAGS} -c\ -DDEFAULT_CONFIG_PATH="\"${DEFAULT_CONFIG_PATH}\""\ -DDEFAULT_GPIO_DEVICE="\"${DEFAULT_GPIO_DEVICE}\""\ diff --git a/src/main.c b/src/main.c index 1d47a8f..78d130f 100644 --- a/src/main.c +++ b/src/main.c @@ -12,7 +12,8 @@ #include "ths.h" #include "button.h" #include "menu.h" -#include "screen.h" +#include "ui/screen.h" +#include "ui/statsby.h" #include #include diff --git a/src/screen.c b/src/screen.c deleted file mode 100644 index c84ed64..0000000 --- a/src/screen.c +++ /dev/null @@ -1,519 +0,0 @@ -/* - * screen.c - Simple menu system - * 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 "screen.h" -#include "ths.h" - -#include -#include -#include -#include - - -void screen_init(Screen *screen, const char *name, - ScreenDispatchFunc dispatch_func, - ScreenCleanupFunc cleanup_func) { - screen->dispatch_func = dispatch_func; - screen->cleanup_func = cleanup_func; - screen->name = strdup_checked(name); -} - -void screen_delete(Screen *screen) { - FREE_CHECKED(screen->name); - if (screen->cleanup_func) { - screen->cleanup_func(screen); - } -} - -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; - sm->cur_screen = 0; - sm->up_btn = button_new(handle, up_pin); - sm->down_btn = button_new(handle, down_pin); - sm->back_btn = button_new(handle, back_pin); - sm->sel_btn = button_new(handle, sel_pin); - sm->cur_menu_line = 0; - sm->last_drawn_menu_line = 0; - sm->force_draw = true; - return sm; -} - -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); - button_delete(sm->up_btn); - button_delete(sm->down_btn); - button_delete(sm->back_btn); - button_delete(sm->sel_btn); - free(sm); -} - -void screen_manager_add(ScreenManager *sm, Screen *screen) { - sm->screens = realloc_checked(sm->screens, - sizeof(Screen *) * ++sm->screen_count); - sm->screens[sm->screen_count - 1] = screen; -} - -static void screen_manager_dispatch_select_menu(ScreenManager *sm) { - if (button_pressed(sm->up_btn) && sm->cur_menu_line != 0) { - --sm->cur_menu_line; - } - if (button_pressed(sm->down_btn) && sm->cur_menu_line < sm->screen_count - 1) { - ++sm->cur_menu_line; - } - // poll the button, but don't do anything with the state - button_pressed(sm->back_btn); - if (sm->cur_menu_line != sm->last_drawn_menu_line || sm->force_draw) { - sm->force_draw = false; - sm->last_drawn_menu_line = sm->cur_menu_line; - lcd_clear(sm->lcd); - lcd_move_to(sm->lcd, 0, 0); - if (sm->screen_count == 1) { - lcd_write_char(sm->lcd, '*'); - lcd_write_string(sm->lcd, sm->screens[0]->name); - } else if (sm->cur_menu_line >= sm->screen_count - 1) { - lcd_write_char(sm->lcd, ' '); - lcd_write_string(sm->lcd, sm->screens[sm->screen_count - 2]->name); - lcd_move_to(sm->lcd, 1, 0); - lcd_write_char(sm->lcd, '*'); - lcd_write_string(sm->lcd, sm->screens[sm->screen_count - 1]->name); - } else { - lcd_write_char(sm->lcd, '*'); - lcd_write_string(sm->lcd, sm->screens[sm->cur_menu_line]->name); - lcd_move_to(sm->lcd, 1, 0); - lcd_write_char(sm->lcd, ' '); - lcd_write_string(sm->lcd, sm->screens[sm->cur_menu_line + 1]->name); - } - } - if (button_pressed(sm->sel_btn)) { - sm->cur_screen = sm->cur_menu_line; - sm->force_draw = true; - } -} - -void screen_manager_dispatch(ScreenManager *sm, uint32_t temp, uint32_t humid) { - if (!sm->screen_count) { - errx(1, "attempt to display empty screen manager"); - } - if (sm->cur_screen < 0) { - screen_manager_dispatch_select_menu(sm); - } else { - Screen *cs = sm->screens[sm->cur_screen]; - if (cs->dispatch_func) { - SensorState state = { - .lcd = sm->lcd, - .temp = temp, - .humid = humid, - .back_down = button_pressed(sm->back_btn), - .up_down = button_pressed(sm->up_btn), - .down_down = button_pressed(sm->down_btn), - .sel_down = button_pressed(sm->sel_btn), - .force_draw = sm->force_draw, - }; - if (cs->dispatch_func(cs, &state)) { - // if this is true, it means return to the main menu - sm->cur_screen = -1; - sm->force_draw = true; - } else { - sm->force_draw = false; - } - } - } -} - -static bool stats_screen_dispatch(StatsScreen *screen, SensorState *state) { - if (state->back_down) { - return true; - } - time_t cur_time = time(NULL); - if (state->force_draw || state->temp != screen->last_temp - || state->humid != screen->last_humid - || screen->last_min != cur_time / 60) { - screen->last_temp = state->temp; - screen->last_humid = state->humid; - screen->last_min = cur_time / 60; - lcd_clear(state->lcd); - lcd_write_string(state->lcd, "temp humi time"); - char buff[17]; - int cur_len = snprintf(buff, sizeof(buff), "%4.1fF %3" PRIu32 "%% ", - DK_TO_F(state->temp), state->humid); - struct tm lt; - localtime_r(&cur_time, <); - strftime(buff + cur_len, sizeof(buff) - cur_len, - "%H:%M", <); - lcd_move_to(state->lcd, 1, 0); - lcd_write_string(state->lcd, buff); - } - return false; -} - -StatsScreen *stats_screen_new() { - StatsScreen *s = malloc_checked(sizeof(StatsScreen)); - screen_init(&s->parent, "Current Stats", - (ScreenDispatchFunc) stats_screen_dispatch, - (ScreenCleanupFunc) free); - s->last_humid = 0; - s->last_temp = 0; - return s; -} - -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 PERIOD_HOUR: - case PERIOD_DAY: - case PERIOD_WEEK: - screen->ds.max_stage = DATE_SEL_DAY; - break; - case PERIOD_MONTH: - screen->ds.max_stage = DATE_SEL_MONTH; - break; - case PERIOD_YEAR: - 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->stage == DATE_SEL_DAY) { - // last days of previous month - ds->day = days_in_month(ds->month, ds->year); - } - } - if (ds->year == ds->end_time.local_year && - ds->month == ds->end_time.local_month && - ds->day > ds->end_time.local_day) { - 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->stage == DATE_SEL_DAY) { - ds->day = 1; // first day of the next month - } - } - if (ds->year == ds->start_time.local_year && - ds->month == ds->start_time.local_month && - ds->day < ds->start_time.local_day) { - ds->day = ds->start_time.local_day; - } -} - -static void date_sel_cleanup(DateSelection *ds) { - date_sel_clamp_time(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; - } - if (screen->need_redraw) { - 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; - 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->offset_scale = 0; - screen->need_redraw = true; - screen->ulimit_reached = false; - break; - } - } -} - -static void stats_by_show_stats(StatsByScreen *screen, SensorState *state) { - 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) { - 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); - printf("%d\n",screen->ds.start_time.local_hour); - if (screen->period == PERIOD_HOUR && - screen->offset_scale == 0 && - screen->ds.year == screen->ds.start_time.local_year && - screen->ds.month == screen->ds.start_time.local_month && - screen->ds.day == screen->ds.start_time.local_day) { - screen->offset_scale = screen->ds.start_time.local_hour; - } - UtilAverageRange data; - if (!get_average_for_range(state->db, screen->ds.year, screen->ds.month, - screen->ds.day, screen->offset_scale, - screen->period, &data)) { - lcd_write_string(state->lcd, "Query failed!"); - warnx("failed to query average temperature and humidity"); - return; - } - screen->ulimit_reached = data.upper_bound; - screen->blimit_reached = data.lower_bound; - char period_string[17]; - switch (screen->period) { - case PERIOD_YEAR: - snprintf(period_string, 17, "Year>%d", data.year); - break; - case PERIOD_MONTH: - snprintf(period_string, 17, "Month>%d-%d", data.year, - data.month); - break; - case PERIOD_WEEK: - snprintf(period_string, 17, "Week>%d-%d-%d", data.year, - data.month, data.day); - break; - case PERIOD_DAY: - snprintf(period_string, 17, "Day>%d-%d-%d", data.year, - data.month, data.day); - break; - case PERIOD_HOUR: - snprintf(period_string, 17, "Hour>%d-%d %d:00", data.month, - data.day, data.hour); - break; - } - lcd_write_string(state->lcd, period_string); - lcd_move_to(state->lcd, 1, 0); - if (data.npoints) { - char data_string[17]; - snprintf(data_string, 17, "T:%.1fF H:%d%%", DK_TO_F(data.temp), data.humid); - lcd_write_string(state->lcd, data_string); - } else { - lcd_write_string(state->lcd, "No data!"); - } - } - if (!screen->ulimit_reached && state->up_down) { - ++screen->offset_scale; - screen->need_redraw = true; - } - if (!screen->blimit_reached && state->down_down) { - --screen->offset_scale; - screen->need_redraw = true; - } -} - -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: - stats_by_show_stats(screen, state); - 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/ui/datesel.c b/src/ui/datesel.c new file mode 100644 index 0000000..574934d --- /dev/null +++ b/src/ui/datesel.c @@ -0,0 +1,166 @@ +/* + * datesel.h - Date selector + * 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 "datesel.h" + +#include + +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->stage == DATE_SEL_DAY) { + // last days of previous month + ds->day = days_in_month(ds->month, ds->year); + } + } + if (ds->year == ds->end_time.local_year && + ds->month == ds->end_time.local_month && + ds->day > ds->end_time.local_day) { + 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->stage == DATE_SEL_DAY) { + ds->day = 1; // first day of the next month + } + } + if (ds->year == ds->start_time.local_year && + ds->month == ds->start_time.local_month && + ds->day < ds->start_time.local_day) { + ds->day = ds->start_time.local_day; + } +} + +static void date_sel_cleanup(DateSelection *ds) { + date_sel_clamp_time(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; +} diff --git a/src/ui/datesel.h b/src/ui/datesel.h new file mode 100644 index 0000000..c422a2e --- /dev/null +++ b/src/ui/datesel.h @@ -0,0 +1,59 @@ +/* + * datesel.h - Date selector + * 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_DATESEL_H +#define INCLUDED_DATESEL_H + +#include "../util.h" +#include "screen.h" + +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); + +#endif diff --git a/src/ui/screen.c b/src/ui/screen.c new file mode 100644 index 0000000..3c48f51 --- /dev/null +++ b/src/ui/screen.c @@ -0,0 +1,172 @@ +/* + * screen.c - Simple menu system + * 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 "screen.h" +#include "../ths.h" + +#include +#include +#include +#include + + +void screen_init(Screen *screen, const char *name, + ScreenDispatchFunc dispatch_func, + ScreenCleanupFunc cleanup_func) { + screen->dispatch_func = dispatch_func; + screen->cleanup_func = cleanup_func; + screen->name = strdup_checked(name); +} + +void screen_delete(Screen *screen) { + FREE_CHECKED(screen->name); + if (screen->cleanup_func) { + screen->cleanup_func(screen); + } +} + +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; + sm->cur_screen = 0; + sm->up_btn = button_new(handle, up_pin); + sm->down_btn = button_new(handle, down_pin); + sm->back_btn = button_new(handle, back_pin); + sm->sel_btn = button_new(handle, sel_pin); + sm->cur_menu_line = 0; + sm->last_drawn_menu_line = 0; + sm->force_draw = true; + return sm; +} + +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); + button_delete(sm->up_btn); + button_delete(sm->down_btn); + button_delete(sm->back_btn); + button_delete(sm->sel_btn); + free(sm); +} + +void screen_manager_add(ScreenManager *sm, Screen *screen) { + sm->screens = realloc_checked(sm->screens, + sizeof(Screen *) * ++sm->screen_count); + sm->screens[sm->screen_count - 1] = screen; +} + +static void screen_manager_dispatch_select_menu(ScreenManager *sm) { + if (button_pressed(sm->up_btn) && sm->cur_menu_line != 0) { + --sm->cur_menu_line; + } + if (button_pressed(sm->down_btn) && sm->cur_menu_line < sm->screen_count - 1) { + ++sm->cur_menu_line; + } + // poll the button, but don't do anything with the state + button_pressed(sm->back_btn); + if (sm->cur_menu_line != sm->last_drawn_menu_line || sm->force_draw) { + sm->force_draw = false; + sm->last_drawn_menu_line = sm->cur_menu_line; + lcd_clear(sm->lcd); + lcd_move_to(sm->lcd, 0, 0); + if (sm->screen_count == 1) { + lcd_write_char(sm->lcd, '*'); + lcd_write_string(sm->lcd, sm->screens[0]->name); + } else if (sm->cur_menu_line >= sm->screen_count - 1) { + lcd_write_char(sm->lcd, ' '); + lcd_write_string(sm->lcd, sm->screens[sm->screen_count - 2]->name); + lcd_move_to(sm->lcd, 1, 0); + lcd_write_char(sm->lcd, '*'); + lcd_write_string(sm->lcd, sm->screens[sm->screen_count - 1]->name); + } else { + lcd_write_char(sm->lcd, '*'); + lcd_write_string(sm->lcd, sm->screens[sm->cur_menu_line]->name); + lcd_move_to(sm->lcd, 1, 0); + lcd_write_char(sm->lcd, ' '); + lcd_write_string(sm->lcd, sm->screens[sm->cur_menu_line + 1]->name); + } + } + if (button_pressed(sm->sel_btn)) { + sm->cur_screen = sm->cur_menu_line; + sm->force_draw = true; + } +} + +void screen_manager_dispatch(ScreenManager *sm, uint32_t temp, uint32_t humid) { + if (!sm->screen_count) { + errx(1, "attempt to display empty screen manager"); + } + if (sm->cur_screen < 0) { + screen_manager_dispatch_select_menu(sm); + } else { + Screen *cs = sm->screens[sm->cur_screen]; + if (cs->dispatch_func) { + SensorState state = { + .lcd = sm->lcd, + .temp = temp, + .humid = humid, + .back_down = button_pressed(sm->back_btn), + .up_down = button_pressed(sm->up_btn), + .down_down = button_pressed(sm->down_btn), + .sel_down = button_pressed(sm->sel_btn), + .force_draw = sm->force_draw, + }; + if (cs->dispatch_func(cs, &state)) { + // if this is true, it means return to the main menu + sm->cur_screen = -1; + sm->force_draw = true; + } else { + sm->force_draw = false; + } + } + } +} + +static bool stats_screen_dispatch(StatsScreen *screen, SensorState *state) { + if (state->back_down) { + return true; + } + time_t cur_time = time(NULL); + if (state->force_draw || state->temp != screen->last_temp + || state->humid != screen->last_humid + || screen->last_min != cur_time / 60) { + screen->last_temp = state->temp; + screen->last_humid = state->humid; + screen->last_min = cur_time / 60; + lcd_clear(state->lcd); + lcd_write_string(state->lcd, "temp humi time"); + char buff[17]; + int cur_len = snprintf(buff, sizeof(buff), "%4.1fF %3" PRIu32 "%% ", + DK_TO_F(state->temp), state->humid); + struct tm lt; + localtime_r(&cur_time, <); + strftime(buff + cur_len, sizeof(buff) - cur_len, + "%H:%M", <); + lcd_move_to(state->lcd, 1, 0); + lcd_write_string(state->lcd, buff); + } + return false; +} + +StatsScreen *stats_screen_new() { + StatsScreen *s = malloc_checked(sizeof(StatsScreen)); + screen_init(&s->parent, "Current Stats", + (ScreenDispatchFunc) stats_screen_dispatch, + (ScreenCleanupFunc) free); + s->last_humid = 0; + s->last_temp = 0; + return s; +} diff --git a/src/screen.h b/src/ui/screen.h similarity index 63% rename from src/screen.h rename to src/ui/screen.h index 9216b64..ea4ed18 100644 --- a/src/screen.h +++ b/src/ui/screen.h @@ -6,12 +6,13 @@ * 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_SCREEN_H + */ +#ifndef INCLUDED_SCREEN_H #define INCLUDED_SCREEN_H -#include "button.h" -#include "lcd.h" -#include "util.h" +#include "../button.h" +#include "../lcd.h" +#include "../util.h" #include #include @@ -109,68 +110,4 @@ typedef struct { */ 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; - UtilPeriod period; - DateSelection ds; - int offset_scale; - bool ulimit_reached; - bool blimit_reached; -} 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/ui/statsby.c b/src/ui/statsby.c new file mode 100644 index 0000000..fc38d92 --- /dev/null +++ b/src/ui/statsby.c @@ -0,0 +1,205 @@ +/* + * statsby.h - Screen for view stats by a specified period (day, week, etc.) + * 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 "statsby.h" +#include "../ths.h" + +#include + +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 PERIOD_HOUR: + case PERIOD_DAY: + case PERIOD_WEEK: + screen->ds.max_stage = DATE_SEL_DAY; + break; + case PERIOD_MONTH: + screen->ds.max_stage = DATE_SEL_MONTH; + break; + case PERIOD_YEAR: + screen->ds.max_stage = DATE_SEL_YEAR; + break; + } + screen->need_redraw = true; + } +} + +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; + } + if (screen->need_redraw) { + 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; + 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->offset_scale = 0; + screen->need_redraw = true; + screen->ulimit_reached = false; + break; + } + } +} + +static void stats_by_show_stats(StatsByScreen *screen, SensorState *state) { + 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) { + 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); + if (screen->period == PERIOD_HOUR && + screen->offset_scale == 0 && + screen->ds.year == screen->ds.start_time.local_year && + screen->ds.month == screen->ds.start_time.local_month && + screen->ds.day == screen->ds.start_time.local_day) { + screen->offset_scale = screen->ds.start_time.local_hour; + } + UtilAverageRange data; + if (!get_average_for_range(state->db, screen->ds.year, screen->ds.month, + screen->ds.day, screen->offset_scale, + screen->period, &data)) { + lcd_write_string(state->lcd, "Query failed!"); + warnx("failed to query average temperature and humidity"); + return; + } + screen->ulimit_reached = data.upper_bound; + screen->blimit_reached = data.lower_bound; + char period_string[17]; + switch (screen->period) { + case PERIOD_YEAR: + snprintf(period_string, 17, "Year>%d", data.year); + break; + case PERIOD_MONTH: + snprintf(period_string, 17, "Month>%d-%d", data.year, + data.month); + break; + case PERIOD_WEEK: + snprintf(period_string, 17, "Week>%d-%d-%d", data.year, + data.month, data.day); + break; + case PERIOD_DAY: + snprintf(period_string, 17, "Day>%d-%d-%d", data.year, + data.month, data.day); + break; + case PERIOD_HOUR: + snprintf(period_string, 17, "Hour>%d-%d %d:00", data.month, + data.day, data.hour); + break; + } + lcd_write_string(state->lcd, period_string); + lcd_move_to(state->lcd, 1, 0); + if (data.npoints) { + char data_string[17]; + snprintf(data_string, 17, "T:%.1fF H:%d%%", DK_TO_F(data.temp), data.humid); + lcd_write_string(state->lcd, data_string); + } else { + lcd_write_string(state->lcd, "No data!"); + } + } + if (!screen->ulimit_reached && state->up_down) { + ++screen->offset_scale; + screen->need_redraw = true; + } + if (!screen->blimit_reached && state->down_down) { + --screen->offset_scale; + screen->need_redraw = true; + } +} + +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: + stats_by_show_stats(screen, state); + 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/ui/statsby.h b/src/ui/statsby.h new file mode 100644 index 0000000..376eb25 --- /dev/null +++ b/src/ui/statsby.h @@ -0,0 +1,36 @@ +/* + * statsby.h - Screen for view stats by a specified period (day, week, etc.) + * 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_STATSBY_H +#define INCLUDED_STATSBY_H + +#include "datesel.h" + +typedef struct { + Screen parent; + enum { + STATS_BY_SELECT_PERIOD = 0, + STATS_BY_SELECT_START, + STATS_BY_SHOWING, + } stage; + bool need_redraw; + UtilPeriod period; + DateSelection ds; + int offset_scale; + bool ulimit_reached; + bool blimit_reached; +} 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