Browse Source

httpd: support multiple languages; don't enable submit until ready

Support multiple configuration languages. Adjust the Javascript to not
allow sending in a configuration or update until everything has loaded.
H. Peter Anvin 2 years ago
parent
commit
e54a4651f2

+ 5 - 5
esp32/max80/config.c

@@ -24,13 +24,13 @@ static void clearenv(void)
     char **old_environ = environ;
 
     environ = new_environ;
-    
+
     for (char **envp = old_environ; *envp; envp++)
 	free(*envp);
     free(old_environ);
 }
 #endif /* HAVE_CLEARENV */
-    
+
 static void reset_config(void)
 {
     clearenv();
@@ -137,7 +137,7 @@ void init_config(void)
 
     if (!esp_spiffs_mounted(spiffs_conf.partition_label))
 	esp_vfs_spiffs_register(&spiffs_conf);
-    
+
     FILE *f = fopen(CONFIG_FILE, "r");
     if (!f)
 	return;			/* No config file */
@@ -149,7 +149,7 @@ void init_config(void)
 long getenv_l(const char *var, long def)
 {
     const char *ep;
-    
+
     var = getenv(var);
     if (!var || !*var)
 	return def;
@@ -168,7 +168,7 @@ void setenv_l(const char *var, long val)
 unsigned long getenv_ul(const char *var, unsigned long def)
 {
     const char *ep;
-    
+
     var = getenv(var);
     if (!var || !*var)
 	return def;

+ 2 - 2
esp32/max80/fpga.c

@@ -117,7 +117,7 @@ int fpga_program_spz(spz_stream *spz)
 
 	if (!idcode_loops) {
 	    MSG("check for JTAG cable connected, or power cycle board\n");
-	    err = FWUPDATE_ERR_DEVICE_MISMATCH;
+	    err = FWUPDATE_ERR_FPGA_MISMATCH;
 	    goto fail;
 	}
 
@@ -167,7 +167,7 @@ int fpga_program_spz(spz_stream *spz)
 		check_status_loops ? "waiting" : "giving up");
 
 	    if (!check_status_loops) {
-		err = FWUPDATE_ERR_PROGRAM_FAILED;
+		err = FWUPDATE_ERR_FPGA_FAILED;
 		goto fail;
 	    }
 

+ 19 - 8
esp32/max80/fw.h

@@ -1,6 +1,7 @@
 #pragma once
 
 #include "common.h"
+#include <zlib.h>
 
 /*
  * Firmware chunk header.
@@ -30,13 +31,23 @@ enum fw_data_flags {
 /*
  * Additional error codes
  */
-#define FWUPDATE_ERR_ERASE_FAILED	(-7)
-#define FWUPDATE_ERR_PROGRAM_FAILED	(-8)
-#define FWUPDATE_ERR_WRITE_PROTECT	(-9)
-#define FWUPDATE_ERR_NOT_READY		(-10)
-#define FWUPDATE_ERR_DETECT		(-11)
-#define FWUPDATE_ERR_DEVICE_MISMATCH	(-12)
-#define FWUPDATE_ERR_IN_PROGRESS	(-13)
+#define FWUPDATE_ERR_IN_PROGRESS	(-10)
+#define FWUPDATE_ERR_BAD_CHUNK		(-11)
+#define FWUPDATE_ERR_ERASE_FAILED	(-12)
+#define FWUPDATE_ERR_PROGRAM_FAILED	(-13)
+#define FWUPDATE_ERR_WRITE_PROTECT	(-14)
+#define FWUPDATE_ERR_NOT_READY		(-15)
+#define FWUPDATE_ERR_FPGA_JTAG          (-16)
+#define FWUPDATE_ERR_FPGA_MISMATCH	(-17)
+#define FWUPDATE_ERR_FPGA_FAILED	(-18)
+#define FWUPDATE_ERR_UNKNOWN            (-19)
+#define FWUPDATE_ERR_ESP_NO_PARTITION	(-20)
+#define FWUPDATE_ERR_ESP_BAD_OTA	(-21)
+#define FWUPDATE_ERR_ESP_FLASH_FAILED	(-22)
+#define FWUPDATE_ERR_ESP_BAD_DATA	(-23)
+#define FWUPDATE_ERR_CONFIG_READ	(-24)
+#define FWUPDATE_ERR_CONFIG_SAVE	(-25)
 
 extern_c int firmware_update(read_func_t read_func, token_t token);
-extern_c esp_err_t esp_update(read_func_t read_func, token_t token, size_t size);
+extern_c int esp_update(read_func_t read_func, token_t token, size_t size);
+extern_c const char *firmware_errstr(int err);

+ 44 - 0
esp32/max80/fwupdate.c

@@ -279,6 +279,50 @@ static int fwupdate_process_chunk(spz_stream *spz)
     }
 }
 
