Просмотр исходного кода

Merge esplink work with mainline

Merge remote-tracking branch 'origin/main'
H. Peter Anvin 2 лет назад
Родитель
Сommit
3847060a6e
49 измененных файлов с 635 добавлено и 318 удалено
  1. 1 0
      .gitignore
  2. 7 8
      Makefile
  3. 1 0
      esp32/.gitignore
  4. 6 0
      esp32/max80/common.h
  5. 39 30
      esp32/max80/config.c
  6. 1 1
      esp32/max80/config.h
  7. 7 3
      esp32/max80/fpgasvc.c
  8. 35 20
      esp32/max80/httpd.c
  9. 6 2
      esp32/max80/max80.ino
  10. 95 27
      esp32/max80/wifi.cpp
  11. BIN
      esp32/output/max80.ino.bin
  12. BIN
      esp32/www/Charm-Bold-PoP.woff2
  13. 1 1
      esp32/www/_redir
  14. 67 61
      esp32/www/config.html
  15. 2 1
      esp32/www/head.html
  16. 29 11
      esp32/www/lang/sv
  17. 50 33
      esp32/www/max80.css
  18. 164 81
      esp32/www/max80.js
  19. 0 5
      esp32/www/showstatus.html
  20. 88 3
      esp32/www/status.html
  21. 17 15
      esp32/www/update.html
  22. 3 3
      fpga/max80.qpf
  23. BIN
      fpga/output/bypass.jic
  24. BIN
      fpga/output/bypass.rbf.gz
  25. BIN
      fpga/output/bypass.rpd.gz
  26. BIN
      fpga/output/bypass.sof
  27. BIN
      fpga/output/bypass.svf.gz
  28. BIN
      fpga/output/bypass.xsvf.gz
  29. BIN
      fpga/output/v1.fw
  30. BIN
      fpga/output/v1.jic
  31. BIN
      fpga/output/v1.rbf.gz
  32. BIN
      fpga/output/v1.rpd.gz
  33. BIN
      fpga/output/v1.sof
  34. BIN
      fpga/output/v1.svf.gz
  35. BIN
      fpga/output/v1.xsvf.gz
  36. BIN
      fpga/output/v2.fw
  37. BIN
      fpga/output/v2.jic
  38. BIN
      fpga/output/v2.rbf.gz
  39. BIN
      fpga/output/v2.rpd.gz
  40. BIN
      fpga/output/v2.sof
  41. BIN
      fpga/output/v2.svf.gz
  42. BIN
      fpga/output/v2.xsvf.gz
  43. 1 1
      rv32/Makefile
  44. 1 1
      rv32/checksum.h
  45. 1 2
      rv32/head.S
  46. 0 4
      rv32/jtagupd.ld
  47. 1 5
      rv32/max80.ld
  48. 6 0
      version.h.sed
  49. 6 0
      version.vh.sed

+ 1 - 0
.gitignore

@@ -32,3 +32,4 @@ fpga/output_files/*.htm
 fpga/output_files/*.htm_files/
 tools/gnu/
 version.*
+!*.sed

+ 7 - 8
Makefile

@@ -23,18 +23,13 @@ $(SUBDIRS):
 version.mk:
 	if [ -d '$(GIT_DIR)' ]; then \
 	    TZ=UTC0 git log -n 1 --date=local --abbrev=8 \
-		--pretty='format:COMMIT_VERSION=$(PROJECT)-%(describe:abbrev=6)%nCOMMIT_ID=%H%nCOMMIT_MAGIC=0x%h%nCOMMIT_DATE=%cd UTC%n' > $@.tmp ; \
+		--pretty='format:COMMIT_VERSION=$(PROJECT)-%(describe:abbrev=6)%nCOMMIT_ID=%H%nCOMMIT_MAGIC=%h%nCOMMIT_DATE=%cd UTC%n' > $@.tmp ; \
 		if cmp -q '$@' '$@.tmp' 2>/dev/null; then \
 			rm -f $@.tmp; else mv -f $@.tmp $@; fi \
 	fi
 
-version.h: version.mk
-	sed -e 's/^/#define /' -e 's/=/ "/' -e 's/$$/"/' \
-		-e '/ COMMIT_MAGIC /s/"//g' < $< > $@
-
-version.vh: version.mk
-	sed -e 's/^/`define /' -e 's/=/ "/' -e 's/$$/"/' \
-		-e '/ COMMIT_MAGIC /s/"//g' < $< > $@
+version.%: version.mk version.%.sed
+	sed -f $@.sed $< > $@
 
 $(SUBDIRS): version.mk
 
@@ -66,3 +61,7 @@ program-% flash-%: prefpga
 upload-esp:
 	$(MAKE) -C esp32 upload
 
+# Update via HTTP
+ip=max80
+upload-%:
+	curl -v --data-binary @fpga/output/$*.fw http://$(ip)/sys/fwupdate

+ 1 - 0
esp32/.gitignore

@@ -4,6 +4,7 @@
 *.zip
 applet/
 build/
+cache/
 *.bootloader.bin
 *.partitions.bin
 *.elf

+ 6 - 0
esp32/max80/common.h

@@ -35,6 +35,12 @@
 #endif
 #define MSG(...)  CMSG(MODULE ": " __VA_ARGS__)
 
+/*
+ * Hack to deal with the lack of this information in FreeRTOS proper
+ */
+#define EVENT_BITS	(configUSE_16_BIT_TICKS ? 8 : 24)
+#define EVENT_ALL_BITS	((1U << EVENT_BITS)-1)
+
 /*
  * Common types for callbacks
  */

+ 39 - 30
esp32/max80/config.c

@@ -6,12 +6,16 @@
 
 #define CONFIG_FILE	"/spiffs/config.txt"
 
-static const char * const default_config[] = {
-    "TZ=CET-1CEST,M3.5.0,M10.5.0/3", /* Sweden */
-    "hostname=max80",
-    "sntp.enabled=on",
-    "sntp.server=time.max80.abc80.org",
-    "LANG=sv"
+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"},
+    {"sntp.enabled", "1"},
+    {"sntp.server","time.max80.abc80.org"}
 };
 
 static int save_config(void);
@@ -32,25 +36,30 @@ int setenv_config(const char *name, const char *value)
 
 static int reset_config(void)
 {
-    char **new_environ = malloc((ARRAY_SIZE(default_config)+1)*sizeof(char *));
-
-    if (!new_environ)
-	return -1;
+    while (1) {
+	char **envp;
+
+	for (envp = environ; *envp; envp++) {
+	    if (!strncmp("status.", *envp, 7))
+		continue;
+	    else
+		break;
+	}
 
-    int i;
-    for (i = 0; i < ARRAY_SIZE(default_config); i++) {
-	if (!(new_environ[i] = strdup(default_config[i])))
-	    return -1;
-    }
+	if (!*envp)
+	    break;
 
-    new_environ[i] = NULL;
+	const char *eq = strchr(*envp, '=');
+	char ename[eq - *envp + 1];
+	memcpy(ename, *envp, eq - *envp);
+	ename[eq - *envp] = '\0';
 
-    char **old_environ = environ;
-    environ = new_environ;
+	unsetenv(ename);
+    }
 
-    for (char **envp = old_environ; *envp; envp++)
-	free(*envp);
-    free(old_environ);
+    size_t i;
+    for (i = 0; i < ARRAY_SIZE(default_config); i++)
+	setenv(default_config[i].var, default_config[i].val, 1);
 
     config_changed = true;
 }
