Browse Source

Merge branch 'sysvars'

H. Peter Anvin 1 year ago
parent
commit
f87b18bfeb

+ 2 - 0
esp32/.gitignore

@@ -10,3 +10,5 @@ cache/
 *.elf
 *.map
 www/version
+*_gen.h
+*_gen.c

+ 5 - 1
esp32/Makefile

@@ -2,10 +2,11 @@ MAKEFLAGS    += -R -r
 
 ARDUINO_CLI   = arduino-cli
 ZIP	      = zip
+PERL	      = perl
 
 SKETCH	      = max80
 TARGET	      = output/$(SKETCH).ino.bin
-GENFILES      = www.zip
+GENFILES      = www.zip $(SKETCH)/sysvars_gen.c $(SKETCH)/sysvars_gen.h
 WWW	      = www
 PORT	     ?= /dev/ttyACM0
 
@@ -35,6 +36,9 @@ $(TARGET): $(shell find $(SKETCH) -type f) $(GENFILES)
 	cd $(SKETCH) && \
 		$(ARDUINO_CLI) compile $(ARDUINO_OPTS)
 
+$(SKETCH)/%_gen.c $(SKETCH)/%_gen.h: %.vars sysvars.pl
+	$(PERL) sysvars.pl $< $(SKETCH)/$*_gen.h $(SKETCH)/$*_gen.c
+
 .PHONY: zip
 zip: zipexclude
 	mkdir -p zip

+ 15 - 0
esp32/max80/IP4.cpp

@@ -0,0 +1,15 @@
+#include "IP4.h"
+
+#include <lwip/inet.h>
+
+IP4::IP4(const char *str) {
+    in_addr_t addr;
+    if (str && inet_aton(str, &addr))
+	*this = addr;
+    else
+	*this = null_ip;
+}
+const char * IP4::cstr() {
+    in_addr_t addr = *this;
+    return inet_ntoa(addr);
+}

+ 115 - 0
esp32/max80/IP4.h

@@ -0,0 +1,115 @@
+#pragma once
+
+#include "common.h"
+#include <WiFi.h>
+#include <lwip/inet.h>
+#include <esp_wifi.h>
+
+//
+// There are no less than 3 different types for IP addresses used
+// by different APIs. This class attempts to unify them to mask the
+// differences.
+//
+class IP4 {
+private:
+    union {
+	uint8_t b[4];
+	uint32_t l;
+    };
+    // assumes gcc
+    static constexpr uint32_t netswap(uint32_t v) {
+#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
+	return v;
+#else
+	return __builtin_bswap32(v);
+#endif
+    }
+    static constexpr uint32_t hostmask(unsigned int n) {
+	return n >= 32 ? 0 : netswap(((uint32_t)1 << n)-1);
+    }
+public:
+    constexpr IP4() : l{0} { }
+    constexpr IP4(nullptr_t) : l{0} { }
+    constexpr IP4(int ll) : l{(uint32_t)ll} { }
+    constexpr IP4(uint32_t ll) : l{ll} { }
+    constexpr IP4(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3) :
+	b{b0,b1,b2,b3} { }
+    IP4(const IPAddress & ip) : l{ip} { }
+    constexpr IP4(const ip_addr_t & ip) :
+	l{ip.type == IPADDR_TYPE_V4 ? ip.u_addr.ip4.addr : 0} { }
+    constexpr IP4(const esp_ip_addr_t & ip) :
+	l{ip.type == IPADDR_TYPE_V4 ? ip.u_addr.ip4.addr : 0} { }
+    constexpr IP4(const IP4 &nip, const IP4 &hip, unsigned int n) :
+	l{(nip.l & ~hostmask(n)) | (hip.l & hostmask(n))} { }
+    constexpr IP4(const IP4 &nip, const IP4 &hip, const IP4 &mask) :
+	l{(nip.l & ~mask.l) | (hip.l & mask.l)} { }
+    IP4(const char *str);
+
+    operator uint32_t () const { return l; }
+    operator IPAddress () const { return IPAddress(l); }
+    constexpr operator ip_addr_t () const {
+	return ip_addr_t {
+	    .u_addr = {.ip4 = {.addr = l}},
+	    .type = IPADDR_TYPE_V4
+	};
+    }
+    constexpr operator esp_ip_addr_t () const {
+	return esp_ip_addr_t {
+	    .u_addr = {.ip4 = {.addr = l}},
+	    .type = IPADDR_TYPE_V4
+	};
+    }
+
+    //
+    // XXX: for C++20, this should implement operator <=>.
+    //
+    constexpr operator bool () const { return l != 0; }
+    constexpr bool operator ! () const { return l == 0; }
+    constexpr bool operator == (const IP4 &b) const { return l == b.l; }
+    constexpr bool operator != (const IP4 &b) const { return l != b.l; }
+
+    constexpr bool operator < (const IP4 &b) const {
+	return netswap(l) < netswap(b.l);
+    }
+    constexpr bool operator >= (const IP4 &b) const {
+	return netswap(l) >= netswap(b.l);
+    }
+    constexpr bool operator <= (const IP4 &b) const {
+	return netswap(l) <= netswap(b.l);
+    }
+    constexpr bool operator > (const IP4 &b) const {
+	return netswap(l) > netswap(b.l);
+    }
+#ifdef __cpp_impl_three_way_comparison
+    constexpr auto operator <=> (const IP4 &b) const {
+	return netswap(l) <=> netswap(b.l);
+    }
+#endif
+
+    constexpr IP4 operator & (const IP4 &b) const { return IP4(l & b.l); }
+    constexpr IP4 operator | (const IP4 &b) const { return IP4(l | b.l); }
+    constexpr IP4 operator ^ (const IP4 &b) const { return IP4(l ^ b.l); }
+    constexpr IP4 operator ~ () const { return IP4(~l); }
+    constexpr IP4 operator / (unsigned int n) const {
+	return IP4(l & ~hostmask(n));
+    }
+    constexpr IP4 operator % (unsigned int n) const {
+	return IP4(l & hostmask(n));
+    }
+    constexpr IP4 operator + (uint32_t n) const {
+	return IP4(netswap(netswap(l) + n));
+    }
+    constexpr IP4 operator - (uint32_t n) const {
+	return IP4(netswap(netswap(l) - n));
+    }
+    constexpr int32_t operator - (const IP4 &b) const {
+	return netswap(l) - netswap(b.l);
+    }
+    constexpr uint8_t operator [] (size_t n) const { return b[n]; }
+    uint8_t & operator [] (size_t n) { return b[n]; }
+
+    const char *cstr();
+};
+
+constexpr IP4 null_ip((uint32_t)0);
+constexpr IP4 any_ip(~(uint32_t)0);

+ 12 - 0
esp32/max80/common.h

@@ -68,6 +68,18 @@ static inline no_return exit_task(void)
     }
 }
 
+/*
+ * Convert a NULL pointer string to ""
+ */
+static inline const char *notempty(const char *str)
+{
+    return str ? str : "";
+}
+static inline char *dupstr(const char *str)
+{
+    return strdup(notempty(str));
+}
+
 /*
  * Reboot system
  */

+ 25 - 172
esp32/max80/config.c

@@ -6,85 +6,8 @@
 
 #define CONFIG_FILE	"/spiffs/config.txt"
 
-struct env_var {
-    const char *var, *val;
-};
-static const struct env_var default_config[] = {
-    {"LANG","sv"},
-    {"TZ", "CET-1CEST,M3.5.0,M10.5.0/3"}, /* Sweden */
-    {"tzname", "Europe/Stockholm"},
-    {"hostname", "max80"},
-    {"mdns.enabled", "1"},
-    {"sntp.enabled", "1"},
-    {"sntp.server","time.max80.abc80.org"},
-    {"abc.hosttype","auto"}
-};
-
 static int save_config(void);
 