+const char *firmware_errstr(int err)
+{
+    static char unknown_err[32];
+    static const char * const errstr[] = {
+	[-Z_STREAM_ERROR]	         = "Decompression error",
+	[-Z_DATA_ERROR]		         = "Invalid data stream",
+	[-Z_MEM_ERROR]		         = "Out of memory",
+	[-Z_BUF_ERROR]		         = "Decompression error",
+	[-FWUPDATE_ERR_IN_PROGRESS]      =
+	    "Firmware update already in progress",
+	[-FWUPDATE_ERR_BAD_CHUNK]        = "Invalid firmware chunk header",
+	[-FWUPDATE_ERR_ERASE_FAILED]     = "FPGA flash erase failed",
+	[-FWUPDATE_ERR_PROGRAM_FAILED]   = "FGPA flash program failed",
+	[-FWUPDATE_ERR_WRITE_PROTECT]    = "FPGA flash write protected",
+	[-FWUPDATE_ERR_NOT_READY]        = "FPGA flash stuck at not ready",
+	[-FWUPDATE_ERR_FPGA_JTAG]        =
+	    "FPGA JTAG bus stuck, check for JTAG adapter or power cycle board",
+	[-FWUPDATE_ERR_FPGA_MISMATCH]    =
+	    "Bad FPGA IDCODE, check for JTAG adapter or power cycle board",
+	[-FWUPDATE_ERR_FPGA_FAILED]      = "FPGA reboot failed",
+	[-FWUPDATE_ERR_UNKNOWN]          = "Unidentified error",
+	[-FWUPDATE_ERR_ESP_NO_PARTITION] = "No available ESP partition",
+	[-FWUPDATE_ERR_ESP_BAD_OTA]      = "ESP OTA information corrupt",
+	[-FWUPDATE_ERR_ESP_FLASH_FAILED] = "ESP flash program failed",
+	[-FWUPDATE_ERR_ESP_BAD_DATA]     = "ESP firmware image corrupt",
+	[-FWUPDATE_ERR_CONFIG_READ]      = "Configuration upload failure",
+	[-FWUPDATE_ERR_CONFIG_SAVE]      = "Error saving configuration"
+    };
+
+    switch (err) {
+    case Z_OK:
+	return errstr[-FWUPDATE_ERR_UNKNOWN];
+    case Z_ERRNO:
+	return strerror(errno);
+    case -ARRAY_SIZE(errstr)+1 ... Z_STREAM_ERROR:
+	if (errstr[-err])
+	    return errstr[-err];
+	/* fall through */
+    default:
+	snprintf(unknown_err, sizeof unknown_err, "error %d", -err);
+	return unknown_err;
+    }
+}
+
 int firmware_update(read_func_t read_data, token_t token)
 {
     struct spz_stream *spz = calloc(1, sizeof *spz);

+ 148 - 50
esp32/max80/httpd.c

@@ -134,23 +134,56 @@ static const char *http_dos_date(uint32_t dos_date)
     return http_date(set_weekday(&tm));
 }
 