@@ -166,11 +175,14 @@ exit:
     return err;
 };
 
-int write_config(FILE *f)
+int write_env(FILE *f, bool status)
 {
+    size_t skip = status ? 7 : 0;
     for (char **var = environ; *var; var++) {
-	fputs(*var, f);
-	putc('\n', f);
+	if (!strncmp(*var, "status.", 7) == status) {
+		fputs(*var + skip, f);
+		putc('\n', f);
+	}
     }
 
     return ferror(f) ? -1 : 0;
@@ -182,7 +194,7 @@ static int save_config(void)
 
     FILE *f = fopen(CONFIG_FILE, "w");
     if (f) {
-	err = write_config(f);
+	err = write_env(f, false);
 	fclose(f);
     }
 
@@ -274,11 +286,8 @@ bool getenv_bool(const char *var)
     unsigned char c = *var;
     unsigned char cl = c | 0x20;
 
-    if (!c || c == '0' || cl == 'f' || cl == 'n' ||
-	cl == 'o' && (var[1] | 0x20) == 'f')
-	return false;
-    else
-	return true;
+    return !(!c || c == '0' || cl == 'f' || cl == 'n' || cl == 'd' ||
+	     (cl == 'o' && (var[1] | 0x20) == 'f'));
 }
 
 void setenv_bool(const char *var, bool val)

+ 1 - 1
esp32/max80/config.h

@@ -7,7 +7,7 @@
 #define MAX_CONFIG_LINE 256
 
 extern_c int read_config(FILE *, bool save);
-extern_c int write_config(FILE *);
+extern_c int write_env(FILE *, bool status);
 extern_c void init_config(void);
 
 extern_c int set_config_url_string(const char *str);

+ 7 - 3
esp32/max80/fpgasvc.c

@@ -134,6 +134,8 @@ esp_err_t fpga_service_init(void)
 
     pinMode(PIN_FPGA_INT, INPUT_PULLUP);
 
+    setenv_bool("status.max80.fpga", false);
+
     fpga_service_evgroup = null_check(xEventGroupCreate());
     spi_mutex = null_check(xSemaphoreCreateRecursiveMutex());
     spi_done_evgroup = null_check(xEventGroupCreate());
@@ -172,7 +174,7 @@ static bool fpga_link_enable(void)
     if (err)
 	goto release_bus_fail;
 
-    xEventGroupClearBits(spi_done_evgroup, -1);
+    xEventGroupClearBits(spi_done_evgroup, EVENT_ALL_BITS);
     
     pinMode(PIN_FPGA_INT, INPUT_PULLUP);
     attachInterrupt(PIN_FPGA_INT, fpga_interrupt, FALLING);
@@ -245,6 +247,7 @@ static bool fpga_online(void)
     printf("[FPGA] online, signature \"%s\"\n", signature_string);
     esplink_start(&head);
 
+    setenv_bool("status.max80.fpga", true);
     xSemaphoreGiveRecursive(spi_mutex);
     return true;
 }
@@ -252,6 +255,7 @@ static bool fpga_online(void)
 static void fpga_offline(void)
 {
     xSemaphoreTakeRecursive(spi_mutex, portMAX_DELAY);
+    setenv_bool("status.max80.fpga", false);
     esplink_start(NULL);	/* Stop esplink */
 }
 
@@ -310,7 +314,7 @@ esp_err_t fpga_iov(const struct fpga_iov *iov, size_t niov)
 	goto fail;
     }
 
-    xEventGroupClearBits(spi_done_evgroup, ~(EventBits_t)0);
+    xEventGroupClearBits(spi_done_evgroup, EVENT_ALL_BITS);
 
     size_t tbit = 1;
     for (size_t i = 0; i < ntrans; i++) {
@@ -403,7 +407,7 @@ static void fpga_service_task(void *dummy)
 	FPGA_ONLINE		/* FPGA services active */
     } fpga_state = FPGA_DISABLED;
 
-    printf("[FPGA] Starting FPGA services task\n");
+    fputs("[FPGA] Starting FPGA services task\n", stdout);
     
     while (1) {
 	uint32_t notifiers, status;

+ 35 - 20
esp32/max80/httpd.c

@@ -141,13 +141,6 @@ static const char *http_dos_date(uint32_t dos_date)
 
 static const char text_plain[] = "text/plain; charset=\"UTF-8\"";
 
-enum hsp_flags {
-    HSP_CLOSE        = 1,
-    HSP_CRLF         = 2,
-    HSP_CLOSE_SOCKET = 4,
-    HSP_REFERER      = 8	/* Use referer as body (for redirects) */
-};
-
 static void httpd_print_request(const httpd_req_t *req)
 {
     printf("[HTTP] %s %s\n", http_method_str(req->method), req->uri);
@@ -169,6 +162,13 @@ static char *httpd_req_get_hdr(httpd_req_t *req, const char *field, size_t *lenp
     return val;
 }
 
+enum hsp_flags {
+    HSP_CRLF         = 1,    /* Append CR LF to the body */
+    HSP_CLOSE        = 2,    /* Add a Connection: close header */
+    HSP_REFERER      = 4,    /* Use referer as body (for redirects) */
+    HSP_UNCACHE      = 8     /* Wipe caches, please... */
+};
+
 static esp_err_t httpd_send_plain(httpd_req_t *req,
 				  unsigned int rcode,
 				  const char *body, size_t blen,
@@ -208,6 +208,8 @@ static esp_err_t httpd_send_plain(httpd_req_t *req,
     }
 
     const char * const refresher = refresher_buf ? refresher_buf : "";
+    const char * const uncacher = (flags & HSP_UNCACHE)
+      ? "Clear-Site-Data: \"cache\"\r\n" : "";
 
     if (redirect) {
 	/* \0 -> \n so don't include it */
@@ -230,11 +232,13 @@ static esp_err_t httpd_send_plain(httpd_req_t *req,
 			"Content-Length: %zu\r\n"
 			"Date %s\r\n"
 			"Location: %.*s\r\n"
-			"%s%s"
+			"%s%s%s"
 			"\r\n"
 			"%3u Redirect ",
 			rcode, text_plain, blen + blenadj,
-			now, (int)blen, body, closer, refresher, rcode);
+			now, (int)blen, body,
+			closer, refresher, uncacher,
+			rcode);
     } else {
 	size_t blenadj = (flags & HSP_CRLF) ? 2 : 0;
 
@@ -244,10 +248,10 @@ static esp_err_t httpd_send_plain(httpd_req_t *req,
 			"Content-Length: %zu\r\n"
 			"Cache-Control: no-cache\r\n"
 			"Date: %s\r\n"
-			"%s%s"
+			"%s%s%s"
 			"\r\n",
 			rcode, text_plain, blen + blenadj, now,
-			closer, refresher);
+			closer, refresher, uncacher);
     }
 
     if (refresher_buf)
@@ -270,8 +274,7 @@ static esp_err_t httpd_send_plain(httpd_req_t *req,
     if (header)
 	free(header);
 
-    /* Sending ESP_FAIL causes the socket to be immediately closed */
-    return err ? err : (flags & HSP_CLOSE_SOCKET) ? ESP_FAIL : ESP_OK;
+    return err;
 }
 
 #define SL(s) (s), (sizeof(s)-1)
@@ -313,9 +316,8 @@ static esp_err_t httpd_update_done(httpd_req_t *req, const char *what, int err)
     if (!response)
 	len = 0;
 
-    esp_err_t rv = httpd_send_plain(req, err ? 422 : 200, response, len,
-				    HSP_CLOSE|HSP_CRLF,
-				    reboot_time+5);
+    esp_err_t rv = httpd_send_plain(req, err ? 400 : 200, response, len,
+				    HSP_CLOSE|HSP_UNCACHE, reboot_time+5);
     if (response)
 	free(response);
 
@@ -355,7 +357,7 @@ static esp_err_t httpd_set_config(httpd_req_t *req, const char *query)
     return httpd_update_done(req, "Configuration", rv1 ? rv1 : rv2);
 }
 
