summaryrefslogtreecommitdiff
path: root/firmware/c1224/src/common/config.c
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-07 18:15:00 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-07 18:15:00 +0000
commitd393e0c30192611d4cac65a5429fd7180ce1f49c (patch)
tree7e47bd930340c75c4802801a81f3bd17c2469d9f /firmware/c1224/src/common/config.c
Initial pushmain
Diffstat (limited to 'firmware/c1224/src/common/config.c')
-rw-r--r--firmware/c1224/src/common/config.c422
1 files changed, 422 insertions, 0 deletions
diff --git a/firmware/c1224/src/common/config.c b/firmware/c1224/src/common/config.c
new file mode 100644
index 0000000..70f2420
--- /dev/null
+++ b/firmware/c1224/src/common/config.c
@@ -0,0 +1,422 @@
+/*
+ * INI defaults + flash overlay (last 4 KiB of Pico flash).
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+#include "config.h"
+#include "hardware/flash.h"
+#include "hardware/sync.h"
+#include "pico/stdlib.h"
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include <stdlib.h>
+
+#define CFG_MAGIC0 'C'
+#define CFG_MAGIC1 '1'
+#define CFG_MAGIC2 '2'
+#define CFG_MAGIC3 '4'
+#define CFG_VERSION 1
+#define CFG_SECTOR_SIZE 4096
+/* Store config in the last flash sector */
+#define CFG_FLASH_OFFSET (PICO_FLASH_SIZE_BYTES - CFG_SECTOR_SIZE)
+
+typedef struct __attribute__((packed)) {
+ uint8_t magic[4];
+ uint8_t version;
+ uint8_t reserved;
+ uint16_t length;
+ uint16_t crc;
+ /* uint8_t data[length] follows */
+} cfg_hdr_t;
+
+static c1224_config_t g_cfg;
+
+static uint16_t crc16_ccitt(const uint8_t *data, size_t len)
+{
+ uint16_t crc = 0xFFFF;
+ for (size_t i = 0; i < len; i++) {
+ crc ^= (uint16_t)data[i] << 8;
+ for (int b = 0; b < 8; b++) {
+ if (crc & 0x8000) {
+ crc = (uint16_t)((crc << 1) ^ 0x1021);
+ } else {
+ crc <<= 1;
+ }
+ }
+ }
+ return crc;
+}
+
+static void copy_text(char *dst, const char *src)
+{
+ size_t n = strlen(src);
+ if (n >= C1224_TEXT_MAX) {
+ n = C1224_TEXT_MAX - 1;
+ }
+ memcpy(dst, src, n);
+ dst[n] = '\0';
+}
+
+void config_load_defaults(void)
+{
+ copy_text(g_cfg.starting, "c1224 is starting");
+ copy_text(g_cfg.ready, "c1224 is ready");
+ copy_text(g_cfg.fault, "c1224 is fault");
+ copy_text(g_cfg.sending, "c1224 is sending");
+ copy_text(g_cfg.signal, "c1224 signal");
+ copy_text(g_cfg.packet, "c1224 packet");
+ copy_text(g_cfg.power, "c1224 power");
+ copy_text(g_cfg.idle, "c1224 idle");
+ copy_text(g_cfg.callsign, "");
+ copy_text(g_cfg.device_id, "c1224");
+ g_cfg.quiet = true;
+ g_cfg.cdt_active_high = false;
+ g_cfg.packet_hold_ms = 500;
+ g_cfg.packet_edge_min = 4;
+ g_cfg.packet_window_ms = 50;
+}
+
+static char *trim(char *s)
+{
+ while (*s && isspace((unsigned char)*s)) {
+ s++;
+ }
+ char *e = s + strlen(s);
+ while (e > s && isspace((unsigned char)e[-1])) {
+ *--e = '\0';
+ }
+ return s;
+}
+
+static bool parse_bool(const char *v, bool *out)
+{
+ if (!v || !*v) {
+ return false;
+ }
+ if (v[0] == '1' || v[0] == 'y' || v[0] == 'Y' || v[0] == 't' || v[0] == 'T') {
+ *out = true;
+ return true;
+ }
+ if (v[0] == '0' || v[0] == 'n' || v[0] == 'N' || v[0] == 'f' || v[0] == 'F') {
+ *out = false;
+ return true;
+ }
+ return false;
+}
+
+bool config_set_kv(const char *key, const char *value)
+{
+ if (!key || !value) {
+ return false;
+ }
+ char kbuf[32];
+ size_t n = strlen(key);
+ if (n >= sizeof(kbuf)) {
+ return false;
+ }
+ for (size_t i = 0; i <= n; i++) {
+ kbuf[i] = (char)tolower((unsigned char)key[i]);
+ }
+
+ if (strcmp(kbuf, "starting") == 0) {
+ copy_text(g_cfg.starting, value);
+ return true;
+ }
+ if (strcmp(kbuf, "ready") == 0) {
+ copy_text(g_cfg.ready, value);
+ return true;
+ }
+ if (strcmp(kbuf, "fault") == 0) {
+ copy_text(g_cfg.fault, value);
+ return true;
+ }
+ if (strcmp(kbuf, "sending") == 0) {
+ copy_text(g_cfg.sending, value);
+ return true;
+ }
+ if (strcmp(kbuf, "signal") == 0) {
+ copy_text(g_cfg.signal, value);
+ return true;
+ }
+ if (strcmp(kbuf, "packet") == 0) {
+ copy_text(g_cfg.packet, value);
+ return true;
+ }
+ if (strcmp(kbuf, "power") == 0) {
+ copy_text(g_cfg.power, value);
+ return true;
+ }
+ if (strcmp(kbuf, "idle") == 0) {
+ copy_text(g_cfg.idle, value);
+ return true;
+ }
+ if (strcmp(kbuf, "callsign") == 0) {
+ copy_text(g_cfg.callsign, value);
+ return true;
+ }
+ if (strcmp(kbuf, "device_id") == 0) {
+ copy_text(g_cfg.device_id, value);
+ return true;
+ }
+ if (strcmp(kbuf, "quiet") == 0) {
+ bool b;
+ if (!parse_bool(value, &b)) {
+ return false;
+ }
+ g_cfg.quiet = b;
+ return true;
+ }
+ if (strcmp(kbuf, "cdt_active_high") == 0) {
+ bool b;
+ if (!parse_bool(value, &b)) {
+ return false;
+ }
+ g_cfg.cdt_active_high = b;
+ return true;
+ }
+ if (strcmp(kbuf, "packet_hold_ms") == 0) {
+ g_cfg.packet_hold_ms = (uint16_t)atoi(value);
+ return true;
+ }
+ if (strcmp(kbuf, "packet_edge_min") == 0) {
+ g_cfg.packet_edge_min = (uint16_t)atoi(value);
+ return true;
+ }
+ if (strcmp(kbuf, "packet_window_ms") == 0) {
+ g_cfg.packet_window_ms = (uint16_t)atoi(value);
+ return true;
+ }
+ return false;
+}
+
+bool config_get_kv(const char *key, char *buf, size_t buflen)
+{
+ if (!key || !buf || buflen == 0) {
+ return false;
+ }
+ char kbuf[32];
+ size_t n = strlen(key);
+ if (n >= sizeof(kbuf)) {
+ return false;
+ }
+ for (size_t i = 0; i <= n; i++) {
+ kbuf[i] = (char)tolower((unsigned char)key[i]);
+ }
+
+ const char *src = NULL;
+ char num[16];
+ if (strcmp(kbuf, "starting") == 0) {
+ src = g_cfg.starting;
+ } else if (strcmp(kbuf, "ready") == 0) {
+ src = g_cfg.ready;
+ } else if (strcmp(kbuf, "fault") == 0) {
+ src = g_cfg.fault;
+ } else if (strcmp(kbuf, "sending") == 0) {
+ src = g_cfg.sending;
+ } else if (strcmp(kbuf, "signal") == 0) {
+ src = g_cfg.signal;
+ } else if (strcmp(kbuf, "packet") == 0) {
+ src = g_cfg.packet;
+ } else if (strcmp(kbuf, "power") == 0) {
+ src = g_cfg.power;
+ } else if (strcmp(kbuf, "idle") == 0) {
+ src = g_cfg.idle;
+ } else if (strcmp(kbuf, "callsign") == 0) {
+ src = g_cfg.callsign;
+ } else if (strcmp(kbuf, "device_id") == 0) {
+ src = g_cfg.device_id;
+ } else if (strcmp(kbuf, "quiet") == 0) {
+ snprintf(num, sizeof(num), "%d", (int)g_cfg.quiet);
+ src = num;
+ } else if (strcmp(kbuf, "cdt_active_high") == 0) {
+ snprintf(num, sizeof(num), "%d", (int)g_cfg.cdt_active_high);
+ src = num;
+ } else if (strcmp(kbuf, "packet_hold_ms") == 0) {
+ snprintf(num, sizeof(num), "%u", (unsigned)g_cfg.packet_hold_ms);
+ src = num;
+ } else if (strcmp(kbuf, "packet_edge_min") == 0) {
+ snprintf(num, sizeof(num), "%u", (unsigned)g_cfg.packet_edge_min);
+ src = num;
+ } else if (strcmp(kbuf, "packet_window_ms") == 0) {
+ snprintf(num, sizeof(num), "%u", (unsigned)g_cfg.packet_window_ms);
+ src = num;
+ } else {
+ return false;
+ }
+ snprintf(buf, buflen, "%s", src);
+ return true;
+}
+
+static void parse_ini_text(char *text)
+{
+ char *save = NULL;
+ for (char *line = strtok_r(text, "\r\n", &save); line;
+ line = strtok_r(NULL, "\r\n", &save)) {
+ line = trim(line);
+ if (line[0] == '\0' || line[0] == '#' || line[0] == ';') {
+ continue;
+ }
+ if (line[0] == '[') {
+ continue; /* section headers ignored — flat keys */
+ }
+ char *eq = strchr(line, '=');
+ if (!eq) {
+ continue;
+ }
+ *eq = '\0';
+ char *k = trim(line);
+ char *v = trim(eq + 1);
+ (void)config_set_kv(k, v);
+ }
+}
+
+bool config_load_flash(void)
+{
+ const uint8_t *base = (const uint8_t *)(XIP_BASE + CFG_FLASH_OFFSET);
+ const cfg_hdr_t *hdr = (const cfg_hdr_t *)base;
+ if (hdr->magic[0] != CFG_MAGIC0 || hdr->magic[1] != CFG_MAGIC1 ||
+ hdr->magic[2] != CFG_MAGIC2 || hdr->magic[3] != CFG_MAGIC3) {
+ return false;
+ }
+ if (hdr->version != CFG_VERSION) {
+ return false;
+ }
+ if (hdr->length == 0 || hdr->length > (CFG_SECTOR_SIZE - sizeof(cfg_hdr_t) - 1)) {
+ return false;
+ }
+ const uint8_t *data = base + sizeof(cfg_hdr_t);
+ if (crc16_ccitt(data, hdr->length) != hdr->crc) {
+ return false;
+ }
+ static char tmp[CFG_SECTOR_SIZE];
+ if (hdr->length + 1 > sizeof(tmp)) {
+ return false;
+ }
+ memcpy(tmp, data, hdr->length);
+ tmp[hdr->length] = '\0';
+ parse_ini_text(tmp);
+ return true;
+}
+
+static size_t build_ini(char *out, size_t outsz)
+{
+ int n = snprintf(out, outsz,
+ "# c1224.ini — operator texts + terminal + sense\n"
+ "[texts]\n"
+ "starting=%s\n"
+ "ready=%s\n"
+ "fault=%s\n"
+ "sending=%s\n"
+ "signal=%s\n"
+ "packet=%s\n"
+ "power=%s\n"
+ "idle=%s\n"
+ "[terminal]\n"
+ "callsign=%s\n"
+ "device_id=%s\n"
+ "quiet=%d\n"
+ "[sense]\n"
+ "cdt_active_high=%d\n"
+ "packet_hold_ms=%u\n"
+ "packet_edge_min=%u\n"
+ "packet_window_ms=%u\n",
+ g_cfg.starting, g_cfg.ready, g_cfg.fault, g_cfg.sending,
+ g_cfg.signal, g_cfg.packet, g_cfg.power, g_cfg.idle,
+ g_cfg.callsign, g_cfg.device_id,
+ (int)g_cfg.quiet,
+ (int)g_cfg.cdt_active_high,
+ (unsigned)g_cfg.packet_hold_ms,
+ (unsigned)g_cfg.packet_edge_min,
+ (unsigned)g_cfg.packet_window_ms);
+ if (n < 0) {
+ return 0;
+ }
+ if ((size_t)n >= outsz) {
+ return outsz - 1;
+ }
+ return (size_t)n;
+}
+
+bool config_save_flash(void)
+{
+ char body[CFG_SECTOR_SIZE - sizeof(cfg_hdr_t)];
+ size_t len = build_ini(body, sizeof(body));
+ if (len == 0) {
+ return false;
+ }
+ uint8_t sector[CFG_SECTOR_SIZE];
+ memset(sector, 0xFF, sizeof(sector));
+ cfg_hdr_t *hdr = (cfg_hdr_t *)sector;
+ hdr->magic[0] = CFG_MAGIC0;
+ hdr->magic[1] = CFG_MAGIC1;
+ hdr->magic[2] = CFG_MAGIC2;
+ hdr->magic[3] = CFG_MAGIC3;
+ hdr->version = CFG_VERSION;
+ hdr->reserved = 0;
+ hdr->length = (uint16_t)len;
+ hdr->crc = crc16_ccitt((const uint8_t *)body, len);
+ memcpy(sector + sizeof(cfg_hdr_t), body, len);
+
+ uint32_t ints = save_and_disable_interrupts();
+ flash_range_erase(CFG_FLASH_OFFSET, CFG_SECTOR_SIZE);
+ flash_range_program(CFG_FLASH_OFFSET, sector, CFG_SECTOR_SIZE);
+ restore_interrupts(ints);
+ return true;
+}
+
+bool config_erase_flash(void)
+{
+ uint32_t ints = save_and_disable_interrupts();
+ flash_range_erase(CFG_FLASH_OFFSET, CFG_SECTOR_SIZE);
+ restore_interrupts(ints);
+ return true;
+}
+
+void config_dump_ini(void)
+{
+ char body[CFG_SECTOR_SIZE - sizeof(cfg_hdr_t)];
+ size_t len = build_ini(body, sizeof(body));
+ if (len == 0) {
+ printf("(empty)\r\n");
+ return;
+ }
+ /* print line by line for CDC */
+ for (size_t i = 0; i < len; i++) {
+ putchar(body[i]);
+ if (body[i] == '\n') {
+ putchar('\r');
+ }
+ }
+ if (len == 0 || body[len - 1] != '\n') {
+ printf("\r\n");
+ }
+}
+
+void config_init(void)
+{
+ config_load_defaults();
+ bool flash_ok = config_load_flash();
+ /* Extra chatter only when quiet=0 (fixed boot banner is separate / always on). */
+ if (config_unsolicited_ok()) {
+ if (flash_ok) {
+ printf("CONFIG: flash INI loaded\r\n");
+ } else {
+ printf("CONFIG: defaults (no flash overlay)\r\n");
+ }
+ }
+}
+
+const c1224_config_t *config_get(void)
+{
+ return &g_cfg;
+}
+
+c1224_config_t *config_mutable(void)
+{
+ return &g_cfg;
+}
+
+bool config_unsolicited_ok(void)
+{
+ return !g_cfg.quiet;
+}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com