aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2022-12-18 06:31:24 +0300
committerEvgeny Zinoviev <me@ch1p.io>2022-12-24 12:57:55 +0300
commit0a065f48be99d4ebae49de622a335f23e50c6ca0 (patch)
treeb591d91fac26e5bf7a4dd6d37178b978061ef060
parent022ec129bb8f511a7bf8cf537f165afce2303262 (diff)
pump-mqtt-bot: wip; relayctl: somewhat stable
-rw-r--r--package.json3
-rwxr-xr-xplatformio/relayctl/make_static.sh3
-rw-r--r--platformio/relayctl/platformio.ini11
-rw-r--r--platformio/relayctl/src/config.cpp30
-rw-r--r--platformio/relayctl/src/config.def.h.example8
-rw-r--r--platformio/relayctl/src/config.h10
-rw-r--r--platformio/relayctl/src/http_server.cpp236
-rw-r--r--platformio/relayctl/src/http_server.h45
-rw-r--r--platformio/relayctl/src/led.cpp17
-rw-r--r--platformio/relayctl/src/led.h19
-rw-r--r--platformio/relayctl/src/logging.cpp37
-rw-r--r--platformio/relayctl/src/logging.h8
-rw-r--r--platformio/relayctl/src/main.cpp45
-rw-r--r--platformio/relayctl/src/mqtt.cpp341
-rw-r--r--platformio/relayctl/src/mqtt.h64
-rw-r--r--platformio/relayctl/src/relay.h6
-rw-r--r--platformio/relayctl/src/static.cpp676
-rw-r--r--platformio/relayctl/src/static.h1
-rw-r--r--platformio/relayctl/src/util.h13
-rw-r--r--platformio/relayctl/src/wifi.cpp25
-rw-r--r--platformio/relayctl/src/wifi.h19
-rw-r--r--platformio/relayctl/static/app.js48
-rw-r--r--platformio/relayctl/static/index.html9
-rw-r--r--platformio/relayctl/static/md5.js615
-rw-r--r--platformio/relayctl/static/style.css2
-rw-r--r--src/home/api/types/types.py1
-rw-r--r--src/home/api/web_api_client.py6
-rw-r--r--src/home/mqtt/__init__.py1
-rw-r--r--src/home/mqtt/mqtt.py21
-rw-r--r--src/home/mqtt/payload/__init__.py1
-rw-r--r--src/home/mqtt/payload/relay.py45
-rw-r--r--src/home/mqtt/relay.py93
-rw-r--r--src/home/telegram/bot.py2
-rwxr-xr-xsrc/pump_mqtt_bot.py191
-rwxr-xr-xtools/mcuota.py98
-rwxr-xr-xtools/mcuota.sh14
-rwxr-xr-xtools/minify.js16
37 files changed, 2154 insertions, 626 deletions
diff --git a/package.json b/package.json
index 16706ba..c38bb94 100644
--- a/package.json
+++ b/package.json
@@ -10,5 +10,6 @@
"html-minifier-terser": "^7.1.0",
"minimist": "^1.2.7",
"terser": "^5.16.1"
- }
+ },
+ "dependencies": {}
}
diff --git a/platformio/relayctl/make_static.sh b/platformio/relayctl/make_static.sh
index 079d26b..879beb4 100755
--- a/platformio/relayctl/make_static.sh
+++ b/platformio/relayctl/make_static.sh
@@ -73,7 +73,8 @@ for ext in html js css ico; do
gzip |
xxd -ps -c 16 |
sed 's/.\{2\}/0x&, /g' |
- sed 's/^/ /'
+ sed 's/^/ /' |
+ sed 's/[ \t]*$//'
echo "};"
echo "const StaticFile $filename PROGMEM = {(sizeof(${filename}_content)/sizeof(${filename}_content[0])), ${filename}_content};"
diff --git a/platformio/relayctl/platformio.ini b/platformio/relayctl/platformio.ini
index 592eba9..662049e 100644
--- a/platformio/relayctl/platformio.ini
+++ b/platformio/relayctl/platformio.ini
@@ -14,7 +14,10 @@ board = esp12e
framework = arduino
upload_port = /dev/ttyUSB0
monitor_speed = 115200
-lib_deps =
- ESP Async WebServer
- knolleary/PubSubClient@^2.8
- me-no-dev/ESPAsyncTCP@^1.2.2
+lib_deps =
+ https://github.com/bertmelis/espMqttClient#unordered-acks
+;build_flags =
+; -DDEBUG
+; -DDEBUG_ESP_SSL
+; -DDEBUG_ESP_PORT=Serial
+build_type = release \ No newline at end of file
diff --git a/platformio/relayctl/src/config.cpp b/platformio/relayctl/src/config.cpp
index d143a7f..796adf0 100644
--- a/platformio/relayctl/src/config.cpp
+++ b/platformio/relayctl/src/config.cpp
@@ -1,7 +1,6 @@
#include <EEPROM.h>
#include <strings.h>
#include "config.h"
-#include "config.def.h"
#include "logging.h"
#define GET_DATA_CRC(data) \
@@ -10,7 +9,7 @@
namespace homekit::config {
static const uint32_t magic = 0xdeadbeef;
-static const uint32_t crc_table[16] = {
+static const uint32_t crc_table[16] PROGMEM = {
0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,
0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,
@@ -20,15 +19,15 @@ static const uint32_t crc_table[16] = {
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 = crc_table[(crc ^ data[index]) & 0x0f] ^ (crc >> 4);
- crc = crc_table[(crc ^ (data[index] >> 4)) & 0x0f] ^ (crc >> 4);
+ 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 {0};
+ ConfigData data;
EEPROM.begin(sizeof(ConfigData));
EEPROM.get(0, data);
EEPROM.end();
@@ -40,25 +39,22 @@ ConfigData read() {
return data;
}
-bool write(ConfigData& data) {
+void write(ConfigData& data) {
EEPROM.begin(sizeof(ConfigData));
data.magic = magic;
data.crc = GET_DATA_CRC(data);
EEPROM.put(0, data);
- return EEPROM.end();
+ EEPROM.end();
}
-bool erase() {
+void erase() {
ConfigData data;
- return erase(data);
+ erase(data);
}
-bool erase(ConfigData& data) {
+void erase(ConfigData& data) {
bzero(reinterpret_cast<uint8_t*>(&data), sizeof(data));
- data.magic = magic;
- EEPROM.begin(sizeof(data));
- EEPROM.put(0, data);
- return EEPROM.end();
+ write(data);
}
bool isValid(ConfigData& data) {
@@ -69,11 +65,11 @@ bool isDirty(ConfigData& data) {
return data.magic != magic;
}
-char* ConfigData::escapeNodeId(char* buf, size_t len) {
+char* ConfigData::escapeHomeId(char* buf, size_t len) {
if (len < 32)
return nullptr;
- size_t id_len = strlen(node_id);
- char* c = node_id;
+ size_t id_len = strlen(home_id);
+ char* c = home_id;
char* dst = buf;
for (size_t i = 0; i < id_len; i++) {
if (*c == '"')
diff --git a/platformio/relayctl/src/config.def.h.example b/platformio/relayctl/src/config.def.h.example
index 13ddd46..dd3d89b 100644
--- a/platformio/relayctl/src/config.def.h.example
+++ b/platformio/relayctl/src/config.def.h.example
@@ -18,15 +18,15 @@
0x8a, 0xb0, 0xc9, 0x9c, 0xbb \
};
-#define DEFAULT_NODE_ID "relay-node"
+#define DEFAULT_HOME_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 SECRET ""
-
-#define DEBUG
+#define HOME_SECRET_SIZE 12
+#define HOME_SECRET ""
#define ARRAY_SIZE(X) sizeof((X))/sizeof((X)[0]) \ No newline at end of file
diff --git a/platformio/relayctl/src/config.h b/platformio/relayctl/src/config.h
index 58a5f25..562b61a 100644
--- a/platformio/relayctl/src/config.h
+++ b/platformio/relayctl/src/config.h
@@ -14,20 +14,20 @@ struct ConfigData {
// helpers
uint32_t crc = 0;
uint32_t magic = 0;
- char node_id[16] = {0};
+ char home_id[16] = {0};
char wifi_ssid[32] = {0};
char wifi_psk[63] = {0};
ConfigFlags flags {0};
// helper methods
- char* escapeNodeId(char* buf, size_t len);
+ char* escapeHomeId(char* buf, size_t len);
} __attribute__((packed));
ConfigData read();
-bool write(ConfigData& data);
-bool erase();
-bool erase(ConfigData& data);
+void write(ConfigData& data);
+void erase();
+void erase(ConfigData& data);
bool isValid(ConfigData& data);
bool isDirty(ConfigData& data);
diff --git a/platformio/relayctl/src/http_server.cpp b/platformio/relayctl/src/http_server.cpp
index 0899231..e1e9c10 100644
--- a/platformio/relayctl/src/http_server.cpp
+++ b/platformio/relayctl/src/http_server.cpp
@@ -1,21 +1,26 @@
#include <Arduino.h>
#include <string.h>
+#include "static.h"
#include "http_server.h"
#include "config.h"
-#include "wifi.h"
#include "config.def.h"
#include "logging.h"
+#include "util.h"
+#include "led.h"
namespace homekit {
-static const char CONTENT_TYPE_HTML[] = "text/html; charset=utf-8";
-static const char CONTENT_TYPE_CSS[] = "text/css";
-static const char CONTENT_TYPE_JS[] = "application/javascript";
-static const char CONTENT_TYPE_JSON[] = "application/json";
-static const char CONTENT_TYPE_FAVICON[] = "image/x-icon";
+using files::StaticFile;
-static const char JSON_STATUS_FMT[] = "{\"node_id\":\"%s\""
+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 = "{\"home_id\":\"%s\""
#ifdef DEBUG
",\"configured\":%d"
",\"crc\":%u"
@@ -25,38 +30,41 @@ static const char JSON_STATUS_FMT[] = "{\"node_id\":\"%s\""
"}";
static const size_t JSON_BUF_SIZE = 192;
-static const char NODE_ID_ERROR[] = "?";
-
-static const char FIELD_NODE_ID[] = "nid";
-static const char FIELD_SSID[] = "ssid";
-static const char FIELD_PSK[] = "psk";
+static const char JSON_SCAN_FIRST_LIST[] PROGMEM = "{\"list\":[";
-static const char MSG_IS_INVALID[] = " is invalid";
-static const char MSG_IS_MISSING[] = " is missing";
+static const char MSG_IS_INVALID[] PROGMEM = " is invalid";
+static const char MSG_IS_MISSING[] PROGMEM = " is missing";
-static const char GZIP[] = "gzip";
-static const char CONTENT_ENCODING[] = "Content-Encoding";
-static const char NOT_FOUND[] = "Not Found";
+static const char GZIP[] PROGMEM = "gzip";
+static const char CONTENT_ENCODING[] PROGMEM = "Content-Encoding";
+static const char NOT_FOUND[] PROGMEM = "Not Found";
-static void do_restart() {
- ESP.restart();
-}
+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("/style.css", HTTP_GET, [](AsyncWebServerRequest* req) { sendGzip(req, files::style_css, CONTENT_TYPE_CSS); });
- _server.on("/app.js", HTTP_GET, [](AsyncWebServerRequest* req) { sendGzip(req, files::app_js, CONTENT_TYPE_JS); });
- _server.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest* req) { sendGzip(req, files::favicon_ico, CONTENT_TYPE_FAVICON); });
- _server.on("/", HTTP_GET, [&](AsyncWebServerRequest* req) { sendGzip(req, files::index_html, CONTENT_TYPE_HTML); });
+ 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("/status", HTTP_GET, [](AsyncWebServerRequest* req) {
+ 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();
- char *ssid, *psk;
- wifi::getConfig(cfg, &ssid, &psk, nullptr);
if (!isValid(cfg) || !cfg.flags.node_configured) {
- sprintf(json_buf, JSON_STATUS_FMT
- , DEFAULT_NODE_ID
+ sprintf_P(json_buf, JSON_STATUS_FMT
+ , DEFAULT_HOME_ID
#ifdef DEBUG
, 0
, cfg.crc
@@ -65,10 +73,10 @@ void HttpServer::start() {
#endif
);
} else {
- char escaped_node_id[32];
- char *escaped_node_id_res = cfg.escapeNodeId(escaped_node_id, 32);
- sprintf(json_buf, JSON_STATUS_FMT
- , escaped_node_id_res == nullptr ? NODE_ID_ERROR : escaped_node_id
+ char escaped_home_id[32];
+ char *escaped_home_id_res = cfg.escapeHomeId(escaped_home_id, 32);
+ sprintf_P(json_buf, JSON_STATUS_FMT
+ , escaped_home_id_res == nullptr ? "?" : escaped_home_id
#ifdef DEBUG
, 1
, cfg.crc
@@ -77,58 +85,54 @@ void HttpServer::start() {
#endif
);
}
- req->send(200, CONTENT_TYPE_JSON, json_buf);
+ server.send(200, FPSTR(CONTENT_TYPE_JSON), json_buf);
});
-
- _server.on("/status", HTTP_POST, [&](AsyncWebServerRequest* req) {
+ server.on(FPSTR(ROUTE_STATUS), HTTP_POST, [&]() {
auto cfg = config::read();
- char *s;
+ String s;
- if (!handleInputStr(req, FIELD_SSID, 32, &s)) return;
- strncpy(cfg.wifi_ssid, s, 32);
+ if (!getInputParam("ssid", 32, s)) return;
+ strncpy(cfg.wifi_ssid, s.c_str(), 32);
PRINTF("saving ssid: %s\n", cfg.wifi_ssid);
- if (!handleInputStr(req, FIELD_PSK, 63, &s)) return;
- strncpy(cfg.wifi_psk, s, 63);
+ if (!getInputParam("psk", 63, s)) return;
+ strncpy(cfg.wifi_psk, s.c_str(), 63);
PRINTF("saving psk: %s\n", cfg.wifi_psk);
- if (!handleInputStr(req, FIELD_NODE_ID, 16, &s)) return;
- strcpy(cfg.node_id, s);
- PRINTF("saving node id: %s\n", cfg.node_id);
+ if (!getInputParam("hid", 16, s)) return;
+ strcpy(cfg.home_id, s.c_str());
+ PRINTF("saving home id: %s\n", cfg.home_id);
cfg.flags.node_configured = 1;
cfg.flags.wifi_configured = 1;
- if (!config::write(cfg)) {
- PRINTLN("eeprom write error");
- return sendError(req, "eeprom error");
- }
+ config::write(cfg);
- restartTimer.once(0, do_restart);
+ restartTimer.once(0, restart);
});
- _server.on("/reset", HTTP_POST, [&](AsyncWebServerRequest* req) {
+ server.on(FPSTR(ROUTE_RESET), HTTP_POST, [&]() {
config::erase();
- restartTimer.once(0, do_restart);
+ restartTimer.once(1, restart);
});
- _server.on("/heap", HTTP_GET, [](AsyncWebServerRequest* req) {
- req->send(200, CONTENT_TYPE_HTML, String(ESP.getFreeHeap()));
+ server.on(FPSTR(ROUTE_HEAP), HTTP_GET, [&]() {
+ server.send(200, FPSTR(CONTENT_TYPE_HTML), String(ESP.getFreeHeap()));
});
- _server.on("/scan", HTTP_GET, [&](AsyncWebServerRequest* req) {
- int i = 0;
+ 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*>(buf1k), ARRAY_SIZE(buf1k));
- char* cur = buf1k;
+ bzero(reinterpret_cast<uint8_t*>(scanBuf), scanBufSize);
+ char* cur = scanBuf;
- strncpy(cur, "{\"list\":[", ARRAY_SIZE(buf1k));
+ strncpy_P(cur, JSON_SCAN_FIRST_LIST, scanBufSize);
cur += 9;
- for (auto& res: *_scanResults) {
+ for (auto& res: *scanResults) {
ssid = res.ssid.c_str();
len = res.ssid.length();
@@ -151,10 +155,10 @@ void HttpServer::start() {
// close array
*cur++ = ']';
- if (cur - buf1k >= ARRAY_SIZE(buf1k)-40)
+ if ((size_t)(cur - scanBuf) >= (size_t) ARRAY_SIZE(scanBuf) - 40)
enough = true;
- if (i < _scanResults->size()-1 || enough)
+ if (i < scanResults->size() - 1 || enough)
*cur++ = ',';
if (enough)
@@ -167,82 +171,108 @@ void HttpServer::start() {
*cur++ = '}';
*cur++ = '\0';
- req->send(200, CONTENT_TYPE_JSON, buf1k);
+ server.send(200, FPSTR(CONTENT_TYPE_JSON), scanBuf);
});
- _server.on("/update", HTTP_POST, [&](AsyncWebServerRequest* req) {
+ server.on(FPSTR(ROUTE_UPDATE), HTTP_POST, [&]() {
char json_buf[16];
- bool should_reboot = !Update.hasError();
+ bool should_reboot = !Update.hasError() && !ota.invalidMd5;
+ Update.clearError();
+
+ sprintf_P(json_buf, JSON_UPDATE_FMT, should_reboot ? 1 : 0);
- sprintf(json_buf, "{\"result\":%d}", should_reboot ? 1 : 0);
+ server.send(200, FPSTR(CONTENT_TYPE_JSON), json_buf);
- auto resp = req->beginResponse(200, CONTENT_TYPE_JSON, json_buf);
- req->send(resp);
+ if (should_reboot)
+ restartTimer.once(1, restart);
+ }, [&]() {
+ HTTPUpload& upload = server.upload();
- if (should_reboot) restartTimer.once(1, do_restart);
- }, [&](AsyncWebServerRequest *req, const String& filename, size_t index, uint8_t *data, size_t len, bool final) {
- if (!index) {
- PRINTF("update start: %s\n", filename.c_str());
- Update.runAsync(true);
- if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000))
+ 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;
- if (!Update.hasError() && len) {
- if (Update.write(data, len) != len) {
+ 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 (final) { // if the final flag is set then this is the last frame of data
if (Update.end(true)) {
- PRINTF("update success: %uB\n", index+len);
+ PRINTF("http/ota: ok, total size %ul\n", upload.totalSize);
} else {
+#ifdef DEBUG
Update.printError(Serial);
+#endif
}
}
});
- _server.onNotFound([](AsyncWebServerRequest* req) {
- req->send(404, CONTENT_TYPE_HTML, NOT_FOUND);
+ server.onNotFound([&]() {
+ server.send(404, FPSTR(CONTENT_TYPE_HTML), NOT_FOUND);
});
- _server.begin();
+ server.begin();
}
-void HttpServer::sendGzip(AsyncWebServerRequest* req, StaticFile file, const char* content_type) {
- auto resp = req->beginResponse_P(200, content_type, file.content, file.size);
- resp->addHeader(CONTENT_ENCODING, GZIP);
- req->send(resp);
+void HttpServer::loop() {
+ server.handleClient();
}
-void HttpServer::sendError(AsyncWebServerRequest* req, const String& message) {
+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(buf, 32, "error: %s", message.c_str()) == 32)
+ if (snprintf_P(buf, 32, PSTR("error: %s"), message.c_str()) == 32)
buf[31] = '\0';
- req->send(400, CONTENT_TYPE_HTML, buf);
+ server.send(400, FPSTR(CONTENT_TYPE_HTML), buf);
}
-bool HttpServer::handleInputStr(AsyncWebServerRequest *req,
- const char *field_name,
- size_t max_len,
- char **dst) {
- const char* s;
- size_t len;
-
- if (!req->hasParam(field_name, true)) {
- sendError(req, String(field_name) + String(MSG_IS_MISSING));
+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;
}
- s = req->getParam(field_name, true)->value().c_str();
- len = strlen(s);
- if (!len || len > max_len) {
- sendError(req, String(FIELD_NODE_ID) + String(MSG_IS_INVALID));
+ 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 = (char*)s;
+ dst = field;
return true;
}
diff --git a/platformio/relayctl/src/http_server.h b/platformio/relayctl/src/http_server.h
index c7e3d9c..35f7c08 100644
--- a/platformio/relayctl/src/http_server.h
+++ b/platformio/relayctl/src/http_server.h
@@ -1,37 +1,56 @@
#pragma once
-
+#include <ESP8266WebServer.h>
+#include <Ticker.h>
#include <memory>
#include <list>
-#include <Ticker.h>
#include <utility>
-#include <ESPAsyncWebServer.h>
-#include "static.h"
#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:
- AsyncWebServer _server;
+ ESP8266WebServer server;
Ticker restartTimer;
- std::shared_ptr<std::list<wifi::ScanResult>> _scanResults;
- char buf1k[1024];
+ std::shared_ptr<std::list<wifi::ScanResult>> scanResults;
+ OTAStatus ota;
- static void sendGzip(AsyncWebServerRequest* req, StaticFile file, const char* content_type);
- static void sendError(AsyncWebServerRequest* req, const String& message);
+ char* scanBuf;
+ size_t scanBufSize;
- static bool handleInputStr(AsyncWebServerRequest* req, const char* field_name, size_t max_len, char** dst);
- // static bool handle_input_addr(AsyncWebServerRequest* req, const char* field_name, ConfigIPv4Addr* addr_dst);
+ 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)) {};
+ : 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/relayctl/src/led.cpp b/platformio/relayctl/src/led.cpp
index eafc662..b6b0d81 100644
--- a/platformio/relayctl/src/led.cpp
+++ b/platformio/relayctl/src/led.cpp
@@ -1,20 +1,9 @@
#include "led.h"
+#include "config.def.h"
namespace homekit {
-void Led::blink(uint8_t count, uint16_t delay_ms) const {
- for (uint8_t i = 0; i < count; i++) {
- on_off(delay_ms, i < count-1);
- }
-}
-
-void Led::on_off(uint16_t delay_ms, bool last_delay) const {
- on();
- delay(delay_ms);
-
- off();
- if (last_delay)
- delay(delay_ms);
-}
+Led board_led(BOARD_LED_PIN);
+Led esp_led(ESP_LED_PIN);
} \ No newline at end of file
diff --git a/platformio/relayctl/src/led.h b/platformio/relayctl/src/led.h
index ccb3f11..315523a 100644
--- a/platformio/relayctl/src/led.h
+++ b/platformio/relayctl/src/led.h
@@ -17,8 +17,23 @@ public:
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;
- void blink(uint8_t count, uint16_t delay_ms) const;
+ 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/relayctl/src/logging.cpp b/platformio/relayctl/src/logging.cpp
deleted file mode 100644
index a929d81..0000000
--- a/platformio/relayctl/src/logging.cpp
+++ /dev/null
@@ -1,37 +0,0 @@
-#include <stdio.h>
-#include "logging.h"
-
-#ifdef DEBUG
-namespace homekit {
-
-void hexdump(const void* data, size_t size) {
- char ascii[17];
- size_t i, j;
- ascii[16] = '\0';
- for (i = 0; i < size; ++i) {
- printf("%02X ", ((unsigned char*)data)[i]);
- if (((unsigned char*)data)[i] >= ' ' && ((unsigned char*)data)[i] <= '~') {
- ascii[i % 16] = ((unsigned char*)data)[i];
- } else {
- ascii[i % 16] = '.';
- }
- if ((i+1) % 8 == 0 || i+1 == size) {
- printf(" ");
- if ((i+1) % 16 == 0) {
- printf("| %s \n", ascii);
- } else if (i+1 == size) {
- ascii[(i+1) % 16] = '\0';
- if ((i+1) % 16 <= 8) {
- printf(" ");
- }
- for (j = (i+1) % 16; j < 16; ++j) {
- printf(" ");
- }
- printf("| %s \n", ascii);
- }
- }
- }
-}
-
-}
-#endif \ No newline at end of file
diff --git a/platformio/relayctl/src/logging.h b/platformio/relayctl/src/logging.h
index c31e98f..070f367 100644
--- a/platformio/relayctl/src/logging.h
+++ b/platformio/relayctl/src/logging.h
@@ -5,20 +5,14 @@
#ifdef DEBUG
-namespace homekit {
-void hexdump(const void* data, size_t size);
-}
-
#define PRINTLN(s) Serial.println(s)
#define PRINT(s) Serial.print(s)
#define PRINTF(fmt, ...) Serial.printf(fmt, ##__VA_ARGS__)
-#define HEXDUMP(data, size) homekit::hexdump((data), (size));
#else
#define PRINTLN(s)
#define PRINT(s)
-#define PRINTF(a)
-#define HEXDUMP(data, size)
+#define PRINTF(...)
#endif
diff --git a/platformio/relayctl/src/main.cpp b/platformio/relayctl/src/main.cpp
index 4a634d6..0c4d469 100644
--- a/platformio/relayctl/src/main.cpp
+++ b/platformio/relayctl/src/main.cpp
@@ -15,9 +15,6 @@
using namespace homekit;
-static Led board_led(BOARD_LED_PIN);
-static Led esp_led(ESP_LED_PIN);
-
enum class WorkingMode {
RECOVERY, // AP mode, http server with configuration
NORMAL, // MQTT client
@@ -39,13 +36,13 @@ static WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler;
static Ticker wifiTimer;
static StopWatch blinkStopWatch;
-static DNSServer* dnsServer;
+static DNSServer* dnsServer = nullptr;
static void onWifiConnected(const WiFiEventStationModeGotIP& event);
static void onWifiDisconnected(const WiFiEventStationModeDisconnected& event);
static void wifiConnect() {
- char* ssid, *psk, *hostname;
+ const char *ssid, *psk, *hostname;
auto cfg = config::read();
wifi::getConfig(cfg, &ssid, &psk, &hostname);
@@ -54,16 +51,10 @@ static void wifiConnect() {
wifi_state = WiFiConnectionState::WAITING;
WiFi.mode(WIFI_STA);
- WiFi.setHostname(hostname);
+ WiFi.hostname(hostname);
WiFi.begin(ssid, psk);
PRINT("connecting to wifi..");
- while (WiFi.status() != WL_CONNECTED) {
- esp_led.blink(2, 50);
- delay(1000);
- PRINT('.');
- }
- PRINT(' ');
}
static void wifiHotspot() {
@@ -72,7 +63,7 @@ static void wifiHotspot() {
auto scanResults = wifi::scan();
WiFi.mode(WIFI_AP);
- WiFi.softAP(wifi::WIFI_AP_SSID);
+ WiFi.softAP(wifi::AP_SSID);
dnsServer = new DNSServer();
dnsServer->start(53, "*", WiFi.softAPIP());
@@ -82,6 +73,8 @@ static void wifiHotspot() {
}
void setup() {
+ WiFi.disconnect();
+
#ifdef DEBUG
Serial.begin(115200);
#endif
@@ -119,6 +112,13 @@ void setup() {
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;
@@ -131,28 +131,29 @@ void loop() {
}
auto mqtt = (mqtt::MQTT*)service;
- if (static_cast<int>(wifi_state) >= 1
- && mqtt != nullptr) {
- if (!mqtt->loop()) {
- PRINTLN("mqtt::loop() returned false");
- // FIXME do something here
- }
+ if (static_cast<int>(wifi_state) >= 1 && mqtt != nullptr) {
+ mqtt->loop();
- if (mqtt->statStopWatch.elapsed(10000)) {
+ if (mqtt->ota.readyToRestart) {
+ mqtt->disconnect();
+ } else if (mqtt->statStopWatch.elapsed(10000)) {
mqtt->sendStat();
}
// periodically blink board led
if (blinkStopWatch.elapsed(5000)) {
+ // PRINTF("free heap: %d\n", ESP.getFreeHeap());
board_led.blink(1, 10);
blinkStopWatch.save();
}
}
-
- delay(500);
} else {
if (dnsServer != nullptr)
dnsServer->processNextRequest();
+
+ auto httpServer = (HttpServer*)service;
+ if (httpServer != nullptr)
+ httpServer->loop();
}
}
diff --git a/platformio/relayctl/src/mqtt.cpp b/platformio/relayctl/src/mqtt.cpp
index cca215b..e1f70c3 100644
--- a/platformio/relayctl/src/mqtt.cpp
+++ b/platformio/relayctl/src/mqtt.cpp
@@ -1,9 +1,13 @@
+#include <ESP8266httpUpdate.h>
#include "mqtt.h"
#include "logging.h"
#include "wifi.h"
#include "config.def.h"
#include "relay.h"
#include "config.h"
+#include "static.h"
+#include "util.h"
+#include "led.h"
namespace homekit::mqtt {
@@ -13,26 +17,124 @@ 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 MQTT_SECRET[] = SECRET;
-static const char TOPIC_RELAY_POWER[] = "relay/power";
static const char TOPIC_STAT[] = "stat";
-static const char TOPIC_STAT1[] = "stat1";
-static const char TOPIC_ADMIN[] = "admin";
-static const char TOPIC_RELAY[] = "relay";
+static const char TOPIC_INITIAL_STAT[] = "stat1";
+static const char TOPIC_OTA_RESPONSE[] = "otares";
+static const char TOPIC_RELAY_POWER[] = "power";
+static const char TOPIC_ADMIN_OTA[] = "admin/ota";
+static const uint16_t MQTT_KEEPALIVE = 30;
+enum class IncomingMessage {
+ UNKNOWN,
+ RELAY_POWER,
+ OTA
+};
-using namespace homekit;
+using namespace espMqttClientTypes;
+
+#define MD5_SIZE 16
+
+MQTT::MQTT() {
+ auto cfg = config::read();
+ homeId = String(cfg.flags.node_configured ? cfg.home_id : wifi::HOME_ID);
-MQTT::MQTT() : client(wifiClient) {
randomSeed(micros());
- wifiClient.setFingerprint(MQTT_CA_FINGERPRINT);
+ client.onConnect([&](bool sessionPresent) {
+ PRINTLN("mqtt: connected");
- client.setServer(MQTT_SERVER, MQTT_PORT);
- client.setCallback([&](char* topic, byte* payload, unsigned int length) {
- this->callback(topic, payload, length);
+ sendInitialStat();
+
+ subscribe(TOPIC_RELAY_POWER, 1);
+ 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_RELAY_POWER)
+ msgType = IncomingMessage::RELAY_POWER;
+ else 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::RELAY_POWER:
+ handleRelayPowerPayload(payload, total);
+ break;
+
+ 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() {
@@ -40,127 +142,96 @@ void MQTT::connect() {
}
void MQTT::reconnect() {
- char buf[128] {0};
-
if (client.connected()) {
PRINTLN("warning: already connected");
return;
}
-
- // Attempt to connect
- if (client.connect(MQTT_CLIENT_ID, MQTT_USERNAME, MQTT_PASSWORD)) {
- PRINTLN("mqtt: connected");
-
- sendInitialStat();
-
- subscribe(TOPIC_RELAY);
- subscribe(TOPIC_ADMIN);
- } else {
- PRINTF("mqtt: failed to connect, rc=%d\n", client.state());
- wifiClient.getLastSSLError(buf, sizeof(buf));
- PRINTF("SSL error: %s\n", buf);
-
- reconnectTimer.once(2, [&]() {
- reconnect();
- });
- }
+ client.connect();
}
void MQTT::disconnect() {
// TODO test how this works???
reconnectTimer.detach();
client.disconnect();
- wifiClient.stop();
}
-bool MQTT::loop() {
- return client.loop();
+uint16_t MQTT::publish(const String &topic, uint8_t *payload, size_t length) {
+ String fullTopic = "hk/" + homeId + "/relay/" + topic;
+ return client.publish(fullTopic.c_str(), 1, false, payload, length);
}
-bool MQTT::publish(const char* topic, uint8_t *payload, size_t length) {
- char full_topic[40] {0};
- strcpy(full_topic, "/hk/");
- strcat(full_topic, wifi::NODE_ID);
- strcat(full_topic, "/");
- strcat(full_topic, topic);
- return client.publish(full_topic, payload, length);
+void MQTT::loop() {
+ client.loop();
}
-bool MQTT::subscribe(const char *topic) {
- char full_topic[40] {0};
- strcpy(full_topic, "/hk/");
- strcat(full_topic, wifi::NODE_ID);
- strcat(full_topic, "/");
- strcat(full_topic, topic);
- strcat(full_topic, "/#");
- bool res = client.subscribe(full_topic, 1);
- if (!res)
- PRINTF("error: failed to subscribe to %s\n", full_topic);
- return res;
+uint16_t MQTT::subscribe(const String &topic, uint8_t qos) {
+ String fullTopic = "hk/" + homeId + "/relay/" + 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::sendInitialStat() {
auto cfg = config::read();
- InitialStatPayload stat {
- .ip = wifi::getIPAsInteger(),
- .fw_version = FW_VERSION,
- .rssi = wifi::getRSSI(),
- .free_heap = ESP.getFreeHeap(),
- .flags = StatFlags {
- .state = static_cast<uint8_t>(relay::getState() ? 1 : 0),
- .config_changed_value_present = 1,
- .config_changed = static_cast<uint8_t>(cfg.flags.node_configured || cfg.flags.wifi_configured ? 1 : 0)
- }
+ InitialStatPayload stat{
+ .ip = wifi::getIPAsInteger(),
+ .fw_version = FW_VERSION,
+ .rssi = wifi::getRSSI(),
+ .free_heap = ESP.getFreeHeap(),
+ .flags = StatFlags{
+ .state = static_cast<uint8_t>(relay::getState() ? 1 : 0),
+ .config_changed_value_present = 1,
+ .config_changed = static_cast<uint8_t>(cfg.flags.node_configured ||
+ cfg.flags.wifi_configured ? 1 : 0)
+ }
};
- publish(TOPIC_STAT1, reinterpret_cast<uint8_t*>(&stat), sizeof(stat));
+ publish(TOPIC_INITIAL_STAT, reinterpret_cast<uint8_t*>(&stat), sizeof(stat));
statStopWatch.save();
}
void MQTT::sendStat() {
- StatPayload stat {
+ StatPayload stat{
.rssi = wifi::getRSSI(),
.free_heap = ESP.getFreeHeap(),
- .flags = StatFlags {
+ .flags = StatFlags{
.state = static_cast<uint8_t>(relay::getState() ? 1 : 0),
.config_changed_value_present = 0,
.config_changed = 0
}
};
-
- PRINTF("free heap: %d\n", ESP.getFreeHeap());
-
publish(TOPIC_STAT, reinterpret_cast<uint8_t*>(&stat), sizeof(stat));
statStopWatch.save();
}
-void MQTT::callback(char* topic, uint8_t* payload, uint32_t length) {
- const size_t bufsize = 16;
- char relevant_topic[bufsize];
- strncpy(relevant_topic, topic+strlen(wifi::NODE_ID)+5, bufsize);
-
- if (strncmp(TOPIC_RELAY_POWER, relevant_topic, bufsize) == 0) {
- handleRelayPowerPayload(payload, length);
- } else {
- PRINTF("error: invalid topic %s\n", topic);
- }
+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::handleRelayPowerPayload(uint8_t *payload, uint32_t length) {
+void MQTT::handleRelayPowerPayload(const uint8_t *payload, uint32_t length) {
if (length != sizeof(PowerPayload)) {
PRINTF("error: size of payload (%ul) does not match expected (%ul)\n",
length, sizeof(PowerPayload));
return;
}
- auto pd = reinterpret_cast<struct PowerPayload*>(payload);
+ auto pd = reinterpret_cast<const struct PowerPayload*>(payload);
if (strncmp(pd->secret, MQTT_SECRET, sizeof(pd->secret)) != 0) {
PRINTLN("error: invalid secret");
return;
}
if (pd->state == 1) {
+ PRINTLN("mqtt: turning relay on");
relay::setOn();
} else if (pd->state == 0) {
+ PRINTLN("mqtt: turning relay off");
relay::setOff();
} else {
PRINTLN("error: unexpected state value");
@@ -169,4 +240,114 @@ void MQTT::handleRelayPowerPayload(uint8_t *payload, uint32_t length) {
sendStat();
}
+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/relayctl/src/mqtt.h b/platformio/relayctl/src/mqtt.h
index a769750..f5ffdab 100644
--- a/platformio/relayctl/src/mqtt.h
+++ b/platformio/relayctl/src/mqtt.h
@@ -1,30 +1,69 @@
#include <ESP8266WiFi.h>
-#include <PubSubClient.h>
+#include <espMqttClient.h>
#include <Ticker.h>
#include "stopwatch.h"
-namespace homekit::mqtt {
+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:
- WiFiClientSecure wifiClient;
- PubSubClient client;
+ String homeId;
+ WiFiClientSecure httpsSecureClient;
+ espMqttClientSecure client;
Ticker reconnectTimer;
+ Ticker restartTimer;
+
+ void handleRelayPowerPayload(const uint8_t* payload, uint32_t length);
+ void handleAdminOtaPayload(uint16_t packetId, const uint8_t* payload, size_t length, size_t index, size_t total);
- void callback(char* topic, uint8_t* payload, size_t length);
- void handleRelayPowerPayload(uint8_t* payload, uint32_t length);
- bool publish(const char* topic, uint8_t* payload, size_t length);
- bool subscribe(const char* topic);
+ uint16_t publish(const String& topic, uint8_t* payload, size_t length);
+ uint16_t subscribe(const String& topic, uint8_t qos = 0);
void sendInitialStat();
+ uint16_t sendOtaResponse(OTAResult status, uint8_t error_code = 0);
public:
StopWatch statStopWatch;
+ OTAStatus ota;
MQTT();
void connect();
void disconnect();
void reconnect();
- bool loop();
+ void loop();
void sendStat();
};
@@ -54,4 +93,9 @@ struct PowerPayload {
uint8_t state;
} __attribute__((packed));
-} \ No newline at end of file
+struct OTAResponse {
+ OTAResult status;
+ uint8_t error_code;
+} __attribute__((packed));
+
+} } \ No newline at end of file
diff --git a/platformio/relayctl/src/relay.h b/platformio/relayctl/src/relay.h
index a3519ac..ba7f5a6 100644
--- a/platformio/relayctl/src/relay.h
+++ b/platformio/relayctl/src/relay.h
@@ -3,14 +3,14 @@
#include <Arduino.h>
#include "config.def.h"
-namespace homekit::relay {
+namespace homekit { namespace relay {
inline void init() {
pinMode(RELAY_PIN, OUTPUT);
}
inline bool getState() {
- return digitalRead(RELAY_PIN) == 1;
+ return digitalRead(RELAY_PIN) == HIGH;
}
inline void setOn() {
@@ -21,4 +21,4 @@ inline void setOff() {
digitalWrite(RELAY_PIN, LOW);
}
-} \ No newline at end of file
+} } \ No newline at end of file
diff --git a/platformio/relayctl/src/static.cpp b/platformio/relayctl/src/static.cpp
index 8e2c56d..0e60044 100644
--- a/platformio/relayctl/src/static.cpp
+++ b/platformio/relayctl/src/static.cpp
@@ -7,269 +7,443 @@
namespace homekit::files {
static const uint8_t index_html_content[] PROGMEM = {
- 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x9d, 0x56, 0xdb, 0x8a, 0xdc, 0x38,
- 0x10, 0x7d, 0x9f, 0xaf, 0x50, 0xf4, 0xb0, 0xcc, 0xc0, 0xb4, 0xbd, 0xd3, 0x81, 0x21, 0x6c, 0x6c,
- 0xc3, 0x92, 0x0b, 0x04, 0xc2, 0x32, 0x4c, 0x13, 0x02, 0x79, 0x69, 0x64, 0xb9, 0xdc, 0x56, 0x5a,
- 0x96, 0xb4, 0x56, 0xd9, 0x3d, 0xbd, 0x5f, 0x9f, 0x92, 0x6c, 0xf7, 0x65, 0xd3, 0x4c, 0x2e, 0x2f,
- 0x6d, 0x57, 0xa9, 0xea, 0xd4, 0xa9, 0xa3, 0x92, 0xd5, 0xd9, 0x8b, 0xca, 0x4a, 0xdc, 0x3b, 0x60,
- 0x0d, 0xb6, 0xba, 0xb8, 0xca, 0xc2, 0x83, 0x69, 0x61, 0x36, 0x39, 0x07, 0xc3, 0x83, 0x03, 0x44,
- 0x45, 0x8f, 0x16, 0x50, 0x50, 0x0c, 0xba, 0x05, 0xfc, 0xdb, 0xab, 0x21, 0xe7, 0xd2, 0x1a, 0x04,
- 0x83, 0x8b, 0x90, 0xcc, 0xd9, 0x64, 0xe5, 0x1c, 0xe1, 0x09, 0xd3, 0x00, 0xf2, 0x9a, 0xc9, 0x46,
- 0x74, 0x1e, 0x30, 0xef, 0xb1, 0x5e, 0xbc, 0xe2, 0x33, 0x86, 0x11, 0x2d, 0xe4, 0x7c, 0x50, 0xb0,
- 0x73, 0xb6, 0xc3, 0x93, 0xcc, 0x9d, 0xaa, 0xb0, 0xc9, 0x2b, 0x18, 0x94, 0x84, 0x45, 0x34, 0x6e,
- 0x95, 0x51, 0xa8, 0x84, 0x5e, 0x78, 0x29, 0x34, 0xe4, 0x77, 0xb7, 0x2d, 0x39, 0xda, 0xbe, 0x3d,
- 0xda, 0xe2, 0xe9, 0xcc, 0xee, 0x3d, 0x74, 0xd1, 0x10, 0x25, 0xd9, 0xc6, 0x86, 0xa2, 0xa8, 0x50,
- 0x43, 0xf1, 0xc6, 0x9a, 0x5a, 0x6d, 0xfa, 0x4e, 0xa0, 0xb2, 0x26, 0x4b, 0x47, 0xe7, 0x55, 0xa6,
- 0x95, 0xd9, 0xb2, 0x0e, 0x74, 0xce, 0x7d, 0x43, 0x6c, 0x64, 0x8f, 0x4c, 0x11, 0x21, 0xce, 0x9a,
- 0x0e, 0xea, 0x9c, 0xa7, 0xb5, 0x18, 0x82, 0x9d, 0xd0, 0x0f, 0x67, 0xa1, 0xd3, 0x9c, 0xab, 0x56,
- 0x6c, 0x20, 0x7d, 0x5a, 0xc4, 0xb8, 0x73, 0x08, 0xdc, 0x6b, 0xf0, 0x0d, 0x00, 0xce, 0xb1, 0x51,
- 0x0c, 0xe9, 0xfd, 0x01, 0x2f, 0x86, 0x24, 0xc1, 0x43, 0x99, 0x5e, 0x76, 0xca, 0x21, 0xf3, 0x9d,
- 0xa4, 0x15, 0xe1, 0x5c, 0xf2, 0x95, 0xdc, 0x59, 0x3a, 0xba, 0x69, 0x3d, 0x9d, 0xa4, 0x2f, 0x6d,
- 0xb5, 0x67, 0xd6, 0x68, 0x2b, 0x2a, 0x2a, 0x4f, 0x92, 0xfc, 0xed, 0xdc, 0xf5, 0x4d, 0x40, 0xa8,
- 0xd4, 0xc0, 0xa4, 0x16, 0xde, 0x53, 0xa9, 0xd0, 0x11, 0x2f, 0x56, 0x80, 0xa8, 0xcc, 0xc6, 0xb3,
- 0xcc, 0x3b, 0x61, 0x98, 0xa2, 0x8c, 0x90, 0x47, 0xae, 0x35, 0x89, 0x02, 0x9a, 0x17, 0xd7, 0x93,
- 0x9d, 0x24, 0xc9, 0x0d, 0x15, 0xa3, 0x28, 0xaa, 0x49, 0x40, 0xe7, 0x70, 0xa5, 0xb6, 0x72, 0x1b,
- 0x4a, 0xd4, 0xb6, 0x6b, 0x19, 0x6d, 0x5c, 0x63, 0x09, 0xca, 0x59, 0x4f, 0xbd, 0x09, 0x19, 0x44,
- 0x8c, 0xdd, 0x08, 0xec, 0xa9, 0xb9, 0x71, 0x4b, 0x0d, 0xe0, 0xce, 0x76, 0xdb, 0xb5, 0x9f, 0x28,
- 0xfc, 0x8f, 0x60, 0x00, 0x9a, 0x39, 0x7c, 0x56, 0xef, 0x15, 0x5b, 0xad, 0x3e, 0xbc, 0xbd, 0x50,
- 0x39, 0xc6, 0x29, 0xe3, 0x7a, 0x8c, 0x1a, 0x81, 0x06, 0x89, 0xb1, 0x0f, 0xef, 0x55, 0xb5, 0x1e,
- 0xed, 0xb9, 0x64, 0x70, 0xf1, 0x43, 0x62, 0xaf, 0xf5, 0x38, 0x37, 0x21, 0xd1, 0xba, 0x40, 0x92,
- 0x0d, 0x42, 0xf7, 0x14, 0xc8, 0x8b, 0x8f, 0x87, 0xae, 0xb3, 0x74, 0x5c, 0x0b, 0x0a, 0x8f, 0x70,
- 0xe1, 0xed, 0x32, 0x8f, 0x53, 0xbe, 0x0f, 0xe4, 0xa6, 0x06, 0xab, 0x1f, 0x72, 0x8e, 0x2f, 0xd3,
- 0x04, 0xb8, 0x29, 0x89, 0x1f, 0x98, 0x4c, 0xd4, 0x9d, 0xdf, 0x5e, 0x62, 0x1e, 0x3b, 0xad, 0x75,
- 0xb5, 0x8e, 0xeb, 0x34, 0xdf, 0x1a, 0xcc, 0x86, 0x8e, 0x05, 0xbf, 0x7f, 0xc9, 0x59, 0xa5, 0x7c,
- 0x18, 0xec, 0xea, 0x42, 0x71, 0xd9, 0x80, 0xdc, 0x96, 0xf6, 0x29, 0x4e, 0x64, 0x20, 0xcd, 0xc8,
- 0x1d, 0xa7, 0x7a, 0x17, 0xa1, 0x8a, 0x33, 0x56, 0x87, 0xe8, 0x59, 0xc7, 0x39, 0x6c, 0x14, 0xfa,
- 0x90, 0xc4, 0xc2, 0x2b, 0x73, 0x87, 0xc6, 0x23, 0xf2, 0x51, 0xad, 0xe7, 0x45, 0xfb, 0xc7, 0x56,
- 0xc0, 0x7e, 0x62, 0x8b, 0x4f, 0x89, 0x85, 0x03, 0x73, 0x22, 0xd5, 0x49, 0xff, 0x77, 0xf7, 0x87,
- 0x39, 0x0b, 0x7b, 0x3e, 0xcb, 0x64, 0x2e, 0x0f, 0xc0, 0xa9, 0x54, 0x53, 0xfd, 0xb2, 0x47, 0xa4,
- 0x81, 0x18, 0xeb, 0xf8, 0xbe, 0x6c, 0x15, 0x1e, 0xc3, 0x66, 0x1d, 0x46, 0x77, 0xb1, 0x12, 0x03,
- 0x30, 0x61, 0x2a, 0xf6, 0x08, 0xa5, 0xb5, 0x98, 0xa5, 0x63, 0x72, 0x00, 0x0b, 0xdc, 0x2f, 0xb6,
- 0x3e, 0x1d, 0xc0, 0x4f, 0xae, 0x12, 0x08, 0xac, 0x56, 0x5d, 0xbb, 0x13, 0x1d, 0xb0, 0xeb, 0xa4,
- 0x54, 0xe6, 0xe6, 0x77, 0x4f, 0x58, 0x1f, 0xd1, 0x38, 0x03, 0x23, 0x47, 0xe2, 0x6d, 0xaf, 0x51,
- 0x39, 0xd1, 0x61, 0x24, 0xb2, 0xa0, 0x55, 0x31, 0xeb, 0x32, 0xc6, 0x3e, 0x7b, 0xfc, 0x2e, 0x6a,
- 0x5e, 0x2b, 0xe2, 0x4d, 0x25, 0x25, 0x38, 0xfa, 0x0a, 0x07, 0xba, 0xb7, 0xe1, 0x27, 0xd9, 0xfc,
- 0x37, 0x23, 0xc7, 0x88, 0x1f, 0x28, 0x79, 0x2e, 0xe0, 0x27, 0x17, 0xbe, 0x32, 0xbf, 0xa2, 0xdb,
- 0x23, 0x10, 0x71, 0x36, 0x93, 0xff, 0x5d, 0xbd, 0xba, 0x80, 0xc2, 0x7f, 0x8a, 0xe3, 0x8c, 0xab,
- 0xfc, 0x7a, 0xca, 0x8a, 0x14, 0x7e, 0x85, 0xf3, 0x07, 0x53, 0xdb, 0x67, 0x98, 0xbe, 0x5b, 0x3d,
- 0xbc, 0x5a, 0xde, 0xdf, 0x2f, 0x4a, 0xe1, 0x69, 0xc2, 0xb2, 0xb2, 0xa0, 0x5b, 0x42, 0xec, 0x25,
- 0x6a, 0xaa, 0x51, 0xdc, 0x1e, 0x47, 0x64, 0x58, 0x66, 0x65, 0x57, 0x5c, 0x3d, 0xd0, 0xae, 0x32,
- 0x5b, 0xb3, 0x4c, 0x4c, 0xb7, 0x45, 0xb8, 0x6d, 0xfd, 0x5f, 0x69, 0xba, 0x51, 0x98, 0xc8, 0xe6,
- 0xce, 0x25, 0xca, 0xa6, 0x8d, 0x6d, 0x61, 0x4b, 0x36, 0xf9, 0x52, 0x5e, 0x4c, 0x56, 0x96, 0x8a,
- 0x82, 0x95, 0xfb, 0xef, 0x33, 0xa7, 0x2c, 0x5e, 0xbc, 0x1b, 0x36, 0x60, 0xf6, 0xec, 0x8b, 0x32,
- 0x96, 0x6e, 0xde, 0x21, 0x26, 0xfc, 0x21, 0xad, 0xdb, 0xbf, 0x66, 0xcb, 0x3f, 0x97, 0xcb, 0xe3,
- 0x89, 0x0e, 0x77, 0x4d, 0xbc, 0x7a, 0xe2, 0xbf, 0x81, 0x6f, 0xa3, 0xf2, 0xc7, 0xe5, 0x1e, 0x08,
- 0x00, 0x00,
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x9d, 0x56, 0x4d, 0x8f, 0xdb, 0x36,
+ 0x10, 0xbd, 0xef, 0xaf, 0x60, 0x78, 0x28, 0x76, 0x81, 0xb5, 0xd4, 0xdd, 0xa0, 0x46, 0xd0, 0x48,
+ 0x02, 0x82, 0x34, 0x45, 0x02, 0xe4, 0xb0, 0x58, 0x23, 0x28, 0xd0, 0x8b, 0x41, 0x51, 0x23, 0x8b,
+ 0x35, 0x45, 0xb2, 0xe2, 0x48, 0x5e, 0xe7, 0xd7, 0x77, 0x48, 0x49, 0xfe, 0xd8, 0x1a, 0x9b, 0x8f,
+ 0x8b, 0xa5, 0x19, 0xce, 0xbc, 0x79, 0xf3, 0x38, 0x14, 0x9d, 0xbd, 0xaa, 0xac, 0xc4, 0xbd, 0x03,
+ 0xd6, 0x60, 0xab, 0x8b, 0xab, 0x2c, 0x3c, 0x98, 0x16, 0x66, 0x93, 0x73, 0x30, 0x3c, 0x38, 0x40,
+ 0x54, 0xf4, 0x68, 0x01, 0x05, 0xc5, 0xa0, 0x5b, 0xc0, 0xbf, 0xbd, 0x1a, 0x72, 0x2e, 0xad, 0x41,
+ 0x30, 0xb8, 0x08, 0xc9, 0x9c, 0x4d, 0x56, 0xce, 0x11, 0x9e, 0x30, 0x0d, 0x20, 0x6f, 0x99, 0x6c,
+ 0x44, 0xe7, 0x01, 0xf3, 0x1e, 0xeb, 0xc5, 0x1b, 0x3e, 0x63, 0x18, 0xd1, 0x42, 0xce, 0x07, 0x05,
+ 0x3b, 0x67, 0x3b, 0x3c, 0xc9, 0xdc, 0xa9, 0x0a, 0x9b, 0xbc, 0x82, 0x41, 0x49, 0x58, 0x44, 0xe3,
+ 0x56, 0x19, 0x85, 0x4a, 0xe8, 0x85, 0x97, 0x42, 0x43, 0x7e, 0x77, 0xdb, 0x92, 0xa3, 0xed, 0xdb,
+ 0xa3, 0x2d, 0x9e, 0xce, 0xec, 0xde, 0x43, 0x17, 0x0d, 0x51, 0x92, 0x6d, 0x6c, 0x28, 0x8a, 0x0a,
+ 0x35, 0x14, 0xef, 0xad, 0xa9, 0xd5, 0xa6, 0xef, 0x04, 0x2a, 0x6b, 0xb2, 0x74, 0x74, 0x5e, 0x65,
+ 0x5a, 0x99, 0x2d, 0xeb, 0x40, 0xe7, 0xdc, 0x37, 0xc4, 0x46, 0xf6, 0xc8, 0x14, 0x11, 0xe2, 0xac,
+ 0xe9, 0xa0, 0xce, 0x79, 0x5a, 0x8b, 0x21, 0xd8, 0x09, 0xfd, 0x70, 0x16, 0x3a, 0xcd, 0xb9, 0x6a,
+ 0xc5, 0x06, 0xd2, 0xa7, 0x45, 0x8c, 0x3b, 0x87, 0xc0, 0xbd, 0x06, 0xdf, 0x00, 0xe0, 0x1c, 0x1b,
+ 0xc5, 0x90, 0xde, 0x1f, 0xf0, 0x62, 0x48, 0x12, 0x3c, 0x94, 0xe9, 0x65, 0xa7, 0x1c, 0x32, 0xdf,
+ 0x49, 0x5a, 0x69, 0xab, 0xdf, 0x92, 0x7f, 0xc8, 0x9d, 0xa5, 0xa3, 0xfb, 0xf9, 0xba, 0x70, 0xee,
+ 0xf9, 0x7a, 0x3a, 0x6d, 0x4d, 0x69, 0xab, 0x3d, 0xb3, 0x46, 0x5b, 0x51, 0x11, 0x3d, 0x92, 0xec,
+ 0x9d, 0x73, 0xd7, 0x37, 0xa1, 0x42, 0xa5, 0x06, 0x26, 0xb5, 0xf0, 0x9e, 0xa8, 0x84, 0x8e, 0x79,
+ 0xb1, 0x02, 0x44, 0x65, 0x36, 0x9e, 0x65, 0xde, 0x09, 0xc3, 0x14, 0x65, 0x84, 0x3c, 0x72, 0xad,
+ 0x49, 0x34, 0xd0, 0xbc, 0xb8, 0x9e, 0xec, 0x24, 0x49, 0x6e, 0xa8, 0x18, 0x45, 0x51, 0x4d, 0x02,
+ 0x3a, 0x87, 0x2b, 0xb5, 0x95, 0xdb, 0x50, 0xa2, 0xb6, 0x5d, 0xcb, 0x68, 0x63, 0x1b, 0x4b, 0x50,
+ 0xce, 0x7a, 0xea, 0x5d, 0xc8, 0x20, 0x72, 0xec, 0x56, 0x60, 0x4f, 0xcd, 0x8f, 0x5b, 0x6e, 0x00,
+ 0x77, 0xb6, 0xdb, 0xae, 0xfd, 0x44, 0xe1, 0x19, 0xc1, 0x00, 0x34, 0x73, 0xf8, 0x4b, 0xfd, 0xa9,
+ 0xd8, 0x6a, 0xf5, 0xe9, 0x8f, 0x0b, 0x95, 0x63, 0x9c, 0x32, 0xae, 0xc7, 0xa8, 0x21, 0x68, 0x90,
+ 0x18, 0xfb, 0xf0, 0x5e, 0x55, 0xeb, 0xd1, 0x9e, 0x4b, 0x06, 0x17, 0x3f, 0x24, 0xf6, 0x5a, 0x8f,
+ 0x73, 0x15, 0x12, 0xad, 0x0b, 0x24, 0xd9, 0x20, 0x74, 0x4f, 0x81, 0xbc, 0xf8, 0x7c, 0xe8, 0x3a,
+ 0x4b, 0xc7, 0xb5, 0xa0, 0xf0, 0x08, 0x17, 0xde, 0x2e, 0xf3, 0x38, 0xe5, 0xfb, 0x40, 0x6e, 0x6a,
+ 0xb0, 0xfa, 0x26, 0xe7, 0xf8, 0x32, 0x4d, 0x88, 0x9b, 0x92, 0xf8, 0x81, 0xc9, 0x44, 0xdd, 0xf9,
+ 0xed, 0x25, 0xe6, 0xb1, 0xd3, 0x5a, 0x57, 0xeb, 0xb8, 0x4e, 0xf3, 0xaf, 0xc1, 0x6c, 0xe8, 0xd8,
+ 0xf0, 0xe5, 0x6b, 0xce, 0x2a, 0xe5, 0xc3, 0xe0, 0x57, 0x17, 0x8a, 0xfb, 0xbe, 0x9c, 0xb8, 0xd2,
+ 0xc4, 0x86, 0x17, 0x46, 0xee, 0x38, 0xf5, 0xbb, 0x08, 0x55, 0x9c, 0xb1, 0x92, 0x0d, 0xc8, 0x6d,
+ 0x69, 0x9f, 0x0e, 0x3a, 0xce, 0x61, 0xa3, 0xd0, 0x87, 0x24, 0x16, 0x5e, 0x99, 0x3b, 0x34, 0x1e,
+ 0x91, 0x8f, 0x6a, 0xbd, 0x2c, 0xda, 0x47, 0xdb, 0x02, 0xfb, 0x8e, 0x2d, 0x3e, 0x25, 0x16, 0x0e,
+ 0xd4, 0x89, 0x54, 0x27, 0xfd, 0xdf, 0x2d, 0x67, 0xb2, 0x4d, 0xd8, 0xf3, 0x59, 0xa6, 0xe6, 0xf2,
+ 0x00, 0x9c, 0x4a, 0x35, 0xd5, 0x2f, 0x7b, 0x44, 0x1a, 0x88, 0xb1, 0x0e, 0xc9, 0xd5, 0x2a, 0x3c,
+ 0x86, 0xcd, 0x3a, 0x8c, 0xee, 0x62, 0x25, 0x06, 0x60, 0xc2, 0x54, 0xec, 0x11, 0x4a, 0x6b, 0x31,
+ 0x4b, 0xc7, 0xe4, 0x00, 0x16, 0xb8, 0x5f, 0x6c, 0x7d, 0x3a, 0x80, 0x5f, 0x5c, 0x25, 0x10, 0x58,
+ 0xad, 0xba, 0x76, 0x27, 0x3a, 0x60, 0xd7, 0x49, 0xa9, 0xcc, 0xcd, 0xcf, 0x9e, 0xb0, 0x3e, 0xa2,
+ 0x71, 0x06, 0x46, 0x8e, 0xc4, 0xdb, 0x5e, 0xa3, 0x72, 0xa2, 0xc3, 0x48, 0x64, 0x41, 0xab, 0x62,
+ 0xd6, 0x65, 0x8c, 0x7d, 0xf1, 0xf8, 0x5d, 0xd4, 0xbc, 0x56, 0xc4, 0x9b, 0x4a, 0x4a, 0x70, 0xf4,
+ 0x95, 0x0e, 0x74, 0x6f, 0xc3, 0x4f, 0xb2, 0xf9, 0x3a, 0x23, 0xc7, 0x88, 0x6f, 0x28, 0x79, 0x26,
+ 0xe0, 0x51, 0xfe, 0x2f, 0x2e, 0x7c, 0x6e, 0x7e, 0x44, 0xc0, 0x47, 0xa0, 0x0e, 0xd8, 0xdc, 0xc5,
+ 0xcf, 0x0a, 0xd7, 0x05, 0x14, 0xfe, 0x7d, 0x64, 0x27, 0x5c, 0xe5, 0xd7, 0x53, 0x56, 0xa4, 0xf0,
+ 0x23, 0x9c, 0x3f, 0x99, 0xda, 0xbe, 0xc0, 0xf4, 0xc3, 0xea, 0xe1, 0xcd, 0xfd, 0x72, 0xb9, 0x28,
+ 0x85, 0xa7, 0x51, 0xcb, 0xca, 0x82, 0xae, 0x13, 0xb1, 0x97, 0xa8, 0xa9, 0x46, 0x71, 0x7b, 0x9c,
+ 0x95, 0x61, 0x99, 0x95, 0x5d, 0x71, 0xf5, 0x40, 0xdb, 0xcb, 0x6c, 0xcd, 0x32, 0x31, 0x5d, 0x2b,
+ 0xe1, 0x5a, 0xf6, 0xbf, 0xa7, 0xe9, 0x46, 0x61, 0x22, 0x9b, 0x3b, 0x97, 0x28, 0x9b, 0x36, 0x74,
+ 0xba, 0xb6, 0x64, 0x93, 0x2f, 0xe5, 0xc5, 0x64, 0x65, 0xa9, 0x28, 0x58, 0xb9, 0xff, 0x7f, 0xe6,
+ 0x94, 0xc5, 0x8b, 0x0f, 0xc3, 0x06, 0xcc, 0x9e, 0xfd, 0xad, 0x8c, 0xa5, 0x2b, 0x7a, 0x88, 0x09,
+ 0xbf, 0x48, 0xeb, 0xf6, 0x6f, 0xd9, 0xfd, 0xaf, 0xf7, 0xf7, 0xc7, 0xa3, 0x1d, 0x2e, 0x9d, 0x78,
+ 0x07, 0xc5, 0xbf, 0x0d, 0xff, 0x01, 0x5f, 0x1c, 0x62, 0xab, 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, 0x56, 0x5b, 0x6f, 0xdc, 0x44,
- 0x14, 0x7e, 0xe7, 0x57, 0x78, 0x47, 0x22, 0xf2, 0xb0, 0x8e, 0x73, 0x81, 0x87, 0x6a, 0x5d, 0x6b,
- 0xd5, 0x2b, 0x2d, 0x6a, 0x9b, 0xaa, 0x49, 0x11, 0x52, 0x14, 0xa2, 0x59, 0xfb, 0x6c, 0xd6, 0x8d,
- 0x77, 0xc6, 0x8c, 0xc7, 0xd9, 0x84, 0x6d, 0x24, 0xda, 0x3e, 0x80, 0x04, 0x52, 0x25, 0xde, 0xe1,
- 0x89, 0x1f, 0x90, 0x06, 0x02, 0x2d, 0x6d, 0xc3, 0x5f, 0xf0, 0xfe, 0x23, 0xce, 0x19, 0x7b, 0xaf,
- 0x89, 0x44, 0xc5, 0x43, 0x36, 0xf6, 0x78, 0xce, 0xed, 0x3b, 0xe7, 0xfb, 0x66, 0xba, 0x85, 0x8c,
- 0x4c, 0xa2, 0xa4, 0x93, 0xe4, 0x1b, 0x9d, 0x27, 0x10, 0x19, 0x17, 0xf8, 0x50, 0x83, 0x29, 0xb4,
- 0x64, 0xdb, 0xca, 0xae, 0x38, 0xd5, 0x87, 0x1d, 0x16, 0x86, 0x61, 0xf5, 0xe8, 0x67, 0x5a, 0x19,
- 0x65, 0x8e, 0x32, 0xf0, 0x8d, 0xda, 0x34, 0x3a, 0x91, 0x7b, 0x7e, 0x24, 0xd2, 0x14, 0x6d, 0x8f,
- 0xbb, 0x63, 0x87, 0x7b, 0x30, 0x75, 0xe5, 0xc4, 0x2a, 0x2a, 0xfa, 0x20, 0x8d, 0xbf, 0x07, 0xe6,
- 0x56, 0x0a, 0xf4, 0x78, 0xfd, 0xe8, 0x6e, 0x3c, 0x67, 0x10, 0x09, 0x19, 0x41, 0x7a, 0xeb, 0x00,
- 0xbf, 0xcd, 0x58, 0x02, 0x06, 0x03, 0x5a, 0xbb, 0x09, 0x5d, 0x51, 0xa4, 0x66, 0x69, 0x69, 0x71,
- 0xc5, 0xe5, 0x1e, 0xf8, 0xb9, 0x51, 0xd9, 0x43, 0xad, 0x32, 0xb1, 0x27, 0xc8, 0x19, 0xed, 0x5a,
- 0x58, 0xb2, 0xdb, 0xaa, 0x18, 0xd7, 0x8b, 0x4e, 0x27, 0x85, 0xb0, 0xb1, 0x8a, 0x2b, 0x55, 0x98,
- 0x2f, 0x45, 0x5a, 0xe0, 0xc2, 0x9a, 0xd7, 0x58, 0x9b, 0x26, 0x04, 0x5a, 0x2b, 0xbd, 0x05, 0x87,
- 0x73, 0xe9, 0x38, 0x89, 0xcc, 0x0d, 0x79, 0x51, 0x5d, 0xe7, 0x16, 0x6d, 0x68, 0x83, 0xdf, 0x87,
- 0x3c, 0x17, 0x7b, 0xd0, 0x82, 0x26, 0x63, 0x53, 0xf3, 0x54, 0x45, 0xfb, 0x64, 0x89, 0x99, 0x80,
- 0xb9, 0x66, 0x10, 0xa6, 0x4e, 0x61, 0xc0, 0x65, 0x71, 0x92, 0x0b, 0x0c, 0x1f, 0x33, 0x6f, 0xfa,
- 0x38, 0x03, 0x43, 0x21, 0xa7, 0x86, 0x1a, 0xfa, 0xea, 0x00, 0x2e, 0xb3, 0x9d, 0x31, 0x48, 0x64,
- 0x62, 0x1e, 0x80, 0x19, 0x28, 0xbd, 0xbf, 0x09, 0xc6, 0x60, 0x33, 0x72, 0x97, 0x0f, 0xa7, 0x55,
- 0xb8, 0xe0, 0x19, 0x3e, 0x94, 0x45, 0x9a, 0x36, 0xc2, 0x10, 0xf1, 0x73, 0xc1, 0x3f, 0xb0, 0xe5,
- 0x1a, 0xee, 0x4d, 0x82, 0x1d, 0x1f, 0x08, 0xed, 0x98, 0x70, 0x35, 0x98, 0xd8, 0x21, 0x60, 0xc3,
- 0x75, 0x6c, 0x79, 0xb3, 0x49, 0x36, 0xd8, 0x4d, 0x96, 0x2a, 0x11, 0xa3, 0xf7, 0xdd, 0x54, 0x74,
- 0x20, 0x65, 0x1c, 0x01, 0x3e, 0x4a, 0xc1, 0xc7, 0x94, 0xb2, 0x54, 0x1c, 0x85, 0x4c, 0x2a, 0x09,
- 0xac, 0x72, 0xa4, 0xc3, 0x49, 0xc3, 0xbb, 0x4a, 0xf7, 0x73, 0x5f, 0x56, 0xf9, 0xed, 0xe6, 0x75,
- 0x82, 0x81, 0xf6, 0x45, 0x1c, 0xdb, 0x5e, 0xdf, 0x4b, 0x72, 0x03, 0x12, 0xb4, 0xcb, 0xf2, 0xa2,
- 0xd3, 0x4f, 0x0c, 0xf3, 0xdc, 0x71, 0x0e, 0x33, 0xb8, 0x6b, 0x5f, 0x26, 0x71, 0x95, 0xb7, 0x8f,
- 0x68, 0xf4, 0x5d, 0xde, 0xd6, 0x7e, 0x96, 0xef, 0xd7, 0x4b, 0x29, 0xc8, 0x3d, 0xd3, 0xbb, 0x7a,
- 0xa5, 0xed, 0x8a, 0x14, 0xb4, 0x71, 0x59, 0xf9, 0x4b, 0x79, 0x56, 0x9e, 0x96, 0x67, 0xa3, 0xef,
- 0xca, 0xf7, 0xa3, 0x1f, 0xcb, 0x37, 0x4e, 0xf9, 0x4f, 0x79, 0x82, 0x2f, 0xe7, 0xe5, 0xdb, 0xd1,
- 0x4f, 0x8e, 0x5b, 0xbe, 0x2b, 0x5f, 0x97, 0xef, 0xf1, 0xef, 0x5d, 0x79, 0x42, 0x2b, 0xf8, 0x7c,
- 0x32, 0x7a, 0xe9, 0x94, 0x7f, 0x94, 0x6f, 0xed, 0x87, 0x13, 0x67, 0xd9, 0xb9, 0xe2, 0x8c, 0x9e,
- 0xd9, 0x1d, 0xa7, 0x64, 0x85, 0x7f, 0xa7, 0x9c, 0x71, 0x6f, 0x7e, 0x4a, 0x79, 0x6b, 0x79, 0x2d,
- 0x0c, 0xb5, 0x9f, 0xe7, 0x98, 0x5d, 0x0e, 0x29, 0x92, 0x03, 0xe2, 0xbb, 0x32, 0x86, 0xc3, 0xb9,
- 0x4c, 0x9c, 0xf2, 0x14, 0x93, 0x78, 0x85, 0xf1, 0x4f, 0xac, 0xf3, 0xd1, 0xf3, 0xf2, 0x7c, 0xf4,
- 0x7d, 0xf9, 0x37, 0x3e, 0x62, 0xc8, 0xf3, 0xd1, 0xb3, 0xd1, 0xf3, 0xd1, 0x0b, 0xca, 0xf0, 0x92,
- 0x00, 0x07, 0x2a, 0x89, 0xab, 0x59, 0xc2, 0x30, 0x16, 0x21, 0xde, 0x9a, 0xf8, 0xfe, 0x99, 0x6a,
- 0x44, 0x17, 0xaf, 0xd1, 0xe3, 0x99, 0x23, 0x55, 0x8c, 0xf3, 0x19, 0x5f, 0x74, 0x72, 0xcc, 0xb9,
- 0x87, 0xd6, 0x3d, 0x35, 0xd8, 0x25, 0xcc, 0x2e, 0x42, 0x1f, 0xf5, 0x84, 0xdc, 0x83, 0x45, 0xe8,
- 0x2d, 0xc0, 0xf3, 0xc3, 0x4b, 0xa4, 0x67, 0x48, 0x1b, 0x23, 0x34, 0x52, 0xd9, 0x8f, 0x7a, 0x10,
- 0xed, 0x43, 0xdc, 0x66, 0x06, 0x69, 0xc2, 0x5a, 0x2c, 0x13, 0x79, 0x8e, 0x8d, 0xa6, 0xf9, 0xac,
- 0x42, 0x12, 0x2e, 0x1f, 0x18, 0xae, 0x9a, 0xc0, 0x89, 0xeb, 0x39, 0x34, 0x83, 0xe5, 0xb5, 0xc9,
- 0xf0, 0xd6, 0xdf, 0x55, 0x46, 0x86, 0xf9, 0xb6, 0xd9, 0xc1, 0xd5, 0x99, 0x59, 0x08, 0x19, 0xab,
- 0x0a, 0x16, 0x4f, 0xc4, 0x21, 0xe9, 0x8d, 0xcb, 0x56, 0x90, 0xb1, 0xa6, 0xc8, 0x99, 0x37, 0x3c,
- 0x9e, 0x09, 0x69, 0x3c, 0xc5, 0x87, 0x46, 0x1f, 0x0d, 0x93, 0xae, 0x6b, 0xb8, 0xe9, 0x69, 0x35,
- 0x70, 0x4c, 0x00, 0xae, 0x1d, 0x35, 0x4f, 0xf9, 0x84, 0xe6, 0x6e, 0x12, 0x3f, 0x7d, 0x4a, 0xd4,
- 0x41, 0xf1, 0xa8, 0x82, 0x78, 0xd3, 0xb7, 0xaa, 0x1b, 0xf5, 0x02, 0xd2, 0xe5, 0x38, 0x12, 0x26,
- 0xea, 0xa1, 0xaf, 0x61, 0xd5, 0x9e, 0xa9, 0x7e, 0x18, 0xcc, 0x68, 0x31, 0x25, 0x6c, 0xd1, 0x42,
- 0x42, 0x96, 0xa7, 0x75, 0x42, 0x50, 0x27, 0x04, 0x41, 0x0d, 0x62, 0x22, 0x11, 0xb9, 0x3b, 0x5b,
- 0xf7, 0xef, 0x61, 0x7d, 0x01, 0x92, 0xca, 0x25, 0xb8, 0x14, 0x12, 0x56, 0x5d, 0x35, 0x7e, 0x8a,
- 0xc8, 0xd6, 0x1c, 0x08, 0x54, 0xb3, 0x59, 0x61, 0x99, 0x84, 0xd5, 0x87, 0x6d, 0xb5, 0xb3, 0xbd,
- 0xba, 0xe3, 0x89, 0x99, 0xd7, 0xb5, 0x9d, 0xb1, 0x5b, 0x91, 0x65, 0x20, 0x63, 0x57, 0xc2, 0xc0,
- 0xd9, 0xb0, 0x80, 0xba, 0x49, 0x93, 0x39, 0x2e, 0x6b, 0x0a, 0xfc, 0x17, 0x5f, 0xef, 0x73, 0xe6,
- 0x25, 0x98, 0x7c, 0xad, 0x13, 0x95, 0xd1, 0x6c, 0xb1, 0x70, 0xb1, 0x58, 0xa8, 0x8a, 0x9d, 0x17,
- 0xa8, 0xc7, 0x59, 0x2c, 0x0c, 0xdc, 0x46, 0x31, 0x70, 0xab, 0xec, 0x60, 0x51, 0x22, 0x0a, 0xbb,
- 0x63, 0xaa, 0x10, 0xf0, 0x41, 0x0a, 0x81, 0x88, 0x21, 0x5a, 0xb3, 0xe3, 0x8e, 0xaa, 0xd6, 0x00,
- 0xbf, 0x9b, 0xa4, 0xd5, 0x4f, 0x5e, 0xe3, 0xc2, 0x6b, 0x21, 0x19, 0x33, 0xe7, 0x37, 0xa4, 0xe2,
- 0x9b, 0xf2, 0xad, 0x83, 0xbc, 0x7e, 0x85, 0x9c, 0x44, 0x76, 0x23, 0xc7, 0xcf, 0x48, 0x13, 0x48,
- 0x07, 0xde, 0x2f, 0x70, 0x16, 0x29, 0xd5, 0x58, 0x0b, 0x2a, 0xad, 0x1c, 0xb3, 0x30, 0xa0, 0x32,
- 0x64, 0x48, 0xd0, 0x7d, 0x75, 0xff, 0xde, 0x1d, 0x63, 0xb2, 0x47, 0xf0, 0x4d, 0x01, 0xb9, 0xf1,
- 0xb4, 0x5d, 0xa4, 0x62, 0x6f, 0x0a, 0x23, 0x82, 0x89, 0x84, 0xd5, 0x60, 0x33, 0xca, 0x8b, 0x48,
- 0x34, 0x4d, 0x12, 0x1b, 0x84, 0xa8, 0x22, 0x06, 0xa4, 0xb1, 0x97, 0x14, 0x8e, 0x07, 0xee, 0x9e,
- 0xc6, 0x53, 0x66, 0xa1, 0x74, 0x9b, 0x01, 0x86, 0x5b, 0x70, 0xe5, 0xe7, 0xc9, 0xb7, 0x10, 0x48,
- 0x6a, 0x38, 0xba, 0x83, 0xf8, 0xaa, 0x6e, 0xdf, 0x17, 0xa6, 0xe7, 0x6b, 0x55, 0x60, 0xf8, 0xf1,
- 0xea, 0x8a, 0xfe, 0x64, 0x6d, 0x75, 0x95, 0xe3, 0x11, 0x7e, 0x3b, 0x39, 0x84, 0xd8, 0x5d, 0xe7,
- 0x2d, 0x7c, 0xf7, 0xc6, 0xf5, 0xcd, 0x4c, 0x9c, 0x6c, 0xb2, 0x8f, 0x19, 0xcd, 0xaf, 0xf4, 0x95,
- 0xd4, 0x20, 0xe2, 0x23, 0x22, 0x14, 0x54, 0x2c, 0x0e, 0x27, 0x09, 0xf1, 0xe1, 0x67, 0x78, 0x5e,
- 0x48, 0xdf, 0xee, 0xd8, 0xa4, 0x1d, 0xc8, 0x4c, 0x94, 0xc7, 0xf0, 0x8b, 0xcd, 0x8d, 0x07, 0x7e,
- 0x26, 0x74, 0x0e, 0x2e, 0x7d, 0xcd, 0x33, 0xe4, 0x2d, 0xd0, 0xac, 0x70, 0x7a, 0xc3, 0x23, 0xbc,
- 0x3d, 0xee, 0xca, 0xaf, 0x8b, 0xcd, 0xa0, 0x3e, 0xfc, 0x85, 0x1d, 0xb0, 0x52, 0x3e, 0xfa, 0xc1,
- 0x2e, 0x9e, 0x7b, 0xce, 0xe8, 0x85, 0xd5, 0x4c, 0xd2, 0xf3, 0x37, 0xf4, 0x44, 0x1a, 0x4d, 0x12,
- 0x4f, 0x7a, 0x7f, 0x66, 0x0d, 0x7e, 0xc7, 0xed, 0x2f, 0xca, 0x3f, 0xf1, 0xe9, 0x0c, 0x37, 0x3e,
- 0x1b, 0xbd, 0x64, 0xbc, 0x35, 0x89, 0x82, 0x8e, 0x5e, 0x63, 0x24, 0xab, 0xbf, 0x97, 0xf4, 0x9f,
- 0x04, 0xc4, 0xd6, 0x6a, 0x87, 0x3a, 0x9c, 0x15, 0xa9, 0xcb, 0x86, 0x9d, 0xb6, 0x62, 0x63, 0x5d,
- 0xf6, 0x70, 0x63, 0x73, 0x8b, 0x79, 0x66, 0xac, 0x50, 0xc2, 0x5a, 0x11, 0x6a, 0x39, 0xb5, 0x5d,
- 0xd3, 0x0c, 0x5d, 0x20, 0xc6, 0xb5, 0x2c, 0x43, 0xe0, 0x2e, 0x3d, 0xc3, 0xbd, 0x45, 0xe2, 0x1c,
- 0x37, 0x66, 0xb0, 0x9e, 0x3f, 0xe0, 0x3d, 0x1c, 0x03, 0x4b, 0x05, 0x1c, 0xc8, 0x4a, 0xb1, 0x3c,
- 0x36, 0xde, 0xc1, 0x1a, 0x21, 0xc9, 0x36, 0x5e, 0x58, 0x74, 0xad, 0x2a, 0x34, 0x9e, 0xf6, 0xf2,
- 0x82, 0x52, 0x8c, 0xf7, 0xb6, 0x8e, 0x88, 0xf6, 0x9d, 0x7e, 0x91, 0x1b, 0xa7, 0x03, 0x8e, 0x70,
- 0x26, 0x76, 0x3c, 0x40, 0x87, 0x0d, 0x73, 0xd1, 0x48, 0x2a, 0xa7, 0xd0, 0xa9, 0x93, 0x67, 0x10,
- 0x25, 0xdd, 0x84, 0xae, 0x23, 0x41, 0x3e, 0x48, 0x6a, 0x3d, 0x88, 0x44, 0x0e, 0xec, 0xf3, 0x5b,
- 0x5b, 0xac, 0x85, 0xd6, 0x93, 0x6b, 0xa5, 0xe4, 0x7c, 0xa2, 0x5a, 0x58, 0xb8, 0x23, 0xb9, 0xf4,
- 0x7b, 0x22, 0xdf, 0x18, 0x48, 0xba, 0xa3, 0x21, 0xa8, 0x47, 0xae, 0xe2, 0x38, 0x2f, 0xa6, 0x19,
- 0xba, 0x74, 0xa6, 0x86, 0x34, 0x80, 0xa8, 0xfc, 0x1b, 0x5d, 0x97, 0xb5, 0x19, 0x6f, 0xe3, 0x4f,
- 0x8b, 0x2d, 0x31, 0xde, 0x04, 0x19, 0xa1, 0x30, 0x3f, 0x7e, 0x74, 0xf7, 0x86, 0xea, 0xe3, 0x20,
- 0x11, 0xe5, 0x15, 0x6f, 0xb2, 0x90, 0x5d, 0xf6, 0x45, 0xa2, 0xda, 0x71, 0x1e, 0x74, 0x70, 0x24,
- 0xf7, 0x03, 0x9b, 0x98, 0xed, 0xd1, 0x62, 0x66, 0xb5, 0x5e, 0x6e, 0xef, 0x04, 0x1f, 0x92, 0x63,
- 0xe2, 0x67, 0x45, 0x8e, 0xa5, 0xfe, 0x8f, 0x44, 0x64, 0x98, 0xf8, 0x4f, 0x54, 0x82, 0xb3, 0x82,
- 0xa5, 0x1c, 0xdb, 0xbb, 0x92, 0xb8, 0x44, 0x41, 0xc6, 0x92, 0x21, 0xaa, 0xc1, 0xa2, 0x83, 0xc1,
- 0xab, 0x32, 0x47, 0x60, 0x90, 0x55, 0x82, 0x4e, 0xe5, 0x7a, 0xef, 0x1d, 0x64, 0x1b, 0x49, 0xc4,
- 0x0d, 0x25, 0x51, 0x2c, 0xcc, 0x72, 0x75, 0x42, 0x33, 0xd4, 0x9a, 0x34, 0x89, 0xec, 0xcd, 0x77,
- 0xe5, 0x70, 0x79, 0x30, 0x18, 0x2c, 0x93, 0xcc, 0x2e, 0x63, 0xdb, 0xaa, 0xec, 0xe8, 0x96, 0x20,
- 0xfe, 0x83, 0xce, 0x88, 0x12, 0x31, 0x5a, 0xcc, 0x30, 0xda, 0x2e, 0xb2, 0xfa, 0x3c, 0x45, 0x88,
- 0xc4, 0xd2, 0x52, 0x63, 0xe5, 0xeb, 0xf5, 0xa7, 0x6b, 0xeb, 0xeb, 0x9f, 0xae, 0xf8, 0x06, 0xf3,
- 0x71, 0x31, 0x39, 0xfb, 0x99, 0x5f, 0x9c, 0x9b, 0x1e, 0x96, 0xe8, 0x50, 0x74, 0x07, 0x0f, 0x98,
- 0xf1, 0xb6, 0x40, 0xbb, 0x76, 0x5e, 0x67, 0x54, 0x42, 0xcc, 0xab, 0x04, 0x22, 0x65, 0x73, 0xbd,
- 0x48, 0x47, 0x8d, 0xd8, 0xd8, 0xf3, 0x97, 0x36, 0x58, 0x96, 0xd9, 0xc9, 0x23, 0x94, 0xda, 0xb4,
- 0xde, 0x42, 0xfa, 0x89, 0xe3, 0x01, 0x4e, 0x92, 0x1a, 0xf8, 0x74, 0xfa, 0x86, 0x43, 0x64, 0x66,
- 0x0b, 0xfc, 0x0e, 0x2e, 0xa1, 0xad, 0xdd, 0xcd, 0xbd, 0x4c, 0xe5, 0xb3, 0x8b, 0x16, 0x69, 0x0c,
- 0xea, 0xf2, 0xe0, 0xa3, 0x7f, 0x01, 0x1a, 0x08, 0xa7, 0x18, 0x21, 0x0d, 0x00, 0x00,
+ 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,
+ 0x24, 0x0c, 0x1f, 0xa5, 0xc9, 0x93, 0x27, 0x44, 0x00, 0x24, 0x78, 0x75, 0x48, 0xb0, 0xfa, 0x55,
+ 0xd5, 0xa0, 0x5e, 0xc0, 0xce, 0x9f, 0x0f, 0x85, 0x1d, 0x8e, 0xd1, 0xd7, 0xac, 0x2a, 0x4a, 0x8a,
+ 0x8f, 0x7c, 0xbe, 0x1e, 0x0a, 0xca, 0xc4, 0x5a, 0x20, 0x8e, 0x65, 0x75, 0x20, 0x50, 0x07, 0x02,
+ 0x51, 0x0d, 0x5e, 0x2a, 0x11, 0xb1, 0xdb, 0x0f, 0xef, 0xdd, 0xc5, 0xbc, 0x22, 0x24, 0x8b, 0x4f,
+ 0x30, 0x49, 0xe4, 0x9c, 0xbc, 0x66, 0xc3, 0x0c, 0x11, 0xad, 0x7b, 0x3b, 0x92, 0xdd, 0x6e, 0x85,
+ 0xa1, 0x8e, 0xab, 0x17, 0xfb, 0xf2, 0x60, 0x7f, 0xfb, 0x20, 0x50, 0xad, 0x9f, 0x3b, 0x07, 0x8d,
+ 0x5b, 0x91, 0xe7, 0x20, 0x13, 0x5f, 0xc2, 0xd4, 0xdb, 0x73, 0x40, 0xfa, 0xba, 0xcb, 0x3c, 0x9f,
+ 0x75, 0x15, 0x7e, 0x25, 0x37, 0x26, 0x9c, 0x05, 0x1a, 0x83, 0x17, 0x7e, 0x65, 0xdf, 0xce, 0x0f,
+ 0x56, 0xf9, 0x41, 0x95, 0xdf, 0xfc, 0x02, 0xff, 0x29, 0x06, 0x58, 0x27, 0x78, 0x91, 0x27, 0xc2,
+ 0xc2, 0x8a, 0xdf, 0xf0, 0x5e, 0xfc, 0x46, 0x5c, 0x10, 0x13, 0x8d, 0xdf, 0x41, 0x07, 0xc2, 0x51,
+ 0x9a, 0x55, 0x1f, 0xa6, 0xce, 0x99, 0xd7, 0xe4, 0x6f, 0x38, 0xf0, 0x13, 0xf2, 0xe8, 0x75, 0xf9,
+ 0xc6, 0x43, 0x2e, 0xfe, 0x8c, 0x84, 0x42, 0x46, 0x22, 0x2f, 0xcf, 0x89, 0xc7, 0xc4, 0xdd, 0xb7,
+ 0x6b, 0x84, 0x43, 0x72, 0x74, 0x76, 0x22, 0x54, 0xd4, 0x86, 0x4c, 0x51, 0x05, 0x2d, 0x61, 0xf2,
+ 0xd7, 0x7b, 0x77, 0x6f, 0x5b, 0x9b, 0xdf, 0x87, 0xaf, 0x0a, 0x30, 0x36, 0x10, 0x6e, 0xf1, 0x16,
+ 0x66, 0xf2, 0xa9, 0xb0, 0x22, 0x6a, 0x8e, 0x6d, 0x50, 0x64, 0x14, 0x14, 0xb1, 0x62, 0x15, 0x21,
+ 0x22, 0xcf, 0xb1, 0x87, 0x8a, 0x9c, 0xf4, 0xef, 0x8a, 0x5c, 0xf1, 0xfe, 0x3b, 0xd4, 0x28, 0xf1,
+ 0x6b, 0xd9, 0xba, 0x08, 0x02, 0x1d, 0xaf, 0xb9, 0x0a, 0x4d, 0xfa, 0x77, 0x88, 0x24, 0x55, 0x12,
+ 0xdd, 0x41, 0x72, 0x4d, 0xf7, 0xef, 0x09, 0x3b, 0x0e, 0xb5, 0x2a, 0xf0, 0xf8, 0x66, 0x75, 0x4b,
+ 0xff, 0x71, 0x67, 0x7b, 0x9b, 0xe3, 0x8d, 0x7a, 0x2b, 0x3d, 0x81, 0xc4, 0xdf, 0xe5, 0x3d, 0xfc,
+ 0x1d, 0x34, 0xf9, 0xb5, 0x5a, 0x49, 0x76, 0xd9, 0x1f, 0x18, 0x35, 0xa6, 0x0c, 0x95, 0xd4, 0x20,
+ 0x92, 0x53, 0x62, 0x08, 0x54, 0xb4, 0x8c, 0x97, 0x01, 0x35, 0xb5, 0x64, 0xe5, 0x8b, 0x75, 0x40,
+ 0x09, 0xcb, 0xff, 0x22, 0x8a, 0x4e, 0x42, 0x17, 0xdf, 0xb9, 0xc5, 0x77, 0x81, 0xb7, 0x78, 0xe6,
+ 0x44, 0x8b, 0x74, 0xf4, 0x35, 0x3d, 0x91, 0x36, 0x92, 0xb4, 0x92, 0xce, 0x9e, 0xbb, 0x0d, 0xff,
+ 0x42, 0xf3, 0x67, 0xe5, 0x7f, 0xf0, 0xe9, 0x1c, 0x0d, 0x9f, 0x2e, 0x9e, 0xb3, 0x08, 0x4b, 0xfc,
+ 0x21, 0xca, 0xa5, 0x0c, 0x5d, 0x24, 0x0f, 0x28, 0x12, 0x4e, 0x74, 0x20, 0x0d, 0xfd, 0xec, 0xc1,
+ 0xde, 0xe7, 0x61, 0x2e, 0xb4, 0x01, 0x9f, 0xde, 0x9b, 0x1c, 0x29, 0x0f, 0x0f, 0x51, 0x6c, 0x38,
+ 0xfd, 0xc2, 0x1b, 0xba, 0x5f, 0x15, 0x1f, 0x78, 0xaf, 0xe9, 0x82, 0x17, 0x18, 0xce, 0x2b, 0x8c,
+ 0xd7, 0xc9, 0xe8, 0x15, 0x9d, 0xc0, 0x2e, 0x11, 0x15, 0x6f, 0xb2, 0xb9, 0x83, 0x02, 0xe8, 0xf2,
+ 0x8d, 0xdb, 0xa2, 0xd4, 0xee, 0x74, 0x32, 0xc1, 0x7a, 0xfb, 0xec, 0x8b, 0xbd, 0x07, 0x0f, 0x59,
+ 0x60, 0x1b, 0x25, 0x12, 0xce, 0x9a, 0xc0, 0x34, 0xd4, 0x0d, 0x82, 0xfa, 0x8a, 0xc0, 0xad, 0x8b,
+ 0xf8, 0x3e, 0xf2, 0x57, 0x35, 0xfa, 0xd2, 0xe3, 0x85, 0x26, 0x9f, 0xad, 0xda, 0xf2, 0x16, 0xae,
+ 0xdf, 0x47, 0x90, 0x40, 0x47, 0x14, 0x2e, 0x95, 0x7d, 0xbd, 0x5a, 0x36, 0x9e, 0xa6, 0x32, 0x51,
+ 0xd3, 0x70, 0x92, 0x7c, 0x54, 0x41, 0x86, 0x20, 0xf1, 0x68, 0x7d, 0x66, 0xa8, 0x62, 0xc6, 0x89,
+ 0x61, 0xab, 0x62, 0x66, 0x1f, 0xcd, 0x63, 0xd6, 0xad, 0x2e, 0xf6, 0x86, 0x10, 0x57, 0x82, 0xd2,
+ 0x60, 0x72, 0x11, 0xe8, 0xc5, 0xb7, 0x74, 0xef, 0xd4, 0x54, 0x5b, 0x7c, 0x53, 0x11, 0x91, 0x2e,
+ 0x2e, 0xf2, 0x41, 0x75, 0xbd, 0x6e, 0x6e, 0xa4, 0x52, 0xe8, 0xd3, 0x6a, 0xd8, 0x5b, 0x4b, 0x95,
+ 0xd8, 0xe2, 0x84, 0xa4, 0xd3, 0x3a, 0xa6, 0x35, 0x07, 0xa2, 0xc8, 0x06, 0x3a, 0x48, 0x2b, 0x35,
+ 0x88, 0x75, 0x25, 0xc9, 0x01, 0x6b, 0x2c, 0x58, 0x27, 0xa6, 0x7b, 0x09, 0xa7, 0xa7, 0xb4, 0x96,
+ 0x4f, 0x02, 0xcb, 0x4d, 0x52, 0x08, 0x36, 0x8e, 0x95, 0x03, 0x31, 0x3c, 0xf2, 0x26, 0x85, 0xb1,
+ 0xde, 0x00, 0x3c, 0xe1, 0x2d, 0xf7, 0x71, 0xea, 0xbd, 0x8e, 0xbc, 0xbc, 0x49, 0x2a, 0xaf, 0xd0,
+ 0x99, 0x67, 0x72, 0x18, 0xa6, 0xa3, 0x94, 0x66, 0xa4, 0xc8, 0x4c, 0xd3, 0xba, 0x69, 0x86, 0xc2,
+ 0x00, 0xfb, 0xf3, 0xcd, 0x87, 0xac, 0x47, 0x82, 0xed, 0xa3, 0x4e, 0x36, 0xba, 0xac, 0x70, 0x92,
+ 0xf3, 0x34, 0xd7, 0xe1, 0x58, 0x98, 0xbd, 0xa9, 0xa4, 0x09, 0x11, 0xa1, 0x3a, 0xf5, 0x15, 0xc7,
+ 0x3b, 0x4a, 0x76, 0x63, 0xdf, 0xcd, 0x05, 0x12, 0x99, 0x88, 0x77, 0xda, 0xde, 0xc8, 0x67, 0x7d,
+ 0xc6, 0xfb, 0xf8, 0xd1, 0x63, 0x1b, 0x8c, 0x77, 0x41, 0x0e, 0x55, 0x02, 0x7f, 0xb9, 0x7f, 0xe7,
+ 0x13, 0x35, 0xc1, 0x3e, 0xc7, 0x96, 0xc1, 0x8d, 0x5d, 0x86, 0x65, 0xb9, 0xe2, 0x8d, 0xde, 0x57,
+ 0x07, 0x9c, 0x47, 0x03, 0xc4, 0xf6, 0x28, 0x72, 0x11, 0xb9, 0xae, 0x5c, 0x86, 0xe4, 0x9a, 0x41,
+ 0xc4, 0xfb, 0x07, 0xd1, 0xfb, 0x04, 0x27, 0xc2, 0xbc, 0x30, 0x28, 0xed, 0xff, 0x47, 0x04, 0x3a,
+ 0x16, 0xe1, 0x63, 0x95, 0x22, 0x2d, 0x30, 0x87, 0x79, 0x3d, 0x12, 0x5e, 0xd6, 0xd0, 0x46, 0x34,
+ 0x4d, 0xc5, 0x21, 0xba, 0x7c, 0x83, 0x2a, 0xe4, 0xd8, 0x5d, 0xec, 0x86, 0x1a, 0xb4, 0xb6, 0xbd,
+ 0xed, 0x5a, 0xdc, 0x67, 0x9f, 0x28, 0x89, 0x8c, 0xb1, 0x9b, 0xd5, 0xd0, 0xc1, 0x50, 0x6d, 0xb3,
+ 0x74, 0xe8, 0x06, 0xee, 0xad, 0x93, 0xcd, 0xe9, 0x74, 0xba, 0x49, 0x77, 0xcb, 0x26, 0x16, 0xaa,
+ 0x8a, 0x8e, 0xc6, 0x1d, 0xf3, 0x3b, 0x82, 0x56, 0x6b, 0x8d, 0x69, 0x6b, 0x0d, 0x2d, 0xb2, 0x7a,
+ 0x44, 0x40, 0x88, 0x70, 0x72, 0xed, 0x6c, 0xfd, 0x6d, 0xf7, 0xc9, 0xce, 0xee, 0xee, 0x9f, 0xb6,
+ 0x42, 0x8b, 0xf1, 0xf8, 0x18, 0x9c, 0x7b, 0xcd, 0x2f, 0x77, 0xca, 0x18, 0x53, 0xf4, 0xe8, 0x74,
+ 0x8f, 0x75, 0x97, 0x66, 0x51, 0xea, 0xbb, 0x0e, 0x6d, 0xa9, 0x97, 0xb9, 0xa8, 0x5e, 0xa4, 0x38,
+ 0xe6, 0x4a, 0xc5, 0x41, 0xad, 0xa9, 0x46, 0x0a, 0x32, 0x70, 0x82, 0xe2, 0x7a, 0x8d, 0x50, 0xea,
+ 0xd3, 0x7a, 0x4f, 0x63, 0x96, 0xf3, 0x9a, 0xe4, 0x34, 0x58, 0xc4, 0x33, 0xe4, 0x51, 0xcf, 0x86,
+ 0x83, 0x94, 0xee, 0x82, 0xc0, 0x59, 0xf3, 0x20, 0x57, 0xa6, 0xbd, 0xe8, 0x90, 0xc6, 0x43, 0xf1,
+ 0x3f, 0x95, 0x7a, 0x6b, 0x2a, 0x53, 0x7b, 0x3d, 0xcf, 0xdb, 0xe0, 0xe0, 0xf4, 0xef, 0xee, 0xf9,
+ 0xe8, 0x83, 0xdf, 0x00, 0xbe, 0x18, 0xb5, 0xbd, 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, 0x95, 0x26, 0x4d, 0x06, 0x65, 0x18, 0x04, 0xf7, 0x43,
- 0x3a, 0xe7, 0x9e, 0x7b, 0xcc, 0x8c, 0x38, 0x44, 0x05, 0x96, 0xba, 0xb1, 0x54, 0x08, 0x55, 0xe5,
- 0x24, 0xc9, 0x4a, 0xea, 0x72, 0x55, 0x91, 0xa4, 0x65, 0x5d, 0x91, 0xd5, 0x88, 0xa6, 0x8a, 0x54,
- 0x65, 0x6b, 0xfc, 0x8d, 0x07, 0x0b, 0x9f, 0x2d, 0xf5, 0x7e, 0x67, 0x9c, 0x78, 0x3a, 0x4d, 0x22,
- 0xec, 0xf1, 0xa9, 0x91, 0xa6, 0xc2, 0xd8, 0xab, 0x3f, 0x40, 0xd2, 0x95, 0xdd, 0x67, 0x7d, 0x28,
- 0x69, 0xa9, 0xf4, 0x81, 0xc4, 0xd4, 0x5a, 0x0d, 0xb1, 0x3f, 0x78, 0x84, 0x32, 0xfa, 0xaa, 0x55,
- 0xb5, 0xfd, 0x49, 0xf9, 0x63, 0x1f, 0x7e, 0x0b, 0x7d, 0xd1, 0x23, 0xe4, 0x06, 0x66, 0xbf, 0x7e,
- 0x44, 0xdf, 0x41, 0xbf, 0x00, 0x2a, 0x4e, 0xa3, 0x2f, 0x4e, 0x51, 0x1d, 0x79, 0x5a, 0xf9, 0xd8,
- 0x83, 0x53, 0xb2, 0x9d, 0xa3, 0x42, 0x0d, 0x47, 0xae, 0x69, 0x62, 0xf7, 0xb3, 0xfe, 0x38, 0xa2,
- 0xed, 0x40, 0xe5, 0x05, 0x92, 0x55, 0x92, 0x64, 0x8c, 0xf2, 0x6d, 0xee, 0x4c, 0x5d, 0x89, 0x98,
- 0x1b, 0x6d, 0x1c, 0xb9, 0x01, 0x29, 0xef, 0xe5, 0x32, 0x63, 0x81, 0x3c, 0xb8, 0x98, 0x99, 0x30,
- 0x59, 0x49, 0xd2, 0x70, 0xdd, 0x1b, 0xad, 0xc4, 0xec, 0x46, 0x6c, 0x20, 0x81, 0x75, 0x36, 0x76,
- 0xdf, 0xaf, 0x57, 0xc0, 0x1e, 0xb2, 0x93, 0x99, 0x96, 0x76, 0xdf, 0xce, 0x99, 0x36, 0x7c, 0x7b,
- 0x46, 0xa1, 0x9d, 0xcb, 0x5a, 0xeb, 0x78, 0xa7, 0x04, 0x16, 0x4d, 0x7f, 0x86, 0x74, 0xf2, 0x31,
- 0xe0, 0xec, 0xbb, 0x8b, 0x5d, 0xdb, 0x11, 0xb2, 0x6b, 0x36, 0xae, 0x7c, 0xd6, 0x94, 0xc1, 0x89,
- 0xe8, 0xb3, 0x64, 0xb6, 0xb8, 0x1c, 0x61, 0xec, 0xed, 0x65, 0x6e, 0x86, 0xb5, 0x1c, 0x59, 0xf7,
- 0x5c, 0xfa, 0x32, 0x2f, 0x80, 0x6f, 0xc3, 0xcb, 0xd3, 0x6b, 0x31, 0x1a, 0x4b, 0xc2, 0x6b, 0xed,
- 0xbb, 0x76, 0x16, 0x79, 0xd0, 0xc0, 0xb1, 0x19, 0x19, 0x3a, 0x2a, 0x54, 0xed, 0xc9, 0x43, 0x20,
- 0x33, 0x64, 0x4e, 0xf5, 0xe1, 0x1b, 0xce, 0xb9, 0xcc, 0x26, 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, 0x1e, 0x2b, 0x23, 0xf0, 0x14, 0x37, 0x97, 0x6e, 0x91, 0x69,
- 0x70, 0xcb, 0x62, 0x72, 0xcb, 0x59, 0x72, 0x64, 0xf1, 0xbf, 0xa2, 0x75, 0xcb, 0x7b, 0x55, 0xed,
- 0xc4, 0xa8, 0xe1, 0x7d, 0x29, 0x27, 0xff, 0x25, 0xc1, 0xc3, 0xd6, 0x78, 0x85, 0x2a, 0xcc, 0xeb,
- 0x40, 0x53, 0x54, 0x2f, 0x90, 0x75, 0x77, 0xe2, 0x62, 0xb0, 0x48, 0xfa, 0xe9, 0x8a, 0x67, 0x46,
- 0x81, 0x2a, 0x83, 0xb7, 0xc7, 0xe9, 0xee, 0x48, 0x61, 0x5e, 0xc0, 0xbd, 0x47, 0xe0, 0x8c, 0xd7,
- 0xce, 0x07, 0x78, 0x6b, 0x54, 0x85, 0xe0, 0xde, 0x8c, 0xcf, 0x96, 0x9c, 0xc3, 0xe2, 0xfc, 0x0f,
- 0xf9, 0x07, 0x22, 0xe5, 0x1d, 0xdf, 0xa6, 0x33, 0x63, 0x50, 0x63, 0x6c, 0x9a, 0x2b, 0xff, 0xec,
- 0xc0, 0x03, 0x46, 0x6f, 0xe2, 0xeb, 0x7c, 0xa7, 0xff, 0x76, 0xb5, 0xd8, 0xa4, 0xeb, 0xf6, 0xc3,
- 0x5f, 0xa4, 0x85, 0xb2, 0x78, 0xae, 0x04, 0x00, 0x00,
+ 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,
+ 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/relayctl/src/static.h b/platformio/relayctl/src/static.h
index 3273e68..90560bf 100644
--- a/platformio/relayctl/src/static.h
+++ b/platformio/relayctl/src/static.h
@@ -15,6 +15,7 @@ typedef struct {
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/relayctl/src/util.h b/platformio/relayctl/src/util.h
new file mode 100644
index 0000000..e0780d8
--- /dev/null
+++ b/platformio/relayctl/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/relayctl/src/wifi.cpp b/platformio/relayctl/src/wifi.cpp
index b85d1dc..daff7bf 100644
--- a/platformio/relayctl/src/wifi.cpp
+++ b/platformio/relayctl/src/wifi.cpp
@@ -1,3 +1,4 @@
+#include <pgmspace.h>
#include "config.def.h"
#include "wifi.h"
#include "config.h"
@@ -8,22 +9,20 @@ namespace homekit::wifi {
using namespace homekit;
using homekit::config::ConfigData;
-const char NODE_ID[] = DEFAULT_NODE_ID;
-const char WIFI_AP_SSID[] = DEFAULT_WIFI_AP_SSID;
-const char WIFI_STA_SSID[] = DEFAULT_WIFI_STA_SSID;
-const char WIFI_STA_PSK[] = DEFAULT_WIFI_STA_PSK;
+const char HOME_ID[] = DEFAULT_HOME_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, char** ssid_dst, char** psk_dst, char** hostname_dst) {
+void getConfig(ConfigData &cfg, const char** ssid, const char** psk, const char** hostname) {
if (cfg.flags.wifi_configured) {
- *ssid_dst = cfg.wifi_ssid;
- *psk_dst = cfg.wifi_psk;
- if (hostname_dst != nullptr)
- *hostname_dst = cfg.node_id;
+ *ssid = cfg.wifi_ssid;
+ *psk = cfg.wifi_psk;
+ *hostname = cfg.home_id;
} else {
- *ssid_dst = (char*)WIFI_STA_SSID;
- *psk_dst = (char*)WIFI_STA_PSK;
- if (hostname_dst != nullptr)
- *hostname_dst = (char*)NODE_ID;
+ *ssid = STA_SSID;
+ *psk = STA_PSK;
+ *hostname = HOME_ID;
}
}
diff --git a/platformio/relayctl/src/wifi.h b/platformio/relayctl/src/wifi.h
index 65dbd53..37d8c02 100644
--- a/platformio/relayctl/src/wifi.h
+++ b/platformio/relayctl/src/wifi.h
@@ -14,12 +14,9 @@ struct ScanResult {
String ssid;
};
-void getConfig(ConfigData &cfg, char **ssid_dst, char **psk_dst, char **hostname_dst);
-std::shared_ptr<std::list<ScanResult>> scan();
+void getConfig(ConfigData& cfg, const char** ssid, const char** psk, const char** hostname);
-inline int8_t getRSSI() {
- return WiFi.RSSI();
-}
+std::shared_ptr<std::list<ScanResult>> scan();
inline uint32_t getIPAsInteger() {
if (!WiFi.isConnected())
@@ -27,9 +24,13 @@ inline uint32_t getIPAsInteger() {
return WiFi.localIP().v4();
}
-extern const char WIFI_AP_SSID[];
-extern const char WIFI_STA_SSID[];
-extern const char WIFI_STA_PSK[];
-extern const char NODE_ID[];
+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 HOME_ID[];
} \ No newline at end of file
diff --git a/platformio/relayctl/static/app.js b/platformio/relayctl/static/app.js
index e345957..2446541 100644
--- a/platformio/relayctl/static/app.js
+++ b/platformio/relayctl/static/app.js
@@ -6,6 +6,10 @@ function ge(id) {
return document.getElementById(id)
}
+function hide(el) {
+ el.style.display = 'none'
+}
+
function cancelEvent(evt) {
if (evt.preventDefault) evt.preventDefault();
if (evt.stopPropagation) evt.stopPropagation();
@@ -100,14 +104,14 @@ function initNetworkSettings() {
function onRequestDone() {
doneRequestsCount++;
if (doneRequestsCount === 2) {
- ge('loading_label').style.display = 'none';
+ hide(ge('loading_label'))
}
}
var form = document.forms.network_settings;
form.addEventListener('submit', function(e) {
- if (!form.nid.value.trim()) {
- alert('Введите node id');
+ if (!form.hid.value.trim()) {
+ alert('Введите home id');
return cancelEvent(e);
}
@@ -116,7 +120,7 @@ function initNetworkSettings() {
return cancelEvent(e);
}
- if (form.ssid.selectedIndex == -1) {
+ if (form.ssid.selectedIndex === -1) {
alert('Не выбрана точка доступа');
return cancelEvent(e);
}
@@ -140,7 +144,7 @@ function initNetworkSettings() {
if (error)
throw error;
- setupField(form.nid, response.node_id || null);
+ setupField(form.hid, response.home_id || null);
setupField(form.psk, null);
setupField(form.submit, null);
@@ -196,17 +200,23 @@ function initUpdateForm() {
form.submit.innerHTML = progress + '%';
});
xhr.onreadystatechange = function() {
+ var errorMessage = 'Ошибка обновления';
+ var successMessage = 'Обновление завершено, устройство перезагружается';
if (xhr.readyState === 4) {
- var response = JSON.parse(xhr.responseText);
- if (response.result === 1) {
- alert('Обновление завершено, устройство перезагружается');
- } else {
- alert('Ошибка обновления');
+ try {
+ var response = JSON.parse(xhr.responseText);
+ if (response.result === 1) {
+ alert(successMessage);
+ } else {
+ alert(errorMessage);
+ }
+ } catch (e) {
+ alert(successMessage);
}
}
};
xhr.onerror = function(e) {
- alert(errorText(e))
+ alert(errorText(e));
};
xhr.open('POST', e.target.action);
@@ -214,9 +224,23 @@ function initUpdateForm() {
return false;
});
+ form.file.addEventListener('change', function(e) {
+ if (e.target.files.length) {
+ var reader = new FileReader();
+ reader.onload = function() {
+ var hash = window.md5(reader.result);
+ form.setAttribute('action', '/update?md5='+hash);
+ unlock(form.submit);
+ };
+ reader.onerror = function() {
+ alert('Ошибка чтения файла');
+ };
+ reader.readAsBinaryString(e.target.files[0]);
+ }
+ });
}
-function initApp() {
+window.initApp = function() {
initNetworkSettings();
initUpdateForm();
} \ No newline at end of file
diff --git a/platformio/relayctl/static/index.html b/platformio/relayctl/static/index.html
index d89967d..d4a8040 100644
--- a/platformio/relayctl/static/index.html
+++ b/platformio/relayctl/static/index.html
@@ -6,6 +6,7 @@
<title>Configuration</title>
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<link rel="stylesheet" type="text/css" href="/style.css">
+ <script src="/md5.js"></script>
<script src="/app.js"></script>
</head>
<body onload="initApp()">
@@ -22,14 +23,14 @@
<div class="form_label">WiFi Password</div>
<div class="form_input">
<input type="password" value="" name="psk" class="full-width" id="fld_psk" maxlength="63" disabled>
- <div class="form_checkbox">
+ <div class="form_sublabel">
<label for="show_psk"><input type="checkbox" name="show_psk" id="show_psk"> show password</label>
</div>
</div>
- <div class="form_label">Node ID</div>
+ <div class="form_label">Home ID</div>
<div class="form_input">
- <input type="text" value="" maxlength="16" name="nid" id="fld_nid" class="full-width" disabled>
+ <input type="text" value="" maxlength="16" name="hid" id="fld_hid" class="full-width" disabled>
</div>
<button type="submit" disabled="disabled" name="submit">Save and Reboot</button>
@@ -42,7 +43,7 @@
<div class="form_input">
<input type="file" accept=".bin,.bin.gz" name="file">
</div>
- <button type="submit" name="submit">Upload</button>
+ <button type="submit" name="submit" disabled="disabled">Upload</button>
</form>
</div>
diff --git a/platformio/relayctl/static/md5.js b/platformio/relayctl/static/md5.js
new file mode 100644
index 0000000..b707a4e
--- /dev/null
+++ b/platformio/relayctl/static/md5.js
@@ -0,0 +1,615 @@
+/**
+ * [js-md5]{@link https://github.com/emn178/js-md5}
+ *
+ * @namespace md5
+ * @version 0.7.3
+ * @author Chen, Yi-Cyuan [emn178@gmail.com]
+ * @copyright Chen, Yi-Cyuan 2014-2017
+ * @license MIT
+ */
+(function () {
+ 'use strict';
+
+ var ERROR = 'input is invalid type';
+ var ARRAY_BUFFER = typeof window.ArrayBuffer !== 'undefined';
+ var HEX_CHARS = '0123456789abcdef'.split('');
+ var EXTRA = [128, 32768, 8388608, -2147483648];
+ var SHIFT = [0, 8, 16, 24];
+ var OUTPUT_TYPES = ['hex', 'array', 'digest', 'buffer', 'arrayBuffer', 'base64'];
+ var BASE64_ENCODE_CHAR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');
+
+ var blocks = [], buffer8;
+ if (ARRAY_BUFFER) {
+ var buffer = new ArrayBuffer(68);
+ buffer8 = new Uint8Array(buffer);
+ blocks = new Uint32Array(buffer);
+ }
+
+ if (!Array.isArray) {
+ Array.isArray = function (obj) {
+ return Object.prototype.toString.call(obj) === '[object Array]';
+ };
+ }
+
+ if (ARRAY_BUFFER && !ArrayBuffer.isView) {
+ ArrayBuffer.isView = function (obj) {
+ return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer;
+ };
+ }
+
+ /**
+ * @method hex
+ * @memberof md5
+ * @description Output hash as hex string
+ * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+ * @returns {String} Hex string
+ * @example
+ * md5.hex('The quick brown fox jumps over the lazy dog');
+ * // equal to
+ * md5('The quick brown fox jumps over the lazy dog');
+ */
+ /**
+ * @method digest
+ * @memberof md5
+ * @description Output hash as bytes array
+ * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+ * @returns {Array} Bytes array
+ * @example
+ * md5.digest('The quick brown fox jumps over the lazy dog');
+ */
+ /**
+ * @method array
+ * @memberof md5
+ * @description Output hash as bytes array
+ * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+ * @returns {Array} Bytes array
+ * @example
+ * md5.array('The quick brown fox jumps over the lazy dog');
+ */
+ /**
+ * @method arrayBuffer
+ * @memberof md5
+ * @description Output hash as ArrayBuffer
+ * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+ * @returns {ArrayBuffer} ArrayBuffer
+ * @example
+ * md5.arrayBuffer('The quick brown fox jumps over the lazy dog');
+ */
+ /**
+ * @method buffer
+ * @deprecated This maybe confuse with Buffer in node.js. Please use arrayBuffer instead.
+ * @memberof md5
+ * @description Output hash as ArrayBuffer
+ * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+ * @returns {ArrayBuffer} ArrayBuffer
+ * @example
+ * md5.buffer('The quick brown fox jumps over the lazy dog');
+ */
+ /**
+ * @method base64
+ * @memberof md5
+ * @description Output hash as base64 string
+ * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+ * @returns {String} base64 string
+ * @example
+ * md5.base64('The quick brown fox jumps over the lazy dog');
+ */
+ var createOutputMethod = function (outputType) {
+ return function (message) {
+ return new Md5(true).update(message)[outputType]();
+ };
+ };
+
+ /**
+ * @method create
+ * @memberof md5
+ * @description Create Md5 object
+ * @returns {Md5} Md5 object.
+ * @example
+ * var hash = md5.create();
+ */
+ /**
+ * @method update
+ * @memberof md5
+ * @description Create and update Md5 object
+ * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+ * @returns {Md5} Md5 object.
+ * @example
+ * var hash = md5.update('The quick brown fox jumps over the lazy dog');
+ * // equal to
+ * var hash = md5.create();
+ * hash.update('The quick brown fox jumps over the lazy dog');
+ */
+ var createMethod = function () {
+ var method = createOutputMethod('hex');
+ method.create = function () {
+ return new Md5();
+ };
+ method.update = function (message) {
+ return method.create().update(message);
+ };
+ for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
+ var type = OUTPUT_TYPES[i];
+ method[type] = createOutputMethod(type);
+ }
+ return method;
+ };
+
+ /**
+ * Md5 class
+ * @class Md5
+ * @description This is internal class.
+ * @see {@link md5.create}
+ */
+ function Md5(sharedMemory) {
+ if (sharedMemory) {
+ blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] =
+ blocks[4] = blocks[5] = blocks[6] = blocks[7] =
+ blocks[8] = blocks[9] = blocks[10] = blocks[11] =
+ blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+ this.blocks = blocks;
+ this.buffer8 = buffer8;
+ } else {
+ if (ARRAY_BUFFER) {
+ var buffer = new ArrayBuffer(68);
+ this.buffer8 = new Uint8Array(buffer);
+ this.blocks = new Uint32Array(buffer);
+ } else {
+ this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+ }
+ }
+ this.h0 = this.h1 = this.h2 = this.h3 = this.start = this.bytes = this.hBytes = 0;
+ this.finalized = this.hashed = false;
+ this.first = true;
+ }
+
+ /**
+ * @method update
+ * @memberof Md5
+ * @instance
+ * @description Update hash
+ * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+ * @returns {Md5} Md5 object.
+ * @see {@link md5.update}
+ */
+ Md5.prototype.update = function (message) {
+ if (this.finalized) {
+ return;
+ }
+
+ var notString, type = typeof message;
+ if (type !== 'string') {
+ if (type === 'object') {
+ if (message === null) {
+ throw ERROR;
+ } else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) {
+ message = new Uint8Array(message);
+ } else if (!Array.isArray(message)) {
+ if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) {
+ throw ERROR;
+ }
+ }
+ } else {
+ throw ERROR;
+ }
+ notString = true;
+ }
+ var code, index = 0, i, length = message.length, blocks = this.blocks;
+ var buffer8 = this.buffer8;
+
+ while (index < length) {
+ if (this.hashed) {
+ this.hashed = false;
+ blocks[0] = blocks[16];
+ blocks[16] = blocks[1] = blocks[2] = blocks[3] =
+ blocks[4] = blocks[5] = blocks[6] = blocks[7] =
+ blocks[8] = blocks[9] = blocks[10] = blocks[11] =
+ blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+ }
+
+ if (notString) {
+ if (ARRAY_BUFFER) {
+ for (i = this.start; index < length && i < 64; ++index) {
+ buffer8[i++] = message[index];
+ }
+ } else {
+ for (i = this.start; index < length && i < 64; ++index) {
+ blocks[i >> 2] |= message[index] << SHIFT[i++ & 3];
+ }
+ }
+ } else {
+ if (ARRAY_BUFFER) {
+ for (i = this.start; index < length && i < 64; ++index) {
+ code = message.charCodeAt(index);
+ if (code < 0x80) {
+ buffer8[i++] = code;
+ } else if (code < 0x800) {
+ buffer8[i++] = 0xc0 | (code >> 6);
+ buffer8[i++] = 0x80 | (code & 0x3f);
+ } else if (code < 0xd800 || code >= 0xe000) {
+ buffer8[i++] = 0xe0 | (code >> 12);
+ buffer8[i++] = 0x80 | ((code >> 6) & 0x3f);
+ buffer8[i++] = 0x80 | (code & 0x3f);
+ } else {
+ code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff));
+ buffer8[i++] = 0xf0 | (code >> 18);
+ buffer8[i++] = 0x80 | ((code >> 12) & 0x3f);
+ buffer8[i++] = 0x80 | ((code >> 6) & 0x3f);
+ buffer8[i++] = 0x80 | (code & 0x3f);
+ }
+ }
+ } else {
+ for (i = this.start; index < length && i < 64; ++index) {
+ code = message.charCodeAt(index);
+ if (code < 0x80) {
+ blocks[i >> 2] |= code << SHIFT[i++ & 3];
+ } else if (code < 0x800) {
+ blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+ } else if (code < 0xd800 || code >= 0xe000) {
+ blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+ } else {
+ code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff));
+ blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+ }
+ }
+ }
+ }
+ this.lastByteIndex = i;
+ this.bytes += i - this.start;
+ if (i >= 64) {
+ this.start = i - 64;
+ this.hash();
+ this.hashed = true;
+ } else {
+ this.start = i;
+ }
+ }
+ if (this.bytes > 4294967295) {
+ this.hBytes += this.bytes / 4294967296 << 0;
+ this.bytes = this.bytes % 4294967296;
+ }
+ return this;
+ };
+
+ Md5.prototype.finalize = function () {
+ if (this.finalized) {
+ return;
+ }
+ this.finalized = true;
+ var blocks = this.blocks, i = this.lastByteIndex;
+ blocks[i >> 2] |= EXTRA[i & 3];
+ if (i >= 56) {
+ if (!this.hashed) {
+ this.hash();
+ }
+ blocks[0] = blocks[16];
+ blocks[16] = blocks[1] = blocks[2] = blocks[3] =
+ blocks[4] = blocks[5] = blocks[6] = blocks[7] =
+ blocks[8] = blocks[9] = blocks[10] = blocks[11] =
+ blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+ }
+ blocks[14] = this.bytes << 3;
+ blocks[15] = this.hBytes << 3 | this.bytes >>> 29;
+ this.hash();
+ };
+
+ Md5.prototype.hash = function () {
+ var a, b, c, d, bc, da, blocks = this.blocks;
+
+ if (this.first) {
+ a = blocks[0] - 680876937;
+ a = (a << 7 | a >>> 25) - 271733879 << 0;
+ d = (-1732584194 ^ a & 2004318071) + blocks[1] - 117830708;
+ d = (d << 12 | d >>> 20) + a << 0;
+ c = (-271733879 ^ (d & (a ^ -271733879))) + blocks[2] - 1126478375;
+ c = (c << 17 | c >>> 15) + d << 0;
+ b = (a ^ (c & (d ^ a))) + blocks[3] - 1316259209;
+ b = (b << 22 | b >>> 10) + c << 0;
+ } else {
+ a = this.h0;
+ b = this.h1;
+ c = this.h2;
+ d = this.h3;
+ a += (d ^ (b & (c ^ d))) + blocks[0] - 680876936;
+ a = (a << 7 | a >>> 25) + b << 0;
+ d += (c ^ (a & (b ^ c))) + blocks[1] - 389564586;
+ d = (d << 12 | d >>> 20) + a << 0;
+ c += (b ^ (d & (a ^ b))) + blocks[2] + 606105819;
+ c = (c << 17 | c >>> 15) + d << 0;
+ b += (a ^ (c & (d ^ a))) + blocks[3] - 1044525330;
+ b = (b << 22 | b >>> 10) + c << 0;
+ }
+
+ a += (d ^ (b & (c ^ d))) + blocks[4] - 176418897;
+ a = (a << 7 | a >>> 25) + b << 0;
+ d += (c ^ (a & (b ^ c))) + blocks[5] + 1200080426;
+ d = (d << 12 | d >>> 20) + a << 0;
+ c += (b ^ (d & (a ^ b))) + blocks[6] - 1473231341;
+ c = (c << 17 | c >>> 15) + d << 0;
+ b += (a ^ (c & (d ^ a))) + blocks[7] - 45705983;
+ b = (b << 22 | b >>> 10) + c << 0;
+ a += (d ^ (b & (c ^ d))) + blocks[8] + 1770035416;
+ a = (a << 7 | a >>> 25) + b << 0;
+ d += (c ^ (a & (b ^ c))) + blocks[9] - 1958414417;
+ d = (d << 12 | d >>> 20) + a << 0;
+ c += (b ^ (d & (a ^ b))) + blocks[10] - 42063;
+ c = (c << 17 | c >>> 15) + d << 0;
+ b += (a ^ (c & (d ^ a))) + blocks[11] - 1990404162;
+ b = (b << 22 | b >>> 10) + c << 0;
+ a += (d ^ (b & (c ^ d))) + blocks[12] + 1804603682;
+ a = (a << 7 | a >>> 25) + b << 0;
+ d += (c ^ (a & (b ^ c))) + blocks[13] - 40341101;
+ d = (d << 12 | d >>> 20) + a << 0;
+ c += (b ^ (d & (a ^ b))) + blocks[14] - 1502002290;
+ c = (c << 17 | c >>> 15) + d << 0;
+ b += (a ^ (c & (d ^ a))) + blocks[15] + 1236535329;
+ b = (b << 22 | b >>> 10) + c << 0;
+ a += (c ^ (d & (b ^ c))) + blocks[1] - 165796510;
+ a = (a << 5 | a >>> 27) + b << 0;
+ d += (b ^ (c & (a ^ b))) + blocks[6] - 1069501632;
+ d = (d << 9 | d >>> 23) + a << 0;
+ c += (a ^ (b & (d ^ a))) + blocks[11] + 643717713;
+ c = (c << 14 | c >>> 18) + d << 0;
+ b += (d ^ (a & (c ^ d))) + blocks[0] - 373897302;
+ b = (b << 20 | b >>> 12) + c << 0;
+ a += (c ^ (d & (b ^ c))) + blocks[5] - 701558691;
+ a = (a << 5 | a >>> 27) + b << 0;
+ d += (b ^ (c & (a ^ b))) + blocks[10] + 38016083;
+ d = (d << 9 | d >>> 23) + a << 0;
+ c += (a ^ (b & (d ^ a))) + blocks[15] - 660478335;
+ c = (c << 14 | c >>> 18) + d << 0;
+ b += (d ^ (a & (c ^ d))) + blocks[4] - 405537848;
+ b = (b << 20 | b >>> 12) + c << 0;
+ a += (c ^ (d & (b ^ c))) + blocks[9] + 568446438;
+ a = (a << 5 | a >>> 27) + b << 0;
+ d += (b ^ (c & (a ^ b))) + blocks[14] - 1019803690;
+ d = (d << 9 | d >>> 23) + a << 0;
+ c += (a ^ (b & (d ^ a))) + blocks[3] - 187363961;
+ c = (c << 14 | c >>> 18) + d << 0;
+ b += (d ^ (a & (c ^ d))) + blocks[8] + 1163531501;
+ b = (b << 20 | b >>> 12) + c << 0;
+ a += (c ^ (d & (b ^ c))) + blocks[13] - 1444681467;
+ a = (a << 5 | a >>> 27) + b << 0;
+ d += (b ^ (c & (a ^ b))) + blocks[2] - 51403784;
+ d = (d << 9 | d >>> 23) + a << 0;
+ c += (a ^ (b & (d ^ a))) + blocks[7] + 1735328473;
+ c = (c << 14 | c >>> 18) + d << 0;
+ b += (d ^ (a & (c ^ d))) + blocks[12] - 1926607734;
+ b = (b << 20 | b >>> 12) + c << 0;
+ bc = b ^ c;
+ a += (bc ^ d) + blocks[5] - 378558;
+ a = (a << 4 | a >>> 28) + b << 0;
+ d += (bc ^ a) + blocks[8] - 2022574463;
+ d = (d << 11 | d >>> 21) + a << 0;
+ da = d ^ a;
+ c += (da ^ b) + blocks[11] + 1839030562;
+ c = (c << 16 | c >>> 16) + d << 0;
+ b += (da ^ c) + blocks[14] - 35309556;
+ b = (b << 23 | b >>> 9) + c << 0;
+ bc = b ^ c;
+ a += (bc ^ d) + blocks[1] - 1530992060;
+ a = (a << 4 | a >>> 28) + b << 0;
+ d += (bc ^ a) + blocks[4] + 1272893353;
+ d = (d << 11 | d >>> 21) + a << 0;
+ da = d ^ a;
+ c += (da ^ b) + blocks[7] - 155497632;
+ c = (c << 16 | c >>> 16) + d << 0;
+ b += (da ^ c) + blocks[10] - 1094730640;
+ b = (b << 23 | b >>> 9) + c << 0;
+ bc = b ^ c;
+ a += (bc ^ d) + blocks[13] + 681279174;
+ a = (a << 4 | a >>> 28) + b << 0;
+ d += (bc ^ a) + blocks[0] - 358537222;
+ d = (d << 11 | d >>> 21) + a << 0;
+ da = d ^ a;
+ c += (da ^ b) + blocks[3] - 722521979;
+ c = (c << 16 | c >>> 16) + d << 0;
+ b += (da ^ c) + blocks[6] + 76029189;
+ b = (b << 23 | b >>> 9) + c << 0;
+ bc = b ^ c;
+ a += (bc ^ d) + blocks[9] - 640364487;
+ a = (a << 4 | a >>> 28) + b << 0;
+ d += (bc ^ a) + blocks[12] - 421815835;
+ d = (d << 11 | d >>> 21) + a << 0;
+ da = d ^ a;
+ c += (da ^ b) + blocks[15] + 530742520;
+ c = (c << 16 | c >>> 16) + d << 0;
+ b += (da ^ c) + blocks[2] - 995338651;
+ b = (b << 23 | b >>> 9) + c << 0;
+ a += (c ^ (b | ~d)) + blocks[0] - 198630844;
+ a = (a << 6 | a >>> 26) + b << 0;
+ d += (b ^ (a | ~c)) + blocks[7] + 1126891415;
+ d = (d << 10 | d >>> 22) + a << 0;
+ c += (a ^ (d | ~b)) + blocks[14] - 1416354905;
+ c = (c << 15 | c >>> 17) + d << 0;
+ b += (d ^ (c | ~a)) + blocks[5] - 57434055;
+ b = (b << 21 | b >>> 11) + c << 0;
+ a += (c ^ (b | ~d)) + blocks[12] + 1700485571;
+ a = (a << 6 | a >>> 26) + b << 0;
+ d += (b ^ (a | ~c)) + blocks[3] - 1894986606;
+ d = (d << 10 | d >>> 22) + a << 0;
+ c += (a ^ (d | ~b)) + blocks[10] - 1051523;
+ c = (c << 15 | c >>> 17) + d << 0;
+ b += (d ^ (c | ~a)) + blocks[1] - 2054922799;
+ b = (b << 21 | b >>> 11) + c << 0;
+ a += (c ^ (b | ~d)) + blocks[8] + 1873313359;
+ a = (a << 6 | a >>> 26) + b << 0;
+ d += (b ^ (a | ~c)) + blocks[15] - 30611744;
+ d = (d << 10 | d >>> 22) + a << 0;
+ c += (a ^ (d | ~b)) + blocks[6] - 1560198380;
+ c = (c << 15 | c >>> 17) + d << 0;
+ b += (d ^ (c | ~a)) + blocks[13] + 1309151649;
+ b = (b << 21 | b >>> 11) + c << 0;
+ a += (c ^ (b | ~d)) + blocks[4] - 145523070;
+ a = (a << 6 | a >>> 26) + b << 0;
+ d += (b ^ (a | ~c)) + blocks[11] - 1120210379;
+ d = (d << 10 | d >>> 22) + a << 0;
+ c += (a ^ (d | ~b)) + blocks[2] + 718787259;
+ c = (c << 15 | c >>> 17) + d << 0;
+ b += (d ^ (c | ~a)) + blocks[9] - 343485551;
+ b = (b << 21 | b >>> 11) + c << 0;
+
+ if (this.first) {
+ this.h0 = a + 1732584193 << 0;
+ this.h1 = b - 271733879 << 0;
+ this.h2 = c - 1732584194 << 0;
+ this.h3 = d + 271733878 << 0;
+ this.first = false;
+ } else {
+ this.h0 = this.h0 + a << 0;
+ this.h1 = this.h1 + b << 0;
+ this.h2 = this.h2 + c << 0;
+ this.h3 = this.h3 + d << 0;
+ }
+ };
+
+ /**
+ * @method hex
+ * @memberof Md5
+ * @instance
+ * @description Output hash as hex string
+ * @returns {String} Hex string
+ * @see {@link md5.hex}
+ * @example
+ * hash.hex();
+ */
+ Md5.prototype.hex = function () {
+ this.finalize();
+
+ var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3;
+
+ return HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] +
+ HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] +
+ HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] +
+ HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] +
+ HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] +
+ HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] +
+ HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] +
+ HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] +
+ HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] +
+ HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] +
+ HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] +
+ HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] +
+ HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] +
+ HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] +
+ HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] +
+ HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F];
+ };
+
+ /**
+ * @method toString
+ * @memberof Md5
+ * @instance
+ * @description Output hash as hex string
+ * @returns {String} Hex string
+ * @see {@link md5.hex}
+ * @example
+ * hash.toString();
+ */
+ Md5.prototype.toString = Md5.prototype.hex;
+
+ /**
+ * @method digest
+ * @memberof Md5
+ * @instance
+ * @description Output hash as bytes array
+ * @returns {Array} Bytes array
+ * @see {@link md5.digest}
+ * @example
+ * hash.digest();
+ */
+ Md5.prototype.digest = function () {
+ this.finalize();
+
+ var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3;
+ return [
+ h0 & 0xFF, (h0 >> 8) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 24) & 0xFF,
+ h1 & 0xFF, (h1 >> 8) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 24) & 0xFF,
+ h2 & 0xFF, (h2 >> 8) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 24) & 0xFF,
+ h3 & 0xFF, (h3 >> 8) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 24) & 0xFF
+ ];
+ };
+
+ /**
+ * @method array
+ * @memberof Md5
+ * @instance
+ * @description Output hash as bytes array
+ * @returns {Array} Bytes array
+ * @see {@link md5.array}
+ * @example
+ * hash.array();
+ */
+ Md5.prototype.array = Md5.prototype.digest;
+
+ /**
+ * @method arrayBuffer
+ * @memberof Md5
+ * @instance
+ * @description Output hash as ArrayBuffer
+ * @returns {ArrayBuffer} ArrayBuffer
+ * @see {@link md5.arrayBuffer}
+ * @example
+ * hash.arrayBuffer();
+ */
+ Md5.prototype.arrayBuffer = function () {
+ this.finalize();
+
+ var buffer = new ArrayBuffer(16);
+ var blocks = new Uint32Array(buffer);
+ blocks[0] = this.h0;
+ blocks[1] = this.h1;
+ blocks[2] = this.h2;
+ blocks[3] = this.h3;
+ return buffer;
+ };
+
+ /**
+ * @method buffer
+ * @deprecated This maybe confuse with Buffer in node.js. Please use arrayBuffer instead.
+ * @memberof Md5
+ * @instance
+ * @description Output hash as ArrayBuffer
+ * @returns {ArrayBuffer} ArrayBuffer
+ * @see {@link md5.buffer}
+ * @example
+ * hash.buffer();
+ */
+ Md5.prototype.buffer = Md5.prototype.arrayBuffer;
+
+ /**
+ * @method base64
+ * @memberof Md5
+ * @instance
+ * @description Output hash as base64 string
+ * @returns {String} base64 string
+ * @see {@link md5.base64}
+ * @example
+ * hash.base64();
+ */
+ Md5.prototype.base64 = function () {
+ var v1, v2, v3, base64Str = '', bytes = this.array();
+ for (var i = 0; i < 15;) {
+ v1 = bytes[i++];
+ v2 = bytes[i++];
+ v3 = bytes[i++];
+ base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] +
+ BASE64_ENCODE_CHAR[(v1 << 4 | v2 >>> 4) & 63] +
+ BASE64_ENCODE_CHAR[(v2 << 2 | v3 >>> 6) & 63] +
+ BASE64_ENCODE_CHAR[v3 & 63];
+ }
+ v1 = bytes[i];
+ base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] +
+ BASE64_ENCODE_CHAR[(v1 << 4) & 63] +
+ '==';
+ return base64Str;
+ };
+
+ window.md5 = createMethod();
+})();
diff --git a/platformio/relayctl/static/style.css b/platformio/relayctl/static/style.css
index 9a950a5..32bd02c 100644
--- a/platformio/relayctl/static/style.css
+++ b/platformio/relayctl/static/style.css
@@ -30,7 +30,7 @@ body, button, input[type="text"], input[type="password"] {
.form_input {
margin-bottom: 15px;
}
-.form_checkbox {
+.form_sublabel {
padding-top: 3px;
}
diff --git a/src/home/api/types/types.py b/src/home/api/types/types.py
index 1d5596f..3f77b19 100644
--- a/src/home/api/types/types.py
+++ b/src/home/api/types/types.py
@@ -8,6 +8,7 @@ class BotType(Enum):
ADMIN = auto()
SOUND = auto()
POLARIS_KETTLE = auto()
+ PUMP_MQTT = auto()
class TemperatureSensorLocation(Enum):
diff --git a/src/home/api/web_api_client.py b/src/home/api/web_api_client.py
index f74b5a1..ca9a9ee 100644
--- a/src/home/api/web_api_client.py
+++ b/src/home/api/web_api_client.py
@@ -183,9 +183,9 @@ class WebAPIClient:
data = json.loads(r.text)
if r.status_code != 200:
raise ApiResponseError(r.status_code,
- data['error']['type'],
- data['error']['message'],
- data['error']['stacktrace'] if 'stacktrace' in data['error'] else None)
+ data['error'],
+ data['message'],
+ data['stacktrace'] if 'stacktrace' in data['error'] else None)
return data['response'] if 'response' in data else True
finally:
diff --git a/src/home/mqtt/__init__.py b/src/home/mqtt/__init__.py
index c0ef9ba..a6f5f5e 100644
--- a/src/home/mqtt/__init__.py
+++ b/src/home/mqtt/__init__.py
@@ -1,2 +1,3 @@
from .mqtt import MQTTBase
from .util import poll_tick
+from .relay import MQTTRelay \ No newline at end of file
diff --git a/src/home/mqtt/mqtt.py b/src/home/mqtt/mqtt.py
index b3334b5..9dd973b 100644
--- a/src/home/mqtt/mqtt.py
+++ b/src/home/mqtt/mqtt.py
@@ -6,8 +6,6 @@ import logging
from typing import Tuple
from ..config import config
-logger = logging.getLogger(__name__)
-
def username_and_password() -> Tuple[str, str]:
username = config['mqtt']['username'] if 'username' in config['mqtt'] else None
@@ -23,11 +21,15 @@ class MQTTBase:
self._client.on_connect = self.on_connect
self._client.on_disconnect = self.on_disconnect
self._client.on_message = self.on_message
+ self._client.on_log = self.on_log
+ self._client.on_publish = self.on_publish
+ self._loop_started = False
self._logger = logging.getLogger(self.__class__.__name__)
username, password = username_and_password()
if username and password:
+ self._logger.debug(f'username={username} password={password}')
self._client.username_pw_set(username, password)
def configure_tls(self):
@@ -50,6 +52,12 @@ class MQTTBase:
self._client.loop_forever()
else:
self._client.loop_start()
+ self._loop_started = True
+
+ def disconnect(self):
+ self._client.disconnect()
+ self._client.loop_write()
+ self._client.loop_stop()
def on_connect(self, client: mqtt.Client, userdata, flags, rc):
self._logger.info("Connected with result code " + str(rc))
@@ -57,5 +65,12 @@ class MQTTBase:
def on_disconnect(self, client: mqtt.Client, userdata, rc):
self._logger.info("Disconnected with result code " + str(rc))
+ def on_log(self, client: mqtt.Client, userdata, level, buf):
+ level = mqtt.LOGGING_LEVEL[level] if level in mqtt.LOGGING_LEVEL else logging.INFO
+ self._logger.log(level, f'MQTT: {buf}')
+
def on_message(self, client: mqtt.Client, userdata, msg):
- self._logger.info(msg.topic + ": " + str(msg.payload))
+ self._logger.debug(msg.topic + ": " + str(msg.payload))
+
+ def on_publish(self, client: mqtt.Client, userdata, mid):
+ self._logger.debug(f'publish done, mid={mid}') \ No newline at end of file
diff --git a/src/home/mqtt/payload/__init__.py b/src/home/mqtt/payload/__init__.py
index e69de29..9fcaf3e 100644
--- a/src/home/mqtt/payload/__init__.py
+++ b/src/home/mqtt/payload/__init__.py
@@ -0,0 +1 @@
+from .base_payload import MQTTPayload \ No newline at end of file
diff --git a/src/home/mqtt/payload/relay.py b/src/home/mqtt/payload/relay.py
index 2a327ba..debc2c8 100644
--- a/src/home/mqtt/payload/relay.py
+++ b/src/home/mqtt/payload/relay.py
@@ -1,6 +1,10 @@
+import hashlib
+
from .base_payload import MQTTPayload, MQTTPayloadCustomField
+# _logger = logging.getLogger(__name__)
+
class StatFlags(MQTTPayloadCustomField):
state: bool
config_changed_value_present: bool
@@ -8,9 +12,11 @@ class StatFlags(MQTTPayloadCustomField):
@staticmethod
def unpack(flags: int):
+ # _logger.debug(f'StatFlags.unpack: flags={flags}')
state = flags & 0x1
ccvp = (flags >> 1) & 0x1
cc = (flags >> 2) & 0x1
+ # _logger.debug(f'StatFlags.unpack: state={state}')
return StatFlags(state=(state == 1),
config_changed_value_present=(ccvp == 1),
config_changed=(cc == 1))
@@ -24,7 +30,7 @@ class StatFlags(MQTTPayloadCustomField):
class InitialStatPayload(MQTTPayload):
- FORMAT = 'IBbIB'
+ FORMAT = '=IBbIB'
ip: int
fw_version: int
@@ -34,7 +40,7 @@ class InitialStatPayload(MQTTPayload):
class StatPayload(MQTTPayload):
- FORMAT = 'bIB'
+ FORMAT = '=bIB'
rssi: int
free_heap: int
@@ -42,13 +48,42 @@ class StatPayload(MQTTPayload):
class PowerPayload(MQTTPayload):
- FORMAT = '12sB'
+ FORMAT = '=12sB'
PACKER = {
- 'state': lambda n: int(n)
+ 'state': lambda n: int(n),
+ 'secret': lambda s: s.encode('utf-8')
}
UNPACKER = {
- 'state': lambda n: bool(n)
+ 'state': lambda n: bool(n),
+ 'secret': lambda s: s.decode('utf-8')
}
secret: str
state: bool
+
+
+class OTAPayload(MQTTPayload):
+ secret: str
+ filename: str
+
+ # structure of returned data:
+ #
+ # uint8_t[len(secret)] secret;
+ # uint8_t[16] md5;
+ # *uint8_t data
+
+ def pack(self):
+ buf = bytearray(self.secret.encode())
+ m = hashlib.md5()
+ with open(self.filename, 'rb') as fd:
+ content = fd.read()
+ m.update(content)
+ buf.extend(m.digest())
+ buf.extend(content)
+ return buf
+
+ def unpack(cls, buf: bytes):
+ raise RuntimeError(f'{cls.__class__.__name__}.unpack: not implemented')
+ # secret = buf[:12].decode()
+ # filename = buf[12:].decode()
+ # return OTAPayload(secret=secret, filename=filename)
diff --git a/src/home/mqtt/relay.py b/src/home/mqtt/relay.py
new file mode 100644
index 0000000..0f97b5b
--- /dev/null
+++ b/src/home/mqtt/relay.py
@@ -0,0 +1,93 @@
+import paho.mqtt.client as mqtt
+import re
+
+from .mqtt import MQTTBase
+from typing import Optional, Union
+from .payload.relay import (
+ InitialStatPayload,
+ StatPayload,
+ PowerPayload,
+ OTAPayload
+)
+
+
+class MQTTRelay(MQTTBase):
+ _home_id: Union[str, int]
+ _secret: str
+ _message_callback: Optional[callable]
+ _ota_publish_callback: Optional[callable]
+
+ def __init__(self,
+ home_id: Union[str, int],
+ secret: str,
+ subscribe_to_updates=True):
+ super().__init__(clean_session=True)
+ self._home_id = home_id
+ self._secret = secret
+ self._message_callback = None
+ self._ota_publish_callback = None
+ self._subscribe_to_updates = subscribe_to_updates
+ self._ota_mid = None
+
+ def on_connect(self, client: mqtt.Client, userdata, flags, rc):
+ super().on_connect(client, userdata, flags, rc)
+
+ if self._subscribe_to_updates:
+ topic = f'hk/{self._home_id}/relay/#'
+ self._logger.info(f"subscribing to {topic}")
+
+ client.subscribe(topic, qos=1)
+
+ def on_publish(self, client: mqtt.Client, userdata, mid):
+ if self._ota_mid is not None and mid == self._ota_mid and self._ota_publish_callback:
+ self._ota_publish_callback()
+
+ def set_message_callback(self, callback: callable):
+ self._message_callback = callback
+
+ def on_message(self, client: mqtt.Client, userdata, msg):
+ try:
+ match = re.match(r'^hk/(.*?)/relay/(stat|stat1|power|otares)$', msg.topic)
+ self._logger.debug(f'topic: {msg.topic}')
+ if not match:
+ return
+
+ name = match.group(1)
+ subtopic = match.group(2)
+
+ if name != self._home_id:
+ return
+
+ message = None
+ if subtopic == 'stat':
+ message = StatPayload.unpack(msg.payload)
+ elif subtopic == 'stat1':
+ message = InitialStatPayload.unpack(msg.payload)
+ elif subtopic == 'power':
+ message = PowerPayload.unpack(msg.payload)
+
+ if message and self._message_callback:
+ self._message_callback(message)
+
+ except Exception as e:
+ self._logger.exception(str(e))
+
+ def set_power(self, enable: bool):
+ payload = PowerPayload(secret=self._secret,
+ state=enable)
+ self._client.publish(f'hk/{self._home_id}/relay/power',
+ payload=payload.pack(),
+ qos=1)
+ self._client.loop_write()
+
+ def push_ota(self,
+ filename: str,
+ publish_callback: callable,
+ qos: int):
+ self._ota_publish_callback = publish_callback
+ payload = OTAPayload(secret=self._secret, filename=filename)
+ publish_result = self._client.publish(f'hk/{self._home_id}/relay/admin/ota',
+ payload=payload.pack(),
+ qos=qos)
+ self._ota_mid = publish_result.mid
+ self._client.loop_write()
diff --git a/src/home/telegram/bot.py b/src/home/telegram/bot.py
index 3c82587..bd09f42 100644
--- a/src/home/telegram/bot.py
+++ b/src/home/telegram/bot.py
@@ -110,6 +110,8 @@ def _handler_of_handler(*args, **kwargs):
ctx.reply_exc(e)
else:
notify_user(ctx.user_id, exc2text(e))
+ else:
+ _logger.exception(e)
def handler(**kwargs):
diff --git a/src/pump_mqtt_bot.py b/src/pump_mqtt_bot.py
new file mode 100755
index 0000000..d87234b
--- /dev/null
+++ b/src/pump_mqtt_bot.py
@@ -0,0 +1,191 @@
+#!/usr/bin/env python3
+import datetime
+
+from enum import Enum
+from typing import Optional
+from telegram import ReplyKeyboardMarkup, User
+
+from home.config import config
+from home.telegram import bot
+from home.telegram._botutil import user_any_name
+from home.api.types import BotType
+from home.mqtt import MQTTRelay
+from home.mqtt.payload import MQTTPayload
+from home.mqtt.payload.relay import InitialStatPayload, StatPayload
+
+
+config.load('pump_mqtt_bot')
+
+bot.initialize()
+bot.lang.ru(
+ start_message="Выберите команду на клавиатуре",
+ start_message_no_access="Доступ запрещён. Вы можете отправить заявку на получение доступа.",
+ unknown_command="Неизвестная команда",
+ send_access_request="Отправить заявку",
+ management="Админка",
+
+ enable="Включить",
+ enabled="Включен ✅",
+
+ disable="Выключить",
+ disabled="Выключен ❌",
+
+ status="Статус",
+ status_updated=' (обновлено %s)',
+
+ done="Готово 👌",
+ user_action_notification='Пользователь <a href="tg://user?id=%d">%s</a> <b>%s</b> насос.',
+ user_action_on="включил",
+ user_action_off="выключил",
+ date_yday="вчера",
+ date_yyday="позавчера",
+ date_at="в"
+)
+bot.lang.en(
+ start_message="Select command on the keyboard",
+ start_message_no_access="You have no access.",
+ unknown_command="Unknown command",
+ send_access_request="Send request",
+ management="Admin options",
+
+ enable="Turn ON",
+ enable_silently="Turn ON silently",
+ enabled="Turned ON ✅",
+
+ disable="Turn OFF",
+ disable_silently="Turn OFF silently",
+ disabled="Turned OFF ❌",
+
+ status="Status",
+ status_updated=' (updated %s)',
+
+ done="Done 👌",
+ user_action_notification='User <a href="tg://user?id=%d">%s</a> turned the pump <b>%s</b>.',
+ user_action_on="ON",
+ user_action_off="OFF",
+
+ date_yday="yesterday",
+ date_yyday="the day before yesterday",
+ date_at="at"
+)
+
+
+class RelayState:
+ enabled: bool
+ update_time: datetime.datetime
+ rssi: int
+ fw_version: int
+ ever_updated: bool
+
+ def __init__(self):
+ self.ever_updated = False
+
+ def update(self,
+ enabled: bool,
+ rssi: int,
+ fw_version=None):
+ self.ever_updated = True
+ self.enabled = enabled
+ self.rssi = rssi
+ self.update_time = datetime.datetime.now()
+ if fw_version:
+ self.fw_version = fw_version
+
+
+mqtt_relay: Optional[MQTTRelay] = None
+relay_state = RelayState()
+
+
+class UserAction(Enum):
+ ON = 'on'
+ OFF = 'off'
+
+
+def on_mqtt_message(message: MQTTPayload):
+ if isinstance(message, InitialStatPayload) or isinstance(message, StatPayload):
+ kwargs = dict(rssi=message.rssi, enabled=message.flags.state)
+ if isinstance(message, InitialStatPayload):
+ kwargs['fw_version'] = message.fw_version
+ relay_state.update(**kwargs)
+
+
+def notify(user: User, action: UserAction) -> None:
+ def text_getter(lang: str):
+ action_name = bot.lang.get(f'user_action_{action.value}', lang)
+ user_name = user_any_name(user)
+ return 'ℹ ' + bot.lang.get('user_action_notification', lang,
+ user.id, user_name, action_name)
+
+ bot.notify_all(text_getter, exclude=(user.id,))
+
+
+@bot.handler(message='enable')
+def enable_handler(ctx: bot.Context) -> None:
+ mqtt_relay.set_power(True)
+ ctx.reply(ctx.lang('done'))
+ notify(ctx.user, UserAction.ON)
+
+
+@bot.handler(message='disable')
+def disable_handler(ctx: bot.Context) -> None:
+ mqtt_relay.set_power(False)
+ ctx.reply(ctx.lang('done'))
+ notify(ctx.user, UserAction.OFF)
+
+
+@bot.handler(message='status')
+def status(ctx: bot.Context) -> None:
+ label = ctx.lang('enabled') if relay_state.enabled else ctx.lang('disabled')
+ if relay_state.ever_updated:
+ date_label = ''
+ today = datetime.date.today()
+ if today != relay_state.update_time.date():
+ yday = today - datetime.timedelta(days=1)
+ yyday = today - datetime.timedelta(days=2)
+ if yday == relay_state.update_time.date():
+ date_label = ctx.lang('date_yday')
+ elif yyday == relay_state.update_time.date():
+ date_label = ctx.lang('date_yyday')
+ else:
+ date_label = relay_state.update_time.strftime('%d.%m.%Y')
+ date_label += ' '
+ date_label += ctx.lang('date_at') + ' '
+ date_label += relay_state.update_time.strftime('%H:%M')
+ label += ctx.lang('status_updated', date_label)
+ ctx.reply(label)
+
+
+def start(ctx: bot.Context) -> None:
+ if ctx.user_id in config['bot']['users'] or ctx.user_id in config['bot']['admin_users']:
+ ctx.reply(ctx.lang('start_message'))
+ else:
+ buttons = [
+ [ctx.lang('send_access_request')]
+ ]
+ ctx.reply(ctx.lang('start_message_no_access'), markup=ReplyKeyboardMarkup(buttons, one_time_keyboard=False))
+
+
+@bot.exceptionhandler
+def exception_handler(e: Exception, ctx: bot.Context) -> bool:
+ return False
+
+
+@bot.defaultreplymarkup
+def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]:
+ buttons = [[ctx.lang('enable'), ctx.lang('disable')], [ctx.lang('status')]]
+ if ctx.user_id in config['bot']['admin_users']:
+ buttons.append([ctx.lang('management')])
+ return ReplyKeyboardMarkup(buttons, one_time_keyboard=False)
+
+
+if __name__ == '__main__':
+ mqtt_relay = MQTTRelay(home_id=config['mqtt']['home_id'],
+ secret=config['mqtt']['relay']['secret'])
+ mqtt_relay.set_message_callback(on_mqtt_message)
+ mqtt_relay.configure_tls()
+ mqtt_relay.connect_and_loop(loop_forever=False)
+
+ # bot.enable_logging(BotType.PUMP_MQTT)
+ bot.run(start_handler=start)
+
+ mqtt_relay.disconnect()
diff --git a/tools/mcuota.py b/tools/mcuota.py
new file mode 100755
index 0000000..f7f2968
--- /dev/null
+++ b/tools/mcuota.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+import sys
+import os.path
+sys.path.extend([
+ os.path.realpath(
+ os.path.join(os.path.dirname(os.path.join(__file__)), '..')
+ )
+])
+
+from time import sleep
+from argparse import ArgumentParser
+from src.home.config import config
+from src.home.mqtt import MQTTRelay
+
+
+def guess_filename(product: str, build_target: str):
+ return os.path.join(
+ products_dir,
+ product,
+ '.pio',
+ 'build',
+ build_target,
+ 'firmware.bin'
+ )
+
+
+def relayctl_publish_ota(filename: str,
+ home_id: str,
+ home_secret: str,
+ qos: int):
+ global stop
+
+ def published():
+ global stop
+ stop = True
+
+ mqtt_relay = MQTTRelay(home_id=home_id,
+ secret=home_secret)
+ mqtt_relay.configure_tls()
+ mqtt_relay.connect_and_loop(loop_forever=False)
+ mqtt_relay.push_ota(filename, published, qos)
+ while not stop:
+ sleep(0.1)
+ mqtt_relay.disconnect()
+
+
+stop = False
+products = {
+ 'relayctl': {
+ 'build_target': 'esp12e',
+ 'callback': relayctl_publish_ota
+ }
+}
+
+products_dir = os.path.join(
+ os.path.dirname(__file__),
+ '..',
+ 'platformio'
+)
+
+
+def main():
+ parser = ArgumentParser()
+ parser.add_argument('--filename', type=str)
+ parser.add_argument('--home-id', type=str, required=True)
+ parser.add_argument('--product', type=str, required=True)
+ parser.add_argument('--qos', type=int, default=1)
+
+ config.load('mcuota_push', parser=parser)
+ arg = parser.parse_args()
+
+ if arg.product not in products:
+ raise ValueError(f'invalid product: \'{arg.product}\' not found')
+
+ if arg.home_id not in config['mqtt']['home_secrets']:
+ raise ValueError(f'home_secret for home {arg.home_id} not found in config!')
+
+ filename = arg.filename if arg.filename else guess_filename(arg.product, products[arg.product]['build_target'])
+ if not os.path.exists(filename):
+ raise OSError(f'file \'{filename}\' does not exists')
+
+ print('Please confirm following OTA params.')
+ print('')
+ print(f' Home ID: {arg.home_id}')
+ print(f' Product: {arg.product}')
+ print(f'Firmware file: {filename}')
+ print('')
+ input('Press any key to continue or Ctrl+C to abort.')
+
+ products[arg.product]['callback'](filename, arg.home_id, config['mqtt']['home_secrets'][arg.home_id], qos=arg.qos)
+
+
+if __name__ == '__main__':
+ try:
+ main()
+ except Exception as e:
+ print(str(e), file=sys.stderr)
+ sys.exit(1)
diff --git a/tools/mcuota.sh b/tools/mcuota.sh
new file mode 100755
index 0000000..b2e7910
--- /dev/null
+++ b/tools/mcuota.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
+
+. "$DIR/lib.bash"
+
+if [ -d "$DIR/../venv" ]; then
+ echoinfo "activating python venv"
+ . "$DIR/../venv/bin/activate"
+else
+ echowarn "python venv not found"
+fi
+
+"$DIR/mcuota.py" "$@" \ No newline at end of file
diff --git a/tools/minify.js b/tools/minify.js
index 105c4eb..159e34b 100755
--- a/tools/minify.js
+++ b/tools/minify.js
@@ -52,12 +52,24 @@ const args = parseArgs(argv, {
break
case 'css':
- console.log(new CleanCSS({level:2}).minify(content).styles)
+ console.log(new CleanCSS({
+ level: 2
+ }).minify(content).styles)
break
case 'js':
console.log((await minifyJs(content, {
- ecma: 5
+ mangle: {
+ eval: true,
+ keep_classnames: false,
+ keep_fnames: false,
+ module: false,
+ toplevel: true,
+ },
+ ecma: 5,
+ format: {
+ comments: false
+ }
})).code)
break
}