+static const char text_plain[] = "text/plain; charset=\"UTF-8\"";
+
+enum hsp_flags {
+    HSP_CLOSE = 1,
+    HSP_CRLF  = 2
+};
+
 static esp_err_t httpd_send_plain(httpd_req_t *req,
 				  unsigned int rcode,
 				  const char *body, size_t blen,
-				  bool close_conn)
+				  enum hsp_flags flags)
 {
     char *header = NULL;
     esp_err_t err;
+    int hlen;
+    const char *now = http_now();
+
+    if (rcode > 499)
+	flags |= HSP_CLOSE;
+
+    const char *closer = flags & HSP_CLOSE ? "Connection: close\r\n" : "";
+    bool redirect = rcode >= 300 && rcode <= 399;
+
+    if (redirect) {
+	size_t blenadj = sizeof("3xx Redirect \r"); /* \0 -> \n so don't include it */
+	flags |= HSP_CRLF;
+
+	hlen = asprintf(&header,
+			"HTTP/1.1 %u\r\n"
+			"Content-Type: %s\r\n"
+			"Content-Length: %zu\r\n"
+			"Date %s\r\n"
+			"Location: %.*s\r\n"
+			"%s"
+			"\r\n"
+			"%3u Redirect ",
+			rcode, text_plain, blen + blenadj,
+			now, (int)blen, body, closer, rcode);
+    } else {
+	size_t blenadj = (flags & HSP_CRLF) ? 2 : 0;
 
-    int hlen = asprintf(&header,
+	hlen = asprintf(&header,
 			"HTTP/1.1 %u\r\n"
-			"Content-Type: text/plain; charset=\"UTF-8\"\r\n"
+			"Content-Type: %s\r\n"
 			"Content-Length: %zu\r\n"
 			"Date: %s\r\n"
 			"%s"
 			"\r\n",
-			rcode, blen, http_now(),
-			close_conn ? "Connection: close\r\n" : "");
+			rcode, text_plain, blen + blenadj, now, closer);
+    }
 
     if (!header)
 	return ESP_ERR_NO_MEM;
@@ -159,6 +192,9 @@ static esp_err_t httpd_send_plain(httpd_req_t *req,
     if (!err && blen) {
 	err = httpd_send_all(req, body, blen);
     }
+    if (!err && (flags & HSP_CRLF)) {
+	err = httpd_send_all(req, "\r\n", 2);
+    }
 
     if (header)
 	free(header);
@@ -166,8 +202,7 @@ static esp_err_t httpd_send_plain(httpd_req_t *req,
     return err;
 }
 
-#define HTTP_ERR(r,e,s) httpd_send_plain((r), (e), s "\r\n", \
-					 sizeof(s) + 1, (e) > 399)
+#define HTTP_ERR(r,e,s) httpd_send_plain((r), (e), s, sizeof(s)-1, HSP_CRLF)
 
 static esp_err_t httpd_err_404(httpd_req_t *req)
 {
@@ -179,22 +214,33 @@ static esp_err_t httpd_send_ok(httpd_req_t *req)
     return HTTP_ERR(req, 200, "OK");
 }
 
-static esp_err_t httpd_update_done(httpd_req_t *req, const char *what)
+static esp_err_t httpd_update_done(httpd_req_t *req, const char *what, int err)
 {
     char *response = NULL;
-    int len = asprintf(&response,
-		       "%s complete\r\n"
+    int len;
+    unsigned int reboot_time = reboot_delayed();
+
+    if (err) {
+	len = asprintf(&response,
+		       "%s update failed: %s\r\n"
 		       "Rebooting in %u seconds\r\n",
-		       what, reboot_delayed());
+		       what, firmware_errstr(err), reboot_time);
+    } else {
+	len = asprintf(&response,
+		       "%s update complete\r\n"
+		       "Rebooting in %u seconds\r\n",
+		       what, reboot_time);
+    }
 
     if (!response)
 	len = 0;
 
-    esp_err_t err = httpd_send_plain(req, 200, response, len, true);
+    esp_err_t rv = httpd_send_plain(req, err ? 400 : 200, response, len,
+				    HSP_CLOSE|HSP_CRLF);
     if (response)
 	free(response);
 
-    return err;
+    return rv;
 }
 
 static esp_err_t httpd_firmware_update(httpd_req_t *req)
@@ -203,13 +249,7 @@ static esp_err_t httpd_firmware_update(httpd_req_t *req)
 
     /* XXX: use httpd_fopen_read() here */
     rv = firmware_update((read_func_t)httpd_req_recv, (token_t)req);
-
-    if (rv == FWUPDATE_ERR_IN_PROGRESS)
-	return HTTP_ERR(req, 409, "Firmware update already in progress");
-    else if (rv)
-	return HTTP_ERR(req, 500, "Firmware update failed");
-
-    return httpd_update_done(req, "Firmware update");
+    return httpd_update_done(req, "Firmware", rv);
 }
 
 static esp_err_t httpd_set_config(httpd_req_t *req)
