Work on the second stats screen

This commit is contained in:
Alexander Rosenberg 2024-03-05 00:54:32 -08:00
parent 81eb22e83a
commit da83874d11
Signed by: school-rpi4
GPG Key ID: 5CCFC80B0B47B04B
6 changed files with 513 additions and 11 deletions

View File

@ -2,7 +2,7 @@
.include "config.mk"
CC=clang
CFLAGS=-std=c11 -Wall ${SQLITE3_CFLAGS}
CFLAGS=-g -std=c11 -Wall ${SQLITE3_CFLAGS}
LD=clang
LDFLAGS=-lgpio -lfigpar -lpthread ${SQLITE3_LDFLAGS}
SRCS=src/main.c src/util.c src/lcd.c src/ths.c src/button.c src/screen.c

View File

@ -91,14 +91,17 @@ int main(int argc, char *const *argv) {
GLOBAL_OPTS.data_pins[7]);
setup_signals();
open_database();
initialize_util_queries(DATABASE);
pthread_t bg_update;
start_update_thread(&bg_update);
ScreenManager *screen_manager = screen_manager_new(lcd, handle,
ScreenManager *screen_manager = screen_manager_new(DATABASE,
lcd, handle,
GLOBAL_OPTS.back_pin,
GLOBAL_OPTS.up_pin,
GLOBAL_OPTS.down_pin,
GLOBAL_OPTS.sel_pin);
screen_manager_add(screen_manager, (Screen *) stats_screen_new());
screen_manager_add(screen_manager, (Screen *) stats_by_screen_new());
while (RUNNING) {
lock_stat_globals();
uint32_t temp = LAST_TEMP;
@ -115,6 +118,7 @@ int main(int argc, char *const *argv) {
err(1, "join of background thread failed");
}
// this needs to be done after bg_update exits
cleanup_util_queries();
sqlite3_close(DATABASE);
pthread_mutex_destroy(&STAT_MUTEX);
gpio_close(handle);
@ -386,12 +390,12 @@ void parse_config_file(const char *path) {
.action = parse_uint_callback,
},
{
.directive = "down_pin",
.directive = "up_pin",
.type = FIGPAR_TYPE_NONE,
.action = parse_uint_callback,
},
{
.directive = "up_pin",
.directive = "down_pin",
.type = FIGPAR_TYPE_NONE,
.action = parse_uint_callback,
},
@ -430,6 +434,9 @@ void parse_config_file(const char *path) {
static void exit_signal_callback(int sig) {
LOG_VERBOSE("Caught signal %d. Exiting...\n", sig);
RUNNING = false;
if (signal(sig, SIG_DFL) == SIG_ERR) {
err(1, "resetting signal handler failed");
}
}
#define SIGNAL_SETUP_CHECKED(sig) \
@ -497,13 +504,13 @@ static void create_db_table() {
LOG_VERBOSE("Ensured env_data table existance\n");
}
static const char *UPDATE_STATES_QUERY =
static const char *UPDATE_STATS_QUERY =
"INSERT INTO env_data (time, temp, humid) "
"VALUES(?, ?, ?);";
static void *update_thread_action(void *_ignored) {
create_db_table();
sqlite3_stmt *insert_statement = NULL;
int status = sqlite3_prepare_v2(DATABASE, UPDATE_STATES_QUERY, -1,
int status = sqlite3_prepare_v2(DATABASE, UPDATE_STATS_QUERY, -1,
&insert_statement, NULL);
if (status != SQLITE_OK) {
errx(1, "could not compile SQL query. sqlite3 error follows:\n%s",

View File

@ -8,11 +8,12 @@
* version. See the LICENSE file for more information.
*/
#include "screen.h"
#include "util.h"
#include "ths.h"
#include <err.h>
#include <inttypes.h>
#include <limits.h>
#include <string.h>
void screen_init(Screen *screen, const char *name,
@ -30,10 +31,11 @@ void screen_delete(Screen *screen) {
}
}
ScreenManager *screen_manager_new(LCD *lcd, gpio_handle_t handle,
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;
@ -133,7 +135,7 @@ void screen_manager_dispatch(ScreenManager *sm, uint32_t temp, uint32_t humid) {
}
}
bool stats_screen_dispatch(StatsScreen *screen, SensorState *state) {
static bool stats_screen_dispatch(StatsScreen *screen, SensorState *state) {
if (state->back_down) {
return true;
}
@ -168,3 +170,292 @@ StatsScreen *stats_screen_new() {
s->last_temp = 0;
return s;
}
static const char *PERIOD_LABELS[] = {
"HOUR",
"DAY",
"WEEK",
"MONTH",
"YEAR",
};
static const size_t NPERIOD = sizeof(PERIOD_LABELS) / sizeof(char *);
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 0:
case 1:
case 2:
screen->ds.max_stage = DATE_SEL_DAY;
break;
case 3:
screen->ds.max_stage = DATE_SEL_MONTH;
break;
case 4:
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->month == ds->end_time.local_year &&
ds->month == ds->end_time.local_month &&
ds->day > ds->end_time.local_day) {
printf("Clamp Day High!\n");
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->year == ds->start_time.local_year &&
ds->month == ds->start_time.local_month &&
ds->day < ds->start_time.local_day) {
printf("Clamp Day Low!\n");
ds->day = ds->start_time.local_day;
}
printf("Max: %d %d %d\n", ds->end_time.local_year, ds->end_time.local_month, ds->end_time.local_day);
printf("Min: %d %d %d\n", ds->start_time.local_year, ds->start_time.local_month, ds->start_time.local_day);
printf("Cur: %d %d %d\n", ds->year, ds->month, ds->day);
}
static void date_sel_cleanup(DateSelection *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;
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;
}
if (screen->need_redraw) {
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->need_redraw = true;
break;
}
}
}
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:
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) {
lcd_clear(state->lcd);
lcd_move_to(state->lcd, 0, 0);
lcd_write_string(state->lcd, "Some data!");
screen->need_redraw = false;
}
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;
}

View File

@ -12,11 +12,14 @@
#include "button.h"
#include "lcd.h"
#include "util.h"
#include <time.h>
#include <libgpio.h>
#include <sqlite3.h>
typedef struct {
sqlite3 *db;
LCD *lcd;
uint32_t temp;
uint32_t humid;
@ -52,6 +55,7 @@ void screen_init(Screen *screen, const char *name,
void screen_delete(Screen *screen);
typedef struct {
sqlite3 *db;
LCD *lcd;
Button *back_btn;
Button *up_btn;
@ -68,10 +72,11 @@ typedef struct {
} ScreenManager;
/*
* Crate a new ScreenManager with the given LCD and buttons
* Crate a new ScreenManager with the given DB, LCD and buttons. DB and LCD
* still owned by the caller!
* Return: the new ScreenManager
*/
ScreenManager *screen_manager_new(LCD *lcd, gpio_handle_t handle,
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);
@ -99,6 +104,71 @@ typedef struct {
time_t last_min;
} StatsScreen;
/*
* Create a new stats screen. This screen will display the current temp. and
* humidity data from a THS sensor.
*/
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;
int period;
DateSelection ds;
} 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

View File

@ -45,3 +45,103 @@ void *strdup_checked(const char *str) {
}
return new_str;
}
int days_in_month(int m, int y) {
switch (m) {
case 2:
if (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) {
return 29;
} else {
return 28;
}
break;
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
return 31;
default:
return 30;
}
}
// is this even a C program anymore?
static const char *DB_LIMITS_QUERY_STR =
"SELECT\n"
"MinUTC,\n"
// labels below are for debugging
"strftime('%Y', MinUTC, 'unixepoch') as MinUTCYear,\n"
"strftime('%m', MinUTC, 'unixepoch') as MinUTCMonth,\n"
"strftime('%e', MinUTC, 'unixepoch') as MinUTCDay,\n"
"MinLocal,\n"
"strftime('%Y', MinLocal, 'unixepoch') as MinLocalYear,\n"
"strftime('%m', MinLocal, 'unixepoch') as MinLocalMonth,\n"
"strftime('%e', MinLocal, 'unixepoch') as MinLocalDay,\n"
"MaxUTC,\n"
"strftime('%Y', MaxUTC, 'unixepoch') as MaxUTCYear,\n"
"strftime('%m', MaxUTC, 'unixepoch') as MaxUTCMonth,\n"
"strftime('%e', MaxUTC, 'unixepoch') as MaxUTCDay,\n"
"MaxLocal,\n"
"strftime('%Y', MaxLocal, 'unixepoch') as MaxLocalYear,\n"
"strftime('%m', MaxLocal, 'unixepoch') as MaxLocalMonth,\n"
"strftime('%e', MaxLocal, 'unixepoch') as MaxLocalDay\n"
"FROM\n"
"(SELECT\n"
"unixepoch(min(time), 'unixepoch', 'start of ' || ?1) as MinUTC,\n"
"unixepoch(max(time), 'unixepoch', '+1 ' || ?1, 'start of ' || ?1, '-1 second') as MaxUTC,\n"
"unixepoch(min(time), 'unixepoch', 'localtime', 'start of ' || ?1) as MinLocal,\n"
"unixepoch(max(time), 'unixepoch', 'localtime', '+1 ' || ?1, 'start of ' || ?1, '-1 second') as MaxLocal\n"
"FROM env_data);";
static sqlite3_stmt *DB_LIMITS_QUERY;
void initialize_util_queries(sqlite3 *db) {
int status = sqlite3_prepare_v2(db, DB_LIMITS_QUERY_STR, -1,
&DB_LIMITS_QUERY, NULL);
if (status != SQLITE_OK) {
errx(1, "failed to compile limits query: %s", sqlite3_errstr(status));
}
}
void cleanup_util_queries() {
sqlite3_finalize(DB_LIMITS_QUERY);
}
bool get_database_limits(sqlite3 *db, const char *period, UtilDate *start,
UtilDate *end) {
if (strcasecmp(period, "week") == 0 || strcasecmp(period, "hour") == 0) {
period = "day";
}
bool success = true;
sqlite3_bind_text(DB_LIMITS_QUERY, 1, period, -1, SQLITE_TRANSIENT);
int status = sqlite3_step(DB_LIMITS_QUERY);
if (status == SQLITE_ROW) {
if (start) {
start->utc = sqlite3_column_int64(DB_LIMITS_QUERY, 0);
start->utc_year = sqlite3_column_int64(DB_LIMITS_QUERY, 1);
start->utc_month = sqlite3_column_int64(DB_LIMITS_QUERY, 2);
start->utc_day = sqlite3_column_int64(DB_LIMITS_QUERY, 3);
start->local = sqlite3_column_int64(DB_LIMITS_QUERY, 4);
start->local_year = sqlite3_column_int64(DB_LIMITS_QUERY, 5);
start->local_month = sqlite3_column_int64(DB_LIMITS_QUERY, 6);
start->local_day = sqlite3_column_int64(DB_LIMITS_QUERY, 7);
}
if (end) {
end->utc = sqlite3_column_int64(DB_LIMITS_QUERY, 8);
end->utc_year = sqlite3_column_int64(DB_LIMITS_QUERY, 9);
end->utc_month = sqlite3_column_int64(DB_LIMITS_QUERY, 10);
end->utc_day = sqlite3_column_int64(DB_LIMITS_QUERY, 11);
end->local = sqlite3_column_int64(DB_LIMITS_QUERY, 12);
end->local_year = sqlite3_column_int64(DB_LIMITS_QUERY, 13);
end->local_month = sqlite3_column_int64(DB_LIMITS_QUERY, 14);
end->local_day = sqlite3_column_int64(DB_LIMITS_QUERY, 15);
}
} 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;
}

View File

@ -17,6 +17,7 @@
#include <stdbool.h>
#include <sys/types.h>
#include <libgpio.h>
#include <sqlite3.h>
typedef struct {
char *config_path; // path to config file
@ -76,4 +77,37 @@ void *strdup_checked(const char *str);
*/
#define LOG_VERBOSE(...) if (GLOBAL_OPTS.verbose) {fprintf(stderr, __VA_ARGS__);}
/*
* Return: the number of days in month M. 1 is January. Y is the year.
*/
int days_in_month(int m, int y);
/*
* Initialize SQL queries used by this file.
*/
void initialize_util_queries(sqlite3 *db);
/*
* Cleanup SQL queries used by this file.
*/
void cleanup_util_queries(void);
typedef struct {
int64_t utc;
int utc_year;
int utc_month;
int utc_day;
int64_t local;
int local_year;
int local_month;
int local_day;
} UtilDate;
/*
* Return the START of the first and END of the last PERIOD (ex. week) of DB.
* Return: false if an error occurred, true otherwise.
*/
bool get_database_limits(sqlite3 *db, const char *period, UtilDate *start,
UtilDate *end);
#endif