diff options
author | Evgeny Zinoviev <me@ch1p.io> | 2023-09-27 00:54:57 +0300 |
---|---|---|
committer | Evgeny Zinoviev <me@ch1p.io> | 2023-09-27 00:54:57 +0300 |
commit | d3a295872c49defb55fc8e4e43e55550991e0927 (patch) | |
tree | b9dca15454f9027d5a9dad0d4443a20de04dbc5d /include/pio/libs | |
parent | b7cbc2571c1870b4582ead45277d0aa7f961bec8 (diff) | |
parent | bdbb296697f55f4c3a07af43c9aaf7a9ea86f3d0 (diff) |
Merge branch 'master' of ch1p.io:homekit
Diffstat (limited to 'include/pio/libs')
42 files changed, 2417 insertions, 0 deletions
diff --git a/include/pio/libs/config/homekit/config.cpp b/include/pio/libs/config/homekit/config.cpp new file mode 100644 index 0000000..5bafcad --- /dev/null +++ b/include/pio/libs/config/homekit/config.cpp @@ -0,0 +1,84 @@ +#include <EEPROM.h> +#include <strings.h> +#include "config.h" +#include <homekit/logging.h> + +#define GET_DATA_CRC(data) \ + eeprom_crc(reinterpret_cast<uint8_t*>(&(data))+4, sizeof(ConfigData)-4) + +namespace homekit::config { + +static const uint32_t magic = 0xdeadbeef; +static const uint32_t crc_table[16] PROGMEM = { + 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, + 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, + 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, + 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c +}; + +static uint32_t eeprom_crc(const uint8_t* data, size_t len) { + uint32_t crc = ~0L; + for (size_t index = 0; index < len; index++) { + crc = pgm_read_word(&crc_table[(crc ^ data[index]) & 0x0f]) ^ (crc >> 4); + crc = pgm_read_word(&crc_table[(crc ^ (data[index] >> 4)) & 0x0f]) ^ (crc >> 4); + crc = ~crc; + } + return crc; +} + +ConfigData read() { + ConfigData data; + EEPROM.begin(sizeof(ConfigData)); + EEPROM.get(0, data); + EEPROM.end(); +#ifdef DEBUG + if (!isValid(data)) { + PRINTLN("config::read(): data is not valid!"); + } +#endif + return data; +} + +void write(ConfigData& data) { + EEPROM.begin(sizeof(ConfigData)); + data.magic = magic; + data.crc = GET_DATA_CRC(data); + EEPROM.put(0, data); + EEPROM.end(); +} + +void erase() { + ConfigData data; + erase(data); +} + +void erase(ConfigData& data) { + bzero(reinterpret_cast<uint8_t*>(&data), sizeof(data)); + write(data); +} + +bool isValid(ConfigData& data) { + return data.crc == GET_DATA_CRC(data); +} + +bool isDirty(ConfigData& data) { + return data.magic != magic; +} + +char* ConfigData::escapeHomeId(char* buf, size_t len) { + if (len < 32) + return nullptr; + size_t id_len = strlen(node_id); + char* c = node_id; + char* dst = buf; + for (size_t i = 0; i < id_len; i++) { + if (*c == '"') + *(dst++) = '\\'; + *(dst++) = *c; + c++; + } + *dst = '\0'; + return buf; +} + +} diff --git a/include/pio/libs/config/homekit/config.h b/include/pio/libs/config/homekit/config.h new file mode 100644 index 0000000..28f01fb --- /dev/null +++ b/include/pio/libs/config/homekit/config.h @@ -0,0 +1,37 @@ +#ifndef COMMON_HOMEKIT_CONFIG_H +#define COMMON_HOMEKIT_CONFIG_H + +#include <Arduino.h> + +namespace homekit::config { + +struct ConfigFlags { + uint8_t wifi_configured: 1; + uint8_t node_configured: 1; + uint8_t reserved: 6; +} __attribute__((packed)); + +struct ConfigData { + // helpers + uint32_t crc = 0; + uint32_t magic = 0; + char node_id[16] = {0}; + char wifi_ssid[32] = {0}; + char wifi_psk[63] = {0}; + ConfigFlags flags {0}; + + // helper methods + char* escapeHomeId(char* buf, size_t len); +} __attribute__((packed)); + + +ConfigData read(); +void write(ConfigData& data); +void erase(); +void erase(ConfigData& data); +bool isValid(ConfigData& data); +bool isDirty(ConfigData& data); + +} + +#endif //COMMON_HOMEKIT_CONFIG_H
\ No newline at end of file diff --git a/include/pio/libs/config/library.json b/include/pio/libs/config/library.json new file mode 100644 index 0000000..720d093 --- /dev/null +++ b/include/pio/libs/config/library.json @@ -0,0 +1,8 @@ +{ + "name": "homekit_config", + "version": "1.0.2", + "build": { + "flags": "-I../../include" + } +} + diff --git a/include/pio/libs/http_server/homekit/http_server.cpp b/include/pio/libs/http_server/homekit/http_server.cpp new file mode 100644 index 0000000..ea81f5b --- /dev/null +++ b/include/pio/libs/http_server/homekit/http_server.cpp @@ -0,0 +1,282 @@ +#include "http_server.h" + +#include <Arduino.h> +#include <string.h> + +#include <homekit/static.h> +#include <homekit/config.h> +#include <homekit/logging.h> +#include <homekit/macros.h> +#include <homekit/util.h> + +namespace homekit { + +using files::StaticFile; + +static const char CONTENT_TYPE_HTML[] PROGMEM = "text/html; charset=utf-8"; +static const char CONTENT_TYPE_CSS[] PROGMEM = "text/css"; +static const char CONTENT_TYPE_JS[] PROGMEM = "application/javascript"; +static const char CONTENT_TYPE_JSON[] PROGMEM = "application/json"; +static const char CONTENT_TYPE_FAVICON[] PROGMEM = "image/x-icon"; + +static const char JSON_UPDATE_FMT[] PROGMEM = "{\"result\":%d}"; +static const char JSON_STATUS_FMT[] PROGMEM = "{\"node_id\":\"%s\"" +#ifdef DEBUG + ",\"configured\":%d" + ",\"crc\":%u" + ",\"fl_n\":%d" + ",\"fl_w\":%d" +#endif + "}"; +static const size_t JSON_BUF_SIZE = 192; + +static const char JSON_SCAN_FIRST_LIST[] PROGMEM = "{\"list\":["; + +static const char MSG_IS_INVALID[] PROGMEM = " is invalid"; +static const char MSG_IS_MISSING[] PROGMEM = " is missing"; + +static const char GZIP[] PROGMEM = "gzip"; +static const char CONTENT_ENCODING[] PROGMEM = "Content-Encoding"; +static const char NOT_FOUND[] PROGMEM = "Not Found"; + +static const char ROUTE_STYLE_CSS[] PROGMEM = "/style.css"; +static const char ROUTE_APP_JS[] PROGMEM = "/app.js"; +static const char ROUTE_MD5_JS[] PROGMEM = "/md5.js"; +static const char ROUTE_FAVICON_ICO[] PROGMEM = "/favicon.ico"; +static const char ROUTE_STATUS[] PROGMEM = "/status"; +static const char ROUTE_SCAN[] PROGMEM = "/scan"; +static const char ROUTE_RESET[] PROGMEM = "/reset"; +// #ifdef DEBUG +static const char ROUTE_HEAP[] PROGMEM = "/heap"; +// #endif +static const char ROUTE_UPDATE[] PROGMEM = "/update"; + +void HttpServer::start() { + server.on(FPSTR(ROUTE_STYLE_CSS), HTTP_GET, [&]() { sendGzip(files::style_css, CONTENT_TYPE_CSS); }); + server.on(FPSTR(ROUTE_APP_JS), HTTP_GET, [&]() { sendGzip(files::app_js, CONTENT_TYPE_JS); }); + server.on(FPSTR(ROUTE_MD5_JS), HTTP_GET, [&]() { sendGzip(files::md5_js, CONTENT_TYPE_JS); }); + server.on(FPSTR(ROUTE_FAVICON_ICO), HTTP_GET, [&]() { sendGzip(files::favicon_ico, CONTENT_TYPE_FAVICON); }); + + server.on("/", HTTP_GET, [&]() { sendGzip(files::index_html, CONTENT_TYPE_HTML); }); + server.on(FPSTR(ROUTE_STATUS), HTTP_GET, [&]() { + char json_buf[JSON_BUF_SIZE]; + auto cfg = config::read(); + + if (!isValid(cfg) || !cfg.flags.node_configured) { + sprintf_P(json_buf, JSON_STATUS_FMT + , CONFIG_NODE_ID +#ifdef DEBUG + , 0 + , cfg.crc + , cfg.flags.node_configured + , cfg.flags.wifi_configured +#endif + ); + } else { + char escaped_node_id[32]; + char *escaped_node_id_res = cfg.escapeHomeId(escaped_node_id, 32); + sprintf_P(json_buf, JSON_STATUS_FMT + , escaped_node_id_res == nullptr ? "?" : escaped_node_id +#ifdef DEBUG + , 1 + , cfg.crc + , cfg.flags.node_configured + , cfg.flags.wifi_configured +#endif + ); + } + server.send(200, FPSTR(CONTENT_TYPE_JSON), json_buf); + }); + server.on(FPSTR(ROUTE_STATUS), HTTP_POST, [&]() { + auto cfg = config::read(); + String s; + + if (!getInputParam("ssid", 32, s)) return; + strncpy(cfg.wifi_ssid, s.c_str(), 32); + PRINTF("saving ssid: %s\n", cfg.wifi_ssid); + + if (!getInputParam("psk", 63, s)) return; + strncpy(cfg.wifi_psk, s.c_str(), 63); + PRINTF("saving psk: %s\n", cfg.wifi_psk); + + if (!getInputParam("hid", 16, s)) return; + strcpy(cfg.node_id, s.c_str()); + PRINTF("saving home id: %s\n", cfg.node_id); + + cfg.flags.node_configured = 1; + cfg.flags.wifi_configured = 1; + + config::write(cfg); + + restartTimer.once(0, restart); + }); + + server.on(FPSTR(ROUTE_RESET), HTTP_POST, [&]() { + config::erase(); + restartTimer.once(1, restart); + }); + + server.on(FPSTR(ROUTE_HEAP), HTTP_GET, [&]() { + server.send(200, FPSTR(CONTENT_TYPE_HTML), String(ESP.getFreeHeap())); + }); + + server.on(FPSTR(ROUTE_SCAN), HTTP_GET, [&]() { + size_t i = 0; + size_t len; + const char* ssid; + bool enough = false; + + bzero(reinterpret_cast<uint8_t*>(scanBuf), scanBufSize); + char* cur = scanBuf; + + strncpy_P(cur, JSON_SCAN_FIRST_LIST, scanBufSize); + cur += 9; + + for (auto& res: *scanResults) { + ssid = res.ssid.c_str(); + len = res.ssid.length(); + + // new item (array with 2 items) + *cur++ = '['; + + // 1. ssid (string) + *cur++ = '"'; + for (size_t j = 0; j < len; j++) { + if (*(ssid+j) == '"') + *cur++ = '\\'; + *cur++ = *(ssid+j); + } + *cur++ = '"'; + *cur++ = ','; + + // 2. rssi (number) + cur += sprintf(cur, "%d", res.rssi); + + // close array + *cur++ = ']'; + + if ((size_t)(cur - scanBuf) >= (size_t) ARRAY_SIZE(scanBuf) - 40) + enough = true; + + if (i < scanResults->size() - 1 || enough) + *cur++ = ','; + + if (enough) + break; + + i++; + } + + *cur++ = ']'; + *cur++ = '}'; + *cur++ = '\0'; + + server.send(200, FPSTR(CONTENT_TYPE_JSON), scanBuf); + }); + + server.on(FPSTR(ROUTE_UPDATE), HTTP_POST, [&]() { + char json_buf[16]; + bool should_reboot = !Update.hasError() && !ota.invalidMd5; + Update.clearError(); + + sprintf_P(json_buf, JSON_UPDATE_FMT, should_reboot ? 1 : 0); + + server.send(200, FPSTR(CONTENT_TYPE_JSON), json_buf); + + if (should_reboot) + restartTimer.once(1, restart); + }, [&]() { + HTTPUpload& upload = server.upload(); + + if (upload.status == UPLOAD_FILE_START) { + ota.clean(); + + String s; + if (!getInputParam("md5", 0, s)) { + ota.invalidMd5 = true; + PRINTLN("http/ota: md5 not found"); + return; + } + + if (!Update.setMD5(s.c_str())) { + ota.invalidMd5 = true; + PRINTLN("http/ota: setMD5() failed"); + return; + } + + Serial.printf("http/ota: starting, filename=%s\n", upload.filename.c_str()); + if (!Update.begin(otaGetMaxUpdateSize())) { +#ifdef DEBUG + Update.printError(Serial); +#endif + } + } else if (upload.status == UPLOAD_FILE_WRITE) { + if (!Update.isRunning()) + return; + + PRINTF("http/ota: writing %ul\n", upload.currentSize); + ota_led(); + + if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { +#ifdef DEBUG + Update.printError(Serial); +#endif + } + } else if (upload.status == UPLOAD_FILE_END) { + if (!Update.isRunning()) + return; + + if (Update.end(true)) { + PRINTF("http/ota: ok, total size %ul\n", upload.totalSize); + } else { +#ifdef DEBUG + Update.printError(Serial); +#endif + } + } + }); + + server.onNotFound([&]() { + server.send(404, FPSTR(CONTENT_TYPE_HTML), NOT_FOUND); + }); + + server.begin(); +} + +void HttpServer::loop() { + server.handleClient(); +} + +void HttpServer::sendGzip(const StaticFile& file, PGM_P content_type) { + server.sendHeader(FPSTR(CONTENT_ENCODING), FPSTR(GZIP)); + server.send_P(200, content_type, (const char*)file.content, file.size); +} + +void HttpServer::sendError(const String& message) { + char buf[32]; + if (snprintf_P(buf, 32, PSTR("error: %s"), message.c_str()) == 32) + buf[31] = '\0'; + server.send(400, FPSTR(CONTENT_TYPE_HTML), buf); +} + +bool HttpServer::getInputParam(const char *field_name, + size_t max_len, + String& dst) { + if (!server.hasArg(field_name)) { + sendError(String(field_name) + String(MSG_IS_MISSING)); + return false; + } + + String field = server.arg(field_name); + if (!field.length() || (max_len != 0 && field.length() > max_len)) { + sendError(String(field_name) + String(MSG_IS_INVALID)); + return false; + } + + dst = field; + return true; +} + +void HttpServer::ota_led() const {} + +} diff --git a/include/pio/libs/http_server/homekit/http_server.h b/include/pio/libs/http_server/homekit/http_server.h new file mode 100644 index 0000000..8725a88 --- /dev/null +++ b/include/pio/libs/http_server/homekit/http_server.h @@ -0,0 +1,62 @@ +#ifndef COMMON_HOMEKIT_HTTP_SERVER_H +#define COMMON_HOMEKIT_HTTP_SERVER_H + +#include <ESP8266WebServer.h> +#include <Ticker.h> +#include <memory> +#include <list> +#include <utility> + +#include <homekit/config.h> +#include <homekit/wifi.h> +#include <homekit/static.h> + +namespace homekit { + +struct OTAStatus { + bool invalidMd5; + + OTAStatus() : invalidMd5(false) {} + + inline void clean() { + invalidMd5 = false; + } +}; + +using files::StaticFile; + +class HttpServer { +private: + ESP8266WebServer server; + Ticker restartTimer; + std::shared_ptr<std::list<wifi::ScanResult>> scanResults; + OTAStatus ota; + + char* scanBuf; + size_t scanBufSize; + + void sendGzip(const StaticFile& file, PGM_P content_type); + void sendError(const String& message); + + bool getInputParam(const char* field_name, size_t max_len, String& dst); + virtual void ota_led() const; + +public: + explicit HttpServer(std::shared_ptr<std::list<wifi::ScanResult>> scanResults) + : server(80) + , scanResults(std::move(scanResults)) + , scanBufSize(512) { + scanBuf = new char[scanBufSize]; + }; + + ~HttpServer() { + delete[] scanBuf; + } + + void start(); + void loop(); +}; + +} + +#endif //COMMON_HOMEKIT_HTTP_SERVER_H diff --git a/include/pio/libs/http_server/library.json b/include/pio/libs/http_server/library.json new file mode 100644 index 0000000..ee2d369 --- /dev/null +++ b/include/pio/libs/http_server/library.json @@ -0,0 +1,8 @@ +{ + "name": "homekit_http_server", + "version": "1.0.3", + "build": { + "flags": "-I../../include" + } +} + diff --git a/include/pio/libs/led/homekit/led.cpp b/include/pio/libs/led/homekit/led.cpp new file mode 100644 index 0000000..ffefb04 --- /dev/null +++ b/include/pio/libs/led/homekit/led.cpp @@ -0,0 +1,27 @@ +#include "led.h" + +namespace homekit::led { + +void Led::on_off(uint16_t delay_ms, bool last_delay) const { + on(); + delay(delay_ms); + + off(); + if (last_delay) + delay(delay_ms); +} + +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); + } +} + + +#ifdef CONFIG_TARGET_NODEMCU +const Led* board_led = new Led(CONFIG_BOARD_LED_GPIO); +#endif +const Led* mcu_led = new Led(CONFIG_MCU_LED_GPIO); + + +} diff --git a/include/pio/libs/led/homekit/led.h b/include/pio/libs/led/homekit/led.h new file mode 100644 index 0000000..775d2eb --- /dev/null +++ b/include/pio/libs/led/homekit/led.h @@ -0,0 +1,33 @@ +#ifndef HOMEKIT_LIB_LED_H +#define HOMEKIT_LIB_LED_H + +#include <Arduino.h> +#include <stdint.h> + +namespace homekit::led { + +class Led { +private: + uint8_t _pin; + +public: + explicit Led(uint8_t pin) : _pin(pin) { + pinMode(_pin, OUTPUT); + off(); + } + + inline void off() const { digitalWrite(_pin, HIGH); } + inline void on() const { digitalWrite(_pin, LOW); } + + void on_off(uint16_t delay_ms, bool last_delay = false) const; + void blink(uint8_t count, uint16_t delay_ms) const; +}; + +#ifdef CONFIG_TARGET_NODEMCU +extern const Led* board_led; +#endif +extern const Led* mcu_led; + +} + +#endif //HOMEKIT_LIB_LED_H diff --git a/include/pio/libs/led/library.json b/include/pio/libs/led/library.json new file mode 100644 index 0000000..6785d42 --- /dev/null +++ b/include/pio/libs/led/library.json @@ -0,0 +1,8 @@ +{ + "name": "homekit_led", + "version": "1.0.8", + "build": { + "flags": "-I../../include" + } +} + diff --git a/include/pio/libs/main/homekit/main.cpp b/include/pio/libs/main/homekit/main.cpp new file mode 100644 index 0000000..816c764 --- /dev/null +++ b/include/pio/libs/main/homekit/main.cpp @@ -0,0 +1,213 @@ +#include "./main.h" +#include <homekit/led.h> +#include <homekit/mqtt/mqtt.h> +#include <homekit/mqtt/module/diagnostics.h> +#include <homekit/mqtt/module/ota.h> + +namespace homekit::main { + +#ifndef CONFIG_TARGET_ESP01 +#ifndef CONFIG_NO_RECOVERY +enum WorkingMode working_mode = WorkingMode::NORMAL; +#endif +#endif + +static const uint16_t recovery_boot_detection_ms = 2000; +static const uint8_t recovery_boot_delay_ms = 100; + +static volatile enum WiFiConnectionState wifi_state = WiFiConnectionState::WAITING; +static void* service = nullptr; +static WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler; +static Ticker wifiTimer; +static mqtt::MqttDiagnosticsModule* mqttDiagModule; +static mqtt::MqttOtaModule* mqttOtaModule; + +#if MQTT_BLINK +static StopWatch blinkStopWatch; +#endif + +#ifndef CONFIG_TARGET_ESP01 +#ifndef CONFIG_NO_RECOVERY +static DNSServer* dnsServer = nullptr; +#endif +#endif + +static void onWifiConnected(const WiFiEventStationModeGotIP& event); +static void onWifiDisconnected(const WiFiEventStationModeDisconnected& event); + +static void wifiConnect() { + const char *ssid, *psk, *hostname; + auto cfg = config::read(); + wifi::getConfig(cfg, &ssid, &psk, &hostname); + + PRINTF("Wi-Fi STA creds: ssid=%s, psk=%s, hostname=%s\n", ssid, psk, hostname); + + wifi_state = WiFiConnectionState::WAITING; + + WiFi.mode(WIFI_STA); + WiFi.hostname(hostname); + WiFi.begin(ssid, psk); + + PRINT("connecting to wifi.."); +} + +#ifndef CONFIG_TARGET_ESP01 +#ifndef CONFIG_NO_RECOVERY +static void wifiHotspot() { + led::mcu_led->on(); + + auto scanResults = wifi::scan(); + + WiFi.mode(WIFI_AP); + WiFi.softAP(wifi::AP_SSID); + + dnsServer = new DNSServer(); + dnsServer->start(53, "*", WiFi.softAPIP()); + + service = new HttpServer(scanResults); + ((HttpServer*)service)->start(); +} + +static void waitForRecoveryPress() { + pinMode(CONFIG_FLASH_GPIO, INPUT_PULLUP); + for (uint16_t i = 0; i < recovery_boot_detection_ms; i += recovery_boot_delay_ms) { + delay(recovery_boot_delay_ms); + if (digitalRead(CONFIG_FLASH_GPIO) == LOW) { + working_mode = WorkingMode::RECOVERY; + break; + } + } +} +#endif +#endif + + +void setup() { + WiFi.disconnect(); +#ifndef CONFIG_NO_RECOVERY +#ifndef CONFIG_TARGET_ESP01 + homekit::main::waitForRecoveryPress(); +#endif +#endif + +#ifdef DEBUG + Serial.begin(115200); +#endif + + auto cfg = config::read(); + if (config::isDirty(cfg)) { + PRINTLN("config is dirty, erasing..."); + config::erase(cfg); +#ifdef CONFIG_TARGET_NODEMCU + led::board_led->blink(10, 50); +#else + led::mcu_led->blink(10, 50); +#endif + } + +#ifndef CONFIG_TARGET_ESP01 +#ifndef CONFIG_NO_RECOVERY + switch (working_mode) { + case WorkingMode::RECOVERY: + wifiHotspot(); + break; + + case WorkingMode::NORMAL: +#endif +#endif + wifiConnectHandler = WiFi.onStationModeGotIP(onWifiConnected); + wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWifiDisconnected); + wifiConnect(); +#ifndef CONFIG_NO_RECOVERY +#ifndef CONFIG_TARGET_ESP01 + break; + } +#endif +#endif +} + +void loop(LoopConfig* config) { +#ifndef CONFIG_NO_RECOVERY +#ifndef CONFIG_TARGET_ESP01 + if (working_mode == WorkingMode::NORMAL) { +#endif +#endif + if (wifi_state == WiFiConnectionState::WAITING) { + PRINT("."); + led::mcu_led->blink(2, 50); + delay(1000); + return; + } + + if (wifi_state == WiFiConnectionState::JUST_CONNECTED) { +#ifdef CONFIG_TARGET_NODEMCU + led::board_led->blink(3, 300); +#else + led::mcu_led->blink(3, 300); +#endif + wifi_state = WiFiConnectionState::CONNECTED; + + if (service == nullptr) { + service = new mqtt::Mqtt(); + mqttDiagModule = new mqtt::MqttDiagnosticsModule(); + mqttOtaModule = new mqtt::MqttOtaModule(); + + ((mqtt::Mqtt*)service)->addModule(mqttDiagModule); + ((mqtt::Mqtt*)service)->addModule(mqttOtaModule); + + if (config != nullptr) + config->onMqttCreated(*(mqtt::Mqtt*)service); + } + + ((mqtt::Mqtt*)service)->connect(); +#if MQTT_BLINK + blinkStopWatch.save(); +#endif + } + + auto mqtt = (mqtt::Mqtt*)service; + if (static_cast<int>(wifi_state) >= 1 && mqtt != nullptr) { + mqtt->loop(); + + if (mqttOtaModule != nullptr && mqttOtaModule->isReadyToRestart()) { + mqtt->disconnect(); + } + +#if MQTT_BLINK + // periodically blink board led + if (blinkStopWatch.elapsed(5000)) { +#ifdef CONFIG_TARGET_NODEMCU + board_led->blink(1, 10); +#endif + blinkStopWatch.save(); + } +#endif + } +#ifndef CONFIG_NO_RECOVERY +#ifndef CONFIG_TARGET_ESP01 + } else { + if (dnsServer != nullptr) + dnsServer->processNextRequest(); + + auto httpServer = (HttpServer*)service; + if (httpServer != nullptr) + httpServer->loop(); + } +#endif +#endif +} + +static void onWifiConnected(const WiFiEventStationModeGotIP& event) { + PRINTF("connected (%s)\n", WiFi.localIP().toString().c_str()); + wifi_state = WiFiConnectionState::JUST_CONNECTED; +} + +static void onWifiDisconnected(const WiFiEventStationModeDisconnected& event) { + PRINTLN("disconnected from wi-fi"); + wifi_state = WiFiConnectionState::WAITING; + if (service != nullptr) + ((mqtt::Mqtt*)service)->disconnect(); + wifiTimer.once(2, wifiConnect); +} + +} diff --git a/include/pio/libs/main/homekit/main.h b/include/pio/libs/main/homekit/main.h new file mode 100644 index 0000000..78a0695 --- /dev/null +++ b/include/pio/libs/main/homekit/main.h @@ -0,0 +1,52 @@ +#ifndef HOMEKIT_LIB_MAIN_H +#define HOMEKIT_LIB_MAIN_H + +#include <Arduino.h> +#include <ESP8266WiFi.h> +#include <DNSServer.h> +#include <Ticker.h> +#include <Wire.h> + +#include <homekit/config.h> +#include <homekit/logging.h> +#ifndef CONFIG_TARGET_ESP01 +#ifndef CONFIG_NO_RECOVERY +#include <homekit/http_server.h> +#endif +#endif +#include <homekit/wifi.h> +#include <homekit/mqtt/mqtt.h> + +#include <functional> + +namespace homekit::main { + +#ifndef CONFIG_TARGET_ESP01 +#ifndef CONFIG_NO_RECOVERY +enum class WorkingMode { + RECOVERY, // AP mode, http server with configuration + NORMAL, // MQTT client +}; + +extern enum WorkingMode working_mode; +#endif +#endif + +enum class WiFiConnectionState { + WAITING = 0, + JUST_CONNECTED = 1, + CONNECTED = 2 +}; + + +struct LoopConfig { + std::function<void(mqtt::Mqtt&)> onMqttCreated; +}; + + +void setup(); +void loop(LoopConfig* config); + +} + +#endif //HOMEKIT_LIB_MAIN_H diff --git a/include/pio/libs/main/library.json b/include/pio/libs/main/library.json new file mode 100644 index 0000000..c5586d8 --- /dev/null +++ b/include/pio/libs/main/library.json @@ -0,0 +1,12 @@ +{ + "name": "homekit_main", + "version": "1.0.11", + "build": { + "flags": "-I../../include" + }, + "dependencies": { + "homekit_mqtt_module_ota": "file://../../include/pio/libs/mqtt_module_ota", + "homekit_mqtt_module_diagnostics": "file://../../include/pio/libs/mqtt_module_diagnostics" + } +} + diff --git a/include/pio/libs/mqtt/homekit/mqtt/module.cpp b/include/pio/libs/mqtt/homekit/mqtt/module.cpp new file mode 100644 index 0000000..0ac7637 --- /dev/null +++ b/include/pio/libs/mqtt/homekit/mqtt/module.cpp @@ -0,0 +1,26 @@ +#include "./module.h" +#include <homekit/logging.h> + +namespace homekit::mqtt { + +bool MqttModule::tickElapsed() { + if (!tickSw.elapsed(tickInterval*1000)) + return false; + + tickSw.save(); + return true; +} + +void MqttModule::handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId, const uint8_t* payload, size_t length, + size_t index, size_t total) { + if (length != total) + PRINTLN("mqtt: received partial message, not supported"); + + // TODO +} + +void MqttModule::handleOnPublish(uint16_t packetId) {} + +void MqttModule::onDisconnect(Mqtt& mqtt, espMqttClientTypes::DisconnectReason reason) {} + +} diff --git a/include/pio/libs/mqtt/homekit/mqtt/module.h b/include/pio/libs/mqtt/homekit/mqtt/module.h new file mode 100644 index 0000000..0a328f3 --- /dev/null +++ b/include/pio/libs/mqtt/homekit/mqtt/module.h @@ -0,0 +1,56 @@ +#ifndef HOMEKIT_LIB_MQTT_MODULE_H +#define HOMEKIT_LIB_MQTT_MODULE_H + +#include "./mqtt.h" +#include "./payload.h" +#include <homekit/stopwatch.h> + + +namespace homekit::mqtt { + +class Mqtt; + +class MqttModule { +protected: + bool initialized; + StopWatch tickSw; + short tickInterval; + + bool receiveOnPublish; + bool receiveOnDisconnect; + + bool tickElapsed(); + +public: + MqttModule(short _tickInterval, bool _receiveOnPublish = false, bool _receiveOnDisconnect = false) + : initialized(false) + , tickInterval(_tickInterval) + , receiveOnPublish(_receiveOnPublish) + , receiveOnDisconnect(_receiveOnDisconnect) {} + + virtual void tick(Mqtt& mqtt) = 0; + + virtual void onConnect(Mqtt& mqtt) = 0; + virtual void onDisconnect(Mqtt& mqtt, espMqttClientTypes::DisconnectReason reason); + + virtual void handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total); + virtual void handleOnPublish(uint16_t packetId); + + inline void setInitialized() { + initialized = true; + } + + inline void unsetInitialized() { + initialized = false; + } + + inline short getTickInterval() const { + return tickInterval; + } + + friend class Mqtt; +}; + +} + +#endif //HOMEKIT_LIB_MQTT_MODULE_H diff --git a/include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp b/include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp new file mode 100644 index 0000000..aa769a5 --- /dev/null +++ b/include/pio/libs/mqtt/homekit/mqtt/mqtt.cpp @@ -0,0 +1,162 @@ +#include "./mqtt.h" + +#include <homekit/config.h> +#include <homekit/wifi.h> +#include <homekit/logging.h> + +namespace homekit::mqtt { + +const uint8_t MQTT_CA_FINGERPRINT[] = { \ + 0x0e, 0xb6, 0x3a, 0x02, 0x1f, \ + 0x4e, 0x1e, 0xe1, 0x6a, 0x67, \ + 0x62, 0xec, 0x64, 0xd4, 0x84, \ + 0x8a, 0xb0, 0xc9, 0x9c, 0xbb \ +};; +const char MQTT_SERVER[] = "mqtt.solarmon.ru"; +const uint16_t MQTT_PORT = 8883; +const char MQTT_USERNAME[] = CONFIG_MQTT_USERNAME; +const char MQTT_PASSWORD[] = CONFIG_MQTT_PASSWORD; +const char MQTT_CLIENT_ID[] = CONFIG_MQTT_CLIENT_ID; +const char MQTT_SECRET[CONFIG_NODE_SECRET_SIZE+1] = CONFIG_NODE_SECRET; + +static const uint16_t MQTT_KEEPALIVE = 30; + +using namespace espMqttClientTypes; + +Mqtt::Mqtt() { + auto cfg = config::read(); + nodeId = String(cfg.flags.node_configured ? cfg.node_id : wifi::NODE_ID); + + randomSeed(micros()); + + client.onConnect([&](bool sessionPresent) { + PRINTLN("mqtt: connected"); + + for (auto* module: modules) { + if (!module->initialized) { + module->onConnect(*this); + module->setInitialized(); + } + } + + connected = true; + }); + + 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 + + for (auto* module: modules) { + module->onDisconnect(*this, reason); + module->unsetInitialized(); + } + + 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); + + const char *ptr = topic + nodeId.length() + 4; + String relevantTopic(ptr); + + auto it = moduleSubscriptions.find(relevantTopic); + if (it != moduleSubscriptions.end()) { + auto module = it->second; + module->handlePayload(*this, relevantTopic, properties.packetId, payload, len, index, total); + } else { + PRINTF("error: module subscription for topic %s not found\n", relevantTopic.c_str()); + } + }); + + client.onPublish([&](uint16_t packetId) { + PRINTF("mqtt: publish ack, packet_id=%d\n", packetId); + + for (auto* module: modules) { + if (module->receiveOnPublish) { + module->handleOnPublish(packetId); + } + } + }); + + client.setServer(MQTT_SERVER, MQTT_PORT); + client.setClientId(MQTT_CLIENT_ID); + client.setCredentials(MQTT_USERNAME, MQTT_PASSWORD); + client.setCleanSession(true); + client.setFingerprint(MQTT_CA_FINGERPRINT); + client.setKeepAlive(MQTT_KEEPALIVE); +} + +void Mqtt::connect() { + reconnect(); +} + +void Mqtt::reconnect() { + if (client.connected()) { + PRINTLN("warning: already connected"); + return; + } + client.connect(); +} + +void Mqtt::disconnect() { + // TODO test how this works??? + reconnectTimer.detach(); + client.disconnect(); +} + +void Mqtt::loop() { + client.loop(); + for (auto& module: modules) { + if (module->getTickInterval() != 0) + module->tick(*this); + } +} + +uint16_t Mqtt::publish(const String& topic, uint8_t* payload, size_t length) { + String fullTopic = "hk/" + nodeId + "/" + topic; + return client.publish(fullTopic.c_str(), 1, false, payload, length); +} + +uint16_t Mqtt::subscribe(const String& topic, uint8_t qos) { + String fullTopic = "hk/" + nodeId + "/" + 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::addModule(MqttModule* module) { + modules.emplace_back(module); + if (connected) { + module->onConnect(*this); + module->setInitialized(); + } +} + +void Mqtt::subscribeModule(String& topic, MqttModule* module, uint8_t qos) { + moduleSubscriptions[topic] = module; + subscribe(topic, qos); +} + +} diff --git a/include/pio/libs/mqtt/homekit/mqtt/mqtt.h b/include/pio/libs/mqtt/homekit/mqtt/mqtt.h new file mode 100644 index 0000000..9e0c2be --- /dev/null +++ b/include/pio/libs/mqtt/homekit/mqtt/mqtt.h @@ -0,0 +1,48 @@ +#ifndef HOMEKIT_LIB_MQTT_H +#define HOMEKIT_LIB_MQTT_H + +#include <vector> +#include <map> +#include <cstdint> +#include <espMqttClient.h> +#include <Ticker.h> +#include "./module.h" + +namespace homekit::mqtt { + +extern const uint8_t MQTT_CA_FINGERPRINT[]; +extern const char MQTT_SERVER[]; +extern const uint16_t MQTT_PORT; +extern const char MQTT_USERNAME[]; +extern const char MQTT_PASSWORD[]; +extern const char MQTT_CLIENT_ID[]; +extern const char MQTT_SECRET[CONFIG_NODE_SECRET_SIZE+1]; + +class MqttModule; + +class Mqtt { +private: + String nodeId; + WiFiClientSecure httpsSecureClient; + espMqttClientSecure client; + Ticker reconnectTimer; + std::vector<MqttModule*> modules; + std::map<String, MqttModule*> moduleSubscriptions; + bool connected; + + uint16_t subscribe(const String& topic, uint8_t qos = 0); + +public: + Mqtt(); + void connect(); + void disconnect(); + void reconnect(); + void loop(); + void addModule(MqttModule* module); + void subscribeModule(String& topic, MqttModule* module, uint8_t qos = 0); + uint16_t publish(const String& topic, uint8_t* payload, size_t length); +}; + +} + +#endif //HOMEKIT_LIB_MQTT_H diff --git a/include/pio/libs/mqtt/homekit/mqtt/payload.h b/include/pio/libs/mqtt/homekit/mqtt/payload.h new file mode 100644 index 0000000..3e0fe0c --- /dev/null +++ b/include/pio/libs/mqtt/homekit/mqtt/payload.h @@ -0,0 +1,15 @@ +#ifndef HOMEKIT_MQTT_PAYLOAD_H +#define HOMEKIT_MQTT_PAYLOAD_H + +#include <unistd.h> + +namespace homekit::mqtt { + +struct MqttPayload { + virtual ~MqttPayload() = default; + virtual size_t size() const = 0; +}; + +} + +#endif
\ No newline at end of file diff --git a/include/pio/libs/mqtt/library.json b/include/pio/libs/mqtt/library.json new file mode 100644 index 0000000..f3f2504 --- /dev/null +++ b/include/pio/libs/mqtt/library.json @@ -0,0 +1,7 @@ +{ + "name": "homekit_mqtt", + "version": "1.0.11", + "build": { + "flags": "-I../../include" + } +} diff --git a/include/pio/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.cpp b/include/pio/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.cpp new file mode 100644 index 0000000..e0f797e --- /dev/null +++ b/include/pio/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.cpp @@ -0,0 +1,56 @@ +#include "./diagnostics.h" +#include <homekit/wifi.h> +#include <ESP8266WiFi.h> + +namespace homekit::mqtt { + +static const char TOPIC_DIAGNOSTICS[] = "diag"; +static const char TOPIC_INITIAL_DIAGNOSTICS[] = "d1ag"; + +void MqttDiagnosticsModule::onConnect(Mqtt &mqtt) { + sendDiagnostics(mqtt); +} + +void MqttDiagnosticsModule::onDisconnect(Mqtt &mqtt, espMqttClientTypes::DisconnectReason reason) { + initialSent = false; +} + +void MqttDiagnosticsModule::tick(Mqtt& mqtt) { + if (!tickElapsed()) + return; + sendDiagnostics(mqtt); +} + +void MqttDiagnosticsModule::sendDiagnostics(Mqtt& mqtt) { + auto cfg = config::read(); + + if (!initialSent) { + MqttInitialDiagnosticsPayload stat{ + .ip = wifi::getIPAsInteger(), + .fw_version = CONFIG_FW_VERSION, + .rssi = wifi::getRSSI(), + .free_heap = ESP.getFreeHeap(), + .flags = DiagnosticsFlags{ + .state = 1, + .config_changed_value_present = 1, + .config_changed = static_cast<uint8_t>(cfg.flags.node_configured || + cfg.flags.wifi_configured ? 1 : 0) + } + }; + mqtt.publish(TOPIC_INITIAL_DIAGNOSTICS, reinterpret_cast<uint8_t*>(&stat), sizeof(stat)); + initialSent = true; + } else { + MqttDiagnosticsPayload stat{ + .rssi = wifi::getRSSI(), + .free_heap = ESP.getFreeHeap(), + .flags = DiagnosticsFlags{ + .state = 1, + .config_changed_value_present = 0, + .config_changed = 0 + } + }; + mqtt.publish(TOPIC_DIAGNOSTICS, reinterpret_cast<uint8_t*>(&stat), sizeof(stat)); + } +} + +} diff --git a/include/pio/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.h b/include/pio/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.h new file mode 100644 index 0000000..bb7a81a --- /dev/null +++ b/include/pio/libs/mqtt_module_diagnostics/homekit/mqtt/module/diagnostics.h @@ -0,0 +1,49 @@ +#ifndef HOMEKIT_LIB_MQTT_MODULE_DIAGNOSTICS_H +#define HOMEKIT_LIB_MQTT_MODULE_DIAGNOSTICS_H + +#include <stdint.h> +#include <homekit/mqtt/module.h> + +namespace homekit::mqtt { + +struct DiagnosticsFlags { + uint8_t state: 1; + uint8_t config_changed_value_present: 1; + uint8_t config_changed: 1; + uint8_t reserved: 5; +} __attribute__((packed)); + +struct MqttInitialDiagnosticsPayload { + uint32_t ip; + uint8_t fw_version; + int8_t rssi; + uint32_t free_heap; + DiagnosticsFlags flags; +} __attribute__((packed)); + +struct MqttDiagnosticsPayload { + int8_t rssi; + uint32_t free_heap; + DiagnosticsFlags flags; +} __attribute__((packed)); + + +class MqttDiagnosticsModule: public MqttModule { +private: + bool initialSent; + + void sendDiagnostics(Mqtt& mqtt); + +public: + MqttDiagnosticsModule() + : MqttModule(30) + , initialSent(false) {} + + void onConnect(Mqtt& mqtt) override; + void onDisconnect(Mqtt& mqtt, espMqttClientTypes::DisconnectReason reason) override; + void tick(Mqtt& mqtt) override; +}; + +} + +#endif //HOMEKIT_LIB_MQTT_MODULE_DIAGNOSTICS_H diff --git a/include/pio/libs/mqtt_module_diagnostics/library.json b/include/pio/libs/mqtt_module_diagnostics/library.json new file mode 100644 index 0000000..70acb79 --- /dev/null +++ b/include/pio/libs/mqtt_module_diagnostics/library.json @@ -0,0 +1,10 @@ +{ + "name": "homekit_mqtt_module_diagnostics", + "version": "1.0.3", + "build": { + "flags": "-I../../include" + }, + "dependencies": { + "homekit_mqtt": "file://../../include/pio/libs/mqtt" + } +} diff --git a/include/pio/libs/mqtt_module_ota/homekit/mqtt/module/ota.cpp b/include/pio/libs/mqtt_module_ota/homekit/mqtt/module/ota.cpp new file mode 100644 index 0000000..4e976cd --- /dev/null +++ b/include/pio/libs/mqtt_module_ota/homekit/mqtt/module/ota.cpp @@ -0,0 +1,160 @@ +#include "./ota.h" +#include <homekit/logging.h> +#include <homekit/util.h> +#include <homekit/led.h> + +namespace homekit::mqtt { + +using homekit::led::mcu_led; + +#define MD5_SIZE 16 + +static const char TOPIC_OTA[] = "ota"; +static const char TOPIC_OTA_RESPONSE[] = "otares"; + +void MqttOtaModule::onConnect(Mqtt& mqtt) { + String topic(TOPIC_OTA); + mqtt.subscribeModule(topic, this); +} + +void MqttOtaModule::tick(Mqtt& mqtt) { + if (!tickElapsed()) + return; +} + +void MqttOtaModule::handlePayload(Mqtt& mqtt, String& topic, 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 < CONFIG_NODE_SECRET_SIZE + MD5_SIZE) { + PRINTLN("mqtt/ota: failed to check secret, first packet size is too small"); + return; + } + + if (memcmp((const char*)payload, CONFIG_NODE_SECRET, CONFIG_NODE_SECRET_SIZE) != 0) { + PRINTLN("mqtt/ota: invalid secret"); + return; + } + + PRINTF("mqtt/ota: starting update, total=%ul\n", total-CONFIG_NODE_SECRET_SIZE); + for (int i = 0; i < MD5_SIZE; i++) { + md5Ptr += sprintf(md5Ptr, "%02x", *((unsigned char*)(payload+CONFIG_NODE_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 - CONFIG_NODE_SECRET_SIZE - MD5_SIZE)) { + ota.clean(); +#ifdef DEBUG + Update.printError(Serial); +#endif + sendResponse(mqtt, OtaResult::UPDATE_ERROR, Update.getError()); + } + + ota.written = Update.write(const_cast<uint8_t*>(payload)+CONFIG_NODE_SECRET_SIZE + MD5_SIZE, length-CONFIG_NODE_SECRET_SIZE - MD5_SIZE); + ota.written += CONFIG_NODE_SECRET_SIZE + MD5_SIZE; + + mcu_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(); + sendResponse(mqtt, OtaResult::WRITE_ERROR); + return; + } + ota.written += length; + + mcu_led->blink(1, 1); + PRINTF("mqtt/ota: updating %u/%u\n", + ota.written - CONFIG_NODE_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 = sendResponse(mqtt, 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(); + + sendResponse(mqtt, OtaResult::UPDATE_ERROR, Update.getError()); + } + } +} + +uint16_t MqttOtaModule::sendResponse(Mqtt& mqtt, OtaResult status, uint8_t error_code) const { + MqttOtaResponsePayload resp{ + .status = status, + .error_code = error_code + }; + return mqtt.publish(TOPIC_OTA_RESPONSE, reinterpret_cast<uint8_t*>(&resp), sizeof(resp)); +} + +void MqttOtaModule::onDisconnect(Mqtt& mqtt, espMqttClientTypes::DisconnectReason reason) { + if (ota.readyToRestart) { + restartTimer.once(1, restart); + } else if (ota.started()) { + PRINTLN("mqtt: update was in progress, canceling.."); + ota.clean(); + Update.end(); + Update.clearError(); + } +} + +void MqttOtaModule::handleOnPublish(uint16_t packetId) { + if (ota.finished && packetId == ota.publishResultPacketId) { + ota.readyToRestart = true; + } +} + +} diff --git a/include/pio/libs/mqtt_module_ota/homekit/mqtt/module/ota.h b/include/pio/libs/mqtt_module_ota/homekit/mqtt/module/ota.h new file mode 100644 index 0000000..df4f7ce --- /dev/null +++ b/include/pio/libs/mqtt_module_ota/homekit/mqtt/module/ota.h @@ -0,0 +1,75 @@ +#ifndef HOMEKIT_LIB_MQTT_MODULE_OTA_H +#define HOMEKIT_LIB_MQTT_MODULE_OTA_H + +#include <stdint.h> +#include <Ticker.h> +#include <homekit/mqtt/module.h> + +namespace homekit::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; + } +}; + +struct MqttOtaResponsePayload { + OtaResult status; + uint8_t error_code; +} __attribute__((packed)); + + +class MqttOtaModule: public MqttModule { +private: + OtaStatus ota; + Ticker restartTimer; + + uint16_t sendResponse(Mqtt& mqtt, OtaResult status, uint8_t error_code = 0) const; + +public: + MqttOtaModule() : MqttModule(0, true, true) {} + + void onConnect(Mqtt& mqtt) override; + void onDisconnect(Mqtt& mqtt, espMqttClientTypes::DisconnectReason reason) override; + + void tick(Mqtt& mqtt) override; + + void handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total) override; + void handleOnPublish(uint16_t packetId) override; + + inline bool isReadyToRestart() const { + return ota.readyToRestart; + } +}; + +} + +#endif //HOMEKIT_LIB_MQTT_MODULE_OTA_H diff --git a/include/pio/libs/mqtt_module_ota/library.json b/include/pio/libs/mqtt_module_ota/library.json new file mode 100644 index 0000000..1577fed --- /dev/null +++ b/include/pio/libs/mqtt_module_ota/library.json @@ -0,0 +1,11 @@ +{ + "name": "homekit_mqtt_module_ota", + "version": "1.0.6", + "build": { + "flags": "-I../../include" + }, + "dependencies": { + "homekit_led": "file://../../include/pio/libs/led", + "homekit_mqtt": "file://../../include/pio/libs/mqtt" + } +} diff --git a/include/pio/libs/mqtt_module_relay/homekit/mqtt/module/relay.cpp b/include/pio/libs/mqtt_module_relay/homekit/mqtt/module/relay.cpp new file mode 100644 index 0000000..90c57f9 --- /dev/null +++ b/include/pio/libs/mqtt_module_relay/homekit/mqtt/module/relay.cpp @@ -0,0 +1,58 @@ +#include "./relay.h" +#include <homekit/relay.h> +#include <homekit/logging.h> + +namespace homekit::mqtt { + +static const char TOPIC_RELAY_SWITCH[] = "relay/switch"; +static const char TOPIC_RELAY_STATUS[] = "relay/status"; + +void MqttRelayModule::onConnect(Mqtt &mqtt) { + String topic(TOPIC_RELAY_SWITCH); + mqtt.subscribeModule(topic, this, 1); +} + +void MqttRelayModule::onDisconnect(Mqtt &mqtt, espMqttClientTypes::DisconnectReason reason) { +#ifdef CONFIG_RELAY_OFF_ON_DISCONNECT + if (relay::state()) { + relay::off(); + } +#endif +} + +void MqttRelayModule::tick(homekit::mqtt::Mqtt& mqtt) {} + +void MqttRelayModule::handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total) { + if (topic != TOPIC_RELAY_SWITCH) + return; + + if (length != sizeof(MqttRelaySwitchPayload)) { + PRINTF("error: size of payload (%ul) does not match expected (%ul)\n", + length, sizeof(MqttRelaySwitchPayload)); + return; + } + + auto pd = reinterpret_cast<const struct MqttRelaySwitchPayload*>(payload); + if (strncmp(pd->secret, MQTT_SECRET, sizeof(pd->secret)) != 0) { + PRINTLN("error: invalid secret"); + return; + } + + MqttRelayStatusPayload resp{}; + + if (pd->state == 1) { + PRINTLN("mqtt: turning relay on"); + relay::on(); + } else if (pd->state == 0) { + PRINTLN("mqtt: turning relay off"); + relay::off(); + } else { + PRINTLN("error: unexpected state value"); + } + + resp.opened = relay::state(); + mqtt.publish(TOPIC_RELAY_STATUS, reinterpret_cast<uint8_t*>(&resp), sizeof(resp)); +} + +} + diff --git a/include/pio/libs/mqtt_module_relay/homekit/mqtt/module/relay.h b/include/pio/libs/mqtt_module_relay/homekit/mqtt/module/relay.h new file mode 100644 index 0000000..e245527 --- /dev/null +++ b/include/pio/libs/mqtt_module_relay/homekit/mqtt/module/relay.h @@ -0,0 +1,29 @@ +#ifndef HOMEKIT_LIB_MQTT_MODULE_RELAY_H +#define HOMEKIT_LIB_MQTT_MODULE_RELAY_H + +#include <homekit/mqtt/module.h> + +namespace homekit::mqtt { + +struct MqttRelaySwitchPayload { + char secret[12]; + uint8_t state; +} __attribute__((packed)); + +struct MqttRelayStatusPayload { + uint8_t opened; +} __attribute__((packed)); + +class MqttRelayModule : public MqttModule { +public: + MqttRelayModule() : MqttModule(0) {} + void onConnect(Mqtt& mqtt) override; + void onDisconnect(Mqtt& mqtt, espMqttClientTypes::DisconnectReason reason) override; + void tick(Mqtt& mqtt) override; + void handlePayload(Mqtt& mqtt, String& topic, uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total) override; +}; + +} + +#endif //HOMEKIT_LIB_MQTT_MODULE_RELAY_H + diff --git a/include/pio/libs/mqtt_module_relay/library.json b/include/pio/libs/mqtt_module_relay/library.json new file mode 100644 index 0000000..18a510c --- /dev/null +++ b/include/pio/libs/mqtt_module_relay/library.json @@ -0,0 +1,11 @@ +{ + "name": "homekit_mqtt_module_relay", + "version": "1.0.6", + "build": { + "flags": "-I../../include" + }, + "dependencies": { + "homekit_mqtt": "file://../../include/pio/libs/mqtt", + "homekit_relay": "file://../../include/pio/libs/relay" + } +} diff --git a/include/pio/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.cpp b/include/pio/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.cpp new file mode 100644 index 0000000..409f38f --- /dev/null +++ b/include/pio/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.cpp @@ -0,0 +1,23 @@ +#include "temphum.h" + +namespace homekit::mqtt { + +static const char TOPIC_TEMPHUM_DATA[] = "temphum/data"; + +void MqttTemphumModule::onConnect(Mqtt &mqtt) {} + +void MqttTemphumModule::tick(homekit::mqtt::Mqtt& mqtt) { + if (!tickElapsed()) + return; + + temphum::SensorData sd = sensor->read(); + MqttTemphumPayload payload { + .temp = sd.temp, + .rh = sd.rh, + .error = sd.error + }; + + mqtt.publish(TOPIC_TEMPHUM_DATA, reinterpret_cast<uint8_t*>(&payload), sizeof(payload)); +} + +} diff --git a/include/pio/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.h b/include/pio/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.h new file mode 100644 index 0000000..7b28afc --- /dev/null +++ b/include/pio/libs/mqtt_module_temphum/homekit/mqtt/module/temphum.h @@ -0,0 +1,28 @@ +#ifndef HOMEKIT_LIB_MQTT_MODULE_TEMPHUM_H +#define HOMEKIT_LIB_MQTT_MODULE_TEMPHUM_H + +#include <homekit/mqtt/module.h> +#include <homekit/temphum.h> + +namespace homekit::mqtt { + +struct MqttTemphumPayload { + double temp = 0; + double rh = 0; + uint8_t error = 0; +} __attribute__((packed)); + + +class MqttTemphumModule : public MqttModule { +private: + temphum::Sensor* sensor; + +public: + MqttTemphumModule(temphum::Sensor* _sensor) : MqttModule(10), sensor(_sensor) {} + void onConnect(Mqtt& mqtt) override; + void tick(Mqtt& mqtt) override; +}; + +} + +#endif //HOMEKIT_LIB_MQTT_MODULE_TEMPHUM_H diff --git a/include/pio/libs/mqtt_module_temphum/library.json b/include/pio/libs/mqtt_module_temphum/library.json new file mode 100644 index 0000000..c7ee7af --- /dev/null +++ b/include/pio/libs/mqtt_module_temphum/library.json @@ -0,0 +1,11 @@ +{ + "name": "homekit_mqtt_module_temphum", + "version": "1.0.10", + "build": { + "flags": "-I../../include" + }, + "dependencies": { + "homekit_mqtt": "file://../../include/pio/libs/mqtt", + "homekit_temphum": "file://../../include/pio/libs/temphum" + } +} diff --git a/include/pio/libs/relay/homekit/relay.cpp b/include/pio/libs/relay/homekit/relay.cpp new file mode 100644 index 0000000..b00a7a2 --- /dev/null +++ b/include/pio/libs/relay/homekit/relay.cpp @@ -0,0 +1,22 @@ +#include <Arduino.h> +#include "./relay.h" + +namespace homekit::relay { + +void init() { + pinMode(CONFIG_RELAY_GPIO, OUTPUT); +} + +bool state() { + return digitalRead(CONFIG_RELAY_GPIO) == HIGH; +} + +void on() { + digitalWrite(CONFIG_RELAY_GPIO, HIGH); +} + +void off() { + digitalWrite(CONFIG_RELAY_GPIO, LOW); +} + +} diff --git a/include/pio/libs/relay/homekit/relay.h b/include/pio/libs/relay/homekit/relay.h new file mode 100644 index 0000000..288cc05 --- /dev/null +++ b/include/pio/libs/relay/homekit/relay.h @@ -0,0 +1,13 @@ +#ifndef HOMEKIT_LIB_RELAY_H +#define HOMEKIT_LIB_RELAY_H + +namespace homekit::relay { + +void init(); +bool state(); +void on(); +void off(); + +} + +#endif //HOMEKIT_LIB_RELAY_H diff --git a/include/pio/libs/relay/library.json b/include/pio/libs/relay/library.json new file mode 100644 index 0000000..e878248 --- /dev/null +++ b/include/pio/libs/relay/library.json @@ -0,0 +1,8 @@ +{ + "name": "homekit_relay", + "version": "1.0.0", + "build": { + "flags": "-I../../include" + } +} + diff --git a/include/pio/libs/static/homekit/static.cpp b/include/pio/libs/static/homekit/static.cpp new file mode 100644 index 0000000..366a09f --- /dev/null +++ b/include/pio/libs/static/homekit/static.cpp @@ -0,0 +1,450 @@ +/** + * This file is autogenerated with make_static.sh script + */ + +#include "static.h" + +namespace homekit::files { + +static const uint8_t index_html_content[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x9d, 0x56, 0x4d, 0x6f, 0xdb, 0x38, + 0x10, 0xbd, 0xe7, 0x57, 0xb0, 0x3c, 0x14, 0x09, 0x10, 0x4b, 0x9b, 0x14, 0xcd, 0x16, 0xad, 0x24, + 0xa0, 0xd8, 0x76, 0xb1, 0x05, 0x7a, 0x08, 0x6a, 0x14, 0x0b, 0xec, 0xc5, 0xa0, 0xa8, 0x91, 0xc5, + 0x9a, 0x22, 0x59, 0x71, 0x24, 0xc7, 0xfd, 0xf5, 0x1d, 0x52, 0x92, 0x3f, 0xb2, 0x46, 0xfa, 0x71, + 0xb1, 0x34, 0xc3, 0x99, 0x37, 0x6f, 0x1e, 0x87, 0xa2, 0xb3, 0x67, 0x95, 0x95, 0xb8, 0x73, 0xc0, + 0x1a, 0x6c, 0x75, 0x71, 0x91, 0x85, 0x07, 0xd3, 0xc2, 0xac, 0x73, 0x0e, 0x86, 0x07, 0x07, 0x88, + 0x8a, 0x1e, 0x2d, 0xa0, 0xa0, 0x18, 0x74, 0x0b, 0xf8, 0xda, 0xab, 0x21, 0xe7, 0xd2, 0x1a, 0x04, + 0x83, 0x8b, 0x90, 0xcc, 0xd9, 0x64, 0xe5, 0x1c, 0xe1, 0x01, 0xd3, 0x00, 0xf2, 0x86, 0xc9, 0x46, + 0x74, 0x1e, 0x30, 0xef, 0xb1, 0x5e, 0xbc, 0xe2, 0x33, 0x86, 0x11, 0x2d, 0xe4, 0x7c, 0x50, 0xb0, + 0x75, 0xb6, 0xc3, 0xa3, 0xcc, 0xad, 0xaa, 0xb0, 0xc9, 0x2b, 0x18, 0x94, 0x84, 0x45, 0x34, 0xae, + 0x95, 0x51, 0xa8, 0x84, 0x5e, 0x78, 0x29, 0x34, 0xe4, 0x37, 0xd7, 0x2d, 0x39, 0xda, 0xbe, 0x3d, + 0xd8, 0xe2, 0xe1, 0xc4, 0xee, 0x3d, 0x74, 0xd1, 0x10, 0x25, 0xd9, 0xc6, 0x86, 0xa2, 0xa8, 0x50, + 0x43, 0xf1, 0x97, 0x35, 0xb5, 0x5a, 0xf7, 0x9d, 0x40, 0x65, 0x4d, 0x96, 0x8e, 0xce, 0x8b, 0x4c, + 0x2b, 0xb3, 0x61, 0x1d, 0xe8, 0x9c, 0xfb, 0x86, 0xd8, 0xc8, 0x1e, 0x99, 0x22, 0x42, 0x9c, 0x35, + 0x1d, 0xd4, 0x39, 0x4f, 0x6b, 0x31, 0x04, 0x3b, 0xa1, 0x1f, 0xce, 0x42, 0xa7, 0x39, 0x57, 0xad, + 0x58, 0x43, 0xfa, 0xb0, 0x88, 0x71, 0xa7, 0x10, 0xb8, 0xd3, 0xe0, 0x1b, 0x00, 0x9c, 0x63, 0xa3, + 0x18, 0xd2, 0xfb, 0x3d, 0x5e, 0x0c, 0x49, 0x82, 0x87, 0x32, 0xbd, 0xec, 0x94, 0x43, 0xe6, 0x3b, + 0x49, 0x2b, 0x6d, 0xf5, 0x32, 0xf9, 0x42, 0xee, 0x2c, 0x1d, 0xdd, 0x8f, 0xd7, 0x85, 0x73, 0x8f, + 0xd7, 0xd3, 0x69, 0x6b, 0x4a, 0x5b, 0xed, 0x98, 0x35, 0xda, 0x8a, 0x8a, 0xe8, 0x91, 0x64, 0x6f, + 0x9d, 0xbb, 0xbc, 0x0a, 0x15, 0x2a, 0x35, 0x30, 0xa9, 0x85, 0xf7, 0x44, 0x25, 0x74, 0xcc, 0x8b, + 0x25, 0x20, 0x2a, 0xb3, 0xf6, 0x2c, 0xf3, 0x4e, 0x18, 0xa6, 0x28, 0x23, 0xe4, 0x91, 0x6b, 0x45, + 0xa2, 0x81, 0xe6, 0xc5, 0xe5, 0x64, 0x27, 0x49, 0x72, 0x45, 0xc5, 0x28, 0x8a, 0x6a, 0x12, 0xd0, + 0x29, 0x5c, 0xa9, 0xad, 0xdc, 0x84, 0x12, 0xb5, 0xed, 0x5a, 0x46, 0x1b, 0xdb, 0x58, 0x82, 0x72, + 0xd6, 0x53, 0xef, 0x42, 0x06, 0x91, 0x63, 0xb7, 0x02, 0x7b, 0x6a, 0x7e, 0xdc, 0x72, 0x03, 0xb8, + 0xb5, 0xdd, 0x66, 0xe5, 0x27, 0x0a, 0x8f, 0x08, 0x06, 0xa0, 0x99, 0xc3, 0xbf, 0xea, 0x6f, 0xc5, + 0x96, 0xcb, 0x0f, 0xef, 0xce, 0x54, 0x8e, 0x71, 0xca, 0xb8, 0x1e, 0xa3, 0x86, 0xa0, 0x41, 0x62, + 0xec, 0xc3, 0x7b, 0x55, 0xad, 0x46, 0x7b, 0x2e, 0x19, 0x5c, 0x7c, 0x9f, 0xd8, 0x6b, 0x3d, 0xce, + 0x55, 0x48, 0xb4, 0x2e, 0x90, 0x64, 0x83, 0xd0, 0x3d, 0x05, 0xf2, 0xe2, 0xe3, 0xbe, 0xeb, 0x2c, + 0x1d, 0xd7, 0x82, 0xc2, 0x23, 0x5c, 0x78, 0x3b, 0xcf, 0xe3, 0x98, 0xef, 0x3d, 0xb9, 0xa9, 0xc1, + 0xea, 0x87, 0x9c, 0xe3, 0xcb, 0x34, 0x21, 0x6e, 0x4a, 0xe2, 0x7b, 0x26, 0x13, 0x75, 0xe7, 0x37, + 0xe7, 0x98, 0xc7, 0x4e, 0x6b, 0x5d, 0xad, 0xe2, 0x3a, 0xcd, 0xbf, 0x06, 0xb3, 0xa6, 0x63, 0xc3, + 0xef, 0x5e, 0x70, 0x56, 0x29, 0x1f, 0x06, 0xbf, 0x3a, 0x53, 0xdc, 0xf7, 0xe5, 0xc4, 0x95, 0x26, + 0x36, 0xbc, 0x30, 0x72, 0xc7, 0xa9, 0xdf, 0x46, 0xa8, 0xe2, 0x84, 0x95, 0x6c, 0x40, 0x6e, 0x4a, + 0xfb, 0xb0, 0xd7, 0x71, 0x0e, 0x1b, 0x85, 0xde, 0x27, 0xb1, 0xf0, 0xca, 0xdc, 0xbe, 0xf1, 0x88, + 0x7c, 0x50, 0xeb, 0x69, 0xd1, 0xfe, 0xb1, 0x2d, 0xb0, 0x9f, 0xd8, 0xe2, 0x63, 0x62, 0xe1, 0x40, + 0x1d, 0x49, 0x75, 0xd4, 0xff, 0xcd, 0xdd, 0x4c, 0xb6, 0x09, 0x7b, 0x3e, 0xcb, 0xd4, 0x9c, 0x1f, + 0x80, 0x63, 0xa9, 0xa6, 0xfa, 0x65, 0x8f, 0x48, 0x03, 0x31, 0xd6, 0x21, 0xb9, 0x5a, 0x85, 0x87, + 0xb0, 0x59, 0x87, 0xd1, 0x5d, 0x2c, 0xc5, 0x00, 0x4c, 0x98, 0x8a, 0x7d, 0x82, 0xd2, 0x5a, 0xcc, + 0xd2, 0x31, 0x39, 0x80, 0x05, 0xee, 0x67, 0x5b, 0x9f, 0x0e, 0xe0, 0x67, 0x57, 0x09, 0x04, 0x56, + 0xab, 0xae, 0xdd, 0x8a, 0x0e, 0xd8, 0x65, 0x52, 0x2a, 0x73, 0xf5, 0xbb, 0x27, 0xac, 0x8f, 0x68, + 0x9c, 0x81, 0x91, 0x23, 0xf1, 0xb6, 0xd7, 0xa8, 0x9c, 0xe8, 0x30, 0x12, 0x59, 0xd0, 0xaa, 0x98, + 0x75, 0x19, 0x63, 0x9f, 0x3c, 0x7e, 0x67, 0x35, 0xaf, 0x15, 0xf1, 0xa6, 0x92, 0x12, 0x1c, 0x7d, + 0xa5, 0x03, 0xdd, 0xeb, 0xf0, 0x93, 0xac, 0xbf, 0xcd, 0xc8, 0x31, 0xe2, 0x07, 0x4a, 0x9e, 0x08, + 0x78, 0x90, 0xff, 0xb3, 0x0b, 0x9f, 0x9b, 0x5f, 0x11, 0xf0, 0x13, 0x50, 0x07, 0x6c, 0xee, 0xe2, + 0x77, 0x85, 0xeb, 0x02, 0x0a, 0xff, 0x39, 0xb2, 0x13, 0xae, 0xf2, 0xab, 0x29, 0x2b, 0x52, 0xf8, + 0x15, 0xce, 0x1f, 0x4c, 0x6d, 0x9f, 0x60, 0xfa, 0x7e, 0x79, 0xff, 0xea, 0xf6, 0xee, 0x6e, 0x51, + 0x0a, 0x4f, 0xa3, 0x96, 0x95, 0x05, 0x5d, 0x27, 0x62, 0x27, 0x51, 0x53, 0x8d, 0xe2, 0xfa, 0x30, + 0x2b, 0xc3, 0x9f, 0x59, 0xd9, 0x15, 0x17, 0xf7, 0xb4, 0xbd, 0xcc, 0xd6, 0x2c, 0x13, 0xd3, 0xb5, + 0x12, 0xae, 0x65, 0xff, 0x3a, 0x4d, 0xd7, 0x0a, 0x13, 0xd9, 0xdc, 0xb8, 0x44, 0xd9, 0xb4, 0xa1, + 0xd3, 0xb5, 0x21, 0x9b, 0x7c, 0x29, 0x2f, 0x26, 0x2b, 0x4b, 0x45, 0xc1, 0xca, 0xdd, 0xff, 0x33, + 0xa7, 0x2c, 0x5e, 0xbc, 0x1f, 0xd6, 0x60, 0x76, 0xec, 0x3f, 0x65, 0x2c, 0x5d, 0xd1, 0x43, 0x4c, + 0x78, 0x2e, 0xad, 0xdb, 0xbd, 0x61, 0xb7, 0x7f, 0xdc, 0xde, 0x1e, 0x8e, 0x76, 0xb8, 0x74, 0xe2, + 0x1d, 0x14, 0xff, 0x36, 0x7c, 0x07, 0x90, 0xb9, 0x94, 0x17, 0x47, 0x08, 0x00, 0x00, +}; +const StaticFile index_html PROGMEM = {(sizeof(index_html_content)/sizeof(index_html_content[0])), index_html_content}; + +static const uint8_t app_js_content[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x9d, 0x57, 0x6d, 0x6f, 0xdb, 0x46, + 0x12, 0xfe, 0xde, 0x5f, 0x41, 0x2d, 0x70, 0x06, 0xf7, 0x44, 0xd3, 0x2f, 0xd7, 0x02, 0x85, 0x18, + 0x42, 0x48, 0xda, 0xe4, 0x92, 0x22, 0xa9, 0x8b, 0x24, 0x57, 0x1c, 0x60, 0xf8, 0x82, 0x95, 0x38, + 0xb2, 0x18, 0x53, 0xbb, 0xec, 0xee, 0xd2, 0xb2, 0x4f, 0x11, 0x70, 0x69, 0x0a, 0xb4, 0x40, 0x0b, + 0x04, 0xe8, 0xf7, 0xcb, 0xa7, 0xfe, 0x00, 0x37, 0x77, 0xbe, 0x4b, 0x2e, 0x4d, 0xfa, 0x17, 0xa8, + 0x7f, 0xd4, 0x99, 0x25, 0x29, 0xd1, 0xb2, 0x81, 0x06, 0xf7, 0xc1, 0x12, 0xb5, 0x9c, 0x9d, 0x9d, + 0x79, 0x66, 0x9e, 0x67, 0xc7, 0xa3, 0x42, 0x0e, 0x6d, 0xaa, 0xa4, 0x07, 0x3e, 0xf0, 0x99, 0x06, + 0x5b, 0x68, 0xc9, 0xf6, 0xd5, 0xe0, 0x31, 0x0c, 0xad, 0xb7, 0xe7, 0xbe, 0x0e, 0x58, 0x1c, 0xc7, + 0xd5, 0x63, 0x98, 0x6b, 0x65, 0x95, 0x3d, 0xcd, 0x21, 0xb4, 0xea, 0x81, 0xd5, 0xa9, 0x3c, 0x0c, + 0x87, 0x22, 0xcb, 0x70, 0xef, 0x7c, 0xd4, 0x78, 0xb2, 0x2b, 0x4f, 0x5e, 0xa2, 0x86, 0xc5, 0x04, + 0xa4, 0x0d, 0x0f, 0xc1, 0xde, 0xcc, 0x80, 0x1e, 0x6f, 0x9c, 0xde, 0x49, 0x2e, 0xd8, 0x4b, 0xb2, + 0x87, 0xd0, 0xd8, 0xd3, 0x0c, 0xc2, 0x24, 0x35, 0x79, 0x26, 0x4e, 0x63, 0x26, 0x95, 0x04, 0xb6, + 0x32, 0xd2, 0x2d, 0xa7, 0x80, 0x61, 0xc0, 0x31, 0xba, 0xfa, 0x14, 0x46, 0xa2, 0xc8, 0xec, 0xc6, + 0xc6, 0xfa, 0x8a, 0xcf, 0x03, 0x72, 0xa8, 0xf2, 0x2f, 0xb4, 0xca, 0xc5, 0xa1, 0x20, 0x17, 0x64, + 0xb5, 0xb6, 0xe4, 0xcc, 0x86, 0x42, 0x0e, 0x21, 0xbb, 0x51, 0x0c, 0x06, 0x19, 0xc4, 0x9d, 0x6d, + 0x5c, 0xa9, 0x8e, 0xf9, 0x52, 0x64, 0x05, 0x2e, 0xec, 0x04, 0x9d, 0x9d, 0x55, 0x18, 0x69, 0x3b, + 0x0c, 0x2f, 0x95, 0xc6, 0xd2, 0x6e, 0x35, 0xf2, 0x6e, 0x6a, 0xad, 0x74, 0x1f, 0xc2, 0x09, 0x18, + 0x23, 0x0e, 0xa1, 0x07, 0x5d, 0xd6, 0x8a, 0x5e, 0xd5, 0x29, 0x82, 0xbd, 0x6e, 0x11, 0xb5, 0x41, + 0x61, 0xc1, 0x67, 0x98, 0xaa, 0xc0, 0x33, 0x13, 0x16, 0xac, 0x1e, 0x5b, 0xb0, 0x88, 0x6a, 0x8f, + 0x86, 0x89, 0x3a, 0x86, 0xab, 0xb6, 0xb5, 0x6c, 0x8d, 0xcf, 0x67, 0xa3, 0x56, 0x25, 0x03, 0xcb, + 0x67, 0xb2, 0xc8, 0xb2, 0x4e, 0x1c, 0x23, 0x3a, 0x3e, 0x84, 0xc7, 0x2e, 0x19, 0xcb, 0x03, 0xf2, + 0x3a, 0x3f, 0x16, 0xda, 0x33, 0xf1, 0x76, 0xb4, 0xdc, 0x52, 0xe0, 0xfe, 0x5d, 0xac, 0x72, 0xb7, + 0x6b, 0x36, 0x36, 0xa4, 0x6f, 0x7d, 0x96, 0x29, 0x91, 0x60, 0x75, 0x1f, 0x65, 0x62, 0x00, 0x19, + 0xe3, 0xd5, 0x9e, 0x24, 0x5e, 0xd6, 0x73, 0xa4, 0xf4, 0xc4, 0x84, 0x12, 0xec, 0x54, 0xe9, 0xa3, + 0x47, 0x98, 0x98, 0x45, 0x6b, 0x13, 0x25, 0xa1, 0x48, 0x92, 0x9b, 0x54, 0x89, 0xbb, 0xa9, 0xb1, + 0x20, 0x41, 0xfb, 0xcc, 0x14, 0x83, 0x49, 0x6a, 0x59, 0xe0, 0x37, 0xc7, 0xb5, 0xfb, 0x23, 0x1c, + 0xa7, 0x49, 0x15, 0x5d, 0x88, 0x19, 0x4e, 0x7c, 0xde, 0x4f, 0xc2, 0xdc, 0x1c, 0xd5, 0x4b, 0x19, + 0xc8, 0x43, 0x3b, 0xbe, 0xf6, 0x71, 0xdf, 0x17, 0x19, 0x68, 0x0c, 0xab, 0xfc, 0x67, 0x79, 0x5e, + 0xbe, 0x2c, 0xcf, 0x17, 0xff, 0x28, 0xdf, 0x2e, 0xbe, 0x2f, 0x5f, 0x7b, 0xe5, 0xaf, 0xe5, 0x19, + 0xfe, 0x78, 0x57, 0xbe, 0x59, 0xfc, 0xe0, 0xf9, 0xe5, 0x2f, 0xe5, 0xab, 0xf2, 0x2d, 0xfe, 0xfd, + 0x52, 0x9e, 0xd1, 0x0a, 0x3e, 0x9f, 0x2d, 0x9e, 0x7b, 0xe5, 0xbf, 0xcb, 0x37, 0xee, 0xc5, 0x99, + 0xb7, 0xe9, 0x7d, 0xec, 0x2d, 0x9e, 0x3a, 0x8b, 0x97, 0xb4, 0x0b, 0xff, 0x5e, 0x72, 0xc6, 0x03, + 0xea, 0x2f, 0xde, 0xdb, 0xdc, 0x41, 0x10, 0x92, 0xd0, 0x18, 0x0c, 0xca, 0x40, 0x86, 0x1d, 0x0f, + 0xc9, 0x1d, 0x99, 0xc0, 0xc9, 0x85, 0x00, 0xbc, 0xf2, 0x25, 0x9e, 0xfd, 0x33, 0x1e, 0x7b, 0xe6, + 0x7c, 0x2e, 0xbe, 0x2e, 0xdf, 0x2d, 0xbe, 0x2d, 0xff, 0x87, 0x8f, 0x78, 0xd2, 0xbb, 0xc5, 0xd3, + 0xc5, 0xd7, 0x8b, 0x67, 0x14, 0xd8, 0xd2, 0xef, 0xb1, 0x4a, 0x13, 0x6c, 0x02, 0x74, 0xea, 0xb0, + 0xe0, 0xbd, 0xa5, 0xbb, 0x1f, 0x29, 0x1b, 0xdc, 0xf5, 0x0a, 0x9d, 0x9c, 0x7b, 0x63, 0x35, 0xc1, + 0xae, 0x4a, 0x9a, 0x7d, 0x73, 0xce, 0x03, 0xdc, 0x33, 0x56, 0xd3, 0x47, 0x84, 0xc9, 0x65, 0x68, + 0x87, 0x63, 0x21, 0x0f, 0x61, 0x0d, 0xda, 0x0a, 0xc0, 0x8b, 0xbd, 0x46, 0x94, 0x65, 0xd8, 0xda, + 0x56, 0x68, 0x64, 0x62, 0x38, 0x1c, 0xc3, 0xf0, 0x08, 0x92, 0x3e, 0xb3, 0x70, 0x62, 0x59, 0x8f, + 0xe5, 0xc2, 0x18, 0x2c, 0x24, 0xf5, 0x54, 0x75, 0x24, 0x01, 0xf0, 0x9e, 0xc7, 0x51, 0x63, 0xd8, + 0x78, 0xe9, 0xfa, 0x02, 0x6c, 0xd1, 0xe6, 0xce, 0xb2, 0x05, 0xeb, 0xf7, 0x2a, 0xa7, 0x8d, 0x66, + 0xdf, 0x1e, 0xe0, 0x6a, 0xab, 0xd6, 0x31, 0x63, 0x55, 0xc2, 0xe2, 0xb1, 0x38, 0x21, 0xb9, 0xf0, + 0xd9, 0x16, 0xb2, 0xcb, 0x16, 0x86, 0x05, 0xb3, 0x79, 0xeb, 0x48, 0x1b, 0x48, 0x3e, 0xb3, 0xfa, + 0x74, 0x96, 0x8e, 0x7c, 0xcb, 0xed, 0x58, 0xab, 0xa9, 0x67, 0x23, 0xf0, 0x5d, 0x2b, 0x05, 0x32, + 0x94, 0x2a, 0x81, 0x47, 0x69, 0xf2, 0xe4, 0x09, 0x11, 0x00, 0x09, 0x5e, 0x1d, 0x12, 0xac, 0x7e, + 0x55, 0x35, 0xa8, 0x17, 0xb0, 0xf3, 0xe7, 0x43, 0x61, 0x87, 0x63, 0xf4, 0x35, 0xab, 0x8a, 0x92, + 0xe2, 0x23, 0x9f, 0xaf, 0x87, 0x82, 0x32, 0xb1, 0x16, 0x88, 0x63, 0x59, 0x1d, 0x08, 0xd4, 0x81, + 0x40, 0x54, 0x83, 0x97, 0x4a, 0x44, 0xec, 0xf6, 0xc3, 0x7b, 0x77, 0x31, 0xaf, 0x08, 0xc9, 0xe2, + 0x13, 0x4c, 0x12, 0x39, 0x27, 0xaf, 0xd9, 0x30, 0x43, 0x44, 0xeb, 0xde, 0x8e, 0x64, 0xb7, 0x5b, + 0x61, 0xa8, 0xe3, 0xea, 0xc5, 0xbe, 0x3c, 0xd8, 0xdf, 0x3e, 0x08, 0x54, 0xeb, 0xe7, 0xce, 0x41, + 0xe3, 0x56, 0xe4, 0x39, 0xc8, 0xc4, 0x97, 0x30, 0xf5, 0xf6, 0x1c, 0x90, 0xbe, 0xee, 0x32, 0xcf, + 0x67, 0x5d, 0x85, 0x5f, 0xc9, 0x8d, 0x09, 0x67, 0x81, 0xc6, 0xe0, 0x85, 0x5f, 0xd9, 0xb7, 0xf3, + 0x83, 0x55, 0x7e, 0x50, 0xe5, 0x37, 0xbf, 0xc0, 0x7f, 0x8a, 0x01, 0xd6, 0x09, 0x5e, 0xe4, 0x89, + 0xb0, 0xb0, 0xe2, 0x37, 0xbc, 0x17, 0xbf, 0x11, 0x17, 0xc4, 0x44, 0xe3, 0x77, 0xd0, 0x81, 0x70, + 0x94, 0x66, 0xd5, 0x87, 0xa9, 0x73, 0xe6, 0x35, 0xf9, 0x1b, 0x0e, 0xfc, 0x84, 0x3c, 0x7a, 0x5d, + 0xbe, 0xf1, 0x90, 0x8b, 0x3f, 0x23, 0xa1, 0x90, 0x91, 0xc8, 0xcb, 0x73, 0xe2, 0x31, 0x71, 0xf7, + 0xed, 0x1a, 0xe1, 0x90, 0x1c, 0x9d, 0x9d, 0x08, 0x15, 0xb5, 0x21, 0x53, 0x54, 0x41, 0x4b, 0x98, + 0xfc, 0xf5, 0xde, 0xdd, 0xdb, 0xd6, 0xe6, 0xf7, 0xe1, 0xab, 0x02, 0x8c, 0x0d, 0x84, 0x5b, 0xbc, + 0x85, 0x99, 0x7c, 0x2a, 0xac, 0x88, 0x9a, 0x63, 0x1b, 0x14, 0x19, 0x05, 0x45, 0xac, 0x58, 0x45, + 0x88, 0xc8, 0x73, 0xec, 0xa1, 0x22, 0x27, 0xfd, 0xbb, 0x22, 0x57, 0xbc, 0xff, 0x0e, 0x35, 0x4a, + 0xfc, 0x5a, 0xb6, 0x2e, 0x82, 0x40, 0xc7, 0x6b, 0xae, 0x42, 0x93, 0xfe, 0x1d, 0x22, 0x49, 0x95, + 0x44, 0x77, 0x90, 0x5c, 0xd3, 0xfd, 0x7b, 0xc2, 0x8e, 0x43, 0xad, 0x0a, 0x3c, 0xbe, 0x59, 0xdd, + 0xd2, 0x7f, 0xdc, 0xd9, 0xde, 0xe6, 0x78, 0xa3, 0xde, 0x4a, 0x4f, 0x20, 0xf1, 0x77, 0x79, 0x0f, + 0x7f, 0x07, 0x4d, 0x7e, 0xad, 0x56, 0x92, 0x5d, 0xf6, 0x07, 0x46, 0x8d, 0x29, 0x43, 0x25, 0x35, + 0x88, 0xe4, 0x94, 0x18, 0x02, 0x15, 0x2d, 0xe3, 0x65, 0x40, 0x4d, 0x2d, 0x59, 0xf9, 0x62, 0x1d, + 0x50, 0xc2, 0xf2, 0xbf, 0x88, 0xa2, 0x93, 0xd0, 0xc5, 0x77, 0x6e, 0xf1, 0x5d, 0xe0, 0x2d, 0x9e, + 0x39, 0xd1, 0x22, 0x1d, 0x7d, 0x4d, 0x4f, 0xa4, 0x8d, 0x24, 0xad, 0xa4, 0xb3, 0xe7, 0x6e, 0xc3, + 0xbf, 0xd0, 0xfc, 0x59, 0xf9, 0x1f, 0x7c, 0x3a, 0x47, 0xc3, 0xa7, 0x8b, 0xe7, 0x2c, 0xc2, 0x12, + 0x7f, 0x88, 0x72, 0x29, 0x43, 0x17, 0xc9, 0x03, 0x8a, 0x84, 0x13, 0x1d, 0x48, 0x43, 0x3f, 0x7b, + 0xb0, 0xf7, 0x79, 0x98, 0x0b, 0x6d, 0xc0, 0xa7, 0xf7, 0x26, 0x47, 0xca, 0xc3, 0x43, 0x14, 0x1b, + 0x4e, 0xbf, 0xf0, 0x86, 0xee, 0x57, 0xc5, 0x07, 0xde, 0x6b, 0xba, 0xe0, 0x05, 0x86, 0xf3, 0x0a, + 0xe3, 0x75, 0x32, 0x7a, 0x45, 0x27, 0xb0, 0x4b, 0x44, 0xc5, 0x9b, 0x6c, 0xee, 0xa0, 0x00, 0xba, + 0x7c, 0xe3, 0xb6, 0x28, 0xb5, 0x3b, 0x9d, 0x4c, 0xb0, 0xde, 0x3e, 0xfb, 0x62, 0xef, 0xc1, 0x43, + 0x16, 0xd8, 0x46, 0x89, 0x84, 0xb3, 0x26, 0x30, 0x0d, 0x75, 0x83, 0xa0, 0xbe, 0x22, 0x70, 0xeb, + 0x22, 0xbe, 0x8f, 0xfc, 0x55, 0x8d, 0xbe, 0xf4, 0x78, 0xa1, 0xc9, 0x67, 0xab, 0xb6, 0xbc, 0x85, + 0xeb, 0xf7, 0x11, 0x24, 0xd0, 0x11, 0x85, 0x4b, 0x65, 0x5f, 0xaf, 0x96, 0x8d, 0xa7, 0xa9, 0x4c, + 0xd4, 0x34, 0x9c, 0x24, 0x1f, 0x55, 0x90, 0x21, 0x48, 0x3c, 0x5a, 0x9f, 0x19, 0xaa, 0x98, 0x71, + 0x62, 0xd8, 0xaa, 0x98, 0xd9, 0x47, 0xf3, 0x98, 0x75, 0xab, 0x8b, 0xbd, 0x21, 0xc4, 0x95, 0xa0, + 0x34, 0x98, 0x5c, 0x04, 0x7a, 0xf1, 0x2d, 0xdd, 0x3b, 0x35, 0xd5, 0x16, 0xdf, 0x54, 0x44, 0xa4, + 0x8b, 0x8b, 0x7c, 0x50, 0x5d, 0xaf, 0x9b, 0x1b, 0xa9, 0x14, 0xfa, 0xb4, 0x1a, 0xf6, 0xd6, 0x52, + 0x25, 0xb6, 0x38, 0x21, 0xe9, 0xb4, 0x8e, 0x69, 0xcd, 0x81, 0x28, 0xb2, 0x81, 0x0e, 0xd2, 0x4a, + 0x0d, 0x62, 0x5d, 0x49, 0x72, 0xc0, 0x1a, 0x0b, 0xd6, 0x89, 0xe9, 0x5e, 0xc2, 0xe9, 0x29, 0xad, + 0xe5, 0x93, 0xc0, 0x72, 0x93, 0x14, 0x82, 0x8d, 0x63, 0xe5, 0x40, 0x0c, 0x8f, 0xbc, 0x49, 0x61, + 0xac, 0x37, 0x00, 0x4f, 0x78, 0xcb, 0x7d, 0x9c, 0x7a, 0xaf, 0x23, 0x2f, 0x6f, 0x92, 0xca, 0x2b, + 0x74, 0xe6, 0x99, 0x1c, 0x86, 0xe9, 0x28, 0xa5, 0x19, 0x29, 0x32, 0xd3, 0xb4, 0x6e, 0x9a, 0xa1, + 0x30, 0xc0, 0xfe, 0x7c, 0xf3, 0x21, 0xeb, 0x91, 0x60, 0xfb, 0xa8, 0x93, 0x8d, 0x2e, 0x2b, 0x9c, + 0xe4, 0x3c, 0xcd, 0x75, 0x38, 0x16, 0x66, 0x6f, 0x2a, 0x69, 0x42, 0x44, 0xa8, 0x4e, 0x7d, 0xc5, + 0xf1, 0x8e, 0x92, 0xdd, 0xd8, 0x77, 0x73, 0x81, 0x44, 0x26, 0xe2, 0x9d, 0xb6, 0x37, 0xf2, 0x59, + 0x9f, 0xf1, 0x3e, 0x7e, 0xf4, 0xd8, 0x06, 0xe3, 0x5d, 0x90, 0x43, 0xbc, 0x72, 0xfe, 0x72, 0xff, + 0xce, 0x27, 0x6a, 0x82, 0x7d, 0x8e, 0x2d, 0x83, 0x1b, 0xbb, 0x0c, 0xcb, 0x72, 0xc5, 0x1b, 0xbd, + 0xaf, 0x0e, 0x38, 0x8f, 0x06, 0x88, 0xed, 0x51, 0xe4, 0x22, 0x72, 0x5d, 0xb9, 0x0c, 0xc9, 0x35, + 0x83, 0x88, 0xf7, 0x0f, 0xa2, 0xf7, 0x09, 0x4e, 0x84, 0x79, 0x61, 0x50, 0xda, 0xff, 0x8f, 0x08, + 0x74, 0x2c, 0xc2, 0xc7, 0x2a, 0x45, 0x5a, 0x60, 0x0e, 0xf3, 0x7a, 0x24, 0xbc, 0xac, 0xa1, 0x8d, + 0x68, 0x9a, 0x8a, 0x43, 0x74, 0xf9, 0x06, 0x55, 0xc8, 0xb1, 0xbb, 0xd8, 0x0d, 0x35, 0x68, 0x6d, + 0x7b, 0xdb, 0xb5, 0xb8, 0xcf, 0x3e, 0x51, 0x12, 0x19, 0x63, 0x37, 0xab, 0xa1, 0x83, 0xa1, 0xda, + 0x66, 0xe9, 0xd0, 0x0d, 0xdc, 0x5b, 0x27, 0x9b, 0xd3, 0xe9, 0x74, 0x93, 0xee, 0x96, 0x4d, 0x2c, + 0x54, 0x15, 0x1d, 0x8d, 0x3b, 0xe6, 0x77, 0x04, 0xad, 0xd6, 0x1a, 0xd3, 0xd6, 0x1a, 0x5a, 0x64, + 0xf5, 0x88, 0x80, 0x10, 0xe1, 0xe4, 0xda, 0xd9, 0xfa, 0xdb, 0xee, 0x93, 0x9d, 0xdd, 0xdd, 0x3f, + 0x6d, 0x85, 0x16, 0xe3, 0xf1, 0x31, 0x38, 0xf7, 0x9a, 0x5f, 0xee, 0x94, 0x31, 0xa6, 0xe8, 0xd1, + 0xe9, 0x1e, 0xeb, 0x2e, 0xcd, 0xa2, 0xd4, 0x77, 0x1d, 0xda, 0x52, 0x2f, 0x73, 0x51, 0xbd, 0x48, + 0x71, 0xcc, 0x95, 0x8a, 0x83, 0x5a, 0x53, 0x8d, 0x14, 0x64, 0xe0, 0x04, 0xc5, 0xf5, 0x1a, 0xa1, + 0xd4, 0xa7, 0xf5, 0x9e, 0xc6, 0x2c, 0xe7, 0x35, 0xc9, 0x69, 0xb0, 0x88, 0x67, 0xc8, 0xa3, 0x9e, + 0x0d, 0x07, 0x29, 0xdd, 0x05, 0x81, 0xb3, 0xe6, 0x41, 0xae, 0x4c, 0x7b, 0xd1, 0x21, 0x8d, 0x87, + 0xe2, 0x7f, 0x2a, 0xf5, 0xd6, 0x54, 0xa6, 0xf6, 0x7a, 0x9e, 0xb7, 0xc1, 0xc1, 0xe9, 0xdf, 0xdd, + 0xf3, 0xd1, 0x07, 0xbf, 0x01, 0xdd, 0x89, 0x77, 0x95, 0xce, 0x0d, 0x00, 0x00, +}; +const StaticFile app_js PROGMEM = {(sizeof(app_js_content)/sizeof(app_js_content[0])), app_js_content}; + +static const uint8_t md5_js_content[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xad, 0x59, 0x79, 0x73, 0x1b, 0xb7, + 0x15, 0xff, 0xbf, 0x9f, 0x42, 0xe2, 0x4c, 0x39, 0xbb, 0xb3, 0x2b, 0x05, 0xf7, 0x61, 0x72, 0xe5, + 0x89, 0x93, 0x1e, 0xe9, 0x95, 0xb6, 0x69, 0xd2, 0x83, 0x43, 0xcd, 0xd0, 0xd2, 0xd2, 0xbb, 0x89, + 0x42, 0xaa, 0x58, 0xd0, 0xb2, 0x62, 0xd2, 0x9f, 0xbd, 0x0f, 0xd8, 0x0b, 0x4b, 0x89, 0x3a, 0xac, + 0x8e, 0x2d, 0x2c, 0x08, 0xbc, 0xf7, 0x80, 0xdf, 0xbb, 0x70, 0x1d, 0x2f, 0x37, 0xab, 0x0b, 0x5b, + 0xae, 0x57, 0x51, 0xfc, 0x71, 0xb4, 0xa9, 0xf2, 0xa3, 0xca, 0x9a, 0xf2, 0xc2, 0x8e, 0x26, 0xef, + 0x17, 0xe6, 0xc8, 0xa6, 0x26, 0x1b, 0x95, 0xab, 0xeb, 0x8d, 0x3d, 0x2a, 0xab, 0xa3, 0x72, 0xf5, + 0x7e, 0x71, 0x55, 0x5e, 0x1e, 0xd9, 0xdb, 0xeb, 0x7c, 0x94, 0x96, 0xd9, 0xfb, 0x35, 0xfc, 0x40, + 0xc7, 0x59, 0x76, 0x53, 0xae, 0x2e, 0xd7, 0x37, 0xa7, 0x5f, 0x1a, 0xb3, 0xb8, 0x7d, 0xb3, 0x59, + 0x2e, 0x73, 0x93, 0x16, 0xd9, 0x08, 0x61, 0x42, 0x19, 0x17, 0x52, 0xe9, 0xc5, 0xdb, 0x8b, 0xcb, + 0x7c, 0x39, 0x3a, 0xad, 0xae, 0xaf, 0x4a, 0x1b, 0x8d, 0x46, 0x71, 0x5a, 0x65, 0x33, 0x4c, 0x54, + 0x4a, 0x89, 0x14, 0x2a, 0x55, 0x54, 0x29, 0x81, 0x54, 0x7a, 0x42, 0x30, 0x93, 0x4c, 0x51, 0xc1, + 0xd4, 0x3c, 0xcd, 0xb3, 0x19, 0x4a, 0x55, 0x8a, 0x45, 0x4a, 0xd8, 0x3c, 0x5d, 0x64, 0xb3, 0x51, + 0x91, 0x7f, 0x18, 0xa5, 0xa3, 0x85, 0x1b, 0x04, 0xbe, 0x97, 0xe5, 0xbb, 0xbc, 0xb2, 0x50, 0x79, + 0xeb, 0x07, 0x6c, 0x7b, 0xde, 0xb4, 0xbf, 0xde, 0x2e, 0xaa, 0x5c, 0xb0, 0xd1, 0x3c, 0x5d, 0x66, + 0xa3, 0x2f, 0xdf, 0x7c, 0xf5, 0xf5, 0x6f, 0x7e, 0xfb, 0xbb, 0xdf, 0x7f, 0xf3, 0x87, 0x3f, 0xfe, + 0xe9, 0xcf, 0x7f, 0xf9, 0xf6, 0xaf, 0x7f, 0xfb, 0xfb, 0x77, 0xff, 0xf8, 0xfe, 0x87, 0x7f, 0xfe, + 0xeb, 0xdf, 0xff, 0xa9, 0x67, 0xf6, 0xae, 0x28, 0x7f, 0xfc, 0xe9, 0xea, 0xe7, 0xd5, 0xfa, 0xfa, + 0xbf, 0xa6, 0xb2, 0x9b, 0xf7, 0x37, 0x1f, 0x6e, 0x7f, 0xe9, 0x67, 0x9f, 0x7c, 0x11, 0xce, 0x7c, + 0x95, 0xcd, 0xe6, 0x93, 0x72, 0x19, 0x95, 0xf1, 0x47, 0xa7, 0xa1, 0x75, 0xb6, 0xca, 0x6f, 0x8e, + 0x02, 0xe4, 0x91, 0x50, 0xf1, 0xc4, 0xfa, 0xd6, 0xef, 0xcb, 0x95, 0x55, 0xbe, 0x2b, 0x5a, 0x3b, + 0xc6, 0xb6, 0x8d, 0x92, 0xb6, 0x71, 0xe7, 0x2b, 0xa7, 0x65, 0xe5, 0xbf, 0xdb, 0x6d, 0x34, 0xf8, + 0x9d, 0x75, 0xa6, 0xb1, 0xf1, 0x47, 0x93, 0xdb, 0x8d, 0x59, 0x8d, 0x66, 0xeb, 0xb7, 0x3f, 0xe6, + 0x17, 0xb6, 0x1e, 0x71, 0x3e, 0xca, 0xb2, 0xec, 0x5b, 0xdf, 0x70, 0x7a, 0x6d, 0xd6, 0x76, 0xed, + 0x2c, 0x73, 0x6a, 0xd7, 0xdf, 0x81, 0x09, 0x57, 0xef, 0x4e, 0x2f, 0x16, 0x57, 0x57, 0xc0, 0xba, + 0x8b, 0xd3, 0x72, 0x3c, 0x3e, 0x0e, 0xe6, 0x08, 0x03, 0xfc, 0x50, 0xe6, 0x37, 0xe3, 0x71, 0x74, + 0xb7, 0xf1, 0xbe, 0x41, 0xeb, 0x31, 0x61, 0x30, 0x27, 0x7f, 0xbd, 0x3c, 0xb2, 0xe3, 0xb1, 0x3d, + 0xad, 0xf5, 0xde, 0xd7, 0x4e, 0x2f, 0xd6, 0x2b, 0xf0, 0x9d, 0xcd, 0x85, 0x5d, 0x1b, 0x98, 0x56, + 0x20, 0x79, 0x17, 0x7b, 0x67, 0xba, 0x0f, 0xcf, 0x51, 0xd7, 0x64, 0xba, 0x26, 0xa7, 0xa6, 0x4d, + 0x74, 0x8c, 0xe2, 0xd3, 0xcd, 0xf5, 0xe5, 0xc2, 0xe6, 0xd0, 0x35, 0xb3, 0xf3, 0x28, 0xde, 0xed, + 0x26, 0x2d, 0x35, 0xf4, 0x03, 0x3d, 0x58, 0xc1, 0xc4, 0xab, 0x19, 0x9a, 0x67, 0xab, 0x19, 0x16, + 0xbe, 0x74, 0x05, 0x71, 0x05, 0x75, 0x05, 0x73, 0x05, 0x77, 0x85, 0xef, 0x95, 0xae, 0x50, 0xae, + 0xd0, 0x9e, 0xb8, 0x66, 0xf4, 0x3c, 0xd8, 0x33, 0x61, 0xcf, 0x85, 0x3d, 0x1b, 0x06, 0x3e, 0x94, + 0xda, 0xa2, 0xac, 0x4e, 0xdf, 0x5e, 0xad, 0x2f, 0x7e, 0xaa, 0xb2, 0x55, 0xf3, 0xcb, 0x63, 0x52, + 0x99, 0x9d, 0xe4, 0x57, 0x10, 0x2e, 0xbd, 0x2b, 0x14, 0xf7, 0xbb, 0x42, 0xc8, 0xb3, 0xe7, 0x15, + 0x45, 0x3c, 0x1c, 0x60, 0xcf, 0x3f, 0x8a, 0x78, 0xe7, 0x87, 0x08, 0x69, 0x20, 0x28, 0x1e, 0xf9, + 0x37, 0xaf, 0x87, 0x2c, 0x50, 0x56, 0x7f, 0x71, 0xf3, 0x25, 0xcd, 0x97, 0xd6, 0xdf, 0xca, 0x2e, + 0x8c, 0xad, 0xab, 0x6f, 0x6f, 0x6d, 0x5e, 0x35, 0xbd, 0x6f, 0x7c, 0xbd, 0x01, 0xbe, 0x2c, 0x57, + 0x10, 0xf0, 0xbf, 0xe4, 0x97, 0x4d, 0xe7, 0xa2, 0x2a, 0xa0, 0x7e, 0x8c, 0xdb, 0x5e, 0x08, 0x96, + 0xec, 0x18, 0xed, 0x36, 0x81, 0xf3, 0xd5, 0x26, 0x1b, 0x58, 0x1a, 0x34, 0x74, 0x3c, 0x14, 0xd7, + 0xe8, 0x0b, 0xb2, 0x40, 0xeb, 0x50, 0x2e, 0xa2, 0x46, 0x95, 0x77, 0xdb, 0x11, 0x24, 0x94, 0xca, + 0x73, 0xb5, 0x7e, 0xe7, 0x1b, 0x6c, 0x61, 0xd6, 0x37, 0x47, 0xc6, 0x11, 0xae, 0x36, 0x57, 0x57, + 0xe0, 0x61, 0x36, 0x6c, 0x2b, 0x9d, 0x23, 0x1e, 0xf4, 0xc0, 0xf8, 0x4e, 0x40, 0xda, 0xb8, 0x33, + 0xdf, 0xf1, 0x30, 0xea, 0xa0, 0x6b, 0xbb, 0x05, 0x71, 0x77, 0x43, 0x03, 0x7a, 0xe2, 0x6e, 0xcc, + 0xc2, 0x21, 0x5f, 0xae, 0x4d, 0xe4, 0xa0, 0x2c, 0xd2, 0x25, 0xc4, 0x37, 0x4a, 0xd7, 0x99, 0x3d, + 0xbd, 0xca, 0x57, 0xef, 0x6c, 0x91, 0xde, 0x66, 0x81, 0xd9, 0xd2, 0x4d, 0x16, 0xfa, 0xc1, 0x64, + 0x35, 0x5d, 0x4f, 0x3c, 0xc4, 0x40, 0xaf, 0x10, 0x8d, 0x7b, 0x5a, 0xbe, 0x75, 0xae, 0x7d, 0xeb, + 0x5c, 0x3b, 0xf5, 0xa5, 0xab, 0xbb, 0x82, 0xb8, 0x82, 0xba, 0x82, 0xb9, 0x82, 0xbb, 0xc2, 0xf7, + 0x4a, 0x57, 0x28, 0x57, 0x68, 0x4f, 0x5c, 0xb3, 0x7b, 0x1e, 0xec, 0x99, 0xb0, 0xe7, 0xc2, 0x9e, + 0xcd, 0x3b, 0x78, 0x9c, 0x16, 0xb1, 0xf7, 0x60, 0x87, 0x64, 0x19, 0x78, 0x86, 0x9b, 0xe2, 0x78, + 0xbc, 0x9c, 0x0a, 0x36, 0x49, 0x92, 0x55, 0xbc, 0x99, 0x2d, 0x93, 0x64, 0x9e, 0xd9, 0xd9, 0x6a, + 0x5e, 0xeb, 0xed, 0x11, 0xfa, 0xdb, 0xd9, 0xf2, 0xec, 0x8c, 0xcc, 0xb7, 0x9e, 0x63, 0x3a, 0xcd, + 0x67, 0x74, 0xec, 0x04, 0x04, 0x21, 0xf3, 0x88, 0x80, 0x68, 0x01, 0xaa, 0xbc, 0x28, 0x16, 0xe6, + 0xab, 0xf5, 0x65, 0xfe, 0xa5, 0x8d, 0x56, 0x71, 0x3c, 0x85, 0x15, 0xe3, 0x75, 0x33, 0x91, 0xc5, + 0xab, 0xc5, 0x94, 0x20, 0xa6, 0x5e, 0x47, 0x4d, 0x03, 0xd6, 0x64, 0xbb, 0x38, 0x3b, 0x13, 0x69, + 0xfb, 0x9b, 0xa8, 0xad, 0xa0, 0xe3, 0x45, 0x0c, 0x84, 0x9c, 0x13, 0x2d, 0xb6, 0xd0, 0x9d, 0x71, + 0x49, 0x19, 0xeb, 0x78, 0x08, 0x61, 0x8e, 0x07, 0x93, 0x90, 0xc9, 0x09, 0x19, 0x0b, 0x7a, 0x57, + 0x0e, 0xcc, 0x48, 0x70, 0x4e, 0x45, 0x12, 0x45, 0x18, 0x11, 0xd7, 0x34, 0x9d, 0x62, 0xb4, 0xf5, + 0xf5, 0xc1, 0x54, 0xdd, 0xf4, 0xe3, 0x96, 0x9f, 0x30, 0xe4, 0xc7, 0x50, 0x7b, 0x63, 0x60, 0xb2, + 0x37, 0xc8, 0xa1, 0x71, 0x9f, 0xa4, 0xee, 0x43, 0xda, 0xea, 0xcc, 0xb0, 0xe8, 0x6d, 0xd0, 0x69, + 0xae, 0xeb, 0x8c, 0x5a, 0xe5, 0xc5, 0x3d, 0x55, 0x1a, 0xf4, 0xb6, 0x53, 0xe9, 0x7b, 0xef, 0xd5, + 0x6a, 0xcf, 0xd1, 0x29, 0xf6, 0xb0, 0xc0, 0x06, 0xee, 0x33, 0x46, 0x7c, 0xa6, 0xfe, 0x83, 0xd9, + 0xb4, 0x26, 0x78, 0x70, 0x36, 0xde, 0x20, 0xff, 0xc7, 0xf9, 0xd6, 0x59, 0xf8, 0x6a, 0x51, 0x59, + 0x97, 0x52, 0xbf, 0x59, 0x5d, 0xe6, 0x1f, 0xb2, 0x65, 0xda, 0xe7, 0xdb, 0x24, 0x5b, 0x9e, 0xf4, + 0xf6, 0x4c, 0x97, 0x67, 0x99, 0x00, 0x1d, 0x06, 0xa9, 0x79, 0x79, 0x22, 0x58, 0xda, 0x65, 0x84, + 0x28, 0x4e, 0x07, 0xd9, 0x01, 0xc5, 0xaf, 0x42, 0xda, 0x5d, 0xb3, 0x76, 0xf6, 0xf2, 0xcf, 0x18, + 0xd1, 0x4c, 0x0b, 0x49, 0x34, 0xef, 0x32, 0xcb, 0x9b, 0x7a, 0xe0, 0x9e, 0xe8, 0x8b, 0x8e, 0x48, + 0x4c, 0xa7, 0x28, 0xdd, 0x5f, 0x0d, 0x7c, 0xf5, 0xd7, 0x3d, 0x4d, 0x3d, 0x87, 0xdd, 0x2e, 0x0d, + 0x33, 0x7e, 0x9b, 0xd5, 0xb3, 0x60, 0x23, 0x79, 0x5f, 0xca, 0xdf, 0x5b, 0x51, 0x8e, 0x51, 0xbd, + 0xc3, 0x1c, 0xa4, 0x49, 0x93, 0xdd, 0xd5, 0xda, 0xc4, 0xce, 0x4c, 0xad, 0xe7, 0x0a, 0x74, 0x6b, + 0xe6, 0xa9, 0x01, 0x8f, 0x13, 0xc3, 0x6c, 0xb9, 0xdd, 0x0e, 0x14, 0xe5, 0xf2, 0xa6, 0xf5, 0x79, + 0xd3, 0x97, 0xae, 0xee, 0x0a, 0xe2, 0x0a, 0xea, 0x0a, 0xe6, 0x0a, 0xee, 0x0a, 0xdf, 0x2b, 0x5d, + 0xa1, 0x5c, 0xa1, 0x3d, 0x71, 0xcd, 0xee, 0x79, 0xb0, 0x67, 0xc2, 0x9e, 0x0b, 0x7b, 0xb6, 0x26, + 0x6f, 0x36, 0x3f, 0x3b, 0x35, 0x4d, 0xa7, 0x34, 0xad, 0x3b, 0x03, 0x5d, 0x43, 0xe3, 0x36, 0x30, + 0x09, 0xc0, 0xd0, 0xa1, 0x49, 0xf7, 0x34, 0xe9, 0x1a, 0x43, 0x2d, 0x36, 0x3b, 0xf0, 0xb4, 0x4c, + 0x61, 0xa9, 0x4c, 0x73, 0xd8, 0x04, 0x07, 0xba, 0x9a, 0xf4, 0x6b, 0xf0, 0x6b, 0x93, 0x45, 0x91, + 0xfb, 0xb3, 0xfe, 0x6f, 0x01, 0xf0, 0x4f, 0x84, 0x42, 0x4a, 0x0a, 0x4d, 0x25, 0xf8, 0xa4, 0xdc, + 0x5a, 0x37, 0x34, 0x8f, 0x4f, 0x88, 0xc4, 0x92, 0x52, 0x25, 0x35, 0x58, 0x3b, 0x3e, 0x8f, 0x4a, + 0xa0, 0x87, 0xbf, 0xbe, 0xf9, 0x3c, 0x2a, 0xa0, 0x09, 0xfe, 0x4e, 0xa0, 0x81, 0x70, 0xc5, 0xb0, + 0x66, 0xe7, 0x04, 0x21, 0x46, 0xb1, 0x42, 0x12, 0x8f, 0x6d, 0x9c, 0x2c, 0x40, 0x97, 0x27, 0x18, + 0x4b, 0x45, 0x91, 0x44, 0x2e, 0xa4, 0x30, 0xd9, 0x16, 0x4e, 0x3c, 0x8a, 0x13, 0xeb, 0xc4, 0x8e, + 0x43, 0x79, 0xb0, 0x62, 0x02, 0x07, 0x71, 0x1c, 0x44, 0x30, 0x60, 0x92, 0xdc, 0xb1, 0xc8, 0x6d, + 0x09, 0x2c, 0x98, 0xc7, 0x49, 0x51, 0xb3, 0x14, 0x0d, 0x25, 0x05, 0x4a, 0x8a, 0x05, 0xe1, 0x9a, + 0x20, 0x0d, 0x94, 0x84, 0x6c, 0x8d, 0xa3, 0x04, 0xe1, 0x25, 0x50, 0xbe, 0x8a, 0x1a, 0x7f, 0x29, + 0x50, 0xeb, 0x2b, 0x05, 0x86, 0x93, 0x48, 0xb3, 0xc3, 0x49, 0xbd, 0x22, 0x92, 0x56, 0x13, 0x89, + 0xc7, 0xd2, 0xec, 0x7a, 0xe2, 0x73, 0x33, 0x8e, 0xca, 0xf3, 0xc2, 0x0f, 0x13, 0x28, 0x48, 0x84, + 0x0a, 0x4a, 0x4c, 0xa0, 0x18, 0x60, 0x37, 0x8d, 0x42, 0xa0, 0x5a, 0x9e, 0xdb, 0x31, 0xfc, 0x2e, + 0xe3, 0x46, 0x05, 0x54, 0x69, 0x2e, 0x18, 0x57, 0xe2, 0x5e, 0x15, 0xd8, 0x73, 0xd3, 0x20, 0x4f, + 0x04, 0x12, 0x18, 0x71, 0x85, 0xf5, 0xa3, 0xc0, 0x11, 0x63, 0x9c, 0x70, 0x4a, 0xd1, 0x5d, 0xe0, + 0xf1, 0x5d, 0x68, 0x45, 0x08, 0x88, 0x01, 0xbb, 0x14, 0x0c, 0x2b, 0xa5, 0xe5, 0xe7, 0x00, 0xe2, + 0xf3, 0x04, 0x83, 0x9d, 0x91, 0x42, 0x8c, 0x3c, 0x8c, 0x48, 0xc0, 0x48, 0x0c, 0x9c, 0x83, 0x62, + 0xca, 0xf0, 0x83, 0x90, 0xe4, 0xfc, 0x84, 0x71, 0x89, 0xb8, 0x56, 0xf4, 0x2e, 0xa0, 0x47, 0xf0, + 0x28, 0x98, 0x8f, 0x94, 0x08, 0x51, 0xce, 0xf0, 0x67, 0x59, 0x48, 0xc3, 0x34, 0xb5, 0x73, 0x60, + 0xc6, 0xb0, 0x7c, 0x10, 0x10, 0x44, 0xfb, 0x09, 0x23, 0xc8, 0xe7, 0xf6, 0xc3, 0x60, 0xb0, 0xf3, + 0x7a, 0xad, 0x11, 0x43, 0x30, 0x21, 0xf2, 0x6c, 0x3c, 0x90, 0x46, 0x12, 0x08, 0x21, 0x26, 0x10, + 0x15, 0x8a, 0x7c, 0x96, 0xcb, 0x81, 0x87, 0x30, 0x04, 0x3a, 0xc7, 0x08, 0x3f, 0x8c, 0xc7, 0xf9, + 0x02, 0x47, 0x60, 0x4d, 0x42, 0x34, 0x7a, 0x18, 0x94, 0x37, 0x3b, 0x15, 0x9c, 0x72, 0x4a, 0xee, + 0x09, 0xb7, 0x1e, 0x54, 0x3b, 0x25, 0x98, 0xcd, 0xb8, 0x87, 0x08, 0xe8, 0x06, 0x31, 0x81, 0x05, + 0x97, 0x5a, 0x70, 0xec, 0x46, 0xe5, 0x35, 0x40, 0xd9, 0x02, 0xec, 0xbd, 0x07, 0x09, 0xcd, 0x11, + 0x16, 0xd4, 0xa9, 0x41, 0xd7, 0x28, 0x68, 0x83, 0xc2, 0x01, 0xee, 0x34, 0x61, 0x9d, 0x02, 0x03, + 0xfd, 0x27, 0x82, 0x51, 0x48, 0x2c, 0x12, 0x7b, 0x4b, 0xb1, 0x1a, 0x94, 0x6a, 0x40, 0xf5, 0x81, + 0x4d, 0x25, 0x84, 0xa6, 0xa4, 0xc8, 0x1b, 0x09, 0xd5, 0x78, 0xc8, 0x67, 0xe0, 0xe1, 0xf3, 0x13, + 0x89, 0x30, 0x87, 0x08, 0xd7, 0xf8, 0x20, 0x1e, 0x70, 0x9e, 0x84, 0x2a, 0x40, 0x83, 0xbc, 0x93, + 0x3f, 0x1d, 0x0e, 0x48, 0x17, 0x02, 0xb9, 0x8c, 0x48, 0xf9, 0x61, 0x38, 0xcc, 0xd9, 0x1c, 0x36, + 0x3d, 0x52, 0x31, 0xf5, 0x42, 0x38, 0x7a, 0x9e, 0x70, 0xa1, 0x18, 0x03, 0x25, 0xaa, 0xc3, 0x70, + 0x9c, 0xef, 0x20, 0xac, 0x15, 0xb8, 0xa9, 0xf7, 0x9d, 0x27, 0x03, 0x72, 0xf9, 0x4b, 0x49, 0x2a, + 0xa8, 0x16, 0xf8, 0x30, 0x1e, 0x17, 0xd6, 0x60, 0x79, 0x4e, 0xc1, 0x41, 0xf1, 0x0b, 0x01, 0xb9, + 0x80, 0x80, 0xe8, 0x66, 0x42, 0x61, 0x26, 0xe4, 0x41, 0x48, 0xb0, 0xf6, 0x70, 0x0c, 0x81, 0x03, + 0x2a, 0x7c, 0x0e, 0x1e, 0xe9, 0x12, 0x90, 0x0b, 0x0b, 0x05, 0xc9, 0xee, 0x30, 0x20, 0xec, 0x96, + 0x36, 0x4d, 0xc0, 0x94, 0x12, 0x36, 0xc1, 0x0f, 0x21, 0xca, 0xb3, 0x0e, 0x54, 0x54, 0x65, 0x0e, + 0xc4, 0x79, 0x87, 0xab, 0x02, 0x69, 0xb5, 0xc7, 0xc1, 0x34, 0xc1, 0xe3, 0x40, 0x0e, 0xab, 0xc1, + 0xa8, 0x06, 0x4c, 0xad, 0xbc, 0x13, 0x02, 0x31, 0xcd, 0x25, 0x60, 0xf6, 0x53, 0xc2, 0x35, 0x18, + 0xdc, 0x81, 0x09, 0xd2, 0x48, 0x0e, 0xf0, 0x9b, 0xb0, 0xc1, 0x8a, 0x6a, 0x44, 0x11, 0xf7, 0x69, + 0x0b, 0x8b, 0x1a, 0x87, 0x68, 0x70, 0xb4, 0x56, 0x07, 0xa8, 0x48, 0x73, 0xee, 0x72, 0x2d, 0xa1, + 0x1e, 0x82, 0xfe, 0x0c, 0x04, 0x2e, 0x07, 0x38, 0x41, 0xb0, 0x7a, 0x0b, 0x74, 0x3f, 0x0a, 0xe6, + 0x52, 0x8e, 0x24, 0x4a, 0x83, 0xe3, 0x3f, 0x15, 0x85, 0x74, 0x62, 0x39, 0xd3, 0xb2, 0x4e, 0x1a, + 0xf7, 0x62, 0x40, 0xce, 0x73, 0x35, 0x18, 0x0b, 0x09, 0x86, 0x5e, 0x88, 0x82, 0x42, 0xae, 0x51, + 0x30, 0x4b, 0x8d, 0x25, 0xbb, 0x1f, 0x85, 0xcb, 0x33, 0x5c, 0x41, 0x60, 0x12, 0x42, 0x9e, 0x08, + 0x02, 0xdc, 0x15, 0xa8, 0x39, 0xc1, 0x5a, 0xea, 0x03, 0x20, 0xc4, 0x3c, 0x91, 0x02, 0x11, 0x8d, + 0x95, 0x7e, 0x19, 0x02, 0x58, 0xfd, 0x40, 0x0b, 0x54, 0x30, 0xa6, 0xe4, 0xfd, 0x00, 0x9c, 0xe3, + 0x32, 0x82, 0x15, 0xe6, 0xaa, 0x4e, 0x40, 0x4f, 0x71, 0x26, 0x58, 0x2e, 0xc0, 0xba, 0x92, 0x01, + 0x0a, 0x74, 0x00, 0x02, 0x88, 0xd5, 0x1a, 0xf6, 0x30, 0x0a, 0x96, 0x81, 0x07, 0x30, 0xf4, 0xf1, + 0x1d, 0x46, 0x77, 0x64, 0xb6, 0x9f, 0xba, 0x3c, 0x0e, 0x59, 0x48, 0x50, 0x04, 0x19, 0x0b, 0xc4, + 0x88, 0x1a, 0x80, 0x68, 0x00, 0x6c, 0x3f, 0x95, 0x5d, 0x90, 0xc2, 0xbe, 0x52, 0x69, 0xcc, 0x30, + 0xaf, 0x8f, 0x83, 0x1e, 0x04, 0x69, 0x41, 0x84, 0xc1, 0x1d, 0x15, 0xdb, 0x4f, 0x41, 0x96, 0x63, + 0x2e, 0x0f, 0x31, 0x8d, 0x3c, 0x1f, 0xaf, 0x81, 0xc8, 0x06, 0xc8, 0xf6, 0x93, 0x6d, 0x97, 0x00, + 0x88, 0x36, 0xea, 0x52, 0xb0, 0xc3, 0x82, 0xeb, 0xd0, 0xc6, 0xcf, 0x05, 0xe3, 0x17, 0x7f, 0xd8, + 0xcc, 0x30, 0x88, 0x6d, 0x89, 0x0f, 0xc3, 0xf1, 0x39, 0x14, 0xce, 0x5a, 0x0a, 0xd2, 0x89, 0x78, + 0x06, 0x1c, 0xef, 0xfa, 0x1c, 0x73, 0x42, 0x1f, 0xc0, 0x82, 0x5d, 0xf6, 0x00, 0xc0, 0x04, 0xdc, + 0x5a, 0xbf, 0x04, 0x8d, 0xcb, 0xe1, 0x90, 0xe9, 0x61, 0xff, 0x47, 0xb9, 0x3e, 0x0c, 0xc6, 0xad, + 0x70, 0x10, 0x8b, 0x70, 0x52, 0xf0, 0x16, 0x7c, 0x22, 0x16, 0xb7, 0x3f, 0xe0, 0xc2, 0xad, 0x40, + 0xb0, 0xaa, 0x3e, 0x84, 0x06, 0x42, 0x14, 0x43, 0xa2, 0xc1, 0x1c, 0x0b, 0xf6, 0x22, 0x38, 0xde, + 0x17, 0x38, 0xe8, 0x0e, 0x8e, 0x33, 0x0f, 0xa0, 0xf1, 0x87, 0x1e, 0x48, 0xbf, 0x18, 0x56, 0x12, + 0xfd, 0x74, 0x3c, 0x60, 0x79, 0x09, 0xda, 0x52, 0x92, 0x78, 0x5d, 0x1d, 0x82, 0x03, 0xf1, 0x4a, + 0xc1, 0xcf, 0xc0, 0x3f, 0xea, 0xa8, 0xd9, 0x03, 0x13, 0x1c, 0xf5, 0xa2, 0xee, 0x72, 0x37, 0xe9, + 0x8e, 0x68, 0xb4, 0xa3, 0x29, 0x70, 0x66, 0x06, 0x87, 0xbc, 0xb4, 0xbd, 0xfb, 0x2d, 0x83, 0x13, + 0x5d, 0xdf, 0x4e, 0xb3, 0x22, 0x69, 0xc9, 0xd5, 0x70, 0xa4, 0xec, 0x18, 0xc7, 0xaf, 0xfa, 0xd1, + 0xea, 0xaf, 0xc7, 0xda, 0x0d, 0xd5, 0x7c, 0xbd, 0xa6, 0xba, 0x81, 0x9a, 0x6f, 0x30, 0xf1, 0xf6, + 0xc6, 0xb9, 0xa0, 0x35, 0xe8, 0xbd, 0x43, 0xaf, 0xbb, 0x17, 0xe9, 0xcf, 0xbc, 0x83, 0x5b, 0x82, + 0x28, 0x0e, 0xef, 0x08, 0x0e, 0x9c, 0xf9, 0xda, 0x9b, 0x6b, 0x3a, 0x69, 0xee, 0x40, 0x8a, 0x19, + 0x98, 0x90, 0x8d, 0x5d, 0xb6, 0x2a, 0xc0, 0x0d, 0xc7, 0xd6, 0x7d, 0xad, 0xbf, 0xe1, 0xa9, 0xdb, + 0xa0, 0xae, 0xfa, 0x2a, 0x41, 0x7d, 0x1d, 0x8b, 0xa0, 0x3d, 0xa4, 0x69, 0xc5, 0x99, 0x81, 0x64, + 0xd3, 0x34, 0x75, 0x92, 0x4d, 0x2f, 0xd9, 0x04, 0x92, 0x4d, 0x20, 0xd9, 0x04, 0x92, 0x4d, 0x20, + 0xb9, 0x1c, 0x48, 0x2e, 0x9b, 0xa6, 0x4e, 0x72, 0xd9, 0x4b, 0x2e, 0x03, 0xc9, 0x65, 0x20, 0xb9, + 0x0c, 0x24, 0x97, 0x81, 0xe4, 0x6a, 0x20, 0xb9, 0x6a, 0x9a, 0x3a, 0xc9, 0x55, 0x2f, 0xb9, 0x0a, + 0x24, 0x57, 0x81, 0xe4, 0x2a, 0x90, 0x5c, 0xb5, 0x92, 0x87, 0x66, 0x6c, 0x1f, 0x9d, 0xb2, 0x3d, + 0xdb, 0x0e, 0x88, 0xea, 0x37, 0xbb, 0x97, 0x99, 0xbb, 0xd8, 0x33, 0xf7, 0x8c, 0x70, 0x30, 0x71, + 0xea, 0x6d, 0x0a, 0xd5, 0xb4, 0xb6, 0x62, 0x53, 0x83, 0x99, 0xba, 0x9a, 0x23, 0x31, 0xa9, 0x69, + 0x49, 0x4c, 0x47, 0x62, 0x06, 0x24, 0x65, 0x5a, 0xb6, 0x24, 0x65, 0x47, 0x52, 0x0e, 0x48, 0x8a, + 0xb4, 0x68, 0x49, 0x8a, 0x8e, 0xa4, 0x68, 0x49, 0xf6, 0x74, 0xe2, 0x1f, 0x24, 0xb3, 0xbb, 0x0a, + 0xb8, 0x4b, 0x54, 0x3f, 0x33, 0x3c, 0xae, 0x98, 0xfd, 0x07, 0x26, 0x58, 0x7a, 0x41, 0x49, 0xfb, + 0xef, 0x46, 0x36, 0x6e, 0x43, 0xc1, 0xf8, 0x9b, 0xb0, 0x56, 0x9b, 0xfe, 0x16, 0xac, 0x51, 0xa8, + 0xf1, 0xb7, 0x61, 0xed, 0xb5, 0x89, 0xbf, 0x15, 0xab, 0xd5, 0x9a, 0xda, 0x21, 0x88, 0xfa, 0xc9, + 0x22, 0x3b, 0x30, 0xe5, 0x21, 0xa9, 0x7f, 0x72, 0x0d, 0x51, 0xb4, 0xef, 0x23, 0xcd, 0x2d, 0x56, + 0x36, 0x1a, 0xb5, 0xe1, 0xea, 0x65, 0x44, 0x71, 0x9a, 0x67, 0x68, 0x92, 0x43, 0x5e, 0x9c, 0xc4, + 0x36, 0xab, 0x66, 0xb9, 0xbb, 0x57, 0x35, 0x6d, 0xa5, 0x6c, 0x2b, 0x90, 0xbf, 0x97, 0x33, 0x9f, + 0x97, 0xe7, 0xc9, 0x72, 0x26, 0x28, 0x1c, 0x76, 0xdd, 0xa6, 0xc6, 0xe5, 0x48, 0x16, 0xb7, 0x4d, + 0x90, 0x85, 0x88, 0xcf, 0xae, 0xa2, 0x6d, 0x2a, 0xe7, 0xad, 0x1e, 0xbc, 0xec, 0x7d, 0x39, 0x4e, + 0xc6, 0x58, 0xc0, 0x42, 0x32, 0xca, 0xb2, 0xd1, 0x2e, 0x6d, 0x1e, 0xb1, 0x7f, 0xbe, 0xe4, 0x77, + 0xee, 0xe0, 0xb2, 0xdb, 0xc8, 0xbf, 0x3e, 0xc7, 0x13, 0x7b, 0x7a, 0x61, 0xf2, 0xc1, 0x03, 0xd7, + 0xf0, 0xd9, 0x72, 0x97, 0xda, 0x3b, 0x4f, 0x60, 0xfd, 0xcb, 0x66, 0xcb, 0x1d, 0x05, 0x4f, 0x9b, + 0xbb, 0x49, 0xab, 0x24, 0x03, 0xaa, 0x30, 0xd3, 0x45, 0xf3, 0x80, 0x34, 0x49, 0x12, 0x53, 0x0f, + 0x5f, 0x66, 0x8b, 0x99, 0x99, 0x4f, 0xec, 0xac, 0x9c, 0xc3, 0x44, 0xca, 0xb8, 0xbb, 0xeb, 0xdd, + 0x45, 0x31, 0xfc, 0x9f, 0xfc, 0xea, 0x7f, 0xf1, 0xc2, 0x99, 0x50, 0xc3, 0x1f, 0x00, 0x00, +}; +const StaticFile md5_js PROGMEM = {(sizeof(md5_js_content)/sizeof(md5_js_content[0])), md5_js_content}; + +static const uint8_t style_css_content[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x9d, 0x53, 0x5d, 0x6b, 0xdb, 0x30, + 0x14, 0x7d, 0xdf, 0xaf, 0x08, 0x94, 0x41, 0x0b, 0x76, 0xb0, 0x9b, 0x26, 0x59, 0x64, 0xf6, 0xb0, + 0x3d, 0x8c, 0xed, 0x61, 0x4f, 0x65, 0x4f, 0xa3, 0x14, 0x7d, 0x5c, 0xd9, 0x22, 0xb2, 0x25, 0xa4, + 0xeb, 0x26, 0x99, 0xf1, 0x7f, 0x9f, 0xfc, 0x11, 0x37, 0x69, 0x32, 0x28, 0xc3, 0x20, 0xb8, 0x1f, + 0xd2, 0x39, 0xf7, 0xdc, 0x63, 0x66, 0xc4, 0x21, 0x2a, 0xb0, 0xd4, 0x8d, 0xa5, 0x42, 0xa8, 0x2a, + 0x27, 0x49, 0x56, 0x52, 0x97, 0xab, 0x8a, 0x24, 0x2d, 0xeb, 0x8a, 0xac, 0x46, 0x34, 0x55, 0xa4, + 0x2a, 0x5b, 0xe3, 0x6f, 0x3c, 0x58, 0xf8, 0x6c, 0xa9, 0xf7, 0x3b, 0xe3, 0xc4, 0xd3, 0x69, 0x12, + 0x61, 0x8f, 0x4f, 0x8d, 0x34, 0x15, 0xc6, 0x5e, 0xfd, 0x01, 0x92, 0xae, 0xec, 0x3e, 0xeb, 0x43, + 0x49, 0x4b, 0xa5, 0x0f, 0x24, 0xa6, 0xd6, 0x6a, 0x88, 0xfd, 0xc1, 0x23, 0x94, 0xd1, 0x57, 0xad, + 0xaa, 0xed, 0x4f, 0xca, 0x1f, 0xfb, 0xf0, 0x5b, 0xe8, 0x8b, 0x1e, 0x21, 0x37, 0x30, 0xfb, 0xf5, + 0x23, 0xfa, 0x0e, 0xfa, 0x05, 0x50, 0x71, 0x1a, 0x7d, 0x71, 0x8a, 0xea, 0xc8, 0xd3, 0xca, 0xc7, + 0x1e, 0x9c, 0x92, 0xed, 0x1c, 0x15, 0x6a, 0x98, 0xb8, 0xa6, 0x89, 0xdd, 0xcf, 0xfa, 0x63, 0x42, + 0xdb, 0x81, 0xca, 0x0b, 0x24, 0xab, 0x24, 0xc9, 0x18, 0xe5, 0xdb, 0xdc, 0x99, 0xba, 0x12, 0x31, + 0x37, 0xda, 0x38, 0x72, 0x03, 0x52, 0xde, 0xcb, 0x65, 0xc6, 0x02, 0x79, 0x70, 0x31, 0x33, 0x61, + 0xb2, 0x92, 0xa4, 0xe1, 0xba, 0x37, 0x5a, 0x89, 0xd9, 0x8d, 0xd8, 0x40, 0x02, 0xeb, 0x6c, 0xec, + 0xbe, 0x5f, 0xaf, 0x80, 0x3d, 0x64, 0x27, 0x33, 0x2d, 0xed, 0xbe, 0x9d, 0x33, 0x6d, 0xf8, 0xf6, + 0x8c, 0x42, 0x3b, 0x97, 0xb5, 0xd6, 0xf1, 0x4e, 0x09, 0x2c, 0x9a, 0xfe, 0x0c, 0xe9, 0xe4, 0x63, + 0xc0, 0xd9, 0x77, 0x17, 0xbb, 0xb6, 0x09, 0xb2, 0x6b, 0x36, 0xae, 0x7c, 0xd6, 0x94, 0xc1, 0x89, + 0xe8, 0xb3, 0x64, 0xb6, 0xb8, 0x1c, 0x61, 0xec, 0xed, 0x65, 0x6e, 0x86, 0xb5, 0x4c, 0xac, 0x7b, + 0x2e, 0x7d, 0xd9, 0xd7, 0xec, 0xec, 0xb5, 0x18, 0x8d, 0x25, 0xe1, 0xb5, 0xf6, 0x5d, 0x3b, 0x8b, + 0x3c, 0x68, 0xe0, 0xd8, 0x8c, 0x0c, 0x1d, 0x15, 0xaa, 0xf6, 0xe4, 0x21, 0x90, 0x19, 0x32, 0xa7, + 0xfa, 0xf0, 0x0d, 0xe7, 0x5c, 0x66, 0x47, 0xd6, 0xeb, 0x50, 0xd9, 0x84, 0x46, 0x53, 0x63, 0x58, + 0x27, 0x04, 0xc7, 0x5c, 0x43, 0x24, 0xd2, 0xf0, 0xda, 0x5f, 0xe0, 0x8e, 0xe9, 0x01, 0x7d, 0x08, + 0x9a, 0x5e, 0xb0, 0x82, 0x0a, 0xb3, 0xeb, 0x15, 0xe9, 0x37, 0xeb, 0x72, 0x46, 0x6f, 0x93, 0xa8, + 0xfb, 0xe6, 0xe9, 0xf2, 0xae, 0x1d, 0xfc, 0x48, 0x84, 0xf2, 0x94, 0x69, 0x10, 0x57, 0x8d, 0x79, + 0xb5, 0x3a, 0xa0, 0x4e, 0x95, 0x11, 0xf8, 0x18, 0x37, 0x97, 0x6e, 0x91, 0x69, 0x70, 0xcb, 0xe2, + 0xe8, 0x96, 0xb3, 0xe4, 0xc8, 0xe2, 0x7f, 0x45, 0xeb, 0x96, 0xf7, 0xaa, 0xda, 0x89, 0x51, 0xc3, + 0xfb, 0x52, 0x1e, 0xfd, 0x97, 0x04, 0x0f, 0x5b, 0xe3, 0x15, 0xaa, 0x30, 0xaf, 0x03, 0x4d, 0x51, + 0xbd, 0x40, 0xd6, 0xdd, 0x89, 0x8b, 0xc1, 0x22, 0xe9, 0xa7, 0x2b, 0x9e, 0x19, 0x05, 0xaa, 0x0c, + 0xde, 0x4e, 0xd3, 0xdd, 0x91, 0xc2, 0xbc, 0x80, 0x7b, 0x8f, 0xc0, 0x19, 0xaf, 0x9d, 0x0f, 0xf0, + 0xd6, 0xa8, 0x0a, 0xc1, 0xbd, 0x19, 0x9f, 0x2d, 0x39, 0x87, 0xc5, 0xf9, 0x1f, 0xf2, 0x0f, 0x44, + 0xca, 0x3b, 0xbe, 0x4d, 0x67, 0xc6, 0xa0, 0xc6, 0xd8, 0x34, 0x57, 0xfe, 0xd9, 0x81, 0x07, 0x8c, + 0xde, 0xc4, 0xd7, 0xf9, 0x1e, 0xff, 0xdb, 0xd5, 0x62, 0x93, 0xae, 0xdb, 0x0f, 0x7f, 0x01, 0x37, + 0xdb, 0x6e, 0xf6, 0xae, 0x04, 0x00, 0x00, +}; +const StaticFile style_css PROGMEM = {(sizeof(style_css_content)/sizeof(style_css_content[0])), style_css_content}; + +static const uint8_t favicon_ico_content[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xed, 0x99, 0x4b, 0x48, 0x15, 0x61, + 0x14, 0xc7, 0xcf, 0xc5, 0x17, 0x2e, 0x4a, 0x57, 0xe5, 0x63, 0xe1, 0x85, 0x42, 0x23, 0x8c, 0x8c, + 0x20, 0x4d, 0x45, 0xdb, 0x59, 0x14, 0x2e, 0x7a, 0xab, 0x68, 0x1b, 0x17, 0xae, 0x24, 0x41, 0xf1, + 0x41, 0xa0, 0x41, 0xa1, 0x11, 0x28, 0x1a, 0x2e, 0x12, 0x72, 0xe7, 0x03, 0x09, 0x74, 0x15, 0x54, + 0x1b, 0x97, 0xd9, 0x53, 0x23, 0x8a, 0x5a, 0x94, 0x25, 0x59, 0x91, 0x82, 0xa0, 0x81, 0x99, 0x39, + 0xfd, 0x8f, 0x73, 0x46, 0xbf, 0xc6, 0xb9, 0x73, 0x67, 0xee, 0x9d, 0xab, 0x41, 0x1e, 0xf8, 0x71, + 0xe7, 0x7e, 0xe7, 0xf1, 0x9f, 0xb9, 0xdf, 0x7c, 0x8f, 0x99, 0x4b, 0xe4, 0xa3, 0x28, 0x4a, 0x4c, + 0x24, 0x7c, 0xfa, 0xa9, 0x2a, 0x9a, 0xe8, 0x08, 0x11, 0x25, 0x25, 0xe9, 0xdf, 0xdb, 0xe2, 0x89, + 0x7a, 0xd1, 0xe6, 0xf7, 0xeb, 0xdf, 0x07, 0x11, 0x97, 0xbe, 0x93, 0x68, 0x1f, 0x62, 0x70, 0x88, + 0x16, 0xbd, 0x7d, 0xd5, 0x10, 0x37, 0x43, 0x3a, 0x0e, 0x2c, 0x06, 0xdc, 0x16, 0x62, 0x9c, 0xa5, + 0xfc, 0x65, 0xb1, 0x60, 0x58, 0x88, 0x75, 0x99, 0x9b, 0x06, 0x2a, 0xc1, 0x1b, 0xa1, 0x52, 0xda, + 0x82, 0x19, 0xae, 0x90, 0xca, 0xc0, 0x04, 0x58, 0x02, 0x9a, 0xb0, 0x24, 0x6d, 0xa5, 0x12, 0x13, + 0xc8, 0xd8, 0x3f, 0xab, 0xe4, 0x99, 0x99, 0x95, 0xfa, 0x56, 0xe6, 0x17, 0x8d, 0x40, 0xb9, 0x06, + 0x13, 0x12, 0x6b, 0xb6, 0x4b, 0xa6, 0x73, 0x0e, 0xc4, 0x92, 0xc4, 0x9a, 0xed, 0xba, 0x83, 0x5c, + 0x83, 0x6b, 0x16, 0xf9, 0x1d, 0x2e, 0xf2, 0x3b, 0x2c, 0xf2, 0xdb, 0x5d, 0xe4, 0xb7, 0x9b, 0x72, + 0xf9, 0xfe, 0xea, 0x76, 0x91, 0xcf, 0xb1, 0x6a, 0x3f, 0x9e, 0x03, 0x5f, 0x5c, 0xe4, 0x73, 0xec, + 0x59, 0x25, 0xbf, 0x00, 0xdc, 0x02, 0x5d, 0xe0, 0x2e, 0x58, 0x00, 0xf7, 0xe5, 0x7b, 0x97, 0x1c, + 0x2f, 0x88, 0xaf, 0x4b, 0x62, 0x0b, 0x4c, 0xd7, 0xe0, 0x93, 0xcf, 0x7c, 0xf0, 0x99, 0xf4, 0x7b, + 0xc9, 0xb0, 0x52, 0x69, 0xcb, 0x37, 0xc5, 0x5a, 0x59, 0x1e, 0x98, 0x06, 0x25, 0x4a, 0x5b, 0x89, + 0xb4, 0xe5, 0xd9, 0xe4, 0x19, 0x96, 0x09, 0x9e, 0x83, 0x22, 0xa5, 0xad, 0x48, 0xda, 0x32, 0xcd, + 0xc1, 0x8b, 0x18, 0x55, 0x93, 0x71, 0x44, 0xa3, 0x51, 0x44, 0x2d, 0x3e, 0x1d, 0xa7, 0xc6, 0x79, + 0x3c, 0xcf, 0x60, 0x2a, 0xa2, 0x2c, 0x52, 0xe6, 0x99, 0x78, 0x57, 0xf3, 0x8c, 0xd9, 0x52, 0xc1, + 0x43, 0xf0, 0x40, 0x8e, 0xbd, 0xb6, 0x1c, 0xf0, 0x5e, 0xc8, 0x89, 0x40, 0xfd, 0x6c, 0xf0, 0x41, + 0xc8, 0xf6, 0xb0, 0x2e, 0x8f, 0x91, 0xbd, 0xa0, 0x0a, 0x7c, 0x15, 0xf8, 0x78, 0x0f, 0xd9, 0xcf, + 0x69, 0xc1, 0x8c, 0x7b, 0x9c, 0xbb, 0xaf, 0x0d, 0x3c, 0x05, 0xf3, 0xb4, 0x3e, 0x4e, 0xe6, 0xa5, + 0xad, 0x15, 0x1c, 0x24, 0xfb, 0xfb, 0xd6, 0xca, 0xf8, 0xbc, 0x78, 0x6c, 0x3a, 0x99, 0x17, 0xc7, + 0x25, 0xd6, 0xe9, 0xb5, 0xf0, 0xb9, 0x9c, 0x27, 0x7d, 0x5c, 0x38, 0x1d, 0xf7, 0xd3, 0x92, 0xe3, + 0xe4, 0x3a, 0xf8, 0x37, 0x79, 0xe1, 0xa2, 0xb6, 0x7a, 0x1d, 0x59, 0x41, 0x6a, 0x73, 0x5f, 0xb6, + 0x85, 0x50, 0xdb, 0xe0, 0x06, 0xd9, 0xaf, 0xc9, 0xe9, 0xa4, 0xf7, 0x5b, 0xa8, 0xf5, 0x9f, 0x48, + 0x8d, 0x40, 0x76, 0x0c, 0xfc, 0x08, 0xa3, 0x3e, 0xcf, 0xa5, 0x85, 0x36, 0xf5, 0xcf, 0x84, 0x51, + 0xdb, 0xe0, 0xb4, 0x4d, 0xfd, 0x72, 0x0f, 0xea, 0x97, 0xdb, 0xd4, 0xaf, 0xf0, 0xa0, 0x7e, 0xc5, + 0x16, 0xd6, 0x2f, 0xf3, 0xa0, 0x7e, 0xa0, 0x7d, 0x12, 0xcf, 0x57, 0x9d, 0x1e, 0xd4, 0xef, 0x94, + 0x5a, 0xaa, 0x61, 0x15, 0xa3, 0x5e, 0xf0, 0xd3, 0x83, 0xfa, 0x5c, 0xe3, 0x8e, 0xd4, 0x34, 0x8c, + 0xe7, 0x8d, 0x26, 0xf0, 0x58, 0x18, 0x03, 0xcf, 0x48, 0x5f, 0xea, 0x38, 0x67, 0x4a, 0xf1, 0x19, + 0x4c, 0x89, 0x6f, 0x46, 0x62, 0xc7, 0x14, 0x5f, 0x13, 0x6d, 0x9c, 0x8b, 0x78, 0x5c, 0xef, 0x06, + 0x29, 0xa4, 0x2f, 0xad, 0xbc, 0xc4, 0x8e, 0x80, 0x15, 0xd2, 0xe7, 0xe1, 0x64, 0xf1, 0xa5, 0xc8, + 0x71, 0xab, 0xf8, 0x46, 0x68, 0x7d, 0x39, 0x4e, 0x91, 0x1a, 0x4e, 0xf6, 0xed, 0x09, 0xa0, 0x5f, + 0x6a, 0x34, 0x58, 0xf8, 0x1b, 0xc4, 0xd7, 0x2f, 0xb1, 0x6e, 0x8d, 0x1f, 0x43, 0x06, 0xa4, 0x46, + 0xa3, 0x85, 0xbf, 0x51, 0x7c, 0x03, 0x12, 0xeb, 0xd6, 0xf8, 0x9c, 0xfa, 0xa4, 0x46, 0xbd, 0x85, + 0xbf, 0x5e, 0x7c, 0x7d, 0x14, 0xda, 0xf9, 0x73, 0xff, 0x5f, 0x01, 0x9f, 0xc0, 0x49, 0x0b, 0x3f, + 0xb7, 0x4d, 0x49, 0x4c, 0x94, 0x85, 0x7f, 0xd5, 0xb4, 0xab, 0xda, 0x2a, 0x2b, 0xd4, 0x4c, 0x8b, + 0x78, 0x94, 0x99, 0xc3, 0xa9, 0x4c, 0x52, 0xdc, 0x2a, 0xa3, 0x48, 0x63, 0x5a, 0x70, 0x2b, 0x04, + 0xc2, 0x6c, 0xdc, 0xb6, 0x98, 0x94, 0xb6, 0x06, 0x77, 0xbc, 0x9f, 0xf4, 0x89, 0x79, 0x6d, 0x1f, + 0x96, 0xb8, 0x71, 0x1f, 0x76, 0xea, 0x72, 0x51, 0x48, 0x48, 0xf9, 0x77, 0xe0, 0x2d, 0x48, 0x0b, + 0xb5, 0x4e, 0x18, 0xfa, 0xd5, 0xb4, 0x3e, 0x36, 0xab, 0xb7, 0x40, 0xbf, 0x56, 0xd1, 0xaf, 0xfd, + 0x0f, 0xf5, 0xeb, 0x14, 0xfd, 0xba, 0x4d, 0xd2, 0xe4, 0xdb, 0x9a, 0x9f, 0x35, 0x5f, 0x82, 0xdf, + 0x8a, 0x3e, 0x1f, 0xf3, 0x9e, 0x92, 0x9f, 0x23, 0x33, 0x22, 0xa0, 0xcb, 0x73, 0xe7, 0x10, 0xe9, + 0xf3, 0x46, 0xb0, 0x75, 0x82, 0x63, 0x06, 0xc1, 0x2e, 0x8f, 0xb4, 0xf3, 0x68, 0x7d, 0x3d, 0x71, + 0xc3, 0x77, 0x90, 0x1b, 0xa6, 0x76, 0x21, 0xe9, 0x7b, 0xb3, 0x50, 0xd7, 0x4c, 0xce, 0x2d, 0x08, + 0xe3, 0x37, 0x77, 0xb3, 0x97, 0x0f, 0xc4, 0x37, 0x90, 0x1c, 0x82, 0xfe, 0x90, 0x07, 0xda, 0x06, + 0x83, 0x2e, 0xb5, 0xf7, 0x9b, 0xee, 0xef, 0x70, 0xe1, 0x5a, 0x07, 0x5c, 0xe8, 0xbb, 0x79, 0x77, + 0xe3, 0x94, 0x9b, 0x2e, 0xf4, 0x5f, 0x47, 0x40, 0xff, 0x95, 0x43, 0x6d, 0xde, 0x23, 0x39, 0x19, + 0xe7, 0xa1, 0xf4, 0x41, 0xb4, 0x03, 0xfd, 0xd4, 0x08, 0x68, 0x1b, 0xa4, 0x38, 0xd0, 0xcf, 0x88, + 0xa0, 0x7e, 0xd0, 0xb9, 0x79, 0x5b, 0x7f, 0x5b, 0x7f, 0xab, 0xf4, 0x61, 0x17, 0xc1, 0xbd, 0x08, + 0xea, 0x73, 0xed, 0x0b, 0x01, 0xb4, 0xe3, 0xc1, 0xaf, 0x08, 0x6a, 0x1b, 0xf0, 0xff, 0x05, 0xf1, + 0x16, 0xfa, 0xfc, 0x50, 0x31, 0xbe, 0x09, 0xfa, 0xfc, 0x0e, 0xcd, 0x67, 0xf3, 0x1b, 0x1c, 0x02, + 0x87, 0x4d, 0xd4, 0x28, 0xf9, 0xc3, 0x16, 0x7e, 0x83, 0x61, 0x25, 0xae, 0xc6, 0xc2, 0x7f, 0xc8, + 0xea, 0xda, 0x1d, 0xdc, 0x93, 0xc5, 0x4a, 0xdd, 0x1e, 0x9b, 0xb8, 0x1e, 0x25, 0xae, 0xd8, 0xad, + 0xce, 0xb6, 0xfe, 0x3f, 0xab, 0x7f, 0x42, 0xa9, 0xdb, 0x6d, 0x13, 0xa7, 0xfe, 0x67, 0x75, 0xdc, + 0x43, 0xfd, 0x1d, 0xa4, 0xbf, 0x27, 0xe2, 0x77, 0xfc, 0x47, 0x6d, 0xe2, 0x72, 0x49, 0xdf, 0xef, + 0x3e, 0xe2, 0x1c, 0x27, 0xb5, 0xb5, 0xe5, 0x42, 0x6d, 0x03, 0x93, 0x71, 0x9a, 0xd6, 0xe2, 0xd3, + 0x70, 0x11, 0x1a, 0x36, 0x42, 0xcd, 0xcb, 0x78, 0x0e, 0x60, 0x16, 0xf1, 0x9c, 0x6f, 0x30, 0x47, + 0x94, 0x60, 0xc7, 0x24, 0x51, 0xdc, 0x28, 0x51, 0x14, 0xc3, 0xc7, 0x5a, 0xeb, 0x47, 0xcd, 0x8a, + 0x3f, 0x05, 0x2f, 0x43, 0xb9, 0xce, 0x1e, 0x00, 0x00, +}; +const StaticFile favicon_ico PROGMEM = {(sizeof(favicon_ico_content)/sizeof(favicon_ico_content[0])), favicon_ico_content}; + +} diff --git a/include/pio/libs/static/homekit/static.h b/include/pio/libs/static/homekit/static.h new file mode 100644 index 0000000..c2617e9 --- /dev/null +++ b/include/pio/libs/static/homekit/static.h @@ -0,0 +1,25 @@ +/** + * This file is autogenerated with make_static.sh script + */ + +#ifndef COMMON_HOMEKIT_STATIC_H +#define COMMON_HOMEKIT_STATIC_H + +#include <stdlib.h> + +namespace homekit::files { + +typedef struct { + size_t size; + const uint8_t* content; +} StaticFile; + +extern const StaticFile index_html; +extern const StaticFile app_js; +extern const StaticFile md5_js; +extern const StaticFile style_css; +extern const StaticFile favicon_ico; + +} + +#endif //COMMON_HOMEKIT_STATIC_H
\ No newline at end of file diff --git a/include/pio/libs/static/library.json b/include/pio/libs/static/library.json new file mode 100644 index 0000000..bc650d7 --- /dev/null +++ b/include/pio/libs/static/library.json @@ -0,0 +1,8 @@ +{ + "name": "homekit_static", + "version": "1.0.1", + "build": { + "flags": "-I../../include" + } +} + diff --git a/include/pio/libs/temphum/homekit/temphum.cpp b/include/pio/libs/temphum/homekit/temphum.cpp new file mode 100644 index 0000000..e69b3a5 --- /dev/null +++ b/include/pio/libs/temphum/homekit/temphum.cpp @@ -0,0 +1,89 @@ +#ifndef CONFIG_TARGET_ESP01 +#include <Arduino.h> +#endif +#include <homekit/logging.h> +#include "temphum.h" + +namespace homekit::temphum { + +void Sensor::setup() const { +#ifndef CONFIG_TARGET_ESP01 + pinMode(CONFIG_SDA_GPIO, OUTPUT); + pinMode(CONFIG_SCL_GPIO, OUTPUT); + + Wire.begin(CONFIG_SDA_GPIO, CONFIG_SCL_GPIO); +#else + Wire.begin(); +#endif +} + +void Sensor::writeCommand(int reg) const { + Wire.beginTransmission(dev_addr); + Wire.write(reg); + Wire.endTransmission(); + delay(500); // wait for the measurement to be ready +} + +SensorData Si7021::read() { + uint8_t error = 0; + writeCommand(0xf3); // command to measure temperature + Wire.requestFrom(dev_addr, 2); + if (Wire.available() < 2) { + PRINTLN("Si7021: 0xf3: could not read 2 bytes"); + error = 1; + } + uint16_t temp_raw = Wire.read() << 8 | Wire.read(); + double temperature = ((175.72 * temp_raw) / 65536.0) - 46.85; + + writeCommand(0xf5); // command to measure humidity + Wire.requestFrom(dev_addr, 2); + if (Wire.available() < 2) { + PRINTLN("Si7021: 0xf5: could not read 2 bytes"); + error = 1; + } + uint16_t hum_raw = Wire.read() << 8 | Wire.read(); + double humidity = ((125.0 * hum_raw) / 65536.0) - 6.0; + + return { + .error = error, + .temp = temperature, + .rh = humidity + }; +} + +SensorData DHT12::read() { + SensorData sd; + byte raw[5]; + sd.error = 1; + + writeCommand(0); + Wire.requestFrom(dev_addr, 5); + + if (Wire.available() < 5) { + PRINTLN("DHT12: could not read 5 bytes"); + goto end; + } + + // Parse the received data + for (uint8_t i = 0; i < 5; i++) + raw[i] = Wire.read(); + + if (((raw[0] + raw[1] + raw[2] + raw[3]) & 0xff) != raw[4]) { + PRINTLN("DHT12: checksum error"); + goto end; + } + + // Calculate temperature and humidity values + sd.temp = raw[2] + (raw[3] & 0x7f) * 0.1; + if (raw[3] & 0x80) + sd.temp *= -1; + + sd.rh = raw[0] + raw[1] * 0.1; + + sd.error = 0; + +end: + return sd; +} + +} diff --git a/include/pio/libs/temphum/homekit/temphum.h b/include/pio/libs/temphum/homekit/temphum.h new file mode 100644 index 0000000..1952ce0 --- /dev/null +++ b/include/pio/libs/temphum/homekit/temphum.h @@ -0,0 +1,38 @@ +#pragma once + +#include <Wire.h> + +namespace homekit::temphum { + +struct SensorData { + uint8_t error = 0; + double temp = 0; // celsius + double rh = 0; // relative humidity percentage +}; + + +class Sensor { +protected: + int dev_addr; +public: + explicit Sensor(int dev) : dev_addr(dev) {} + void setup() const; + void writeCommand(int reg) const; + virtual SensorData read() = 0; +}; + + +class Si7021 : public Sensor { +public: + SensorData read() override; + Si7021() : Sensor(0x40) {} +}; + + +class DHT12 : public Sensor { +public: + SensorData read() override; + DHT12() : Sensor(0x5c) {} +}; + +}
\ No newline at end of file diff --git a/include/pio/libs/temphum/library.json b/include/pio/libs/temphum/library.json new file mode 100644 index 0000000..329b7ca --- /dev/null +++ b/include/pio/libs/temphum/library.json @@ -0,0 +1,8 @@ +{ + "name": "homekit_temphum", + "version": "1.0.3", + "build": { + "flags": "-I../../include" + } +} + diff --git a/include/pio/libs/wifi/homekit/wifi.cpp b/include/pio/libs/wifi/homekit/wifi.cpp new file mode 100644 index 0000000..3060dd6 --- /dev/null +++ b/include/pio/libs/wifi/homekit/wifi.cpp @@ -0,0 +1,47 @@ +#include <pgmspace.h> +#include "wifi.h" +#include <homekit/config.h> +#include <homekit/logging.h> + +namespace homekit::wifi { + +using namespace homekit; +using homekit::config::ConfigData; + +const char NODE_ID[] = CONFIG_NODE_ID; +const char AP_SSID[] = CONFIG_WIFI_AP_SSID; +const char STA_SSID[] = CONFIG_WIFI_STA_SSID; +const char STA_PSK[] = CONFIG_WIFI_STA_PSK; + +void getConfig(ConfigData &cfg, const char** ssid, const char** psk, const char** hostname) { + if (cfg.flags.wifi_configured) { + *ssid = cfg.wifi_ssid; + *psk = cfg.wifi_psk; + *hostname = cfg.node_id; + } else { + *ssid = STA_SSID; + *psk = STA_PSK; + *hostname = NODE_ID; + } +} + +std::shared_ptr<std::list<ScanResult>> scan() { + if (WiFi.getMode() != WIFI_STA) { + PRINTLN("wifi::scan: switching mode to STA"); + WiFi.mode(WIFI_STA); + } + + std::shared_ptr<std::list<ScanResult>> results(new std::list<ScanResult>); + int count = WiFi.scanNetworks(); + for (int i = 0; i < count; i++) { + results->push_back(ScanResult { + .rssi = WiFi.RSSI(i), + .ssid = WiFi.SSID(i) + }); + } + + WiFi.scanDelete(); + return results; +} + +}
\ No newline at end of file diff --git a/include/pio/libs/wifi/homekit/wifi.h b/include/pio/libs/wifi/homekit/wifi.h new file mode 100644 index 0000000..3fe77cb --- /dev/null +++ b/include/pio/libs/wifi/homekit/wifi.h @@ -0,0 +1,40 @@ +#ifndef HOMEKIT_TEPMHUM_WIFI_H +#define HOMEKIT_TEPMHUM_WIFI_H + +#include <ESP8266WiFi.h> +#include <list> +#include <memory> + +#include <homekit/config.h> + +namespace homekit::wifi { + +using homekit::config::ConfigData; + +struct ScanResult { + int rssi; + String ssid; +}; + +void getConfig(ConfigData& cfg, const char** ssid, const char** psk, const char** hostname); + +std::shared_ptr<std::list<ScanResult>> scan(); + +inline uint32_t getIPAsInteger() { + if (!WiFi.isConnected()) + return 0; + return WiFi.localIP().v4(); +} + +inline int8_t getRSSI() { + return WiFi.RSSI(); +} + +extern const char AP_SSID[]; +extern const char STA_SSID[]; +extern const char STA_PSK[]; +extern const char NODE_ID[]; + +} + +#endif //HOMEKIT_TEPMHUM_WIFI_H
\ No newline at end of file diff --git a/include/pio/libs/wifi/library.json b/include/pio/libs/wifi/library.json new file mode 100644 index 0000000..c7faecd --- /dev/null +++ b/include/pio/libs/wifi/library.json @@ -0,0 +1,8 @@ +{ + "name": "homekit_wifi", + "version": "1.0.1", + "build": { + "flags": "-I../../include" + } +} + |