-static esp_err_t httpd_get_config(httpd_req_t *req)
+static esp_err_t httpd_get_config_status(httpd_req_t *req, bool status)
 {
     FILE *f = httpd_fopen_write(req);
     if (!f)
@@ -364,11 +366,21 @@ static esp_err_t httpd_get_config(httpd_req_t *req)
     httpd_resp_set_type(req, text_plain);
     httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
 
-    int rv = write_config(f);
+    int rv = write_env(f, status);
     fclose(f);
     return rv ? ESP_FAIL : ESP_OK;
 }
 
+static esp_err_t httpd_get_status(httpd_req_t *req)
+{
+    char timebuf[64];
+    time_t now = time(NULL);
+    const struct tm *tm = localtime(&now);
+    strftime(timebuf, sizeof timebuf, "%Y-%m-%d %H:%M:%S %z (%Z)", tm);
+    setenv("status.localtime", timebuf, 1);
+    return httpd_get_config_status(req, true);
+}
+
 static esp_err_t httpd_set_lang(httpd_req_t *req, const char *query)
 {
     if (query) {
@@ -426,8 +438,11 @@ static esp_err_t httpd_sys_handler(httpd_req_t *req)
     if (STRING_MATCHES(file, filelen, "lang"))
 	return httpd_lang_redirect(req);
 
+    if (STRING_MATCHES(file, filelen, "getstatus"))
+	return httpd_get_status(req);
+    
     if (STRING_MATCHES(file, filelen, "getconfig"))
-	return httpd_get_config(req);
+	return httpd_get_config_status(req, false);
 
     if (req->method == HTTP_POST && STRING_MATCHES(file, filelen, "fwupdate"))
 	return httpd_firmware_update(req);
@@ -656,7 +671,7 @@ static esp_err_t httpd_static_handler(httpd_req_t *req)
     len = snprintf(buffer, buffer_size-2,
 		   "HTTP/1.1 %s\r\n"
 		   "Date: %s\r\n"
-		   "Cache-Control: max-age=10\r\n"
+		   "Cache-Control: max-age=10, immutable\r\n"
 		   "ETag: %s\r\n",
 		   response,
 		   http_now(),

+ 6 - 2
esp32/max80/max80.ino

@@ -18,7 +18,8 @@
 static void dump_config()
 {
     printf("--- Configuration:\n");
-    write_config(stdout);
+    write_env(stdout, false);
+    write_env(stdout, true);
     printf("--- End configuration\n");
 }
 
@@ -63,13 +64,16 @@ static void init_hw()
     heap_caps_malloc_extmem_enable(2048); // >= 2K allocations in PSRAM
 
     printf("[PCB]  MAX80 board version: %u\n", max80_board_version);
+    setenv_ul("status.max80.hw.ver", max80_board_version);
 }
 
 void setup() {
-    printf("[START] MAX80 firmware compiled on " __DATE__ " " __TIME__ "\n");
+    const char *fwdate = __DATE__ " " __TIME__;
+    printf("[START] MAX80 firmware compiled on %s\n", fwdate);
     init_hw();
     fpga_service_init();
     init_config();
+    setenv("status.max80.fw.date", fwdate, 1);
     fpga_service_enable(true);
     SetupWiFi();
     printf("[RDY]\n");

+ 95 - 27
esp32/max80/wifi.cpp

@@ -50,6 +50,7 @@ static void sntp_sync_cb(struct timeval *tv)
 	sntp_set_sync_mode(SNTP_SYNC_MODE_IMMED); // Until first sync
 	if (prev_sync_status != sync_status) {
 	    printf("[SNTP] time synchronization lost\n");
+	    setenv_bool("status.net.sntp.sync", false);
 	}
 	break;
     case SNTP_SYNC_STATUS_COMPLETED:
@@ -59,6 +60,7 @@ static void sntp_sync_cb(struct timeval *tv)
 	    const struct tm *tm = localtime(&tv->tv_sec);
 	    strftime(timebuf, sizeof timebuf, "%a %Y-%m-%d %H:%M:%S %z", tm);
 	    printf("[SNTP] Time synchronized: %s\n", timebuf);
+	    setenv_bool("status.net.sntp.sync", true);
 	}
 	break;
     default:
@@ -70,6 +72,8 @@ static void sntp_sync_cb(struct timeval *tv)
 
 static void my_sntp_start()
 {
+    setenv_bool("status.net.sntp.sync", false);
+
     if (getenv_bool("sntp.enabled")) {
 	sntp_set_time_sync_notification_cb(sntp_sync_cb);
 	sntp_setoperatingmode(SNTP_OPMODE_POLL);
@@ -112,6 +116,7 @@ static void start_services(void)
 
     dns_ip = dns_getserver(0);
     printf("[DNS]  DNS server: %s\n", inet_ntoa(*dns_ip));
+    setenv("status.net.dns.server", inet_ntoa(*dns_ip), 1);
 
     // If Arduino supported both of these at the same that would be
     // awesome, but it requires ESP-IDF reconfiguration...
@@ -125,8 +130,13 @@ static void start_services(void)
 	    }
 	}
 
-	if (!invalid_ip(sntp_ip))
-	    printf("[SNTP] Time server: %s\n", inet_ntoa(*sntp_ip));
+	if (!invalid_ip(sntp_ip)) {
+	    const char *sntp_server = inet_ntoa(*sntp_ip);
+	    printf("[SNTP] Time server: %s\n", sntp_server);
+	    setenv("status.net.sntp.server", sntp_server, 1);
+	} else {
+	    unsetenv("status.net.sntp.server");
+	}
     }
 
     /* Only run on first start */
@@ -137,11 +147,20 @@ static void start_services(void)
     }
 }
 
-static String wifi_local_ip(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)
 {
-    return WiFi.localIP().toString();
+    setenv_config(var, ip_str(ip));
 }
 
+static bool force_conn_update;
+
 static void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
 {
     bool retry_sta  = false;
@@ -151,9 +170,13 @@ static void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
 	CON_ETH = 2,
 	CON_AP  = 4
     };
-    static int connected;
+    static unsigned int connected;
+    unsigned int prev_connected = connected;
     static int ap_clients = 0;
