diff options
author | Evgeny Zinoviev <me@ch1p.io> | 2023-05-11 04:18:08 +0300 |
---|---|---|
committer | Evgeny Zinoviev <me@ch1p.io> | 2023-05-11 04:18:12 +0300 |
commit | 0aba139aeff8ff80757c5d36502413299a0b449e (patch) | |
tree | 2b8e760ff14d4691783eb7c7d341f093199aab82 /platformio/temphum/src | |
parent | 586d84b0c0a8b4dc1b5057733892b754397234ec (diff) |
mqtt, esp: add new esp8266-based device
Diffstat (limited to 'platformio/temphum/src')
-rw-r--r-- | platformio/temphum/src/config.cpp | 84 | ||||
-rw-r--r-- | platformio/temphum/src/config.def.h.example | 33 | ||||
-rw-r--r-- | platformio/temphum/src/config.h | 34 | ||||
-rw-r--r-- | platformio/temphum/src/http_server.cpp | 279 | ||||
-rw-r--r-- | platformio/temphum/src/http_server.h | 56 | ||||
-rw-r--r-- | platformio/temphum/src/led.cpp | 9 | ||||
-rw-r--r-- | platformio/temphum/src/led.h | 39 | ||||
-rw-r--r-- | platformio/temphum/src/logging.h | 18 | ||||
-rw-r--r-- | platformio/temphum/src/main.cpp | 183 | ||||
-rw-r--r-- | platformio/temphum/src/mqtt.cpp | 325 | ||||
-rw-r--r-- | platformio/temphum/src/mqtt.h | 107 | ||||
-rw-r--r-- | platformio/temphum/src/static.cpp | 450 | ||||
-rw-r--r-- | platformio/temphum/src/static.h | 22 | ||||
-rw-r--r-- | platformio/temphum/src/stopwatch.h | 30 | ||||
-rw-r--r-- | platformio/temphum/src/temphum.cpp | 53 | ||||
-rw-r--r-- | platformio/temphum/src/temphum.h | 15 | ||||
-rw-r--r-- | platformio/temphum/src/util.h | 13 | ||||
-rw-r--r-- | platformio/temphum/src/wifi.cpp | 48 | ||||
-rw-r--r-- | platformio/temphum/src/wifi.h | 36 |
19 files changed, 1834 insertions, 0 deletions
diff --git a/platformio/temphum/src/config.cpp b/platformio/temphum/src/config.cpp new file mode 100644 index 0000000..54922c1 --- /dev/null +++ b/platformio/temphum/src/config.cpp @@ -0,0 +1,84 @@ +#include <EEPROM.h> +#include <strings.h> +#include "config.h" +#include "logging.h" + +#define GET_DATA_CRC(data) \ + eeprom_crc(reinterpret_cast<uint8_t*>(&(data))+4, sizeof(ConfigData)-4) + +namespace homekit::config { + +static const uint32_t magic = 0xdeadbeef; +static const uint32_t crc_table[16] PROGMEM = { + 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, + 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, + 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, + 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c +}; + +static uint32_t eeprom_crc(const uint8_t* data, size_t len) { + uint32_t crc = ~0L; + for (size_t index = 0; index < len; index++) { + crc = pgm_read_word(&crc_table[(crc ^ data[index]) & 0x0f]) ^ (crc >> 4); + crc = pgm_read_word(&crc_table[(crc ^ (data[index] >> 4)) & 0x0f]) ^ (crc >> 4); + crc = ~crc; + } + return crc; +} + +ConfigData read() { + ConfigData data; + EEPROM.begin(sizeof(ConfigData)); + EEPROM.get(0, data); + EEPROM.end(); +#ifdef DEBUG + if (!isValid(data)) { + PRINTLN("config::read(): data is not valid!"); + } +#endif + return data; +} + +void write(ConfigData& data) { + EEPROM.begin(sizeof(ConfigData)); + data.magic = magic; + data.crc = GET_DATA_CRC(data); + EEPROM.put(0, data); + EEPROM.end(); +} + +void erase() { + ConfigData data; + erase(data); +} + +void erase(ConfigData& data) { + bzero(reinterpret_cast<uint8_t*>(&data), sizeof(data)); + write(data); +} + +bool isValid(ConfigData& data) { + return data.crc == GET_DATA_CRC(data); +} + +bool isDirty(ConfigData& data) { + return data.magic != magic; +} + +char* ConfigData::escapeHomeId(char* buf, size_t len) { + if (len < 32) + return nullptr; + size_t id_len = strlen(node_id); + char* c = node_id; + char* dst = buf; + for (size_t i = 0; i < id_len; i++) { + if (*c == '"') + *(dst++) = '\\'; + *(dst++) = *c; + c++; + } + *dst = '\0'; + return buf; +} + +}
\ No newline at end of file diff --git a/platformio/temphum/src/config.def.h.example b/platformio/temphum/src/config.def.h.example new file mode 100644 index 0000000..06c551e --- /dev/null +++ b/platformio/temphum/src/config.def.h.example @@ -0,0 +1,33 @@ +#pragma once + +#define FW_VERSION 7 + +#define DEFAULT_WIFI_AP_SSID "" +#define DEFAULT_WIFI_STA_SSID "" +#define DEFAULT_WIFI_STA_PSK "" + +#define DEFAULT_MQTT_SERVER "mqtt.solarmon.ru" +#define DEFAULT_MQTT_PORT 8883 +#define DEFAULT_MQTT_USERNAME "" +#define DEFAULT_MQTT_PASSWORD "" +#define DEFAULT_MQTT_CLIENT_ID "" +#define DEFAULT_MQTT_CA_FINGERPRINT { \ + 0x0e, 0xb6, 0x3a, 0x02, 0x1f, \ + 0x4e, 0x1e, 0xe1, 0x6a, 0x67, \ + 0x62, 0xec, 0x64, 0xd4, 0x84, \ + 0x8a, 0xb0, 0xc9, 0x9c, 0xbb \ +}; + +#define DEFAULT_NODE_ID "relay-node" + +#define FLASH_BUTTON_PIN 0 +#define ESP_LED_PIN 2 +#define BOARD_LED_PIN 16 +#define RELAY_PIN 5 + +// 12 bytes string +#define HOME_SECRET_SIZE 12 +#define HOME_SECRET "" +#define MQTT_BLINK 1 + +#define ARRAY_SIZE(X) sizeof((X))/sizeof((X)[0])
\ No newline at end of file diff --git a/platformio/temphum/src/config.h b/platformio/temphum/src/config.h new file mode 100644 index 0000000..2a5b36a --- /dev/null +++ b/platformio/temphum/src/config.h @@ -0,0 +1,34 @@ +#pragma once + +#include <Arduino.h> + +namespace homekit::config { + +struct ConfigFlags { + uint8_t wifi_configured: 1; + uint8_t node_configured: 1; + uint8_t reserved: 6; +} __attribute__((packed)); + +struct ConfigData { + // helpers + uint32_t crc = 0; + uint32_t magic = 0; + char node_id[16] = {0}; + char wifi_ssid[32] = {0}; + char wifi_psk[63] = {0}; + ConfigFlags flags {0}; + + // helper methods + char* escapeHomeId(char* buf, size_t len); +} __attribute__((packed)); + + +ConfigData read(); +void write(ConfigData& data); +void erase(); +void erase(ConfigData& data); +bool isValid(ConfigData& data); +bool isDirty(ConfigData& data); + +}
\ No newline at end of file diff --git a/platformio/temphum/src/http_server.cpp b/platformio/temphum/src/http_server.cpp new file mode 100644 index 0000000..5fbcb8a --- /dev/null +++ b/platformio/temphum/src/http_server.cpp @@ -0,0 +1,279 @@ +#include <Arduino.h> +#include <string.h> + +#include "static.h" +#include "http_server.h" +#include "config.h" +#include "config.def.h" +#include "logging.h" +#include "util.h" +#include "led.h" + +namespace homekit { + +using files::StaticFile; + +static const char CONTENT_TYPE_HTML[] PROGMEM = "text/html; charset=utf-8"; +static const char CONTENT_TYPE_CSS[] PROGMEM = "text/css"; +static const char CONTENT_TYPE_JS[] PROGMEM = "application/javascript"; +static const char CONTENT_TYPE_JSON[] PROGMEM = "application/json"; +static const char CONTENT_TYPE_FAVICON[] PROGMEM = "image/x-icon"; + +static const char JSON_UPDATE_FMT[] PROGMEM = "{\"result\":%d}"; +static const char JSON_STATUS_FMT[] PROGMEM = "{\"node_id\":\"%s\"" +#ifdef DEBUG + ",\"configured\":%d" + ",\"crc\":%u" + ",\"fl_n\":%d" + ",\"fl_w\":%d" +#endif + "}"; +static const size_t JSON_BUF_SIZE = 192; + +static const char JSON_SCAN_FIRST_LIST[] PROGMEM = "{\"list\":["; + +static const char MSG_IS_INVALID[] PROGMEM = " is invalid"; +static const char MSG_IS_MISSING[] PROGMEM = " is missing"; + +static const char GZIP[] PROGMEM = "gzip"; +static const char CONTENT_ENCODING[] PROGMEM = "Content-Encoding"; +static const char NOT_FOUND[] PROGMEM = "Not Found"; + +static const char ROUTE_STYLE_CSS[] PROGMEM = "/style.css"; +static const char ROUTE_APP_JS[] PROGMEM = "/app.js"; +static const char ROUTE_MD5_JS[] PROGMEM = "/md5.js"; +static const char ROUTE_FAVICON_ICO[] PROGMEM = "/favicon.ico"; +static const char ROUTE_STATUS[] PROGMEM = "/status"; +static const char ROUTE_SCAN[] PROGMEM = "/scan"; +static const char ROUTE_RESET[] PROGMEM = "/reset"; +// #ifdef DEBUG +static const char ROUTE_HEAP[] PROGMEM = "/heap"; +// #endif +static const char ROUTE_UPDATE[] PROGMEM = "/update"; + +void HttpServer::start() { + server.on(FPSTR(ROUTE_STYLE_CSS), HTTP_GET, [&]() { sendGzip(files::style_css, CONTENT_TYPE_CSS); }); + server.on(FPSTR(ROUTE_APP_JS), HTTP_GET, [&]() { sendGzip(files::app_js, CONTENT_TYPE_JS); }); + server.on(FPSTR(ROUTE_MD5_JS), HTTP_GET, [&]() { sendGzip(files::md5_js, CONTENT_TYPE_JS); }); + server.on(FPSTR(ROUTE_FAVICON_ICO), HTTP_GET, [&]() { sendGzip(files::favicon_ico, CONTENT_TYPE_FAVICON); }); + + server.on("/", HTTP_GET, [&]() { sendGzip(files::index_html, CONTENT_TYPE_HTML); }); + server.on(FPSTR(ROUTE_STATUS), HTTP_GET, [&]() { + char json_buf[JSON_BUF_SIZE]; + auto cfg = config::read(); + + if (!isValid(cfg) || !cfg.flags.node_configured) { + sprintf_P(json_buf, JSON_STATUS_FMT + , DEFAULT_NODE_ID +#ifdef DEBUG + , 0 + , cfg.crc + , cfg.flags.node_configured + , cfg.flags.wifi_configured +#endif + ); + } else { + char escaped_node_id[32]; + char *escaped_node_id_res = cfg.escapeHomeId(escaped_node_id, 32); + sprintf_P(json_buf, JSON_STATUS_FMT + , escaped_node_id_res == nullptr ? "?" : escaped_node_id +#ifdef DEBUG + , 1 + , cfg.crc + , cfg.flags.node_configured + , cfg.flags.wifi_configured +#endif + ); + } + server.send(200, FPSTR(CONTENT_TYPE_JSON), json_buf); + }); + server.on(FPSTR(ROUTE_STATUS), HTTP_POST, [&]() { + auto cfg = config::read(); + String s; + + if (!getInputParam("ssid", 32, s)) return; + strncpy(cfg.wifi_ssid, s.c_str(), 32); + PRINTF("saving ssid: %s\n", cfg.wifi_ssid); + + if (!getInputParam("psk", 63, s)) return; + strncpy(cfg.wifi_psk, s.c_str(), 63); + PRINTF("saving psk: %s\n", cfg.wifi_psk); + + if (!getInputParam("hid", 16, s)) return; + strcpy(cfg.node_id, s.c_str()); + PRINTF("saving home id: %s\n", cfg.node_id); + + cfg.flags.node_configured = 1; + cfg.flags.wifi_configured = 1; + + config::write(cfg); + + restartTimer.once(0, restart); + }); + + server.on(FPSTR(ROUTE_RESET), HTTP_POST, [&]() { + config::erase(); + restartTimer.once(1, restart); + }); + + server.on(FPSTR(ROUTE_HEAP), HTTP_GET, [&]() { + server.send(200, FPSTR(CONTENT_TYPE_HTML), String(ESP.getFreeHeap())); + }); + + server.on(FPSTR(ROUTE_SCAN), HTTP_GET, [&]() { + size_t i = 0; + size_t len; + const char* ssid; + bool enough = false; + + bzero(reinterpret_cast<uint8_t*>(scanBuf), scanBufSize); + char* cur = scanBuf; + + strncpy_P(cur, JSON_SCAN_FIRST_LIST, scanBufSize); + cur += 9; + + for (auto& res: *scanResults) { + ssid = res.ssid.c_str(); + len = res.ssid.length(); + + // new item (array with 2 items) + *cur++ = '['; + + // 1. ssid (string) + *cur++ = '"'; + for (size_t j = 0; j < len; j++) { + if (*(ssid+j) == '"') + *cur++ = '\\'; + *cur++ = *(ssid+j); + } + *cur++ = '"'; + *cur++ = ','; + + // 2. rssi (number) + cur += sprintf(cur, "%d", res.rssi); + + // close array + *cur++ = ']'; + + if ((size_t)(cur - scanBuf) >= (size_t) ARRAY_SIZE(scanBuf) - 40) + enough = true; + + if (i < scanResults->size() - 1 || enough) + *cur++ = ','; + + if (enough) + break; + + i++; + } + + *cur++ = ']'; + *cur++ = '}'; + *cur++ = '\0'; + + server.send(200, FPSTR(CONTENT_TYPE_JSON), scanBuf); + }); + + server.on(FPSTR(ROUTE_UPDATE), HTTP_POST, [&]() { + char json_buf[16]; + bool should_reboot = !Update.hasError() && !ota.invalidMd5; + Update.clearError(); + + sprintf_P(json_buf, JSON_UPDATE_FMT, should_reboot ? 1 : 0); + + server.send(200, FPSTR(CONTENT_TYPE_JSON), json_buf); + + if (should_reboot) + restartTimer.once(1, restart); + }, [&]() { + HTTPUpload& upload = server.upload(); + + if (upload.status == UPLOAD_FILE_START) { + ota.clean(); + + String s; + if (!getInputParam("md5", 0, s)) { + ota.invalidMd5 = true; + PRINTLN("http/ota: md5 not found"); + return; + } + + if (!Update.setMD5(s.c_str())) { + ota.invalidMd5 = true; + PRINTLN("http/ota: setMD5() failed"); + return; + } + + Serial.printf("http/ota: starting, filename=%s\n", upload.filename.c_str()); + if (!Update.begin(otaGetMaxUpdateSize())) { +#ifdef DEBUG + Update.printError(Serial); +#endif + } + } else if (upload.status == UPLOAD_FILE_WRITE) { + if (!Update.isRunning()) + return; + + PRINTF("http/ota: writing %ul\n", upload.currentSize); + esp_led.blink(1, 1); + if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { +#ifdef DEBUG + Update.printError(Serial); +#endif + } + } else if (upload.status == UPLOAD_FILE_END) { + if (!Update.isRunning()) + return; + + if (Update.end(true)) { + PRINTF("http/ota: ok, total size %ul\n", upload.totalSize); + } else { +#ifdef DEBUG + Update.printError(Serial); +#endif + } + } + }); + + server.onNotFound([&]() { + server.send(404, FPSTR(CONTENT_TYPE_HTML), NOT_FOUND); + }); + + server.begin(); +} + +void HttpServer::loop() { + server.handleClient(); +} + +void HttpServer::sendGzip(const StaticFile& file, PGM_P content_type) { + server.sendHeader(FPSTR(CONTENT_ENCODING), FPSTR(GZIP)); + server.send_P(200, content_type, (const char*)file.content, file.size); +} + +void HttpServer::sendError(const String& message) { + char buf[32]; + if (snprintf_P(buf, 32, PSTR("error: %s"), message.c_str()) == 32) + buf[31] = '\0'; + server.send(400, FPSTR(CONTENT_TYPE_HTML), buf); +} + +bool HttpServer::getInputParam(const char *field_name, + size_t max_len, + String& dst) { + if (!server.hasArg(field_name)) { + sendError(String(field_name) + String(MSG_IS_MISSING)); + return false; + } + + String field = server.arg(field_name); + if (!field.length() || (max_len != 0 && field.length() > max_len)) { + sendError(String(field_name) + String(MSG_IS_INVALID)); + return false; + } + + dst = field; + return true; +} + +}
\ No newline at end of file diff --git a/platformio/temphum/src/http_server.h b/platformio/temphum/src/http_server.h new file mode 100644 index 0000000..35f7c08 --- /dev/null +++ b/platformio/temphum/src/http_server.h @@ -0,0 +1,56 @@ +#pragma once +#include <ESP8266WebServer.h> +#include <Ticker.h> +#include <memory> +#include <list> +#include <utility> +#include "config.h" +#include "wifi.h" +#include "static.h" + +namespace homekit { + +struct OTAStatus { + bool invalidMd5; + + OTAStatus() : invalidMd5(false) {} + + inline void clean() { + invalidMd5 = false; + } +}; + +using files::StaticFile; + +class HttpServer { +private: + ESP8266WebServer server; + Ticker restartTimer; + std::shared_ptr<std::list<wifi::ScanResult>> scanResults; + OTAStatus ota; + + char* scanBuf; + size_t scanBufSize; + + void sendGzip(const StaticFile& file, PGM_P content_type); + void sendError(const String& message); + + bool getInputParam(const char* field_name, size_t max_len, String& dst); + +public: + explicit HttpServer(std::shared_ptr<std::list<wifi::ScanResult>> scanResults) + : server(80) + , scanResults(std::move(scanResults)) + , scanBufSize(512) { + scanBuf = new char[scanBufSize]; + }; + + ~HttpServer() { + delete[] scanBuf; + } + + void start(); + void loop(); +}; + +}
\ No newline at end of file diff --git a/platformio/temphum/src/led.cpp b/platformio/temphum/src/led.cpp new file mode 100644 index 0000000..b6b0d81 --- /dev/null +++ b/platformio/temphum/src/led.cpp @@ -0,0 +1,9 @@ +#include "led.h" +#include "config.def.h" + +namespace homekit { + +Led board_led(BOARD_LED_PIN); +Led esp_led(ESP_LED_PIN); + +}
\ No newline at end of file diff --git a/platformio/temphum/src/led.h b/platformio/temphum/src/led.h new file mode 100644 index 0000000..315523a --- /dev/null +++ b/platformio/temphum/src/led.h @@ -0,0 +1,39 @@ +#pragma once + +#include <Arduino.h> + +namespace homekit { + +class Led { +private: + uint8_t _pin; + +public: + explicit Led(uint8_t pin) : _pin(pin) { + pinMode(_pin, OUTPUT); + off(); + } + + inline void off() const { digitalWrite(_pin, HIGH); } + inline void on() const { digitalWrite(_pin, LOW); } + + void on_off(uint16_t delay_ms, bool last_delay = false) const { + on(); + delay(delay_ms); + + off(); + if (last_delay) + delay(delay_ms); + } + + void blink(uint8_t count, uint16_t delay_ms) const { + for (uint8_t i = 0; i < count; i++) { + on_off(delay_ms, i < count-1); + } + } +}; + +extern Led board_led; +extern Led esp_led; + +}
\ No newline at end of file diff --git a/platformio/temphum/src/logging.h b/platformio/temphum/src/logging.h new file mode 100644 index 0000000..070f367 --- /dev/null +++ b/platformio/temphum/src/logging.h @@ -0,0 +1,18 @@ +#pragma once + +#include <stdlib.h> +#include "config.def.h" + +#ifdef DEBUG + +#define PRINTLN(s) Serial.println(s) +#define PRINT(s) Serial.print(s) +#define PRINTF(fmt, ...) Serial.printf(fmt, ##__VA_ARGS__) + +#else + +#define PRINTLN(s) +#define PRINT(s) +#define PRINTF(...) + +#endif diff --git a/platformio/temphum/src/main.cpp b/platformio/temphum/src/main.cpp new file mode 100644 index 0000000..1fa7e66 --- /dev/null +++ b/platformio/temphum/src/main.cpp @@ -0,0 +1,183 @@ +#include <Arduino.h> +#include <ESP8266WiFi.h> +#include <DNSServer.h> +#include <Ticker.h> +#include <Wire.h> + +#include "mqtt.h" +#include "config.h" +#include "logging.h" +#include "http_server.h" +#include "led.h" +#include "config.def.h" +#include "wifi.h" +#include "temphum.h" +#include "stopwatch.h" + +using namespace homekit; + +enum class WorkingMode { + RECOVERY, // AP mode, http server with configuration + NORMAL, // MQTT client +}; +static enum WorkingMode working_mode = WorkingMode::NORMAL; + +enum class WiFiConnectionState { + WAITING = 0, + JUST_CONNECTED = 1, + CONNECTED = 2 +}; + +static const uint16_t recovery_boot_detection_ms = 2000; +static const uint8_t recovery_boot_delay_ms = 100; + +static volatile enum WiFiConnectionState wifi_state = WiFiConnectionState::WAITING; +static void* service = nullptr; +static WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler; +static Ticker wifiTimer; +#if MQTT_BLINK +static StopWatch blinkStopWatch; +#endif + +static DNSServer* dnsServer = nullptr; + +static void onWifiConnected(const WiFiEventStationModeGotIP& event); +static void onWifiDisconnected(const WiFiEventStationModeDisconnected& event); + +static void wifiConnect() { + const char *ssid, *psk, *hostname; + auto cfg = config::read(); + wifi::getConfig(cfg, &ssid, &psk, &hostname); + + PRINTF("Wi-Fi STA creds: ssid=%s, psk=%s, hostname=%s\n", ssid, psk, hostname); + + wifi_state = WiFiConnectionState::WAITING; + + WiFi.mode(WIFI_STA); + WiFi.hostname(hostname); + WiFi.begin(ssid, psk); + + PRINT("connecting to wifi.."); +} + +static void wifiHotspot() { + esp_led.on(); + + auto scanResults = wifi::scan(); + + WiFi.mode(WIFI_AP); + WiFi.softAP(wifi::AP_SSID); + + dnsServer = new DNSServer(); + dnsServer->start(53, "*", WiFi.softAPIP()); + + service = new HttpServer(scanResults); + ((HttpServer*)service)->start(); +} + +static void waitForRecoveryPress() { + pinMode(FLASH_BUTTON_PIN, INPUT_PULLUP); + for (uint16_t i = 0; i < recovery_boot_detection_ms; i += recovery_boot_delay_ms) { + delay(recovery_boot_delay_ms); + if (digitalRead(FLASH_BUTTON_PIN) == LOW) { + working_mode = WorkingMode::RECOVERY; + break; + } + } +} + +void setup() { + WiFi.disconnect(); + waitForRecoveryPress(); + + temphum::setup(); + +#ifdef DEBUG + Serial.begin(115200); +#endif + + auto cfg = config::read(); + if (config::isDirty(cfg)) { + PRINTLN("config is dirty, erasing..."); + config::erase(cfg); + board_led.blink(10, 50); + } + + switch (working_mode) { + case WorkingMode::RECOVERY: + wifiHotspot(); + break; + + case WorkingMode::NORMAL: + wifiConnectHandler = WiFi.onStationModeGotIP(onWifiConnected); + wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWifiDisconnected); + wifiConnect(); + break; + } +} + +void loop() { + if (working_mode == WorkingMode::NORMAL) { + if (wifi_state == WiFiConnectionState::WAITING) { + PRINT("."); + esp_led.blink(2, 50); + delay(1000); + return; + } + + if (wifi_state == WiFiConnectionState::JUST_CONNECTED) { + board_led.blink(3, 300); + wifi_state = WiFiConnectionState::CONNECTED; + + if (service == nullptr) + service = new mqtt::MQTT(); + + ((mqtt::MQTT*)service)->connect(); +#if MQTT_BLINK + blinkStopWatch.save(); +#endif + } + + auto mqtt = (mqtt::MQTT*)service; + if (static_cast<int>(wifi_state) >= 1 && mqtt != nullptr) { + mqtt->loop(); + + if (mqtt->ota.readyToRestart) { + mqtt->disconnect(); + } else if (mqtt->diagnosticsStopWatch.elapsed(10000)) { + mqtt->sendDiagnostics(); + + auto data = temphum::read(); + mqtt->sendTempHumData(data.temp, data.rh); + } + +#if MQTT_BLINK + // periodically blink board led + if (blinkStopWatch.elapsed(5000)) { + board_led.blink(1, 10); + blinkStopWatch.save(); + } +#endif + } + } else { + if (dnsServer != nullptr) + dnsServer->processNextRequest(); + + auto httpServer = (HttpServer*)service; + if (httpServer != nullptr) + httpServer->loop(); + } +} + +static void onWifiConnected(const WiFiEventStationModeGotIP& event) { + PRINTF("connected (%s)\n", WiFi.localIP().toString().c_str()); + wifi_state = WiFiConnectionState::JUST_CONNECTED; +} + +static void onWifiDisconnected(const WiFiEventStationModeDisconnected& event) { + PRINTLN("disconnected from wi-fi"); + wifi_state = WiFiConnectionState::WAITING; + if (service != nullptr) + ((mqtt::MQTT*)service)->disconnect(); + wifiTimer.once(2, wifiConnect); +}
\ No newline at end of file diff --git a/platformio/temphum/src/mqtt.cpp b/platformio/temphum/src/mqtt.cpp new file mode 100644 index 0000000..ff13e43 --- /dev/null +++ b/platformio/temphum/src/mqtt.cpp @@ -0,0 +1,325 @@ +#include <ESP8266httpUpdate.h> +#include "mqtt.h" +#include "logging.h" +#include "wifi.h" +#include "config.def.h" +#include "config.h" +#include "static.h" +#include "util.h" +#include "led.h" + +namespace homekit::mqtt { + +static const uint8_t MQTT_CA_FINGERPRINT[] = DEFAULT_MQTT_CA_FINGERPRINT; +static const char MQTT_SERVER[] = DEFAULT_MQTT_SERVER; +static const uint16_t MQTT_PORT = DEFAULT_MQTT_PORT; +static const char MQTT_USERNAME[] = DEFAULT_MQTT_USERNAME; +static const char MQTT_PASSWORD[] = DEFAULT_MQTT_PASSWORD; +static const char MQTT_CLIENT_ID[] = DEFAULT_MQTT_CLIENT_ID; +static const char MQTT_SECRET[HOME_SECRET_SIZE+1] = HOME_SECRET; + +static const char TOPIC_DIAGNOSTICS[] = "stat"; +static const char TOPIC_INITIAL_DIAGNOSTICS[] = "stat1"; +static const char TOPIC_OTA_RESPONSE[] = "otares"; +static const char TOPIC_TEMPHUM_DATA[] = "data"; +static const char TOPIC_ADMIN_OTA[] = "admin/ota"; +static const uint16_t MQTT_KEEPALIVE = 30; + +enum class IncomingMessage { + UNKNOWN, + OTA +}; + +using namespace espMqttClientTypes; + +#define MD5_SIZE 16 + +MQTT::MQTT() { + auto cfg = config::read(); + homeId = String(cfg.flags.node_configured ? cfg.node_id : wifi::NODE_ID); + + randomSeed(micros()); + + client.onConnect([&](bool sessionPresent) { + PRINTLN("mqtt: connected"); + + sendInitialDiagnostics(); + subscribe(TOPIC_ADMIN_OTA); + }); + + client.onDisconnect([&](DisconnectReason reason) { + PRINTF("mqtt: disconnected, reason=%d\n", static_cast<int>(reason)); +#ifdef DEBUG + if (reason == DisconnectReason::TLS_BAD_FINGERPRINT) + PRINTLN("reason: bad fingerprint"); +#endif + + if (ota.started()) { + PRINTLN("mqtt: update was in progress, canceling.."); + ota.clean(); + Update.end(); + Update.clearError(); + } + + if (ota.readyToRestart) { + restartTimer.once(1, restart); + } else { + reconnectTimer.once(2, [&]() { + reconnect(); + }); + } + }); + + client.onSubscribe([&](uint16_t packetId, const SubscribeReturncode* returncodes, size_t len) { + PRINTF("mqtt: subscribe ack, packet_id=%d\n", packetId); + for (size_t i = 0; i < len; i++) { + PRINTF(" return code: %u\n", static_cast<unsigned int>(*(returncodes+i))); + } + }); + + client.onUnsubscribe([&](uint16_t packetId) { + PRINTF("mqtt: unsubscribe ack, packet_id=%d\n", packetId); + }); + + client.onMessage([&](const MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { + PRINTF("mqtt: message received, topic=%s, qos=%d, dup=%d, retain=%d, len=%ul, index=%ul, total=%ul\n", + topic, properties.qos, (int)properties.dup, (int)properties.retain, len, index, total); + + IncomingMessage msgType = IncomingMessage::UNKNOWN; + + const char *ptr = topic + homeId.length() + 10; + String relevantTopic(ptr); + + if (relevantTopic == TOPIC_ADMIN_OTA) + msgType = IncomingMessage::OTA; + + if (len != total && msgType != IncomingMessage::OTA) { + PRINTLN("mqtt: received partial message, not supported"); + return; + } + + switch (msgType) { + case IncomingMessage::OTA: + if (ota.finished) + break; + handleAdminOtaPayload(properties.packetId, payload, len, index, total); + break; + + case IncomingMessage::UNKNOWN: + PRINTF("error: invalid topic %s\n", topic); + break; + } + }); + + client.onPublish([&](uint16_t packetId) { + PRINTF("mqtt: publish ack, packet_id=%d\n", packetId); + + if (ota.finished && packetId == ota.publishResultPacketId) { + ota.readyToRestart = true; + } + }); + + client.setServer(MQTT_SERVER, MQTT_PORT); + client.setClientId(MQTT_CLIENT_ID); + client.setCredentials(MQTT_USERNAME, MQTT_PASSWORD); + client.setCleanSession(true); + client.setFingerprint(MQTT_CA_FINGERPRINT); + client.setKeepAlive(MQTT_KEEPALIVE); +} + +void MQTT::connect() { + reconnect(); +} + +void MQTT::reconnect() { + if (client.connected()) { + PRINTLN("warning: already connected"); + return; + } + client.connect(); +} + +void MQTT::disconnect() { + // TODO test how this works??? + reconnectTimer.detach(); + client.disconnect(); +} + +uint16_t MQTT::publish(const String &topic, uint8_t *payload, size_t length) { + String fullTopic = "hk/" + homeId + "/temphum/" + topic; + return client.publish(fullTopic.c_str(), 1, false, payload, length); +} + +void MQTT::loop() { + client.loop(); +} + +uint16_t MQTT::subscribe(const String &topic, uint8_t qos) { + String fullTopic = "hk/" + homeId + "/temphum/" + topic; + PRINTF("mqtt: subscribing to %s...\n", fullTopic.c_str()); + + uint16_t packetId = client.subscribe(fullTopic.c_str(), qos); + if (!packetId) + PRINTF("error: failed to subscribe to %s\n", fullTopic.c_str()); + return packetId; +} + +void MQTT::sendInitialDiagnostics() { + auto cfg = config::read(); + InitialDiagnosticsPayload stat{ + .ip = wifi::getIPAsInteger(), + .fw_version = FW_VERSION, + .rssi = wifi::getRSSI(), + .free_heap = ESP.getFreeHeap(), + .flags = DiagnosticsFlags{ + .state = 1, + .config_changed_value_present = 1, + .config_changed = static_cast<uint8_t>(cfg.flags.node_configured || + cfg.flags.wifi_configured ? 1 : 0) + } + }; + publish(TOPIC_INITIAL_DIAGNOSTICS, reinterpret_cast<uint8_t*>(&stat), sizeof(stat)); + diagnosticsStopWatch.save(); +} + +void MQTT::sendDiagnostics() { + DiagnosticsPayload stat{ + .rssi = wifi::getRSSI(), + .free_heap = ESP.getFreeHeap(), + .flags = DiagnosticsFlags{ + .state = 1, + .config_changed_value_present = 0, + .config_changed = 0 + } + }; + publish(TOPIC_DIAGNOSTICS, reinterpret_cast<uint8_t*>(&stat), sizeof(stat)); + diagnosticsStopWatch.save(); +} + +void MQTT::sendTempHumData(double temp, double rh) { + TempHumDataPayload data { + .temp = temp, + .rh = rh + }; + publish(TOPIC_TEMPHUM_DATA, reinterpret_cast<uint8_t*>(&data), sizeof(data)); +} + +uint16_t MQTT::sendOtaResponse(OTAResult status, uint8_t error_code) { + OTAResponse resp{ + .status = status, + .error_code = error_code + }; + return publish(TOPIC_OTA_RESPONSE, reinterpret_cast<uint8_t*>(&resp), sizeof(resp)); +} + +void MQTT::handleAdminOtaPayload(uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total) { + char md5[33]; + char* md5Ptr = md5; + + if (index != 0 && ota.dataPacketId != packetId) { + PRINTLN("mqtt/ota: non-matching packet id"); + return; + } + + Update.runAsync(true); + + if (index == 0) { + if (length < HOME_SECRET_SIZE + MD5_SIZE) { + PRINTLN("mqtt/ota: failed to check secret, first packet size is too small"); + return; + } + + if (memcmp((const char*)payload, HOME_SECRET, HOME_SECRET_SIZE) != 0) { + PRINTLN("mqtt/ota: invalid secret"); + return; + } + + PRINTF("mqtt/ota: starting update, total=%ul\n", total-HOME_SECRET_SIZE); + for (int i = 0; i < MD5_SIZE; i++) { + md5Ptr += sprintf(md5Ptr, "%02x", *((unsigned char*)(payload+HOME_SECRET_SIZE+i))); + } + md5[32] = '\0'; + PRINTF("mqtt/ota: md5 is %s\n", md5); + PRINTF("mqtt/ota: first packet is %ul bytes length\n", length); + + md5[32] = '\0'; + + if (Update.isRunning()) { + Update.end(); + Update.clearError(); + } + + if (!Update.setMD5(md5)) { + PRINTLN("mqtt/ota: setMD5 failed"); + return; + } + + ota.dataPacketId = packetId; + + if (!Update.begin(total - HOME_SECRET_SIZE - MD5_SIZE)) { + ota.clean(); +#ifdef DEBUG + Update.printError(Serial); +#endif + sendOtaResponse(OTAResult::UPDATE_ERROR, Update.getError()); + } + + ota.written = Update.write(const_cast<uint8_t*>(payload)+HOME_SECRET_SIZE + MD5_SIZE, length-HOME_SECRET_SIZE - MD5_SIZE); + ota.written += HOME_SECRET_SIZE + MD5_SIZE; + + esp_led.blink(1, 1); + PRINTF("mqtt/ota: updating %u/%u\n", ota.written, Update.size()); + + } else { + if (!Update.isRunning()) { + PRINTLN("mqtt/ota: update is not running"); + return; + } + + if (index == ota.written) { + size_t written; + if ((written = Update.write(const_cast<uint8_t*>(payload), length)) != length) { + PRINTF("mqtt/ota: error: tried to write %ul bytes, write() returned %ul\n", + length, written); + ota.clean(); + Update.end(); + Update.clearError(); + sendOtaResponse(OTAResult::WRITE_ERROR); + return; + } + ota.written += length; + + esp_led.blink(1, 1); + PRINTF("mqtt/ota: updating %u/%u\n", + ota.written - HOME_SECRET_SIZE - MD5_SIZE, + Update.size()); + } else { + PRINTF("mqtt/ota: position is invalid, expected %ul, got %ul\n", ota.written, index); + ota.clean(); + Update.end(); + Update.clearError(); + } + } + + if (Update.isFinished()) { + ota.dataPacketId = 0; + + if (Update.end()) { + ota.finished = true; + ota.publishResultPacketId = sendOtaResponse(OTAResult::OK); + PRINTF("mqtt/ota: ok, otares packet_id=%d\n", ota.publishResultPacketId); + } else { + ota.clean(); + + PRINTF("mqtt/ota: error: %u\n", Update.getError()); +#ifdef DEBUG + Update.printError(Serial); +#endif + Update.clearError(); + + sendOtaResponse(OTAResult::UPDATE_ERROR, Update.getError()); + } + } +} + +}
\ No newline at end of file diff --git a/platformio/temphum/src/mqtt.h b/platformio/temphum/src/mqtt.h new file mode 100644 index 0000000..38ed988 --- /dev/null +++ b/platformio/temphum/src/mqtt.h @@ -0,0 +1,107 @@ +#include <ESP8266WiFi.h> +#include <espMqttClient.h> +#include <Ticker.h> +#include "stopwatch.h" + +namespace homekit { namespace mqtt { + +enum class OTAResult: uint8_t { + OK = 0, + UPDATE_ERROR = 1, + WRITE_ERROR = 2, +}; + +struct OTAStatus { + uint16_t dataPacketId; + uint16_t publishResultPacketId; + bool finished; + bool readyToRestart; + size_t written; + + OTAStatus() + : dataPacketId(0) + , publishResultPacketId(0) + , finished(false) + , readyToRestart(false) + , written(0) + {} + + inline void clean() { + dataPacketId = 0; + publishResultPacketId = 0; + finished = false; + readyToRestart = false; + written = 0; + } + + inline bool started() const { + return dataPacketId != 0; + } +}; + +class MQTT { +private: + String homeId; + WiFiClientSecure httpsSecureClient; + espMqttClientSecure client; + Ticker reconnectTimer; + Ticker restartTimer; + + void handleAdminOtaPayload(uint16_t packetId, const uint8_t* payload, size_t length, size_t index, size_t total); + + uint16_t publish(const String& topic, uint8_t* payload, size_t length); + uint16_t subscribe(const String& topic, uint8_t qos = 0); + + void sendInitialDiagnostics(); + uint16_t sendOtaResponse(OTAResult status, uint8_t error_code = 0); + +public: + StopWatch diagnosticsStopWatch; + OTAStatus ota; + + MQTT(); + void connect(); + void disconnect(); + void reconnect(); + void loop(); + void sendDiagnostics(); + void sendTempHumData(double temp, double rh); +}; + +struct DiagnosticsFlags { + uint8_t state: 1; + uint8_t config_changed_value_present: 1; + uint8_t config_changed: 1; + uint8_t reserved: 5; +} __attribute__((packed)); + +struct InitialDiagnosticsPayload { + uint32_t ip; + uint8_t fw_version; + int8_t rssi; + uint32_t free_heap; + DiagnosticsFlags flags; +} __attribute__((packed)); + +struct DiagnosticsPayload { + int8_t rssi; + uint32_t free_heap; + DiagnosticsFlags flags; +} __attribute__((packed)); + +struct PowerPayload { + char secret[12]; + uint8_t state; +} __attribute__((packed)); + +struct TempHumDataPayload { + double temp; + double rh; +} __attribute__((packed)); + +struct OTAResponse { + OTAResult status; + uint8_t error_code; +} __attribute__((packed)); + +} }
\ No newline at end of file diff --git a/platformio/temphum/src/static.cpp b/platformio/temphum/src/static.cpp new file mode 100644 index 0000000..366a09f --- /dev/null +++ b/platformio/temphum/src/static.cpp @@ -0,0 +1,450 @@ +/** + * This file is autogenerated with make_static.sh script + */ + +#include "static.h" + +namespace homekit::files { + +static const uint8_t index_html_content[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x9d, 0x56, 0x4d, 0x6f, 0xdb, 0x38, + 0x10, 0xbd, 0xe7, 0x57, 0xb0, 0x3c, 0x14, 0x09, 0x10, 0x4b, 0x9b, 0x14, 0xcd, 0x16, 0xad, 0x24, + 0xa0, 0xd8, 0x76, 0xb1, 0x05, 0x7a, 0x08, 0x6a, 0x14, 0x0b, 0xec, 0xc5, 0xa0, 0xa8, 0x91, 0xc5, + 0x9a, 0x22, 0x59, 0x71, 0x24, 0xc7, 0xfd, 0xf5, 0x1d, 0x52, 0x92, 0x3f, 0xb2, 0x46, 0xfa, 0x71, + 0xb1, 0x34, 0xc3, 0x99, 0x37, 0x6f, 0x1e, 0x87, 0xa2, 0xb3, 0x67, 0x95, 0x95, 0xb8, 0x73, 0xc0, + 0x1a, 0x6c, 0x75, 0x71, 0x91, 0x85, 0x07, 0xd3, 0xc2, 0xac, 0x73, 0x0e, 0x86, 0x07, 0x07, 0x88, + 0x8a, 0x1e, 0x2d, 0xa0, 0xa0, 0x18, 0x74, 0x0b, 0xf8, 0xda, 0xab, 0x21, 0xe7, 0xd2, 0x1a, 0x04, + 0x83, 0x8b, 0x90, 0xcc, 0xd9, 0x64, 0xe5, 0x1c, 0xe1, 0x01, 0xd3, 0x00, 0xf2, 0x86, 0xc9, 0x46, + 0x74, 0x1e, 0x30, 0xef, 0xb1, 0x5e, 0xbc, 0xe2, 0x33, 0x86, 0x11, 0x2d, 0xe4, 0x7c, 0x50, 0xb0, + 0x75, 0xb6, 0xc3, 0xa3, 0xcc, 0xad, 0xaa, 0xb0, 0xc9, 0x2b, 0x18, 0x94, 0x84, 0x45, 0x34, 0xae, + 0x95, 0x51, 0xa8, 0x84, 0x5e, 0x78, 0x29, 0x34, 0xe4, 0x37, 0xd7, 0x2d, 0x39, 0xda, 0xbe, 0x3d, + 0xd8, 0xe2, 0xe1, 0xc4, 0xee, 0x3d, 0x74, 0xd1, 0x10, 0x25, 0xd9, 0xc6, 0x86, 0xa2, 0xa8, 0x50, + 0x43, 0xf1, 0x97, 0x35, 0xb5, 0x5a, 0xf7, 0x9d, 0x40, 0x65, 0x4d, 0x96, 0x8e, 0xce, 0x8b, 0x4c, + 0x2b, 0xb3, 0x61, 0x1d, 0xe8, 0x9c, 0xfb, 0x86, 0xd8, 0xc8, 0x1e, 0x99, 0x22, 0x42, 0x9c, 0x35, + 0x1d, 0xd4, 0x39, 0x4f, 0x6b, 0x31, 0x04, 0x3b, 0xa1, 0x1f, 0xce, 0x42, 0xa7, 0x39, 0x57, 0xad, + 0x58, 0x43, 0xfa, 0xb0, 0x88, 0x71, 0xa7, 0x10, 0xb8, 0xd3, 0xe0, 0x1b, 0x00, 0x9c, 0x63, 0xa3, + 0x18, 0xd2, 0xfb, 0x3d, 0x5e, 0x0c, 0x49, 0x82, 0x87, 0x32, 0xbd, 0xec, 0x94, 0x43, 0xe6, 0x3b, + 0x49, 0x2b, 0x6d, 0xf5, 0x32, 0xf9, 0x42, 0xee, 0x2c, 0x1d, 0xdd, 0x8f, 0xd7, 0x85, 0x73, 0x8f, + 0xd7, 0xd3, 0x69, 0x6b, 0x4a, 0x5b, 0xed, 0x98, 0x35, 0xda, 0x8a, 0x8a, 0xe8, 0x91, 0x64, 0x6f, + 0x9d, 0xbb, 0xbc, 0x0a, 0x15, 0x2a, 0x35, 0x30, 0xa9, 0x85, 0xf7, 0x44, 0x25, 0x74, 0xcc, 0x8b, + 0x25, 0x20, 0x2a, 0xb3, 0xf6, 0x2c, 0xf3, 0x4e, 0x18, 0xa6, 0x28, 0x23, 0xe4, 0x91, 0x6b, 0x45, + 0xa2, 0x81, 0xe6, 0xc5, 0xe5, 0x64, 0x27, 0x49, 0x72, 0x45, 0xc5, 0x28, 0x8a, 0x6a, 0x12, 0xd0, + 0x29, 0x5c, 0xa9, 0xad, 0xdc, 0x84, 0x12, 0xb5, 0xed, 0x5a, 0x46, 0x1b, 0xdb, 0x58, 0x82, 0x72, + 0xd6, 0x53, 0xef, 0x42, 0x06, 0x91, 0x63, 0xb7, 0x02, 0x7b, 0x6a, 0x7e, 0xdc, 0x72, 0x03, 0xb8, + 0xb5, 0xdd, 0x66, 0xe5, 0x27, 0x0a, 0x8f, 0x08, 0x06, 0xa0, 0x99, 0xc3, 0xbf, 0xea, 0x6f, 0xc5, + 0x96, 0xcb, 0x0f, 0xef, 0xce, 0x54, 0x8e, 0x71, 0xca, 0xb8, 0x1e, 0xa3, 0x86, 0xa0, 0x41, 0x62, + 0xec, 0xc3, 0x7b, 0x55, 0xad, 0x46, 0x7b, 0x2e, 0x19, 0x5c, 0x7c, 0x9f, 0xd8, 0x6b, 0x3d, 0xce, + 0x55, 0x48, 0xb4, 0x2e, 0x90, 0x64, 0x83, 0xd0, 0x3d, 0x05, 0xf2, 0xe2, 0xe3, 0xbe, 0xeb, 0x2c, + 0x1d, 0xd7, 0x82, 0xc2, 0x23, 0x5c, 0x78, 0x3b, 0xcf, 0xe3, 0x98, 0xef, 0x3d, 0xb9, 0xa9, 0xc1, + 0xea, 0x87, 0x9c, 0xe3, 0xcb, 0x34, 0x21, 0x6e, 0x4a, 0xe2, 0x7b, 0x26, 0x13, 0x75, 0xe7, 0x37, + 0xe7, 0x98, 0xc7, 0x4e, 0x6b, 0x5d, 0xad, 0xe2, 0x3a, 0xcd, 0xbf, 0x06, 0xb3, 0xa6, 0x63, 0xc3, + 0xef, 0x5e, 0x70, 0x56, 0x29, 0x1f, 0x06, 0xbf, 0x3a, 0x53, 0xdc, 0xf7, 0xe5, 0xc4, 0x95, 0x26, + 0x36, 0xbc, 0x30, 0x72, 0xc7, 0xa9, 0xdf, 0x46, 0xa8, 0xe2, 0x84, 0x95, 0x6c, 0x40, 0x6e, 0x4a, + 0xfb, 0xb0, 0xd7, 0x71, 0x0e, 0x1b, 0x85, 0xde, 0x27, 0xb1, 0xf0, 0xca, 0xdc, 0xbe, 0xf1, 0x88, + 0x7c, 0x50, 0xeb, 0x69, 0xd1, 0xfe, 0xb1, 0x2d, 0xb0, 0x9f, 0xd8, 0xe2, 0x63, 0x62, 0xe1, 0x40, + 0x1d, 0x49, 0x75, 0xd4, 0xff, 0xcd, 0xdd, 0x4c, 0xb6, 0x09, 0x7b, 0x3e, 0xcb, 0xd4, 0x9c, 0x1f, + 0x80, 0x63, 0xa9, 0xa6, 0xfa, 0x65, 0x8f, 0x48, 0x03, 0x31, 0xd6, 0x21, 0xb9, 0x5a, 0x85, 0x87, + 0xb0, 0x59, 0x87, 0xd1, 0x5d, 0x2c, 0xc5, 0x00, 0x4c, 0x98, 0x8a, 0x7d, 0x82, 0xd2, 0x5a, 0xcc, + 0xd2, 0x31, 0x39, 0x80, 0x05, 0xee, 0x67, 0x5b, 0x9f, 0x0e, 0xe0, 0x67, 0x57, 0x09, 0x04, 0x56, + 0xab, 0xae, 0xdd, 0x8a, 0x0e, 0xd8, 0x65, 0x52, 0x2a, 0x73, 0xf5, 0xbb, 0x27, 0xac, 0x8f, 0x68, + 0x9c, 0x81, 0x91, 0x23, 0xf1, 0xb6, 0xd7, 0xa8, 0x9c, 0xe8, 0x30, 0x12, 0x59, 0xd0, 0xaa, 0x98, + 0x75, 0x19, 0x63, 0x9f, 0x3c, 0x7e, 0x67, 0x35, 0xaf, 0x15, 0xf1, 0xa6, 0x92, 0x12, 0x1c, 0x7d, + 0xa5, 0x03, 0xdd, 0xeb, 0xf0, 0x93, 0xac, 0xbf, 0xcd, 0xc8, 0x31, 0xe2, 0x07, 0x4a, 0x9e, 0x08, + 0x78, 0x90, 0xff, 0xb3, 0x0b, 0x9f, 0x9b, 0x5f, 0x11, 0xf0, 0x13, 0x50, 0x07, 0x6c, 0xee, 0xe2, + 0x77, 0x85, 0xeb, 0x02, 0x0a, 0xff, 0x39, 0xb2, 0x13, 0xae, 0xf2, 0xab, 0x29, 0x2b, 0x52, 0xf8, + 0x15, 0xce, 0x1f, 0x4c, 0x6d, 0x9f, 0x60, 0xfa, 0x7e, 0x79, 0xff, 0xea, 0xf6, 0xee, 0x6e, 0x51, + 0x0a, 0x4f, 0xa3, 0x96, 0x95, 0x05, 0x5d, 0x27, 0x62, 0x27, 0x51, 0x53, 0x8d, 0xe2, 0xfa, 0x30, + 0x2b, 0xc3, 0x9f, 0x59, 0xd9, 0x15, 0x17, 0xf7, 0xb4, 0xbd, 0xcc, 0xd6, 0x2c, 0x13, 0xd3, 0xb5, + 0x12, 0xae, 0x65, 0xff, 0x3a, 0x4d, 0xd7, 0x0a, 0x13, 0xd9, 0xdc, 0xb8, 0x44, 0xd9, 0xb4, 0xa1, + 0xd3, 0xb5, 0x21, 0x9b, 0x7c, 0x29, 0x2f, 0x26, 0x2b, 0x4b, 0x45, 0xc1, 0xca, 0xdd, 0xff, 0x33, + 0xa7, 0x2c, 0x5e, 0xbc, 0x1f, 0xd6, 0x60, 0x76, 0xec, 0x3f, 0x65, 0x2c, 0x5d, 0xd1, 0x43, 0x4c, + 0x78, 0x2e, 0xad, 0xdb, 0xbd, 0x61, 0xb7, 0x7f, 0xdc, 0xde, 0x1e, 0x8e, 0x76, 0xb8, 0x74, 0xe2, + 0x1d, 0x14, 0xff, 0x36, 0x7c, 0x07, 0x90, 0xb9, 0x94, 0x17, 0x47, 0x08, 0x00, 0x00, +}; +const StaticFile index_html PROGMEM = {(sizeof(index_html_content)/sizeof(index_html_content[0])), index_html_content}; + +static const uint8_t app_js_content[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x9d, 0x57, 0x6d, 0x6f, 0xdb, 0x46, + 0x12, 0xfe, 0xde, 0x5f, 0x41, 0x2d, 0x70, 0x06, 0xf7, 0x44, 0xd3, 0x2f, 0xd7, 0x02, 0x85, 0x18, + 0x42, 0x48, 0xda, 0xe4, 0x92, 0x22, 0xa9, 0x8b, 0x24, 0x57, 0x1c, 0x60, 0xf8, 0x82, 0x95, 0x38, + 0xb2, 0x18, 0x53, 0xbb, 0xec, 0xee, 0xd2, 0xb2, 0x4f, 0x11, 0x70, 0x69, 0x0a, 0xb4, 0x40, 0x0b, + 0x04, 0xe8, 0xf7, 0xcb, 0xa7, 0xfe, 0x00, 0x37, 0x77, 0xbe, 0x4b, 0x2e, 0x4d, 0xfa, 0x17, 0xa8, + 0x7f, 0xd4, 0x99, 0x25, 0x29, 0xd1, 0xb2, 0x81, 0x06, 0xf7, 0xc1, 0x12, 0xb5, 0x9c, 0x9d, 0x9d, + 0x79, 0x66, 0x9e, 0x67, 0xc7, 0xa3, 0x42, 0x0e, 0x6d, 0xaa, 0xa4, 0x07, 0x3e, 0xf0, 0x99, 0x06, + 0x5b, 0x68, 0xc9, 0xf6, 0xd5, 0xe0, 0x31, 0x0c, 0xad, 0xb7, 0xe7, 0xbe, 0x0e, 0x58, 0x1c, 0xc7, + 0xd5, 0x63, 0x98, 0x6b, 0x65, 0x95, 0x3d, 0xcd, 0x21, 0xb4, 0xea, 0x81, 0xd5, 0xa9, 0x3c, 0x0c, + 0x87, 0x22, 0xcb, 0x70, 0xef, 0x7c, 0xd4, 0x78, 0xb2, 0x2b, 0x4f, 0x5e, 0xa2, 0x86, 0xc5, 0x04, + 0xa4, 0x0d, 0x0f, 0xc1, 0xde, 0xcc, 0x80, 0x1e, 0x6f, 0x9c, 0xde, 0x49, 0x2e, 0xd8, 0x4b, 0xb2, + 0x87, 0xd0, 0xd8, 0xd3, 0x0c, 0xc2, 0x24, 0x35, 0x79, 0x26, 0x4e, 0x63, 0x26, 0x95, 0x04, 0xb6, + 0x32, 0xd2, 0x2d, 0xa7, 0x80, 0x61, 0xc0, 0x31, 0xba, 0xfa, 0x14, 0x46, 0xa2, 0xc8, 0xec, 0xc6, + 0xc6, 0xfa, 0x8a, 0xcf, 0x03, 0x72, 0xa8, 0xf2, 0x2f, 0xb4, 0xca, 0xc5, 0xa1, 0x20, 0x17, 0x64, + 0xb5, 0xb6, 0xe4, 0xcc, 0x86, 0x42, 0x0e, 0x21, 0xbb, 0x51, 0x0c, 0x06, 0x19, 0xc4, 0x9d, 0x6d, + 0x5c, 0xa9, 0x8e, 0xf9, 0x52, 0x64, 0x05, 0x2e, 0xec, 0x04, 0x9d, 0x9d, 0x55, 0x18, 0x69, 0x3b, + 0x0c, 0x2f, 0x95, 0xc6, 0xd2, 0x6e, 0x35, 0xf2, 0x6e, 0x6a, 0xad, 0x74, 0x1f, 0xc2, 0x09, 0x18, + 0x23, 0x0e, 0xa1, 0x07, 0x5d, 0xd6, 0x8a, 0x5e, 0xd5, 0x29, 0x82, 0xbd, 0x6e, 0x11, 0xb5, 0x41, + 0x61, 0xc1, 0x67, 0x98, 0xaa, 0xc0, 0x33, 0x13, 0x16, 0xac, 0x1e, 0x5b, 0xb0, 0x88, 0x6a, 0x8f, + 0x86, 0x89, 0x3a, 0x86, 0xab, 0xb6, 0xb5, 0x6c, 0x8d, 0xcf, 0x67, 0xa3, 0x56, 0x25, 0x03, 0xcb, + 0x67, 0xb2, 0xc8, 0xb2, 0x4e, 0x1c, 0x23, 0x3a, 0x3e, 0x84, 0xc7, 0x2e, 0x19, 0xcb, 0x03, 0xf2, + 0x3a, 0x3f, 0x16, 0xda, 0x33, 0xf1, 0x76, 0xb4, 0xdc, 0x52, 0xe0, 0xfe, 0x5d, 0xac, 0x72, 0xb7, + 0x6b, 0x36, 0x36, 0xa4, 0x6f, 0x7d, 0x96, 0x29, 0x91, 0x60, 0x75, 0x1f, 0x65, 0x62, 0x00, 0x19, + 0xe3, 0xd5, 0x9e, 0x24, 0x5e, 0xd6, 0x73, 0xa4, 0xf4, 0xc4, 0x84, 0x12, 0xec, 0x54, 0xe9, 0xa3, + 0x47, 0x98, 0x98, 0x45, 0x6b, 0x13, 0x25, 0xa1, 0x48, 0x92, 0x9b, 0x54, 0x89, 0xbb, 0xa9, 0xb1, + 0x20, 0x41, 0xfb, 0xcc, 0x14, 0x83, 0x49, 0x6a, 0x59, 0xe0, 0x37, 0xc7, 0xb5, 0xfb, 0x23, 0x1c, + 0xa7, 0x49, 0x15, 0x5d, 0x88, 0x19, 0x4e, 0x7c, 0xde, 0x4f, 0xc2, 0xdc, 0x1c, 0xd5, 0x4b, 0x19, + 0xc8, 0x43, 0x3b, 0xbe, 0xf6, 0x71, 0xdf, 0x17, 0x19, 0x68, 0x0c, 0xab, 0xfc, 0x67, 0x79, 0x5e, + 0xbe, 0x2c, 0xcf, 0x17, 0xff, 0x28, 0xdf, 0x2e, 0xbe, 0x2f, 0x5f, 0x7b, 0xe5, 0xaf, 0xe5, 0x19, + 0xfe, 0x78, 0x57, 0xbe, 0x59, 0xfc, 0xe0, 0xf9, 0xe5, 0x2f, 0xe5, 0xab, 0xf2, 0x2d, 0xfe, 0xfd, + 0x52, 0x9e, 0xd1, 0x0a, 0x3e, 0x9f, 0x2d, 0x9e, 0x7b, 0xe5, 0xbf, 0xcb, 0x37, 0xee, 0xc5, 0x99, + 0xb7, 0xe9, 0x7d, 0xec, 0x2d, 0x9e, 0x3a, 0x8b, 0x97, 0xb4, 0x0b, 0xff, 0x5e, 0x72, 0xc6, 0x03, + 0xea, 0x2f, 0xde, 0xdb, 0xdc, 0x41, 0x10, 0x92, 0xd0, 0x18, 0x0c, 0xca, 0x40, 0x86, 0x1d, 0x0f, + 0xc9, 0x1d, 0x99, 0xc0, 0xc9, 0x85, 0x00, 0xbc, 0xf2, 0x25, 0x9e, 0xfd, 0x33, 0x1e, 0x7b, 0xe6, + 0x7c, 0x2e, 0xbe, 0x2e, 0xdf, 0x2d, 0xbe, 0x2d, 0xff, 0x87, 0x8f, 0x78, 0xd2, 0xbb, 0xc5, 0xd3, + 0xc5, 0xd7, 0x8b, 0x67, 0x14, 0xd8, 0xd2, 0xef, 0xb1, 0x4a, 0x13, 0x6c, 0x02, 0x74, 0xea, 0xb0, + 0xe0, 0xbd, 0xa5, 0xbb, 0x1f, 0x29, 0x1b, 0xdc, 0xf5, 0x0a, 0x9d, 0x9c, 0x7b, 0x63, 0x35, 0xc1, + 0xae, 0x4a, 0x9a, 0x7d, 0x73, 0xce, 0x03, 0xdc, 0x33, 0x56, 0xd3, 0x47, 0x84, 0xc9, 0x65, 0x68, + 0x87, 0x63, 0x21, 0x0f, 0x61, 0x0d, 0xda, 0x0a, 0xc0, 0x8b, 0xbd, 0x46, 0x94, 0x65, 0xd8, 0xda, + 0x56, 0x68, 0x64, 0x62, 0x38, 0x1c, 0xc3, 0xf0, 0x08, 0x92, 0x3e, 0xb3, 0x70, 0x62, 0x59, 0x8f, + 0xe5, 0xc2, 0x18, 0x2c, 0x24, 0xf5, 0x54, 0x75, 0x24, 0x01, 0xf0, 0x9e, 0xc7, 0x51, 0x63, 0xd8, + 0x78, 0xe9, 0xfa, 0x02, 0x6c, 0xd1, 0xe6, 0xce, 0xb2, 0x05, 0xeb, 0xf7, 0x2a, 0xa7, 0x8d, 0x66, + 0xdf, 0x1e, 0xe0, 0x6a, 0xab, 0xd6, 0x31, 0x63, 0x55, 0xc2, 0xe2, 0xb1, 0x38, 0x21, 0xb9, 0xf0, + 0xd9, 0x16, 0xb2, 0xcb, 0x16, 0x86, 0x05, 0xb3, 0x79, 0xeb, 0x48, 0x1b, 0x48, 0x3e, 0xb3, 0xfa, + 0x74, 0x96, 0x8e, 0x7c, 0xcb, 0xed, 0x58, 0xab, 0xa9, 0x67, 0x23, 0xf0, 0x5d, 0x2b, 0x05, 0x32, + 0x94, 0x2a, 0x81, 0x47, 0x69, 0xf2, 0xe4, 0x09, 0x11, 0x00, 0x09, 0x5e, 0x1d, 0x12, 0xac, 0x7e, + 0x55, 0x35, 0xa8, 0x17, 0xb0, 0xf3, 0xe7, 0x43, 0x61, 0x87, 0x63, 0xf4, 0x35, 0xab, 0x8a, 0x92, + 0xe2, 0x23, 0x9f, 0xaf, 0x87, 0x82, 0x32, 0xb1, 0x16, 0x88, 0x63, 0x59, 0x1d, 0x08, 0xd4, 0x81, + 0x40, 0x54, 0x83, 0x97, 0x4a, 0x44, 0xec, 0xf6, 0xc3, 0x7b, 0x77, 0x31, 0xaf, 0x08, 0xc9, 0xe2, + 0x13, 0x4c, 0x12, 0x39, 0x27, 0xaf, 0xd9, 0x30, 0x43, 0x44, 0xeb, 0xde, 0x8e, 0x64, 0xb7, 0x5b, + 0x61, 0xa8, 0xe3, 0xea, 0xc5, 0xbe, 0x3c, 0xd8, 0xdf, 0x3e, 0x08, 0x54, 0xeb, 0xe7, 0xce, 0x41, + 0xe3, 0x56, 0xe4, 0x39, 0xc8, 0xc4, 0x97, 0x30, 0xf5, 0xf6, 0x1c, 0x90, 0xbe, 0xee, 0x32, 0xcf, + 0x67, 0x5d, 0x85, 0x5f, 0xc9, 0x8d, 0x09, 0x67, 0x81, 0xc6, 0xe0, 0x85, 0x5f, 0xd9, 0xb7, 0xf3, + 0x83, 0x55, 0x7e, 0x50, 0xe5, 0x37, 0xbf, 0xc0, 0x7f, 0x8a, 0x01, 0xd6, 0x09, 0x5e, 0xe4, 0x89, + 0xb0, 0xb0, 0xe2, 0x37, 0xbc, 0x17, 0xbf, 0x11, 0x17, 0xc4, 0x44, 0xe3, 0x77, 0xd0, 0x81, 0x70, + 0x94, 0x66, 0xd5, 0x87, 0xa9, 0x73, 0xe6, 0x35, 0xf9, 0x1b, 0x0e, 0xfc, 0x84, 0x3c, 0x7a, 0x5d, + 0xbe, 0xf1, 0x90, 0x8b, 0x3f, 0x23, 0xa1, 0x90, 0x91, 0xc8, 0xcb, 0x73, 0xe2, 0x31, 0x71, 0xf7, + 0xed, 0x1a, 0xe1, 0x90, 0x1c, 0x9d, 0x9d, 0x08, 0x15, 0xb5, 0x21, 0x53, 0x54, 0x41, 0x4b, 0x98, + 0xfc, 0xf5, 0xde, 0xdd, 0xdb, 0xd6, 0xe6, 0xf7, 0xe1, 0xab, 0x02, 0x8c, 0x0d, 0x84, 0x5b, 0xbc, + 0x85, 0x99, 0x7c, 0x2a, 0xac, 0x88, 0x9a, 0x63, 0x1b, 0x14, 0x19, 0x05, 0x45, 0xac, 0x58, 0x45, + 0x88, 0xc8, 0x73, 0xec, 0xa1, 0x22, 0x27, 0xfd, 0xbb, 0x22, 0x57, 0xbc, 0xff, 0x0e, 0x35, 0x4a, + 0xfc, 0x5a, 0xb6, 0x2e, 0x82, 0x40, 0xc7, 0x6b, 0xae, 0x42, 0x93, 0xfe, 0x1d, 0x22, 0x49, 0x95, + 0x44, 0x77, 0x90, 0x5c, 0xd3, 0xfd, 0x7b, 0xc2, 0x8e, 0x43, 0xad, 0x0a, 0x3c, 0xbe, 0x59, 0xdd, + 0xd2, 0x7f, 0xdc, 0xd9, 0xde, 0xe6, 0x78, 0xa3, 0xde, 0x4a, 0x4f, 0x20, 0xf1, 0x77, 0x79, 0x0f, + 0x7f, 0x07, 0x4d, 0x7e, 0xad, 0x56, 0x92, 0x5d, 0xf6, 0x07, 0x46, 0x8d, 0x29, 0x43, 0x25, 0x35, + 0x88, 0xe4, 0x94, 0x18, 0x02, 0x15, 0x2d, 0xe3, 0x65, 0x40, 0x4d, 0x2d, 0x59, 0xf9, 0x62, 0x1d, + 0x50, 0xc2, 0xf2, 0xbf, 0x88, 0xa2, 0x93, 0xd0, 0xc5, 0x77, 0x6e, 0xf1, 0x5d, 0xe0, 0x2d, 0x9e, + 0x39, 0xd1, 0x22, 0x1d, 0x7d, 0x4d, 0x4f, 0xa4, 0x8d, 0x24, 0xad, 0xa4, 0xb3, 0xe7, 0x6e, 0xc3, + 0xbf, 0xd0, 0xfc, 0x59, 0xf9, 0x1f, 0x7c, 0x3a, 0x47, 0xc3, 0xa7, 0x8b, 0xe7, 0x2c, 0xc2, 0x12, + 0x7f, 0x88, 0x72, 0x29, 0x43, 0x17, 0xc9, 0x03, 0x8a, 0x84, 0x13, 0x1d, 0x48, 0x43, 0x3f, 0x7b, + 0xb0, 0xf7, 0x79, 0x98, 0x0b, 0x6d, 0xc0, 0xa7, 0xf7, 0x26, 0x47, 0xca, 0xc3, 0x43, 0x14, 0x1b, + 0x4e, 0xbf, 0xf0, 0x86, 0xee, 0x57, 0xc5, 0x07, 0xde, 0x6b, 0xba, 0xe0, 0x05, 0x86, 0xf3, 0x0a, + 0xe3, 0x75, 0x32, 0x7a, 0x45, 0x27, 0xb0, 0x4b, 0x44, 0xc5, 0x9b, 0x6c, 0xee, 0xa0, 0x00, 0xba, + 0x7c, 0xe3, 0xb6, 0x28, 0xb5, 0x3b, 0x9d, 0x4c, 0xb0, 0xde, 0x3e, 0xfb, 0x62, 0xef, 0xc1, 0x43, + 0x16, 0xd8, 0x46, 0x89, 0x84, 0xb3, 0x26, 0x30, 0x0d, 0x75, 0x83, 0xa0, 0xbe, 0x22, 0x70, 0xeb, + 0x22, 0xbe, 0x8f, 0xfc, 0x55, 0x8d, 0xbe, 0xf4, 0x78, 0xa1, 0xc9, 0x67, 0xab, 0xb6, 0xbc, 0x85, + 0xeb, 0xf7, 0x11, 0x24, 0xd0, 0x11, 0x85, 0x4b, 0x65, 0x5f, 0xaf, 0x96, 0x8d, 0xa7, 0xa9, 0x4c, + 0xd4, 0x34, 0x9c, 0x24, 0x1f, 0x55, 0x90, 0x21, 0x48, 0x3c, 0x5a, 0x9f, 0x19, 0xaa, 0x98, 0x71, + 0x62, 0xd8, 0xaa, 0x98, 0xd9, 0x47, 0xf3, 0x98, 0x75, 0xab, 0x8b, 0xbd, 0x21, 0xc4, 0x95, 0xa0, + 0x34, 0x98, 0x5c, 0x04, 0x7a, 0xf1, 0x2d, 0xdd, 0x3b, 0x35, 0xd5, 0x16, 0xdf, 0x54, 0x44, 0xa4, + 0x8b, 0x8b, 0x7c, 0x50, 0x5d, 0xaf, 0x9b, 0x1b, 0xa9, 0x14, 0xfa, 0xb4, 0x1a, 0xf6, 0xd6, 0x52, + 0x25, 0xb6, 0x38, 0x21, 0xe9, 0xb4, 0x8e, 0x69, 0xcd, 0x81, 0x28, 0xb2, 0x81, 0x0e, 0xd2, 0x4a, + 0x0d, 0x62, 0x5d, 0x49, 0x72, 0xc0, 0x1a, 0x0b, 0xd6, 0x89, 0xe9, 0x5e, 0xc2, 0xe9, 0x29, 0xad, + 0xe5, 0x93, 0xc0, 0x72, 0x93, 0x14, 0x82, 0x8d, 0x63, 0xe5, 0x40, 0x0c, 0x8f, 0xbc, 0x49, 0x61, + 0xac, 0x37, 0x00, 0x4f, 0x78, 0xcb, 0x7d, 0x9c, 0x7a, 0xaf, 0x23, 0x2f, 0x6f, 0x92, 0xca, 0x2b, + 0x74, 0xe6, 0x99, 0x1c, 0x86, 0xe9, 0x28, 0xa5, 0x19, 0x29, 0x32, 0xd3, 0xb4, 0x6e, 0x9a, 0xa1, + 0x30, 0xc0, 0xfe, 0x7c, 0xf3, 0x21, 0xeb, 0x91, 0x60, 0xfb, 0xa8, 0x93, 0x8d, 0x2e, 0x2b, 0x9c, + 0xe4, 0x3c, 0xcd, 0x75, 0x38, 0x16, 0x66, 0x6f, 0x2a, 0x69, 0x42, 0x44, 0xa8, 0x4e, 0x7d, 0xc5, + 0xf1, 0x8e, 0x92, 0xdd, 0xd8, 0x77, 0x73, 0x81, 0x44, 0x26, 0xe2, 0x9d, 0xb6, 0x37, 0xf2, 0x59, + 0x9f, 0xf1, 0x3e, 0x7e, 0xf4, 0xd8, 0x06, 0xe3, 0x5d, 0x90, 0x43, 0xbc, 0x72, 0xfe, 0x72, 0xff, + 0xce, 0x27, 0x6a, 0x82, 0x7d, 0x8e, 0x2d, 0x83, 0x1b, 0xbb, 0x0c, 0xcb, 0x72, 0xc5, 0x1b, 0xbd, + 0xaf, 0x0e, 0x38, 0x8f, 0x06, 0x88, 0xed, 0x51, 0xe4, 0x22, 0x72, 0x5d, 0xb9, 0x0c, 0xc9, 0x35, + 0x83, 0x88, 0xf7, 0x0f, 0xa2, 0xf7, 0x09, 0x4e, 0x84, 0x79, 0x61, 0x50, 0xda, 0xff, 0x8f, 0x08, + 0x74, 0x2c, 0xc2, 0xc7, 0x2a, 0x45, 0x5a, 0x60, 0x0e, 0xf3, 0x7a, 0x24, 0xbc, 0xac, 0xa1, 0x8d, + 0x68, 0x9a, 0x8a, 0x43, 0x74, 0xf9, 0x06, 0x55, 0xc8, 0xb1, 0xbb, 0xd8, 0x0d, 0x35, 0x68, 0x6d, + 0x7b, 0xdb, 0xb5, 0xb8, 0xcf, 0x3e, 0x51, 0x12, 0x19, 0x63, 0x37, 0xab, 0xa1, 0x83, 0xa1, 0xda, + 0x66, 0xe9, 0xd0, 0x0d, 0xdc, 0x5b, 0x27, 0x9b, 0xd3, 0xe9, 0x74, 0x93, 0xee, 0x96, 0x4d, 0x2c, + 0x54, 0x15, 0x1d, 0x8d, 0x3b, 0xe6, 0x77, 0x04, 0xad, 0xd6, 0x1a, 0xd3, 0xd6, 0x1a, 0x5a, 0x64, + 0xf5, 0x88, 0x80, 0x10, 0xe1, 0xe4, 0xda, 0xd9, 0xfa, 0xdb, 0xee, 0x93, 0x9d, 0xdd, 0xdd, 0x3f, + 0x6d, 0x85, 0x16, 0xe3, 0xf1, 0x31, 0x38, 0xf7, 0x9a, 0x5f, 0xee, 0x94, 0x31, 0xa6, 0xe8, 0xd1, + 0xe9, 0x1e, 0xeb, 0x2e, 0xcd, 0xa2, 0xd4, 0x77, 0x1d, 0xda, 0x52, 0x2f, 0x73, 0x51, 0xbd, 0x48, + 0x71, 0xcc, 0x95, 0x8a, 0x83, 0x5a, 0x53, 0x8d, 0x14, 0x64, 0xe0, 0x04, 0xc5, 0xf5, 0x1a, 0xa1, + 0xd4, 0xa7, 0xf5, 0x9e, 0xc6, 0x2c, 0xe7, 0x35, 0xc9, 0x69, 0xb0, 0x88, 0x67, 0xc8, 0xa3, 0x9e, + 0x0d, 0x07, 0x29, 0xdd, 0x05, 0x81, 0xb3, 0xe6, 0x41, 0xae, 0x4c, 0x7b, 0xd1, 0x21, 0x8d, 0x87, + 0xe2, 0x7f, 0x2a, 0xf5, 0xd6, 0x54, 0xa6, 0xf6, 0x7a, 0x9e, 0xb7, 0xc1, 0xc1, 0xe9, 0xdf, 0xdd, + 0xf3, 0xd1, 0x07, 0xbf, 0x01, 0xdd, 0x89, 0x77, 0x95, 0xce, 0x0d, 0x00, 0x00, +}; +const StaticFile app_js PROGMEM = {(sizeof(app_js_content)/sizeof(app_js_content[0])), app_js_content}; + +static const uint8_t md5_js_content[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xad, 0x59, 0x79, 0x73, 0x1b, 0xb7, + 0x15, 0xff, 0xbf, 0x9f, 0x42, 0xe2, 0x4c, 0x39, 0xbb, 0xb3, 0x2b, 0x05, 0xf7, 0x61, 0x72, 0xe5, + 0x89, 0x93, 0x1e, 0xe9, 0x95, 0xb6, 0x69, 0xd2, 0x83, 0x43, 0xcd, 0xd0, 0xd2, 0xd2, 0xbb, 0x89, + 0x42, 0xaa, 0x58, 0xd0, 0xb2, 0x62, 0xd2, 0x9f, 0xbd, 0x0f, 0xd8, 0x0b, 0x4b, 0x89, 0x3a, 0xac, + 0x8e, 0x2d, 0x2c, 0x08, 0xbc, 0xf7, 0x80, 0xdf, 0xbb, 0x70, 0x1d, 0x2f, 0x37, 0xab, 0x0b, 0x5b, + 0xae, 0x57, 0x51, 0xfc, 0x71, 0xb4, 0xa9, 0xf2, 0xa3, 0xca, 0x9a, 0xf2, 0xc2, 0x8e, 0x26, 0xef, + 0x17, 0xe6, 0xc8, 0xa6, 0x26, 0x1b, 0x95, 0xab, 0xeb, 0x8d, 0x3d, 0x2a, 0xab, 0xa3, 0x72, 0xf5, + 0x7e, 0x71, 0x55, 0x5e, 0x1e, 0xd9, 0xdb, 0xeb, 0x7c, 0x94, 0x96, 0xd9, 0xfb, 0x35, 0xfc, 0x40, + 0xc7, 0x59, 0x76, 0x53, 0xae, 0x2e, 0xd7, 0x37, 0xa7, 0x5f, 0x1a, 0xb3, 0xb8, 0x7d, 0xb3, 0x59, + 0x2e, 0x73, 0x93, 0x16, 0xd9, 0x08, 0x61, 0x42, 0x19, 0x17, 0x52, 0xe9, 0xc5, 0xdb, 0x8b, 0xcb, + 0x7c, 0x39, 0x3a, 0xad, 0xae, 0xaf, 0x4a, 0x1b, 0x8d, 0x46, 0x71, 0x5a, 0x65, 0x33, 0x4c, 0x54, + 0x4a, 0x89, 0x14, 0x2a, 0x55, 0x54, 0x29, 0x81, 0x54, 0x7a, 0x42, 0x30, 0x93, 0x4c, 0x51, 0xc1, + 0xd4, 0x3c, 0xcd, 0xb3, 0x19, 0x4a, 0x55, 0x8a, 0x45, 0x4a, 0xd8, 0x3c, 0x5d, 0x64, 0xb3, 0x51, + 0x91, 0x7f, 0x18, 0xa5, 0xa3, 0x85, 0x1b, 0x04, 0xbe, 0x97, 0xe5, 0xbb, 0xbc, 0xb2, 0x50, 0x79, + 0xeb, 0x07, 0x6c, 0x7b, 0xde, 0xb4, 0xbf, 0xde, 0x2e, 0xaa, 0x5c, 0xb0, 0xd1, 0x3c, 0x5d, 0x66, + 0xa3, 0x2f, 0xdf, 0x7c, 0xf5, 0xf5, 0x6f, 0x7e, 0xfb, 0xbb, 0xdf, 0x7f, 0xf3, 0x87, 0x3f, 0xfe, + 0xe9, 0xcf, 0x7f, 0xf9, 0xf6, 0xaf, 0x7f, 0xfb, 0xfb, 0x77, 0xff, 0xf8, 0xfe, 0x87, 0x7f, 0xfe, + 0xeb, 0xdf, 0xff, 0xa9, 0x67, 0xf6, 0xae, 0x28, 0x7f, 0xfc, 0xe9, 0xea, 0xe7, 0xd5, 0xfa, 0xfa, + 0xbf, 0xa6, 0xb2, 0x9b, 0xf7, 0x37, 0x1f, 0x6e, 0x7f, 0xe9, 0x67, 0x9f, 0x7c, 0x11, 0xce, 0x7c, + 0x95, 0xcd, 0xe6, 0x93, 0x72, 0x19, 0x95, 0xf1, 0x47, 0xa7, 0xa1, 0x75, 0xb6, 0xca, 0x6f, 0x8e, + 0x02, 0xe4, 0x91, 0x50, 0xf1, 0xc4, 0xfa, 0xd6, 0xef, 0xcb, 0x95, 0x55, 0xbe, 0x2b, 0x5a, 0x3b, + 0xc6, 0xb6, 0x8d, 0x92, 0xb6, 0x71, 0xe7, 0x2b, 0xa7, 0x65, 0xe5, 0xbf, 0xdb, 0x6d, 0x34, 0xf8, + 0x9d, 0x75, 0xa6, 0xb1, 0xf1, 0x47, 0x93, 0xdb, 0x8d, 0x59, 0x8d, 0x66, 0xeb, 0xb7, 0x3f, 0xe6, + 0x17, 0xb6, 0x1e, 0x71, 0x3e, 0xca, 0xb2, 0xec, 0x5b, 0xdf, 0x70, 0x7a, 0x6d, 0xd6, 0x76, 0xed, + 0x2c, 0x73, 0x6a, 0xd7, 0xdf, 0x81, 0x09, 0x57, 0xef, 0x4e, 0x2f, 0x16, 0x57, 0x57, 0xc0, 0xba, + 0x8b, 0xd3, 0x72, 0x3c, 0x3e, 0x0e, 0xe6, 0x08, 0x03, 0xfc, 0x50, 0xe6, 0x37, 0xe3, 0x71, 0x74, + 0xb7, 0xf1, 0xbe, 0x41, 0xeb, 0x31, 0x61, 0x30, 0x27, 0x7f, 0xbd, 0x3c, 0xb2, 0xe3, 0xb1, 0x3d, + 0xad, 0xf5, 0xde, 0xd7, 0x4e, 0x2f, 0xd6, 0x2b, 0xf0, 0x9d, 0xcd, 0x85, 0x5d, 0x1b, 0x98, 0x56, + 0x20, 0x79, 0x17, 0x7b, 0x67, 0xba, 0x0f, 0xcf, 0x51, 0xd7, 0x64, 0xba, 0x26, 0xa7, 0xa6, 0x4d, + 0x74, 0x8c, 0xe2, 0xd3, 0xcd, 0xf5, 0xe5, 0xc2, 0xe6, 0xd0, 0x35, 0xb3, 0xf3, 0x28, 0xde, 0xed, + 0x26, 0x2d, 0x35, 0xf4, 0x03, 0x3d, 0x58, 0xc1, 0xc4, 0xab, 0x19, 0x9a, 0x67, 0xab, 0x19, 0x16, + 0xbe, 0x74, 0x05, 0x71, 0x05, 0x75, 0x05, 0x73, 0x05, 0x77, 0x85, 0xef, 0x95, 0xae, 0x50, 0xae, + 0xd0, 0x9e, 0xb8, 0x66, 0xf4, 0x3c, 0xd8, 0x33, 0x61, 0xcf, 0x85, 0x3d, 0x1b, 0x06, 0x3e, 0x94, + 0xda, 0xa2, 0xac, 0x4e, 0xdf, 0x5e, 0xad, 0x2f, 0x7e, 0xaa, 0xb2, 0x55, 0xf3, 0xcb, 0x63, 0x52, + 0x99, 0x9d, 0xe4, 0x57, 0x10, 0x2e, 0xbd, 0x2b, 0x14, 0xf7, 0xbb, 0x42, 0xc8, 0xb3, 0xe7, 0x15, + 0x45, 0x3c, 0x1c, 0x60, 0xcf, 0x3f, 0x8a, 0x78, 0xe7, 0x87, 0x08, 0x69, 0x20, 0x28, 0x1e, 0xf9, + 0x37, 0xaf, 0x87, 0x2c, 0x50, 0x56, 0x7f, 0x71, 0xf3, 0x25, 0xcd, 0x97, 0xd6, 0xdf, 0xca, 0x2e, + 0x8c, 0xad, 0xab, 0x6f, 0x6f, 0x6d, 0x5e, 0x35, 0xbd, 0x6f, 0x7c, 0xbd, 0x01, 0xbe, 0x2c, 0x57, + 0x10, 0xf0, 0xbf, 0xe4, 0x97, 0x4d, 0xe7, 0xa2, 0x2a, 0xa0, 0x7e, 0x8c, 0xdb, 0x5e, 0x08, 0x96, + 0xec, 0x18, 0xed, 0x36, 0x81, 0xf3, 0xd5, 0x26, 0x1b, 0x58, 0x1a, 0x34, 0x74, 0x3c, 0x14, 0xd7, + 0xe8, 0x0b, 0xb2, 0x40, 0xeb, 0x50, 0x2e, 0xa2, 0x46, 0x95, 0x77, 0xdb, 0x11, 0x24, 0x94, 0xca, + 0x73, 0xb5, 0x7e, 0xe7, 0x1b, 0x6c, 0x61, 0xd6, 0x37, 0x47, 0xc6, 0x11, 0xae, 0x36, 0x57, 0x57, + 0xe0, 0x61, 0x36, 0x6c, 0x2b, 0x9d, 0x23, 0x1e, 0xf4, 0xc0, 0xf8, 0x4e, 0x40, 0xda, 0xb8, 0x33, + 0xdf, 0xf1, 0x30, 0xea, 0xa0, 0x6b, 0xbb, 0x05, 0x71, 0x77, 0x43, 0x03, 0x7a, 0xe2, 0x6e, 0xcc, + 0xc2, 0x21, 0x5f, 0xae, 0x4d, 0xe4, 0xa0, 0x2c, 0xd2, 0x25, 0xc4, 0x37, 0x4a, 0xd7, 0x99, 0x3d, + 0xbd, 0xca, 0x57, 0xef, 0x6c, 0x91, 0xde, 0x66, 0x81, 0xd9, 0xd2, 0x4d, 0x16, 0xfa, 0xc1, 0x64, + 0x35, 0x5d, 0x4f, 0x3c, 0xc4, 0x40, 0xaf, 0x10, 0x8d, 0x7b, 0x5a, 0xbe, 0x75, 0xae, 0x7d, 0xeb, + 0x5c, 0x3b, 0xf5, 0xa5, 0xab, 0xbb, 0x82, 0xb8, 0x82, 0xba, 0x82, 0xb9, 0x82, 0xbb, 0xc2, 0xf7, + 0x4a, 0x57, 0x28, 0x57, 0x68, 0x4f, 0x5c, 0xb3, 0x7b, 0x1e, 0xec, 0x99, 0xb0, 0xe7, 0xc2, 0x9e, + 0xcd, 0x3b, 0x78, 0x9c, 0x16, 0xb1, 0xf7, 0x60, 0x87, 0x64, 0x19, 0x78, 0x86, 0x9b, 0xe2, 0x78, + 0xbc, 0x9c, 0x0a, 0x36, 0x49, 0x92, 0x55, 0xbc, 0x99, 0x2d, 0x93, 0x64, 0x9e, 0xd9, 0xd9, 0x6a, + 0x5e, 0xeb, 0xed, 0x11, 0xfa, 0xdb, 0xd9, 0xf2, 0xec, 0x8c, 0xcc, 0xb7, 0x9e, 0x63, 0x3a, 0xcd, + 0x67, 0x74, 0xec, 0x04, 0x04, 0x21, 0xf3, 0x88, 0x80, 0x68, 0x01, 0xaa, 0xbc, 0x28, 0x16, 0xe6, + 0xab, 0xf5, 0x65, 0xfe, 0xa5, 0x8d, 0x56, 0x71, 0x3c, 0x85, 0x15, 0xe3, 0x75, 0x33, 0x91, 0xc5, + 0xab, 0xc5, 0x94, 0x20, 0xa6, 0x5e, 0x47, 0x4d, 0x03, 0xd6, 0x64, 0xbb, 0x38, 0x3b, 0x13, 0x69, + 0xfb, 0x9b, 0xa8, 0xad, 0xa0, 0xe3, 0x45, 0x0c, 0x84, 0x9c, 0x13, 0x2d, 0xb6, 0xd0, 0x9d, 0x71, + 0x49, 0x19, 0xeb, 0x78, 0x08, 0x61, 0x8e, 0x07, 0x93, 0x90, 0xc9, 0x09, 0x19, 0x0b, 0x7a, 0x57, + 0x0e, 0xcc, 0x48, 0x70, 0x4e, 0x45, 0x12, 0x45, 0x18, 0x11, 0xd7, 0x34, 0x9d, 0x62, 0xb4, 0xf5, + 0xf5, 0xc1, 0x54, 0xdd, 0xf4, 0xe3, 0x96, 0x9f, 0x30, 0xe4, 0xc7, 0x50, 0x7b, 0x63, 0x60, 0xb2, + 0x37, 0xc8, 0xa1, 0x71, 0x9f, 0xa4, 0xee, 0x43, 0xda, 0xea, 0xcc, 0xb0, 0xe8, 0x6d, 0xd0, 0x69, + 0xae, 0xeb, 0x8c, 0x5a, 0xe5, 0xc5, 0x3d, 0x55, 0x1a, 0xf4, 0xb6, 0x53, 0xe9, 0x7b, 0xef, 0xd5, + 0x6a, 0xcf, 0xd1, 0x29, 0xf6, 0xb0, 0xc0, 0x06, 0xee, 0x33, 0x46, 0x7c, 0xa6, 0xfe, 0x83, 0xd9, + 0xb4, 0x26, 0x78, 0x70, 0x36, 0xde, 0x20, 0xff, 0xc7, 0xf9, 0xd6, 0x59, 0xf8, 0x6a, 0x51, 0x59, + 0x97, 0x52, 0xbf, 0x59, 0x5d, 0xe6, 0x1f, 0xb2, 0x65, 0xda, 0xe7, 0xdb, 0x24, 0x5b, 0x9e, 0xf4, + 0xf6, 0x4c, 0x97, 0x67, 0x99, 0x00, 0x1d, 0x06, 0xa9, 0x79, 0x79, 0x22, 0x58, 0xda, 0x65, 0x84, + 0x28, 0x4e, 0x07, 0xd9, 0x01, 0xc5, 0xaf, 0x42, 0xda, 0x5d, 0xb3, 0x76, 0xf6, 0xf2, 0xcf, 0x18, + 0xd1, 0x4c, 0x0b, 0x49, 0x34, 0xef, 0x32, 0xcb, 0x9b, 0x7a, 0xe0, 0x9e, 0xe8, 0x8b, 0x8e, 0x48, + 0x4c, 0xa7, 0x28, 0xdd, 0x5f, 0x0d, 0x7c, 0xf5, 0xd7, 0x3d, 0x4d, 0x3d, 0x87, 0xdd, 0x2e, 0x0d, + 0x33, 0x7e, 0x9b, 0xd5, 0xb3, 0x60, 0x23, 0x79, 0x5f, 0xca, 0xdf, 0x5b, 0x51, 0x8e, 0x51, 0xbd, + 0xc3, 0x1c, 0xa4, 0x49, 0x93, 0xdd, 0xd5, 0xda, 0xc4, 0xce, 0x4c, 0xad, 0xe7, 0x0a, 0x74, 0x6b, + 0xe6, 0xa9, 0x01, 0x8f, 0x13, 0xc3, 0x6c, 0xb9, 0xdd, 0x0e, 0x14, 0xe5, 0xf2, 0xa6, 0xf5, 0x79, + 0xd3, 0x97, 0xae, 0xee, 0x0a, 0xe2, 0x0a, 0xea, 0x0a, 0xe6, 0x0a, 0xee, 0x0a, 0xdf, 0x2b, 0x5d, + 0xa1, 0x5c, 0xa1, 0x3d, 0x71, 0xcd, 0xee, 0x79, 0xb0, 0x67, 0xc2, 0x9e, 0x0b, 0x7b, 0xb6, 0x26, + 0x6f, 0x36, 0x3f, 0x3b, 0x35, 0x4d, 0xa7, 0x34, 0xad, 0x3b, 0x03, 0x5d, 0x43, 0xe3, 0x36, 0x30, + 0x09, 0xc0, 0xd0, 0xa1, 0x49, 0xf7, 0x34, 0xe9, 0x1a, 0x43, 0x2d, 0x36, 0x3b, 0xf0, 0xb4, 0x4c, + 0x61, 0xa9, 0x4c, 0x73, 0xd8, 0x04, 0x07, 0xba, 0x9a, 0xf4, 0x6b, 0xf0, 0x6b, 0x93, 0x45, 0x91, + 0xfb, 0xb3, 0xfe, 0x6f, 0x01, 0xf0, 0x4f, 0x84, 0x42, 0x4a, 0x0a, 0x4d, 0x25, 0xf8, 0xa4, 0xdc, + 0x5a, 0x37, 0x34, 0x8f, 0x4f, 0x88, 0xc4, 0x92, 0x52, 0x25, 0x35, 0x58, 0x3b, 0x3e, 0x8f, 0x4a, + 0xa0, 0x87, 0xbf, 0xbe, 0xf9, 0x3c, 0x2a, 0xa0, 0x09, 0xfe, 0x4e, 0xa0, 0x81, 0x70, 0xc5, 0xb0, + 0x66, 0xe7, 0x04, 0x21, 0x46, 0xb1, 0x42, 0x12, 0x8f, 0x6d, 0x9c, 0x2c, 0x40, 0x97, 0x27, 0x18, + 0x4b, 0x45, 0x91, 0x44, 0x2e, 0xa4, 0x30, 0xd9, 0x16, 0x4e, 0x3c, 0x8a, 0x13, 0xeb, 0xc4, 0x8e, + 0x43, 0x79, 0xb0, 0x62, 0x02, 0x07, 0x71, 0x1c, 0x44, 0x30, 0x60, 0x92, 0xdc, 0xb1, 0xc8, 0x6d, + 0x09, 0x2c, 0x98, 0xc7, 0x49, 0x51, 0xb3, 0x14, 0x0d, 0x25, 0x05, 0x4a, 0x8a, 0x05, 0xe1, 0x9a, + 0x20, 0x0d, 0x94, 0x84, 0x6c, 0x8d, 0xa3, 0x04, 0xe1, 0x25, 0x50, 0xbe, 0x8a, 0x1a, 0x7f, 0x29, + 0x50, 0xeb, 0x2b, 0x05, 0x86, 0x93, 0x48, 0xb3, 0xc3, 0x49, 0xbd, 0x22, 0x92, 0x56, 0x13, 0x89, + 0xc7, 0xd2, 0xec, 0x7a, 0xe2, 0x73, 0x33, 0x8e, 0xca, 0xf3, 0xc2, 0x0f, 0x13, 0x28, 0x48, 0x84, + 0x0a, 0x4a, 0x4c, 0xa0, 0x18, 0x60, 0x37, 0x8d, 0x42, 0xa0, 0x5a, 0x9e, 0xdb, 0x31, 0xfc, 0x2e, + 0xe3, 0x46, 0x05, 0x54, 0x69, 0x2e, 0x18, 0x57, 0xe2, 0x5e, 0x15, 0xd8, 0x73, 0xd3, 0x20, 0x4f, + 0x04, 0x12, 0x18, 0x71, 0x85, 0xf5, 0xa3, 0xc0, 0x11, 0x63, 0x9c, 0x70, 0x4a, 0xd1, 0x5d, 0xe0, + 0xf1, 0x5d, 0x68, 0x45, 0x08, 0x88, 0x01, 0xbb, 0x14, 0x0c, 0x2b, 0xa5, 0xe5, 0xe7, 0x00, 0xe2, + 0xf3, 0x04, 0x83, 0x9d, 0x91, 0x42, 0x8c, 0x3c, 0x8c, 0x48, 0xc0, 0x48, 0x0c, 0x9c, 0x83, 0x62, + 0xca, 0xf0, 0x83, 0x90, 0xe4, 0xfc, 0x84, 0x71, 0x89, 0xb8, 0x56, 0xf4, 0x2e, 0xa0, 0x47, 0xf0, + 0x28, 0x98, 0x8f, 0x94, 0x08, 0x51, 0xce, 0xf0, 0x67, 0x59, 0x48, 0xc3, 0x34, 0xb5, 0x73, 0x60, + 0xc6, 0xb0, 0x7c, 0x10, 0x10, 0x44, 0xfb, 0x09, 0x23, 0xc8, 0xe7, 0xf6, 0xc3, 0x60, 0xb0, 0xf3, + 0x7a, 0xad, 0x11, 0x43, 0x30, 0x21, 0xf2, 0x6c, 0x3c, 0x90, 0x46, 0x12, 0x08, 0x21, 0x26, 0x10, + 0x15, 0x8a, 0x7c, 0x96, 0xcb, 0x81, 0x87, 0x30, 0x04, 0x3a, 0xc7, 0x08, 0x3f, 0x8c, 0xc7, 0xf9, + 0x02, 0x47, 0x60, 0x4d, 0x42, 0x34, 0x7a, 0x18, 0x94, 0x37, 0x3b, 0x15, 0x9c, 0x72, 0x4a, 0xee, + 0x09, 0xb7, 0x1e, 0x54, 0x3b, 0x25, 0x98, 0xcd, 0xb8, 0x87, 0x08, 0xe8, 0x06, 0x31, 0x81, 0x05, + 0x97, 0x5a, 0x70, 0xec, 0x46, 0xe5, 0x35, 0x40, 0xd9, 0x02, 0xec, 0xbd, 0x07, 0x09, 0xcd, 0x11, + 0x16, 0xd4, 0xa9, 0x41, 0xd7, 0x28, 0x68, 0x83, 0xc2, 0x01, 0xee, 0x34, 0x61, 0x9d, 0x02, 0x03, + 0xfd, 0x27, 0x82, 0x51, 0x48, 0x2c, 0x12, 0x7b, 0x4b, 0xb1, 0x1a, 0x94, 0x6a, 0x40, 0xf5, 0x81, + 0x4d, 0x25, 0x84, 0xa6, 0xa4, 0xc8, 0x1b, 0x09, 0xd5, 0x78, 0xc8, 0x67, 0xe0, 0xe1, 0xf3, 0x13, + 0x89, 0x30, 0x87, 0x08, 0xd7, 0xf8, 0x20, 0x1e, 0x70, 0x9e, 0x84, 0x2a, 0x40, 0x83, 0xbc, 0x93, + 0x3f, 0x1d, 0x0e, 0x48, 0x17, 0x02, 0xb9, 0x8c, 0x48, 0xf9, 0x61, 0x38, 0xcc, 0xd9, 0x1c, 0x36, + 0x3d, 0x52, 0x31, 0xf5, 0x42, 0x38, 0x7a, 0x9e, 0x70, 0xa1, 0x18, 0x03, 0x25, 0xaa, 0xc3, 0x70, + 0x9c, 0xef, 0x20, 0xac, 0x15, 0xb8, 0xa9, 0xf7, 0x9d, 0x27, 0x03, 0x72, 0xf9, 0x4b, 0x49, 0x2a, + 0xa8, 0x16, 0xf8, 0x30, 0x1e, 0x17, 0xd6, 0x60, 0x79, 0x4e, 0xc1, 0x41, 0xf1, 0x0b, 0x01, 0xb9, + 0x80, 0x80, 0xe8, 0x66, 0x42, 0x61, 0x26, 0xe4, 0x41, 0x48, 0xb0, 0xf6, 0x70, 0x0c, 0x81, 0x03, + 0x2a, 0x7c, 0x0e, 0x1e, 0xe9, 0x12, 0x90, 0x0b, 0x0b, 0x05, 0xc9, 0xee, 0x30, 0x20, 0xec, 0x96, + 0x36, 0x4d, 0xc0, 0x94, 0x12, 0x36, 0xc1, 0x0f, 0x21, 0xca, 0xb3, 0x0e, 0x54, 0x54, 0x65, 0x0e, + 0xc4, 0x79, 0x87, 0xab, 0x02, 0x69, 0xb5, 0xc7, 0xc1, 0x34, 0xc1, 0xe3, 0x40, 0x0e, 0xab, 0xc1, + 0xa8, 0x06, 0x4c, 0xad, 0xbc, 0x13, 0x02, 0x31, 0xcd, 0x25, 0x60, 0xf6, 0x53, 0xc2, 0x35, 0x18, + 0xdc, 0x81, 0x09, 0xd2, 0x48, 0x0e, 0xf0, 0x9b, 0xb0, 0xc1, 0x8a, 0x6a, 0x44, 0x11, 0xf7, 0x69, + 0x0b, 0x8b, 0x1a, 0x87, 0x68, 0x70, 0xb4, 0x56, 0x07, 0xa8, 0x48, 0x73, 0xee, 0x72, 0x2d, 0xa1, + 0x1e, 0x82, 0xfe, 0x0c, 0x04, 0x2e, 0x07, 0x38, 0x41, 0xb0, 0x7a, 0x0b, 0x74, 0x3f, 0x0a, 0xe6, + 0x52, 0x8e, 0x24, 0x4a, 0x83, 0xe3, 0x3f, 0x15, 0x85, 0x74, 0x62, 0x39, 0xd3, 0xb2, 0x4e, 0x1a, + 0xf7, 0x62, 0x40, 0xce, 0x73, 0x35, 0x18, 0x0b, 0x09, 0x86, 0x5e, 0x88, 0x82, 0x42, 0xae, 0x51, + 0x30, 0x4b, 0x8d, 0x25, 0xbb, 0x1f, 0x85, 0xcb, 0x33, 0x5c, 0x41, 0x60, 0x12, 0x42, 0x9e, 0x08, + 0x02, 0xdc, 0x15, 0xa8, 0x39, 0xc1, 0x5a, 0xea, 0x03, 0x20, 0xc4, 0x3c, 0x91, 0x02, 0x11, 0x8d, + 0x95, 0x7e, 0x19, 0x02, 0x58, 0xfd, 0x40, 0x0b, 0x54, 0x30, 0xa6, 0xe4, 0xfd, 0x00, 0x9c, 0xe3, + 0x32, 0x82, 0x15, 0xe6, 0xaa, 0x4e, 0x40, 0x4f, 0x71, 0x26, 0x58, 0x2e, 0xc0, 0xba, 0x92, 0x01, + 0x0a, 0x74, 0x00, 0x02, 0x88, 0xd5, 0x1a, 0xf6, 0x30, 0x0a, 0x96, 0x81, 0x07, 0x30, 0xf4, 0xf1, + 0x1d, 0x46, 0x77, 0x64, 0xb6, 0x9f, 0xba, 0x3c, 0x0e, 0x59, 0x48, 0x50, 0x04, 0x19, 0x0b, 0xc4, + 0x88, 0x1a, 0x80, 0x68, 0x00, 0x6c, 0x3f, 0x95, 0x5d, 0x90, 0xc2, 0xbe, 0x52, 0x69, 0xcc, 0x30, + 0xaf, 0x8f, 0x83, 0x1e, 0x04, 0x69, 0x41, 0x84, 0xc1, 0x1d, 0x15, 0xdb, 0x4f, 0x41, 0x96, 0x63, + 0x2e, 0x0f, 0x31, 0x8d, 0x3c, 0x1f, 0xaf, 0x81, 0xc8, 0x06, 0xc8, 0xf6, 0x93, 0x6d, 0x97, 0x00, + 0x88, 0x36, 0xea, 0x52, 0xb0, 0xc3, 0x82, 0xeb, 0xd0, 0xc6, 0xcf, 0x05, 0xe3, 0x17, 0x7f, 0xd8, + 0xcc, 0x30, 0x88, 0x6d, 0x89, 0x0f, 0xc3, 0xf1, 0x39, 0x14, 0xce, 0x5a, 0x0a, 0xd2, 0x89, 0x78, + 0x06, 0x1c, 0xef, 0xfa, 0x1c, 0x73, 0x42, 0x1f, 0xc0, 0x82, 0x5d, 0xf6, 0x00, 0xc0, 0x04, 0xdc, + 0x5a, 0xbf, 0x04, 0x8d, 0xcb, 0xe1, 0x90, 0xe9, 0x61, 0xff, 0x47, 0xb9, 0x3e, 0x0c, 0xc6, 0xad, + 0x70, 0x10, 0x8b, 0x70, 0x52, 0xf0, 0x16, 0x7c, 0x22, 0x16, 0xb7, 0x3f, 0xe0, 0xc2, 0xad, 0x40, + 0xb0, 0xaa, 0x3e, 0x84, 0x06, 0x42, 0x14, 0x43, 0xa2, 0xc1, 0x1c, 0x0b, 0xf6, 0x22, 0x38, 0xde, + 0x17, 0x38, 0xe8, 0x0e, 0x8e, 0x33, 0x0f, 0xa0, 0xf1, 0x87, 0x1e, 0x48, 0xbf, 0x18, 0x56, 0x12, + 0xfd, 0x74, 0x3c, 0x60, 0x79, 0x09, 0xda, 0x52, 0x92, 0x78, 0x5d, 0x1d, 0x82, 0x03, 0xf1, 0x4a, + 0xc1, 0xcf, 0xc0, 0x3f, 0xea, 0xa8, 0xd9, 0x03, 0x13, 0x1c, 0xf5, 0xa2, 0xee, 0x72, 0x37, 0xe9, + 0x8e, 0x68, 0xb4, 0xa3, 0x29, 0x70, 0x66, 0x06, 0x87, 0xbc, 0xb4, 0xbd, 0xfb, 0x2d, 0x83, 0x13, + 0x5d, 0xdf, 0x4e, 0xb3, 0x22, 0x69, 0xc9, 0xd5, 0x70, 0xa4, 0xec, 0x18, 0xc7, 0xaf, 0xfa, 0xd1, + 0xea, 0xaf, 0xc7, 0xda, 0x0d, 0xd5, 0x7c, 0xbd, 0xa6, 0xba, 0x81, 0x9a, 0x6f, 0x30, 0xf1, 0xf6, + 0xc6, 0xb9, 0xa0, 0x35, 0xe8, 0xbd, 0x43, 0xaf, 0xbb, 0x17, 0xe9, 0xcf, 0xbc, 0x83, 0x5b, 0x82, + 0x28, 0x0e, 0xef, 0x08, 0x0e, 0x9c, 0xf9, 0xda, 0x9b, 0x6b, 0x3a, 0x69, 0xee, 0x40, 0x8a, 0x19, + 0x98, 0x90, 0x8d, 0x5d, 0xb6, 0x2a, 0xc0, 0x0d, 0xc7, 0xd6, 0x7d, 0xad, 0xbf, 0xe1, 0xa9, 0xdb, + 0xa0, 0xae, 0xfa, 0x2a, 0x41, 0x7d, 0x1d, 0x8b, 0xa0, 0x3d, 0xa4, 0x69, 0xc5, 0x99, 0x81, 0x64, + 0xd3, 0x34, 0x75, 0x92, 0x4d, 0x2f, 0xd9, 0x04, 0x92, 0x4d, 0x20, 0xd9, 0x04, 0x92, 0x4d, 0x20, + 0xb9, 0x1c, 0x48, 0x2e, 0x9b, 0xa6, 0x4e, 0x72, 0xd9, 0x4b, 0x2e, 0x03, 0xc9, 0x65, 0x20, 0xb9, + 0x0c, 0x24, 0x97, 0x81, 0xe4, 0x6a, 0x20, 0xb9, 0x6a, 0x9a, 0x3a, 0xc9, 0x55, 0x2f, 0xb9, 0x0a, + 0x24, 0x57, 0x81, 0xe4, 0x2a, 0x90, 0x5c, 0xb5, 0x92, 0x87, 0x66, 0x6c, 0x1f, 0x9d, 0xb2, 0x3d, + 0xdb, 0x0e, 0x88, 0xea, 0x37, 0xbb, 0x97, 0x99, 0xbb, 0xd8, 0x33, 0xf7, 0x8c, 0x70, 0x30, 0x71, + 0xea, 0x6d, 0x0a, 0xd5, 0xb4, 0xb6, 0x62, 0x53, 0x83, 0x99, 0xba, 0x9a, 0x23, 0x31, 0xa9, 0x69, + 0x49, 0x4c, 0x47, 0x62, 0x06, 0x24, 0x65, 0x5a, 0xb6, 0x24, 0x65, 0x47, 0x52, 0x0e, 0x48, 0x8a, + 0xb4, 0x68, 0x49, 0x8a, 0x8e, 0xa4, 0x68, 0x49, 0xf6, 0x74, 0xe2, 0x1f, 0x24, 0xb3, 0xbb, 0x0a, + 0xb8, 0x4b, 0x54, 0x3f, 0x33, 0x3c, 0xae, 0x98, 0xfd, 0x07, 0x26, 0x58, 0x7a, 0x41, 0x49, 0xfb, + 0xef, 0x46, 0x36, 0x6e, 0x43, 0xc1, 0xf8, 0x9b, 0xb0, 0x56, 0x9b, 0xfe, 0x16, 0xac, 0x51, 0xa8, + 0xf1, 0xb7, 0x61, 0xed, 0xb5, 0x89, 0xbf, 0x15, 0xab, 0xd5, 0x9a, 0xda, 0x21, 0x88, 0xfa, 0xc9, + 0x22, 0x3b, 0x30, 0xe5, 0x21, 0xa9, 0x7f, 0x72, 0x0d, 0x51, 0xb4, 0xef, 0x23, 0xcd, 0x2d, 0x56, + 0x36, 0x1a, 0xb5, 0xe1, 0xea, 0x65, 0x44, 0x71, 0x9a, 0x67, 0x68, 0x92, 0x43, 0x5e, 0x9c, 0xc4, + 0x36, 0xab, 0x66, 0xb9, 0xbb, 0x57, 0x35, 0x6d, 0xa5, 0x6c, 0x2b, 0x90, 0xbf, 0x97, 0x33, 0x9f, + 0x97, 0xe7, 0xc9, 0x72, 0x26, 0x28, 0x1c, 0x76, 0xdd, 0xa6, 0xc6, 0xe5, 0x48, 0x16, 0xb7, 0x4d, + 0x90, 0x85, 0x88, 0xcf, 0xae, 0xa2, 0x6d, 0x2a, 0xe7, 0xad, 0x1e, 0xbc, 0xec, 0x7d, 0x39, 0x4e, + 0xc6, 0x58, 0xc0, 0x42, 0x32, 0xca, 0xb2, 0xd1, 0x2e, 0x6d, 0x1e, 0xb1, 0x7f, 0xbe, 0xe4, 0x77, + 0xee, 0xe0, 0xb2, 0xdb, 0xc8, 0xbf, 0x3e, 0xc7, 0x13, 0x7b, 0x7a, 0x61, 0xf2, 0xc1, 0x03, 0xd7, + 0xf0, 0xd9, 0x72, 0x97, 0xda, 0x3b, 0x4f, 0x60, 0xfd, 0xcb, 0x66, 0xcb, 0x1d, 0x05, 0x4f, 0x9b, + 0xbb, 0x49, 0xab, 0x24, 0x03, 0xaa, 0x30, 0xd3, 0x45, 0xf3, 0x80, 0x34, 0x49, 0x12, 0x53, 0x0f, + 0x5f, 0x66, 0x8b, 0x99, 0x99, 0x4f, 0xec, 0xac, 0x9c, 0xc3, 0x44, 0xca, 0xb8, 0xbb, 0xeb, 0xdd, + 0x45, 0x31, 0xfc, 0x9f, 0xfc, 0xea, 0x7f, 0xf1, 0xc2, 0x99, 0x50, 0xc3, 0x1f, 0x00, 0x00, +}; +const StaticFile md5_js PROGMEM = {(sizeof(md5_js_content)/sizeof(md5_js_content[0])), md5_js_content}; + +static const uint8_t style_css_content[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x9d, 0x53, 0x5d, 0x6b, 0xdb, 0x30, + 0x14, 0x7d, 0xdf, 0xaf, 0x08, 0x94, 0x41, 0x0b, 0x76, 0xb0, 0x9b, 0x26, 0x59, 0x64, 0xf6, 0xb0, + 0x3d, 0x8c, 0xed, 0x61, 0x4f, 0x65, 0x4f, 0xa3, 0x14, 0x7d, 0x5c, 0xd9, 0x22, 0xb2, 0x25, 0xa4, + 0xeb, 0x26, 0x99, 0xf1, 0x7f, 0x9f, 0xfc, 0x11, 0x37, 0x69, 0x32, 0x28, 0xc3, 0x20, 0xb8, 0x1f, + 0xd2, 0x39, 0xf7, 0xdc, 0x63, 0x66, 0xc4, 0x21, 0x2a, 0xb0, 0xd4, 0x8d, 0xa5, 0x42, 0xa8, 0x2a, + 0x27, 0x49, 0x56, 0x52, 0x97, 0xab, 0x8a, 0x24, 0x2d, 0xeb, 0x8a, 0xac, 0x46, 0x34, 0x55, 0xa4, + 0x2a, 0x5b, 0xe3, 0x6f, 0x3c, 0x58, 0xf8, 0x6c, 0xa9, 0xf7, 0x3b, 0xe3, 0xc4, 0xd3, 0x69, 0x12, + 0x61, 0x8f, 0x4f, 0x8d, 0x34, 0x15, 0xc6, 0x5e, 0xfd, 0x01, 0x92, 0xae, 0xec, 0x3e, 0xeb, 0x43, + 0x49, 0x4b, 0xa5, 0x0f, 0x24, 0xa6, 0xd6, 0x6a, 0x88, 0xfd, 0xc1, 0x23, 0x94, 0xd1, 0x57, 0xad, + 0xaa, 0xed, 0x4f, 0xca, 0x1f, 0xfb, 0xf0, 0x5b, 0xe8, 0x8b, 0x1e, 0x21, 0x37, 0x30, 0xfb, 0xf5, + 0x23, 0xfa, 0x0e, 0xfa, 0x05, 0x50, 0x71, 0x1a, 0x7d, 0x71, 0x8a, 0xea, 0xc8, 0xd3, 0xca, 0xc7, + 0x1e, 0x9c, 0x92, 0xed, 0x1c, 0x15, 0x6a, 0x98, 0xb8, 0xa6, 0x89, 0xdd, 0xcf, 0xfa, 0x63, 0x42, + 0xdb, 0x81, 0xca, 0x0b, 0x24, 0xab, 0x24, 0xc9, 0x18, 0xe5, 0xdb, 0xdc, 0x99, 0xba, 0x12, 0x31, + 0x37, 0xda, 0x38, 0x72, 0x03, 0x52, 0xde, 0xcb, 0x65, 0xc6, 0x02, 0x79, 0x70, 0x31, 0x33, 0x61, + 0xb2, 0x92, 0xa4, 0xe1, 0xba, 0x37, 0x5a, 0x89, 0xd9, 0x8d, 0xd8, 0x40, 0x02, 0xeb, 0x6c, 0xec, + 0xbe, 0x5f, 0xaf, 0x80, 0x3d, 0x64, 0x27, 0x33, 0x2d, 0xed, 0xbe, 0x9d, 0x33, 0x6d, 0xf8, 0xf6, + 0x8c, 0x42, 0x3b, 0x97, 0xb5, 0xd6, 0xf1, 0x4e, 0x09, 0x2c, 0x9a, 0xfe, 0x0c, 0xe9, 0xe4, 0x63, + 0xc0, 0xd9, 0x77, 0x17, 0xbb, 0xb6, 0x09, 0xb2, 0x6b, 0x36, 0xae, 0x7c, 0xd6, 0x94, 0xc1, 0x89, + 0xe8, 0xb3, 0x64, 0xb6, 0xb8, 0x1c, 0x61, 0xec, 0xed, 0x65, 0x6e, 0x86, 0xb5, 0x4c, 0xac, 0x7b, + 0x2e, 0x7d, 0xd9, 0xd7, 0xec, 0xec, 0xb5, 0x18, 0x8d, 0x25, 0xe1, 0xb5, 0xf6, 0x5d, 0x3b, 0x8b, + 0x3c, 0x68, 0xe0, 0xd8, 0x8c, 0x0c, 0x1d, 0x15, 0xaa, 0xf6, 0xe4, 0x21, 0x90, 0x19, 0x32, 0xa7, + 0xfa, 0xf0, 0x0d, 0xe7, 0x5c, 0x66, 0x47, 0xd6, 0xeb, 0x50, 0xd9, 0x84, 0x46, 0x53, 0x63, 0x58, + 0x27, 0x04, 0xc7, 0x5c, 0x43, 0x24, 0xd2, 0xf0, 0xda, 0x5f, 0xe0, 0x8e, 0xe9, 0x01, 0x7d, 0x08, + 0x9a, 0x5e, 0xb0, 0x82, 0x0a, 0xb3, 0xeb, 0x15, 0xe9, 0x37, 0xeb, 0x72, 0x46, 0x6f, 0x93, 0xa8, + 0xfb, 0xe6, 0xe9, 0xf2, 0xae, 0x1d, 0xfc, 0x48, 0x84, 0xf2, 0x94, 0x69, 0x10, 0x57, 0x8d, 0x79, + 0xb5, 0x3a, 0xa0, 0x4e, 0x95, 0x11, 0xf8, 0x18, 0x37, 0x97, 0x6e, 0x91, 0x69, 0x70, 0xcb, 0xe2, + 0xe8, 0x96, 0xb3, 0xe4, 0xc8, 0xe2, 0x7f, 0x45, 0xeb, 0x96, 0xf7, 0xaa, 0xda, 0x89, 0x51, 0xc3, + 0xfb, 0x52, 0x1e, 0xfd, 0x97, 0x04, 0x0f, 0x5b, 0xe3, 0x15, 0xaa, 0x30, 0xaf, 0x03, 0x4d, 0x51, + 0xbd, 0x40, 0xd6, 0xdd, 0x89, 0x8b, 0xc1, 0x22, 0xe9, 0xa7, 0x2b, 0x9e, 0x19, 0x05, 0xaa, 0x0c, + 0xde, 0x4e, 0xd3, 0xdd, 0x91, 0xc2, 0xbc, 0x80, 0x7b, 0x8f, 0xc0, 0x19, 0xaf, 0x9d, 0x0f, 0xf0, + 0xd6, 0xa8, 0x0a, 0xc1, 0xbd, 0x19, 0x9f, 0x2d, 0x39, 0x87, 0xc5, 0xf9, 0x1f, 0xf2, 0x0f, 0x44, + 0xca, 0x3b, 0xbe, 0x4d, 0x67, 0xc6, 0xa0, 0xc6, 0xd8, 0x34, 0x57, 0xfe, 0xd9, 0x81, 0x07, 0x8c, + 0xde, 0xc4, 0xd7, 0xf9, 0x1e, 0xff, 0xdb, 0xd5, 0x62, 0x93, 0xae, 0xdb, 0x0f, 0x7f, 0x01, 0x37, + 0xdb, 0x6e, 0xf6, 0xae, 0x04, 0x00, 0x00, +}; +const StaticFile style_css PROGMEM = {(sizeof(style_css_content)/sizeof(style_css_content[0])), style_css_content}; + +static const uint8_t favicon_ico_content[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xed, 0x99, 0x4b, 0x48, 0x15, 0x61, + 0x14, 0xc7, 0xcf, 0xc5, 0x17, 0x2e, 0x4a, 0x57, 0xe5, 0x63, 0xe1, 0x85, 0x42, 0x23, 0x8c, 0x8c, + 0x20, 0x4d, 0x45, 0xdb, 0x59, 0x14, 0x2e, 0x7a, 0xab, 0x68, 0x1b, 0x17, 0xae, 0x24, 0x41, 0xf1, + 0x41, 0xa0, 0x41, 0xa1, 0x11, 0x28, 0x1a, 0x2e, 0x12, 0x72, 0xe7, 0x03, 0x09, 0x74, 0x15, 0x54, + 0x1b, 0x97, 0xd9, 0x53, 0x23, 0x8a, 0x5a, 0x94, 0x25, 0x59, 0x91, 0x82, 0xa0, 0x81, 0x99, 0x39, + 0xfd, 0x8f, 0x73, 0x46, 0xbf, 0xc6, 0xb9, 0x73, 0x67, 0xee, 0x9d, 0xab, 0x41, 0x1e, 0xf8, 0x71, + 0xe7, 0x7e, 0xe7, 0xf1, 0x9f, 0xb9, 0xdf, 0x7c, 0x8f, 0x99, 0x4b, 0xe4, 0xa3, 0x28, 0x4a, 0x4c, + 0x24, 0x7c, 0xfa, 0xa9, 0x2a, 0x9a, 0xe8, 0x08, 0x11, 0x25, 0x25, 0xe9, 0xdf, 0xdb, 0xe2, 0x89, + 0x7a, 0xd1, 0xe6, 0xf7, 0xeb, 0xdf, 0x07, 0x11, 0x97, 0xbe, 0x93, 0x68, 0x1f, 0x62, 0x70, 0x88, + 0x16, 0xbd, 0x7d, 0xd5, 0x10, 0x37, 0x43, 0x3a, 0x0e, 0x2c, 0x06, 0xdc, 0x16, 0x62, 0x9c, 0xa5, + 0xfc, 0x65, 0xb1, 0x60, 0x58, 0x88, 0x75, 0x99, 0x9b, 0x06, 0x2a, 0xc1, 0x1b, 0xa1, 0x52, 0xda, + 0x82, 0x19, 0xae, 0x90, 0xca, 0xc0, 0x04, 0x58, 0x02, 0x9a, 0xb0, 0x24, 0x6d, 0xa5, 0x12, 0x13, + 0xc8, 0xd8, 0x3f, 0xab, 0xe4, 0x99, 0x99, 0x95, 0xfa, 0x56, 0xe6, 0x17, 0x8d, 0x40, 0xb9, 0x06, + 0x13, 0x12, 0x6b, 0xb6, 0x4b, 0xa6, 0x73, 0x0e, 0xc4, 0x92, 0xc4, 0x9a, 0xed, 0xba, 0x83, 0x5c, + 0x83, 0x6b, 0x16, 0xf9, 0x1d, 0x2e, 0xf2, 0x3b, 0x2c, 0xf2, 0xdb, 0x5d, 0xe4, 0xb7, 0x9b, 0x72, + 0xf9, 0xfe, 0xea, 0x76, 0x91, 0xcf, 0xb1, 0x6a, 0x3f, 0x9e, 0x03, 0x5f, 0x5c, 0xe4, 0x73, 0xec, + 0x59, 0x25, 0xbf, 0x00, 0xdc, 0x02, 0x5d, 0xe0, 0x2e, 0x58, 0x00, 0xf7, 0xe5, 0x7b, 0x97, 0x1c, + 0x2f, 0x88, 0xaf, 0x4b, 0x62, 0x0b, 0x4c, 0xd7, 0xe0, 0x93, 0xcf, 0x7c, 0xf0, 0x99, 0xf4, 0x7b, + 0xc9, 0xb0, 0x52, 0x69, 0xcb, 0x37, 0xc5, 0x5a, 0x59, 0x1e, 0x98, 0x06, 0x25, 0x4a, 0x5b, 0x89, + 0xb4, 0xe5, 0xd9, 0xe4, 0x19, 0x96, 0x09, 0x9e, 0x83, 0x22, 0xa5, 0xad, 0x48, 0xda, 0x32, 0xcd, + 0xc1, 0x8b, 0x18, 0x55, 0x93, 0x71, 0x44, 0xa3, 0x51, 0x44, 0x2d, 0x3e, 0x1d, 0xa7, 0xc6, 0x79, + 0x3c, 0xcf, 0x60, 0x2a, 0xa2, 0x2c, 0x52, 0xe6, 0x99, 0x78, 0x57, 0xf3, 0x8c, 0xd9, 0x52, 0xc1, + 0x43, 0xf0, 0x40, 0x8e, 0xbd, 0xb6, 0x1c, 0xf0, 0x5e, 0xc8, 0x89, 0x40, 0xfd, 0x6c, 0xf0, 0x41, + 0xc8, 0xf6, 0xb0, 0x2e, 0x8f, 0x91, 0xbd, 0xa0, 0x0a, 0x7c, 0x15, 0xf8, 0x78, 0x0f, 0xd9, 0xcf, + 0x69, 0xc1, 0x8c, 0x7b, 0x9c, 0xbb, 0xaf, 0x0d, 0x3c, 0x05, 0xf3, 0xb4, 0x3e, 0x4e, 0xe6, 0xa5, + 0xad, 0x15, 0x1c, 0x24, 0xfb, 0xfb, 0xd6, 0xca, 0xf8, 0xbc, 0x78, 0x6c, 0x3a, 0x99, 0x17, 0xc7, + 0x25, 0xd6, 0xe9, 0xb5, 0xf0, 0xb9, 0x9c, 0x27, 0x7d, 0x5c, 0x38, 0x1d, 0xf7, 0xd3, 0x92, 0xe3, + 0xe4, 0x3a, 0xf8, 0x37, 0x79, 0xe1, 0xa2, 0xb6, 0x7a, 0x1d, 0x59, 0x41, 0x6a, 0x73, 0x5f, 0xb6, + 0x85, 0x50, 0xdb, 0xe0, 0x06, 0xd9, 0xaf, 0xc9, 0xe9, 0xa4, 0xf7, 0x5b, 0xa8, 0xf5, 0x9f, 0x48, + 0x8d, 0x40, 0x76, 0x0c, 0xfc, 0x08, 0xa3, 0x3e, 0xcf, 0xa5, 0x85, 0x36, 0xf5, 0xcf, 0x84, 0x51, + 0xdb, 0xe0, 0xb4, 0x4d, 0xfd, 0x72, 0x0f, 0xea, 0x97, 0xdb, 0xd4, 0xaf, 0xf0, 0xa0, 0x7e, 0xc5, + 0x16, 0xd6, 0x2f, 0xf3, 0xa0, 0x7e, 0xa0, 0x7d, 0x12, 0xcf, 0x57, 0x9d, 0x1e, 0xd4, 0xef, 0x94, + 0x5a, 0xaa, 0x61, 0x15, 0xa3, 0x5e, 0xf0, 0xd3, 0x83, 0xfa, 0x5c, 0xe3, 0x8e, 0xd4, 0x34, 0x8c, + 0xe7, 0x8d, 0x26, 0xf0, 0x58, 0x18, 0x03, 0xcf, 0x48, 0x5f, 0xea, 0x38, 0x67, 0x4a, 0xf1, 0x19, + 0x4c, 0x89, 0x6f, 0x46, 0x62, 0xc7, 0x14, 0x5f, 0x13, 0x6d, 0x9c, 0x8b, 0x78, 0x5c, 0xef, 0x06, + 0x29, 0xa4, 0x2f, 0xad, 0xbc, 0xc4, 0x8e, 0x80, 0x15, 0xd2, 0xe7, 0xe1, 0x64, 0xf1, 0xa5, 0xc8, + 0x71, 0xab, 0xf8, 0x46, 0x68, 0x7d, 0x39, 0x4e, 0x91, 0x1a, 0x4e, 0xf6, 0xed, 0x09, 0xa0, 0x5f, + 0x6a, 0x34, 0x58, 0xf8, 0x1b, 0xc4, 0xd7, 0x2f, 0xb1, 0x6e, 0x8d, 0x1f, 0x43, 0x06, 0xa4, 0x46, + 0xa3, 0x85, 0xbf, 0x51, 0x7c, 0x03, 0x12, 0xeb, 0xd6, 0xf8, 0x9c, 0xfa, 0xa4, 0x46, 0xbd, 0x85, + 0xbf, 0x5e, 0x7c, 0x7d, 0x14, 0xda, 0xf9, 0x73, 0xff, 0x5f, 0x01, 0x9f, 0xc0, 0x49, 0x0b, 0x3f, + 0xb7, 0x4d, 0x49, 0x4c, 0x94, 0x85, 0x7f, 0xd5, 0xb4, 0xab, 0xda, 0x2a, 0x2b, 0xd4, 0x4c, 0x8b, + 0x78, 0x94, 0x99, 0xc3, 0xa9, 0x4c, 0x52, 0xdc, 0x2a, 0xa3, 0x48, 0x63, 0x5a, 0x70, 0x2b, 0x04, + 0xc2, 0x6c, 0xdc, 0xb6, 0x98, 0x94, 0xb6, 0x06, 0x77, 0xbc, 0x9f, 0xf4, 0x89, 0x79, 0x6d, 0x1f, + 0x96, 0xb8, 0x71, 0x1f, 0x76, 0xea, 0x72, 0x51, 0x48, 0x48, 0xf9, 0x77, 0xe0, 0x2d, 0x48, 0x0b, + 0xb5, 0x4e, 0x18, 0xfa, 0xd5, 0xb4, 0x3e, 0x36, 0xab, 0xb7, 0x40, 0xbf, 0x56, 0xd1, 0xaf, 0xfd, + 0x0f, 0xf5, 0xeb, 0x14, 0xfd, 0xba, 0x4d, 0xd2, 0xe4, 0xdb, 0x9a, 0x9f, 0x35, 0x5f, 0x82, 0xdf, + 0x8a, 0x3e, 0x1f, 0xf3, 0x9e, 0x92, 0x9f, 0x23, 0x33, 0x22, 0xa0, 0xcb, 0x73, 0xe7, 0x10, 0xe9, + 0xf3, 0x46, 0xb0, 0x75, 0x82, 0x63, 0x06, 0xc1, 0x2e, 0x8f, 0xb4, 0xf3, 0x68, 0x7d, 0x3d, 0x71, + 0xc3, 0x77, 0x90, 0x1b, 0xa6, 0x76, 0x21, 0xe9, 0x7b, 0xb3, 0x50, 0xd7, 0x4c, 0xce, 0x2d, 0x08, + 0xe3, 0x37, 0x77, 0xb3, 0x97, 0x0f, 0xc4, 0x37, 0x90, 0x1c, 0x82, 0xfe, 0x90, 0x07, 0xda, 0x06, + 0x83, 0x2e, 0xb5, 0xf7, 0x9b, 0xee, 0xef, 0x70, 0xe1, 0x5a, 0x07, 0x5c, 0xe8, 0xbb, 0x79, 0x77, + 0xe3, 0x94, 0x9b, 0x2e, 0xf4, 0x5f, 0x47, 0x40, 0xff, 0x95, 0x43, 0x6d, 0xde, 0x23, 0x39, 0x19, + 0xe7, 0xa1, 0xf4, 0x41, 0xb4, 0x03, 0xfd, 0xd4, 0x08, 0x68, 0x1b, 0xa4, 0x38, 0xd0, 0xcf, 0x88, + 0xa0, 0x7e, 0xd0, 0xb9, 0x79, 0x5b, 0x7f, 0x5b, 0x7f, 0xab, 0xf4, 0x61, 0x17, 0xc1, 0xbd, 0x08, + 0xea, 0x73, 0xed, 0x0b, 0x01, 0xb4, 0xe3, 0xc1, 0xaf, 0x08, 0x6a, 0x1b, 0xf0, 0xff, 0x05, 0xf1, + 0x16, 0xfa, 0xfc, 0x50, 0x31, 0xbe, 0x09, 0xfa, 0xfc, 0x0e, 0xcd, 0x67, 0xf3, 0x1b, 0x1c, 0x02, + 0x87, 0x4d, 0xd4, 0x28, 0xf9, 0xc3, 0x16, 0x7e, 0x83, 0x61, 0x25, 0xae, 0xc6, 0xc2, 0x7f, 0xc8, + 0xea, 0xda, 0x1d, 0xdc, 0x93, 0xc5, 0x4a, 0xdd, 0x1e, 0x9b, 0xb8, 0x1e, 0x25, 0xae, 0xd8, 0xad, + 0xce, 0xb6, 0xfe, 0x3f, 0xab, 0x7f, 0x42, 0xa9, 0xdb, 0x6d, 0x13, 0xa7, 0xfe, 0x67, 0x75, 0xdc, + 0x43, 0xfd, 0x1d, 0xa4, 0xbf, 0x27, 0xe2, 0x77, 0xfc, 0x47, 0x6d, 0xe2, 0x72, 0x49, 0xdf, 0xef, + 0x3e, 0xe2, 0x1c, 0x27, 0xb5, 0xb5, 0xe5, 0x42, 0x6d, 0x03, 0x93, 0x71, 0x9a, 0xd6, 0xe2, 0xd3, + 0x70, 0x11, 0x1a, 0x36, 0x42, 0xcd, 0xcb, 0x78, 0x0e, 0x60, 0x16, 0xf1, 0x9c, 0x6f, 0x30, 0x47, + 0x94, 0x60, 0xc7, 0x24, 0x51, 0xdc, 0x28, 0x51, 0x14, 0xc3, 0xc7, 0x5a, 0xeb, 0x47, 0xcd, 0x8a, + 0x3f, 0x05, 0x2f, 0x43, 0xb9, 0xce, 0x1e, 0x00, 0x00, +}; +const StaticFile favicon_ico PROGMEM = {(sizeof(favicon_ico_content)/sizeof(favicon_ico_content[0])), favicon_ico_content}; + +} diff --git a/platformio/temphum/src/static.h b/platformio/temphum/src/static.h new file mode 100644 index 0000000..90560bf --- /dev/null +++ b/platformio/temphum/src/static.h @@ -0,0 +1,22 @@ +/** + * This file is autogenerated with make_static.sh script + */ + +#pragma once + +#include <stdlib.h> + +namespace homekit::files { + +typedef struct { + size_t size; + const uint8_t* content; +} StaticFile; + +extern const StaticFile index_html; +extern const StaticFile app_js; +extern const StaticFile md5_js; +extern const StaticFile style_css; +extern const StaticFile favicon_ico; + +} diff --git a/platformio/temphum/src/stopwatch.h b/platformio/temphum/src/stopwatch.h new file mode 100644 index 0000000..bac2fcc --- /dev/null +++ b/platformio/temphum/src/stopwatch.h @@ -0,0 +1,30 @@ +#pragma once + +#include <Arduino.h> + +namespace homekit { + +class StopWatch { +private: + unsigned long time; + +public: + StopWatch() : time(0) {}; + + inline void save() { + time = millis(); + } + + inline bool elapsed(unsigned long ms) { + unsigned long now = millis(); + if (now < time) { + // rollover? + time = now; + } else if (now - time >= ms) { + return true; + } + return false; + } +}; + +}
\ No newline at end of file diff --git a/platformio/temphum/src/temphum.cpp b/platformio/temphum/src/temphum.cpp new file mode 100644 index 0000000..0b6fbbe --- /dev/null +++ b/platformio/temphum/src/temphum.cpp @@ -0,0 +1,53 @@ +#include "temphum.h" +#include "logging.h" +#include <Arduino.h> + +namespace homekit::temphum { + +static const int addr = 0x40; + +void setup() { + pinMode(D2, OUTPUT); + pinMode(D3, OUTPUT); + + Wire.begin(D2, D3); + + Wire.beginTransmission(addr); + Wire.write(0xfe); + Wire.endTransmission(); + + delay(500); +} + +struct data read() { + // Request temperature measurement from the Si7021 sensor + Wire.beginTransmission(addr); + Wire.write(0xF3); // command to measure temperature + Wire.endTransmission(); + + delay(500); // wait for the measurement to be ready + + // Read the temperature measurement from the Si7021 sensor + Wire.requestFrom(addr, 2); + uint16_t temp_raw = Wire.read() << 8 | Wire.read(); + double temperature = ((175.72 * temp_raw) / 65536.0) - 46.85; + + // Request humidity measurement from the Si7021 sensor + Wire.beginTransmission(addr); + Wire.write(0xF5); // command to measure humidity + Wire.endTransmission(); + + delay(500); // wait for the measurement to be ready + + // Read the humidity measurement from the Si7021 sensor + Wire.requestFrom(addr, 2); + uint16_t hum_raw = Wire.read() << 8 | Wire.read(); + double humidity = ((125.0 * hum_raw) / 65536.0) - 6.0; + + return { + .temp = temperature, + .rh = humidity + }; +} + +}
\ No newline at end of file diff --git a/platformio/temphum/src/temphum.h b/platformio/temphum/src/temphum.h new file mode 100644 index 0000000..824b9bf --- /dev/null +++ b/platformio/temphum/src/temphum.h @@ -0,0 +1,15 @@ +#pragma once + +#include <Wire.h> + +namespace homekit::temphum { + +struct data { + double temp; // celsius + double rh; // relative humidity percentage +}; + +void setup(); +struct data read(); + +}
\ No newline at end of file diff --git a/platformio/temphum/src/util.h b/platformio/temphum/src/util.h new file mode 100644 index 0000000..e0780d8 --- /dev/null +++ b/platformio/temphum/src/util.h @@ -0,0 +1,13 @@ +#pragma once + +namespace homekit { + +inline size_t otaGetMaxUpdateSize() { + return (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; +} + +inline void restart() { + ESP.restart(); +} + +}
\ No newline at end of file diff --git a/platformio/temphum/src/wifi.cpp b/platformio/temphum/src/wifi.cpp new file mode 100644 index 0000000..3d7e092 --- /dev/null +++ b/platformio/temphum/src/wifi.cpp @@ -0,0 +1,48 @@ +#include <pgmspace.h> +#include "config.def.h" +#include "wifi.h" +#include "config.h" +#include "logging.h" + +namespace homekit::wifi { + +using namespace homekit; +using homekit::config::ConfigData; + +const char NODE_ID[] = DEFAULT_NODE_ID; +const char AP_SSID[] = DEFAULT_WIFI_AP_SSID; +const char STA_SSID[] = DEFAULT_WIFI_STA_SSID; +const char STA_PSK[] = DEFAULT_WIFI_STA_PSK; + +void getConfig(ConfigData &cfg, const char** ssid, const char** psk, const char** hostname) { + if (cfg.flags.wifi_configured) { + *ssid = cfg.wifi_ssid; + *psk = cfg.wifi_psk; + *hostname = cfg.node_id; + } else { + *ssid = STA_SSID; + *psk = STA_PSK; + *hostname = NODE_ID; + } +} + +std::shared_ptr<std::list<ScanResult>> scan() { + if (WiFi.getMode() != WIFI_STA) { + PRINTLN("wifi::scan: switching mode to STA"); + WiFi.mode(WIFI_STA); + } + + std::shared_ptr<std::list<ScanResult>> results(new std::list<ScanResult>); + int count = WiFi.scanNetworks(); + for (int i = 0; i < count; i++) { + results->push_back(ScanResult { + .rssi = WiFi.RSSI(i), + .ssid = WiFi.SSID(i) + }); + } + + WiFi.scanDelete(); + return results; +} + +}
\ No newline at end of file diff --git a/platformio/temphum/src/wifi.h b/platformio/temphum/src/wifi.h new file mode 100644 index 0000000..1b95b2f --- /dev/null +++ b/platformio/temphum/src/wifi.h @@ -0,0 +1,36 @@ +#pragma once + +#include <ESP8266WiFi.h> +#include <list> +#include <memory> +#include "config.h" + +namespace homekit::wifi { + +using homekit::config::ConfigData; + +struct ScanResult { + int rssi; + String ssid; +}; + +void getConfig(ConfigData& cfg, const char** ssid, const char** psk, const char** hostname); + +std::shared_ptr<std::list<ScanResult>> scan(); + +inline uint32_t getIPAsInteger() { + if (!WiFi.isConnected()) + return 0; + return WiFi.localIP().v4(); +} + +inline int8_t getRSSI() { + return WiFi.RSSI(); +} + +extern const char AP_SSID[]; +extern const char STA_SSID[]; +extern const char STA_PSK[]; +extern const char NODE_ID[]; + +}
\ No newline at end of file |