Add stats-by menu
This commit is contained in:
parent
375f8494f4
commit
be0040bb2f
19
Makefile
19
Makefile
@ -4,9 +4,9 @@
|
||||
CC=clang
|
||||
CFLAGS=-g -std=c11 -Wall ${SQLITE3_CFLAGS}
|
||||
LD=clang
|
||||
LDFLAGS=-lgpio -lfigpar -lgeom -lpthread ${SQLITE3_LDFLAGS}
|
||||
LDFLAGS=-lgpio -lfigpar -lpthread ${SQLITE3_LDFLAGS}
|
||||
SRCS=src/main.c src/util.c src/lcd.c src/ths.c src/button.c src/ui/screen.c\
|
||||
src/ui/datesel.c src/ui/statsby.c
|
||||
src/ui/datesel.c src/ui/statsby.c src/ui/datapoints.c src/ui/timesel.c
|
||||
PROG=rpi4b-temp-humidity
|
||||
|
||||
OBJS=${SRCS:C/^src/bin/:C/.c$/.o/}
|
||||
@ -15,15 +15,24 @@ bin/${PROG}: ${OBJS}
|
||||
|
||||
bin/main.o bin/util.o bin/lcd.o bin/ths.o bin/button.o: src/util.h
|
||||
bin/ui/screen.o bin/ui/statsby.o bin/ui/datesel.o: src/util.h
|
||||
bin/ui/datapoints.o bin/ui/timesel.o: src/util.h
|
||||
|
||||
bin/main.o bin/lcd.o bin/screen.o bin/ui/datesel.o: src/lcd.h
|
||||
bin/ui/statsby.o: src/lcd.h
|
||||
bin/ui/statsby.o bin/ui/timesel.o: src/lcd.h
|
||||
|
||||
bin/main.o bin/ui/screen.o bin/ui/statsby.o: src/ui/screen.h
|
||||
bin/ui/datesel.o bin/ui/datapoints.o bin/ui/timesel.o: src/ui/screen.h
|
||||
|
||||
bin/main.o bin/ui/datesel.o bin/ui/statsby.o: src/ui/datesel.h
|
||||
bin/ui/datapoints.o bin/ui/timesel.o: src/ui/datesel.h
|
||||
|
||||
bin/main.o bin/ths.o bin/ui/screen.o bin/ui/statsby.o: src/ths.h
|
||||
bin/ui/timesel.o: src/ths.h
|
||||
|
||||
bin/main.o bin/ui/timesel.o bin/ui/datapoints.o: src/ui/timesel.h
|
||||
bin/main.o bin/button.o bin/ui/screen.o: src/button.h
|
||||
bin/main.o bin/ui/screen.o bin/ui/statsby.o bin/ui/datesel.o: src/ui/screen.h
|
||||
bin/main.o bin/ui/datesel.o bin/ui/statsby.o: src/ui/datesel.h
|
||||
bin/main.o bin/ui/statsby.o: src/ui/statsby.h
|
||||
bin/main.o bin/ui/datapoints.o: src/ui/datapoints.h
|
||||
|
||||
${OBJS}: ${.TARGET:C/^bin/src/:C/.o$/.c/}
|
||||
@mkdir -p ${.TARGET:H}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user