@@ -217,13 +257,18 @@ static esp_err_t httpd_set_config(httpd_req_t *req)
     FILE *f = httpd_fopen_read(req);
     if (!f)
 	return HTTP_ERR(req, 500, "Unable to get request handle");
-    
+
     int rv = read_config(f);
     fclose(f);
-    if (rv || save_config())
-	return HTTP_ERR(req, 503, "Unable to update configuration");
+    if (rv) {
+	rv = FWUPDATE_ERR_CONFIG_READ;
+    } else {
+	rv = save_config();
+	if (rv)
+	    rv = FWUPDATE_ERR_CONFIG_SAVE;
+    }
 
-    return httpd_update_done(req, "Configuration update");
+    return httpd_update_done(req, "Configuration", rv);
 }
 
 static esp_err_t httpd_sys_post_handler(httpd_req_t *req)
@@ -249,7 +294,7 @@ static esp_err_t httpd_get_config(httpd_req_t *req)
     if (!f)
 	return HTTP_ERR(req, 500, "Unable to get request handle");
 
-    httpd_resp_set_type(req, "text/plain; charset=\"UTF-8\"");
+    httpd_resp_set_type(req, text_plain);
 
     int rv = write_config(f);
     fclose(f);
@@ -301,7 +346,8 @@ static const struct mime_type mime_types[] = {
 static esp_err_t httpd_static_handler(httpd_req_t *req)
 {
     static const char index_filename[] = "index.html";
-    const size_t buffer_size = UNZ_BUFSIZE;
+    static const char default_lang[] = "en";
+    size_t buffer_size = UNZ_BUFSIZE;
     const char *uri, *enduri;
     bool add_index;
     char *buffer = NULL;
@@ -315,17 +361,41 @@ static esp_err_t httpd_static_handler(httpd_req_t *req)
     while (*uri == '/')
 	uri++;			/* Skip leading slashes */
 
-    enduri = strchr(uri, '\0');
+    const char *first_slash = NULL, *last_slash = NULL;
+
+    for (enduri = uri; *enduri; enduri++) {
+	if (*enduri == '/') {
+	    last_slash = enduri;
+	    if (!first_slash)
+		first_slash = enduri;
+	}
+    }
     if (enduri == uri) {
 	add_index = true;
-    } else if (enduri[-1] == '/') {
+    } else if (last_slash == enduri-1) {
 	add_index = true;
 	enduri--;		/* Drop terminal slash */
+	if (first_slash == last_slash)
+	    first_slash = NULL;
     } else {
 	add_index = false;	/* Try the plain filename first */
     }
 
     MSG("requesting: /%.*s\n", enduri - uri, uri);
+    if (first_slash)
+	MSG("first_slash = %.*s\n", enduri - first_slash, first_slash);
+    else
+	MSG("first_slash = NULL\n");
+
+    const char *lang = getenv("LANG");
+    const size_t lang_size = lang ? strlen(lang)+1 : 0;
+    const size_t lang_space = (lang_size < sizeof default_lang
+			       ? sizeof default_lang : lang_size) + 1;
+    const size_t filename_buffer_size =
+	(enduri - uri) + lang_space + 2 + sizeof index_filename;
+
+    if (buffer_size < filename_buffer_size)
+	buffer_size = filename_buffer_size;
 
     buffer = malloc(buffer_size);
     zip = malloc(sizeof *zip);
@@ -334,13 +404,9 @@ static esp_err_t httpd_static_handler(httpd_req_t *req)
 	goto out;
     }
 
-    if (enduri - uri + 1 + sizeof index_filename >= buffer_size) {
-	err = HTTP_ERR(req, 414, "URI too long");
-	goto out;
-    }
-
-    char *p = mempcpy(buffer, uri, enduri - uri);
-    *p = '\0';
+    char * const filebase = buffer + lang_space;
+    char * const endbase  = mempcpy(filebase, uri, enduri - uri);
+    filebase[-1] = '/';
 
     unz = unzOpen(NULL, (void *)gwwwzipData, gwwwzipSize,
 		  zip, NULL, NULL, NULL, NULL);
@@ -350,35 +416,67 @@ static esp_err_t httpd_static_handler(httpd_req_t *req)
 	goto out;
     }
 
