diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h index 13358966d499dcd430f3ff6dcca4df9165e48311..096c48f3ad412d89b18109b68fca77b323f35063 100644 --- a/epicardium/epicardium.h +++ b/epicardium/epicardium.h @@ -143,6 +143,9 @@ typedef _Bool bool; #define API_WS2812_WRITE 0x0120 +#define API_CONFIG_GET_STRING 0x130 +#define API_CONFIG_GET_INTEGER 0x131 +#define API_CONFIG_GET_BOOLEAN 0x132 /* clang-format on */ typedef uint32_t api_int_id_t; @@ -1924,5 +1927,53 @@ API(API_USB_CDCACM, int epic_usb_cdcacm(void)); */ API(API_WS2812_WRITE, void epic_ws2812_write(uint8_t pin, uint8_t *pixels, uint32_t n_bytes)); + +/** + * Configuration + * ====== + */ + +/** + * Read an integer from the configuration file + * + * :param char* key: Name of the option to read + * :param int* value: Place to read the value into + * :return: `0` on success or a negative value if an error occured. Possible + * errors: + * + * - ``-ENOENT``: Value can not be read + */ +API(API_CONFIG_GET_INTEGER, int epic_config_get_integer(const char *key, int *value)); + +/** + * Read a boolean from the configuration file + * + * :param char* key: Name of the option to read + * :param bool* value: Place to read the value into + * :return: `0` on success or a negative value if an error occured. Possible + * errors: + * + * - ``-ENOENT``: Value can not be read + */ +API(API_CONFIG_GET_BOOLEAN, int epic_config_get_boolean(const char *key, bool *value)); + +/** + * Read a string from the configuration file. + * + * If the buffer supplied is too small for the config option, + * no error is reported and the first `buf_len - 1` characters + * are returned (0 terminated). + * + * :param char* key: Name of the option to read + * :param char* buf: Place to read the string into + * :param size_t buf_len: Size of the provided buffer + * :return: `0` on success or a negative value if an error occured. Possible + * errors: + * + * - ``-ENOENT``: Value can not be read + */ +API(API_CONFIG_GET_STRING, int epic_config_get_string(const char *key, char *buf, size_t buf_len)); + + #endif /* _EPICARDIUM_H */ diff --git a/epicardium/modules/config.c b/epicardium/modules/config.c index cb0094c22fcd5c826cbde040ebb195939a9e39e6..6bd22d22bc8212e2db15d5bdc568446c9794029e 100644 --- a/epicardium/modules/config.c +++ b/epicardium/modules/config.c @@ -1,6 +1,7 @@ #include "modules/log.h" #include "modules/config.h" #include "modules/filesystem.h" +#include "epicardium.h" #include <assert.h> #include <stdbool.h> @@ -8,172 +9,128 @@ #include <string.h> #include <stdlib.h> #include <unistd.h> +#include <stddef.h> -#define CONFIG_MAX_LINE_LENGTH 80 - -enum OptionType { - OptionType_Boolean, - OptionType_Int, - OptionType_Float, - OptionType_String, -}; - -struct config_option { - const char *name; - enum OptionType type; - union { - bool boolean; - long integer; - double floating_point; - char *string; - } value; -}; - -static struct config_option s_options[_EpicOptionCount] = { -/* clang-format off */ - #define INIT_Boolean(v) { .boolean = (v) } - #define INIT_Int(v) { .integer = (v) } - #define INIT_Float(v) { .floating_point = (v) } - #define INIT_String(v) { .string = (v) } - #define INIT_(tp, v) INIT_ ## tp (v) - #define INIT(tp, v) INIT_ (tp, v) - - #define CARD10_SETTING(identifier, spelling, tp, default_value) \ - [Option ## identifier] = { .name = (spelling), \ - .type = OptionType_ ## tp, \ - .value = INIT(tp, (default_value)) }, - - #include "modules/config.def" - /* clang-format on */ -}; - -static struct config_option *findOption(const char *key) +#define MAX_LINE_LENGTH 80 +#define KEYS_PER_BLOCK 16 +#define KEY_LENGTH 16 +#define NOT_INT_MAGIC 0x80000000 + +// one key-value pair representing a line in the config +typedef struct { + char key[KEY_LENGTH]; + + // the value in the config file, if it's an integer. + // for strings it's set to NOT_INT_MAGIC + int value; + + // the byte offset in the config file to read the value string + size_t value_offset; +} config_slot; + +// a block of 16 config slots +// if more are needed, this becomes a linked list +typedef struct { + config_slot slots[KEYS_PER_BLOCK]; + void *next; +} config_block; + +static config_block *config_data = NULL; + +// returns the config slot for a key name +static config_slot *find_config_slot(const char *key) { - for (int i = 0; i < _EpicOptionCount; ++i) { - if (!strcmp(key, s_options[i].name)) { - return &s_options[i]; + config_block *current = config_data; + + while (current) { + for (int i = 0; i < KEYS_PER_BLOCK; i++) { + config_slot *k = ¤t->slots[i]; + + if (strcmp(k->key, key) == 0) { + // found what we're looking for + return k; + + } else if (*k->key == '\0') { + // found the first empty key + return NULL; + } } + current = current->next; } + return NULL; } -static bool set_bool(struct config_option *opt, const char *value) +// returns the next available config slot, or allocates a new block if needed +static config_slot *allocate_config_slot() { - bool val; - if (!strcmp(value, "1")) { - val = true; - } else if (!strcmp(value, "true")) { - val = true; - } else if (!strcmp(value, "0")) { - val = false; - } else if (!strcmp(value, "false")) { - val = false; - } else { - return false; + config_block *current; + + if (config_data == NULL) { + config_data = malloc(sizeof(config_block)); + assert(config_data != NULL); + memset(config_data, 0, sizeof(config_block)); } - opt->value.boolean = val; - LOG_DEBUG( - "card10.cfg", - "setting '%s' to %s", - opt->name, - val ? "true" : "false" - ); - return true; -} -static bool set_int(struct config_option *opt, const char *value) -{ - char *endptr; - size_t len = strlen(value); - int v = strtol(value, &endptr, 0); - if (endptr != (value + len)) { - return false; + current = config_data; + + while (true) { + for (int i = 0; i < KEYS_PER_BLOCK; i++) { + config_slot *k = ¤t->slots[i]; + if (*k->key == '\0') { + return k; + } + } + + // this block is full and there's no next allocated block + if (current->next == NULL) { + current->next = malloc(sizeof(config_block)); + assert(current->next != NULL); + memset(current->next, 0, sizeof(config_block)); + } + current = current->next; } - opt->value.integer = v; - LOG_DEBUG("card10.cfg", "setting '%s' to %d (0x%08x)", opt->name, v, v); - return true; } -static bool set_float(struct config_option *opt, const char *value) +// parses an int out of 'value' or returns NOT_INT_MAGIC +static int try_parse_int(const char *value) { char *endptr; size_t len = strlen(value); - double v = strtod(value, &endptr); - if (endptr != (value + len)) { - return false; - } - opt->value.floating_point = v; - LOG_DEBUG("card10.cfg", "setting '%s' to %f", opt->name, v); - return true; -} + int v = strtol(value, &endptr, 0); -const char *elide(const char *str) -{ - static char ret[21]; - size_t len = strlen(str); - if (len <= 20) { - return str; + if (endptr != (value + len)) { + return NOT_INT_MAGIC; } - strncpy(ret, str, 17); - ret[17] = '.'; - ret[18] = '.'; - ret[19] = '.'; - ret[20] = '\0'; - return ret; -} -static bool set_string(struct config_option *opt, const char *value) -{ - //this leaks, but the lifetime of these ends when epicardium exits, so... - char *leaks = strdup(value); - opt->value.string = leaks; - LOG_DEBUG("card10.cfg", "setting '%s' to %s", opt->name, elide(leaks)); - return true; + return v; } -static void configure(const char *key, const char *value, int lineNumber) -{ - struct config_option *opt = findOption(key); - if (!opt) { - //invalid key +// loads a key/value pair into a new config slot +static void add_config_pair( + const char *key, const char *value, int line_number, size_t value_offset +) { + if (strlen(key) > KEY_LENGTH - 1) { LOG_WARN( "card10.cfg", - "line %d: ignoring unknown option '%s'", - lineNumber, - key + "line:%d: too long - aborting", + line_number ); return; } - bool ok = false; - switch (opt->type) { - case OptionType_Boolean: - ok = set_bool(opt, value); - break; - case OptionType_Int: - ok = set_int(opt, value); - break; - case OptionType_Float: - ok = set_float(opt, value); - break; - case OptionType_String: - ok = set_string(opt, value); - break; - default: - assert(0 && "unreachable"); - } - if (!ok) { - LOG_WARN( - "card10.cfg", - "line %d: ignoring invalid value '%s' for option '%s'", - lineNumber, - value, - key - ); - } + + config_slot *slot = allocate_config_slot(); + strncpy(slot->key, key, KEY_LENGTH); + slot->value = try_parse_int(value); + slot->value_offset = value_offset; } -static void doline(char *line, char *eol, int lineNumber) +// parses one line of the config file +static void +parse_line(char *line, char *eol, int line_number, size_t line_offset) { + char *line_start = line; + //skip leading whitespace while (*line && isspace((int)*line)) ++line; @@ -189,9 +146,8 @@ static void doline(char *line, char *eol, int lineNumber) if (*key) { LOG_WARN( "card10.cfg", - "line %d (%s): syntax error", - lineNumber, - elide(line) + "line %d: syntax error", + line_number ); } return; @@ -203,7 +159,7 @@ static void doline(char *line, char *eol, int lineNumber) --e_key; e_key[1] = '\0'; if (*key == '\0') { - LOG_WARN("card10.cfg", "line %d: empty key", lineNumber); + LOG_WARN("card10.cfg", "line %d: empty key", line_number); return; } @@ -220,43 +176,30 @@ static void doline(char *line, char *eol, int lineNumber) LOG_WARN( "card10.cfg", "line %d: empty value for option '%s'", - lineNumber, + line_number, key ); return; } - configure(key, value, lineNumber); -} - -bool config_get_boolean(enum EpicConfigOption option) -{ - struct config_option *opt = &s_options[option]; - assert(opt->type == OptionType_Boolean); - return opt->value.boolean; -} + size_t value_offset = value - line_start + line_offset; -long config_get_integer(enum EpicConfigOption option) -{ - struct config_option *opt = &s_options[option]; - assert(opt->type == OptionType_Int); - return opt->value.integer; -} - -double config_get_float(enum EpicConfigOption option) -{ - struct config_option *opt = &s_options[option]; - assert(opt->type == OptionType_Float); - return opt->value.floating_point; + add_config_pair(key, value, line_number, value_offset); } -const char *config_get_string(enum EpicConfigOption option) +// convert windows line endings to unix line endings. +// we don't care about the extra empty lines +static void convert_crlf_to_lflf(char *buf, int n) { - struct config_option *opt = &s_options[option]; - assert(opt->type == OptionType_String); - return opt->value.string; + while (n--) { + if (*buf == '\r') { + *buf = '\n'; + } + buf++; + } } +// parses the entire config file void load_config(void) { LOG_DEBUG("card10.cfg", "loading..."); @@ -270,12 +213,14 @@ void load_config(void) ); return; } - char buf[CONFIG_MAX_LINE_LENGTH + 1]; - int lineNumber = 0; + char buf[MAX_LINE_LENGTH + 1]; + int line_number = 0; + size_t file_offset = 0; int nread; do { - nread = epic_file_read(fd, buf, CONFIG_MAX_LINE_LENGTH); - if (nread < CONFIG_MAX_LINE_LENGTH) { + nread = epic_file_read(fd, buf, MAX_LINE_LENGTH); + convert_crlf_to_lflf(buf, nread); + if (nread < MAX_LINE_LENGTH) { //add fake EOL to ensure termination buf[nread++] = '\n'; } @@ -285,64 +230,184 @@ void load_config(void) char *eol = NULL; int last_eol = 0; while (line) { - //line points one character past the las (if any) '\n' hence '- 1' + //line points one character past the last (if any) '\n' hence '- 1' last_eol = line - buf - 1; eol = strchr(line, '\n'); - ++lineNumber; + ++line_number; if (eol) { *eol = '\0'; - doline(line, eol, lineNumber); + parse_line(line, eol, line_number, file_offset); + file_offset += eol - line + 1; line = eol + 1; - } else { - if (line == buf) { - //line did not fit into buf - LOG_WARN( - "card10.cfg", - "line:%d: too long - aborting", - lineNumber - ); - return; - } else { - int seek_back = last_eol - nread; - LOG_DEBUG( - "card10.cfg", - "nread, last_eol, seek_back: %d,%d,%d", - nread, - last_eol, - seek_back - ); - assert(seek_back <= 0); - if (seek_back) { - int rc = epic_file_seek( - fd, - seek_back, - SEEK_CUR - ); - if (rc < 0) { - LOG_ERR("card10.cfg", - "seek failed, aborting"); - return; - } - char newline; - rc = epic_file_read( - fd, &newline, 1 - ); - if (rc < 0 || newline != '\n') { - LOG_ERR("card10.cfg", - "seek failed, aborting"); - LOG_DEBUG( - "card10.cfg", - "seek failed at read-back of newline: rc: %d read: %d", - rc, - (int)newline - ); - return; - } - } - break; - } + continue; + } + if (line == buf) { + //line did not fit into buf + LOG_WARN( + "card10.cfg", + "line:%d: too long - aborting", + line_number + ); + return; + } + int seek_back = last_eol - nread; + + LOG_DEBUG( + "card10.cfg", + "nread, last_eol, seek_back: %d,%d,%d", + nread, + last_eol, + seek_back + ); + assert(seek_back <= 0); + if (!seek_back) { + break; } + + int rc = epic_file_seek(fd, seek_back, SEEK_CUR); + if (rc < 0) { + LOG_ERR("card10.cfg", "seek failed, aborting"); + return; + } + char newline; + rc = epic_file_read(fd, &newline, 1); + if (rc < 0 || (newline != '\n' && newline != '\r')) { + LOG_ERR("card10.cfg", "seek failed, aborting"); + LOG_DEBUG( + "card10.cfg", + "seek failed at read-back of newline: rc: %d read: %d", + rc, + (int)newline + ); + return; + } + break; } - } while (nread == sizeof(buf)); + } while (nread == MAX_LINE_LENGTH); epic_file_close(fd); } + +// opens the config file, seeks to seek_offset and reads buf_len bytes +// used for reading strings without storing them in memory +// since we don't need to optimize for that use case as much +static size_t read_config_offset(size_t seek_offset, char *buf, size_t buf_len) +{ + int fd = epic_file_open("card10.cfg", "r"); + if (fd < 0) { + LOG_DEBUG( + "card10.cfg", + "opening config failed: %s (%d)", + strerror(-fd), + fd + ); + return 0; + } + + int rc = epic_file_seek(fd, seek_offset, SEEK_SET); + if (rc < 0) { + LOG_ERR("card10.cfg", "seek failed, aborting"); + return 0; + } + + // one byte less to accommodate the 0 termination + int nread = epic_file_read(fd, buf, buf_len - 1); + + buf[nread] = '\0'; + + epic_file_close(fd); + + return nread; +} + +// returns error if not found or invalid +int epic_config_get_integer(const char *key, int *value) +{ + config_slot *slot = find_config_slot(key); + if (slot && slot->value != NOT_INT_MAGIC) { + *value = slot->value; + return 0; + } + return -ENOENT; +} + +// returns default_value if not found or invalid +int config_get_integer_with_default(const char *key, int default_value) +{ + int value; + int ret = epic_config_get_integer(key, &value); + if (ret) { + return default_value; + } else { + return value; + } +} + +// returns error if not found +int epic_config_get_string(const char *key, char *buf, size_t buf_len) +{ + config_slot *slot = find_config_slot(key); + if (!(slot && slot->value_offset)) { + return -ENOENT; + } + + size_t nread = read_config_offset(slot->value_offset, buf, buf_len); + if (nread == 0) { + return -ENOENT; + } + + char *eol = strchr(buf, '\n'); + if (eol) { + *eol = '\0'; + } + + return 0; +} + +// returns dflt if not found, otherwise same pointer as buf +char *config_get_string_with_default( + const char *key, char *buf, size_t buf_len, char *dflt +) { + int ret = epic_config_get_string(key, buf, buf_len); + if (ret) { + return dflt; + } else { + return buf; + } +} + +// returns error if not found or invalid +int epic_config_get_boolean(const char *key, bool *value) +{ + int int_value; + int ret = epic_config_get_integer(key, &int_value); + + if (ret == 0) { + *value = !!int_value; + return 0; + } + + char buf[MAX_LINE_LENGTH]; + epic_config_get_string(key, buf, MAX_LINE_LENGTH); + + if (!strcmp(buf, "true")) { + *value = true; + return 0; + } else if (!strcmp(buf, "false")) { + *value = false; + return 0; + } + + return -ERANGE; +} + +// returns default_value if not found or invalid +bool config_get_boolean_with_default(const char *key, bool default_value) +{ + bool value; + int ret = epic_config_get_boolean(key, &value); + if (ret) { + return default_value; + } else { + return value; + } +} diff --git a/epicardium/modules/config.def b/epicardium/modules/config.def deleted file mode 100644 index 455aaedd8d61f821c1d08ee99bb45c3a61d983ba..0000000000000000000000000000000000000000 --- a/epicardium/modules/config.def +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef CARD10_SETTING -# define CARD10_SETTING(identifier, spelling, type, default_value) -#endif - -CARD10_SETTING(ExecuteElf, "execute_elf", Boolean, false) -//CARD10_SETTING(Nick, "nick", String, "an0n") -//CARD10_SETTING(Timeout, "timeout", Integer, 123) -//CARD10_SETTING(Dampening, "dampening", Float, 420) - - -#undef CARD10_SETTING diff --git a/epicardium/modules/config.h b/epicardium/modules/config.h index b72b08a5205bf9344bc3e3d805423dc826f30ccc..c95d504cfb07f94d829d316eb15d84b20979c0cf 100644 --- a/epicardium/modules/config.h +++ b/epicardium/modules/config.h @@ -2,20 +2,16 @@ #define EPICARDIUM_MODULES_CONFIG_H_INCLUDED #include <stdbool.h> - -enum EpicConfigOption { - #define CARD10_SETTING(identifier, spelling, type, default_value) Option ## identifier, - #include "modules/config.def" - _EpicOptionCount -}; +#include <stddef.h> //initialize configuration values and load card10.cfg void load_config(void); -bool config_get_boolean(enum EpicConfigOption option); -long config_get_integer(enum EpicConfigOption option); -double config_get_float(enum EpicConfigOption option); -const char* config_get_string(enum EpicConfigOption option); +// returns default_value if not found or invalid +bool config_get_boolean_with_default(const char *key, bool default_value); +int config_get_integer_with_default(const char *key, int default_value); +// returns dflt if not found, otherwise same pointer as buf +char *config_get_string_with_default(const char *key, char *buf, size_t buf_len, char *dflt); #endif//EPICARDIUM_MODULES_CONFIG_H_INCLUDED diff --git a/epicardium/modules/lifecycle.c b/epicardium/modules/lifecycle.c index 25689bd3db224789e5874deedd501fc1bcf8bfea..2e72880fb892fc38db427c36494b4044d40d1401 100644 --- a/epicardium/modules/lifecycle.c +++ b/epicardium/modules/lifecycle.c @@ -362,7 +362,7 @@ void vLifecycleTask(void *pvParameters) hardware_init(); - execute_elfs = config_get_boolean(OptionExecuteElf); + execute_elfs = config_get_boolean_with_default("execute_elf", false); /* When triggered, reset core 1 to menu */ while (1) {