-static bool config_changed;
-int setenv_config(const char *name, const char *value)
-{
-    config_changed = true;
-    if (name[0] == '-') {
-	name++;
-	value = NULL;
-    }
-    return setenv_cond(name, value);
-}
-
-int setenv_cond(const char *name, const char *value)
-{
-    const char *pfx;
-    size_t skip;
-
-    if (!strncmp("status.", name, 7)) {
-	pfx = "STATUS";
-	skip = 7;
-    } else {
-	pfx = "CONFIG";
-	skip = 0;
-    }
-    logmsg(pfx, "%s <- %s\n", name+skip, value ? value : "(deleted)");
-
-    if (value)
-      return setenv(name, value, 1);
-    else
-      return unsetenv(name);
-}
-
-static void reset_config(void)
-{
-    while (1) {
-	char **envp;
-
-	for (envp = environ; *envp; envp++) {
-	    if (!strncmp("status.", *envp, 7))
-		continue;
-	    else
-		break;
-	}
-
-	if (!*envp)
-	    break;
-
-	const char *eq = strchr(*envp, '=');
-	if (!eq)
-	    continue;		/* This should never happen... */
-
-	char ename[eq - *envp + 1];
-	memcpy(ename, *envp, eq - *envp);
-	ename[eq - *envp] = '\0';
-
-	setenv_cond(ename, NULL);
-    }
-
-    size_t i;
-    for (i = 0; i < ARRAY_SIZE(default_config); i++)
-	setenv_cond(default_config[i].var, default_config[i].val);
-
-    config_changed = true;
-}
 
 static bool is_end_of_string(int c)
 {
@@ -124,7 +47,10 @@ static int set_config_string(char *str, unsigned int separator)
 	    return -EOVERFLOW;
 
 	q[-1] = '\0';
-	setenv_config(var, p);
+	size_t nvar = sysvar_find(sysvar_ns_config, var);
+	if (nvar) {
+	    setvar_fromstr(nvar, p);
+	}
 
 	p = q;
     } while (c == separator);
@@ -134,12 +60,13 @@ static int set_config_string(char *str, unsigned int separator)
 
 static void finish_config_update(bool save)
 {
-    if (config_changed) {
+    if (sysvar_changed < sysvar_ns[sysvar_ns_status].first) {
 	if (save)
 	    save_config();
-	tzset();
-	config_changed = false;
+
+	/* Can do other things here... */
     }
+    sysvar_changed = sysvar_count;
 }
 
 /*
@@ -197,14 +124,16 @@ exit:
     return err;
 };
 
-int write_env(FILE *f, bool status)
+int write_sysvars(FILE *f, bool status)
 {
-    size_t skip = status ? 7 : 0;
-    for (char **var = environ; *var; var++) {
-	if (!strncmp(*var, "status.", 7) == status) {
-		fputs(*var + skip, f);
-		putc('\n', f);
-	}
+    size_t ns = status ? sysvar_ns_status : sysvar_ns_config;
+    const sysvar_ns_t * const nsi = &sysvar_ns[ns];
+
+    for (enum sysvar_enum var = nsi->first; var < nsi[1].first; var++) {
+	fputs(sysvar_name[var], f);
+	putc('=', f);
+	fputs(notempty(getvar_tostr(var)), f);
+	putc('\n', f);
     }
 
     return ferror(f) ? -1 : 0;
@@ -216,7 +145,7 @@ static int save_config(void)
 
     FILE *f = fopen(CONFIG_FILE, "w");
     if (f) {
-	err = write_env(f, false);
+	err = write_sysvars(f, false);
 	fclose(f);
     }
 
@@ -243,7 +172,7 @@ void init_config(void)
 		   spiffs_conf.base_path, err);
     }
 
-    reset_config();
+    sysvar_reset(sysvar_ns_config);
 
     FILE *f = fopen(CONFIG_FILE, "r");
     if (!f)
@@ -254,90 +183,14 @@ void init_config(void)
 	fclose(f);
 }
 
-const char *getenv_def(const char *var, const char *def)
-{
-    const char *val = getenv(var);
-    return val ? val : def;
-}
-
-long getenv_l(const char *var, long def)
-{
-    const char *ep;
-
-    var = getenv(var);
-    if (!var || !*var)
-	return def;
-
-    long val = strtol(var, (char **)&ep, 0);
-    return *ep ? def : val;
-}
-
-void setenv_l(const char *var, long val)
-{
-    char vbuf[2+3*sizeof val];
-    snprintf(vbuf, sizeof vbuf, "%ld", val);
-    setenv_cond(var, vbuf);
-}
-
-unsigned long getenv_ul(const char *var, unsigned long def)
-{
-    const char *ep;
-
-    var = getenv(var);
-    if (!var || !*var)
-	return def;
-
-    unsigned long val = strtol(var, (char **)&ep, 0);
-    return *ep ? def : val;
-}
-
-void setenv_ul(const char *var, unsigned long val)
-{
-    char vbuf[2+3*sizeof val];
-    snprintf(vbuf, sizeof vbuf, "%lu", val);
-    setenv_cond(var, vbuf);
-}
-
-bool getenv_bool(const char *var)
-{
-    var = getenv(var);
-
-    if (!var)
-	return false;
-
-    unsigned char c = *var;
-    unsigned char cl = c | 0x20;
-
-    return !(!c || c == '0' || cl == 'f' || cl == 'n' || cl == 'd' ||
-	     (cl == 'o' && (var[1] | 0x20) == 'f'));
-}
-
-void setenv_bool(const char *var, bool val)
+void log_config_status(void)
 {
-    return setenv_ul(var, val);
-}
+    const sysvar_ns_t *nsi = &sysvar_ns[0];
 
-const char *getenv_notempty(const char *env)
-{
-    const char *str = getenv(env);
-    if (str && !*str)
-	str = NULL;
-    return str;
-}
+    for (enum sysvar_enum var = 1; var < sysvar_count; var++) {
+	if (var >= nsi[1].first)
+	    nsi++;
 
-void log_config_status(void)
-{
-    const char *pfx;
-    size_t skip;
-
-    for (char **var = environ; *var; var++) {
-	if (!strncmp(*var, "status.", 7)) {
-	    pfx = "STATUS";
-	    skip = 7;
-	} else {
-	    pfx = "CONFIG";
-	    skip = 0;
-	}
-	logmsg(pfx, "%s\n", *var+skip);
+	logmsg(nsi->name, "%s\n", notempty(getvar_tostr(var)));
     }
 }

+ 2 - 12
esp32/max80/config.h

@@ -1,25 +1,15 @@
 #pragma once
 
 #include "common.h"
+#include "sysvars.h"
 
 #include <stdio.h>
 
 #define MAX_CONFIG_LINE 256
 
 extern_c int read_config(FILE *, bool save);
-extern_c int write_env(FILE *, bool status);
+extern_c int write_sysvars(FILE *, bool status);
 extern_c void init_config(void);
-
 extern_c int set_config_url_string(const char *str);
 
-extern_c int setenv_cond(const char *var, const char *val);
-extern_c int setenv_config(const char *var, const char *val);
-extern_c const char *getenv_def(const char *var, const char *def);
-extern_c long getenv_l(const char *var, long def);
-extern_c void setenv_l(const char *var, long val);
-extern_c unsigned long getenv_ul(const char *var, unsigned long def);
-extern_c void setenv_ul(const char *var, unsigned long val);
-extern_c bool getenv_bool(const char *var);
-extern_c void setenv_bool(const char *var, bool val);
-extern_c const char *getenv_notempty(const char *var);
 extern_c void log_config_status(void);

+ 3 - 3
esp32/max80/fpgasvc.c

@@ -142,7 +142,7 @@ esp_err_t fpga_service_init(void)
 {
     pinMode(PIN_FPGA_INT, INPUT);
 
-    setenv_bool("status.max80.fpga", false);
+    setvar_bool(status_max80_fpga, false);
 
     fpga_service_evgroup = null_check(xEventGroupCreate());
     spi_mutex = null_check(xSemaphoreCreateRecursiveMutex());
@@ -255,7 +255,7 @@ static bool fpga_online(void)
 	   (int)(sizeof head.signature - 1), head.signature);
     esplink_start(&head);
 
-    setenv_bool("status.max80.fpga", true);
+    setvar_bool(status_max80_fpga, true);
     xSemaphoreGiveRecursive(spi_mutex);
 
     xTimerStart(fpga_timesync_timer, portMAX_DELAY);
@@ -269,7 +269,7 @@ static void fpga_offline(void)
     memset(&head, 0, sizeof head);
     xSemaphoreTakeRecursive(spi_mutex, portMAX_DELAY);
     xTimerStop(fpga_timesync_timer, portMAX_DELAY);
-    setenv_bool("status.max80.fpga", false);
+    setvar_bool(status_max80_fpga, false);
     esplink_start(NULL);	/* Stop esplink */
 }
 

+ 16 - 12
esp32/max80/httpd.c

