Add menu system, fix Makefile
This commit is contained in:
		
							
								
								
									
										13
									
								
								src/button.c
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								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; | ||||
| } | ||||
|  | ||||
							
								
								
									
										18
									
								
								src/button.h
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								src/button.h
									
									
									
									
									
								
							| @ -14,13 +14,9 @@ | ||||
| #include <libgpio.h> | ||||
| #include <stdbool.h> | ||||
|  | ||||
| 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 | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
							
								
								
									
										216
									
								
								src/main.c
									
									
									
									
									
								
							
							
						
						
									
										216
									
								
								src/main.c
									
									
									
									
									
								
							| @ -11,6 +11,8 @@ | ||||
| #include "util.h" | ||||
| #include "ths.h" | ||||
| #include "button.h" | ||||
| #include "menu.h" | ||||
| #include "screen.h" | ||||
|  | ||||
| #include <unistd.h> | ||||
| #include <err.h> | ||||
| @ -23,6 +25,9 @@ | ||||
| #include <time.h> | ||||
| #include <sys/time.h> | ||||
| #include <unistd.h> | ||||
| #include <pthread.h> | ||||
| #include <stdatomic.h> | ||||
| #include <signal.h> | ||||
|  | ||||
| /* | ||||
|  * 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"); | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										170
									
								
								src/screen.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								src/screen.c
									
									
									
									
									
										Normal file
									
								
							| @ -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 <err.h> | ||||
| #include <inttypes.h> | ||||
|  | ||||
|  | ||||
| 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; | ||||
| } | ||||
							
								
								
									
										104
									
								
								src/screen.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/screen.h
									
									
									
									
									
										Normal file
									
								
							| @ -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 <time.h> | ||||
| #include <libgpio.h> | ||||
|  | ||||
| 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 | ||||
| @ -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!"); | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user