564 lines
18 KiB
C

/*
* main.c - Program entry and argument handling
* 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 "lcd.h"
#include "util.h"
#include "ths.h"
#include "button.h"
#include "menu.h"
#include "ui/screen.h"
#include "ui/statsby.h"
#include <unistd.h>
#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <figpar.h>
#include <inttypes.h>
#include <time.h>
#include <sys/time.h>
#include <unistd.h>
#include <pthread.h>
#include <stdatomic.h>
#include <signal.h>
#include <sqlite3.h>
/*
* Print help message to standard output.
*/
void print_help(const char *exec_name);
/*
* Parse command line arguments and save the options to OPTS.
*/
void parse_arguments(int argc, char *const *argv);
/*
* Parse config file PATH.
*/
void parse_config_file(const char *path);
/*
* Setup signals used to ensure clean termination.
*/
void setup_signals(void);
/*
* Open an sqlite3 database connection.
*/
void open_database(void);
/*
* Read the temp. and humid., store it in the configured database, and output it
* to the given uint32_t pointers.
*/
void update_stats(THS *ths, sqlite3_stmt *insert_statement);
/*
* Start the thread used to update the temp. and humid. and record them.
*/
void start_update_thread(pthread_t *thread);
// cross thread variables
sqlite3 *DATABASE;
pthread_mutex_t STAT_MUTEX;
uint32_t LAST_TEMP, LAST_HUMID;
_Atomic bool RUNNING = true;
/*
* Lock the cross thread variables above.
*/
void lock_stat_globals(void);
/*
* Unlock the cross thread variables above.
*/
#define unlock_stat_globals() pthread_mutex_unlock(&STAT_MUTEX)
int main(int argc, char *const *argv) {
parse_arguments(argc, argv);
parse_config_file(GLOBAL_OPTS.config_path);
gpio_handle_t handle = gpio_open_device(GLOBAL_OPTS.gpio_path);
if (handle == GPIO_INVALID_HANDLE) {
err(1, "could not open GPIO device \"%s\"", GLOBAL_OPTS.gpio_path);
}
LOG_VERBOSE("Opened GPIO device: \"%s\"\n", GLOBAL_OPTS.gpio_path);
LCD *lcd = lcd_open(handle, GLOBAL_OPTS.rs_pin, GLOBAL_OPTS.rw_pin,
GLOBAL_OPTS.en_pin, GLOBAL_OPTS.data_pins[0],
GLOBAL_OPTS.data_pins[1], GLOBAL_OPTS.data_pins[2],
GLOBAL_OPTS.data_pins[3], GLOBAL_OPTS.data_pins[4],
GLOBAL_OPTS.data_pins[5], GLOBAL_OPTS.data_pins[6],
GLOBAL_OPTS.data_pins[7]);
setup_signals();
open_database();
initialize_util_queries(DATABASE);
pthread_t bg_update;
start_update_thread(&bg_update);
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;
uint32_t humid = LAST_HUMID;
unlock_stat_globals();
screen_manager_dispatch(screen_manager, temp, humid);
// 10ms is probably faster than anyone will press a button
usleep(10 * 1000);
}
screen_manager_delete(screen_manager);
lcd_close(lcd);
if (pthread_join(bg_update, NULL) != 0) {
sqlite3_close(DATABASE); // hopefully prevent data loss
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);
cleanup_options(&GLOBAL_OPTS);
return 0;
}
void print_help(const char *exec_name) {
printf("usage: %s [-h] [-vs] [-f CONFIG_PATH]\n", exec_name);
}
void parse_arguments(int argc, char *const *argv) {
int c;
while ((c = getopt(argc, argv, "hf:vs")) != -1) {
switch (c) {
case 'h':
print_help(argv[0]);
exit(0);
case 'f':
FREE_CHECKED(GLOBAL_OPTS.config_path);
GLOBAL_OPTS.config_path = optarg;
LOG_VERBOSE("Config file path set: \"%s\"\n", optarg);
break;
case 's':
GLOBAL_OPTS.strict_config = true;
LOG_VERBOSE("Strict config mode enabled\n");
break;
case 'v':
GLOBAL_OPTS.verbose = true;
LOG_VERBOSE("Verbose mode enabled\n");
break;
case '?':
default:
print_help(argv[0]);
exit(1);
}
}
if (!GLOBAL_OPTS.config_path) {
GLOBAL_OPTS.config_path = strdup_checked(DEFAULT_CONFIG_PATH);
LOG_VERBOSE("Config file path set: \"%s\"\n", GLOBAL_OPTS.config_path);
}
}
static int unknown_key_callback(struct figpar_config *opt, uint32_t line,
char *directive, char *value) {
if (GLOBAL_OPTS.strict_config) {
errx(1, "line %" PRIu32 ": unknown configuration option: \"%s\"",
line, directive);
} else {
warnx("line %" PRIu32 ": unknown configuration option: \"%s\"",
line, directive);
}
return 0;
}
static int parse_uint_callback(struct figpar_config *opt, uint32_t line,
char *directive, char *value) {
char last_char;
if (sscanf(value, "%" SCNu32 " %c", &opt->value.u_num, &last_char) < 1 ||
(last_char && !isdigit(last_char))) {
warnx("line %" PRIu32 ": not a valid number \"%s\"", line, value);
return 1;
}
LOG_VERBOSE("Loaded config uint option: %s = %" PRIu32 "\n", directive,
opt->value.u_num);
opt->type = FIGPAR_TYPE_UINT;
return 0;
}
#define CONFIG_UINT_ARR_TYPE FIGPAR_TYPE_DATA1
struct UIntArr {
uint32_t *arr;
size_t size;
};
static int parse_uint_arr_callback(struct figpar_config *opt, uint32_t line,
char *directive, char *value) {
struct UIntArr *arr = malloc_checked(sizeof(struct UIntArr));
arr->size = 0;
arr->arr = NULL;
uint32_t num;
char last_char = 1;
int jump_len;
while (last_char && sscanf(value, "%" SCNu32 " %c%n",
&num, &last_char, &jump_len) >= 1) {
if (last_char && !isdigit(last_char)) {
warnx("line %" PRIu32 ": not a valid number array \"%s\"",
line, value);
FREE_CHECKED(arr->arr);
free(arr);
return 1;
}
arr->arr = realloc_checked(arr->arr, sizeof(uint32_t) * ++arr->size);
arr->arr[arr->size - 1] = num;
value += jump_len - 1; // -1 to add back the first digit
}
opt->type = CONFIG_UINT_ARR_TYPE;
opt->value.data = arr;
if (GLOBAL_OPTS.verbose) {
fprintf(stderr, "Loaded config uint array option: %s = ", directive);
for (size_t i = 0; i < arr->size; ++i) {
fprintf(stderr, "%" PRIu32, arr->arr[i]);
if (i < arr->size - 1) {
fputc(' ', stderr);
}
}
fputc('\n', stderr);
}
return 0;
}
static int parse_str_callback(struct figpar_config *opt, uint32_t line,
char *directive, char *value) {
FREE_CHECKED(opt->value.str);
if (!value[0]) {
opt->type = FIGPAR_TYPE_STR;
opt->value.str = NULL;
} else {
opt->type = FIGPAR_TYPE_STR;
opt->value.str = strdup_checked(value);
LOG_VERBOSE("Loaded config string option: %s = %s\n", directive, value);
}
return 0;
}
static char *steal_opt_if_set(char *str) {
if (str && !*str) {
free(str);
return NULL;
}
return str;
}
#define REQUIRE_KEY(ind, name) if (entries[ind].type == FIGPAR_TYPE_NONE) {\
errx(1, "%s must be specified", # name);\
}
static void set_options_from_entries(struct figpar_config *entries,
size_t nentries) {
entries[0].type = FIGPAR_TYPE_NONE;
GLOBAL_OPTS.gpio_path = entries[0].value.str;
LOG_VERBOSE("Using gpio_path: \"%s\"\n", GLOBAL_OPTS.gpio_path);
REQUIRE_KEY(1, rs_pin);
GLOBAL_OPTS.rs_pin = entries[1].value.u_num;
REQUIRE_KEY(2, rw_pin);
GLOBAL_OPTS.rw_pin = entries[2].value.u_num;
REQUIRE_KEY(3, en_pin);
GLOBAL_OPTS.en_pin = entries[3].value.u_num;
REQUIRE_KEY(4, data_pins);
struct UIntArr *arr = entries[4].value.data;
if (arr->size != 8) {
errx(1, "data_pins must be an array of 8 uints");
}
memcpy(GLOBAL_OPTS.data_pins, arr->arr, sizeof(uint32_t) * 8);
entries[5].type = FIGPAR_TYPE_NONE;
GLOBAL_OPTS.temp_key = steal_opt_if_set(entries[5].value.str);
LOG_VERBOSE("Using temp_key: \"%s\"\n", GLOBAL_OPTS.temp_key);
entries[6].type = FIGPAR_TYPE_NONE;
GLOBAL_OPTS.humid_key = steal_opt_if_set(entries[6].value.str);
LOG_VERBOSE("Using humid_key: \"%s\"\n", GLOBAL_OPTS.humid_key);
entries[7].type = FIGPAR_TYPE_NONE;
GLOBAL_OPTS.fail_key = steal_opt_if_set(entries[7].value.str);
LOG_VERBOSE("Using fail_key: \"%s\"\n", GLOBAL_OPTS.fail_key);
GLOBAL_OPTS.fail_limit = entries[8].value.u_num;
entries[9].type = FIGPAR_TYPE_NONE;
GLOBAL_OPTS.database_location = entries[9].value.str;
LOG_VERBOSE("Using database_location: \"%s\"\n",
GLOBAL_OPTS.database_location);
GLOBAL_OPTS.refresh_time = entries[10].value.u_num;
REQUIRE_KEY(11, sel_pin);
GLOBAL_OPTS.sel_pin = entries[11].value.u_num;
REQUIRE_KEY(12, up_pin);
GLOBAL_OPTS.up_pin = entries[12].value.u_num;
REQUIRE_KEY(13, down_pin);
GLOBAL_OPTS.down_pin = entries[13].value.u_num;
REQUIRE_KEY(14, back_pin);
GLOBAL_OPTS.back_pin = entries[14].value.u_num;
}
static char *strdup_default_opt(const char *def) {
if (!*def) {
return NULL;
}
return strdup_checked(def);
}
void parse_config_file(const char *path) {
LOG_VERBOSE("Loading config file: \"%s\"\n", path);
struct figpar_config entries[] = {
{
.directive = "gpio_file",
.type = FIGPAR_TYPE_STR,
.action = parse_str_callback,
.value = {.str = strdup_default_opt(DEFAULT_GPIO_DEVICE)},
},
{
.directive = "rs_pin",
.type = FIGPAR_TYPE_NONE,
.action = parse_uint_callback,
},
{
.directive = "rw_pin",
.type = FIGPAR_TYPE_NONE,
.action = parse_uint_callback,
},
{
.directive = "en_pin",
.type = FIGPAR_TYPE_NONE,
.action = parse_uint_callback,
},
{
.directive = "data_pins",
.type = FIGPAR_TYPE_NONE,
.action = parse_uint_arr_callback,
},
{
.directive = "temp_key",
.type = FIGPAR_TYPE_STR,
.action = parse_str_callback,
.value = {.str = strdup_default_opt(DEFAULT_TEMP_KEY)},
},
{
.directive = "humid_key",
.type = FIGPAR_TYPE_STR,
.action = parse_str_callback,
.value = {.str = strdup_default_opt(DEFAULT_HUMID_KEY)},
},
{
.directive = "fail_key",
.type = FIGPAR_TYPE_STR,
.action = parse_str_callback,
.value = {.str = strdup_default_opt(DEFAULT_FAIL_KEY)},
},
{
.directive = "fail_limit",
.type = FIGPAR_TYPE_UINT,
.action = parse_uint_callback,
.value = {.u_num = DEFAULT_FAIL_LIMIT},
},
{
.directive = "database_location",
.type = FIGPAR_TYPE_STR,
.action = parse_str_callback,
.value = {.str = strdup_default_opt(DEFAULT_DATABASE_LOCATION)},
},
{
.directive = "refresh_time",
.type = FIGPAR_TYPE_UINT,
.action = parse_uint_callback,
.value = {.u_num = DEFAULT_REFRESH_TIME},
},
{
.directive = "sel_pin",
.type = FIGPAR_TYPE_NONE,
.action = parse_uint_callback,
},
{
.directive = "up_pin",
.type = FIGPAR_TYPE_NONE,
.action = parse_uint_callback,
},
{
.directive = "down_pin",
.type = FIGPAR_TYPE_NONE,
.action = parse_uint_callback,
},
{
.directive = "back_pin",
.type = FIGPAR_TYPE_NONE,
.action = parse_uint_callback,
},
{ .directive = NULL },
};
size_t entry_count = sizeof(entries) / sizeof(struct figpar_config);
errno = 0;
int status = parse_config(entries, path, unknown_key_callback,
FIGPAR_BREAK_ON_EQUALS);
if (status < 0) {
err(1, "could not parse config file: \"%s\"", path);
} else if (status > 1) {
errx(1, "could not parse config file: \"%s\"", path);
}
set_options_from_entries(entries, entry_count);
for (size_t i = 0; i < entry_count; ++i) {
switch (entries[i].type) {
case FIGPAR_TYPE_STR:
FREE_CHECKED(entries[i].value.str);
break;
case CONFIG_UINT_ARR_TYPE:
FREE_CHECKED(((struct UIntArr *) entries[i].value.data)->arr);
FREE_CHECKED(entries[i].value.data);
break;
default: ; // ignore
}
}
LOG_VERBOSE("Finished loading config file\n");
}
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) \
if (signal(sig, exit_signal_callback) == SIG_ERR) {\
err(1, "failed to setup signal %s", #sig);\
}
void setup_signals() {
SIGNAL_SETUP_CHECKED(SIGTERM);
SIGNAL_SETUP_CHECKED(SIGINT);
}
void open_database() {
int status = sqlite3_config(SQLITE_CONFIG_SERIALIZED);
if (status != SQLITE_OK) {
errx(1, "failed to enable multi-thread mode for sqlite: %s",
sqlite3_errstr(status));
}
status = sqlite3_open(GLOBAL_OPTS.database_location, &DATABASE);
if (status != SQLITE_OK) {
sqlite3_close(DATABASE);
errx(1, "failed to open database: %s",
sqlite3_errstr(status));
}
}
void update_stats(THS *ths, sqlite3_stmt *insert_statement) {
if (GLOBAL_OPTS.fail_key && ths_read_fails(ths) > GLOBAL_OPTS.fail_limit) {
errx(1, "THS fail limit reached");
}
uint32_t temp = ths_read_temp(ths);
uint32_t humid = ths_read_humid(ths);
time_t read_time = time(NULL);
LOG_VERBOSE("Temp. and humid. read\n");
lock_stat_globals();
LAST_TEMP = temp;
LAST_HUMID = humid;
unlock_stat_globals();
sqlite3_reset(insert_statement);
sqlite3_bind_int64(insert_statement, 1, read_time);
sqlite3_bind_int(insert_statement, 2, temp);
sqlite3_bind_int(insert_statement, 3, humid);
int status = sqlite3_step(insert_statement);
if (status != SQLITE_DONE) {
errx(1, "failed to insert temp. and humid. data into database: %s",
sqlite3_errstr(status));
}
}
static const char *CREATE_DB_TABLE_QUERY =
"CREATE TABLE IF NOT EXISTS env_data("
"time INTEGER PRIMARY KEY,"
"temp INTEGER,"
"humid INTEGER"
");";
static void create_db_table() {
char *errmsg;
int status = sqlite3_exec(DATABASE, CREATE_DB_TABLE_QUERY, NULL, NULL,
&errmsg);
if (status != SQLITE_OK) {
errx(1, "could not create table. sqlite3 error follows:\n%s",
errmsg ? errmsg : "No message generated");
}
LOG_VERBOSE("Ensured env_data table existance\n");
}
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_STATS_QUERY, -1,
&insert_statement, NULL);
if (status != SQLITE_OK) {
errx(1, "could not compile SQL query. sqlite3 error follows:\n%s",
sqlite3_errstr(status));
}
sigset_t to_block;
sigemptyset(&to_block);
sigaddset(&to_block, SIGTERM);
sigaddset(&to_block, SIGINT);
if (pthread_sigmask(SIG_BLOCK, &to_block, NULL) != 0) {
err(1, "failed to set background thread signal mask");
}
THS *ths = ths_open(GLOBAL_OPTS.temp_key, GLOBAL_OPTS.humid_key,
GLOBAL_OPTS.fail_key);
struct timespec last_update, cur_time, diff_time;
timespecclear(&last_update);
struct timespec refresh_time = {
.tv_sec = GLOBAL_OPTS.refresh_time / 1000,
.tv_nsec = (GLOBAL_OPTS.refresh_time % 1000) * 1000 * 1000,
};
while (RUNNING) {
clock_gettime(CLOCK_UPTIME, &cur_time);
timespecsub(&cur_time, &last_update, &diff_time);
if (timespeccmp(&diff_time, &refresh_time, >=)) {
update_stats(ths, insert_statement);
last_update = cur_time;
}
usleep(10 * 1000); // 10ms, a reasonable delay
}
sqlite3_finalize(insert_statement);
ths_close(ths);
return NULL;
}
void start_update_thread(pthread_t *thread) {
if (pthread_mutex_init(&STAT_MUTEX, NULL) != 0) {
err(1, "could not create mutex");
}
if (pthread_create(thread, NULL, &update_thread_action, NULL) != 0) {
err(1, "could not create backgroup update thread");
}
LOG_VERBOSE("Created background update thread\n");
}
void lock_stat_globals() {
if (pthread_mutex_lock(&STAT_MUTEX) != 0) {
err(1, "trying to lock mutex reported an error");
}
}