diff --git a/.gitignore b/.gitignore index ef54fe1..a5bcd5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ bin/ *.core -rpi4b-temp-humidity compile_commands.json .cache/ +config.conf diff --git a/Makefile b/Makefile index 3618790..263ef01 100644 --- a/Makefile +++ b/Makefile @@ -11,10 +11,14 @@ SRCS=src/main.c src/util.c src/lcd.c src/ths.c src/button.c src/ui/screen.c\ src/ui/exportscreen.c PROG=rpi4b-temp-humidity +all: config.conf bin/${PROG} + OBJS=${SRCS:C/^src/bin/:C/.c$/.o/} bin/${PROG}: ${OBJS} ${LD} ${LDFLAGS} -o ${@} ${OBJS} +bin/main.o bin/config.o: config.mk + 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 bin/ui/statrange.o: src/util.h @@ -30,7 +34,6 @@ bin/ui/statrange.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 bin/ui/statrange.o: src/ui/datesel.h - bin/main.o bin/ui/timesel.o bin/ui/datapoints.o: src/ui/timesel.h bin/ui/statrange.o: src/ui/timesel.h @@ -49,23 +52,34 @@ bin/main.o bin/drive.o bin/ui/exportscreen.o: src/drive.h .error Invalid temperature unit "${DEFAULT_TEMP_UNIT}" .endif +DEFINES:=\ +-DDEFAULT_CONFIG_PATH="${DEFAULT_CONFIG_PATH}"\ +-DDEFAULT_GPIO_DEVICE="${DEFAULT_GPIO_DEVICE}"\ +-DDEFAULT_TEMP_KEY="${DEFAULT_TEMP_KEY}"\ +-DDEFAULT_HUMID_KEY="${DEFAULT_HUMID_KEY}"\ +-DDEFAULT_FAIL_KEY="${DEFAULT_FAIL_KEY}"\ +-DDEFAULT_FAIL_LIMIT="${DEFAULT_FAIL_LIMIT}"\ +-DDEFAULT_DATABASE_LOCATION="${DEFAULT_DATABASE_LOCATION}"\ +-DDEFAULT_REFRESH_TIME="${DEFAULT_REFRESH_TIME}"\ +-DDEFAULT_TEMP_UNIT="${DEFAULT_TEMP_UNIT}"\ +-DDEFAULT_EXPORT_FILE_NAME="${DEFAULT_EXPORT_FILE_NAME}"\ + ${OBJS}: ${.TARGET:C/^bin/src/:C/.o$/.c/} @mkdir -p ${.TARGET:H} - ${CC} ${CFLAGS} -c\ --DDEFAULT_CONFIG_PATH="\"${DEFAULT_CONFIG_PATH}\""\ --DDEFAULT_GPIO_DEVICE="\"${DEFAULT_GPIO_DEVICE}\""\ --DDEFAULT_TEMP_KEY="\"${DEFAULT_TEMP_KEY}\""\ --DDEFAULT_HUMID_KEY="\"${DEFAULT_HUMID_KEY}\""\ --DDEFAULT_FAIL_KEY="\"${DEFAULT_FAIL_KEY}\""\ --DDEFAULT_FAIL_LIMIT="${DEFAULT_FAIL_LIMIT}"\ --DDEFAULT_DATABASE_LOCATION="\"${DEFAULT_DATABASE_LOCATION}\""\ --DDEFAULT_REFRESH_TIME="${DEFAULT_REFRESH_TIME}"\ --DDEFAULT_TEMP_UNIT="'${DEFAULT_TEMP_UNIT}'"\ --DDEFAULT_EXPORT_FILE_NAME="\"${DEFAULT_EXPORT_FILE_NAME}\""\ --o ${@} ${.TARGET:C/^bin/src/:C/.o$/.c/} + ${CC} ${CFLAGS} -c ${DEFINES} -o ${@} ${.TARGET:C/^bin/src/:C/.o$/.c/} + +config.conf: config.conf.m4 + m4 ${DEFINES} config.conf.m4 >config.conf + +install: all + mkdir -p "${PREFIX}/bin" + install -o root -g wheel "bin/${PROG}" "${PREFIX}/bin" + mkdir -p "${PREFIX}/etc/rpi4b-temp-humidity" + install -b -o root -g wheel -m 0644 config.conf "${PREFIX}/etc/rpi4b-temp-humidity" + install -o root -g wheel -m 0555 rpi4b-temp-humidity "${PREFIX}/etc/rc.d" clean: - rm -rf bin/ + rm -rf config.conf bin/ .SUFFIXES: .c .o -.PHONY: clean +.PHONY: all install clean diff --git a/config.conf.m4 b/config.conf.m4 new file mode 100644 index 0000000..d6aee03 --- /dev/null +++ b/config.conf.m4 @@ -0,0 +1,49 @@ +dnl The following line enables macro expansion in # comments +changecom(`//') +# These options do not have default value and MUST be set before running + +# LCD pins (the ones below match up with the diagram in README.md) +# Register select +rs_pin = 4 +# Read-write +rw_pin = 17 +# Enable +en_pin = 27 +# Optional backlight pin, set to -1 to disable +bl_pin = 26 +# d0-d7 +data_pins = 22 10 9 11 5 6 13 19 + +# Button pins +sel_pin = 14 +back_pin = 15 +up_pin = 18 +down_pin = 23 + +# Options below this comment have compile-time defaults (which are show below) + +# GPIO device file +#gpio_file = DEFAULT_GPIO_DEVICE + +# sysctl(8) keys for the temperature sensor +#temp_key = DEFAULT_TEMP_KEY +#humid_key = DEFAULT_HUMID_KEY +# Set to empty to disable +#fail_key = DEFAULT_FAIL_KEY + +# Number of temperature sensor fails before exit +#fail_limit = DEFAULT_FAIL_LIMIT + +# Database location +#database_location = DEFAULT_DATABASE_LOCATION + +# Time between data points, note that this is probably limited by the kernel's +# gpioths driver (5 seconds) +#refresh_time = DEFAULT_REFRESH_TIME + +# Temperature unit F for Fahrenheit, C for Celsius +#temp_unit = DEFAULT_TEMP_UNIT + +# The base name for export, .sqlite3 will be appended for SQLite exports and +# .csv will be appended for CSV exports +#export_file_name = DEFAULT_EXPORT_FILE_NAME diff --git a/config.mk b/config.mk index 9efa167..f770f2c 100644 --- a/config.mk +++ b/config.mk @@ -2,14 +2,35 @@ SQLITE3_LDFLAGS=-L/usr/local/lib -lsqlite3 SQLITE3_CFLAGS=-I/usr/local/include +# Install prefix +PREFIX=/usr/local + # Default option values, empty means NULL (for strings) -DEFAULT_CONFIG_PATH=config.conf +# Config file path +DEFAULT_CONFIG_PATH=/usr/local/etc/rpi4b-temp-humidity/config.conf + +# GPIO device DEFAULT_GPIO_DEVICE=/dev/gpioc0 + +# sysctl(8) keys for the temperature sensor DEFAULT_TEMP_KEY=dev.gpioths.0.temperature DEFAULT_HUMID_KEY=dev.gpioths.0.humidity +# Set this to empty to disable checking failures DEFAULT_FAIL_KEY=dev.gpioths.0.fails + +# Max failures of the temperature sensor before exit DEFAULT_FAIL_LIMIT=5 -DEFAULT_DATABASE_LOCATION=/var/db/rpi4-temp-humidity/db.sqlite + +# Database location +DEFAULT_DATABASE_LOCATION=/var/db/rpi4b-temp-humidity/db.sqlite + +# Time between data points, note that this is probably limited by the kernel's +# gpioths driver (5 seconds) DEFAULT_REFRESH_TIME=5000 + +# F for Fahrenheit, C for Celsius DEFAULT_TEMP_UNIT=F + +# The base name for export, .sqlite3 will be appended for SQLite exports and +# .csv will be appended for CSV exports DEFAULT_EXPORT_FILE_NAME=env_data diff --git a/rpi4b-temp-humidity b/rpi4b-temp-humidity new file mode 100644 index 0000000..676d6aa --- /dev/null +++ b/rpi4b-temp-humidity @@ -0,0 +1,44 @@ +#!/bin/sh + +. /etc/rc.subr + +name=rpi4b_temp_humidity +rcvar=rpi4b_temp_humidity_enable + +command="/usr/sbin/daemon" +procname="/usr/local/bin/rpi4b-temp-humidity" + +start_precmd="${name}_prestart" + +load_rc_config ${name} +: ${rpi4b_temp_humidity_enable:=NO} +: ${rpi4b_temp_humidity_config_file:=""} +: ${rpi4b_temp_humidity_strict_config:=NO} +: ${rpi4b_temp_humidity_verbose:=YES} +: ${rpi4b_temp_humidity_log_file:="/var/log/rpi4b-temp-humidity.log"} + +rpi4b_temp_humidity_prestart() +{ + if checkyesno rpi4b_temp_humidity_strict_config; then + rc_flags="-s ${rc_flags}" + fi + if ! [ -z "${rpi4b_temp_humidity_config_file}" ]; then + rc_flags="-f \"${rpi4b_temp_humidity_config_file}\" ${rc_flags}" + fi + if checkyesno rpi4b_temp_humidity_verbose; then + rc_flags="-v ${rc_flags}" + fi + if ! [ -z "${rpi4b_temp_humidity_log_file}" ]; then + # simple log rotation + mv "${rpi4b_temp_humidity_log_file}" "${rpi4b_temp_humidity_log_file}.old" + rpi4b_temp_humidity_daemon_flags="-o \"${rpi4b_temp_humidity_log_file}\"" + if ! mkdir -p "$(dirname "${rpi4b_temp_humidity_log_file}")"; then + return 1 + fi + else + rpi4b_temp_humidity_daemon_flags="-f" + fi + rc_flags="${rpi4b_temp_humidity_daemon_flags} ${procname} ${rc_flags}" +} + +run_rc_command "$1" diff --git a/src/config.c b/src/config.c index d4ca93a..6768da9 100644 --- a/src/config.c +++ b/src/config.c @@ -221,7 +221,7 @@ void parse_config_file(const char *path) { .directive = "gpio_file", .type = FIGPAR_TYPE_STR, .action = parse_str_callback, - .value = {.str = strdup_default_opt(DEFAULT_GPIO_DEVICE)}, + .value = {.str = strdup_default_opt(STRINGIFY(DEFAULT_GPIO_DEVICE))}, }, { .directive = "rs_pin", @@ -247,19 +247,19 @@ void parse_config_file(const char *path) { .directive = "temp_key", .type = FIGPAR_TYPE_STR, .action = parse_str_callback, - .value = {.str = strdup_default_opt(DEFAULT_TEMP_KEY)}, + .value = {.str = strdup_default_opt(STRINGIFY(DEFAULT_TEMP_KEY))}, }, { .directive = "humid_key", .type = FIGPAR_TYPE_STR, .action = parse_str_callback, - .value = {.str = strdup_default_opt(DEFAULT_HUMID_KEY)}, + .value = {.str = strdup_default_opt(STRINGIFY(DEFAULT_HUMID_KEY))}, }, { .directive = "fail_key", .type = FIGPAR_TYPE_STR, .action = parse_str_callback, - .value = {.str = strdup_default_opt(DEFAULT_FAIL_KEY)}, + .value = {.str = strdup_default_opt(STRINGIFY(DEFAULT_FAIL_KEY))}, }, { .directive = "fail_limit", @@ -271,7 +271,7 @@ void parse_config_file(const char *path) { .directive = "database_location", .type = FIGPAR_TYPE_STR, .action = parse_str_callback, - .value = {.str = strdup_default_opt(DEFAULT_DATABASE_LOCATION)}, + .value = {.str = strdup_default_opt(STRINGIFY(DEFAULT_DATABASE_LOCATION))}, }, { .directive = "refresh_time", @@ -303,7 +303,7 @@ void parse_config_file(const char *path) { .directive = "temp_unit", .type = FIGPAR_TYPE_INT, .action = parse_temp_unit_callback, - .value = {.num = DEFAULT_TEMP_UNIT}, + .value = {.num = STRINGIFY(DEFAULT_TEMP_UNIT)[0]}, }, { .directive = "bl_pin", @@ -315,7 +315,7 @@ void parse_config_file(const char *path) { .directive = "export_file_name", .type = FIGPAR_TYPE_STR, .action = parse_str_callback, - .value = {.str = strdup_default_opt(DEFAULT_EXPORT_FILE_NAME)}, + .value = {.str = strdup_default_opt(STRINGIFY(DEFAULT_EXPORT_FILE_NAME))}, }, { .directive = NULL }, }; diff --git a/src/main.c b/src/main.c index 0da045b..54a8063 100644 --- a/src/main.c +++ b/src/main.c @@ -35,6 +35,7 @@ #include #include #include +#include /* * Print help message to standard output. @@ -49,9 +50,13 @@ void parse_arguments(int argc, char *const *argv); */ void setup_signals(void); /* - * Open an sqlite3 database connection. + * 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. @@ -93,6 +98,7 @@ int main(int argc, char *const *argv) { 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); @@ -167,7 +173,7 @@ void parse_arguments(int argc, char *const *argv) { } } if (!GLOBAL_OPTS.config_path) { - GLOBAL_OPTS.config_path = strdup_checked(DEFAULT_CONFIG_PATH); + GLOBAL_OPTS.config_path = strdup_checked(STRINGIFY(DEFAULT_CONFIG_PATH)); LOG_VERBOSE("Config file path set: \"%s\"\n", GLOBAL_OPTS.config_path); } } @@ -190,18 +196,43 @@ void setup_signals() { 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) { @@ -229,28 +260,10 @@ void update_stats(THS *ths, sqlite3_stmt *insert_statement) { } } -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); diff --git a/src/util.c b/src/util.c index 73e9276..7533780 100644 --- a/src/util.c +++ b/src/util.c @@ -18,6 +18,7 @@ #include #include #include +#include const char *PERIOD_LABELS[] = { "HOUR", @@ -164,7 +165,7 @@ static const char *AVG_FOR_PERIOD_QUERY_STR = "min(humid) AS minhumid\n" "FROM env_data\n" "WHERE time >= stime\n" - "AND time <= etime);\n"; + "AND time <= etime);"; static sqlite3_stmt *AVG_FOR_PERIOD_QUERY; static const char *DATA_POINT_QUERY_STR = "SELECT max(time) IS NULL, max(time), temp, humid FROM env_data WHERE time < ?1\n" @@ -528,3 +529,39 @@ bool export_database_as_csv(sqlite3 *db, const char *dest) { pthread_cleanup_pop(true); // close file return status; } + +static bool ensure_dir_exists(const char *path, mode_t mode) { + struct stat statbuf; + errno = 0; + if (mkdir(path, mode) < 0) { + if (errno != EEXIST) { + warn("mkdir failed: %s", path); + return false; + } + if (stat(path, &statbuf) < 0) { + warn("mkdir and stat failed: %s", path); + return false; + } else if (!S_ISDIR(statbuf.st_mode)) { + warn("not a directory: %s", path); + return false; + } + } + return true; +} + +bool mkdirs(const char *path, mode_t mode) { + LOG_VERBOSE("Creating directory: \"%s\"\n", path) + char *copy = strdup_checked(path); + char *work_str = copy, *token; + while ((token = strsep(&work_str, "/"))) { + if (token != copy) { + token[-1] = '/'; + } + if (*copy && *token && !ensure_dir_exists(copy, mode)) { + free(copy); + return false; + } + } + free(copy); + return true; +} diff --git a/src/util.h b/src/util.h index f9bb137..30c19bc 100644 --- a/src/util.h +++ b/src/util.h @@ -19,6 +19,9 @@ #include #include +#define _STRINGIFY_LIT(s) (#s) +#define STRINGIFY(v) _STRINGIFY_LIT(v) + typedef enum { TEMP_UNIT_F = 'F', TEMP_UNIT_C = 'C' @@ -239,4 +242,10 @@ bool export_database(sqlite3 *db, const char *dest); */ bool export_database_as_csv(sqlite3 *db, const char *dest); +/* + * Create all the missing directories along path. + * Return: true on success, false otherwise + */ +bool mkdirs(const char *path, mode_t mode); + #endif