@@ -444,8 +444,7 @@ static esp_err_t httpd_set_board_rev(httpd_req_t *req)
 		    /* Otherwise input truncated or invalid */
 		    printf("[HTTP] setting board revision: %s\n", rev_str);
 		    if (!board_info_set(rev_str)) {
-			setenv_cond("status.max80.hw.ver",
-				    board_info.version_str);
+			setvar_str(status_max80_hw_ver, board_info.version_str);
 			err = Z_OK;
 		    } else {
 			err = FWUPDATE_ERR_CONFIG_SAVE;
@@ -476,13 +475,12 @@ static esp_err_t httpd_set_board_rev(httpd_req_t *req)
 
 static void httpd_get_status_extra(FILE *f, httpd_req_t *req)
 {
-    static const char refresh_time_config[] = "http.status.refresh";
     char timebuf[64];
     size_t len;
     struct timeval tv;
 
     unsigned long statref;
-    statref = Max(getenv_ul(refresh_time_config, 0), MIN_STATUS_REF);
+    statref = Max(getvar_uint(config_http_status_refresh), MIN_STATUS_REF);
 
     if (httpd_req_get_url_query_str(req, timebuf, sizeof timebuf) == ESP_OK &&
 	*timebuf) {
@@ -490,11 +488,11 @@ static void httpd_get_status_extra(FILE *f, httpd_req_t *req)
 	unsigned long newstatref = strtoul(timebuf, &ep, 10);
 	if (!*ep && newstatref >= MIN_STATUS_REF && newstatref != statref) {
 	    statref = newstatref;
-	    setenv_config(refresh_time_config, timebuf);
+	    setvar_uint(config_http_status_refresh, statref);
 	    read_config(NULL, true); /* Save changed config */
 	}
     }
-    fprintf(f, "%s=%lu\n", refresh_time_config, statref);
+    fprintf(f, "http.status.refresh=%lu\n", statref);
 
     gettimeofday(&tv,NULL);
     const struct tm *tm = localtime(&tv.tv_sec);
@@ -518,7 +516,7 @@ static esp_err_t httpd_get_config_status(httpd_req_t *req, bool status)
     if (status)
 	httpd_get_status_extra(f, req);
 
-    int rv = write_env(f, status);
+    int rv = write_sysvars(f, status);
     fclose(f);
     return rv ? ESP_FAIL : ESP_OK;
 }
@@ -527,7 +525,7 @@ static esp_err_t httpd_set_lang(httpd_req_t *req, const char *query)
 {
     if (query) {
 	int qlen = strlen(query);
-	setenv_config("LANG", qlen && qlen <= MAX_LANG_LEN ? query : NULL);
+	setvar_str(config_LANG, qlen && qlen <= MAX_LANG_LEN ? query : NULL);
 	read_config(NULL, true);	/* Save configuration */
     }
 
@@ -539,18 +537,24 @@ static esp_err_t httpd_set_lang(httpd_req_t *req, const char *query)
     return httpd_send_plain(req, 303, NULL, 0, HSP_REFERER, 0);
 }
 
-static esp_err_t httpd_get_lang(httpd_req_t *req)
+static const char *get_lang(void)
 {
-    const char *lang = getenv_def("LANG", "");
+    const char *lang = getvar_str(config_LANG);
+    if (!lang)
+	lang = fallback_language;
+    return lang;
+}
 
+static esp_err_t httpd_get_lang(httpd_req_t *req)
+{
+    const char *lang = get_lang();
     return httpd_send_plain(req, 200, lang, strlen(lang), HSP_CRLF, 0);
 }
 
 static esp_err_t httpd_lang_redirect(httpd_req_t *req)
 {
     char lang_buf[sizeof req->uri + MAX_LANG_LEN];
-    int len = snprintf(lang_buf, sizeof lang_buf, "/lang/%s%s",
-		       getenv_def("LANG", fallback_language),
+    int len = snprintf(lang_buf, sizeof lang_buf, "/lang/%s%s", get_lang(),
 		       req->uri + (sizeof("/sys/lang")-1));
 
     return httpd_send_plain(req, 302, lang_buf, len, 0, 0);

+ 7 - 5
esp32/max80/max80.ino

@@ -54,9 +54,9 @@ static void heap_info()
 static void dump_config()
 {
     printf("--- Configuration:\n");
-    write_env(stdout, false);
+    write_sysvars(stdout, false);
     printf("--- Status:\n");
-    write_env(stdout, true);
+    write_sysvars(stdout, true);
     printf("--- End configuration and status\n");
 }
 
@@ -102,15 +102,17 @@ void setup() {
     TTY::init();
     heap_info();
 
+    sysvar_init();
+
     printf("[FW]   MAX80 firmware compiled on %s\n", fwdate);
     printf("[PCB]  MAX80 board version: %s\n", board_info.version_str);
-    setenv_cond("status.max80.hw.ver", board_info.version_str);
+    setvar_str(status_max80_hw_ver, board_info.version_str);
     printf("[PCB]  MAX80 serial number: %s\n", serial_number);
-    setenv_cond("status.max80.hw.serial", serial_number);
+    setvar_str(status_max80_hw_serial, serial_number);
     Serial.println("MAX80 start");
 
     init_config();
-    setenv_cond("status.max80.fw.date", fwdate);
+    setvar_str(status_max80_fw_date, fwdate);
     fpga_service_init();
     fpga_service_enable(true);
     SetupWiFi();

+ 419 - 0
esp32/max80/sysvars.c

@@ -0,0 +1,419 @@
+#include "common.h"
+#include "sysvars.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <time.h>
+
+#define DEBUG 1
+
+#ifndef DEBUG
+# define DEBUG 0
+#endif
+
+static const char *sysvar_bool_tostr(sysvar_t from, char *buf)
+{
+    buf[0] = '0' + from.v_bool;
+    buf[1] = '\0';
+    return buf;
+}
+
+static bool sysvar_bool_fromstr(sysvar_t *to, const char *from)
+{
+    char c = from[0] | 0x20;
+    switch (c) {
+    case '1':
+    case 't':
+    case 'y':
+    case 'j':
+    case 's':
+	to->v_bool = true;
+	return true;
+    case ' ':			/* Blank or null */
+    case '0':
+    case 'f':
+    case 'n':
+	to->v_bool = false;
+	return true;
+    default:
+	return false;
+    }
+}
+
+const struct sysvar_ops sysvar_bool_ops = {
+    .tostr = sysvar_bool_tostr,
+    .fromstr = sysvar_bool_fromstr,
+    .buflen = BOOL_BUFLEN
+};
+
+static const char *sysvar_int_tostr(sysvar_t from, char *buf)
+{
+    snprintf(buf, INT_BUFLEN, "%ld", from.v_int);
+    return buf;
+}
+
+static bool sysvar_int_fromstr(sysvar_t *to, const char *from)
+{
+    char *ep;
+    long v = strtol(from, &ep, 0);
+    if (ep == from || *ep)
+	return false;
+    to->v_int = v;
+    return true;
+}
+
+const struct sysvar_ops sysvar_int_ops = {
+    .tostr = sysvar_int_tostr,
+    .fromstr = sysvar_int_fromstr,
+    .buflen = INT_BUFLEN
+};
+
+static const char *sysvar_uint_tostr(sysvar_t from, char *buf)
+{
+    snprintf(buf, UINT_BUFLEN, "%lu", from.v_uint);
+    return buf;
+}
+
+static bool sysvar_uint_fromstr(sysvar_t *to, const char *from)
+{
+    char *ep;
+    unsigned long v = strtoul(from, &ep, 0);
+    if (ep == from || *ep)
+	return false;
+    to->v_uint = v;
+    return true;
+}
+
+const struct sysvar_ops sysvar_uint_ops = {
+    .tostr = sysvar_uint_tostr,
+    .fromstr = sysvar_uint_fromstr,
+    .buflen = UINT_BUFLEN
+};
+
+static const char *sysvar_str_tostr(sysvar_t from, char *buf)
+{
+    (void)buf;
+    return from.v_str;
+}
+
+static bool sysvar_str_fromstr(sysvar_t *to, const char *from)
+{
+    char *ns;
+
+    if (!from) {
+	ns = NULL;
+    } else {
+	ns = strdup(from);
+	if (!ns)
+	    return false;
+    }
+    to->v_str = ns;
+    return true;
+}
+
+static bool sysvar_str_set(sysvar_t *to, sysvar_t from)
+{
+    return sysvar_str_fromstr(to, from.v_str);
+}
+
+static size_t sysvar_str_datasize(sysvar_t val)
+{
+    return (val.v_str ? strlen(val.v_str) : 0) + 1;
+}
+
+const struct sysvar_ops sysvar_str_ops = {
+    .set = sysvar_str_set,
+    .tostr = sysvar_str_tostr,
+    .fromstr = sysvar_str_fromstr,
+    .datasize = sysvar_str_datasize
+};
+
+static void sysvar_tz_update(sysvar_t val, bool isset)
+{
+    if (isset)
+	setenv("TZ", val.v_tz, 1);
+    else
+	unsetenv("TZ");
+    tzset();
+}
+
+const struct sysvar_ops sysvar_tz_ops = {
+    .set = sysvar_str_set,
+    .tostr = sysvar_str_tostr,
+    .fromstr = sysvar_str_fromstr,
+    .update = sysvar_tz_update,
+    .datasize = sysvar_str_datasize
+};
+
+static const char *sysvar_ip_tostr(sysvar_t from, char *buf)
+{
+    union ip_bytes {
+	uint8_t b[4];
+	uint32_t l;
+    } ip;
+
+    ip.l = from.v_ip;
+    snprintf(buf, IP_BUFLEN,
+	     "%u.%u.%u.%u", ip.b[0], ip.b[1], ip.b[2], ip.b[3]);
+    return buf;
+}
+
+static bool sysvar_ip_fromstr(sysvar_t *to, const char *str)
+{
+    union ip_bytes {
+	uint8_t b[4];
+	uint32_t l;
+    } ip;
+
+    ip.l = 0;
+    for (int i = 0; i < 4; i++) {
+	char *ep;
+	unsigned long v = strtoul(str, &ep, 10);
+	if (ep == str || *ep != (i == 3) ? '\0' : '.' || v > 255)
+	    return false;
+	str = ep + 1;
+	ip.b[i] = v;
+    }
+
+    to->v_ip = ip.l;
+    return true;
+}
+
+const struct sysvar_ops sysvar_ip_ops = {
+    .tostr = sysvar_ip_tostr,
+    .fromstr = sysvar_ip_fromstr,
+    .buflen = IP_BUFLEN
+};
+
+static bool sysvar_mac_set(sysvar_t *to, sysvar_t from)
+{
+    uint8_t *buf;
+
+    if (!from.v_mac) {
+	buf = NULL;
+    } else {
+	buf = malloc(6);
+	if (!buf)
+	    return false;
+
+	memcpy(buf, from.v_mac, 6);
+    }
+    to->v_mac = buf;
+    return true;
+}
+
+static const char *sysvar_mac_tostr(sysvar_t from, char *buf)
+{
+    const uint8_t *m = from.v_mac;
+    if (!m)
+	return NULL;
+
+    snprintf(buf, MAC_BUFLEN, "%x:%x:%x:%x:%x:%x",
+	     m[0], m[1], m[2], m[3], m[4], m[5]);
+    return buf;
+}
+
+static bool sysvar_mac_fromstr(sysvar_t *to, const char *str)
+{
+    sysvar_t from;
+    uint8_t m[6];
+
+    if (!str) {
+	from.v_mac = NULL;
+    } else {
+	for (int i = 0; i < 6; i++) {
+	    char *ep;
+	    unsigned long v = strtoul(str, &ep, 16);
+	    if (ep == str || *ep != (i == 5) ? '\0' : ':' || v > 255)
+		return false;
+	    str = ep + 1;
+	    m[i] = v;
+	}
+
+	from.v_mac = m;
+    }
+
+    return sysvar_mac_set(to, from);
+}
+
+static size_t sysvar_mac_datasize(sysvar_t val)
+{
+    (void)val;
+    return 6;
+}
+
+const struct sysvar_ops sysvar_mac_ops = {
+    .set = sysvar_mac_set,
+    .tostr = sysvar_mac_tostr,
+    .fromstr = sysvar_mac_fromstr,
+    .buflen = MAC_BUFLEN,
+    .datasize = sysvar_mac_datasize
+};
+
+/* --- Generic getters/setters --- */
+
+/* Contains the lowest numbered sysvar changed; or sysvar_count if nothing */
+enum sysvar_enum sysvar_changed;
+
+sysvar_t getvar(size_t var)
+{
+    if (var >= (size_t)sysvar_count)
+	return sysvar_defval[sysvar_null];
+
+    return sysvar_val[var];
+}
+
+static bool do_setvar(size_t var, sysvar_t val, bool is_set)
+{
+    const struct sysvar_ops *type = sysvar_types[var];
+    sysvar_t *to = &sysvar_val[var];
+    void *free_ptr = NULL;
+
+    if (type->datasize)
+	free_ptr = to->v_ptr;
+
+    if (DEBUG) {
+	char tostr_buf[SYSVAR_BUFLEN];
+	printf("%ssetvar %zu %s <- %s\n", is_set ? "" : "un",
+	       var, sysvar_name[var],
+	       notempty(type->tostr(val, tostr_buf)));
+    }
+
+    if (!type->set || (type->datasize && !val.v_ptr)) {
+	sysvar_val[var] = val;
+    } else {
+	if (!type->set(to, val))
+	    return false;
+    }
+
+    if (free_ptr)
+	free(free_ptr);
+
+    if (var < (size_t)sysvar_changed)
+	sysvar_changed = (enum sysvar_enum)var;
+
+    sysvar_isset[var] = is_set;
+    if (type->update)
+	type->update(*to, is_set);
+
+    return true;
+}
+
+bool setvar(size_t var, sysvar_t val)
+{
+    if (var >= (size_t)sysvar_count)
+	return false;
+
+    return do_setvar(var, val, true);
+}
+
+bool unsetvar(size_t var)
+{
+    if (var >= (size_t)sysvar_count)
+	return false;
+
+    return do_setvar(var, sysvar_defval[var], false);
+}
+
+/* --- Getters/setters converting to/from strings --- */
+
+const char *getvar_tostr(size_t var)
+{
+    static char buf[SYSVAR_BUFLEN];
+    return getvar_tostr_r(var, buf);
+}
+
+const char *getvar_tostr_r(size_t var, char *buf)
+{
+    if (var >= (size_t)sysvar_count)
+	return NULL;
+
+    const struct sysvar_ops *type = sysvar_types[var];
+
+    /* A tostr method is required */
+    return type->tostr(sysvar_val[var], buf);
+}
+
+bool setvar_fromstr(size_t var, const char *str)
+{
+    if (var >= (size_t)sysvar_count)
+	return NULL;
+
+    if (!str)
+	return unsetvar(var);
+
+    const struct sysvar_ops *type = sysvar_types[var];
+    sysvar_t *to = &sysvar_val[var];
+    void *free_ptr = NULL;
+
+    if (type->datasize)
+	free_ptr = to->v_ptr;
+
+    if (DEBUG) {
+	printf("setvar_fromstr %zu %s <- %s\n", var, sysvar_name[var], str);
+    }
+
+    /* A fromstr method is required */
+    if (!type->fromstr(to, str))
+	return false;
+
+    if (var < (size_t)sysvar_changed)
+	sysvar_changed = (enum sysvar_enum)var;
+
+    if (free_ptr)
+	free(free_ptr);
+
+    sysvar_isset[var] = true;
+    return true;
+}
+
+/* --- Find the index of a specific variable --- */
+
+static int string_ptr_compare(const void *a, const void *b)
+{
+    const char * const *aa = a;
+    const char * const *bb = b;
+    return strcmp(*aa, *bb);
+}
+
+size_t sysvar_find(size_t ns, const char *name)
+{
+    if (ns >= (size_t)sysvar_nscount)
+	return 0;
+
+    const sysvar_ns_t *nsi = &sysvar_ns[ns];
+    const char * const *varname;
+    const size_t count = nsi[1].first - nsi->first;
+
+    varname = bsearch(&name, sysvar_name + nsi->first,
+		      count, sizeof *sysvar_name,
+		      string_ptr_compare);
+    if (!varname)
+	return 0;
+
+    return varname - sysvar_name;
+}
+
+/* --- Initialization/reset to defaults --- */
+
+void sysvar_reset(size_t ns)
+{
+    if (ns >= (size_t)sysvar_nscount)
+	return;
+
+    enum sysvar_enum i;
+    for (i = sysvar_ns[ns].first; i < sysvar_ns[ns+1].first; i++)
+	unsetvar(i);
+
+    if (sysvar_changed < i)
+	sysvar_changed = i;
+}
+
+void sysvar_init(void)
+{
+    for (enum sysvar_enum i = sysvar_null+1; i < sysvar_count; i++)
+	unsetvar(i);
+
+    sysvar_changed = sysvar_count;
+}

+ 120 - 0
esp32/max80/sysvars.h

@@ -0,0 +1,120 @@
+#ifndef SYSVARS_H
+#define SYSVARS_H
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <inttypes.h>
+
+#ifndef extern_c
+# ifdef __cplusplus
+#  define extern_c extern "C"
+# else
+#  define extern_c extern
+# endif
+#endif
+
+typedef union sysvar_value {
+    bool v_bool;
+    long int v_int;
+    unsigned long int v_uint;
+    const char *v_str;
+    const char *v_tz;		/* Timezone */
+    uint32_t v_ip;		/* IPv4 address */
+    const uint8_t *v_mac;	/* MAC address */
+    void *v_ptr;
+} sysvar_t;
+
+struct sysvar_ops {
+    bool (*set)(sysvar_t *, sysvar_t);
+    /* bool (*unset)(sysvar_t *); - not used */
+    const char * (*tostr)(sysvar_t, char *);
+    bool (*fromstr)(sysvar_t *, const char *);
+    void (*update)(sysvar_t, bool); /* Called after set or fromstr; bool = isset */
+    size_t (*datasize)(sysvar_t);   /* If pointer, return size of data */
+    size_t buflen;		/* Minimal buffer size for string if needed */
+};
+
+typedef const struct sysvar_ops *sysvar_type_t;
+
+extern_c const struct sysvar_ops sysvar_bool_ops;
+extern_c const struct sysvar_ops sysvar_int_ops;
+extern_c const struct sysvar_ops sysvar_uint_ops;
+extern_c const struct sysvar_ops sysvar_str_ops;
+extern_c const struct sysvar_ops sysvar_tz_ops;
+extern_c const struct sysvar_ops sysvar_ip_ops;
+extern_c const struct sysvar_ops sysvar_mac_ops;
+
+#define SYSVAR_NULLTYPE NULL
+#define SYSVAR_TYPE(x) (&sysvar ## x ## _ops)
+
+#include "sysvars_gen.h"
+
+typedef struct sysvar_namespace {
+    const char *name;
+    enum sysvar_enum first;
+} sysvar_ns_t;
+
+extern_c const sysvar_ns_t sysvar_ns[(size_t)sysvar_nscount+1];
+extern_c enum sysvar_enum sysvar_changed;
+
+/* Buffer size needed to represent some data types */
+#define BOOL_BUFLEN 2
+#define INT_BUFLEN  (3*sizeof(unsigned int)+2)
+#define UINT_BUFLEN INT_BUFLEN
+#define IP_BUFLEN   (4*4)
+#define MAC_BUFLEN  (3*6)
+
+#define SYSVAR_BUFLEN	32	/* Conservative minimum */
+
+extern_c sysvar_t getvar(size_t var);
+extern_c bool setvar(size_t var, sysvar_t val);
+extern_c bool unsetvar(size_t var);
+extern_c const char *getvar_tostr(size_t var);
+extern_c const char *getvar_tostr_r(size_t var, char *buf);
+extern_c bool setvar_fromstr(size_t var, const char *str);
+extern_c void sysvar_init(void);
+extern_c void sysvar_reset(size_t ns);
+extern_c size_t sysvar_find(size_t ns, const char *name);
+extern_c size_t sysvar_marshall(enum sysvar_enum first, size_t count,
+				void *buf, size_t *buflen, uintptr_t extaddr);
+
+/* Type-specific definitions/getters/setters */
+/* Note that t contains a leading underscore to avoid bool/_Bool issues */
+
+#define const_assert(cond, str)					     \
+    do {							     \
+	extern void fail(void) __attribute__((error(str)));	     \
+	if (__builtin_constant_p(cond) && !(cond))		     \
+	    fail();						     \
+    } while (0)
+
+#define TRY_ASSERT_TYPE(var,t)						\
+    const_assert(sysvar_type(var) == SYSVAR_TYPE(t),			\
+	"invalid type for sysvar " #var)
+
+#define SYSVAR_MKTYPE(t,c_type)						\
+    static inline c_type getvar ## t (size_t var)			\
+    {									\
+	TRY_ASSERT_TYPE(var,t);						\
+	/* If var is constant and >= sysvar_count, TRY_ASSERT_TYPE() fails */ \
+	if (__builtin_constant_p(var < (size_t)sysvar_count))		\
+	    return sysvar_val[var].v ## t;				\
+	return getvar(var).v ## t ;					\
+    }									\
+    static inline bool setvar ## t (size_t var, c_type v)		\
+    {									\
+	sysvar_t vv;							\
+	TRY_ASSERT_TYPE(var,t);						\
+	vv.v ## t = v;							\
+	return setvar(var, vv);						\
+    }
+
+SYSVAR_MKTYPE(_bool, bool);
+SYSVAR_MKTYPE(_int, long int);
+SYSVAR_MKTYPE(_uint, unsigned long int);
+SYSVAR_MKTYPE(_str, const char *);
+SYSVAR_MKTYPE(_tz, const char *);
+SYSVAR_MKTYPE(_ip, uint32_t);
+SYSVAR_MKTYPE(_mac, const uint8_t *);
+
+#endif /* SYSVARS_H */

+ 53 - 0
esp32/max80/sysvars_marshall.c

@@ -0,0 +1,53 @@
+/*
+ * Marshall sysvars into a buffer to be sent to the FPGA.
+ */
+
+#include "common.h"
+#include "sysvars.h"
+
+/*
+ * Marshall a set or subset of sysvar entries into a buffer
+ * for use by another CPU (for which the address to said buffer
+ * might differ from what this CPU uses.)
+ *
+ * Returns the number of entries successfully marshalled.
+ * extaddr refers to the external address of the buffer, for which
+ * internal pointers should be adjusted.
+ *
+ * On return, *buflen is adjusted to the number of bytes actually used.
+ */
+size_t sysvar_marshall(enum sysvar_enum first, size_t count,
+		       void *buf, size_t *buflen, uintptr_t extaddr)
+{
+    static const size_t align_mask = 3;
+    size_t bytes = count * sizeof(sysvar_t);
+    if (*buflen < bytes) {
+	/* Only space for a partial result */
+	count = *buflen / sizeof(sysvar_t);
+	bytes = count * sizeof(sysvar_t);
+    }
+    sysvar_t *vp = (sysvar_t *)buf;
+    size_t bufsize = *buflen;
+    const sysvar_t *v = &sysvar_val[first];
+    const sysvar_type_t *t = &sysvar_types[first];
+    size_t ok = 0;
+    intptr_t adjust = extaddr - (uintptr_t)buf;
+
+    while (count--) {
+	sysvar_t vv = *vp++ = *v++;
+	sysvar_type_t type = *t++;
+	if (type->datasize && vv.v_ptr) {
+	    size_t dsize = type->datasize(vv);
+	    size_t adj_bytes = (bytes + align_mask) & ~align_mask;
+	    if (adj_bytes + dsize > bufsize)
+		break;		/* Insufficient buffer space */
+	    char *dp = (char *)buf + adj_bytes;
+	    memcpy(dp, vv.v_ptr, dsize);
+	    vv.v_ptr = (void *)((uintptr_t)vv.v_ptr + adjust);
+	    bytes = adj_bytes + dsize;
+	}
+	ok++;
+    }
+    *buflen = bytes;
+    return ok;
+}

+ 1 - 1
esp32/max80/time.c

@@ -20,7 +20,7 @@ void time_net_sync(const struct timeval *tv)
 
     if (synced != time_net_sync_status) {
 	time_net_sync_status = synced;
-	setenv_bool("status.net.sntp.sync", synced);
+	setvar_bool(status_net_sntp_sync, synced);
 	if (!synced) {
 	    printf("[SNTP] Time synchronization lost\n");
 	} else {

+ 67 - 92
esp32/max80/wifi.cpp

@@ -14,9 +14,12 @@
 #include <esp_sntp.h>
 #include <esp_wifi.h>
 
+#include "IP4.h"
+
 WiFiUDP UDP;
 
-static const char *ssid, *password, *hostname, *dnsserver;
+static const char *ssid, *password, *hostname;
+static IP4 dnsserver;
 static TimerHandle_t sta_failure_timer;
 
 enum connected {
@@ -28,6 +31,15 @@ static volatile bool sta_timeout_enabled;
 static volatile unsigned int sta_timeout_count;
 static unsigned int connected;
 
+static inline bool setvar_ip4(enum sysvar_enum var, const IP4 &ip)
+{
+    return setvar_ip(var, static_cast<uint32_t>(ip));
+}
+static inline IP4 getvar_ip4(enum sysvar_enum var)
+{
+    return IP4(getvar_ip(var));
+}
+
 static void sta_bounce(void)
 {
     if (!ssid)
@@ -93,13 +105,13 @@ static void sntp_sync_cb(struct timeval *tv)
 
 static void my_sntp_start(void)
 {
-    setenv_bool("status.net.sntp.sync", false);
+    setvar_bool(status_net_sntp_sync, false);
 
-    if (getenv_bool("sntp.enabled")) {
+    if (getvar_bool(config_sntp_enabled)) {
 	sntp_set_time_sync_notification_cb(sntp_sync_cb);
 
 	sntp_setoperatingmode(SNTP_OPMODE_POLL);
-	sntp_servermode_dhcp(!getenv_bool("ip4.dhcp.nosntp"));
+	sntp_servermode_dhcp(!getvar_bool(config_ip4_dhcp_nosntp));
 	sntp_set_sync_mode(SNTP_SYNC_MODE_IMMED); // Until first sync
 	sntp_init();
     } else {
@@ -119,21 +131,15 @@ static void stop_services(void)
     }
 }
 
-static inline bool invalid_ip(const ip_addr_t *ip)
-{
-    return !memcmp(ip, &ip_addr_any, sizeof *ip);
-}
-
 static void sntp_server_show(void)
 {
-    const ip_addr_t *sntp_ip = sntp_getserver(0);
+    IP4 sntp_ip = *sntp_getserver(0);
 
-    if (!invalid_ip(sntp_ip)) {
-	const char *sntp_server = inet_ntoa(*sntp_ip);
-	printf("[SNTP] Time server: %s\n", sntp_server);
-	setenv_cond("status.net.sntp.server", sntp_server);
+    if (!sntp_ip) {
+	printf("[SNTP] Time server: %s\n", sntp_ip.cstr());
+	setvar_ip4(status_net_sntp_server, sntp_ip);
     } else {
-	setenv_cond("status.net.sntp.server", NULL);
+	setvar_ip4(status_net_sntp_server, null_ip);
     }
 }
 
@@ -143,7 +149,7 @@ static void sntp_server_found(const char *name, const ip_addr_t *addr,
     (void)name;
     (void)arg;
 
-    if (invalid_ip(addr))
+    if (!IP4(*addr))
 	return;
 
     sntp_setserver(0, addr);
@@ -163,29 +169,19 @@ static void sntp_set_server(const char *name)
 
 static void dns_setup(void)
 {
-    const ip_addr_t *dns_ip = dns_getserver(0);
+    IP4 dns_ip = *dns_getserver(0);
 
-    if (invalid_ip(dns_ip) || getenv_bool("ip4.dhcp.nodns")) {
+    if (!dns_ip || getvar_bool(config_ip4_dhcp_nodns)) {
 	/* Static DNS server configuration */
-	ip_addr_t addr;
-	if (dnsserver && inet_aton(dnsserver, &addr)) {
-	    if (memcmp(dns_ip, &addr, sizeof addr))
-		dns_setserver(0, &addr);
+	if (dns_ip != dnsserver) {
+	    ip_addr_t addr = dnsserver;
+	    dns_setserver(0, &addr);
 	}
     }
 
-    dns_ip = dns_getserver(0);
-    const char *dns_server_str = inet_ntoa(*dns_ip);
-    printf("[DNS]  DNS server: %s\n", dns_server_str);
-    setenv_cond("status.net.dns.server", dns_server_str);
-}
-
-static esp_ip_addr_t ipaddr_toesp(IPAddress ip)
-{
-    esp_ip_addr_t eip;
-    memset(&eip, 0, sizeof eip);
-    eip.u_addr.ip4.addr = (uint32_t)ip;
-    return eip;
+    dns_ip = *dns_getserver(0);
+    printf("[DNS]  DNS server: %s\n", dns_ip.cstr());
+    setvar_ip4(status_net_dns_server, dns_ip);
 }
 
 static void mdns_setup(void)
@@ -204,7 +200,7 @@ static void mdns_setup(void)
     if (mdns_started)
 	mdns_free();
 
-    if (!getenv_bool("mdns.enabled"))
+    if (!getvar_bool(config_mdns_enabled))
 	return;
 
     mdns_started = mdns_init() == ESP_OK;
@@ -219,7 +215,7 @@ static void mdns_setup(void)
     snprintf(unique_name, sizeof unique_name, "MAX80-%s", serial_number);
     if (connected & CON_STA) {
 	mdns_ip_addr_t iplist;
-	iplist.addr = ipaddr_toesp(WiFi.localIP());
+	iplist.addr = IP4(WiFi.localIP());
 	iplist.next = NULL;
 	unique_mdns = mdns_delegate_hostname_add(unique_name, &iplist);
 	printf("[MDNS] mDNS unique hostname: %s\n", unique_name);
@@ -242,11 +238,12 @@ static void start_services(void)
 
     // If Arduino supported both of these at the same that would be
     // awesome, but it requires ESP-IDF reconfiguration...
-    if (getenv_bool("sntp.enabled")) {
-	if (!invalid_ip(sntp_getserver(0))) {
+    if (getvar_bool(config_sntp_enabled)) {
+	IP4 sntp_ip = *sntp_getserver(0);
+	if (sntp_ip) {
 	    sntp_server_show();
 	} else {
-	    sntp_set_server(getenv("sntp.server"));
+	    sntp_set_server(getvar_str(config_sntp_server));
 	}
     }
 
@@ -259,18 +256,6 @@ static void start_services(void)
     }
 }
 
-static const char *ip_str(const IPAddress &ip)
-{
-    static char ip_buf[4*4];
-    const IPAddress ip_none(0,0,0,0);
-    return strcpy(ip_buf, ip == ip_none ? "" : ip.toString().c_str());
-}
-
-static void setenv_ip(const char *var, const IPAddress &ip)
-{
-    setenv_config(var, ip_str(ip));
-}
-
 static bool force_conn_update;
 
 static void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
@@ -280,8 +265,8 @@ static void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
     unsigned int prev_connected = connected;
     static int ap_clients;
     int prev_ap_clients = ap_clients;
-    IPAddress wifi_local_ip = WiFi.localIP();
-    const char *local_ip = ip_str(wifi_local_ip);
+    IP4 wifi_local_ip = WiFi.localIP();
+    const char *local_ip = wifi_local_ip.cstr();
 
     switch (event) {
     case ARDUINO_EVENT_WIFI_READY:
@@ -416,20 +401,20 @@ static void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
 	force_conn_update = false;
 
 	if (conn_change & CON_STA) {
-	    setenv_bool("status.net.sta.conn", connected & CON_STA);
-	    setenv_config("status.net.sta.ip4",
-			  connected & CON_STA ? local_ip : "");
-	    setenv_ip("status.net.sta.ip4.mask", WiFi.subnetMask());
-	    setenv_ip("status.net.sta.ip4.gw", WiFi.gatewayIP());
+	    setvar_bool(status_net_sta_conn, connected & CON_STA);
+	    setvar_ip4(status_net_sta_ip4,
+		      connected & CON_STA ? wifi_local_ip : null_ip);
+	    setvar_ip4(status_net_sta_ip4_mask, WiFi.subnetMask());
+	    setvar_ip4(status_net_sta_ip4_gw, WiFi.gatewayIP());
 	}
 	if (conn_change & CON_AP)
-	    setenv_bool("status.net.ap.conn", connected & CON_AP);
+	    setvar_bool(status_net_ap_conn, connected & CON_AP);
 	if (conn_change & CON_ETH) {
-	    setenv_bool("status.net.eth.conn", connected & CON_ETH);
-	    setenv_config("status.net.eth.ip4",
-			  connected & CON_ETH ? local_ip : "");
-	    setenv_ip("status.net.eth.ip4.mask", WiFi.subnetMask());
-	    setenv_ip("status.net.eth.ip4.gw", WiFi.gatewayIP());
+	    setvar_bool(status_net_eth_conn, connected & CON_ETH);
+	    setvar_ip4(status_net_eth_ip4,
+		      connected & CON_STA ? wifi_local_ip : null_ip);
+	    setvar_ip4(status_net_eth_ip4_mask, WiFi.subnetMask());
+	    setvar_ip4(status_net_eth_ip4_gw, WiFi.gatewayIP());
 	}
 
 	if (!ssid) {
@@ -445,7 +430,7 @@ static void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
     }
 
     if (ap_clients != prev_ap_clients)
-	setenv_ul("status.net.ap.clients", ap_clients);
+	setvar_uint(status_net_ap_clients, ap_clients);
 
     /*
      * Don't keep retrying if there are AP clients - makes the AP
@@ -456,39 +441,29 @@ static void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
     }
 }
 
-static void setenv_mac(const char *var, const uint8_t mac[6])
-{
-    char mac_str[3*6];
-
-    snprintf(mac_str, sizeof mac_str, "%02x:%02x:%02x:%02x:%02x:%02x",
-	     mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
-    setenv_cond(var, mac_str);
-}
-
 static void wifi_config_ap(void)
 {
     /* No network configuration set */
-    IPAddress AP_IP      = IPAddress(192,168,0,1);
-    IPAddress AP_Netmask = IPAddress(255,255,255,0);
-    IPAddress AP_Gateway = IPAddress(0,0,0,0); // No gateway
+    IP4 AP_IP      = IP4(192,168,0,1);
+    IP4 AP_Netmask = IP4(255,255,255,0);
+    IP4 AP_Gateway = IP4(0,0,0,0); // No gateway
     unsigned int channel = (time(NULL) % 11) + 1;	   // Pseudo-random
     uint8_t mac[6];
     static char ap_ssid[64];
 
     WiFi.softAPmacAddress(mac);
-    setenv_mac("status.net.ap.mac", mac);
+    setvar_mac(status_net_ap_mac, mac);
 
     /* The last two bytes of the efuse MAC */
     snprintf(ap_ssid, sizeof ap_ssid, "MAX80_%02X%02X",
 	     efuse_default_mac[4], efuse_default_mac[5]);
 
-    printf("[WIFI] AP SSID %s IP %s netmask %s channel %u\n",
-	   ap_ssid, AP_IP.toString().c_str(),
-	   AP_Netmask.toString().c_str(), channel);
-    setenv_cond("status.net.ap.ssid", ap_ssid);
-    setenv_ip("status.net.ap.ip4", AP_IP);
-    setenv_ip("status.net.ap.ip4.mask", AP_Netmask);
-    setenv_ul("status.net.ap.clients", 0);
+    printf("[WIFI] AP SSID %s IP %s ", ap_ssid, AP_IP.cstr());
+    printf("netmask %s channel %u\n", AP_Netmask.cstr(), channel);
+    setvar_str(status_net_ap_ssid, ap_ssid);
+    setvar_ip4(status_net_ap_ip4, AP_IP);
+    setvar_ip4(status_net_ap_ip4_mask, AP_Netmask);
+    setvar_uint(status_net_ap_clients, 0);
 
     printf("WiFi.softAP\n");
     WiFi.softAP(ap_ssid, NULL, channel, 0, 4, true);
@@ -513,10 +488,10 @@ static void wifi_config_sta(void)
     printf("WiFi.macAddress\n");
     WiFi.macAddress(mac);
     printf("setenv_mac\n");
-    setenv_mac("status.net.sta.mac", mac);
+    setvar_mac(status_net_sta_mac, mac);
 
     printf("setenv ssid\n");
-    setenv_cond("status.net.sta.ssid", ssid);
+    setvar_str(status_net_sta_ssid, ssid);
     if (!ssid) {
 	WiFi.enableSTA(false);
 	return;
@@ -539,10 +514,10 @@ static void wifi_config_sta(void)
 
 static void wifi_config(void)
 {
-    ssid         = getenv_notempty("wifi.ssid");
-    password     = getenv_notempty("wifi.psk");
-    hostname     = getenv_def("hostname", "max80");
-    dnsserver    = getenv_notempty("ip4.dns");
+    ssid         = dupstr(getvar_str(config_wifi_ssid));
+    password     = dupstr(getvar_str(config_wifi_psk));
+    hostname     = dupstr(getvar_str(config_hostname));
+    dnsserver    = getvar_ip4(config_ip4_dns);
 
     force_conn_update = true;
 
@@ -553,7 +528,7 @@ static void wifi_config(void)
 
     WiFi.setTxPower(WIFI_POWER_19_5dBm);
 
-    setenv_config("status.hostname", hostname);
+    setvar_str(status_hostname, hostname);
     WiFi.hostname(hostname);
 
     printf("wifi_config_ap\n");

BIN
esp32/output/max80.ino.bin


+ 168 - 0
esp32/sysvars.pl

@@ -0,0 +1,168 @@
+#!/usr/bin/perl
+use strict;
+use integer;
+use File::Basename;
+
+sub c_name($) {
+    my($s) = @_;
+
+    $s =~ s/[^A-Za-z0-9_]+/_/g;
+    return $s;
+}
+
+die "Usage: $0 infile hfile cfile\n" unless (scalar @ARGV >= 3);
+
+my($infile, $hfile, $cfile) = @ARGV;
+
+my $err = 0;
+
+open(my $in, '<', $infile) or die "$0: $infile: $!\n";
+my $ns;
+my %vars;
+my %nsprefix;
+
+my $line = 0;
+while (defined(my $l = <$in>)) {
+    $line++;
+    $l =~ s/^\s+//;
+    $l =~ s/\s+$//;
+    next if ($l =~ /^(\#.*)?$/);
+    if ($l =~ /^\@(\w+)(\s+([\w\.]+))?/) {
+	$ns = $1;
+	if (!defined($vars{$ns})) {
+	    $vars{$ns} = {};
+	}
+	$nsprefix{$ns} = $3;
+	next;
+    }
+
+    if (!defined{$ns}) {
+	print STDERR "$0:$infile:$line: variable without namespace\n";
+	$err++;
+	next;
+    }
+
+    my($var,$type,$defval) = split(/\s+/, $l);
+    if (!defined($type)) {
+	print STDERR "$0:$infile:$line: variable $var lacks type\n";
+	$err++;
+	next;
+    }
+    $vars{$ns}->{$var} = [$type, $defval];
+}
+
+close($in);
+
+exit 1 if ($err);
+
+my @nslist = sort keys(%vars);
+my %nsfirst;
+my %nscount;
+my @varname = (undef);
+my @varenum = ('sysvar_null');
+my %vartype = ('sysvar_null' => 'SYSVAR_NULLTYPE');
+my @vardef;
+
+my $cnt = 1;
+foreach my $ns (@nslist) {
+    $nsfirst{$ns} = $cnt;
+    foreach my $var (sort keys(%{$vars{$ns}})) {
+	my($type, $defval) = @{$vars{$ns}->{$var}};
+	push(@varname, $nsprefix{$ns}.$var);
+
+	my $ename = c_name($ns.'.'.$var);
+	push(@varenum, $ename);
+
+	$vartype{$ename} = "SYSVAR_TYPE(_$type)";
+	if (defined($defval)) {
+	    # Add mangling here if needed
+	    push(@vardef, "\t[$ename] = { .v_$type = $defval },\n");
+	}
+	$cnt++;
+    }
+    $nscount{$ns} = $cnt - $nsfirst{$ns};
+}
+
+open(my $h, '>', $hfile) or die "$0:$hfile: $!\n";
+my $htag = c_name(uc(basename($hfile)));
+print $h "#ifndef $htag\n";
+print $h "#define $htag\n\n";
+
+print $h "enum sysvar_ns_enum {\n";
+print $h map { "\tsysvar_ns_$_,\n" } @nslist;
+print $h "\tsysvar_nscount\n};\n\n";
+
+print $h "enum sysvar_enum {\n";
+print $h map { "\t$_,\n" } @varenum;
+print $h "\tsysvar_count\n};\n\n";
+
+push(@varenum, 'sysvar_count');
+
+print $h "\n";
+
+# Sometimes it is convenient to have this as a preprocessor symbol
+print $h "#define SYSVAR_COUNT $cnt\n\n";
+
+print $h "extern_c const char * const sysvar_name[sysvar_count];\n";
+print $h "extern_c const sysvar_type_t sysvar_types[sysvar_count];\n";
+print $h "extern_c const sysvar_t sysvar_defval[sysvar_count];\n";
+print $h "extern_c sysvar_t sysvar_val[sysvar_count];\n";
+print $h "extern_c bool sysvar_isset[sysvar_count];\n\n";
+
+print $h "static inline sysvar_type_t sysvar_type(size_t var)\n";
+print $h "{\n";
+print $h "\tif (!__builtin_constant_p(var)) {\n";
+print $h "\t\tif(var >= (size_t)sysvar_count)\n";
+print $h "\t\t\treturn SYSVAR_NULLTYPE;\n";
+print $h "\t\telse\n";
+print $h "\t\t\treturn sysvar_types[var];\n";
+print $h "\t}\n\n";
+print $h "\tswitch(var) {\n";
+foreach my $v (@varenum) {
+    next unless (defined($vartype{$v}));
+    print $h "\t\tcase ", $v, ":\n";
+    print $h "\t\t\treturn ", $vartype{$v}, ";\n";
+}
+print $h "\t\tdefault:\n";
+print $h "\t\t\treturn SYSVAR_NULLTYPE;\n";
+print $h "\t};\n";
+print $h "}\n\n";
+
+print $h "#endif /* $htag */\n";
+close($h);
+
+open(my $c, '>', $cfile) or die "$0:$cfile: $!\n";
+print $c "#include \"sysvars.h\"\n\n";
+
+printf $c "const sysvar_ns_t sysvar_ns[%d] = {\n", scalar(@nslist)+1;
+foreach my $ns (@nslist) {
+    printf $c "\t{ \"%s\", %d },\n", $ns, $nsfirst{$ns}, $nscount{$ns};
+}
+printf $c "\t{ NULL, %d }\n", $cnt;
+printf $c "};\n";
+
+print $c "const char * const sysvar_name[$cnt] = {\n";
+print $c "\tNULL,\n";
+foreach my $ns (@nslist) {
+    print $c "\n\t/* ---- $ns ---- */\n";
+    my $v_end = $nsfirst{$ns} + $nscount{$ns};
+    for (my $v = $nsfirst{$ns}; $v < $v_end; $v++) {
+	print $c "\t\"", $varname[$v], "\",\n";
+    }
+}
+print $c "};\n";
+
+print $c "const sysvar_type_t sysvar_types[$cnt] = {\n";
+foreach my $v (@varenum) {
+    next unless (defined($vartype{$v}));
+    print $c "\t[$v] = ", $vartype{$v}, ",\n";
+}
+print $c "};\n";
+
+print $c "const sysvar_t sysvar_defval[$cnt] = {\n";
+print $c @vardef;
+print $c "};\n";
+
+print $c "sysvar_t sysvar_val[$cnt];\n";
+print $c "bool sysvar_isset[$cnt];\n";
+close($c);

+ 41 - 0
esp32/sysvars.vars

@@ -0,0 +1,41 @@
+@config
+LANG			str "sv"
+TZ			tz "CET-1CEST,M3.5.0,M10.5.0/3"
+abc.hosttype		str "auto"
+hostname		str "max80"
+http.status.refresh	uint 10
+ip4.dhcp.nodns		bool
+ip4.dhcp.nosntp		bool
+ip4.dns			ip
+mdns.enabled		bool true
+sntp.enabled		bool true
+sntp.server		str "time.max80.abc80.org"
+tzname			str "Europe/Stockholm"
+wifi.psk		str
+wifi.ssid		str
+
+@status
+hostname		str
+max80.fpga		bool
+max80.fw.date		str
+max80.hw.serial		str
+max80.hw.ver		str
+net.ap.clients		uint
+net.ap.conn		bool
+net.ap.ip4		ip
+net.ap.ip4.mask		ip
+net.ap.mac		mac
+net.ap.ssid		str
+net.dns.server		ip
+net.eth.conn		bool
+net.eth.ip4		ip
+net.eth.ip4.gw		ip
+net.eth.ip4.mask	ip
+net.sntp.server		ip
+net.sntp.sync		bool
+net.sta.conn		bool
+net.sta.ip4		ip
+net.sta.ip4.gw		ip
+net.sta.ip4.mask	ip
+net.sta.mac		mac
+net.sta.ssid		str

+ 3 - 3
fpga/max80.qpf

@@ -19,15 +19,15 @@
 #
 # Quartus Prime
 # Version 22.1std.0 Build 915 10/25/2022 SC Lite Edition
-# Date created = 22:54:03  February 09, 2023
+# Date created = 13:58:18  September 05, 2023
 #
 # -------------------------------------------------------------------------- #
 
 QUARTUS_VERSION = "22.1"
-DATE = "22:54:03  February 09, 2023"
+DATE = "13:58:18  September 05, 2023"
 
 # Revisions
 
-PROJECT_REVISION = "v2"
 PROJECT_REVISION = "v1"
+PROJECT_REVISION = "v2"
 PROJECT_REVISION = "bypass"

BIN
fpga/output/bypass.jic


BIN
fpga/output/bypass.rbf.gz


BIN
fpga/output/bypass.rpd.gz


BIN
fpga/output/bypass.sof


BIN
fpga/output/bypass.svf.gz


BIN
fpga/output/bypass.xsvf.gz


BIN
fpga/output/max80.fw


BIN
fpga/output/v1.fw


BIN
fpga/output/v1.jic


BIN
fpga/output/v1.rbf.gz


BIN
fpga/output/v1.rpd.gz


BIN
fpga/output/v1.sof


BIN
fpga/output/v1.svf.gz


BIN
fpga/output/v1.xsvf.gz


BIN
fpga/output/v2.fw


BIN
fpga/output/v2.jic


BIN
fpga/output/v2.rbf.gz


BIN
fpga/output/v2.rpd.gz


BIN
fpga/output/v2.sof


BIN
fpga/output/v2.svf.gz


BIN
fpga/output/v2.xsvf.gz


+ 1 - 1
rv32/checksum.h

@@ -1,4 +1,4 @@
 #ifndef CHECKSUM_H
 #define CHECKSUM_H
-#define SDRAM_SUM 0x8751cf2a
+#define SDRAM_SUM 0x674c2aeb
 #endif

+ 19 - 13
tools/flashmax.pl

@@ -128,7 +128,7 @@ sub unquote_cmd($) {
     return @a;
 }
 
-# Similar to grep, but for a hash; also filters out 
+# Similar to grep, but for a hash; also filters out
 sub hgrep(&%) {
     my($mfunc, %hash) = @_;
 
@@ -216,13 +216,13 @@ sub match_version($$)
     my @vv = target_string_valid($version);
 
     return 0 unless (defined($vv[0]));
-    
+
     my $v_board = $vv[0];
     my @v_ver   = split(/\./, $vv[1]);
     my $v_flags = $vv[2];
 
     return 1 if ($pattern eq $v_board); # Board only matchall pattern
-    
+
     if ($pattern !~ /^(\S+) v((?:0|[1-9][0-9]*)(?:\.\.0|\.[1-9][0-9]*)*)(?:(\-)((?:0|[1-9][0-9]*)(?:\.\.0|\.[1-9][0-9]*)*))?(?: ([\+\-a-zA-Z0-9]*))?$/) {
 	return 0;
     }
@@ -256,12 +256,18 @@ sub match_version($$)
 
     return 1;
 }
-    
+
+my %espopt = ('before' => 'default_reset', 'after' => 'hard_reset',
+	      'connect-attempts' => 8);
+
+
 sub get_target_board($$)
 {
     my($port,$boardinfo) = @_;
 
-    run_esptool($port, { 'before' => 'default_reset', 'after' => 'hard_reset' },
+    my %myespopt = (%espopt);
+    $myespopt{'after'} = 'no_reset';
+    run_esptool($port, \%myespopt,
 		'read_flash', { },
 		''.$boardinfo_addr, ''.$boardinfo_len, $boardinfo);
 
@@ -286,7 +292,7 @@ sub get_target_board($$)
 
     return undef;
 }
-    
+
 my @args = @ARGV;
 my $esponly = 0;
 my $file;
@@ -349,7 +355,7 @@ if (defined($target_board)) {
     push(@espfiles, ''.$boardinfo_addr, $boardinfo);
 } else {
     # Get the board version from target flash
-    
+
     $target_board = get_target_board($port, $boardinfo);
     if (!defined($target_board) || !target_string_valid($target_board)) {
 	die "$0: $port: board version not programmed, specify with --setver\n";
@@ -414,7 +420,7 @@ while (read($fw, $hdr, 16) == 16) {
 	    $err = 1;
 	    last;
 	}
-	
+
 	if ($c->{'vmin'} > $version_match->{'vmax'} ||
 	    $c->{'vmax'} < $version_match->{'vmin'} ||
 	    (($c->{'vmatch'} ^ $version_match->{'vmatch'}) & $c->{'vmask'})) {
@@ -422,7 +428,7 @@ while (read($fw, $hdr, 16) == 16) {
 	    next;
 	}
     }
-    
+
     push(@chunks, $c);
 
     last if ($t eq 'end'); # End of stream
@@ -431,10 +437,10 @@ while (read($fw, $hdr, 16) == 16) {
 close($fw);
 exit $err if ($err);
 
-my %espopt = ('before' => 'default_reset', 'after' => 'hard_reset',
-	      'baud' => 115200, 'port' => undef, 'chip' => undef,
-	      'flash_mode' => undef, 'flash_freq' => undef,
-	      'flash_size' => undef);
+%espopt = (%espopt,
+	   'baud' => 115200, 'port' => undef, 'chip' => undef,
+	   'flash_mode' => undef, 'flash_freq' => undef,
+	   'flash_size' => undef);
 
 # Create a compressed data buffer without the ESP32 chunks
 my $fpgadata;