-    while (1) {
-	if (add_index) {
-	    if (p > buffer)
-		*p++ = '/';
-	    memcpy(p, index_filename, sizeof index_filename);
-	    p += sizeof index_filename - 1; /* Point to final NUL */
+    char *filename, *endfile;
+
+    unsigned int m;
+    bool found = false;
+    for (m = add_index; m < 6; m += (add_index+1)) {
+	if (m & 1) {
+	    char *sx = endbase - (endbase[-1] == '/');
+	    *sx++ = '/';
+	    endfile = mempcpy(sx, index_filename, sizeof index_filename) - 1;
+	} else {
+	    endfile = endbase;
 	}
-
-	MSG("trying to open: %s\n", buffer);
-	if (unzLocateFile(unz, buffer, 1) == UNZ_OK)
+	*endfile = '\0';
+
+	switch (m >> 1) {
+	case 1:
+	    if (!lang) {
+		filename = NULL;
+	    } else {
+		filename = filebase - lang_size;
+		memcpy(filename, lang, lang_size-1);
+	    }
 	    break;
+	case 2:
+	    filename = filebase - sizeof default_lang;
+	    memcpy(filename, default_lang, sizeof default_lang - 1);
+	    break;
+	default:
+	    filename = filebase;
+	    break;
+	}
 
-	if (add_index) {
-	    err = httpd_err_404(req);
-	    goto out;
+	if (!filename)
+	    continue;
+
+	filename[-1] = '/';
+	MSG("trying to open: %s\n", filename);
+	if (unzLocateFile(unz, filename, 1) == UNZ_OK) {
+	    found = true;
+	    break;
 	}
+    }
+
+    size_t filelen = endfile - filename;
 
-	add_index = true;	/* Try again with the index filename */
+    if (!found) {
+	err = httpd_err_404(req);
+	goto out;
+    } else if (m) {
+	err = httpd_send_plain(req, 302 - (m == 1), filename-1, filelen+1, 0);
+	goto out;
     }
 
     /* Note: p points to the end of the filename string */
-    size_t filelen = p - buffer;
 
     const struct mime_type *mime_type = mime_types;
     /* The default entry with length 0 will always match */
     while (mime_type->ext_len) {
 	len = mime_type->ext_len;
 
-	if (len < filelen && !memcmp(p - len, mime_type->ext, len))
+	if (len < filelen && !memcmp(endfile - len, mime_type->ext, len))
 	    break;
 
 	mime_type++;

+ 37 - 7
esp32/max80/ota.c

@@ -8,10 +8,34 @@
 
 #define BUFFER_SIZE	4096
 
-esp_err_t esp_update(read_func_t read_func, token_t token, size_t size)
+static int esp_err_to_fwupdate(int err)
+{
+    switch (err) {
+    case ESP_OK:
+	return Z_OK;
+    case ESP_ERR_NO_MEM:
+	return Z_MEM_ERROR;
+    case ESP_ERR_NOT_FOUND:
+	return FWUPDATE_ERR_ESP_NO_PARTITION;
+    case ESP_ERR_INVALID_ARG:
+    case ESP_ERR_OTA_PARTITION_CONFLICT:
+    case ESP_ERR_OTA_SELECT_INFO_INVALID:
+    case ESP_ERR_OTA_ROLLBACK_INVALID_STATE:
+	return FWUPDATE_ERR_ESP_BAD_OTA;
+    case ESP_ERR_FLASH_OP_TIMEOUT:
+    case ESP_ERR_FLASH_OP_FAIL:
+	return FWUPDATE_ERR_ESP_FLASH_FAILED;
+    case ESP_ERR_OTA_VALIDATE_FAILED:
+	return FWUPDATE_ERR_ESP_BAD_DATA;
+    default:
+	return FWUPDATE_ERR_UNKNOWN;
+    }
+}
+
+int esp_update(read_func_t read_func, token_t token, size_t size)
 {
     const esp_partition_t *partition;
-    esp_err_t err = ESP_ERR_INVALID_ARG;
+    int err = ESP_FAIL;
     esp_ota_handle_t handle;
     bool ota_started = false;
     int len;
@@ -49,8 +73,9 @@ esp_err_t esp_update(read_func_t read_func, token_t token, size_t size)
 
 	left -= len;
 
-	MSG("writing %d bytes, %zu left\n", len, left);
+	MSG("writing %d bytes, %zu left\r", len, left);
 	err = esp_ota_write(handle, buf, len);
+	CMSG("\n");
 	if (err)
 	    goto fail;
     }
@@ -67,16 +92,21 @@ esp_err_t esp_update(read_func_t read_func, token_t token, size_t size)
 	goto fail;
 
     free(buf);
+    buf = NULL;
 
     MSG("setting boot partition\n");
-    return esp_ota_set_boot_partition(partition);
+    err = esp_ota_set_boot_partition(partition);
 
 fail:
-    MSG("failure: error 0x%x\n", err);
+    err = esp_err_to_fwupdate(err);
 
-    if (ota_started)
-	esp_ota_abort(handle);
+    if (err) {
+	MSG("failure: error %d\n", err);
+	if (ota_started)
+	    esp_ota_abort(handle);
+    }
     if (buf)
 	free(buf);
+
     return err;
 }

BIN
esp32/output/max80.ino.bin


+ 0 - 0
esp32/www/Prisma-MAX.woff2 → esp32/www/com/Prisma-MAX.woff2


+ 0 - 0
esp32/www/max80.css → esp32/www/com/max80.css


+ 32 - 10
esp32/www/max80.js → esp32/www/com/max80.js

@@ -1,3 +1,8 @@
+// Get an element by id or an Element object
+function getelem(id) {
+    return (id instanceof Element) ? id : document.getElementById(id);
+}
+
 // Read a key=value text file and return it as a Promise of a Map
 function fetchconfig(url) {
     var xdr = new XMLHttpRequest();
@@ -23,25 +28,36 @@ function fetchconfig(url) {
 }
 
 // Initialize a form from a map
-function initform(root, map) {
-    var form = root.elements;
-    
-    for (const [key,val] of map) {
-	var field = form[key];
-	if (field != undefined && field.tagName == "INPUT") {
-	    if (field.type == "checkbox") {
-		field.checked = val && val != "0" && val != "off";
+function initform(form,map) {
+    var button = null;
+
+    for (const field of getelem(form).elements) {
+	if (field instanceof HTMLInputElement) {
+	    var val = map.get(field.name);
+	    if (!val)
+		val = '';
+	    if (field.type == 'checkbox') {
+		field.checked = !val.match(/^(0*|[fn].*|of.*)$/i);
+	    } else if (field.type == 'radio') {
+		field.checked = (val == field.name);
 	    } else {
 		field.value = val;
 	    }
+	} else if (field instanceof HTMLButtonElement &&
+		   field.type == 'submit') {
+	    button = field;
 	}
     }
+
+    if (button) {
+	button.disabled = false; // All loaded, enable the submit button
+    }
 }
 
 // Load form initialization data
 function loadform(form, url) {
     fetchconfig(url)
-	.then(map => { initform(document.querySelector(form), map) })
+	.then(map => { initform(form, map) })
 	.catch(() => {});
 }
 
@@ -108,9 +124,15 @@ function uploadfile(event) {
     xhr.send(file);
 }
 
+// Enable a button (this ensures that the necessary scripts have been
+// run before one can submit)
+function enablebutton(id,on) {
+    getelem(id).disabled = !on;
+}
+
 // Flip the status of an INPUT element between text and password
 function showpwd(id,me) {
-    var pwd = document.getElementById(id);
+    var pwd = getelem(id);
     const was_visible = pwd.getAttribute('type') == 'text';
     const new_type = was_visible ? 'password' : 'text';
     const new_icon = was_visible ? 'show' : 'hide';

+ 5 - 5
esp32/www/config.html → esp32/www/en/config.html

@@ -1,9 +1,9 @@
 <!DOCTYPE html>
 <html>
   <head>
-    <link rel="stylesheet" href="max80.css" />
+    <link rel="stylesheet" href="../com/max80.css" />
     <title>MAX80: Configuration</title>
-  <script src="max80.js"></script>
+  <script src="../com/max80.js"></script>
   <script>
     function addextrainfo(event) {
 	var form = event.currentTarget;
@@ -20,7 +20,7 @@
   <body>
     <script>inc("head.html")</script>
     <h1>Configuration</h1>
-    <form id="config" action="sys/setconfig" enctype="text/plain"
+    <form id="config" action="../sys/setconfig" enctype="text/plain"
 	  method="post" onsubmit="addextrainfo(event)">
       <fieldset>
 	<legend>Network</legend>
@@ -43,8 +43,8 @@
 	<label for="ip4.dhcp.nosntp">Ignore DHCP-provided NTP server:</label>
 	<input type="checkbox" id="ip4.dhcp.nosntp" name="ip4.dhcp.nosntp" />
       </fieldset>
-      <button type="submit">Update configuration</button>
+      <button type="submit" disabled>Update configuration</button>
     </form>
-    <script>loadform('form#config','sys/getconfig')</script>
+    <script>loadform('config','../sys/getconfig')</script>
   </body>
 </html>

+ 0 - 0
esp32/www/head.html → esp32/www/en/head.html


+ 2 - 2
esp32/www/index.html → esp32/www/en/index.html

@@ -1,9 +1,9 @@
 <!DOCTYPE html>
 <html>
   <head>
-    <link rel="stylesheet" href="max80.css" />
+    <link rel="stylesheet" href="../com/max80.css" />
     <title>MAX80</title>
-    <script src="max80.js"></script>
+    <script src="../com/max80.js"></script>
   </head>
   <body>
     <script>inc("head.html")</script>

+ 3 - 0
esp32/www/en/showstatus.html

@@ -0,0 +1,3 @@
+<h1>Status</h1>
+<p>(not implemented yet)</p>
+<script>loadstatus('../sys/getstatus')</script>

+ 2 - 2
esp32/www/status.html → esp32/www/en/status.html

@@ -1,9 +1,9 @@
 <!DOCTYPE html>
 <html>
   <head>
-    <link rel="stylesheet" href="max80.css" />
+    <link rel="stylesheet" href="../com/max80.css" />
     <title>MAX80: Status</title>
-    <script src="max80.js"></script>
+    <script src="../com/max80.js"></script>
   </head>
   <body>
     <script>inc("head.html")</script>

+ 26 - 0
esp32/www/en/update.html

@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <link rel="stylesheet" href="../com/max80.css" />
+    <title>MAX80: Update</title>
+    <script src="../com/max80.js"></script>
+  </head>
+  <body>
+    <script>inc("head.html")</script>
+    <h1>Update</h1>
+    <form id="upload" action="../sys/fwupdate" enctype="multipart/form-data"
+	  method="post" onsubmit="uploadfile(event)">
+      <fieldset>
+	<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>
+	<button id="upload.start" disabled>Update firmware</button><br />
+	<progress value="0"></progress>
+	<pre class="result hide"></pre>
+      </fieldset>
+    </form>
+  </body>
+</html>

+ 0 - 11
esp32/www/other.html

@@ -1,11 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <title>MAX80: Hello, Other!</title>
-  </head>
-  <body>
-    <h1>Hello, Other!</h1>
-    <p>This is a file which is not called index.html.</p>
-  </body>
-</html>
-

+ 0 - 3
esp32/www/showstatus.html

@@ -1,3 +0,0 @@
-<h1>Status</h1>
-<p>(not implemented yet)</p>
-<script>loadstatus('sys/getstatus')</script>

+ 0 - 25
esp32/www/update.html

@@ -1,25 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <link rel="stylesheet" href="max80.css" />
-    <title>MAX80: Update</title>
-    <script src="max80.js"></script>
-  </head>
-  <body>
-    <script>inc("head.html")</script>
-    <h1>Update</h1>
-    <form id="upload" action="sys/fwupdate" enctype="multipart/form-data"
-	  method="post" onsubmit="event.preventDefault(); uploadfile(event)">
-      <fieldset>
-	<legend>Firmware</legend>
-	<div>
-	  <label for="file">Select firmware file:</label>
-	  <input type="file" name="file" />
-	</div>
-	<button>Update firmware</button><br />
-	<progress value="0"></progress>
-	<pre class="result hide"></pre>
-      </fieldset>
-    </form>
-  </body>
-</html>

BIN
fpga/output/v1.fw


BIN
fpga/output/v2.fw