diff --git a/components/config/CMakeLists.txt b/components/config/CMakeLists.txt new file mode 100644 index 0000000..5010f11 --- /dev/null +++ b/components/config/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "config.c" + INCLUDE_DIRS "." + REQUIRES fatal nvs_flash +) diff --git a/components/config/config.c b/components/config/config.c new file mode 100644 index 0000000..09b67c6 --- /dev/null +++ b/components/config/config.c @@ -0,0 +1,159 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * Copyright (c) Camden Dixie O'Brien + */ + +#include "config.h" + +#include "fatal.h" + +#include "esp_log.h" +#include "nvs_flash.h" +#include +#include + +#define TAG "Config" + +#define NAMESPACE "config" +#define MAX_CONSUMERS 8 + +#define DEFAULT_HOSTNAME "bedside-clock" +#define HOSTNAME_KEY "hostname" + +typedef struct { + char hostname[MAX_HOSTNAME_SIZE]; +} Config; + +typedef struct { + ConfigStringCallback callbacks[MAX_CONSUMERS]; + unsigned count; +} StringConsumers; + +typedef struct { + Config config; + StringConsumers hostname_consumers; +} State; + +static State state; + +static bool load_hostname() +{ + esp_err_t error; + + nvs_handle_t handle; + error = nvs_open(NAMESPACE, NVS_READONLY, &handle); + if (error == ESP_ERR_NVS_NOT_FOUND) + return false; + if (error != ESP_OK) { + ESP_LOGE(TAG, "Error opening NVS: %04x", error); + return false; + } + + size_t size = MAX_HOSTNAME_SIZE; + error = nvs_get_str(handle, HOSTNAME_KEY, state.config.hostname, &size); + if (error != ESP_OK) { + ESP_LOGE(TAG, "Error loading hostname from storage: %04x", error); + nvs_close(handle); + return false; + } + + nvs_close(handle); + return true; +} + +static void save_hostname() +{ + esp_err_t error; + + nvs_handle_t handle; + error = nvs_open(NAMESPACE, NVS_READWRITE, &handle); + if (error == ESP_ERR_NVS_NOT_FOUND) + return; + if (error != ESP_OK) { + ESP_LOGE(TAG, "Error opening NVS: %04x", error); + return; + } + + error = nvs_set_str(handle, HOSTNAME_KEY, state.config.hostname); + if (error != ESP_OK) { + ESP_LOGE(TAG, "Error loading hostname from storage: %04x", error); + nvs_close(handle); + return; + } + + error = nvs_commit(handle); + if (error != ESP_OK) + ESP_LOGE(TAG, "Error commiting NVS update: %04x", error); + + nvs_close(handle); +} + +void config_init() +{ + esp_err_t error; + + error = nvs_flash_init(); + if (error == ESP_ERR_NVS_NO_FREE_PAGES + || error == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_LOGI(TAG, "NVS partition full or outdated; erasing"); + (void)nvs_flash_erase(); + error = nvs_flash_init(); + } + if (error != ESP_OK) { + ESP_LOGE(TAG, "Error initializing NVS store: %04x", error); + FATAL(); + } + + memset(&state, 0, sizeof(state)); + + if (!load_hostname()) { + config_set_hostname(DEFAULT_HOSTNAME); + save_hostname(); + } +} + +void config_set_hostname(const char *hostname) +{ + if (hostname == NULL) { + ESP_LOGW(TAG, "Null pointer passed to %s(); ignored", __func__); + return; + } + size_t len = strlen(hostname); + if (len >= MAX_HOSTNAME_SIZE) { + ESP_LOGW( + TAG, "Hostname \"%s\" exceeds maximum size; truncated", + hostname); + len = MAX_HOSTNAME_SIZE - 1; + } + + memcpy(state.config.hostname, hostname, len); + state.config.hostname[len] = '\0'; + + save_hostname(); + + for (unsigned i = 0; i < state.hostname_consumers.count; ++i) + state.hostname_consumers.callbacks[i](state.config.hostname); +} + +size_t config_get_hostname(char *buffer, size_t buffer_size) +{ + size_t len = strlen(state.config.hostname); + if (len < buffer_size) { + memcpy(buffer, state.config.hostname, len); + buffer[len] = '\0'; + } + return len; +} + +void config_add_hostname_consumer(ConfigStringCallback callback) +{ + if (callback == NULL) { + } else if (state.hostname_consumers.count >= MAX_CONSUMERS) { + ESP_LOGE( + TAG, "Max consumers exceeded for hostname; callback discarded"); + } else { + state.hostname_consumers.callbacks[state.hostname_consumers.count] + = callback; + ++state.hostname_consumers.count; + } +} diff --git a/components/config/config.h b/components/config/config.h new file mode 100644 index 0000000..b8ad893 --- /dev/null +++ b/components/config/config.h @@ -0,0 +1,54 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * Copyright (c) Camden Dixie O'Brien + */ + +#ifndef CONFIG_H +#define CONFIG_H + +#include + +#define MAX_HOSTNAME_SIZE 32 + +/** + * Callback type for consumers of string settings. + */ +typedef void (*ConfigStringCallback)(const char *value); + +/** + * Initialize the configuration module. + * + * If there is a saved configuration, it will be loaded. Otherwise, + * the default configuration will be loaded and saved. + */ +void config_init(); + +/** + * Set the device's hostname. + * + * The argument should be a null-terminated string. If the maximum + * length is exceeded, the value will still be used, but will be + * truncated. + */ +void config_set_hostname(const char *hostname); + +/** + * Write the device's hostname into the given buffer. + * + * The length of the hostname is returned. If the value's size exceeds + * the size of the buffer, nothing will be written to the buffer but + * the length is still returned. + */ +size_t config_get_hostname(char *buffer, size_t buffer_size); + +/** + * Add a callback for hostname updates. + * + * The function specified in the argument will be invoked whenever a + * the hostname is updated, with the new value as its argument. The + * lifetime of the passed argument will be static, but the value may + * be modified once the callback returns. + */ +void config_add_hostname_callback(ConfigStringCallback callback); + +#endif diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index bef49b1..86f52a4 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( SRCS "main.c" INCLUDE_DIRS "." - REQUIRES fatal + REQUIRES config ) diff --git a/main/main.c b/main/main.c index 27909ff..e0cad45 100644 --- a/main/main.c +++ b/main/main.c @@ -3,9 +3,9 @@ * Copyright (c) Camden Dixie O'Brien */ -#include "fatal.h" +#include "config.h" void app_main(void) { - FATAL(); + config_init(); }