diff options
Diffstat (limited to 'include/pio')
52 files changed, 3579 insertions, 0 deletions
diff --git a/include/pio/include/homekit/logging.h b/include/pio/include/homekit/logging.h new file mode 100644 index 0000000..559ca33 --- /dev/null +++ b/include/pio/include/homekit/logging.h @@ -0,0 +1,20 @@ +#ifndef COMMON_HOMEKIT_LOGGING_H +#define COMMON_HOMEKIT_LOGGING_H + +#include <stdlib.h> + +#ifdef DEBUG + +#define PRINTLN(s) Serial.println(s) +#define PRINT(s) Serial.print(s) +#define PRINTF(fmt, ...) Serial.printf(fmt, ##__VA_ARGS__) + +#else + +#define PRINTLN(s) +#define PRINT(s) +#define PRINTF(...) + +#endif + +#endif //COMMON_HOMEKIT_LOGGING_H
\ No newline at end of file diff --git a/include/pio/include/homekit/macros.h b/include/pio/include/homekit/macros.h new file mode 100644 index 0000000..7d3ad83 --- /dev/null +++ b/include/pio/include/homekit/macros.h @@ -0,0 +1 @@ +#define ARRAY_SIZE(X) sizeof((X))/sizeof((X)[0])
\ No newline at end of file diff --git a/include/pio/include/homekit/stopwatch.h b/include/pio/include/homekit/stopwatch.h new file mode 100644 index 0000000..bac2fcc --- /dev/null +++ b/include/pio/include/homekit/stopwatch.h @@ -0,0 +1,30 @@ +#pragma once + +#include <Arduino.h> + +namespace homekit { + +class StopWatch { +private: + unsigned long time; + +public: + StopWatch() : time(0) {}; + + inline void save() { + time = millis(); + } + + inline bool elapsed(unsigned long ms) { + unsigned long now = millis(); + if (now < time) { + // rollover? + time = now; + } else if (now - time >= ms) { + return true; + } + return false; + } +}; + +}
\ No newline at end of file diff --git a/include/pio/include/homekit/util.h b/include/pio/include/homekit/util.h new file mode 100644 index 0000000..e0780d8 --- /dev/null +++ b/include/pio/include/homekit/util.h @@ -0,0 +1,13 @@ +#pragma once + +namespace homekit { + +inline size_t otaGetMaxUpdateSize() { + return (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; +} + +inline void restart() { + ESP.restart(); +} + +}
\ No newline at end of file diff --git a/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" + } +} + diff --git a/include/pio/make_static.sh b/include/pio/make_static.sh new file mode 100755 index 0000000..d207e57 --- /dev/null +++ b/include/pio/make_static.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +#set -x +#set -e + +COMMON_DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)" +PROJECT_DIR="$(pwd)" + +fw_version="$(cat "$PROJECT_DIR/src/config.def.h" | grep "^#define FW_VERSION" | awk '{print $3}')" +header="$PROJECT_DIR/src/static.h" +source="$PROJECT_DIR/src/static.cpp" + +[ -f "$header" ] && rm "$header" +[ -f "$source" ] && rm "$source" + +is_minifyable() { + local ext="$1" + [ "$ext" = "html" ] || [ "$ext" = "css" ] || [ "$ext" = "js" ] +} + +minify() { + local ext="$1" + local bin="$(realpath "$COMMON_DIR"/../../tools/minify.js)" + "$bin" --type "$ext" +} + +# .h header +cat <<EOF >> "$header" +/** + * This file is autogenerated with make_static.sh script + */ + +#pragma once + +#include <stdlib.h> + +namespace homekit::files { + +typedef struct { + size_t size; + const uint8_t* content; +} StaticFile; + +EOF + +cat <<EOF >> "$source" +/** + * This file is autogenerated with make_static.sh script + */ + +#include "static.h" + +namespace homekit::files { + +EOF + +# loop over files +for ext in html js css ico; do + for f in "$COMMON_DIR"/static/*.$ext; do + filename="$(basename "$f")" + echo "processing ${filename}..." + filename="${filename/./_}" + + # write .h + echo "extern const StaticFile $filename;" >> "$header" + + # write .c + { + echo "static const uint8_t ${filename}_content[] PROGMEM = {" + + cat "$f" | + ( [ "$ext" = "html" ] && sed "s/{version}/$fw_version/" || cat ) | + ( is_minifyable "$ext" && minify "$ext" || cat ) | + gzip | + xxd -ps -c 16 | + sed 's/.\{2\}/0x&, /g' | + sed 's/^/ /' | + sed 's/[ \t]*$//' + + echo "};" + echo "const StaticFile $filename PROGMEM = {(sizeof(${filename}_content)/sizeof(${filename}_content[0])), ${filename}_content};" + echo "" + } >> "$source" + done +done + +# end of homekit::files +( echo ""; echo "}" ) >> "$header" +echo "}" >> "$source" diff --git a/include/pio/static/app.js b/include/pio/static/app.js new file mode 100644 index 0000000..299230c --- /dev/null +++ b/include/pio/static/app.js @@ -0,0 +1,246 @@ +function isObject(o) { + return Object.prototype.toString.call(o) === '[object Object]'; +} + +function ge(id) { + return document.getElementById(id) +} + +function hide(el) { + el.style.display = 'none' +} + +function cancelEvent(evt) { + if (evt.preventDefault) evt.preventDefault(); + if (evt.stopPropagation) evt.stopPropagation(); + + evt.cancelBubble = true; + evt.returnValue = false; + + return false; +} + +function errorText(e) { + return e instanceof Error ? e.message : e+'' +} + +(function() { + function request(method, url, data, callback) { + data = data || null; + + if (typeof callback != 'function') { + throw new Error('callback must be a function'); + } + + if (!url) + throw new Error('no url specified'); + + switch (method) { + case 'GET': + if (isObject(data)) { + for (var k in data) { + if (data.hasOwnProperty(k)) + url += (url.indexOf('?') === -1 ? '?' : '&')+encodeURIComponent(k)+'='+encodeURIComponent(data[k]) + } + } + break; + + case 'POST': + if (isObject(data)) { + var sdata = []; + for (var k in data) { + if (data.hasOwnProperty(k)) + sdata.push(encodeURIComponent(k)+'='+encodeURIComponent(data[k])); + } + data = sdata.join('&'); + } + break; + } + + var xhr = new XMLHttpRequest(); + xhr.open(method, url); + + if (method === 'POST') + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + if ('status' in xhr && !/^2|1223/.test(xhr.status)) + throw new Error('http code '+xhr.status) + callback(null, JSON.parse(xhr.responseText)); + } + }; + xhr.onerror = function(e) { + callback(e, null); + }; + + xhr.send(method === 'GET' ? null : data); + return xhr; + } + + window.ajax = { + get: request.bind(request, 'GET'), + post: request.bind(request, 'POST') + } +})(); + + +function lock(el) { + el.setAttribute('disabled', 'disabled'); +} + +function unlock(el) { + el.removeAttribute('disabled'); +} + +function initNetworkSettings() { + function setupField(el, value) { + if (value !== null) + el.value = value; + unlock(el); + } + + var doneRequestsCount = 0; + function onRequestDone() { + doneRequestsCount++; + if (doneRequestsCount === 2) { + hide(ge('loading_label')) + } + } + + var form = document.forms.network_settings; + form.addEventListener('submit', function(e) { + if (!form.hid.value.trim()) { + alert('Введите home id'); + return cancelEvent(e); + } + + if (form.psk.value.length < 8) { + alert('Неверный пароль (минимальная длина - 8 символов)'); + return cancelEvent(e); + } + + if (form.ssid.selectedIndex === -1) { + alert('Не выбрана точка доступа'); + return cancelEvent(e); + } + + lock(form.submit) + }) + form.show_psk.addEventListener('change', function(e) { + form.psk.setAttribute('type', e.target.checked ? 'text' : 'password'); + }); + form.ssid.addEventListener('change', function(e) { + var i = e.target.selectedIndex; + if (i !== -1) { + var opt = e.target.options[i]; + if (opt) + form.psk.value = ''; + } + }); + + ajax.get('/status', {}, function(error, response) { + try { + if (error) + throw error; + + setupField(form.hid, response.node_id || null); + setupField(form.psk, null); + setupField(form.submit, null); + + onRequestDone(); + } catch (error) { + alert(errorText(error)); + } + }); + + ajax.get('/scan', {}, function(error, response) { + try { + if (error) + throw error; + + form.ssid.innerHTML = ''; + for (var i = 0; i < response.list.length; i++) { + var ssid = response.list[i][0]; + var rssi = response.list[i][1]; + form.ssid.append(new Option(ssid + ' (' + rssi + ' dBm)', ssid)); + } + unlock(form.ssid); + + onRequestDone(); + } catch (error) { + alert(errorText(error)); + } + }); +} + +function initUpdateForm() { + var form = document.forms.update_settings; + form.addEventListener('submit', function(e) { + cancelEvent(e); + if (!form.file.files.length) { + alert('Файл обновления не выбран'); + return false; + } + + lock(form.submit); + + var xhr = new XMLHttpRequest(); + var fd = new FormData(); + fd.append('file', form.file.files[0]); + + xhr.upload.addEventListener('progress', function (e) { + var total = form.file.files[0].size; + var progress; + if (e.loaded < total) { + progress = Math.round(e.loaded / total * 100).toFixed(2); + } else { + progress = 100; + } + form.submit.innerHTML = progress + '%'; + }); + xhr.onreadystatechange = function() { + var errorMessage = 'Ошибка обновления'; + var successMessage = 'Обновление завершено, устройство перезагружается'; + if (xhr.readyState === 4) { + try { + var response = JSON.parse(xhr.responseText); + if (response.result === 1) { + alert(successMessage); + } else { + alert(errorMessage); + } + } catch (e) { + alert(successMessage); + } + } + }; + xhr.onerror = function(e) { + alert(errorText(e)); + }; + + xhr.open('POST', e.target.action); + xhr.send(fd); + + return false; + }); + form.file.addEventListener('change', function(e) { + if (e.target.files.length) { + var reader = new FileReader(); + reader.onload = function() { + var hash = window.md5(reader.result); + form.setAttribute('action', '/update?md5='+hash); + unlock(form.submit); + }; + reader.onerror = function() { + alert('Ошибка чтения файла'); + }; + reader.readAsBinaryString(e.target.files[0]); + } + }); +} + +window.initApp = function() { + initNetworkSettings(); + initUpdateForm(); +}
\ No newline at end of file diff --git a/include/pio/static/favicon.ico b/include/pio/static/favicon.ico Binary files differnew file mode 100644 index 0000000..6940e4f --- /dev/null +++ b/include/pio/static/favicon.ico diff --git a/include/pio/static/index.html b/include/pio/static/index.html new file mode 100644 index 0000000..d4a8040 --- /dev/null +++ b/include/pio/static/index.html @@ -0,0 +1,63 @@ +<!doctype html> +<html lang="en"> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> + <title>Configuration</title> + <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"> + <link rel="stylesheet" type="text/css" href="/style.css"> + <script src="/md5.js"></script> + <script src="/app.js"></script> +</head> +<body onload="initApp()"> +<div class="title">Settings <span id="loading_label">(loading...)</span></div> +<div class="block"> + <form method="post" action="/status" name="network_settings"> + <div class="form_label">WiFi SSID</div> + <div class="form_input"> + <select id="ssid_select" name="ssid" class="full-width"> + <option value="">Loading...</option> + </select> + </div> + + <div class="form_label">WiFi Password</div> + <div class="form_input"> + <input type="password" value="" name="psk" class="full-width" id="fld_psk" maxlength="63" disabled> + <div class="form_sublabel"> + <label for="show_psk"><input type="checkbox" name="show_psk" id="show_psk"> show password</label> + </div> + </div> + + <div class="form_label">Home ID</div> + <div class="form_input"> + <input type="text" value="" maxlength="16" name="hid" id="fld_hid" class="full-width" disabled> + </div> + + <button type="submit" disabled="disabled" name="submit">Save and Reboot</button> + </form> +</div> + +<div class="title">Update firmware (.bin)</div> +<div class="block"> + <form method="post" action="/update" enctype="multipart/form-data" name="update_settings"> + <div class="form_input"> + <input type="file" accept=".bin,.bin.gz" name="file"> + </div> + <button type="submit" name="submit" disabled="disabled">Upload</button> + </form> +</div> + +<div class="title">Reset settings</div> +<div class="block"> + <form method="post" action="/reset"> + <button type="submit" name="submit" class="is_reset">Reset</button> + </form> +</div> + +<div class="title">Info</div> +<div class="block"> + ESP8266-based <b>relayctl</b>, firmware v{version}<br> + Part of <a href="https://git.ch1p.io/homekit.git/">homekit</a> by <a href="https://ch1p.io">Evgeny Zinoviev</a> © 2022 +</div> +</body> +</html>
\ No newline at end of file diff --git a/include/pio/static/md5.js b/include/pio/static/md5.js new file mode 100644 index 0000000..b707a4e --- /dev/null +++ b/include/pio/static/md5.js @@ -0,0 +1,615 @@ +/** + * [js-md5]{@link https://github.com/emn178/js-md5} + * + * @namespace md5 + * @version 0.7.3 + * @author Chen, Yi-Cyuan [emn178@gmail.com] + * @copyright Chen, Yi-Cyuan 2014-2017 + * @license MIT + */ +(function () { + 'use strict'; + + var ERROR = 'input is invalid type'; + var ARRAY_BUFFER = typeof window.ArrayBuffer !== 'undefined'; + var HEX_CHARS = '0123456789abcdef'.split(''); + var EXTRA = [128, 32768, 8388608, -2147483648]; + var SHIFT = [0, 8, 16, 24]; + var OUTPUT_TYPES = ['hex', 'array', 'digest', 'buffer', 'arrayBuffer', 'base64']; + var BASE64_ENCODE_CHAR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split(''); + + var blocks = [], buffer8; + if (ARRAY_BUFFER) { + var buffer = new ArrayBuffer(68); + buffer8 = new Uint8Array(buffer); + blocks = new Uint32Array(buffer); + } + + if (!Array.isArray) { + Array.isArray = function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }; + } + + if (ARRAY_BUFFER && !ArrayBuffer.isView) { + ArrayBuffer.isView = function (obj) { + return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer; + }; + } + + /** + * @method hex + * @memberof md5 + * @description Output hash as hex string + * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash + * @returns {String} Hex string + * @example + * md5.hex('The quick brown fox jumps over the lazy dog'); + * // equal to + * md5('The quick brown fox jumps over the lazy dog'); + */ + /** + * @method digest + * @memberof md5 + * @description Output hash as bytes array + * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash + * @returns {Array} Bytes array + * @example + * md5.digest('The quick brown fox jumps over the lazy dog'); + */ + /** + * @method array + * @memberof md5 + * @description Output hash as bytes array + * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash + * @returns {Array} Bytes array + * @example + * md5.array('The quick brown fox jumps over the lazy dog'); + */ + /** + * @method arrayBuffer + * @memberof md5 + * @description Output hash as ArrayBuffer + * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash + * @returns {ArrayBuffer} ArrayBuffer + * @example + * md5.arrayBuffer('The quick brown fox jumps over the lazy dog'); + */ + /** + * @method buffer + * @deprecated This maybe confuse with Buffer in node.js. Please use arrayBuffer instead. + * @memberof md5 + * @description Output hash as ArrayBuffer + * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash + * @returns {ArrayBuffer} ArrayBuffer + * @example + * md5.buffer('The quick brown fox jumps over the lazy dog'); + */ + /** + * @method base64 + * @memberof md5 + * @description Output hash as base64 string + * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash + * @returns {String} base64 string + * @example + * md5.base64('The quick brown fox jumps over the lazy dog'); + */ + var createOutputMethod = function (outputType) { + return function (message) { + return new Md5(true).update(message)[outputType](); + }; + }; + + /** + * @method create + * @memberof md5 + * @description Create Md5 object + * @returns {Md5} Md5 object. + * @example + * var hash = md5.create(); + */ + /** + * @method update + * @memberof md5 + * @description Create and update Md5 object + * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash + * @returns {Md5} Md5 object. + * @example + * var hash = md5.update('The quick brown fox jumps over the lazy dog'); + * // equal to + * var hash = md5.create(); + * hash.update('The quick brown fox jumps over the lazy dog'); + */ + var createMethod = function () { + var method = createOutputMethod('hex'); + method.create = function () { + return new Md5(); + }; + method.update = function (message) { + return method.create().update(message); + }; + for (var i = 0; i < OUTPUT_TYPES.length; ++i) { + var type = OUTPUT_TYPES[i]; + method[type] = createOutputMethod(type); + } + return method; + }; + + /** + * Md5 class + * @class Md5 + * @description This is internal class. + * @see {@link md5.create} + */ + function Md5(sharedMemory) { + if (sharedMemory) { + blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + this.blocks = blocks; + this.buffer8 = buffer8; + } else { + if (ARRAY_BUFFER) { + var buffer = new ArrayBuffer(68); + this.buffer8 = new Uint8Array(buffer); + this.blocks = new Uint32Array(buffer); + } else { + this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + } + } + this.h0 = this.h1 = this.h2 = this.h3 = this.start = this.bytes = this.hBytes = 0; + this.finalized = this.hashed = false; + this.first = true; + } + + /** + * @method update + * @memberof Md5 + * @instance + * @description Update hash + * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash + * @returns {Md5} Md5 object. + * @see {@link md5.update} + */ + Md5.prototype.update = function (message) { + if (this.finalized) { + return; + } + + var notString, type = typeof message; + if (type !== 'string') { + if (type === 'object') { + if (message === null) { + throw ERROR; + } else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) { + message = new Uint8Array(message); + } else if (!Array.isArray(message)) { + if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) { + throw ERROR; + } + } + } else { + throw ERROR; + } + notString = true; + } + var code, index = 0, i, length = message.length, blocks = this.blocks; + var buffer8 = this.buffer8; + + while (index < length) { + if (this.hashed) { + this.hashed = false; + blocks[0] = blocks[16]; + blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + } + + if (notString) { + if (ARRAY_BUFFER) { + for (i = this.start; index < length && i < 64; ++index) { + buffer8[i++] = message[index]; + } + } else { + for (i = this.start; index < length && i < 64; ++index) { + blocks[i >> 2] |= message[index] << SHIFT[i++ & 3]; + } + } + } else { + if (ARRAY_BUFFER) { + for (i = this.start; index < length && i < 64; ++index) { + code = message.charCodeAt(index); + if (code < 0x80) { + buffer8[i++] = code; + } else if (code < 0x800) { + buffer8[i++] = 0xc0 | (code >> 6); + buffer8[i++] = 0x80 | (code & 0x3f); + } else if (code < 0xd800 || code >= 0xe000) { + buffer8[i++] = 0xe0 | (code >> 12); + buffer8[i++] = 0x80 | ((code >> 6) & 0x3f); + buffer8[i++] = 0x80 | (code & 0x3f); + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); + buffer8[i++] = 0xf0 | (code >> 18); + buffer8[i++] = 0x80 | ((code >> 12) & 0x3f); + buffer8[i++] = 0x80 | ((code >> 6) & 0x3f); + buffer8[i++] = 0x80 | (code & 0x3f); + } + } + } else { + for (i = this.start; index < length && i < 64; ++index) { + code = message.charCodeAt(index); + if (code < 0x80) { + blocks[i >> 2] |= code << SHIFT[i++ & 3]; + } else if (code < 0x800) { + blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else if (code < 0xd800 || code >= 0xe000) { + blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); + blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } + } + } + } + this.lastByteIndex = i; + this.bytes += i - this.start; + if (i >= 64) { + this.start = i - 64; + this.hash(); + this.hashed = true; + } else { + this.start = i; + } + } + if (this.bytes > 4294967295) { + this.hBytes += this.bytes / 4294967296 << 0; + this.bytes = this.bytes % 4294967296; + } + return this; + }; + + Md5.prototype.finalize = function () { + if (this.finalized) { + return; + } + this.finalized = true; + var blocks = this.blocks, i = this.lastByteIndex; + blocks[i >> 2] |= EXTRA[i & 3]; + if (i >= 56) { + if (!this.hashed) { + this.hash(); + } + blocks[0] = blocks[16]; + blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + } + blocks[14] = this.bytes << 3; + blocks[15] = this.hBytes << 3 | this.bytes >>> 29; + this.hash(); + }; + + Md5.prototype.hash = function () { + var a, b, c, d, bc, da, blocks = this.blocks; + + if (this.first) { + a = blocks[0] - 680876937; + a = (a << 7 | a >>> 25) - 271733879 << 0; + d = (-1732584194 ^ a & 2004318071) + blocks[1] - 117830708; + d = (d << 12 | d >>> 20) + a << 0; + c = (-271733879 ^ (d & (a ^ -271733879))) + blocks[2] - 1126478375; + c = (c << 17 | c >>> 15) + d << 0; + b = (a ^ (c & (d ^ a))) + blocks[3] - 1316259209; + b = (b << 22 | b >>> 10) + c << 0; + } else { + a = this.h0; + b = this.h1; + c = this.h2; + d = this.h3; + a += (d ^ (b & (c ^ d))) + blocks[0] - 680876936; + a = (a << 7 | a >>> 25) + b << 0; + d += (c ^ (a & (b ^ c))) + blocks[1] - 389564586; + d = (d << 12 | d >>> 20) + a << 0; + c += (b ^ (d & (a ^ b))) + blocks[2] + 606105819; + c = (c << 17 | c >>> 15) + d << 0; + b += (a ^ (c & (d ^ a))) + blocks[3] - 1044525330; + b = (b << 22 | b >>> 10) + c << 0; + } + + a += (d ^ (b & (c ^ d))) + blocks[4] - 176418897; + a = (a << 7 | a >>> 25) + b << 0; + d += (c ^ (a & (b ^ c))) + blocks[5] + 1200080426; + d = (d << 12 | d >>> 20) + a << 0; + c += (b ^ (d & (a ^ b))) + blocks[6] - 1473231341; + c = (c << 17 | c >>> 15) + d << 0; + b += (a ^ (c & (d ^ a))) + blocks[7] - 45705983; + b = (b << 22 | b >>> 10) + c << 0; + a += (d ^ (b & (c ^ d))) + blocks[8] + 1770035416; + a = (a << 7 | a >>> 25) + b << 0; + d += (c ^ (a & (b ^ c))) + blocks[9] - 1958414417; + d = (d << 12 | d >>> 20) + a << 0; + c += (b ^ (d & (a ^ b))) + blocks[10] - 42063; + c = (c << 17 | c >>> 15) + d << 0; + b += (a ^ (c & (d ^ a))) + blocks[11] - 1990404162; + b = (b << 22 | b >>> 10) + c << 0; + a += (d ^ (b & (c ^ d))) + blocks[12] + 1804603682; + a = (a << 7 | a >>> 25) + b << 0; + d += (c ^ (a & (b ^ c))) + blocks[13] - 40341101; + d = (d << 12 | d >>> 20) + a << 0; + c += (b ^ (d & (a ^ b))) + blocks[14] - 1502002290; + c = (c << 17 | c >>> 15) + d << 0; + b += (a ^ (c & (d ^ a))) + blocks[15] + 1236535329; + b = (b << 22 | b >>> 10) + c << 0; + a += (c ^ (d & (b ^ c))) + blocks[1] - 165796510; + a = (a << 5 | a >>> 27) + b << 0; + d += (b ^ (c & (a ^ b))) + blocks[6] - 1069501632; + d = (d << 9 | d >>> 23) + a << 0; + c += (a ^ (b & (d ^ a))) + blocks[11] + 643717713; + c = (c << 14 | c >>> 18) + d << 0; + b += (d ^ (a & (c ^ d))) + blocks[0] - 373897302; + b = (b << 20 | b >>> 12) + c << 0; + a += (c ^ (d & (b ^ c))) + blocks[5] - 701558691; + a = (a << 5 | a >>> 27) + b << 0; + d += (b ^ (c & (a ^ b))) + blocks[10] + 38016083; + d = (d << 9 | d >>> 23) + a << 0; + c += (a ^ (b & (d ^ a))) + blocks[15] - 660478335; + c = (c << 14 | c >>> 18) + d << 0; + b += (d ^ (a & (c ^ d))) + blocks[4] - 405537848; + b = (b << 20 | b >>> 12) + c << 0; + a += (c ^ (d & (b ^ c))) + blocks[9] + 568446438; + a = (a << 5 | a >>> 27) + b << 0; + d += (b ^ (c & (a ^ b))) + blocks[14] - 1019803690; + d = (d << 9 | d >>> 23) + a << 0; + c += (a ^ (b & (d ^ a))) + blocks[3] - 187363961; + c = (c << 14 | c >>> 18) + d << 0; + b += (d ^ (a & (c ^ d))) + blocks[8] + 1163531501; + b = (b << 20 | b >>> 12) + c << 0; + a += (c ^ (d & (b ^ c))) + blocks[13] - 1444681467; + a = (a << 5 | a >>> 27) + b << 0; + d += (b ^ (c & (a ^ b))) + blocks[2] - 51403784; + d = (d << 9 | d >>> 23) + a << 0; + c += (a ^ (b & (d ^ a))) + blocks[7] + 1735328473; + c = (c << 14 | c >>> 18) + d << 0; + b += (d ^ (a & (c ^ d))) + blocks[12] - 1926607734; + b = (b << 20 | b >>> 12) + c << 0; + bc = b ^ c; + a += (bc ^ d) + blocks[5] - 378558; + a = (a << 4 | a >>> 28) + b << 0; + d += (bc ^ a) + blocks[8] - 2022574463; + d = (d << 11 | d >>> 21) + a << 0; + da = d ^ a; + c += (da ^ b) + blocks[11] + 1839030562; + c = (c << 16 | c >>> 16) + d << 0; + b += (da ^ c) + blocks[14] - 35309556; + b = (b << 23 | b >>> 9) + c << 0; + bc = b ^ c; + a += (bc ^ d) + blocks[1] - 1530992060; + a = (a << 4 | a >>> 28) + b << 0; + d += (bc ^ a) + blocks[4] + 1272893353; + d = (d << 11 | d >>> 21) + a << 0; + da = d ^ a; + c += (da ^ b) + blocks[7] - 155497632; + c = (c << 16 | c >>> 16) + d << 0; + b += (da ^ c) + blocks[10] - 1094730640; + b = (b << 23 | b >>> 9) + c << 0; + bc = b ^ c; + a += (bc ^ d) + blocks[13] + 681279174; + a = (a << 4 | a >>> 28) + b << 0; + d += (bc ^ a) + blocks[0] - 358537222; + d = (d << 11 | d >>> 21) + a << 0; + da = d ^ a; + c += (da ^ b) + blocks[3] - 722521979; + c = (c << 16 | c >>> 16) + d << 0; + b += (da ^ c) + blocks[6] + 76029189; + b = (b << 23 | b >>> 9) + c << 0; + bc = b ^ c; + a += (bc ^ d) + blocks[9] - 640364487; + a = (a << 4 | a >>> 28) + b << 0; + d += (bc ^ a) + blocks[12] - 421815835; + d = (d << 11 | d >>> 21) + a << 0; + da = d ^ a; + c += (da ^ b) + blocks[15] + 530742520; + c = (c << 16 | c >>> 16) + d << 0; + b += (da ^ c) + blocks[2] - 995338651; + b = (b << 23 | b >>> 9) + c << 0; + a += (c ^ (b | ~d)) + blocks[0] - 198630844; + a = (a << 6 | a >>> 26) + b << 0; + d += (b ^ (a | ~c)) + blocks[7] + 1126891415; + d = (d << 10 | d >>> 22) + a << 0; + c += (a ^ (d | ~b)) + blocks[14] - 1416354905; + c = (c << 15 | c >>> 17) + d << 0; + b += (d ^ (c | ~a)) + blocks[5] - 57434055; + b = (b << 21 | b >>> 11) + c << 0; + a += (c ^ (b | ~d)) + blocks[12] + 1700485571; + a = (a << 6 | a >>> 26) + b << 0; + d += (b ^ (a | ~c)) + blocks[3] - 1894986606; + d = (d << 10 | d >>> 22) + a << 0; + c += (a ^ (d | ~b)) + blocks[10] - 1051523; + c = (c << 15 | c >>> 17) + d << 0; + b += (d ^ (c | ~a)) + blocks[1] - 2054922799; + b = (b << 21 | b >>> 11) + c << 0; + a += (c ^ (b | ~d)) + blocks[8] + 1873313359; + a = (a << 6 | a >>> 26) + b << 0; + d += (b ^ (a | ~c)) + blocks[15] - 30611744; + d = (d << 10 | d >>> 22) + a << 0; + c += (a ^ (d | ~b)) + blocks[6] - 1560198380; + c = (c << 15 | c >>> 17) + d << 0; + b += (d ^ (c | ~a)) + blocks[13] + 1309151649; + b = (b << 21 | b >>> 11) + c << 0; + a += (c ^ (b | ~d)) + blocks[4] - 145523070; + a = (a << 6 | a >>> 26) + b << 0; + d += (b ^ (a | ~c)) + blocks[11] - 1120210379; + d = (d << 10 | d >>> 22) + a << 0; + c += (a ^ (d | ~b)) + blocks[2] + 718787259; + c = (c << 15 | c >>> 17) + d << 0; + b += (d ^ (c | ~a)) + blocks[9] - 343485551; + b = (b << 21 | b >>> 11) + c << 0; + + if (this.first) { + this.h0 = a + 1732584193 << 0; + this.h1 = b - 271733879 << 0; + this.h2 = c - 1732584194 << 0; + this.h3 = d + 271733878 << 0; + this.first = false; + } else { + this.h0 = this.h0 + a << 0; + this.h1 = this.h1 + b << 0; + this.h2 = this.h2 + c << 0; + this.h3 = this.h3 + d << 0; + } + }; + + /** + * @method hex + * @memberof Md5 + * @instance + * @description Output hash as hex string + * @returns {String} Hex string + * @see {@link md5.hex} + * @example + * hash.hex(); + */ + Md5.prototype.hex = function () { + this.finalize(); + + var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3; + + return HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] + + HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] + + HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] + + HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] + + HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] + + HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] + + HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] + + HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] + + HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] + + HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] + + HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] + + HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] + + HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] + + HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] + + HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] + + HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F]; + }; + + /** + * @method toString + * @memberof Md5 + * @instance + * @description Output hash as hex string + * @returns {String} Hex string + * @see {@link md5.hex} + * @example + * hash.toString(); + */ + Md5.prototype.toString = Md5.prototype.hex; + + /** + * @method digest + * @memberof Md5 + * @instance + * @description Output hash as bytes array + * @returns {Array} Bytes array + * @see {@link md5.digest} + * @example + * hash.digest(); + */ + Md5.prototype.digest = function () { + this.finalize(); + + var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3; + return [ + h0 & 0xFF, (h0 >> 8) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 24) & 0xFF, + h1 & 0xFF, (h1 >> 8) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 24) & 0xFF, + h2 & 0xFF, (h2 >> 8) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 24) & 0xFF, + h3 & 0xFF, (h3 >> 8) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 24) & 0xFF + ]; + }; + + /** + * @method array + * @memberof Md5 + * @instance + * @description Output hash as bytes array + * @returns {Array} Bytes array + * @see {@link md5.array} + * @example + * hash.array(); + */ + Md5.prototype.array = Md5.prototype.digest; + + /** + * @method arrayBuffer + * @memberof Md5 + * @instance + * @description Output hash as ArrayBuffer + * @returns {ArrayBuffer} ArrayBuffer + * @see {@link md5.arrayBuffer} + * @example + * hash.arrayBuffer(); + */ + Md5.prototype.arrayBuffer = function () { + this.finalize(); + + var buffer = new ArrayBuffer(16); + var blocks = new Uint32Array(buffer); + blocks[0] = this.h0; + blocks[1] = this.h1; + blocks[2] = this.h2; + blocks[3] = this.h3; + return buffer; + }; + + /** + * @method buffer + * @deprecated This maybe confuse with Buffer in node.js. Please use arrayBuffer instead. + * @memberof Md5 + * @instance + * @description Output hash as ArrayBuffer + * @returns {ArrayBuffer} ArrayBuffer + * @see {@link md5.buffer} + * @example + * hash.buffer(); + */ + Md5.prototype.buffer = Md5.prototype.arrayBuffer; + + /** + * @method base64 + * @memberof Md5 + * @instance + * @description Output hash as base64 string + * @returns {String} base64 string + * @see {@link md5.base64} + * @example + * hash.base64(); + */ + Md5.prototype.base64 = function () { + var v1, v2, v3, base64Str = '', bytes = this.array(); + for (var i = 0; i < 15;) { + v1 = bytes[i++]; + v2 = bytes[i++]; + v3 = bytes[i++]; + base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] + + BASE64_ENCODE_CHAR[(v1 << 4 | v2 >>> 4) & 63] + + BASE64_ENCODE_CHAR[(v2 << 2 | v3 >>> 6) & 63] + + BASE64_ENCODE_CHAR[v3 & 63]; + } + v1 = bytes[i]; + base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] + + BASE64_ENCODE_CHAR[(v1 << 4) & 63] + + '=='; + return base64Str; + }; + + window.md5 = createMethod(); +})(); diff --git a/include/pio/static/style.css b/include/pio/static/style.css new file mode 100644 index 0000000..32bd02c --- /dev/null +++ b/include/pio/static/style.css @@ -0,0 +1,85 @@ +body, html { + padding: 0; + margin: 0; +} +body, button, input[type="text"], input[type="password"] { + font-size: 16px; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif; +} + +.title { + padding: 10px 10px 6px; + font-weight: 600; + background-color: #eff2f5; + border-bottom: 1px #d9e0e7 solid; + color: #276eb4; + font-size: 15px; +} +.block { + padding: 10px; +} +.full-width { + width: 100%; + box-sizing: border-box; +} + +.form_label { + padding: 0 0 3px; + font-weight: 600; +} +.form_input { + margin-bottom: 15px; +} +.form_sublabel { + padding-top: 3px; +} + +input[type="text"], +input[type="password"], +select { + border-radius: 4px; + border: 1px #c9cccf solid; + padding: 7px 9px; + outline: none; +} +input[type="text"]:focus, +input[type="password"]:focus, +select:focus { + box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); +} +input[type="text"]:disabled, +input[type="password"]:disabled, +select:disabled { + background-color: #f1f2f3; + border-color: #f1f2f3; +} + +button { + border-radius: 4px; + border: 1px #c9cccf solid; + padding: 7px 15px; + outline: none; + background: #fff; + color: #000; /* fix for iOS */ + position: relative; + line-height: 18px; + font-weight: 600; +} +button:disabled { + background-color: #f1f2f3; + border-color: #f1f2f3; +} +button:not(:disabled):hover { + box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); + cursor: pointer; + border-color: #b5cce3; + color: #276eb4; +} +button:not(:disabled):active { + top: 1px; +} + +button.is_reset, +button.is_reset:not(:disabled):hover { + color: #e63917; +}
\ No newline at end of file |