-
+    int prev_ap_clients = ap_clients;
+    IPAddress wifi_local_ip = WiFi.localIP();
+    const char *local_ip = ip_str(wifi_local_ip);
+    
     switch (event) {
     case ARDUINO_EVENT_WIFI_READY:
 	printf("[WIFI] Interface ready\n");
@@ -180,15 +203,19 @@ static void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
 	printf("[WIFI] Authentication mode of access point has changed\n");
 	break;
     case ARDUINO_EVENT_WIFI_STA_GOT_IP:
-	printf("[WIFI] Obtained IP address: %s\n", wifi_local_ip().c_str());
+    {
+	printf("[WIFI] Obtained IP address: %s\n", local_ip);
 	connected |= CON_STA;
 	is_connect = true;
 	break;
+    }
     case ARDUINO_EVENT_WIFI_STA_LOST_IP:
-	printf("[WIFI] Lost IP address and IP address is reset to 0\n");
+    {
+	printf("[WIFI] Lost IP address\n");
 	connected &= ~CON_STA;
 	retry_sta = true;
 	break;
+    }
     case ARDUINO_EVENT_WPS_ER_SUCCESS:
 	printf("[WIFI] WiFi Protected Setup (WPS): succeeded in enrollee mode\n");
 	break;
@@ -254,9 +281,10 @@ static void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
 	retry_sta = true;
 	break;
     case ARDUINO_EVENT_ETH_GOT_IP:
-	printf("[ETH]  Obtained IP address: %s\n", wifi_local_ip().c_str());
+	printf("[ETH]  Obtained IP address: %s\n", local_ip);
 	connected |= CON_ETH;
 	is_connect = true;
+	break;
     default:
 	break;
     }
@@ -269,23 +297,59 @@ static void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
 	sta_timeout_enable();
     }
 
-    if (ssid == "") {
-	// No network configured
-	led_set(LED_GREEN, connected & CON_AP ? LED_FLASH_SLOW : LED_OFF);
-    } else {
-	led_set(LED_GREEN, connected & CON_AP ? LED_FLASH_FAST : LED_ON);
+    unsigned int conn_change = force_conn_update ?
+	-1U : (connected ^ prev_connected);
+
+    if (conn_change) {
+	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());
+	}
+	if (conn_change & CON_AP)
+	    setenv_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());
+	}
+
+	if (ssid == "") {
+	    // No network configured
+	    led_set(LED_GREEN, connected & CON_AP ? LED_FLASH_SLOW : LED_OFF);
+	} else {
+	    led_set(LED_GREEN, connected & CON_AP ? LED_FLASH_FAST : LED_ON);
+	}
     }
     
-    /* Maybe need to do these in a different thread? */
     if (is_connect) {
 	start_services();
     }
+
+    if (ap_clients != prev_ap_clients)
+	setenv_ul("status.net.ap.clients", ap_clients);
+
     if (retry_sta) {
 	WiFi.disconnect();
 	WiFi.begin();
     }
 }
 
