Bladeren bron

esp32: single configuration system; AP mode for provisioning

Single configuration system based on environment variables;
if no SSID is configured go to AP mode for provisioning.

Clean up the intro webpage a bit; inline the svg logo, otherwise it
seems browsers will not correctly pick up the font file.
H. Peter Anvin 2 jaren geleden
bovenliggende
commit
6c4c450c3a

+ 207 - 0
esp32/max80/config.c

@@ -0,0 +1,207 @@
+#include "common.h"
+#include "config.h"
+
+#include <esp_spiffs.h>
+#include <ctype.h>
+
+#define MAX_CONFIG_LINE 256
+#define CONFIG_FILE	"/spiffs/config.txt"
+
+struct configvar {
+    const char *name;
+    const char *val;
+};
+
+static const struct configvar default_config[] = {
+    { "TZ", "CET-1CEST,M3.5.0,M10.5.0/3" }, /* Sweden */
+    { "hostname", "max80" }
+};
+
+#ifndef HAVE_CLEARENV
+static void clearenv(void)
+{
+    char **new_environ = calloc(ARRAY_SIZE(default_config)+1, sizeof(char *));
+    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();
+    for (size_t i = 0; i < ARRAY_SIZE(default_config); i++)
+	setenv(default_config[i].name, default_config[i].val, 1);
+}
+
+static bool is_end_of_string(int c)
+{
+    return c <= 0 || c == '\n' || c == '\r';
+}
+
+static void skip_rest_of_line(FILE *f)
+{
+    int c;
+    do {
+	c = getc(f);
+    } while (!is_end_of_string(c));
+}
+
+int read_config(FILE *f)
+{
+    char *linebuf = NULL;
+    int err = -1;
+
+    linebuf = malloc(MAX_CONFIG_LINE);
+    if (!linebuf)
+	goto exit;
+
+    while (fgets(linebuf, MAX_CONFIG_LINE, f)) {
+	char *p, *q;
+	unsigned char c;
+
+	p = linebuf;
+
+	do {
+	    c = *p++;
+	} while (isalnum(c) || c == '.' || c == '-');
+	if (c != '=')
+	    continue;		/* Invalid config line (blank, comment...) */
+
+	p[-1] = '\0';
+
+	q = p;
+
+	do {
+	    c = *q++;
+	} while (!is_end_of_string(c));
+
+	if (q >= linebuf + MAX_CONFIG_LINE) {
+	    /* Overlong line, discard rest and drop */
+	    skip_rest_of_line(f);
+	} else {
+	    q[-1] = '\0';
+	    if (linebuf[0] == '-')
+		unsetenv(linebuf+1);
+	    else
+		setenv(linebuf, p, 1);
+	}
+    }
+
+    err = 0;
+
+exit:
+    if (linebuf)
+	free(linebuf);
+
+    tzset();
+    return err;
+};
+
+int write_config(FILE *f)
+{
+    for (char **var = environ; *var; var++) {
+	fputs(*var, f);
+	putc('\n', f);
+    }
+
+    return ferror(f) ? -1 : 0;
+}
+
+int save_config(void)
+{
+    FILE *f = fopen(CONFIG_FILE, "w");
+    if (!f)
+	return -1;
+
+    int err = write_config(f);
+    fclose(f);
+
+    return err;
+}
+
+static const esp_vfs_spiffs_conf_t spiffs_conf = {
+    .base_path              = "/spiffs",
+    .partition_label        = NULL,
+    .max_files              = 4,
+    .format_if_mount_failed = true
+};
+
+void init_config(void)
+{
+    reset_config();
+
+    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 */
+
+    read_config(f);
+    fclose(f);
+}
+
+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(var, vbuf, 1);
+}
+
+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(var, vbuf, 1);
+}
+
+bool getenv_bool(const char *var)
+{
+    var = getenv(var);
+
+    if (!var)
+	return false;
+
+    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;
+}
+
+void setenv_bool(const char *var, bool val)
+{
+    return setenv_ul(var, val);
+}

+ 21 - 0
esp32/max80/config.h

