/* * 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 #include #include #include #include #include #include #include #include #include #include #include /* * 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"); } }