+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(var, mac_str, 1);
+}
+
 static void wifi_config_ap(void)
 {
     /* No network configuration set */
@@ -294,38 +358,40 @@ static void wifi_config_ap(void)
     IPAddress AP_Gateway = IPAddress(0,0,0,0); // No gateway
     unsigned int channel = time(NULL) % 11;	   // Pseudo-random
     uint8_t mac[6];
+    char mac_str[6*3];
     char ap_ssid[64];
 
-    WiFi.macAddress(mac);
+    WiFi.softAPmacAddress(mac);
+    setenv_mac("status.net.ap.mac", mac);
     /* The last two bytes of the MAC */
     snprintf(ap_ssid, sizeof ap_ssid, "MAX80_%02X%02X", mac[4], mac[5]);
-
+    
     printf("[WIFI] AP SSID %s IP %s netmask %s channel %u\n",
 	       ap_ssid, AP_IP.toString(), AP_Netmask.toString(), channel+1);
+    setenv("status.net.ap.ssid", ap_ssid, 1);
+    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.softAP\n");
-    
     WiFi.softAP(ap_ssid, NULL, channel+1, 0, 4, true);
-
-    printf("WiFi.softAPConfig\n");
     WiFi.softAPConfig(AP_IP, AP_Gateway, AP_Netmask);
-
-    printf("WiFi.softAPsetHostname\n");
     WiFi.softAPsetHostname(ap_ssid);
 
     // Conservative setting: 20 MHz (single channel) only; this is for
     // reliability, not performance.
-    printf("esp_wifi_set_bandwidth\n");
     esp_wifi_set_bandwidth((wifi_interface_t)ESP_IF_WIFI_AP, WIFI_BW_HT20);
 
-    printf("WiFi.enableAP\n");
+    // Enable unconditionally if no SSID
     WiFi.enableAP(ssid == "");
-
-    printf("wifi_config_ap done\n");
 }
 
 static void wifi_config_sta(void)
 {
+    uint8_t mac[6];
+    WiFi.macAddress(mac);
+    setenv_mac("status.net.sta.mac", mac);
+
+    setenv("status.net.sta.ssid", ssid.c_str(), 1);
     if (ssid == "") {
 	WiFi.enableSTA(false);
 	return;
@@ -349,6 +415,8 @@ static void wifi_config(void)
     hostname     = getenv("hostname");
     dnsserver    = getenv("ip4.dns");
 
+    force_conn_update = true;
+
     WiFi.persistent(false);
     WiFi.setSleep(false);
 

BIN
esp32/output/max80.ino.bin


BIN
esp32/www/Charm-Bold-PoP.woff2


+ 1 - 1
esp32/www/_redir

@@ -1 +1 @@
-/config.html
+/status.html

+ 67 - 61
esp32/www/config.html

@@ -2,83 +2,89 @@
 <html>
   <head>
     <link rel="stylesheet" href="max80.css" />
-    <title>MAX80: Configuration</title>
+    <title class="config">MAX80: Configuration</title>
   <script src="max80.js"></script>
   </head>
   <body>
     <script>inc("head.html")</script>
     <h1 class="config">Configuration</h1>
-    <form id="setconfig" action="sys/setconfig" enctype="text/plain" method="post">
+    <form id="setconfig" action="sys/setconfig" method="post" onsubmit="uploadform()">
       <fieldset class="network">
 	<legend>Network</legend>
-	<div>
-	  <label for="wifi.ssid">Network name (SSID):</label>
-	  <input type="text" id="wifi.ssid" name="wifi.ssid" />
-	</div>
-	<div>
-	  <label for="wifi.psk">Network password (PSK):</label>
-	  <input class="mono" type="password" id="wifi.psk" name="wifi.psk" size="50" />
-	  <button type="button" class="show" onclick="showpwd('wifi.psk',this)"><span class="show">show</span><span class="hide">hide</span></button>
-	</div>
+	<label class="wifi-ssid">
+	  <span>Network name (SSID):</span>
+	  <input type="text" name="wifi.ssid" />
+	</label>
+	<label class="wifi-psk">
+	  <span>Network password (PSK):</span>
+	  <input class="mono" type="password" name="wifi.psk" />
+	  <button type="button" class="show" onclick="showpwd()"><span class="show">show</span><span class="hide">hide</span></button>
+	</label>
       </fieldset>
       <fieldset class="datetime">
 	<legend>Date and Time</legend>
-	<div>
-	  <label for="tz">Timezone configuration:</label>
-	  <select id="tzname" name="tzname" onchange="tzn(event,'tz')">
+	<label class="tz">
+	  <span>Time zone:</span>
+	  <select name="tzname" id="tzname" onchange="tzn()">
 	  </select>
-	  <input type="text" id="tz" name="TZ" oninput="tzt(event,'tzname')" />
-	</div>
-	<div>
-	  <label for="sntp.enabled">Synchronize time from network:</label>
-	  <input type="checkbox" id="sntp.enabled" name="sntp.enabled" />
-	</div>
-	<div>
-	  <label for="sntp.server">NTP server:</label>
-	  <input type="text" id="sntp.server" name="sntp.server" />
-	</div>
-	<div>
-	  <label for="ip4.dhcp.nosntp">Ignore DHCP-provided NTP server:</label>
-	  <input type="checkbox" id="ip4.dhcp.nosntp" name="ip4.dhcp.nosntp" />
-	</div>
+	  <input type="text" name="TZ" oninput="tzt()" />
+	</label>
+	<label class="sntp-enabled">
+	  <span>Synchronize time from network:</span>
+	  <input type="checkbox" name="sntp.enabled" />
+	</label>
+	<label class="sntp-server">
+	  <span>NTP server:</span>
+	  <input type="text" name="sntp.server" />
+	</label>
+	<label class="ip4-dhcp-sntp">
+	  <span>Use DHCP-provided NTP server:</span>
+	  <input type="checkbox" name="ip4.dhcp.nosntp" value="0" />
+	</label>
       </fieldset>
       <button class="submit" type="submit" disabled>Update configuration</button>
+      <br />
+      <output></output>
     </form>
     <script>
-      fetchconfig('tz.txt').then(map => {
-	  var elem = getelem('tzname');
-	  var grp = elem;
-	  map.set('',''); map.set('UTC','UTC0');
-	  var zones = Array.from(map.keys());
-	  zones = zones.filter(v => v && v != 'UTC').sort();
-	  zones.unshift('','UTC');
-	  for (const z of zones) {
-	      const zz = z.match(/^(?:(\S+?)\/)?(\S*)$/,z);
-	      if (!zz) { continue; }
-	      if (zz[1] && zz[1] != grp.label) {
-		  grp = document.createElement('OPTGROUP');
-		  grp.label = zz[1];
-		  grp.classList.add('tz.'+zz[1]);
-		  elem.append(grp);
-	      } else if (!zz[1]) {
-		  grp = elem;
-	      }
-	      var opt = document.createElement('OPTION');
-	      opt.value = z;
-	      opt.classList.add('tz.'+z.replaceAll('/','.'));
-	      opt.classList.add('tz.'+zz[2].replaceAll('/','.'));
-	      opt.label = zz[2].replaceAll('_',' ').replaceAll('/',': ');
-	      opt.dataset.tz = map.get(z);
-	      grp.append(opt);
-	  }
-      });
-      function tzn(e,p) {
-	  var tz = e.target.selectedOptions[0].dataset.tz;
-	  if (tz) { getelem(p).value = tz; }
+      function tzn() {
+	  const tz = event.target.selectedOptions[0].dataset.tz;
+	  if (tz) sib(event.target,'input').value = tz;
       }
-      function tzt(e,p) { getelem(p).value = ''; }
+      function tzt() { sib(event.target,'select').value = ''; }
+
+      fetchconfig('tz.txt')
+          .then(map => {
+	      function cln(z) {
+		  return ('tz/'+z).replaceAll('/','_ ')
+		      .replaceAll(/[^\w ]+/g,'-');
+	      }
+	      var elem = getelem('tzname');
+	      var grp = elem;
+	      map.set('',''); map.set('UTC','UTC0');
+	      var zones = Array.from(map.keys());
+	      zones = zones.filter(v => v && v != 'UTC').sort();
+	      zones.unshift('','UTC');
+	      for (const z of zones) {
+		  const zz = z.match(/^(?:(\S+?)\/)?(\S*)$/,z);
+		  if (!zz) { continue; }
+		  if (zz[1] && zz[1] != grp.label) {
+		      grp = document.createElement('OPTGROUP');
+		      grp.label = zz[1];
+		      grp.className = cln(zz[1]);
+		      elem.append(grp);
+		  } else if (!zz[1]) {
+		      grp = elem;
+		  }
+		  const pz = zz[2].replaceAll('_',' ').replaceAll('/',': ');
+		  var opt = new Option(pz, z);
+		  opt.className = cln(z);
+		  opt.dataset.tz = map.get(z);
+		  grp.append(opt);
+	      }
+	      translate(elem);
+	  })
+          .finally(() => {loadform('setconfig','sys/getconfig');} );
     </script>
-    <script>loadform('setconfig','sys/getconfig')</script>
-    <script>translate()</script>
   </body>
 </html>

+ 2 - 1
esp32/www/head.html

@@ -16,7 +16,8 @@
     <rect x="0" y="0" width="100%" height="100%" fill="black"
 	  mask="url(#mask)" />
   </svg>
-  <span class="logo2">by Peter &amp; Per</span>
+  <span class="pad"></span>
+  <span class="logo2">Peter &amp; Per</span>
 </div>
 
 <nav class="navbar" role="navigation">

+ 29 - 11
esp32/www/lang/sv

@@ -1,7 +1,4 @@
-#LANG=[lang]:not([lang='sv']) { display: none; }
-.logo2=Peter &amp; Per
-button .show=Visa
-button .hide=Göm
+LANG=sv
 body .status=Status
 body .config=Konfiguration
 body .update=Uppdatera
@@ -9,15 +6,36 @@ title.status=MAX80: Status
 title.config=MAX80: Konfiguration
 title.update=MAX80: Uppdatera
 .firmware legend=Mjukvara
-"label[for='file']"=Välj <code class="file">.fw</code>-fil:
 .firmware .submit=Uppdatera mjukvara
 .network legend=Nätverk
-"label[for='wifi.ssid']"=Nätverksnamn (SSID):
-"label[for='wifi.psk']"=Lösenord (PSK):
+.wifi-ssid span=Nätverksnamn (SSID):
+.wifi-psk span=Lösenord (PSK):
 .datetime legend=Datum och tid
-"label[for='TZ']"=Tidszonsbeskrivning (POSIX):
-"label[for='sntp.enabled']"=Synkronisera tid från nätet (NTP):
-"label[for='sntp.server']"=NTP-server:
-"label[for='ip4.dhcp.nosntp']"=Använd ej NTP-server från DHCP:
+.tz span=Tidszon:
+.sntp-enabled span=Synkronisera tid från nätet (NTP):
+.sntp-server span=NTP-server:
+.ip4-dhcp-sntp span=Använd NTP-server från DHCP:
 #setconfig .submit=Konfigurera
 .notyet=(jobbar på det)
+button .show=Visa
+button .hide=Göm
+.onerr h3=Förslag till felsökning
+.onerr .retry=Försök igen
+.onerr .jtag=Se till att ingen JTAG-kabel är inkopplad
+.onerr .power=Slå av strömmer (koppla ur båda USB och stäng av ABC)
+.onerr .esp=Uppdatera ESP32 över USB och försök igen
+.onerr .serial=Se serieport för mer detaljerad status
+.wifi-sta legend=Wifi-klient
+.wifi-ap legend=Wifi-router
+.hwver span=Hårdvaruversion:
+.fwdate span=Mjukvaran kompilerad:
+.fpgaok span=FPGA aktiv:
+.net-connected span=Ansluten:
+.net-connected-clients span=Anslutna klienter:
+.wifi-ap .net-connected span=Aktiv:
+.net-mac span=MAC-adress:
+.net-ip4 span=IP-adress:
+.net-ip4-mask span=Nätmask:
+.net-ip4-gw span=Router:
+.sntp-sync span=Klockan i synk med NTP:
+.localtime span=Systemklocka:

+ 50 - 33
esp32/www/max80.css

@@ -9,27 +9,29 @@
 }
 body {
     background: #e6c185;
-    font-family: "arial", "sans-serif";
+    font-family: "arial", "helvetica", "sans-serif";
 }
-.mono {
+.mono, pre, output, tt, code {
     font-family: "source code pro", "monospace";
 }
 div.title {
+    display: flex;
     width: 100%;
     white-space: nowrap;
-    padding: 1em;
+    padding: 1em 0em;
     overflow: hidden;
+    align-items: center;
 }
 div.title svg {
-    display: inline;
-    vertical-align: middle;
+    padding: 0 20px;
+}
+div.title *, nav * {
+    display: flex;
+    margin: 0;
 }
 div.title .logo2 {
-    flex: 1;
-    display: inline;
-    vertical-align: middle;
     text-align: right;
-    margin: 1em 4em;
+    padding: 0 25px;
     font-weight: 700;
     font-size: 50px;
     font-family: "Charm","Brush Script MT","cursive";
@@ -43,11 +45,6 @@ nav {
     white-space: nowrap;
     overflow: hidden;
     align-items: stretch;
-
-}
-nav * {
-    display: flex;
-    margin: 0;
 }
 nav a {
     color: black;
@@ -61,7 +58,7 @@ nav a {
 nav a:hover {
     background: #af9365;
 }
-nav .pad {
+.pad {
     flex-grow: 1;
 }
 nav .text {
@@ -84,20 +81,20 @@ form fieldset legend {
     padding: 0.25em;
     background: #af9365;
 }
-form div {
+form label {
     margin: 0;
     padding: 0.5ch;
     display: flex;
 }
-form label {
+form label span {
     width: 30ch;
 }
-input[type=text], input[type=password] {
+input[type='text'], input[type='password'] {
     flex: 1;
 }
-input[type=file] {
-    font-family: "arial", "sans-serif";
-    font-size: 80%;
+.tz select {
+    padding: 0;
+    margin: 0 0 0 1px;
 }
 button.show, button.hide {
     width: 6ch;
@@ -113,28 +110,48 @@ button.hide .show {
 }
 button {
     width: 28ch;
-    margin: 1em 1ch 1em 1ch;
+    margin: 1em;
     padding: 0.25em;
-    vertical-align: center;
+    vertical-align: middle;
     font-family: "arial", "sans-serif";
     font-size: 100%;
 }
 progress {
-    display: inline-block;
-    width: 100%;
-}
-.result {
     display: block;
-    border: 2px solid black;
-    padding: 0.5ch;
-    border-radius: 5px;
+    width: 100%;
+    margin: 0.5em 0;
 }
-.result.hide {
+output {
     display: none;
+    border: 2px solid black;
+    margin: 1em;
+    padding: 0.75em 1.25em;
+    border-radius: 1em;
+    white-space: pre-wrap;
 }
-.result.ok {
+.ok output {
+    display: block;
     background: #e0ffe0;
 }
-.result.err {
+.err output {
+    display: block;
     background: #ffe0e0;
 }
+.onerr {
+    display: none;
+}
+.err .onerr {
+    display: block;
+}
+.onerr h3 {
+    font-weight: bold;
+    text-decoration: underline;
+}
+.getstatus input {
+    font-family: inherit;
+    font-size: inherit;
+    background: inherit;
+    color: inherit;
+    border: 0;
+    opacity: 1;
+}

+ 164 - 81
esp32/www/max80.js

@@ -1,8 +1,25 @@
+const PassiveListener = { passive: true };
+
 // Get an element by id or an Element object
 function getelem(id) {
     return (id instanceof Element) ? id : document.getElementById(id);
 }
 
+// Find a child with a specific tag
+function chi(me,tag) { return me.getElementsByTagName(tag)[0]; }
+
+// Find a sibling element with a specific tag
+function sib(me,tag) { return chi(me.parentElement, tag); }
+
+// Add/remove class entries in bulk; tags is an Array each containing
+// an Array of arguments to toggle. On return the second element of
+// each Array will be updated to the current value.
+function classmod(elem,tags) {
+    for (var i = 0; i < tags.length; i++)
+	tags[i][1] = elem.classList.toggle(...tags[i]);
+    return tags;
+}
+
 // Read a key=value text file and return it as a Promise of a Map
 function fetchconfig(url) {
     return fetch(url, {redirect: "follow"})
@@ -26,53 +43,66 @@ function fetchconfig(url) {
 	});
 }
 
-function boolcfg(str) { return str && !str.match(/^(0*|[fnd].*|of.*)$/i); }
+// Parse a string for a valid boolean truth value
+function cfgbool(str) {
+    return str && !str.match(/^(0*|[fnd].*|of.*)$/i);
+}
 
-// Initialize a form from a map
-function initform(form,map) {
-    var button = null;
-    var clearers = new Map;
+// Initialize a form from a map. Checkboxes take a cfgbool string;
+// if in the HTML their value is set to a false cfgbool string then
+// the checkbox is inverted logic.
+function initform(form,map,ro = false) {
     form = getelem(form);
 
-    for (var field of form.elements) {
-	if ((field instanceof HTMLInputElement ||
-	     field instanceof HTMLSelectElement) &&
-	    !field.classList.contains("noload")) {
-	    const val = map.get(field.name) || '';
-	    if (field.type == 'checkbox') {
-		field.checked = !boolcfg(val);
-	    } else if (field.type == 'radio') {
-		field.checked = (val == field.name);
-		field.remove();
+    for (var e of form.elements) {
+	if (e.classList.contains('noinit'))
+	    continue;
+	if (ro && e.disabled != undefined)
+	    e.disabled = true;
+	if (e instanceof HTMLInputElement ||
+	    e instanceof HTMLSelectElement) {
+	    const val = map.get(e.name);
+	    if (val == null)
+		continue;
+	    if (ro && e.disabled != undefined)
+		e.disabled = true;
+	    if (e.type == 'checkbox') {
+		e.checked = cfgbool(val) == cfgbool(e.value);
+	    } else if (e.type == 'radio') {
+		e.checked = (val == e.name);
 	    } else {
-		field.value = val;
+		e.value = val;
 	    }
-	} else if (field instanceof HTMLButtonElement &&
-		   field.type == 'submit') {
-	    button = field;
+	} else if (e instanceof HTMLButtonElement) {
+	    e.disabled = ro;
 	}
     }
-
-    if (button) {
-	button.disabled = false; // All loaded, enable the submit button
-    }
 }
 
 // Load form initialization data
-function loadform(form, url) {
+function loadform(form,url,ro = false) {
     fetchconfig(url)
-	.then(map => { initform(form, map) })
+	.then((map) => initform(form,map,ro))
 	.catch(() => {});
 }
 
-// Replace the contents of selected HTML elements based on a map with selectors
-function fillin(map,html)
+// Replace the contents of selected HTML elements based on a map with
+// selectors.
+function fillin(map,html = false,top = document)
 {
     for (const [key,val] of map) {
 	try {
-	    for (var e of document.querySelectorAll(key))
-		if (html) e.innerHTML = val; else e.innerText = val;
-	} catch (error) { }
+	    const m = key.match(/(.*?)(?:\;([\w.-]*)(\??))?$/);
+	    for (var e of top.querySelectorAll(m[1])) {
+		try {
+		    if (!html) e.textContents = val;
+		    else if (!m[2]) e.innerHTML = val;
+		    else if (!m[3]) e.setAttribute(m[2],val);
+		    else if (!cfgbool(val)) e.removeAttribute(m[2]);
+		    else e.setAttribute(m[2],'');
+		} catch(e) { };
+	    }
+	} catch(e) { };
     }
 }
 
@@ -80,71 +110,125 @@ function fillin(map,html)
 function load(url,html = false)
 {
     fetchconfig(url)
-	.then(map => { fillin(map,html) })
+	.then(map => fillin(map,html))
 	.catch(() => {});
 }
 
-// POST file upload with progress and response text
-function uploadfile(event) {
-    event.preventDefault();
-    var form = event.currentTarget;
-    var elem = form.elements;
-    var files = elem["file"].files;
-    if (files.length != 1) {
-	/* Show error */
-	return;
-    }
-    var file = files[0];
+// POST upload of data from within a form, with progress and response text
+function upload(form,data) {
     var xhr = new XMLHttpRequest();
 
-    var progress = form.querySelector("progress");
-    if (progress != undefined) {
+    var progress = chi(form,'progress');
+    if (progress) {
 	progress.value = 0;
-	xhr.upload.addEventListener("progress", (event) => {
-	    if (event.lengthComputable) {
-		progress.max   = event.total * 1.05;
-		progress.value = event.loaded;
-	    }
-	});
+	xhr.upload.addEventListener('progress', (e) => {
+	    if (!e.lengthComputable)
+		return;
+	    progress.max   = e.total * 1.05;
+	    progress.value = e.loaded;
+	}, PassiveListener);
     }
 
-    var result = form.querySelector("pre.result");
-    if (result != undefined) {
-	result.className = "result hide";
-    }
-    xhr.addEventListener("loadend", (event) => {
-	const ok = xhr.status >= 200 && xhr.status <= 299;
-	if (progress != undefined) {
-	    progress.value = ok ? progress.max : 0;
-	}
-	if (result != undefined) {
-	    result.className = "result " + (ok ? "ok" : "err");
-	    result.innerText = xhr.responseText;
+    classmod(form, [['started',1],['done',0],['ok',0],['err',0],['running',1]]);
+
+    xhr.addEventListener('loadend', (e) => {
+	const ok = xhr.status >= 200 && xhr.status < 400;
+	if (progress && ok)
+	    progress.value = progress.max;
+
+	var result = chi(form,'output');
+	if (result) {
+	    var msg = xhr.responseText.trimEnd();
+	    if (!msg)
+		msg = xhr.status + ' ' + xhr.statusText;
+	    result.textContent = msg;
 	}
-    });
 
-    xhr.open("POST", form.action);
+	classmod(form, [['ok',ok],['err',!ok],['running',0],['done',1]]);
+
+	// Automatically reload the page after successful upload
+	const rf = parseInt(xhr.getResponseHeader('Refresh'));
+	if (rf && ok)
+	    setTimeout(() => window.location.reload(), rf * 1000);
+    }, PassiveListener);
+
+    xhr.open(form.method, form.action);
     xhr.responseType = 'text';
-    xhr.send(file);
+    xhr.send(data);
+    return xhr;
 }
 
-// POST upload of a form as key=value pairs, *including* transmitting
-// unchecked checkboxes as name=0.
+// Upload a data file blob
+function uploadfile() {
+    event.preventDefault();
+    const form = event.target.form || event.target;
+    var files = form.elements['file'];
+    return (files.files.length == 1)
+	? upload(form,files.files[0]) : files.click();
+}
 
-// Enable a button (this ensures that the necessary scripts have been
-// run before one can submit)
-function enablebutton(id,on) {
-    getelem(id).disabled = !on;
+// key=value formatting of form data; including inverted checkboxes
+function textformdata(form) {
+    var data = '';
+    for (var e of form.elements) {
+	var val = e.value;
+	if (val == undefined || !e.name || e instanceof HTMLButtonElement) {
+	    continue;
+	} else if (e instanceof HTMLInputElement) {
+	    if (e.type == 'checkbox')
+		val = e.checked == cfgbool(val) ? '1' : '0';
+	    else if (e.type == 'radio' && !e.checked)
+		continue;
+	}
+	data += e.name + '=' + val + "\r\n";
+    }
+    return data;
+}
+
+// POST form contents upload with response text
+function uploadform() {
+    event.preventDefault();
+    const form = event.target.form || event.target;
+    return upload(form,textformdata(form));
 }
 
 // Flip the status of an INPUT element between text and password
-function showpwd(id,me) {
-    var pwd = getelem(id);
+function showpwd(me = event.target) {
     const now_visible = me.classList.toggle('hide');
-    me.classList.toggle('show',!now_visible);
     const new_type = now_visible ? 'text' : 'password';
-    pwd.setAttribute('type', new_type);
+    me.classList.toggle('show',!now_visible);
+    sib(me,'input').setAttribute('type', new_type);
+}
+
+// Insert translations as needed
+var translations = null;
+var delay_translate = false;
+function translate(top = document) {
+    delay_translate = (!translations || document.readyState != 'complete');
+    if (!delay_translate)
+	fillin(translations, true, top);
+}
+document.addEventListener('load', (e) => translate(), PassiveListener);
+
+var lang_styleobj = document.createElement('style');
+document.head.append(lang_styleobj);
+function setlang(lang)  {
+    if (lang) {
+	if (lang_styleobj.sheet.rules.length)
+	    lang_styleobj.sheet.deleteRule(0);
+	lang_styleobj.sheet.insertRule('[lang]:not([lang="'+lang+'"]) { display: none; }');
+    }
 }
+setlang('en');
+
+fetchconfig('/sys/lang')
+    .then((map) => {
+	setlang(map.get('LANG'));
+	translations = map;
+	if (delay_translate)
+	    translate();
+    })
+    .catch(() => {});
 
 // Hack to include an HTML files. Sadly, does not support
 // including files with <script> tags.
@@ -152,10 +236,9 @@ function inc(url) {
     var me = document.currentScript;
     fetch(url, {redirect: "follow"})
 	.then((response) => response.text())
-	.then((text) => { me.outerHTML = text; });
-}
-
-// Insert translations
-function translate() {
-    load('/sys/lang', true);
+	.then((text) => {
+	    const p = me.parentElement;
+	    me.outerHTML = text;
+	    translate(p);
+	});
 }

+ 0 - 5
esp32/www/showstatus.html

@@ -1,5 +0,0 @@
-<h1 class="status">Status</h1>
-<p>
-  <img src="wip.png" width="72" height="64" alt="WIP" />
-  <span class="notyet">(not implemented yet)</span>
-</p>

+ 88 - 3
esp32/www/status.html

@@ -7,8 +7,93 @@
   </head>
   <body>
     <script>inc("head.html")</script>
-    <script>inc("showstatus.html")</script>
-    <script>load('sys/getstatus')</script>
-    <script>translate()</script>
+    <h1 class="status">Status</h1>
+    <form class="getstatus" id="getstatus">
+      <fieldset class="status-max80">
+	<legend>MAX80</legend>
+	<label class="hwver">
+	  <span>Hardware version:</span>
+	  <input type="text" name="max80.hw.ver" />
+	</label>
+	<label class="fwdate">
+	  <span>Firmware build date:</span>
+	  <input type="text" name="max80.fw.date" />
+	</label>
+	<label class="fpgaok">
+	  <span>FPGA online:</span>
+	  <input type="checkbox" name="max80.fpga" />
+	</label>
+      </fieldset>
+      <fieldset class="wifi-sta">
+	<legend>Wifi Client</legend>
+	<label class="net-connected">
+	  <span>Connected:</span>
+	  <input type="checkbox" name="net.sta.conn" />
+	</label>
+	<label class="wifi-ssid">
+	  <span>Network name (SSID):</span>
+	  <input type="text" name="net.sta.ssid" />
+	</label>
+	<label class="net-mac">
+	  <span>MAC address:</span>
+	  <input type="text" name="net.sta.mac" />
+	</label>
+	<label class="net-ip4">
+	  <span>IP address:</span>
+	  <input type="text" name="net.sta.ip4" />
+	</label>
+	<label class="net-ip4-mask">
+	  <span>Netmask:</span>
+	  <input type="text" name="net.sta.ip4.mask" />
+	</label>
+	<label class="net-ip4-gw">
+	  <span>Gateway:</span>
+	  <input type="text" name="net.sta.ip4.gw" />
+	</label>
+      </fieldset>
+      <fieldset class="wifi-ap">
+	<legend>Wifi Access Point</legend>
+	<label class="net-connected">
+	  <span>Active:</span>
+	  <input type="checkbox" name="net.ap.conn" />
+	</label>
+	<label class="wifi-ssid">
+	  <span>Network name (SSID):</span>
+	  <input type="text" name="net.ap.ssid" />
+	</label>
+	<label class="net-mac">
+	  <span>MAC address:</span>
+	  <input type="text" name="net.ap.mac" />
+	</label>
+	<label class="net-ip4">
+	  <span>IP address:</span>
+	  <input type="text" name="net.ap.ip4" />
+	</label>
+	<label class="net-ip4-mask">
+	  <span>Netmask:</span>
+	  <input type="text" name="net.ap.ip4.mask" />
+	</label>
+	<label class="net-connected-clients">
+	  <span>Connected clients:</span>
+	  <input type="text" name="net.ap.clients" />
+	</label>
+      </fieldset>
+      <fieldset class="datetime">
+	<legend>Date and Time</legend>
+	<label class="localtime">
+	  <span>Current date and time:</span>
+	  <input type="text" name="localtime" />
+	</label>
+	<label class="sntp-server">
+	  <span>NTP server:</span>
+	  <input type="text" name="net.sntp.server" />
+	</label>
+	<label class="sntp-sync">
+	  <span>Time synchronized:</span>
+	  <input type="checkbox" name="net.sntp.sync" />
+	</label>
+      </fieldset>
+    </form>
+    <script>loadform('getstatus','sys/getstatus',true)</script>
   </body>
 </html>

+ 17 - 15
esp32/www/update.html

@@ -8,24 +8,26 @@
   <body>
     <script>inc("head.html")</script>
     <h1 class="update">Update</h1>
-    <form id="upload" action="sys/fwupdate" enctype="multipart/form-data"
-	  method="post" onsubmit="uploadfile(event)">
+    <form id="upload" class="idle" action="sys/fwupdate"
+	  method="post" onsubmit="uploadfile()">
       <fieldset class="firmware">
 	<legend>Firmware</legend>
-	<div>
-	  <label for="file">Select firmware file:</label>
-	  <input type="file" name="file" required
-		 onchange="enablebutton('upload.start',files.length==1)" />
-	</div>
-	<div>
-	  <button class="submit" type="submit" id="upload.start" disabled>Update firmware</button>
-	</div>
-	<div>
-	  <progress value="0"></progress>
-	</div>
-	<pre class="result hide"></pre>
+	<input type="file" name="file" hidden accept=".fw" onchange="uploadfile()" />
+	<button class="submit" type="submit" id="upload.start"
+		onclick="sib(this,'input').value=''">Update firmware</button>
+	<progress value="0"></progress>
       </fieldset>
+      <output></output>
+      <div class="onerr">
+	<h3>Possible troubleshooting steps:</h3>
+	<ol>
+	  <li class="retry">Try again</li>
+	  <li class="jtag">Make sure no JTAG cable is plugged in</li>
+	  <li class="power">Power cycle board (remove both USB and power off ABC host)</li>
+	  <li class="esp">Separately update ESP32 over USB before trying again</li>
+	  <li class="console">Check serial console for more detailed status messages</li>
+	</ol>
+      </div>
     </form>
-    <script>translate()</script>
   </body>
 </html>

+ 3 - 3
fpga/max80.qpf

@@ -19,15 +19,15 @@
 #
 # Quartus Prime
 # Version 21.1.0 Build 842 10/21/2021 SJ Lite Edition
-# Date created = 23:09:19  May 13, 2022
+# Date created = 02:29:52  May 14, 2022
 #
 # -------------------------------------------------------------------------- #
 
 QUARTUS_VERSION = "21.1"
-DATE = "23:09:19  May 13, 2022"
+DATE = "02:29:52  May 14, 2022"
 
 # Revisions
 
 PROJECT_REVISION = "v1"
-PROJECT_REVISION = "v2"
+PROJECT_REVISION = "v1"
 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/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/Makefile

@@ -35,7 +35,7 @@ gendeps   = -MD -MF $(@D)/.$(@F).d -MT $@
 genhdrs = iodevs.h irqtable.h
 gensrcs =
 
-all: sram.bin jtagupd.bin dram.bin.gz dram.hex checksum.h
+all: sram.bin jtagupd.bin dram.bin dram.hex checksum.h
 
 LIBS    = max80.a fatfs.a zlib.a
 

+ 1 - 1
rv32/checksum.h

@@ -1,4 +1,4 @@
 #ifndef CHECKSUM_H
 #define CHECKSUM_H
-#define SDRAM_SUM 0x1b820774
+#define SDRAM_SUM 0x910d2974
 #endif

+ 1 - 2
rv32/head.S

@@ -60,8 +60,7 @@ __start:
 	addqxi gp,gp,0		// Set gp for interrupt code too
 
 	// Clear esplink_head.magic as quickly as possible
-	la a0,__esplink_start
-	sw zero,(a0)
+	sw zero,esplink_head,a0
 	
 	// Clear bss
 	la a0,__BSS_START__

+ 0 - 4
rv32/jtagupd.ld

@@ -172,10 +172,6 @@ SECTIONS
 	__BSS_END__ = .;
 	__BSS_LEN__ = __BSS_END__ - __BSS_START__;
 
-	. = ALIGN(4);
-	__BSS_END__ = .;
-	__BSS_LEN__ = __BSS_END__ - __BSS_START__;
-
 	__global_pointer$ = 0;
 
 	PROVIDE(___text = .);

+ 1 - 5
rv32/max80.ld

@@ -170,10 +170,6 @@ SECTIONS
 	__BSS_END__ = .;
 	__BSS_LEN__ = __BSS_END__ - __BSS_START__;
 
-	. = ALIGN(4);
-	__BSS_END__ = .;
-	__BSS_LEN__ = __BSS_END__ - __BSS_START__;
-
 	__global_pointer$ = 0;
 
 	PROVIDE(___text = .);
@@ -252,7 +248,7 @@ SECTIONS
 	. = ALIGN(4096);	/* Align to a flash page */
 	__dram_init_start = .;
 
-	.dram.abcrom : AT(SRAM_SIZE) ALIGN(4) {
+	.dram.abcrom : AT(SRAM_SIZE) ALIGN(4096) {
 		__abcrom_start = .;
 		KEEP(*(SORT_NONE(.dram.abcrom*)))
 		__abcrom_end = .;

+ 6 - 0
version.h.sed

@@ -0,0 +1,6 @@
+/^COMMIT_MAGIC=/s/=/ 0x/
+t def
+s/=/ "/
+s/$/"/
+: def
+s/^/#define /

+ 6 - 0
version.vh.sed

@@ -0,0 +1,6 @@
+/^COMMIT_MAGIC=/s/=/ 32'h/
+t def
+s/=/ "/
+s/$/"/
+: def
+s/^/`define /