@@ -0,0 +1,21 @@
+#pragma once
+
+#include "common.h"
+
+#include <stdio.h>
+
+extern_c int read_config(FILE *);
+extern_c int write_config(FILE *);
+extern_c int save_config(void);
+extern_c void init_config(void);
+
+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);
+
+
+
+

+ 10 - 24
esp32/max80/httpd.c

@@ -77,25 +77,14 @@ static esp_err_t httpd_error(httpd_req_t *req,
     int hlen, blen;
     int rv = ESP_ERR_NO_MEM;
 
-    blen = asprintf(&body,
-		    "<!DOCTYPE html>\r\n"
-		    "<html>\r\n"
-		    "<head>\r\n"
-		    "<title>Error %u: %s</title>\r\n"
-		    "</head>\r\n"
-		    "<body>\r\n"
-		    "<h1>Error %u</h1>\r\n"
-		    "<p>%s</p>\r\n"
-		    "</body>\r\n"
-		    "</html>\r\n",
-		    errcode, msg, errcode, msg);
+    blen = asprintf(&body, "%u %s\r\n", errcode, msg);
 
     if (!body)
 	goto out;
 
     hlen = asprintf(&header,
 		    "HTTP/1.1 %u %s\r\n"
-		    "Content-Type: text/html; charset=\"UTF-8\"\r\n"
+		    "Content-Type: text/plain; charset=\"UTF-8\"\r\n"
 		    "Content-Length: %d\r\n"
 		    "Date: %s\r\n"
 		    "Cache-Control: no-cache\r\n"
@@ -140,19 +129,12 @@ esp_err_t httpd_firmware_upgrade_handler(httpd_req_t *req)
 	return httpd_error(req, 500, "Firmware update failed");
 
     len = asprintf(&response,
-		   "<!DOCTYPE html>\r\n"
-		   "<html>\r\n"
-		   "<head>\r\n"
-		   "<title>Firmware update completed</title>\r\n"
-		   "</head>\r\n"
-		   "<body>\r\n"
-		   "<h1>Firmware update completed</h1>\r\n"
-		   "<p>Rebooting in %u seconds</p>\r\n"
-		   "</body>\r\n"
-		   "</html>\r\n",
+		   "Firmware update completed\r\n"
+		   "Rebooting in %u seconds\r\n",
 		   reboot_delayed());
 
-    /* 200 and text/html are the response defaults, no need to set */
+    /* 200 is default, no need to set */
+    httpd_resp_set_type(req, "text/plain; charset=\"UTF-8\"");
     httpd_resp_set_hdr(req, "Date", http_now());
     httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
     httpd_resp_set_hdr(req, "Connection", "close");
@@ -182,6 +164,10 @@ static const struct mime_type mime_types[] = {
     { ".png",   4, 0,          "image/png"                 },
     { ".ico",   4, 0,          "image/png"                 }, /* favicon.ico */
     { ".svg",   4, MT_CHARSET, "image/svg+xml"             },
+    { ".otf",   4, 0,          "font/otf"                  },
+    { ".ttf",   4, 0,          "font/ttf"                  },
+    { ".woff",  5, 0,          "font/woff"                 },
+    { ".woff2", 6, 0,          "font/woff2"                },
     { ".pdf",   4, 0,          "application/pdf"           },
     { ".js",    3, MT_CHARSET, "text/javascript"           },
     { ".mjs",   4, MT_CHARSET, "text/javascript"           },

+ 17 - 2
esp32/max80/max80.ino

@@ -7,14 +7,29 @@
 #include <freertos/task_snapshot.h>
 #include "fpga.h"
 #include "wifi.h"
-#include "storage.h"
+#include "config.h"
+
+// On board v1, IO7 is N/C.
+// On board v2, IO7 is USB_PWR_EN and has a 36k pulldown.
+static int get_board_version()
+{
+    return 2;			// For now
+}
+
+static void dump_config()
+{
+    printf("--- Configuration:\n");
+    write_config(stdout);
+    printf("--- End configuration\n");
+}
 
 void setup() {
     printf("[START] MAX80 firmware compiled on " __DATE__ " " __TIME__ "\n");
-    InitStorage();
+    init_config();
     SetupWiFi();
     //fpga_services_start();
     printf("[RDY]\n");
+    dump_config();
 }
 
 static inline char task_state(eTaskState state)

+ 0 - 130
esp32/max80/storage.cpp

@@ -1,130 +0,0 @@
-#include "storage.h"
-#include <EEPROM.h>
-
-#define SSID_LENGTH 64
-#define PASSWORD_LENGTH 64
-#define HOSTNAME_LENGTH 32
-
-struct Config {
-  // WIFI
-  char SSID[SSID_LENGTH];
-  char WifiPassword[PASSWORD_LENGTH];
-  char Hostname[HOSTNAME_LENGTH];
-  char OTAPassword[PASSWORD_LENGTH];
-} currentConfig;
-
-const size_t ConfigLength = sizeof(currentConfig);
-
-void ReadConfig() {
-  char *c = (char *)(&currentConfig);
-  for (int i=0; i<ConfigLength;i++) {
-    c[i] = EEPROM.read(i);
-    if (c[i] == (char)255)
-      c[i] = 0;
-  }
-
-  // Pad all strings to be null terminated
-  currentConfig.SSID[SSID_LENGTH-1] = 0x00;
-  currentConfig.WifiPassword[PASSWORD_LENGTH-1] = 0x00;
-  currentConfig.OTAPassword[PASSWORD_LENGTH-1] = 0x00;
-  currentConfig.Hostname[HOSTNAME_LENGTH-1] = 0x00;
-}
-
-void SaveConfig() {
-  currentConfig.SSID[SSID_LENGTH-1] = 0x00;
-  currentConfig.WifiPassword[PASSWORD_LENGTH-1] = 0x00;
-  currentConfig.OTAPassword[PASSWORD_LENGTH-1] = 0x00;
-  currentConfig.Hostname[HOSTNAME_LENGTH-1] = 0x00;
-
-  char *c = (char *)(&currentConfig);
-  for (int i=0; i<ConfigLength;i++) {
-    EEPROM.write(i, c[i]);
-  }
-  EEPROM.commit();
-}
-
-String GetWifiSSID() {
-  return String(currentConfig.SSID);
-}
-String GetWifiPassword() {
-  return String(currentConfig.WifiPassword);
-}
-String GetOTAPassword() {
-  return String(currentConfig.OTAPassword);
-}
-String GetHostname() {
-  return String(currentConfig.Hostname);
-}
-
-void SaveWifiSSID(String ssid) {
-    int maxLen = SSID_LENGTH-1;
-    if (ssid.length() < maxLen) {
-      maxLen = ssid.length();
-    }
-    for (int i = 0; i < SSID_LENGTH; i++) {
-      if (i < maxLen) {
-        currentConfig.SSID[i] = ssid[i];
-      } else {
-        currentConfig.SSID[i] = 0x00;
-      }
-    }
-    SaveConfig();
-}
-
-void SaveWifiPassword(String pass) {
-    int maxLen = PASSWORD_LENGTH-1;
-    if (pass.length() < maxLen) {
-      maxLen = pass.length();
-    }
-    for (int i = 0; i < PASSWORD_LENGTH; i++) {
-      if (i < maxLen) {
-        currentConfig.WifiPassword[i] = pass[i];
-      } else {
-        currentConfig.WifiPassword[i] = 0x00;
-      }
-    }
-    SaveConfig();
-}
-
-
-void SaveOTAPassword(String pass) {
-    int maxLen = PASSWORD_LENGTH-1;
-    if (pass.length() < maxLen) {
-      maxLen = pass.length();
-    }
-    for (int i = 0; i < PASSWORD_LENGTH; i++) {
-      if (i < maxLen) {
-        currentConfig.OTAPassword[i] = pass[i];
-      } else {
-        currentConfig.OTAPassword[i] = 0x00;
-      }
-    }
-    SaveConfig();
-}
-
-void SaveHostname(String hostname) {
-    int maxLen = HOSTNAME_LENGTH-1;
-    if (hostname.length() < maxLen) {
-      maxLen = hostname.length();
-    }
-    for (int i = 0; i < HOSTNAME_LENGTH; i++) {
-      if (i < maxLen) {
-        currentConfig.Hostname[i] = hostname[i];
-      } else {
-        currentConfig.Hostname[i] = 0x00;
-      }
-    }
-    SaveConfig();
-}
-
-void InitStorage() {
-  EEPROM.begin(ConfigLength);
-#if 1
-  SaveWifiSSID("Hyperion-2");
-  SaveWifiPassword("eUrPp7xtbexWm4TEu7nDtGLRcGP9hvYo");
-  SaveHostname("max80");
-  SaveOTAPassword("max80");
-#endif
-
-  ReadConfig();
-}

+ 0 - 15
esp32/max80/storage.h

@@ -1,15 +0,0 @@
-#pragma once
-
-#include "Arduino.h"
-
-String GetWifiSSID();
-String GetWifiPassword();
-String GetHostname();
-String GetOTAPassword();
-
-void SaveWifiSSID(String ssid);
-void SaveWifiPassword(String pass);
-void SaveHostname(String hostname);
-void SaveOTAPassword(String password);
-
-void InitStorage();

+ 87 - 40
esp32/max80/wifi.cpp

@@ -1,27 +1,23 @@
 #include "common.h"
 #include "WiFi.h"
 #include "wifi.h"
-#include "storage.h"
+#include "config.h"
 #include "httpd.h"
 
 #include <lwip/dns.h>
 #include <lwip/inet.h>
 #include <lwip/apps/sntp.h>
 #include <esp_sntp.h>
+#include <esp_wifi.h>
 
-static String ssid        = "";
-static String password    = "";
-static String otaPassword = "";
-static String hostname    = "max80";
-static String DNSServer   = "";
-static String SNTPServer  = "";
-static String TimeZone    = "CET-1CEST,M3.5.0,M10.5.0/3"; // Sweden
+static wifi_mode_t mode;
+static String ssid, password, hostname, dnsserver, sntpserver;
 
 static void sntp_sync_cb(struct timeval *tv)
 {
     static uint8_t prev_sync_status = SNTP_SYNC_STATUS_RESET;
     uint8_t sync_status = sntp_get_sync_status();
-    
+
     switch (sync_status) {
     case SNTP_SYNC_STATUS_RESET:
 	sntp_set_sync_mode(SNTP_SYNC_MODE_IMMED); // Until first sync
@@ -72,25 +68,29 @@ static inline bool invalid_ip(const ip_addr_t *ip)
 
 static void start_services(void)
 {
-    /* Always run after connect */
+    /* Always run after (re)connect */
     const ip_addr_t *dns_ip = dns_getserver(0);
-    if (invalid_ip(dns_ip)) {
-	/* No DNS server from DHCP? */
+    if (invalid_ip(dns_ip) || mode == WIFI_AP ||
+	getenv_bool("ip4.dhcp.nodns")) {
+	/* Static DNS server configuration */
 	ip_addr_t addr;
-	if (inet_aton(DNSServer.c_str(), &addr)) {
+	if (dnsserver != "" && inet_aton(dnsserver.c_str(), &addr)) {
 	    dns_setserver(0, &addr);
 	}
     }
 
     dns_ip = dns_getserver(0);
     printf("[DNS]  DNS server: %s\n", inet_ntoa(*dns_ip));
- 
+
     // If Arduino supported both of these at the same that would be
     // awesome, but it requires ESP-IDF reconfiguration...
     const ip_addr_t *sntp_ip = sntp_getserver(0);
 
-    if (invalid_ip(sntp_ip) && !SNTPServer.isEmpty())
-	sntp_setservername(0, SNTPServer.c_str());
+    if (invalid_ip(sntp_ip) || mode == WIFI_AP ||
+	getenv_bool("ip4.dhcp.nosntp")) {
+	if (sntpserver != "")
+	    sntp_setservername(0, sntpserver.c_str());
+    }
 
     sntp_ip = sntp_getserver(0);
     printf("[SNTP] Time server: %s\n", inet_ntoa(*sntp_ip));
@@ -112,6 +112,7 @@ static void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
 {
     bool retry = false;
     bool connected = false;
+    static int ap_clients = 0;
 
     switch (event) {
     case ARDUINO_EVENT_WIFI_READY:
@@ -158,18 +159,25 @@ static void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
 	break;
     case ARDUINO_EVENT_WIFI_AP_START:
 	printf("[WIFI] Access point started\n");
+	ap_clients = 0;
+	connected = true;
 	break;
     case ARDUINO_EVENT_WIFI_AP_STOP:
 	printf("[WIFI] Access point stopped\n");
+	ap_clients = 0;
+	connected = false;
 	break;
     case ARDUINO_EVENT_WIFI_AP_STACONNECTED:
 	printf("[WIFI] Client connected\n");
+	ap_clients++;
 	break;
     case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED:
 	printf("[WIFI] Client disconnected\n");
+	ap_clients--;
 	break;
     case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED:
-	printf("[WIFI] Assigned IP address to client\n");
+	printf("[WIFI] Assigned IP address %s to client\n",
+	       inet_ntoa(info.wifi_ap_staipassigned.ip));
 	break;
     case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED:
 	printf("[WIFI] Received probe request\n");
@@ -218,38 +226,77 @@ static void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
     }
 }
 
-void SetupWiFi() {
-    services_started = false;
+static void wifi_config(void)
+{
+    ssid       = getenv("wifi.ssid");
+    password   = getenv("wifi.psk");
+    hostname   = getenv("hostname");
+    sntpserver = getenv_bool("sntp") ? getenv("sntp.server") : "";
+    dnsserver  = getenv("ip4.dns");
+
+    WiFi.persistent(false);
 
-    ssid = GetWifiSSID();
-    password = GetWifiPassword();
-    hostname = GetHostname();
+    if (hostname != "")
+	WiFi.hostname(hostname);
 
-    if (!TimeZone.isEmpty()) {
-	printf("[INFO] Setting TZ = %s\n", TimeZone.c_str());
-	setenv("TZ", TimeZone.c_str(), 1);
-	tzset();
+    if (ssid == "") {
+	uint8_t mac[6];
+	char ssid_buf[64];
+
+	mode     = WIFI_AP;
+	password = "";
+
+	WiFi.macAddress(mac);
+	/* Skip first 4 bytes of MAC */
+	snprintf(ssid_buf, sizeof ssid_buf, "MAX80_%02X%02X", mac[4], mac[5]);
+	ssid     = String(ssid_buf);
+    } else {
+	mode     = WIFI_STA;
     }
-    my_sntp_start();
-    
-    printf("[INFO] Setting up WiFi\n");
-    printf("[INFO] SSID: %s\n", ssid.c_str());
 
-    WiFi.onEvent(WiFiEvent);
+    printf("[WIFI] SSID [%s]: %s\n",
+	   mode == WIFI_AP ? "AP" : "STA", ssid.c_str());
 
-    if (WiFi.getMode() != WIFI_STA) {
-	WiFi.mode(WIFI_STA);
+    if (WiFi.getMode() != mode) {
+	WiFi.mode(mode);
 	delay(10);
     }
 
-    if (WiFi.SSID() != ssid || WiFi.psk() != password) {
-	printf("[INFO] WiFi config changed.\n");
-	// ... Try to connect to WiFi station.
-	WiFi.begin(ssid.c_str(), password.c_str());
+    if (mode == WIFI_AP) {
+	/* 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
+	unsigned int channel = time(NULL) % 11;
+
+	printf("[WIFI] AP IP %s netmask %s channel %u\n",
+	       AP_IP.toString(), AP_Netmask.toString(), channel+1);
+
+	WiFi.softAP(ssid.c_str(), password.c_str(), channel+1, 0, 4, true);
+
+	//
+	// Conservative settings: 20 MHz, maximum power; this is for
+	// reliability, not performance.
+	//
+	WiFi.setTxPower((wifi_power_t)0);
+	esp_wifi_set_bandwidth((wifi_interface_t)ESP_IF_WIFI_AP, WIFI_BW_HT20);
+
+	WiFi.softAPsetHostname(ssid.c_str());
+	WiFi.softAPConfig(AP_IP, AP_Gateway, AP_Netmask);
     } else {
-	WiFi.begin();
+	WiFi.begin(ssid.c_str(), password.c_str());
+	WiFi.setAutoConnect(true);
+	WiFi.setAutoReconnect(true);
     }
+}
 
-    WiFi.setAutoReconnect(true);
-    WiFi.setHostname(hostname.c_str());
+void SetupWiFi() {
+    services_started = false;
+
+    WiFi.onEvent(WiFiEvent);
+
+    my_sntp_start();
+
+    printf("[INFO] Setting up WiFi\n");
+    wifi_config();
 }

BIN
esp32/output/max80.ino.bin


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


+ 0 - 13
esp32/www/img/max80-trans.svg

@@ -1,13 +0,0 @@
-<?xml version="1.0" standalone="no" ?>
-<svg viewBox="0 0 315 100" xmlns="http://www.w3.org/2000/svg">
-  <style>@font-face { font-family: Prisma; src: url(Prisma-MAX.woff2); }</style>
-  <mask id="mask">
-    <g dominant-baseline="middle" text-rendering="geometricPrecision" paint-order="stroke fill" fill="white" stroke="black">
-      <text font-family="Prisma" x="164" y="50" font-size="100">X</text>
-      <text font-family="Prisma" x="100.4" y="50" font-size="100" stroke-width="3">A</text>
-      <text font-family="Prisma" x="5" y="50" font-size="100" stroke-width="3">M</text>
-      <text font-family="Helvetica" x="150" y="56" font-size="50" font-weight="bold" transform="scale(1.5 1)">80</text>
-    </g>
-  </mask>
-  <rect x="0" y="0" width="100%" height="100%" fill="black" mask="url(#mask)" />
-</svg>

+ 12 - 1
esp32/www/index.html

@@ -1,11 +1,22 @@
 <!DOCTYPE html>
 <html>
   <head>
+    <link rel="stylesheet" href="max80.css" />
     <title>MAX80: Hello, World!</title>
   </head>
   <body>
     <p>
-      <img src="img/max80-trans.svg" alt="MAX80" width="252" height="80" />
+      <svg width="315" height="100" viewBox="0 0 315 100">
+	<mask id="mask">
+	  <g dominant-baseline="middle" text-rendering="geometricPrecision" paint-order="stroke fill" fill="white" stroke="black">
+	    <text font-family="Prisma" x="164" y="50" font-size="100">X</text>
+	    <text font-family="Prisma" x="100.4" y="50" font-size="100" stroke-width="3">A</text>
+	    <text font-family="Prisma" x="5" y="50" font-size="100" stroke-width="3">M</text>
+	    <text font-family="Helvetica" x="150" y="56" font-size="50" font-weight="bold" transform="scale(1.5 1)">80</text>
+	  </g>
+	</mask>
+	<rect x="0" y="0" width="100%" height="100%" fill="black" mask="url(#mask)" />
+      </svg>
       <br />
       by Peter &amp; Per
     </p>

+ 5 - 0
esp32/www/max80.css

@@ -0,0 +1,5 @@
+@font-face {
+    font-family: "Prisma";
+    src: url(Prisma-MAX.woff2);
+}
+

BIN
fpga/output/v1.fw


BIN
fpga/output/v2.fw


BIN
img/Prisma-MAX.woff2


BIN
img/Prisma.otf


+ 1 - 1
img/max80-trans.svg

@@ -1,6 +1,6 @@
 <?xml version="1.0" standalone="no" ?>
 <svg viewBox="0 0 315 100" xmlns="http://www.w3.org/2000/svg">
-  <style>@font-face { font-family: Prisma; src: url(Prisma-MAX.woff2); }</style>
+  <style>@font-face {font-family:"Prisma";src:url(Prisma-MAX.woff2);}</style>
   <mask id="mask">
     <g dominant-baseline="middle" text-rendering="geometricPrecision" paint-order="stroke fill" fill="white" stroke="black">
       <text font-family="Prisma" x="164" y="50" font-size="100">X</text>