Add stats-by menu
This commit is contained in:
		| @ -14,6 +14,7 @@ | ||||
| #include "menu.h" | ||||
| #include "ui/screen.h" | ||||
| #include "ui/statsby.h" | ||||
| #include "ui/datapoints.h" | ||||
|  | ||||
| #include <unistd.h> | ||||
| #include <err.h> | ||||
| @ -103,6 +104,7 @@ int main(int argc, char *const *argv) { | ||||
|                                                        GLOBAL_OPTS.sel_pin); | ||||
|     screen_manager_add(screen_manager, (Screen *) stats_screen_new()); | ||||
|     screen_manager_add(screen_manager, (Screen *) stats_by_screen_new()); | ||||
|     screen_manager_add(screen_manager, (Screen *) data_points_screen_new()); | ||||
|     while (RUNNING) { | ||||
|         lock_stat_globals(); | ||||
|         uint32_t temp = LAST_TEMP; | ||||
|  | ||||
							
								
								
									
										164
									
								
								src/ui/datapoints.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								src/ui/datapoints.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,164 @@ | ||||
| /* | ||||
|  * datapoints.c - Screen for viewing individual data points | ||||
|  * 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 "datapoints.h" | ||||
| #include "../ths.h" | ||||
|  | ||||
| #include <inttypes.h> | ||||
| #include <time.h> | ||||
| #include <err.h> | ||||
|  | ||||
| static bool data_points_select_date(DataPointsScreen *screen, | ||||
|                                     SensorState *state) { | ||||
|     if (state->up_down || state->down_down || state->sel_down || state->back_down) { | ||||
|         screen->need_redraw = true; | ||||
|     } | ||||
|     if (screen->need_redraw) { | ||||
|         screen->need_redraw = false; | ||||
|         DateSelectionState dss = date_sel_dispatch(&screen->ds, state, | ||||
|                                                    PERIOD_DAY, "Date:"); | ||||
|         switch (dss) { | ||||
|         case DATE_SEL_CONTINUE: | ||||
|             break; // ignore | ||||
|         case DATE_SEL_DONE: | ||||
|             ++screen->stage; | ||||
|             screen->need_redraw = true; | ||||
|             break; | ||||
|         case DATE_SEL_BACK: | ||||
|         case DATE_SEL_ERROR: | ||||
|             screen->need_redraw = true; | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| static void data_points_calculate_initial_time(DataPointsScreen *screen) { | ||||
|     struct tm broken_time = { | ||||
|         .tm_sec = 0, | ||||
|         .tm_min = screen->ts.minute, | ||||
|         .tm_hour = screen->ts.hour, | ||||
|         .tm_mday = screen->ds.day, | ||||
|         .tm_mon = screen->ds.month - 1, | ||||
|         .tm_year = screen->ds.year - 1900, | ||||
|         .tm_isdst = -1, | ||||
|     }; | ||||
|     time_t it = mktime(&broken_time); | ||||
|     if (it == -1) { | ||||
|         warnx("could not convert selected date and time"); | ||||
|         screen->cur_point = 0; | ||||
|     } else { | ||||
|         screen->cur_point = it; | ||||
|     } | ||||
| } | ||||
|  | ||||
| static void data_points_select_time(DataPointsScreen *screen, | ||||
|                                     SensorState *state) { | ||||
|     if (state->up_down || state->down_down || state->sel_down || state->back_down) { | ||||
|         screen->need_redraw = true; | ||||
|     } | ||||
|     if (screen->need_redraw) { | ||||
|         screen->need_redraw = false; | ||||
|         TimeSelState tss = time_sel_dispatch(&screen->ts, state); | ||||
|         switch (tss) { | ||||
|         case TIME_SEL_CONTINUE: | ||||
|             break; // ignore | ||||
|         case TIME_SEL_DONE: | ||||
|             ++screen->stage; | ||||
|             screen->need_redraw = true; | ||||
|             data_points_calculate_initial_time(screen); | ||||
|             break; | ||||
|         case TIME_SEL_BACK: | ||||
|         case TIME_SEL_ERROR: | ||||
|             screen->need_redraw = true; | ||||
|             --screen->stage; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| static void data_points_show(DataPointsScreen *screen, SensorState *state) { | ||||
|     if (state->back_down) { | ||||
|         screen->stage = DP_STAGE_DATE; | ||||
|         screen->need_redraw = true; | ||||
|         return; | ||||
|     } | ||||
|     if (state->up_down || state->down_down) { | ||||
|         screen->need_redraw = true; | ||||
|     } | ||||
|     if (screen->need_redraw) { | ||||
|         screen->need_redraw = false; | ||||
|         UtilDataPointInfo info; | ||||
|         if (!get_data_point_info(state->db, screen->cur_point, &info)) { | ||||
|             warnx("could not get info for data point: %" PRIi64 "", screen->cur_point); | ||||
|             screen->stage = DP_STAGE_DATE; | ||||
|             screen->need_redraw = true; | ||||
|             return; | ||||
|         } | ||||
|         screen->cur_point = info.time; | ||||
|         int temp = info.temp; | ||||
|         int humid = info.humid; | ||||
|         if (state->up_down && info.has_next) { | ||||
|             screen->cur_point = info.next_time; | ||||
|             temp = info.next_temp; | ||||
|             humid = info.next_humid; | ||||
|         } else if (state->down_down && info.has_prev) { | ||||
|             screen->cur_point = info.prev_time; | ||||
|             temp = info.prev_temp; | ||||
|             humid = info.prev_humid; | ||||
|         } | ||||
|         struct tm broken_time; | ||||
|         localtime_r(&screen->cur_point, &broken_time); | ||||
|         char str_time[17]; | ||||
|         if (!strftime(str_time, 17, "%y/%m/%d %H:%M", &broken_time)) { | ||||
|             warnx("could not format time: %" PRIi64 "", screen->cur_point); | ||||
|             screen->stage = DP_STAGE_DATE; | ||||
|             screen->need_redraw = true; | ||||
|             return; | ||||
|         } | ||||
|         lcd_clear(state->lcd); | ||||
|         lcd_move_to(state->lcd, 0, 0); | ||||
|         lcd_write_string(state->lcd, str_time); | ||||
|         char data_line[17]; | ||||
|         snprintf(data_line, 17, "%02ds %.1fF %d%%", broken_time.tm_sec, | ||||
|                  DK_TO_F(temp), humid); | ||||
|         lcd_move_to(state->lcd, 1, 0); | ||||
|         lcd_write_string(state->lcd, data_line); | ||||
|     } | ||||
| } | ||||
|  | ||||
| static bool data_points_screen_dispatch(DataPointsScreen *screen, SensorState *state) { | ||||
|     if (state->force_draw) { | ||||
|         screen->need_redraw = true; | ||||
|     } | ||||
|     switch (screen->stage) { | ||||
|     case DP_STAGE_DATE: | ||||
|         return data_points_select_date(screen, state); | ||||
|     case DP_STAGE_TIME: | ||||
|         data_points_select_time(screen, state); | ||||
|         return false; | ||||
|     case DP_STAGE_SHOW: | ||||
|         data_points_show(screen, state); | ||||
|         return false; | ||||
|     default: | ||||
|         warnx("Attempt to show bad data points screen stage"); | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
|  | ||||
| DataPointsScreen *data_points_screen_new() { | ||||
|     DataPointsScreen *s = malloc_checked(sizeof(DataPointsScreen)); | ||||
|     screen_init(&s->parent, "Data Points", | ||||
|                 (ScreenDispatchFunc) data_points_screen_dispatch, | ||||
|                 (ScreenCleanupFunc) free); | ||||
|     s->need_redraw = true; | ||||
|     s->stage = DP_STAGE_DATE; | ||||
|     date_sel_init(&s->ds, DATE_SEL_DAY); | ||||
|     time_sel_init(&s->ts, &s->ds); | ||||
|     return s; | ||||
| } | ||||
							
								
								
									
										35
									
								
								src/ui/datapoints.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/ui/datapoints.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| /* | ||||
|  * datapoints.h - Screen for viewing individual data points | ||||
|  * 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_DATAPOINTS_H | ||||
| #define INCLUDED_DATAPOINTS_H | ||||
|  | ||||
| #include "datesel.h" | ||||
| #include "timesel.h" | ||||
|  | ||||
| typedef struct { | ||||
|     Screen parent; | ||||
|     bool need_redraw; | ||||
|     enum { | ||||
|         DP_STAGE_DATE = 0, | ||||
|         DP_STAGE_TIME, | ||||
|         DP_STAGE_SHOW, | ||||
|     } stage; | ||||
|     DateSelection ds; | ||||
|     TimeSelection ts; | ||||
|     int64_t cur_point; | ||||
| } DataPointsScreen; | ||||
|  | ||||
| /* | ||||
|  * Create a new data points screen. This screen will display individual | ||||
|  * temperature and humidity data points. | ||||
|  */ | ||||
| DataPointsScreen *data_points_screen_new(void); | ||||
|  | ||||
| #endif | ||||
| @ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * datesel.h - Date selector | ||||
|  * datesel.c - Date selector | ||||
|  * Copyright (C) 2024  Alexander Rosenberg | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify it under | ||||
| @ -10,12 +10,10 @@ | ||||
| #include "datesel.h" | ||||
|  | ||||
| #include <string.h> | ||||
| #include <err.h> | ||||
|  | ||||
| void date_sel_init(DateSelection *ds, int start_line, int start_col, | ||||
|                    DateSelectionStage max_stage) { | ||||
| void date_sel_init(DateSelection *ds, 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); | ||||
| @ -99,7 +97,23 @@ static void date_sel_add_units(DateSelection *ds, int n) { | ||||
|     date_sel_cleanup(ds); | ||||
| } | ||||
|  | ||||
| DateSelectionState date_sel_dispatch(DateSelection *ds, SensorState *state) { | ||||
| DateSelectionState date_sel_dispatch(DateSelection *ds, | ||||
|                                      SensorState *state, | ||||
|                                      UtilPeriod limit_period, | ||||
|                                      const char *label) { | ||||
|     UtilDate start, end; | ||||
|     if (!get_database_limits(state->db, limit_period, &start, &end)) { | ||||
|         warnx("failed to query database limits"); | ||||
|         lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, | ||||
|                             LCD_DISPLAY_ON); | ||||
|         return DATE_SEL_ERROR; | ||||
|     } | ||||
|     ds->start_time = start; | ||||
|     ds->end_time = end; | ||||
|     lcd_clear(state->lcd); | ||||
|     lcd_move_to(state->lcd, 0, 0); | ||||
|     lcd_write_string(state->lcd, label); | ||||
|     lcd_move_to(state->lcd, 1, 0); | ||||
|     if (ds->year == -1) { | ||||
|         time_t utc_time = time(NULL); | ||||
|         struct tm local_time; | ||||
| @ -121,6 +135,8 @@ DateSelectionState date_sel_dispatch(DateSelection *ds, SensorState *state) { | ||||
|     } | ||||
|     if (state->back_down) { | ||||
|         if (ds->stage == DATE_SEL_YEAR) { | ||||
|             lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, | ||||
|                                 LCD_DISPLAY_ON); | ||||
|             return DATE_SEL_BACK; | ||||
|         } | ||||
|         --ds->stage; | ||||
| @ -130,11 +146,13 @@ DateSelectionState date_sel_dispatch(DateSelection *ds, SensorState *state) { | ||||
|     } | ||||
|     if (state->sel_down) { | ||||
|         if (ds->stage == ds->max_stage) { | ||||
|             lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, | ||||
|                                 LCD_DISPLAY_ON); | ||||
|             return DATE_SEL_DONE; | ||||
|         } | ||||
|         ++ds->stage; | ||||
|     } | ||||
|     int buf_size = 17 - ds->start_col; | ||||
|     int buf_size = 17; | ||||
|     char buff[buf_size]; | ||||
|     int cursor_pos; | ||||
|     const char *format; | ||||
| @ -153,14 +171,15 @@ DateSelectionState date_sel_dispatch(DateSelection *ds, SensorState *state) { | ||||
|         break; | ||||
|     default: | ||||
|         LOG_VERBOSE("Date selector tried to select bad field\n"); | ||||
|         lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, | ||||
|                             LCD_DISPLAY_ON); | ||||
|         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_move_to(state->lcd, 1, 0); | ||||
|     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); | ||||
|     lcd_move_to(state->lcd, 1, cursor_pos); | ||||
|     return DATE_SEL_CONTINUE; | ||||
| } | ||||
|  | ||||
| @ -25,8 +25,6 @@ typedef struct { | ||||
|     int day; | ||||
|     DateSelectionStage stage; | ||||
|     DateSelectionStage max_stage; | ||||
|     int start_line; | ||||
|     int start_col; | ||||
|     UtilDate start_time; | ||||
|     UtilDate end_time; | ||||
| } DateSelection; | ||||
| @ -34,16 +32,15 @@ typedef struct { | ||||
| typedef enum { | ||||
|     DATE_SEL_BACK, | ||||
|     DATE_SEL_CONTINUE, | ||||
|     DATE_SEL_DONE | ||||
|     DATE_SEL_DONE, | ||||
|     DATE_SEL_ERROR | ||||
| } 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 | ||||
|  * Initialize a DateSelection. MAX_STAGE is the most specific field | ||||
|  * to select. | ||||
|  */ | ||||
| void date_sel_init(DateSelection *ds, int start_line, int start_col, | ||||
|                    DateSelectionStage max_stage); | ||||
| void date_sel_init(DateSelection *ds, DateSelectionStage max_stage); | ||||
|  | ||||
| /* | ||||
|  * Reset the date on DS. | ||||
| @ -54,6 +51,9 @@ 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); | ||||
| DateSelectionState date_sel_dispatch(DateSelection *ds, | ||||
|                                      SensorState *state, | ||||
|                                      UtilPeriod limit_period, | ||||
|                                      const char *label); | ||||
|  | ||||
| #endif | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| /* | ||||
|  * statsby.h - Screen for view stats by a specified period (day, week, etc.) | ||||
|  * statsby.c - 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 | ||||
| @ -59,36 +59,24 @@ static void stats_by_select_start(StatsByScreen *screen, SensorState *state) { | ||||
|         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); | ||||
|         DateSelectionState dss = date_sel_dispatch(&screen->ds, state, | ||||
|                                                    screen->period, "Start:"); | ||||
|         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->blimit_reached = false; | ||||
|             screen->ulimit_reached = false; | ||||
|             break; | ||||
|         case DATE_SEL_BACK: | ||||
|         case DATE_SEL_ERROR: | ||||
|             --screen->stage; | ||||
|             screen->need_redraw = true; | ||||
|             break; | ||||
|         } | ||||
|     }  | ||||
| } | ||||
| @ -200,6 +188,8 @@ StatsByScreen *stats_by_screen_new() { | ||||
|                 (ScreenDispatchFunc) stats_by_screen_dispatch, | ||||
|                 (ScreenCleanupFunc) free); | ||||
|     s->need_redraw = true; | ||||
|     date_sel_init(&s->ds, 1, 0, DATE_SEL_DAY); | ||||
|     s->stage = STATS_BY_SELECT_PERIOD; | ||||
|     s->period = PERIOD_HOUR; | ||||
|     date_sel_init(&s->ds, DATE_SEL_DAY); | ||||
|     return s; | ||||
| } | ||||
|  | ||||
							
								
								
									
										118
									
								
								src/ui/timesel.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								src/ui/timesel.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,118 @@ | ||||
| /* | ||||
|  * timesel.c - Time 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 "timesel.h" | ||||
|  | ||||
| #include <err.h> | ||||
|  | ||||
| void time_sel_init(TimeSelection *ts, DateSelection *ds) { | ||||
|     ts->hour = 0; | ||||
|     ts->minute = 0; | ||||
|     ts->hour_set = false; | ||||
|     ts->ds = ds; | ||||
| } | ||||
|  | ||||
| static void time_sel_clamp(TimeSelection *ts) { | ||||
|     if (ts->ds) { | ||||
|         if (ts->ds->end_time.local_year == ts->ds->year && | ||||
|             ts->ds->end_time.local_month == ts->ds->month && | ||||
|             ts->ds->end_time.local_day == ts->ds->day) { | ||||
|             if (ts->hour > ts->ds->end_time.local_hour) { | ||||
|                 ts->hour = ts->ds->end_time.local_hour; | ||||
|             } | ||||
|             if (ts->hour == ts->ds->end_time.local_hour && | ||||
|                 ts->minute > ts->ds->end_time.local_minute) { | ||||
|                 ts->minute = ts->ds->end_time.local_minute; | ||||
|             } | ||||
|         } else if (ts->ds->start_time.local_year == ts->ds->year && | ||||
|                    ts->ds->start_time.local_month == ts->ds->month && | ||||
|                    ts->ds->start_time.local_day == ts->ds->day) { | ||||
|             if (ts->hour < ts->ds->start_time.local_hour) { | ||||
|                 ts->hour = ts->ds->start_time.local_hour; | ||||
|             } | ||||
|             if (ts->hour == ts->ds->start_time.local_hour && | ||||
|                 ts->minute < ts->ds->start_time.local_minute) { | ||||
|                 ts->minute = ts->ds->start_time.local_minute; | ||||
|             } | ||||
|         } | ||||
|     }  | ||||
| } | ||||
|  | ||||
| static void time_sel_add(TimeSelection *ts, int amount) { | ||||
|     if (!ts->hour_set) { | ||||
|         ts->hour = (ts->hour + amount) % 24; | ||||
|     } else { | ||||
|         if (ts->minute + amount >= 60) { | ||||
|             ts->hour = (ts->hour + 1) % 24; | ||||
|         } else if (ts->minute + amount < 0) { | ||||
|             ts->hour = (ts->hour - 1) % 24; | ||||
|         } | ||||
|         ts->minute = (ts->minute + amount) % 60; | ||||
|     } | ||||
|     if (ts->hour < 0) { | ||||
|         ts->hour = 24 + ts->hour; | ||||
|     } | ||||
|     if (ts->minute < 0) { | ||||
|         ts->minute = 60 + ts->minute; | ||||
|     } | ||||
|     time_sel_clamp(ts); | ||||
| } | ||||
|  | ||||
| TimeSelState time_sel_dispatch(TimeSelection *ts, SensorState *state) { | ||||
|     if (state->back_down) { | ||||
|         if (ts->hour_set) { | ||||
|             ts->hour_set = false; | ||||
|         } else { | ||||
|             lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, | ||||
|                                 LCD_DISPLAY_ON); | ||||
|             return TIME_SEL_BACK; | ||||
|         } | ||||
|     } | ||||
|     if (state->sel_down) { | ||||
|         if (!ts->hour_set) { | ||||
|             ts->hour_set = true; | ||||
|         } else { | ||||
|             lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, | ||||
|                                 LCD_DISPLAY_ON); | ||||
|             return TIME_SEL_DONE; | ||||
|         } | ||||
|     } | ||||
|     if (ts->ds) { | ||||
|         UtilDate start, end; | ||||
|         if (!get_database_limits(state->db, PERIOD_DAY, &start, &end)) { | ||||
|             warnx("failed to query database limits"); | ||||
|             lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, | ||||
|                                 LCD_DISPLAY_ON); | ||||
|             return TIME_SEL_ERROR; | ||||
|         } | ||||
|         ts->ds->start_time = start; | ||||
|         ts->ds->end_time = end; | ||||
|     } | ||||
|     time_sel_add(ts, state->up_down - state->down_down); | ||||
|     lcd_clear(state->lcd); | ||||
|     lcd_move_to(state->lcd, 0, 0); | ||||
|     lcd_write_string(state->lcd, "Time:"); | ||||
|     const char *format; | ||||
|     int cursor_pos; | ||||
|     if (!ts->hour_set) { | ||||
|         format = ">%02d:%02d"; | ||||
|         cursor_pos = 0; | ||||
|     } else { | ||||
|         format = "%02d:>%02d"; | ||||
|         cursor_pos = 3; | ||||
|     } | ||||
|     char time_line[17]; | ||||
|     snprintf(time_line, 17, format, ts->hour, ts->minute); | ||||
|     lcd_move_to(state->lcd, 1, 0); | ||||
|     lcd_write_string(state->lcd, time_line); | ||||
|     lcd_move_to(state->lcd, 1, cursor_pos); | ||||
|     lcd_display_control(state->lcd, LCD_CURSOR_BLINK, LCD_CURSOR_ON, | ||||
|                         LCD_DISPLAY_ON); | ||||
|     return TIME_SEL_CONTINUE; | ||||
| } | ||||
							
								
								
									
										43
									
								
								src/ui/timesel.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/ui/timesel.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | ||||
| /* | ||||
|  * timesel.h - Time 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_TIMESEL_H | ||||
| #define INCLUDED_TIMESEL_H | ||||
|  | ||||
| #include "../util.h" | ||||
| #include "screen.h" | ||||
| #include "datesel.h" | ||||
|  | ||||
| typedef struct { | ||||
|     DateSelection *ds; | ||||
|     int hour; | ||||
|     bool hour_set; | ||||
|     int minute; | ||||
| } TimeSelection; | ||||
|  | ||||
| typedef enum { | ||||
|     TIME_SEL_BACK, | ||||
|     TIME_SEL_CONTINUE, | ||||
|     TIME_SEL_DONE, | ||||
|     TIME_SEL_ERROR, | ||||
| } TimeSelState; | ||||
|  | ||||
| /* | ||||
|  * Initialize the TimeSelection TS. Optionally, get the year, month, and day, | ||||
|  * and limits from DS. | ||||
|  */ | ||||
| void time_sel_init(TimeSelection *ts, DateSelection *ds); | ||||
|  | ||||
| /* | ||||
|  * Dispatch (process input and draw) the TimeSelection TS according to STATE. | ||||
|  * Return: the current state of the selection (back, continue, done). | ||||
|  */ | ||||
| TimeSelState time_sel_dispatch(TimeSelection *ts, SensorState *state); | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										120
									
								
								src/util.c
									
									
									
									
									
								
							
							
						
						
									
										120
									
								
								src/util.c
									
									
									
									
									
								
							| @ -11,6 +11,7 @@ | ||||
|  | ||||
| #include <err.h> | ||||
| #include <string.h> | ||||
| #include <stdlib.h> | ||||
|  | ||||
| const char *PERIOD_LABELS[] = { | ||||
|     "HOUR", | ||||
| @ -86,21 +87,25 @@ static const char *DB_LIMITS_QUERY_STR = | ||||
|     "strftime('%m', MinUTC, 'unixepoch') as MinUTCMonth,\n" | ||||
|     "strftime('%e', MinUTC, 'unixepoch') as MinUTCDay,\n" | ||||
|     "strftime('%H', RealMin, 'unixepoch') as MinUTCHour,\n" | ||||
|     "strftime('%M', RealMin, 'unixepoch') as MinUTCMin,\n" | ||||
|     "MinLocal,\n" | ||||
|     "strftime('%Y', MinLocal, 'unixepoch') as MinLocalYear,\n" | ||||
|     "strftime('%m', MinLocal, 'unixepoch') as MinLocalMonth,\n" | ||||
|     "strftime('%e', MinLocal, 'unixepoch') as MinLocalDay,\n" | ||||
|     "strftime('%H', RealMin, 'unixepoch', 'localtime') as MinLocalHour,\n" | ||||
|     "strftime('%M', RealMin, 'unixepoch', 'localtime') as MinLocalMin,\n" | ||||
|     "MaxUTC,\n" | ||||
|     "strftime('%Y', MaxUTC, 'unixepoch') as MaxUTCYear,\n" | ||||
|     "strftime('%m', MaxUTC, 'unixepoch') as MaxUTCMonth,\n" | ||||
|     "strftime('%e', MaxUTC, 'unixepoch') as MaxUTCDay,\n" | ||||
|     "strftime('%H', RealMax, 'unixepoch') as MaxUTCHour,\n" | ||||
|     "strftime('%M', RealMax, 'unixepoch') as MaxUTCMin,\n" | ||||
|     "MaxLocal,\n" | ||||
|     "strftime('%Y', MaxLocal, 'unixepoch') as MaxLocalYear,\n" | ||||
|     "strftime('%m', MaxLocal, 'unixepoch') as MaxLocalMonth,\n" | ||||
|     "strftime('%e', MaxLocal, 'unixepoch') as MaxLocalDay,\n" | ||||
|     "strftime('%H', RealMax, 'unixepoch', 'localtime') as MaxLocalHour\n" | ||||
|     "strftime('%H', RealMax, 'unixepoch', 'localtime') as MaxLocalHour,\n" | ||||
|     "strftime('%M', RealMax, 'unixepoch', 'localtime') as MaxLocalMin\n" | ||||
|     "FROM\n" | ||||
|     "(SELECT\n" | ||||
|     "RealMax, RealMin,\n" | ||||
| @ -110,7 +115,7 @@ static const char *DB_LIMITS_QUERY_STR = | ||||
|     "unixepoch(RealMax, 'unixepoch', 'localtime', '+1 ' || ?1, 'start of ' || ?1, '-1 second') as MaxLocal\n" | ||||
|     "FROM (SELECT max(time) as RealMax, min(time) as RealMin FROM env_data));"; | ||||
| static sqlite3_stmt *DB_LIMITS_QUERY; | ||||
| const char *AVG_FOR_RANGE_QUERY_STR = | ||||
| static const char *AVG_FOR_RANGE_QUERY_STR = | ||||
|     "SELECT\n" | ||||
|     "strftime('%Y', stime, 'unixepoch', 'localtime') as y,\n" | ||||
|     "strftime('%m', stime, 'unixepoch', 'localtime') as m,\n" | ||||
| @ -131,6 +136,13 @@ const char *AVG_FOR_RANGE_QUERY_STR = | ||||
|     "WHERE time > stime\n" | ||||
|     "AND time < etime);\n"; | ||||
| static sqlite3_stmt *AVG_FOR_RANGE_QUERY; | ||||
| static const char *DATA_POINT_QUERY_STR = | ||||
|     "SELECT max(time) IS NULL, max(time), temp, humid FROM env_data WHERE time < ?1\n" | ||||
|     "UNION ALL\n" | ||||
|     "SELECT min(time) IS NULL, min(time), temp, humid FROM env_data WHERE time > ?1\n" | ||||
|     "UNION ALL\n" | ||||
|     "SELECT false, * FROM env_data WHERE time == ?1;"; | ||||
| static sqlite3_stmt *DATA_POINT_QUERY; | ||||
| void initialize_util_queries(sqlite3 *db) { | ||||
|     int status = sqlite3_prepare_v2(db, DB_LIMITS_QUERY_STR, -1, | ||||
|                                     &DB_LIMITS_QUERY, NULL); | ||||
| @ -142,21 +154,27 @@ void initialize_util_queries(sqlite3 *db) { | ||||
|     if (status != SQLITE_OK) { | ||||
|         errx(1, "failed to compile range average query: %s", sqlite3_errstr(status)); | ||||
|     } | ||||
|     status = sqlite3_prepare_v2(db, DATA_POINT_QUERY_STR, -1, | ||||
|                                 &DATA_POINT_QUERY, NULL); | ||||
|     if (status != SQLITE_OK) { | ||||
|         errx(1, "failed to compile data point query: %s", sqlite3_errstr(status)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void cleanup_util_queries() { | ||||
|     sqlite3_finalize(DB_LIMITS_QUERY); | ||||
|     sqlite3_finalize(AVG_FOR_RANGE_QUERY); | ||||
|     sqlite3_finalize(DATA_POINT_QUERY); | ||||
| } | ||||
|  | ||||
|  | ||||
| bool get_database_limits(sqlite3 *db, const char *period, UtilDate *start, | ||||
| bool get_database_limits(sqlite3 *db, UtilPeriod period, UtilDate *start, | ||||
|                          UtilDate *end) { | ||||
|     if (strcasecmp(period, "week") == 0 || strcasecmp(period, "hour") == 0) { | ||||
|         period = "day"; | ||||
|     if (period == PERIOD_WEEK || period == PERIOD_HOUR) { | ||||
|         period = PERIOD_DAY; | ||||
|     } | ||||
|     bool success = true; | ||||
|     sqlite3_bind_text(DB_LIMITS_QUERY, 1, period, -1, SQLITE_STATIC); | ||||
|     sqlite3_bind_text(DB_LIMITS_QUERY, 1, PERIOD_LABELS[period], -1, | ||||
|                       SQLITE_STATIC); | ||||
|     int status = sqlite3_step(DB_LIMITS_QUERY); | ||||
|     if (status == SQLITE_ROW) { | ||||
|         if (start) { | ||||
| @ -165,29 +183,31 @@ bool get_database_limits(sqlite3 *db, const char *period, UtilDate *start, | ||||
|             start->utc_month = sqlite3_column_int64(DB_LIMITS_QUERY, 2); | ||||
|             start->utc_day = sqlite3_column_int64(DB_LIMITS_QUERY, 3); | ||||
|             start->utc_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 4); | ||||
|             start->local = sqlite3_column_int64(DB_LIMITS_QUERY, 5); | ||||
|             start->local_year = sqlite3_column_int64(DB_LIMITS_QUERY, 6); | ||||
|             start->local_month = sqlite3_column_int64(DB_LIMITS_QUERY, 7); | ||||
|             start->local_day = sqlite3_column_int64(DB_LIMITS_QUERY, 8); | ||||
|             start->local_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 9); | ||||
|             start->utc_minute = sqlite3_column_int64(DB_LIMITS_QUERY, 5); | ||||
|             start->local = sqlite3_column_int64(DB_LIMITS_QUERY, 6); | ||||
|             start->local_year = sqlite3_column_int64(DB_LIMITS_QUERY, 7); | ||||
|             start->local_month = sqlite3_column_int64(DB_LIMITS_QUERY, 8); | ||||
|             start->local_day = sqlite3_column_int64(DB_LIMITS_QUERY, 9); | ||||
|             start->local_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 10); | ||||
|             start->local_minute = sqlite3_column_int64(DB_LIMITS_QUERY, 11); | ||||
|         } | ||||
|         if (end) { | ||||
|             end->utc = sqlite3_column_int64(DB_LIMITS_QUERY, 10); | ||||
|             end->utc_year = sqlite3_column_int64(DB_LIMITS_QUERY, 11); | ||||
|             end->utc_month = sqlite3_column_int64(DB_LIMITS_QUERY, 12); | ||||
|             end->utc_day = sqlite3_column_int64(DB_LIMITS_QUERY, 13); | ||||
|             end->utc_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 14); | ||||
|             end->local = sqlite3_column_int64(DB_LIMITS_QUERY, 15); | ||||
|             end->local_year = sqlite3_column_int64(DB_LIMITS_QUERY, 16); | ||||
|             end->local_month = sqlite3_column_int64(DB_LIMITS_QUERY, 17); | ||||
|             end->local_day = sqlite3_column_int64(DB_LIMITS_QUERY, 18); | ||||
|             end->local_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 19); | ||||
|             end->utc = sqlite3_column_int64(DB_LIMITS_QUERY, 12); | ||||
|             end->utc_year = sqlite3_column_int64(DB_LIMITS_QUERY, 13); | ||||
|             end->utc_month = sqlite3_column_int64(DB_LIMITS_QUERY, 14); | ||||
|             end->utc_day = sqlite3_column_int64(DB_LIMITS_QUERY, 15); | ||||
|             end->utc_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 16); | ||||
|             end->utc_minute = sqlite3_column_int64(DB_LIMITS_QUERY, 17); | ||||
|             end->local = sqlite3_column_int64(DB_LIMITS_QUERY, 18); | ||||
|             end->local_year = sqlite3_column_int64(DB_LIMITS_QUERY, 19); | ||||
|             end->local_month = sqlite3_column_int64(DB_LIMITS_QUERY, 20); | ||||
|             end->local_day = sqlite3_column_int64(DB_LIMITS_QUERY, 21); | ||||
|             end->local_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 22); | ||||
|             end->local_minute = sqlite3_column_int64(DB_LIMITS_QUERY, 23); | ||||
|         } | ||||
|     } else { | ||||
|         success = false; | ||||
|     } | ||||
|     // unbind the string so it can be freed by the caller | ||||
|     sqlite3_bind_null(DB_LIMITS_QUERY, 1); | ||||
|     sqlite3_reset(DB_LIMITS_QUERY); | ||||
|     return success; | ||||
| } | ||||
| @ -227,3 +247,55 @@ bool get_average_for_range(sqlite3 *db, int year, int month, int day, | ||||
|     sqlite3_reset(AVG_FOR_RANGE_QUERY); | ||||
|     return success; | ||||
| } | ||||
|  | ||||
| bool get_data_point_info(sqlite3 *db, int64_t time, UtilDataPointInfo *info) { | ||||
|     sqlite3_bind_int64(DATA_POINT_QUERY, 1, time); | ||||
|     if (sqlite3_step(DATA_POINT_QUERY) != SQLITE_ROW) { | ||||
|         sqlite3_reset(DATA_POINT_QUERY); | ||||
|         return false; | ||||
|     } | ||||
|     info->has_prev = !sqlite3_column_int(DATA_POINT_QUERY, 0); | ||||
|     info->prev_time = sqlite3_column_int64(DATA_POINT_QUERY, 1); | ||||
|     info->prev_temp = sqlite3_column_int64(DATA_POINT_QUERY, 2); | ||||
|     info->prev_humid = sqlite3_column_int64(DATA_POINT_QUERY, 3); | ||||
|     if (sqlite3_step(DATA_POINT_QUERY) != SQLITE_ROW) { | ||||
|         sqlite3_reset(DATA_POINT_QUERY); | ||||
|         return false; | ||||
|     } | ||||
|     info->has_next = !sqlite3_column_int(DATA_POINT_QUERY, 0); | ||||
|     info->next_time = sqlite3_column_int64(DATA_POINT_QUERY, 1); | ||||
|     info->next_temp = sqlite3_column_int64(DATA_POINT_QUERY, 2); | ||||
|     info->next_humid = sqlite3_column_int64(DATA_POINT_QUERY, 3); | ||||
|     int status = sqlite3_step(DATA_POINT_QUERY); | ||||
|     if (status == SQLITE_DONE) { | ||||
|         // we need to select the closes data point | ||||
|         int64_t closest_time; | ||||
|         if (!info->has_next && !info->has_prev) { | ||||
|             // no data points | ||||
|             return false; | ||||
|         } else if (info->has_next && !info->has_prev) { | ||||
|             closest_time = info->next_time; | ||||
|         } else if (info->has_prev && !info->has_next) { | ||||
|             closest_time = info->prev_time; | ||||
|         } else { | ||||
|             int64_t next_dist = labs(time - info->next_time); | ||||
|             int64_t prev_dist = labs(time - info->prev_time); | ||||
|             if (next_dist < prev_dist) { | ||||
|                 closest_time = info->next_time; | ||||
|             } else { | ||||
|                 closest_time = info->prev_time; | ||||
|             } | ||||
|         } | ||||
|         sqlite3_reset(DATA_POINT_QUERY); | ||||
|         return get_data_point_info(db, closest_time, info); | ||||
|     } else if (status == SQLITE_ROW) { | ||||
|         info->time = sqlite3_column_int64(DATA_POINT_QUERY, 1); | ||||
|         info->temp = sqlite3_column_int64(DATA_POINT_QUERY, 2); | ||||
|         info->humid = sqlite3_column_int64(DATA_POINT_QUERY, 3); | ||||
|         sqlite3_reset(DATA_POINT_QUERY); | ||||
|         return true; | ||||
|     } else { | ||||
|         sqlite3_reset(DATA_POINT_QUERY); | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										26
									
								
								src/util.h
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								src/util.h
									
									
									
									
									
								
							| @ -98,11 +98,13 @@ typedef struct { | ||||
|     int utc_month; | ||||
|     int utc_day; | ||||
|     int utc_hour; | ||||
|     int utc_minute; | ||||
|     int64_t local; | ||||
|     int local_year; | ||||
|     int local_month; | ||||
|     int local_day; | ||||
|     int local_hour; | ||||
|     int local_minute; | ||||
| } UtilDate; | ||||
|  | ||||
| enum { | ||||
| @ -118,9 +120,10 @@ extern const size_t NPERIOD; | ||||
|  | ||||
| /* | ||||
|  * Return the START of the first and END of the last PERIOD (ex. week) of DB. | ||||
|  * Also get the hour and minute limits for the first and last day. | ||||
|  * Return: false if an error occurred, true otherwise. | ||||
|  */ | ||||
| bool get_database_limits(sqlite3 *db, const char *period, UtilDate *start, | ||||
| bool get_database_limits(sqlite3 *db, UtilPeriod period, UtilDate *start, | ||||
|                          UtilDate *end); | ||||
|  | ||||
| typedef struct { | ||||
| @ -144,4 +147,25 @@ bool get_average_for_range(sqlite3 *db, int year, int month, int day, | ||||
|                            int64_t count, UtilPeriod period, | ||||
|                            UtilAverageRange *data); | ||||
|  | ||||
| typedef struct { | ||||
|     int64_t time; | ||||
|     int temp; | ||||
|     int humid; | ||||
|     bool has_next; | ||||
|     int64_t next_time; | ||||
|     int next_temp; | ||||
|     int next_humid; | ||||
|     bool has_prev; | ||||
|     int64_t prev_time; | ||||
|     int prev_temp; | ||||
|     int prev_humid; | ||||
| } UtilDataPointInfo; | ||||
|  | ||||
| /* | ||||
|  * Get the data, next point, and previous point for data point TIME. INFO must | ||||
|  * not be NULL. | ||||
|  * Return: false if an error occurred, true otherwise. | ||||
|  */ | ||||
| bool get_data_point_info(sqlite3 *db, int64_t time, UtilDataPointInfo *info); | ||||
|  | ||||
| #endif | ||||
|  | ||||
		Reference in New Issue
	
	Block a user