From 57974cc0250ab1877f4161e4a094160d568d515b Mon Sep 17 00:00:00 2001 From: Alexander Rosenberg Date: Fri, 1 Mar 2024 00:21:20 -0800 Subject: [PATCH] Add menu system, fix Makefile --- .gitignore | 5 +- Makefile | 26 ++++--- config.mk | 5 ++ src/button.c | 13 +--- src/button.h | 18 ++--- src/lcd.c | 2 +- src/main.c | 216 +++++++++++++++++++++++++++++++++++++++++---------- src/screen.c | 170 ++++++++++++++++++++++++++++++++++++++++ src/screen.h | 104 +++++++++++++++++++++++++ src/util.c | 3 + src/util.h | 6 ++ 11 files changed, 488 insertions(+), 80 deletions(-) create mode 100644 src/screen.c create mode 100644 src/screen.h diff --git a/.gitignore b/.gitignore index a4c519d..ef54fe1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -*.o +bin/ +*.core rpi4b-temp-humidity compile_commands.json -.cache/ \ No newline at end of file +.cache/ diff --git a/Makefile b/Makefile index d40105e..b431507 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,22 @@ -CC=clang -CFLAGS=-std=c99 -Wall -LD=clang -LDFLAGS=-lgpio -lfigpar -SRCS=src/main.c src/util.c src/lcd.c src/ths.c src/button.c -PROG=rpi4b-temp-humidity - +# -*- mode: makefile -*- .include "config.mk" +CC=clang +CFLAGS=-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 +PROG=rpi4b-temp-humidity + OBJS=${SRCS:C/^src/bin/:C/.c$/.o/} bin/${PROG}: ${OBJS} ${LD} ${LDFLAGS} -o ${@} ${OBJS} -main.o util.o lcd.o ths.o button.o: util.h -main.o lcd.o: lcd.h -main.o ths.o: ths.h -main.o button.o: button.h +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 ${OBJS}: ${.TARGET:C/^bin/src/:C/.o$/.c/} @mkdir -p bin/ @@ -27,7 +29,7 @@ ${OBJS}: ${.TARGET:C/^bin/src/:C/.o$/.c/} -DDEFAULT_FAIL_LIMIT="${DEFAULT_FAIL_LIMIT}"\ -DDEFAULT_LCD_VERSION="\"${DEFAULT_LCD_VERSION}\""\ -DDEFAULT_REFRESH_TIME="${DEFAULT_REFRESH_TIME}"\ --o ${@} ${.ALLSRC} +-o ${@} ${.TARGET:C/^bin/src/:C/.o$/.c/} clean: rm -rf bin/ diff --git a/config.mk b/config.mk index 032b9e9..f61da81 100644 --- a/config.mk +++ b/config.mk @@ -1,3 +1,8 @@ +# Library and compile options +SQLITE3_LDFLAGS=-L/usr/local/lib -lsqlite3 +SQLITE3_CFLAGS=-I/usr/local/include + +# Default option values, empty means NULL (for strings) DEFAULT_CONFIG_PATH=config.conf DEFAULT_GPIO_DEVICE=/dev/gpioc0 DEFAULT_TEMP_KEY=dev.gpioths.0.temperature diff --git a/src/button.c b/src/button.c index 8f76d6d..52d4746 100644 --- a/src/button.c +++ b/src/button.c @@ -17,8 +17,7 @@ static const struct timespec DEBOUNCE_TIME = { .tv_nsec = 10 * 1000 * 1000, // 10ms }; -Button *button_new(gpio_handle_t handle, gpio_pin_t pin, ButtonAction action, - void *user_data) { +Button *button_new(gpio_handle_t handle, gpio_pin_t pin) { Button *button = malloc_checked(sizeof(Button)); button->handle = handle; button->pin = pin; @@ -27,8 +26,6 @@ Button *button_new(gpio_handle_t handle, gpio_pin_t pin, ButtonAction action, .g_flags = GPIO_PIN_INPUT | GPIO_PIN_PULLUP, }; gpio_pin_set_flags(handle, &cfg); - button->action = action; - button->user_data = user_data; button->is_down = false; timespecclear(&button->last_detect); return button; @@ -38,7 +35,7 @@ void button_delete(Button *button) { free(button); } -int button_dispatch(Button *button) { +bool button_pressed(Button *button) { struct timespec cur_time, diff_time; clock_gettime(CLOCK_UPTIME, &cur_time); timespecsub(&cur_time, &button->last_detect, &diff_time); @@ -47,13 +44,11 @@ int button_dispatch(Button *button) { if (!button->is_down && status == GPIO_PIN_LOW) { button->is_down = true; button->last_detect= cur_time; - if (button->action) { - return button->action(button->user_data); - } + return true; } else if (button->is_down && status == GPIO_PIN_HIGH) { button->last_detect = cur_time; button->is_down = false; } } - return 0; + return false; } diff --git a/src/button.h b/src/button.h index b8415a8..c02140d 100644 --- a/src/button.h +++ b/src/button.h @@ -14,13 +14,9 @@ #include #include -typedef int(*ButtonAction)(void *); - typedef struct { gpio_handle_t handle; gpio_pin_t pin; - ButtonAction action; - void *user_data; struct timespec last_detect; bool is_down; @@ -28,24 +24,20 @@ typedef struct { /* * Create a Button object with the given HANDLE, PIN and ACTION. USER_DATA will - * be passed to ACTION every time it is run. It is up to the caller to free - * USER_DATA when the Button is freed. + * be passed to ACTION every time it is run. * Return: the created Button. */ -Button *button_new(gpio_handle_t uandle, gpio_pin_t pin, ButtonAction action, - void *user_data); +Button *button_new(gpio_handle_t handle, gpio_pin_t pin); /* * Delete BUTTON. - * NOTE: This does NOT free USER_DATA. */ void button_delete(Button *button); /* - * Check if the pin for this button is HIGH, if it is, call ACTION and prevent - * it from being called again until button becomes LOW. - * Return: 0 if nothing is done, otherwise, the return value of ACTION. + * Return: true if BUTTON is down */ -int button_dispatch(Button *button); +bool button_pressed(Button *button); + #endif diff --git a/src/lcd.c b/src/lcd.c index d05cc75..c11b0fb 100644 --- a/src/lcd.c +++ b/src/lcd.c @@ -68,7 +68,7 @@ LCD *lcd_open(gpio_handle_t handle, gpio_pin_t rs, gpio_pin_t rw, gpio_pin_t en, lcd_call(lcd, 0, 0, 0, 0, 1, 1, 1, 0, 0); // 5x8 font, 2 lines, 8bit lcd_entry_mode(lcd, LCD_INCREMENT, LCD_CURSOR_MOVE); lcd_display_control(lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, LCD_DISPLAY_ON); - LOG_VERBOSE("Initializing LCD: rs=%d, rw=%d, en=%d, d={%d, %d, %d, %d, %d," + LOG_VERBOSE("Initializing LCD: rs=%d, rw=%d, en=%d, d={%d, %d, %d, %d, %d, " "%d, %d, %d}\n", rs, rw, en, d0, d1, d2, d3, d4, d5, d6, d7); return lcd; } diff --git a/src/main.c b/src/main.c index 216b886..ed79904 100644 --- a/src/main.c +++ b/src/main.c @@ -11,6 +11,8 @@ #include "util.h" #include "ths.h" #include "button.h" +#include "menu.h" +#include "screen.h" #include #include @@ -23,6 +25,9 @@ #include #include #include +#include +#include +#include /* * Print help message to standard output. @@ -36,6 +41,33 @@ void parse_arguments(int argc, char *const *argv); * Parse config file PATH. */ void parse_config_file(const char *path); +/* + * Read the temp. and humid., store it in the configured database, and output it + * to the given uint32_t pointers. + */ +void update_stats(THS *ths, struct timespec *cur_time); +/* + * Setup signals used to ensure clean termination. + */ +void setup_signals(void); +/* + * Start the thread used to update the temp. and humid. and record them. + */ +void start_update_thread(pthread_t *thread); + +// cross thread variables +pthread_mutex_t STAT_MUTEX; +uint32_t LAST_TEMP, LAST_HUMID; +time_t LAST_READ_SEC; +_Atomic bool RUNNING = true; +/* + * Lock the cross thread variables above. + */ +void lock_stat_globals(void); +/* + * Unlock the cross thread variables above. + */ +#define unlock_stat_globals() pthread_mutex_unlock(&STAT_MUTEX) int main(int argc, char *const *argv) { parse_arguments(argc, argv); @@ -52,54 +84,38 @@ int main(int argc, char *const *argv) { GLOBAL_OPTS.data_pins[3], GLOBAL_OPTS.data_pins[4], GLOBAL_OPTS.data_pins[5], GLOBAL_OPTS.data_pins[6], GLOBAL_OPTS.data_pins[7]); - lcd_display_control(lcd, LCD_CURSOR_BLINK, LCD_CURSOR_ON, LCD_DISPLAY_ON); - THS *ths = ths_open(GLOBAL_OPTS.temp_key, GLOBAL_OPTS.humid_key, - GLOBAL_OPTS.fail_key); - if (!GLOBAL_OPTS.fail_key) { - warnx("it's probably a bad idea to not set fail_key"); - } - struct timespec last_update, cur_time, diff_time; - timespecclear(&last_update); - struct timespec refresh_time = { - .tv_sec = GLOBAL_OPTS.refresh_time / 1000, - .tv_nsec = (GLOBAL_OPTS.refresh_time % 1000) * 1000 * 1000, - }; - Button *btn = button_new(handle, 23, (ButtonAction) printf, "Button Pressed!\n"); - while (true) { - clock_gettime(CLOCK_UPTIME, &cur_time); - timespecsub(&cur_time, &last_update, &diff_time); - if (timespeccmp(&diff_time, &refresh_time, >=)) { - if (GLOBAL_OPTS.fail_key && ths_read_fails(ths) > GLOBAL_OPTS.fail_limit) { - errx(1, "THS fail limit reached"); - } - lcd_clear(lcd); - lcd_write_string(lcd, "temp humi time"); - char buff[17]; - int cur_len = snprintf(buff, sizeof(buff), "%4.1fF %3" PRIu32 "%% ", - DK_TO_F(ths_read_temp(ths)), ths_read_humid(ths)); - struct tm lt; - localtime_r(&cur_time.tv_sec, <); - strftime(buff + cur_len, sizeof(buff) - cur_len, - "%H:%M", <); - lcd_move_to(lcd, 1, 0); - lcd_write_string(lcd, buff); - LOG_VERBOSE("Temp. and humid. display updated\n"); - last_update = cur_time; - } - button_dispatch(btn); + setup_signals(); + pthread_t bg_update; + start_update_thread(&bg_update); + ScreenManager *screen_manager = screen_manager_new(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()); + while (RUNNING) { + lock_stat_globals(); + uint32_t temp = LAST_TEMP; + uint32_t humid = LAST_HUMID; + unlock_stat_globals(); + screen_manager_dispatch(screen_manager, temp, humid); // 10ms is probably faster than anyone will press a button usleep(10 * 1000); } - button_delete(btn); - ths_close(ths); + screen_manager_delete(screen_manager); lcd_close(lcd); + if (pthread_join(bg_update, NULL) != 0) { + err(1, "join of background thread failed"); + } + // this needs to be done after bg_update exits + pthread_mutex_destroy(&STAT_MUTEX); gpio_close(handle); cleanup_options(&GLOBAL_OPTS); return 0; } void print_help(const char *exec_name) { - printf("usage: %s [-h] [-v] [-s] [-f CONFIG_PATH]\n", exec_name); + printf("usage: %s [-h] [-vs] [-f CONFIG_PATH]\n", exec_name); } void parse_arguments(int argc, char *const *argv) { @@ -269,6 +285,25 @@ static void set_options_from_entries(struct figpar_config *entries, LOG_VERBOSE("Using lcd_version: \"%s\"\n", GLOBAL_OPTS.lcd_version); GLOBAL_OPTS.refresh_time = entries[10].value.u_num; + + REQUIRE_KEY(11, sel_pin); + GLOBAL_OPTS.sel_pin = entries[11].value.u_num; + + REQUIRE_KEY(12, up_pin); + GLOBAL_OPTS.up_pin = entries[12].value.u_num; + + REQUIRE_KEY(13, down_pin); + GLOBAL_OPTS.down_pin = entries[13].value.u_num; + + REQUIRE_KEY(14, back_pin); + GLOBAL_OPTS.back_pin = entries[14].value.u_num; +} + +static char *strdup_default_opt(const char *def) { + if (!*def) { + return NULL; + } + return strdup_checked(def); } void parse_config_file(const char *path) { @@ -278,7 +313,7 @@ void parse_config_file(const char *path) { .directive = "gpio_file", .type = FIGPAR_TYPE_STR, .action = parse_str_callback, - .value = {.str = strdup_checked(DEFAULT_GPIO_DEVICE)}, + .value = {.str = strdup_default_opt(DEFAULT_GPIO_DEVICE)}, }, { .directive = "rs_pin", @@ -304,19 +339,19 @@ void parse_config_file(const char *path) { .directive = "temp_key", .type = FIGPAR_TYPE_STR, .action = parse_str_callback, - .value = {.str = strdup_checked(DEFAULT_TEMP_KEY)}, + .value = {.str = strdup_default_opt(DEFAULT_TEMP_KEY)}, }, { .directive = "humid_key", .type = FIGPAR_TYPE_STR, .action = parse_str_callback, - .value = {.str = strdup_checked(DEFAULT_HUMID_KEY)}, + .value = {.str = strdup_default_opt(DEFAULT_HUMID_KEY)}, }, { .directive = "fail_key", .type = FIGPAR_TYPE_STR, .action = parse_str_callback, - .value = {.str = strdup_checked(DEFAULT_FAIL_KEY)}, + .value = {.str = strdup_default_opt(DEFAULT_FAIL_KEY)}, }, { .directive = "fail_limit", @@ -328,7 +363,7 @@ void parse_config_file(const char *path) { .directive = "lcd_version", .type = FIGPAR_TYPE_STR, .action = parse_str_callback, - .value = {.str = strdup_checked(DEFAULT_LCD_VERSION)}, + .value = {.str = strdup_default_opt(DEFAULT_LCD_VERSION)}, }, { .directive = "refresh_time", @@ -336,6 +371,26 @@ void parse_config_file(const char *path) { .action = parse_uint_callback, .value = {.u_num = DEFAULT_REFRESH_TIME}, }, + { + .directive = "sel_pin", + .type = FIGPAR_TYPE_NONE, + .action = parse_uint_callback, + }, + { + .directive = "down_pin", + .type = FIGPAR_TYPE_NONE, + .action = parse_uint_callback, + }, + { + .directive = "up_pin", + .type = FIGPAR_TYPE_NONE, + .action = parse_uint_callback, + }, + { + .directive = "back_pin", + .type = FIGPAR_TYPE_NONE, + .action = parse_uint_callback, + }, { .directive = NULL }, }; size_t entry_count = sizeof(entries) / sizeof(struct figpar_config); @@ -362,3 +417,78 @@ void parse_config_file(const char *path) { } LOG_VERBOSE("Finished loading config file\n"); } + +static void exit_signal_callback(int sig) { + LOG_VERBOSE("Caught signal %d. Exiting...\n", sig); + RUNNING = false; +} + +#define SIGNAL_SETUP_CHECKED(sig) \ +if (signal(sig, exit_signal_callback) == SIG_ERR) {\ +err(1, "failed to setup signal %s", #sig);\ +} +void setup_signals() { + SIGNAL_SETUP_CHECKED(SIGTERM); + SIGNAL_SETUP_CHECKED(SIGINT); +} + +void update_stats(THS *ths, struct timespec *cur_time) { + if (GLOBAL_OPTS.fail_key && ths_read_fails(ths) > GLOBAL_OPTS.fail_limit) { + errx(1, "THS fail limit reached"); + } + uint32_t temp = ths_read_temp(ths); + uint32_t humid = ths_read_humid(ths); + LOG_VERBOSE("Temp. and humid. read\n"); + // TODO store in database + + lock_stat_globals(); + LAST_TEMP = temp; + LAST_HUMID = humid; + LAST_READ_SEC = cur_time->tv_sec; + unlock_stat_globals(); +} + +static void *update_thread_action(void *_ignored) { + sigset_t to_block; + sigemptyset(&to_block); + sigaddset(&to_block, SIGTERM); + sigaddset(&to_block, SIGINT); + if (pthread_sigmask(SIG_BLOCK, &to_block, NULL) != 0) { + err(1, "failed to set background thread signal mask"); + } + THS *ths = ths_open(GLOBAL_OPTS.temp_key, GLOBAL_OPTS.humid_key, + GLOBAL_OPTS.fail_key); + struct timespec last_update, cur_time, diff_time; + timespecclear(&last_update); + struct timespec refresh_time = { + .tv_sec = GLOBAL_OPTS.refresh_time / 1000, + .tv_nsec = (GLOBAL_OPTS.refresh_time % 1000) * 1000 * 1000, + }; + while (RUNNING) { + clock_gettime(CLOCK_UPTIME, &cur_time); + timespecsub(&cur_time, &last_update, &diff_time); + if (timespeccmp(&diff_time, &refresh_time, >=)) { + update_stats(ths, &cur_time); + last_update = cur_time; + } + usleep(10 * 1000); // 10ms, a reasonable delay + } + ths_close(ths); + return NULL; +} + +void start_update_thread(pthread_t *thread) { + if (pthread_mutex_init(&STAT_MUTEX, NULL) != 0) { + err(1, "could not create mutex"); + } + if (pthread_create(thread, NULL, &update_thread_action, NULL) != 0) { + err(1, "could not create backgroup update thread"); + } + LOG_VERBOSE("Created background update thread\n"); +} + +void lock_stat_globals() { + if (pthread_mutex_lock(&STAT_MUTEX) != 0) { + err(1, "trying to lock mutex reported an error"); + } +} diff --git a/src/screen.c b/src/screen.c new file mode 100644 index 0000000..115611c --- /dev/null +++ b/src/screen.c @@ -0,0 +1,170 @@ +/* + * 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 "util.h" +#include "ths.h" + +#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(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->lcd = lcd; + sm->screens = NULL; + sm->screen_count = 0; + sm->cur_scren = -1; + 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_scren = 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_scren < 0) { + screen_manager_dispatch_select_menu(sm); + } else { + Screen *cs = sm->screens[sm->cur_scren]; + 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_scren = -1; + sm->force_draw = true; + } else { + sm->force_draw = false; + } + } + } +} + +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/screen.h new file mode 100644 index 0000000..7e77c55 --- /dev/null +++ b/src/screen.h @@ -0,0 +1,104 @@ +/* + * screen.h - 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. + */ +#ifndef INCLUDED_SCREEN_H +#define INCLUDED_SCREEN_H + +#include "button.h" +#include "lcd.h" + +#include +#include + +typedef struct { + LCD *lcd; + uint32_t temp; + uint32_t humid; + bool back_down; + bool up_down; + bool down_down; + bool sel_down; + bool force_draw; +} SensorState; + +typedef struct _Screen Screen; +// should return true if we are to go back +typedef bool (*ScreenDispatchFunc)(Screen *, SensorState *); +typedef void (*ScreenCleanupFunc)(Screen *); + +struct _Screen { + char *name; + ScreenDispatchFunc dispatch_func; + ScreenCleanupFunc cleanup_func; +}; + +/* + * Init Screen with NAME and the given callbacks. + * Return: the new Screen + */ +void screen_init(Screen *screen, const char *name, + ScreenDispatchFunc dispatch_func, + ScreenCleanupFunc cleanup_func); + +/* + * Cleanup screen by calling its cleanup func and then freeing its name + */ +void screen_delete(Screen *screen); + +typedef struct { + LCD *lcd; + Button *back_btn; + Button *up_btn; + Button *down_btn; + Button *sel_btn; + Screen **screens; + size_t screen_count; + + // internal + ssize_t cur_scren; + size_t cur_menu_line; + size_t last_drawn_menu_line; + bool force_draw; +} ScreenManager; + +/* + * Crate a new ScreenManager with the given LCD and buttons + * Return: the new ScreenManager + */ +ScreenManager *screen_manager_new(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); + +/* + * Delete ScreenManager and the buttons it manages. This does NOT delete it's + * LCD. + */ +void screen_manager_delete(ScreenManager *sm); + +/* + * Add SCREEN to SM. + */ +void screen_manager_add(ScreenManager *sm, Screen *screen); + +/* + * Process input and, if needed, redraw the screen for ScreenManager. TEMP and + * HUMID should be the last values read by the connected THS. + */ +void screen_manager_dispatch(ScreenManager *sm, uint32_t temp, uint32_t humid); + +typedef struct { + Screen parent; + uint32_t last_temp; + uint32_t last_humid; + time_t last_min; +} StatsScreen; + +StatsScreen *stats_screen_new(void); + +#endif diff --git a/src/util.c b/src/util.c index dd4f2a1..0e68bf6 100644 --- a/src/util.c +++ b/src/util.c @@ -36,6 +36,9 @@ void *realloc_checked(void *old_ptr, size_t n) { } void *strdup_checked(const char *str) { + if (!str) { + return NULL; + } char *new_str = strdup(str); if (!new_str) { errx(1, "out of memory!"); diff --git a/src/util.h b/src/util.h index 514d016..6b24570 100644 --- a/src/util.h +++ b/src/util.h @@ -34,6 +34,12 @@ typedef struct { char *fail_key; // sysctl key for number of fails uint32_t fail_limit; // limit for number of failures before exit uint32_t refresh_time; // time between temp and humid refresh + + // menu navigation pins + gpio_pin_t up_pin; + gpio_pin_t down_pin; + gpio_pin_t back_pin; + gpio_pin_t sel_pin; } Options; extern Options GLOBAL_OPTS;