Refactor ui code
This commit is contained in:
		| @ -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 <unistd.h> | ||||
| #include <err.h> | ||||
|  | ||||
							
								
								
									
										519
									
								
								src/screen.c
									
									
									
									
									
								
							
							
						
						
									
										519
									
								
								src/screen.c
									
									
									
									
									
								
							| @ -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 <err.h> | ||||
| #include <inttypes.h> | ||||
| #include <limits.h> | ||||
| #include <string.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(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; | ||||
| } | ||||
							
								
								
									
										166
									
								
								src/ui/datesel.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								src/ui/datesel.c
									
									
									
									
									
										Normal file
									
								
							| @ -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 <string.h> | ||||
|  | ||||
| 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; | ||||
| } | ||||
							
								
								
									
										59
									
								
								src/ui/datesel.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/ui/datesel.h
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
							
								
								
									
										172
									
								
								src/ui/screen.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								src/ui/screen.c
									
									
									
									
									
										Normal file
									
								
							| @ -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 <err.h> | ||||
| #include <inttypes.h> | ||||
| #include <limits.h> | ||||
| #include <string.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(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; | ||||
| } | ||||
| @ -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 <time.h> | ||||
| #include <libgpio.h> | ||||
| @ -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 | ||||
							
								
								
									
										205
									
								
								src/ui/statsby.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								src/ui/statsby.c
									
									
									
									
									
										Normal file
									
								
							| @ -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 <err.h> | ||||
|  | ||||
| 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; | ||||
| } | ||||
							
								
								
									
										36
									
								
								src/ui/statsby.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/ui/statsby.h
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
		Reference in New Issue
	
	Block a user