348 lines
12 KiB
C
348 lines
12 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 "config.h"
|
|
#include "ui/screen.h"
|
|
#include "ui/statsby.h"
|
|
#include "ui/datapoints.h"
|
|
#include "ui/statrange.h"
|
|
#include "ui/blankscreen.h"
|
|
#include "ui/exportscreen.h"
|
|
#include "ui/viewdatescreen.h"
|
|
#include "ui/setdatescreen.h"
|
|
#include "ui/powerscreen.h"
|
|
#include "ui/settzscreen.h"
|
|
#include "ui/cleardatascreen.h"
|
|
|
|
#include <unistd.h>
|
|
#include <err.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <figpar.h>
|
|
#include <time.h>
|
|
#include <sys/time.h>
|
|
#include <unistd.h>
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <sqlite3.h>
|
|
#include <libgen.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);
|
|
/*
|
|
* Setup signals used to ensure clean termination.
|
|
*/
|
|
void setup_signals(void);
|
|
/*
|
|
* Open the sqlite3 database connection.
|
|
*/
|
|
void open_database(void);
|
|
/*
|
|
* Create the env_data table in DATABASE
|
|
*/
|
|
void create_db_table(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)
|
|
|
|
// should we restart instead of exiting
|
|
bool SHOULD_RESTART = false;
|
|
bool NEED_CLEAR_TZ = false;
|
|
|
|
int main(int argc, char *const *argv) {
|
|
parse_arguments(argc, argv);
|
|
parse_config_file(GLOBAL_OPTS.config_path);
|
|
if (GLOBAL_OPTS.timezone) {
|
|
if (!getenv("TZ")) {
|
|
setenv("TZ", GLOBAL_OPTS.timezone, true);
|
|
NEED_CLEAR_TZ = true;
|
|
} else {
|
|
LOG_VERBOSE("Config timezone option shadowed by local environment variable\n");
|
|
}
|
|
}
|
|
tzset();
|
|
|
|
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], GLOBAL_OPTS.bl_pin);
|
|
setup_signals();
|
|
open_database();
|
|
create_db_table();
|
|
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());
|
|
screen_manager_add(screen_manager, (Screen *) data_points_screen_new());
|
|
screen_manager_add(screen_manager, (Screen *) stat_range_screen_new());
|
|
if (GLOBAL_OPTS.export_file_name) {
|
|
screen_manager_add(screen_manager, (Screen *) export_screen_new());
|
|
}
|
|
if (GLOBAL_OPTS.bl_pin >= 0) {
|
|
screen_manager_add(screen_manager, blank_screen_new());
|
|
}
|
|
screen_manager_add(screen_manager, (Screen *) power_screen_new());
|
|
screen_manager_add(screen_manager, (Screen *) view_date_screen_new());
|
|
screen_manager_add(screen_manager, (Screen *) set_date_screen_new());
|
|
screen_manager_add(screen_manager, (Screen *) set_tz_screen_new());
|
|
screen_manager_add(screen_manager, (Screen *) clear_data_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);
|
|
if (SHOULD_RESTART) {
|
|
LOG_VERBOSE("Re-invoking...\n");
|
|
if (NEED_CLEAR_TZ) {
|
|
unsetenv("TZ");
|
|
}
|
|
execv(argv[0], argv);
|
|
err(1, "re-invoke with execv(2) failed");
|
|
}
|
|
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(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(STRINGIFY(DEFAULT_CONFIG_PATH));
|
|
LOG_VERBOSE("Config file path set: \"%s\"\n", GLOBAL_OPTS.config_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, func) \
|
|
if (signal(sig, func) == SIG_ERR) {\
|
|
err(1, "failed to setup signal %s", #sig);\
|
|
}
|
|
void setup_signals() {
|
|
SIGNAL_SETUP_CHECKED(SIGTERM, exit_signal_callback);
|
|
SIGNAL_SETUP_CHECKED(SIGINT, exit_signal_callback);
|
|
SIGNAL_SETUP_CHECKED(SIGUSR1, SIG_IGN); // used to kill export operations
|
|
}
|
|
|
|
static const char *CREATE_DB_TABLE_QUERY =
|
|
"CREATE TABLE IF NOT EXISTS env_data("
|
|
"time INTEGER PRIMARY KEY,"
|
|
"temp INTEGER,"
|
|
"humid INTEGER"
|
|
");";
|
|
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");
|
|
}
|
|
|
|
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));
|
|
}
|
|
char *to_free = strdup_checked(GLOBAL_OPTS.database_location);
|
|
char *db_dir = dirname(to_free);
|
|
if (!mkdirs(db_dir, 0755)) {
|
|
errx(1, "failed to create database directory: %s", db_dir);
|
|
}
|
|
free(to_free);
|
|
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));
|
|
}
|
|
LOG_VERBOSE("Successfully opened database at \"%s\"\n",
|
|
GLOBAL_OPTS.database_location);
|
|
}
|
|
|
|
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) {
|
|
warnx("failed to insert temp. and humid. data into database: %s",
|
|
sqlite3_errstr(status));
|
|
lock_stat_globals();
|
|
// this means error
|
|
LAST_TEMP = UINT32_MAX;
|
|
LAST_HUMID = UINT32_MAX;
|
|
unlock_stat_globals();
|
|
}
|
|
}
|
|
|
|
static const char *UPDATE_STATS_QUERY =
|
|
"INSERT INTO env_data (time, temp, humid) "
|
|
"VALUES(?, ?, ?);";
|
|
static void *update_thread_action(void *_ignored) {
|
|
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");
|
|
}
|
|
}
|