mqtt, esp: add new esp8266-based device
This commit is contained in:
parent
586d84b0c0
commit
0aba139aef
@ -3,11 +3,12 @@
|
||||
#set -x
|
||||
#set -e
|
||||
|
||||
DIR="$(dirname "$(realpath "$0")")"
|
||||
COMMON_DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)"
|
||||
PROJECT_DIR="$(pwd)"
|
||||
|
||||
fw_version="$(cat "$DIR/src/config.def.h" | grep "^#define FW_VERSION" | awk '{print $3}')"
|
||||
header="$DIR/src/static.h"
|
||||
source="$DIR/src/static.cpp"
|
||||
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"
|
||||
@ -19,7 +20,7 @@ is_minifyable() {
|
||||
|
||||
minify() {
|
||||
local ext="$1"
|
||||
local bin="$(realpath "$DIR"/../../tools/minify.js)"
|
||||
local bin="$(realpath "$COMMON_DIR"/../../tools/minify.js)"
|
||||
"$bin" --type "$ext"
|
||||
}
|
||||
|
||||
@ -55,7 +56,7 @@ EOF
|
||||
|
||||
# loop over files
|
||||
for ext in html js css ico; do
|
||||
for f in "$DIR"/static/*.$ext; do
|
||||
for f in "$COMMON_DIR"/static/*.$ext; do
|
||||
filename="$(basename "$f")"
|
||||
echo "processing ${filename}..."
|
||||
filename="${filename/./_}"
|
@ -144,7 +144,7 @@ function initNetworkSettings() {
|
||||
if (error)
|
||||
throw error;
|
||||
|
||||
setupField(form.hid, response.home_id || null);
|
||||
setupField(form.hid, response.node_id || null);
|
||||
setupField(form.psk, null);
|
||||
setupField(form.submit, null);
|
||||
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
@ -140,8 +140,8 @@ void loop() {
|
||||
|
||||
if (mqtt->ota.readyToRestart) {
|
||||
mqtt->disconnect();
|
||||
} else if (mqtt->statStopWatch.elapsed(10000)) {
|
||||
mqtt->sendStat();
|
||||
} else if (mqtt->diagnosticsStopWatch.elapsed(10000)) {
|
||||
mqtt->sendDiagnostics();
|
||||
}
|
||||
|
||||
#if MQTT_BLINK
|
||||
|
@ -19,8 +19,8 @@ static const char MQTT_PASSWORD[] = DEFAULT_MQTT_PASSWORD;
|
||||
static const char MQTT_CLIENT_ID[] = DEFAULT_MQTT_CLIENT_ID;
|
||||
static const char MQTT_SECRET[HOME_SECRET_SIZE+1] = HOME_SECRET;
|
||||
|
||||
static const char TOPIC_STAT[] = "stat";
|
||||
static const char TOPIC_INITIAL_STAT[] = "stat1";
|
||||
static const char TOPIC_DIAGNOSTICS[] = "stat";
|
||||
static const char TOPIC_INITIAL_DIAGNOSTICS[] = "stat1";
|
||||
static const char TOPIC_OTA_RESPONSE[] = "otares";
|
||||
static const char TOPIC_RELAY_POWER[] = "power";
|
||||
static const char TOPIC_ADMIN_OTA[] = "admin/ota";
|
||||
@ -45,7 +45,7 @@ MQTT::MQTT() {
|
||||
client.onConnect([&](bool sessionPresent) {
|
||||
PRINTLN("mqtt: connected");
|
||||
|
||||
sendInitialStat();
|
||||
sendInitialDiagnostics();
|
||||
|
||||
subscribe(TOPIC_RELAY_POWER, 1);
|
||||
subscribe(TOPIC_ADMIN_OTA);
|
||||
@ -174,36 +174,36 @@ uint16_t MQTT::subscribe(const String &topic, uint8_t qos) {
|
||||
return packetId;
|
||||
}
|
||||
|
||||
void MQTT::sendInitialStat() {
|
||||
void MQTT::sendInitialDiagnostics() {
|
||||
auto cfg = config::read();
|
||||
InitialStatPayload stat{
|
||||
InitialDiagnosticsPayload stat{
|
||||
.ip = wifi::getIPAsInteger(),
|
||||
.fw_version = FW_VERSION,
|
||||
.rssi = wifi::getRSSI(),
|
||||
.free_heap = ESP.getFreeHeap(),
|
||||
.flags = StatFlags{
|
||||
.flags = DiagnosticsFlags{
|
||||
.state = static_cast<uint8_t>(relay::getState() ? 1 : 0),
|
||||
.config_changed_value_present = 1,
|
||||
.config_changed = static_cast<uint8_t>(cfg.flags.node_configured ||
|
||||
cfg.flags.wifi_configured ? 1 : 0)
|
||||
}
|
||||
};
|
||||
publish(TOPIC_INITIAL_STAT, reinterpret_cast<uint8_t*>(&stat), sizeof(stat));
|
||||
statStopWatch.save();
|
||||
publish(TOPIC_INITIAL_DIAGNOSTICS, reinterpret_cast<uint8_t*>(&stat), sizeof(stat));
|
||||
diagnosticsStopWatch.save();
|
||||
}
|
||||
|
||||
void MQTT::sendStat() {
|
||||
StatPayload stat{
|
||||
void MQTT::sendDiagnostics() {
|
||||
DiagnosticsPayload stat{
|
||||
.rssi = wifi::getRSSI(),
|
||||
.free_heap = ESP.getFreeHeap(),
|
||||
.flags = StatFlags{
|
||||
.flags = DiagnosticsFlags{
|
||||
.state = static_cast<uint8_t>(relay::getState() ? 1 : 0),
|
||||
.config_changed_value_present = 0,
|
||||
.config_changed = 0
|
||||
}
|
||||
};
|
||||
publish(TOPIC_STAT, reinterpret_cast<uint8_t*>(&stat), sizeof(stat));
|
||||
statStopWatch.save();
|
||||
publish(TOPIC_DIAGNOSTICS, reinterpret_cast<uint8_t*>(&stat), sizeof(stat));
|
||||
diagnosticsStopWatch.save();
|
||||
}
|
||||
|
||||
uint16_t MQTT::sendOtaResponse(OTAResult status, uint8_t error_code) {
|
||||
@ -237,7 +237,7 @@ void MQTT::handleRelayPowerPayload(const uint8_t *payload, uint32_t length) {
|
||||
PRINTLN("error: unexpected state value");
|
||||
}
|
||||
|
||||
sendStat();
|
||||
sendDiagnostics();
|
||||
}
|
||||
|
||||
void MQTT::handleAdminOtaPayload(uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total) {
|
||||
|
@ -52,11 +52,11 @@ private:
|
||||
|
||||
uint16_t publish(const String& topic, uint8_t* payload, size_t length);
|
||||
uint16_t subscribe(const String& topic, uint8_t qos = 0);
|
||||
void sendInitialStat();
|
||||
void sendInitialDiagnostics();
|
||||
uint16_t sendOtaResponse(OTAResult status, uint8_t error_code = 0);
|
||||
|
||||
public:
|
||||
StopWatch statStopWatch;
|
||||
StopWatch diagnosticsStopWatch;
|
||||
OTAStatus ota;
|
||||
|
||||
MQTT();
|
||||
@ -64,28 +64,28 @@ public:
|
||||
void disconnect();
|
||||
void reconnect();
|
||||
void loop();
|
||||
void sendStat();
|
||||
void sendDiagnostics();
|
||||
};
|
||||
|
||||
struct StatFlags {
|
||||
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 InitialStatPayload {
|
||||
struct InitialDiagnosticsPayload {
|
||||
uint32_t ip;
|
||||
uint8_t fw_version;
|
||||
int8_t rssi;
|
||||
uint32_t free_heap;
|
||||
StatFlags flags;
|
||||
DiagnosticsFlags flags;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct StatPayload {
|
||||
struct DiagnosticsPayload {
|
||||
int8_t rssi;
|
||||
uint32_t free_heap;
|
||||
StatFlags flags;
|
||||
DiagnosticsFlags flags;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PowerPayload {
|
||||
|
3
platformio/temphum/.gitignore
vendored
Normal file
3
platformio/temphum/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.pio
|
||||
CMakeListsPrivate.txt
|
||||
cmake-build-*/
|
33
platformio/temphum/CMakeLists.txt
Normal file
33
platformio/temphum/CMakeLists.txt
Normal file
@ -0,0 +1,33 @@
|
||||
# !!! WARNING !!! AUTO-GENERATED FILE, PLEASE DO NOT MODIFY IT AND USE
|
||||
# https://docs.platformio.org/page/projectconf/section_env_build.html#build-flags
|
||||
#
|
||||
# If you need to override existing CMake configuration or add extra,
|
||||
# please create `CMakeListsUser.txt` in the root of project.
|
||||
# The `CMakeListsUser.txt` will not be overwritten by PlatformIO.
|
||||
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
set(CMAKE_SYSTEM_NAME Generic)
|
||||
set(CMAKE_C_COMPILER_WORKS 1)
|
||||
set(CMAKE_CXX_COMPILER_WORKS 1)
|
||||
|
||||
project("temphum" C CXX)
|
||||
|
||||
include(CMakeListsPrivate.txt)
|
||||
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/CMakeListsUser.txt)
|
||||
include(CMakeListsUser.txt)
|
||||
endif()
|
||||
|
||||
add_custom_target(
|
||||
Production ALL
|
||||
COMMAND platformio -c clion run "$<$<NOT:$<CONFIG:All>>:-e${CMAKE_BUILD_TYPE}>"
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
add_custom_target(
|
||||
Debug ALL
|
||||
COMMAND platformio -c clion debug "$<$<NOT:$<CONFIG:All>>:-e${CMAKE_BUILD_TYPE}>"
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
add_executable(Z_DUMMY_TARGET ${SRC_LIST})
|
23
platformio/temphum/platformio.ini
Normal file
23
platformio/temphum/platformio.ini
Normal file
@ -0,0 +1,23 @@
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[env:esp12e]
|
||||
platform = espressif8266
|
||||
board = esp12e
|
||||
framework = arduino
|
||||
upload_port = /dev/ttyUSB0
|
||||
monitor_speed = 115200
|
||||
lib_deps =
|
||||
https://github.com/bertmelis/espMqttClient#unordered-acks
|
||||
;build_flags =
|
||||
; -DDEBUG
|
||||
; -DDEBUG_ESP_SSL
|
||||
; -DDEBUG_ESP_PORT=Serial
|
||||
build_type = release
|
84
platformio/temphum/src/config.cpp
Normal file
84
platformio/temphum/src/config.cpp
Normal file
@ -0,0 +1,84 @@
|
||||
#include <EEPROM.h>
|
||||
#include <strings.h>
|
||||
#include "config.h"
|
||||
#include "logging.h"
|
||||
|
||||
#define GET_DATA_CRC(data) \
|
||||
eeprom_crc(reinterpret_cast<uint8_t*>(&(data))+4, sizeof(ConfigData)-4)
|
||||
|
||||
namespace homekit::config {
|
||||
|
||||
static const uint32_t magic = 0xdeadbeef;
|
||||
static const uint32_t crc_table[16] PROGMEM = {
|
||||
0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,
|
||||
0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
|
||||
0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,
|
||||
0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c
|
||||
};
|
||||
|
||||
static uint32_t eeprom_crc(const uint8_t* data, size_t len) {
|
||||
uint32_t crc = ~0L;
|
||||
for (size_t index = 0; index < len; index++) {
|
||||
crc = pgm_read_word(&crc_table[(crc ^ data[index]) & 0x0f]) ^ (crc >> 4);
|
||||
crc = pgm_read_word(&crc_table[(crc ^ (data[index] >> 4)) & 0x0f]) ^ (crc >> 4);
|
||||
crc = ~crc;
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
ConfigData read() {
|
||||
ConfigData data;
|
||||
EEPROM.begin(sizeof(ConfigData));
|
||||
EEPROM.get(0, data);
|
||||
EEPROM.end();
|
||||
#ifdef DEBUG
|
||||
if (!isValid(data)) {
|
||||
PRINTLN("config::read(): data is not valid!");
|
||||
}
|
||||
#endif
|
||||
return data;
|
||||
}
|
||||
|
||||
void write(ConfigData& data) {
|
||||
EEPROM.begin(sizeof(ConfigData));
|
||||
data.magic = magic;
|
||||
data.crc = GET_DATA_CRC(data);
|
||||
EEPROM.put(0, data);
|
||||
EEPROM.end();
|
||||
}
|
||||
|
||||
void erase() {
|
||||
ConfigData data;
|
||||
erase(data);
|
||||
}
|
||||
|
||||
void erase(ConfigData& data) {
|
||||
bzero(reinterpret_cast<uint8_t*>(&data), sizeof(data));
|
||||
write(data);
|
||||
}
|
||||
|
||||
bool isValid(ConfigData& data) {
|
||||
return data.crc == GET_DATA_CRC(data);
|
||||
}
|
||||
|
||||
bool isDirty(ConfigData& data) {
|
||||
return data.magic != magic;
|
||||
}
|
||||
|
||||
char* ConfigData::escapeHomeId(char* buf, size_t len) {
|
||||
if (len < 32)
|
||||
return nullptr;
|
||||
size_t id_len = strlen(node_id);
|
||||
char* c = node_id;
|
||||
char* dst = buf;
|
||||
for (size_t i = 0; i < id_len; i++) {
|
||||
if (*c == '"')
|
||||
*(dst++) = '\\';
|
||||
*(dst++) = *c;
|
||||
c++;
|
||||
}
|
||||
*dst = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
}
|
33
platformio/temphum/src/config.def.h.example
Normal file
33
platformio/temphum/src/config.def.h.example
Normal file
@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#define FW_VERSION 7
|
||||
|
||||
#define DEFAULT_WIFI_AP_SSID ""
|
||||
#define DEFAULT_WIFI_STA_SSID ""
|
||||
#define DEFAULT_WIFI_STA_PSK ""
|
||||
|
||||
#define DEFAULT_MQTT_SERVER "mqtt.solarmon.ru"
|
||||
#define DEFAULT_MQTT_PORT 8883
|
||||
#define DEFAULT_MQTT_USERNAME ""
|
||||
#define DEFAULT_MQTT_PASSWORD ""
|
||||
#define DEFAULT_MQTT_CLIENT_ID ""
|
||||
#define DEFAULT_MQTT_CA_FINGERPRINT { \
|
||||
0x0e, 0xb6, 0x3a, 0x02, 0x1f, \
|
||||
0x4e, 0x1e, 0xe1, 0x6a, 0x67, \
|
||||
0x62, 0xec, 0x64, 0xd4, 0x84, \
|
||||
0x8a, 0xb0, 0xc9, 0x9c, 0xbb \
|
||||
};
|
||||
|
||||
#define DEFAULT_NODE_ID "relay-node"
|
||||
|
||||
#define FLASH_BUTTON_PIN 0
|
||||
#define ESP_LED_PIN 2
|
||||
#define BOARD_LED_PIN 16
|
||||
#define RELAY_PIN 5
|
||||
|
||||
// 12 bytes string
|
||||
#define HOME_SECRET_SIZE 12
|
||||
#define HOME_SECRET ""
|
||||
#define MQTT_BLINK 1
|
||||
|
||||
#define ARRAY_SIZE(X) sizeof((X))/sizeof((X)[0])
|
34
platformio/temphum/src/config.h
Normal file
34
platformio/temphum/src/config.h
Normal file
@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace homekit::config {
|
||||
|
||||
struct ConfigFlags {
|
||||
uint8_t wifi_configured: 1;
|
||||
uint8_t node_configured: 1;
|
||||
uint8_t reserved: 6;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct ConfigData {
|
||||
// helpers
|
||||
uint32_t crc = 0;
|
||||
uint32_t magic = 0;
|
||||
char node_id[16] = {0};
|
||||
char wifi_ssid[32] = {0};
|
||||
char wifi_psk[63] = {0};
|
||||
ConfigFlags flags {0};
|
||||
|
||||
// helper methods
|
||||
char* escapeHomeId(char* buf, size_t len);
|
||||
} __attribute__((packed));
|
||||
|
||||
|
||||
ConfigData read();
|
||||
void write(ConfigData& data);
|
||||
void erase();
|
||||
void erase(ConfigData& data);
|
||||
bool isValid(ConfigData& data);
|
||||
bool isDirty(ConfigData& data);
|
||||
|
||||
}
|
279
platformio/temphum/src/http_server.cpp
Normal file
279
platformio/temphum/src/http_server.cpp
Normal file
@ -0,0 +1,279 @@
|
||||
#include <Arduino.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "static.h"
|
||||
#include "http_server.h"
|
||||
#include "config.h"
|
||||
#include "config.def.h"
|
||||
#include "logging.h"
|
||||
#include "util.h"
|
||||
#include "led.h"
|
||||
|
||||
namespace homekit {
|
||||
|
||||
using files::StaticFile;
|
||||
|
||||
static const char CONTENT_TYPE_HTML[] PROGMEM = "text/html; charset=utf-8";
|
||||
static const char CONTENT_TYPE_CSS[] PROGMEM = "text/css";
|
||||
static const char CONTENT_TYPE_JS[] PROGMEM = "application/javascript";
|
||||
static const char CONTENT_TYPE_JSON[] PROGMEM = "application/json";
|
||||
static const char CONTENT_TYPE_FAVICON[] PROGMEM = "image/x-icon";
|
||||
|
||||
static const char JSON_UPDATE_FMT[] PROGMEM = "{\"result\":%d}";
|
||||
static const char JSON_STATUS_FMT[] PROGMEM = "{\"node_id\":\"%s\""
|
||||
#ifdef DEBUG
|
||||
",\"configured\":%d"
|
||||
",\"crc\":%u"
|
||||
",\"fl_n\":%d"
|
||||
",\"fl_w\":%d"
|
||||
#endif
|
||||
"}";
|
||||
static const size_t JSON_BUF_SIZE = 192;
|
||||
|
||||
static const char JSON_SCAN_FIRST_LIST[] PROGMEM = "{\"list\":[";
|
||||
|
||||
static const char MSG_IS_INVALID[] PROGMEM = " is invalid";
|
||||
static const char MSG_IS_MISSING[] PROGMEM = " is missing";
|
||||
|
||||
static const char GZIP[] PROGMEM = "gzip";
|
||||
static const char CONTENT_ENCODING[] PROGMEM = "Content-Encoding";
|
||||
static const char NOT_FOUND[] PROGMEM = "Not Found";
|
||||
|
||||
static const char ROUTE_STYLE_CSS[] PROGMEM = "/style.css";
|
||||
static const char ROUTE_APP_JS[] PROGMEM = "/app.js";
|
||||
static const char ROUTE_MD5_JS[] PROGMEM = "/md5.js";
|
||||
static const char ROUTE_FAVICON_ICO[] PROGMEM = "/favicon.ico";
|
||||
static const char ROUTE_STATUS[] PROGMEM = "/status";
|
||||
static const char ROUTE_SCAN[] PROGMEM = "/scan";
|
||||
static const char ROUTE_RESET[] PROGMEM = "/reset";
|
||||
// #ifdef DEBUG
|
||||
static const char ROUTE_HEAP[] PROGMEM = "/heap";
|
||||
// #endif
|
||||
static const char ROUTE_UPDATE[] PROGMEM = "/update";
|
||||
|
||||
void HttpServer::start() {
|
||||
server.on(FPSTR(ROUTE_STYLE_CSS), HTTP_GET, [&]() { sendGzip(files::style_css, CONTENT_TYPE_CSS); });
|
||||
server.on(FPSTR(ROUTE_APP_JS), HTTP_GET, [&]() { sendGzip(files::app_js, CONTENT_TYPE_JS); });
|
||||
server.on(FPSTR(ROUTE_MD5_JS), HTTP_GET, [&]() { sendGzip(files::md5_js, CONTENT_TYPE_JS); });
|
||||
server.on(FPSTR(ROUTE_FAVICON_ICO), HTTP_GET, [&]() { sendGzip(files::favicon_ico, CONTENT_TYPE_FAVICON); });
|
||||
|
||||
server.on("/", HTTP_GET, [&]() { sendGzip(files::index_html, CONTENT_TYPE_HTML); });
|
||||
server.on(FPSTR(ROUTE_STATUS), HTTP_GET, [&]() {
|
||||
char json_buf[JSON_BUF_SIZE];
|
||||
auto cfg = config::read();
|
||||
|
||||
if (!isValid(cfg) || !cfg.flags.node_configured) {
|
||||
sprintf_P(json_buf, JSON_STATUS_FMT
|
||||
, DEFAULT_NODE_ID
|
||||
#ifdef DEBUG
|
||||
, 0
|
||||
, cfg.crc
|
||||
, cfg.flags.node_configured
|
||||
, cfg.flags.wifi_configured
|
||||
#endif
|
||||
);
|
||||
} else {
|
||||
char escaped_node_id[32];
|
||||
char *escaped_node_id_res = cfg.escapeHomeId(escaped_node_id, 32);
|
||||
sprintf_P(json_buf, JSON_STATUS_FMT
|
||||
, escaped_node_id_res == nullptr ? "?" : escaped_node_id
|
||||
#ifdef DEBUG
|
||||
, 1
|
||||
, cfg.crc
|
||||
, cfg.flags.node_configured
|
||||
, cfg.flags.wifi_configured
|
||||
#endif
|
||||
);
|
||||
}
|
||||
server.send(200, FPSTR(CONTENT_TYPE_JSON), json_buf);
|
||||
});
|
||||
server.on(FPSTR(ROUTE_STATUS), HTTP_POST, [&]() {
|
||||
auto cfg = config::read();
|
||||
String s;
|
||||
|
||||
if (!getInputParam("ssid", 32, s)) return;
|
||||
strncpy(cfg.wifi_ssid, s.c_str(), 32);
|
||||
PRINTF("saving ssid: %s\n", cfg.wifi_ssid);
|
||||
|
||||
if (!getInputParam("psk", 63, s)) return;
|
||||
strncpy(cfg.wifi_psk, s.c_str(), 63);
|
||||
PRINTF("saving psk: %s\n", cfg.wifi_psk);
|
||||
|
||||
if (!getInputParam("hid", 16, s)) return;
|
||||
strcpy(cfg.node_id, s.c_str());
|
||||
PRINTF("saving home id: %s\n", cfg.node_id);
|
||||
|
||||
cfg.flags.node_configured = 1;
|
||||
cfg.flags.wifi_configured = 1;
|
||||
|
||||
config::write(cfg);
|
||||
|
||||
restartTimer.once(0, restart);
|
||||
});
|
||||
|
||||
server.on(FPSTR(ROUTE_RESET), HTTP_POST, [&]() {
|
||||
config::erase();
|
||||
restartTimer.once(1, restart);
|
||||
});
|
||||
|
||||
server.on(FPSTR(ROUTE_HEAP), HTTP_GET, [&]() {
|
||||
server.send(200, FPSTR(CONTENT_TYPE_HTML), String(ESP.getFreeHeap()));
|
||||
});
|
||||
|
||||
server.on(FPSTR(ROUTE_SCAN), HTTP_GET, [&]() {
|
||||
size_t i = 0;
|
||||
size_t len;
|
||||
const char* ssid;
|
||||
bool enough = false;
|
||||
|
||||
bzero(reinterpret_cast<uint8_t*>(scanBuf), scanBufSize);
|
||||
char* cur = scanBuf;
|
||||
|
||||
strncpy_P(cur, JSON_SCAN_FIRST_LIST, scanBufSize);
|
||||
cur += 9;
|
||||
|
||||
for (auto& res: *scanResults) {
|
||||
ssid = res.ssid.c_str();
|
||||
len = res.ssid.length();
|
||||
|
||||
// new item (array with 2 items)
|
||||
*cur++ = '[';
|
||||
|
||||
// 1. ssid (string)
|
||||
*cur++ = '"';
|
||||
for (size_t j = 0; j < len; j++) {
|
||||
if (*(ssid+j) == '"')
|
||||
*cur++ = '\\';
|
||||
*cur++ = *(ssid+j);
|
||||
}
|
||||
*cur++ = '"';
|
||||
*cur++ = ',';
|
||||
|
||||
// 2. rssi (number)
|
||||
cur += sprintf(cur, "%d", res.rssi);
|
||||
|
||||
// close array
|
||||
*cur++ = ']';
|
||||
|
||||
if ((size_t)(cur - scanBuf) >= (size_t) ARRAY_SIZE(scanBuf) - 40)
|
||||
enough = true;
|
||||
|
||||
if (i < scanResults->size() - 1 || enough)
|
||||
*cur++ = ',';
|
||||
|
||||
if (enough)
|
||||
break;
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
*cur++ = ']';
|
||||
*cur++ = '}';
|
||||
*cur++ = '\0';
|
||||
|
||||
server.send(200, FPSTR(CONTENT_TYPE_JSON), scanBuf);
|
||||
});
|
||||
|
||||
server.on(FPSTR(ROUTE_UPDATE), HTTP_POST, [&]() {
|
||||
char json_buf[16];
|
||||
bool should_reboot = !Update.hasError() && !ota.invalidMd5;
|
||||
Update.clearError();
|
||||
|
||||
sprintf_P(json_buf, JSON_UPDATE_FMT, should_reboot ? 1 : 0);
|
||||
|
||||
server.send(200, FPSTR(CONTENT_TYPE_JSON), json_buf);
|
||||
|
||||
if (should_reboot)
|
||||
restartTimer.once(1, restart);
|
||||
}, [&]() {
|
||||
HTTPUpload& upload = server.upload();
|
||||
|
||||
if (upload.status == UPLOAD_FILE_START) {
|
||||
ota.clean();
|
||||
|
||||
String s;
|
||||
if (!getInputParam("md5", 0, s)) {
|
||||
ota.invalidMd5 = true;
|
||||
PRINTLN("http/ota: md5 not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Update.setMD5(s.c_str())) {
|
||||
ota.invalidMd5 = true;
|
||||
PRINTLN("http/ota: setMD5() failed");
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("http/ota: starting, filename=%s\n", upload.filename.c_str());
|
||||
if (!Update.begin(otaGetMaxUpdateSize())) {
|
||||
#ifdef DEBUG
|
||||
Update.printError(Serial);
|
||||
#endif
|
||||
}
|
||||
} else if (upload.status == UPLOAD_FILE_WRITE) {
|
||||
if (!Update.isRunning())
|
||||
return;
|
||||
|
||||
PRINTF("http/ota: writing %ul\n", upload.currentSize);
|
||||
esp_led.blink(1, 1);
|
||||
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
|
||||
#ifdef DEBUG
|
||||
Update.printError(Serial);
|
||||
#endif
|
||||
}
|
||||
} else if (upload.status == UPLOAD_FILE_END) {
|
||||
if (!Update.isRunning())
|
||||
return;
|
||||
|
||||
if (Update.end(true)) {
|
||||
PRINTF("http/ota: ok, total size %ul\n", upload.totalSize);
|
||||
} else {
|
||||
#ifdef DEBUG
|
||||
Update.printError(Serial);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
server.onNotFound([&]() {
|
||||
server.send(404, FPSTR(CONTENT_TYPE_HTML), NOT_FOUND);
|
||||
});
|
||||
|
||||
server.begin();
|
||||
}
|
||||
|
||||
void HttpServer::loop() {
|
||||
server.handleClient();
|
||||
}
|
||||
|
||||
void HttpServer::sendGzip(const StaticFile& file, PGM_P content_type) {
|
||||
server.sendHeader(FPSTR(CONTENT_ENCODING), FPSTR(GZIP));
|
||||
server.send_P(200, content_type, (const char*)file.content, file.size);
|
||||
}
|
||||
|
||||
void HttpServer::sendError(const String& message) {
|
||||
char buf[32];
|
||||
if (snprintf_P(buf, 32, PSTR("error: %s"), message.c_str()) == 32)
|
||||
buf[31] = '\0';
|
||||
server.send(400, FPSTR(CONTENT_TYPE_HTML), buf);
|
||||
}
|
||||
|
||||
bool HttpServer::getInputParam(const char *field_name,
|
||||
size_t max_len,
|
||||
String& dst) {
|
||||
if (!server.hasArg(field_name)) {
|
||||
sendError(String(field_name) + String(MSG_IS_MISSING));
|
||||
return false;
|
||||
}
|
||||
|
||||
String field = server.arg(field_name);
|
||||
if (!field.length() || (max_len != 0 && field.length() > max_len)) {
|
||||
sendError(String(field_name) + String(MSG_IS_INVALID));
|
||||
return false;
|
||||
}
|
||||
|
||||
dst = field;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
56
platformio/temphum/src/http_server.h
Normal file
56
platformio/temphum/src/http_server.h
Normal file
@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
#include <ESP8266WebServer.h>
|
||||
#include <Ticker.h>
|
||||
#include <memory>
|
||||
#include <list>
|
||||
#include <utility>
|
||||
#include "config.h"
|
||||
#include "wifi.h"
|
||||
#include "static.h"
|
||||
|
||||
namespace homekit {
|
||||
|
||||
struct OTAStatus {
|
||||
bool invalidMd5;
|
||||
|
||||
OTAStatus() : invalidMd5(false) {}
|
||||
|
||||
inline void clean() {
|
||||
invalidMd5 = false;
|
||||
}
|
||||
};
|
||||
|
||||
using files::StaticFile;
|
||||
|
||||
class HttpServer {
|
||||
private:
|
||||
ESP8266WebServer server;
|
||||
Ticker restartTimer;
|
||||
std::shared_ptr<std::list<wifi::ScanResult>> scanResults;
|
||||
OTAStatus ota;
|
||||
|
||||
char* scanBuf;
|
||||
size_t scanBufSize;
|
||||
|
||||
void sendGzip(const StaticFile& file, PGM_P content_type);
|
||||
void sendError(const String& message);
|
||||
|
||||
bool getInputParam(const char* field_name, size_t max_len, String& dst);
|
||||
|
||||
public:
|
||||
explicit HttpServer(std::shared_ptr<std::list<wifi::ScanResult>> scanResults)
|
||||
: server(80)
|
||||
, scanResults(std::move(scanResults))
|
||||
, scanBufSize(512) {
|
||||
scanBuf = new char[scanBufSize];
|
||||
};
|
||||
|
||||
~HttpServer() {
|
||||
delete[] scanBuf;
|
||||
}
|
||||
|
||||
void start();
|
||||
void loop();
|
||||
};
|
||||
|
||||
}
|
9
platformio/temphum/src/led.cpp
Normal file
9
platformio/temphum/src/led.cpp
Normal file
@ -0,0 +1,9 @@
|
||||
#include "led.h"
|
||||
#include "config.def.h"
|
||||
|
||||
namespace homekit {
|
||||
|
||||
Led board_led(BOARD_LED_PIN);
|
||||
Led esp_led(ESP_LED_PIN);
|
||||
|
||||
}
|
39
platformio/temphum/src/led.h
Normal file
39
platformio/temphum/src/led.h
Normal file
@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace homekit {
|
||||
|
||||
class Led {
|
||||
private:
|
||||
uint8_t _pin;
|
||||
|
||||
public:
|
||||
explicit Led(uint8_t pin) : _pin(pin) {
|
||||
pinMode(_pin, OUTPUT);
|
||||
off();
|
||||
}
|
||||
|
||||
inline void off() const { digitalWrite(_pin, HIGH); }
|
||||
inline void on() const { digitalWrite(_pin, LOW); }
|
||||
|
||||
void on_off(uint16_t delay_ms, bool last_delay = false) const {
|
||||
on();
|
||||
delay(delay_ms);
|
||||
|
||||
off();
|
||||
if (last_delay)
|
||||
delay(delay_ms);
|
||||
}
|
||||
|
||||
void blink(uint8_t count, uint16_t delay_ms) const {
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
on_off(delay_ms, i < count-1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern Led board_led;
|
||||
extern Led esp_led;
|
||||
|
||||
}
|
18
platformio/temphum/src/logging.h
Normal file
18
platformio/temphum/src/logging.h
Normal file
@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
#include "config.def.h"
|
||||
|
||||
#ifdef DEBUG
|
||||
|
||||
#define PRINTLN(s) Serial.println(s)
|
||||
#define PRINT(s) Serial.print(s)
|
||||
#define PRINTF(fmt, ...) Serial.printf(fmt, ##__VA_ARGS__)
|
||||
|
||||
#else
|
||||
|
||||
#define PRINTLN(s)
|
||||
#define PRINT(s)
|
||||
#define PRINTF(...)
|
||||
|
||||
#endif
|
183
platformio/temphum/src/main.cpp
Normal file
183
platformio/temphum/src/main.cpp
Normal file
@ -0,0 +1,183 @@
|
||||
#include <Arduino.h>
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <DNSServer.h>
|
||||
#include <Ticker.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#include "mqtt.h"
|
||||
#include "config.h"
|
||||
#include "logging.h"
|
||||
#include "http_server.h"
|
||||
#include "led.h"
|
||||
#include "config.def.h"
|
||||
#include "wifi.h"
|
||||
#include "temphum.h"
|
||||
#include "stopwatch.h"
|
||||
|
||||
using namespace homekit;
|
||||
|
||||
enum class WorkingMode {
|
||||
RECOVERY, // AP mode, http server with configuration
|
||||
NORMAL, // MQTT client
|
||||
};
|
||||
static enum WorkingMode working_mode = WorkingMode::NORMAL;
|
||||
|
||||
enum class WiFiConnectionState {
|
||||
WAITING = 0,
|
||||
JUST_CONNECTED = 1,
|
||||
CONNECTED = 2
|
||||
};
|
||||
|
||||
static const uint16_t recovery_boot_detection_ms = 2000;
|
||||
static const uint8_t recovery_boot_delay_ms = 100;
|
||||
|
||||
static volatile enum WiFiConnectionState wifi_state = WiFiConnectionState::WAITING;
|
||||
static void* service = nullptr;
|
||||
static WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler;
|
||||
static Ticker wifiTimer;
|
||||
#if MQTT_BLINK
|
||||
static StopWatch blinkStopWatch;
|
||||
#endif
|
||||
|
||||
static DNSServer* dnsServer = nullptr;
|
||||
|
||||
static void onWifiConnected(const WiFiEventStationModeGotIP& event);
|
||||
static void onWifiDisconnected(const WiFiEventStationModeDisconnected& event);
|
||||
|
||||
static void wifiConnect() {
|
||||
const char *ssid, *psk, *hostname;
|
||||
auto cfg = config::read();
|
||||
wifi::getConfig(cfg, &ssid, &psk, &hostname);
|
||||
|
||||
PRINTF("Wi-Fi STA creds: ssid=%s, psk=%s, hostname=%s\n", ssid, psk, hostname);
|
||||
|
||||
wifi_state = WiFiConnectionState::WAITING;
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.hostname(hostname);
|
||||
WiFi.begin(ssid, psk);
|
||||
|
||||
PRINT("connecting to wifi..");
|
||||
}
|
||||
|
||||
static void wifiHotspot() {
|
||||
esp_led.on();
|
||||
|
||||
auto scanResults = wifi::scan();
|
||||
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP(wifi::AP_SSID);
|
||||
|
||||
dnsServer = new DNSServer();
|
||||
dnsServer->start(53, "*", WiFi.softAPIP());
|
||||
|
||||
service = new HttpServer(scanResults);
|
||||
((HttpServer*)service)->start();
|
||||
}
|
||||
|
||||
static void waitForRecoveryPress() {
|
||||
pinMode(FLASH_BUTTON_PIN, INPUT_PULLUP);
|
||||
for (uint16_t i = 0; i < recovery_boot_detection_ms; i += recovery_boot_delay_ms) {
|
||||
delay(recovery_boot_delay_ms);
|
||||
if (digitalRead(FLASH_BUTTON_PIN) == LOW) {
|
||||
working_mode = WorkingMode::RECOVERY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
WiFi.disconnect();
|
||||
waitForRecoveryPress();
|
||||
|
||||
temphum::setup();
|
||||
|
||||
#ifdef DEBUG
|
||||
Serial.begin(115200);
|
||||
#endif
|
||||
|
||||
auto cfg = config::read();
|
||||
if (config::isDirty(cfg)) {
|
||||
PRINTLN("config is dirty, erasing...");
|
||||
config::erase(cfg);
|
||||
board_led.blink(10, 50);
|
||||
}
|
||||
|
||||
switch (working_mode) {
|
||||
case WorkingMode::RECOVERY:
|
||||
wifiHotspot();
|
||||
break;
|
||||
|
||||
case WorkingMode::NORMAL:
|
||||
wifiConnectHandler = WiFi.onStationModeGotIP(onWifiConnected);
|
||||
wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWifiDisconnected);
|
||||
wifiConnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (working_mode == WorkingMode::NORMAL) {
|
||||
if (wifi_state == WiFiConnectionState::WAITING) {
|
||||
PRINT(".");
|
||||
esp_led.blink(2, 50);
|
||||
delay(1000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wifi_state == WiFiConnectionState::JUST_CONNECTED) {
|
||||
board_led.blink(3, 300);
|
||||
wifi_state = WiFiConnectionState::CONNECTED;
|
||||
|
||||
if (service == nullptr)
|
||||
service = new mqtt::MQTT();
|
||||
|
||||
((mqtt::MQTT*)service)->connect();
|
||||
#if MQTT_BLINK
|
||||
blinkStopWatch.save();
|
||||
#endif
|
||||
}
|
||||
|
||||
auto mqtt = (mqtt::MQTT*)service;
|
||||
if (static_cast<int>(wifi_state) >= 1 && mqtt != nullptr) {
|
||||
mqtt->loop();
|
||||
|
||||
if (mqtt->ota.readyToRestart) {
|
||||
mqtt->disconnect();
|
||||
} else if (mqtt->diagnosticsStopWatch.elapsed(10000)) {
|
||||
mqtt->sendDiagnostics();
|
||||
|
||||
auto data = temphum::read();
|
||||
mqtt->sendTempHumData(data.temp, data.rh);
|
||||
}
|
||||
|
||||
#if MQTT_BLINK
|
||||
// periodically blink board led
|
||||
if (blinkStopWatch.elapsed(5000)) {
|
||||
board_led.blink(1, 10);
|
||||
blinkStopWatch.save();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
if (dnsServer != nullptr)
|
||||
dnsServer->processNextRequest();
|
||||
|
||||
auto httpServer = (HttpServer*)service;
|
||||
if (httpServer != nullptr)
|
||||
httpServer->loop();
|
||||
}
|
||||
}
|
||||
|
||||
static void onWifiConnected(const WiFiEventStationModeGotIP& event) {
|
||||
PRINTF("connected (%s)\n", WiFi.localIP().toString().c_str());
|
||||
wifi_state = WiFiConnectionState::JUST_CONNECTED;
|
||||
}
|
||||
|
||||
static void onWifiDisconnected(const WiFiEventStationModeDisconnected& event) {
|
||||
PRINTLN("disconnected from wi-fi");
|
||||
wifi_state = WiFiConnectionState::WAITING;
|
||||
if (service != nullptr)
|
||||
((mqtt::MQTT*)service)->disconnect();
|
||||
wifiTimer.once(2, wifiConnect);
|
||||
}
|
325
platformio/temphum/src/mqtt.cpp
Normal file
325
platformio/temphum/src/mqtt.cpp
Normal file
@ -0,0 +1,325 @@
|
||||
#include <ESP8266httpUpdate.h>
|
||||
#include "mqtt.h"
|
||||
#include "logging.h"
|
||||
#include "wifi.h"
|
||||
#include "config.def.h"
|
||||
#include "config.h"
|
||||
#include "static.h"
|
||||
#include "util.h"
|
||||
#include "led.h"
|
||||
|
||||
namespace homekit::mqtt {
|
||||
|
||||
static const uint8_t MQTT_CA_FINGERPRINT[] = DEFAULT_MQTT_CA_FINGERPRINT;
|
||||
static const char MQTT_SERVER[] = DEFAULT_MQTT_SERVER;
|
||||
static const uint16_t MQTT_PORT = DEFAULT_MQTT_PORT;
|
||||
static const char MQTT_USERNAME[] = DEFAULT_MQTT_USERNAME;
|
||||
static const char MQTT_PASSWORD[] = DEFAULT_MQTT_PASSWORD;
|
||||
static const char MQTT_CLIENT_ID[] = DEFAULT_MQTT_CLIENT_ID;
|
||||
static const char MQTT_SECRET[HOME_SECRET_SIZE+1] = HOME_SECRET;
|
||||
|
||||
static const char TOPIC_DIAGNOSTICS[] = "stat";
|
||||
static const char TOPIC_INITIAL_DIAGNOSTICS[] = "stat1";
|
||||
static const char TOPIC_OTA_RESPONSE[] = "otares";
|
||||
static const char TOPIC_TEMPHUM_DATA[] = "data";
|
||||
static const char TOPIC_ADMIN_OTA[] = "admin/ota";
|
||||
static const uint16_t MQTT_KEEPALIVE = 30;
|
||||
|
||||
enum class IncomingMessage {
|
||||
UNKNOWN,
|
||||
OTA
|
||||
};
|
||||
|
||||
using namespace espMqttClientTypes;
|
||||
|
||||
#define MD5_SIZE 16
|
||||
|
||||
MQTT::MQTT() {
|
||||
auto cfg = config::read();
|
||||
homeId = String(cfg.flags.node_configured ? cfg.node_id : wifi::NODE_ID);
|
||||
|
||||
randomSeed(micros());
|
||||
|
||||
client.onConnect([&](bool sessionPresent) {
|
||||
PRINTLN("mqtt: connected");
|
||||
|
||||
sendInitialDiagnostics();
|
||||
subscribe(TOPIC_ADMIN_OTA);
|
||||
});
|
||||
|
||||
client.onDisconnect([&](DisconnectReason reason) {
|
||||
PRINTF("mqtt: disconnected, reason=%d\n", static_cast<int>(reason));
|
||||
#ifdef DEBUG
|
||||
if (reason == DisconnectReason::TLS_BAD_FINGERPRINT)
|
||||
PRINTLN("reason: bad fingerprint");
|
||||
#endif
|
||||
|
||||
if (ota.started()) {
|
||||
PRINTLN("mqtt: update was in progress, canceling..");
|
||||
ota.clean();
|
||||
Update.end();
|
||||
Update.clearError();
|
||||
}
|
||||
|
||||
if (ota.readyToRestart) {
|
||||
restartTimer.once(1, restart);
|
||||
} else {
|
||||
reconnectTimer.once(2, [&]() {
|
||||
reconnect();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
client.onSubscribe([&](uint16_t packetId, const SubscribeReturncode* returncodes, size_t len) {
|
||||
PRINTF("mqtt: subscribe ack, packet_id=%d\n", packetId);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
PRINTF(" return code: %u\n", static_cast<unsigned int>(*(returncodes+i)));
|
||||
}
|
||||
});
|
||||
|
||||
client.onUnsubscribe([&](uint16_t packetId) {
|
||||
PRINTF("mqtt: unsubscribe ack, packet_id=%d\n", packetId);
|
||||
});
|
||||
|
||||
client.onMessage([&](const MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) {
|
||||
PRINTF("mqtt: message received, topic=%s, qos=%d, dup=%d, retain=%d, len=%ul, index=%ul, total=%ul\n",
|
||||
topic, properties.qos, (int)properties.dup, (int)properties.retain, len, index, total);
|
||||
|
||||
IncomingMessage msgType = IncomingMessage::UNKNOWN;
|
||||
|
||||
const char *ptr = topic + homeId.length() + 10;
|
||||
String relevantTopic(ptr);
|
||||
|
||||
if (relevantTopic == TOPIC_ADMIN_OTA)
|
||||
msgType = IncomingMessage::OTA;
|
||||
|
||||
if (len != total && msgType != IncomingMessage::OTA) {
|
||||
PRINTLN("mqtt: received partial message, not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (msgType) {
|
||||
case IncomingMessage::OTA:
|
||||
if (ota.finished)
|
||||
break;
|
||||
handleAdminOtaPayload(properties.packetId, payload, len, index, total);
|
||||
break;
|
||||
|
||||
case IncomingMessage::UNKNOWN:
|
||||
PRINTF("error: invalid topic %s\n", topic);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
client.onPublish([&](uint16_t packetId) {
|
||||
PRINTF("mqtt: publish ack, packet_id=%d\n", packetId);
|
||||
|
||||
if (ota.finished && packetId == ota.publishResultPacketId) {
|
||||
ota.readyToRestart = true;
|
||||
}
|
||||
});
|
||||
|
||||
client.setServer(MQTT_SERVER, MQTT_PORT);
|
||||
client.setClientId(MQTT_CLIENT_ID);
|
||||
client.setCredentials(MQTT_USERNAME, MQTT_PASSWORD);
|
||||
client.setCleanSession(true);
|
||||
client.setFingerprint(MQTT_CA_FINGERPRINT);
|
||||
client.setKeepAlive(MQTT_KEEPALIVE);
|
||||
}
|
||||
|
||||
void MQTT::connect() {
|
||||
reconnect();
|
||||
}
|
||||
|
||||
void MQTT::reconnect() {
|
||||
if (client.connected()) {
|
||||
PRINTLN("warning: already connected");
|
||||
return;
|
||||
}
|
||||
client.connect();
|
||||
}
|
||||
|
||||
void MQTT::disconnect() {
|
||||
// TODO test how this works???
|
||||
reconnectTimer.detach();
|
||||
client.disconnect();
|
||||
}
|
||||
|
||||
uint16_t MQTT::publish(const String &topic, uint8_t *payload, size_t length) {
|
||||
String fullTopic = "hk/" + homeId + "/temphum/" + topic;
|
||||
return client.publish(fullTopic.c_str(), 1, false, payload, length);
|
||||
}
|
||||
|
||||
void MQTT::loop() {
|
||||
client.loop();
|
||||
}
|
||||
|
||||
uint16_t MQTT::subscribe(const String &topic, uint8_t qos) {
|
||||
String fullTopic = "hk/" + homeId + "/temphum/" + topic;
|
||||
PRINTF("mqtt: subscribing to %s...\n", fullTopic.c_str());
|
||||
|
||||
uint16_t packetId = client.subscribe(fullTopic.c_str(), qos);
|
||||
if (!packetId)
|
||||
PRINTF("error: failed to subscribe to %s\n", fullTopic.c_str());
|
||||
return packetId;
|
||||
}
|
||||
|
||||
void MQTT::sendInitialDiagnostics() {
|
||||
auto cfg = config::read();
|
||||
InitialDiagnosticsPayload stat{
|
||||
.ip = wifi::getIPAsInteger(),
|
||||
.fw_version = FW_VERSION,
|
||||
.rssi = wifi::getRSSI(),
|
||||
.free_heap = ESP.getFreeHeap(),
|
||||
.flags = DiagnosticsFlags{
|
||||
.state = 1,
|
||||
.config_changed_value_present = 1,
|
||||
.config_changed = static_cast<uint8_t>(cfg.flags.node_configured ||
|
||||
cfg.flags.wifi_configured ? 1 : 0)
|
||||
}
|
||||
};
|
||||
publish(TOPIC_INITIAL_DIAGNOSTICS, reinterpret_cast<uint8_t*>(&stat), sizeof(stat));
|
||||
diagnosticsStopWatch.save();
|
||||
}
|
||||
|
||||
void MQTT::sendDiagnostics() {
|
||||
DiagnosticsPayload stat{
|
||||
.rssi = wifi::getRSSI(),
|
||||
.free_heap = ESP.getFreeHeap(),
|
||||
.flags = DiagnosticsFlags{
|
||||
.state = 1,
|
||||
.config_changed_value_present = 0,
|
||||
.config_changed = 0
|
||||
}
|
||||
};
|
||||
publish(TOPIC_DIAGNOSTICS, reinterpret_cast<uint8_t*>(&stat), sizeof(stat));
|
||||
diagnosticsStopWatch.save();
|
||||
}
|
||||
|
||||
void MQTT::sendTempHumData(double temp, double rh) {
|
||||
TempHumDataPayload data {
|
||||
.temp = temp,
|
||||
.rh = rh
|
||||
};
|
||||
publish(TOPIC_TEMPHUM_DATA, reinterpret_cast<uint8_t*>(&data), sizeof(data));
|
||||
}
|
||||
|
||||
uint16_t MQTT::sendOtaResponse(OTAResult status, uint8_t error_code) {
|
||||
OTAResponse resp{
|
||||
.status = status,
|
||||
.error_code = error_code
|
||||
};
|
||||
return publish(TOPIC_OTA_RESPONSE, reinterpret_cast<uint8_t*>(&resp), sizeof(resp));
|
||||
}
|
||||
|
||||
void MQTT::handleAdminOtaPayload(uint16_t packetId, const uint8_t *payload, size_t length, size_t index, size_t total) {
|
||||
char md5[33];
|
||||
char* md5Ptr = md5;
|
||||
|
||||
if (index != 0 && ota.dataPacketId != packetId) {
|
||||
PRINTLN("mqtt/ota: non-matching packet id");
|
||||
return;
|
||||
}
|
||||
|
||||
Update.runAsync(true);
|
||||
|
||||
if (index == 0) {
|
||||
if (length < HOME_SECRET_SIZE + MD5_SIZE) {
|
||||
PRINTLN("mqtt/ota: failed to check secret, first packet size is too small");
|
||||
return;
|
||||
}
|
||||
|
||||
if (memcmp((const char*)payload, HOME_SECRET, HOME_SECRET_SIZE) != 0) {
|
||||
PRINTLN("mqtt/ota: invalid secret");
|
||||
return;
|
||||
}
|
||||
|
||||
PRINTF("mqtt/ota: starting update, total=%ul\n", total-HOME_SECRET_SIZE);
|
||||
for (int i = 0; i < MD5_SIZE; i++) {
|
||||
md5Ptr += sprintf(md5Ptr, "%02x", *((unsigned char*)(payload+HOME_SECRET_SIZE+i)));
|
||||
}
|
||||
md5[32] = '\0';
|
||||
PRINTF("mqtt/ota: md5 is %s\n", md5);
|
||||
PRINTF("mqtt/ota: first packet is %ul bytes length\n", length);
|
||||
|
||||
md5[32] = '\0';
|
||||
|
||||
if (Update.isRunning()) {
|
||||
Update.end();
|
||||
Update.clearError();
|
||||
}
|
||||
|
||||
if (!Update.setMD5(md5)) {
|
||||
PRINTLN("mqtt/ota: setMD5 failed");
|
||||
return;
|
||||
}
|
||||
|
||||
ota.dataPacketId = packetId;
|
||||
|
||||
if (!Update.begin(total - HOME_SECRET_SIZE - MD5_SIZE)) {
|
||||
ota.clean();
|
||||
#ifdef DEBUG
|
||||
Update.printError(Serial);
|
||||
#endif
|
||||
sendOtaResponse(OTAResult::UPDATE_ERROR, Update.getError());
|
||||
}
|
||||
|
||||
ota.written = Update.write(const_cast<uint8_t*>(payload)+HOME_SECRET_SIZE + MD5_SIZE, length-HOME_SECRET_SIZE - MD5_SIZE);
|
||||
ota.written += HOME_SECRET_SIZE + MD5_SIZE;
|
||||
|
||||
esp_led.blink(1, 1);
|
||||
PRINTF("mqtt/ota: updating %u/%u\n", ota.written, Update.size());
|
||||
|
||||
} else {
|
||||
if (!Update.isRunning()) {
|
||||
PRINTLN("mqtt/ota: update is not running");
|
||||
return;
|
||||
}
|
||||
|
||||
if (index == ota.written) {
|
||||
size_t written;
|
||||
if ((written = Update.write(const_cast<uint8_t*>(payload), length)) != length) {
|
||||
PRINTF("mqtt/ota: error: tried to write %ul bytes, write() returned %ul\n",
|
||||
length, written);
|
||||
ota.clean();
|
||||
Update.end();
|
||||
Update.clearError();
|
||||
sendOtaResponse(OTAResult::WRITE_ERROR);
|
||||
return;
|
||||
}
|
||||
ota.written += length;
|
||||
|
||||
esp_led.blink(1, 1);
|
||||
PRINTF("mqtt/ota: updating %u/%u\n",
|
||||
ota.written - HOME_SECRET_SIZE - MD5_SIZE,
|
||||
Update.size());
|
||||
} else {
|
||||
PRINTF("mqtt/ota: position is invalid, expected %ul, got %ul\n", ota.written, index);
|
||||
ota.clean();
|
||||
Update.end();
|
||||
Update.clearError();
|
||||
}
|
||||
}
|
||||
|
||||
if (Update.isFinished()) {
|
||||
ota.dataPacketId = 0;
|
||||
|
||||
if (Update.end()) {
|
||||
ota.finished = true;
|
||||
ota.publishResultPacketId = sendOtaResponse(OTAResult::OK);
|
||||
PRINTF("mqtt/ota: ok, otares packet_id=%d\n", ota.publishResultPacketId);
|
||||
} else {
|
||||
ota.clean();
|
||||
|
||||
PRINTF("mqtt/ota: error: %u\n", Update.getError());
|
||||
#ifdef DEBUG
|
||||
Update.printError(Serial);
|
||||
#endif
|
||||
Update.clearError();
|
||||
|
||||
sendOtaResponse(OTAResult::UPDATE_ERROR, Update.getError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
107
platformio/temphum/src/mqtt.h
Normal file
107
platformio/temphum/src/mqtt.h
Normal file
@ -0,0 +1,107 @@
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <espMqttClient.h>
|
||||
#include <Ticker.h>
|
||||
#include "stopwatch.h"
|
||||
|
||||
namespace homekit { namespace mqtt {
|
||||
|
||||
enum class OTAResult: uint8_t {
|
||||
OK = 0,
|
||||
UPDATE_ERROR = 1,
|
||||
WRITE_ERROR = 2,
|
||||
};
|
||||
|
||||
struct OTAStatus {
|
||||
uint16_t dataPacketId;
|
||||
uint16_t publishResultPacketId;
|
||||
bool finished;
|
||||
bool readyToRestart;
|
||||
size_t written;
|
||||
|
||||
OTAStatus()
|
||||
: dataPacketId(0)
|
||||
, publishResultPacketId(0)
|
||||
, finished(false)
|
||||
, readyToRestart(false)
|
||||
, written(0)
|
||||
{}
|
||||
|
||||
inline void clean() {
|
||||
dataPacketId = 0;
|
||||
publishResultPacketId = 0;
|
||||
finished = false;
|
||||
readyToRestart = false;
|
||||
written = 0;
|
||||
}
|
||||
|
||||
inline bool started() const {
|
||||
return dataPacketId != 0;
|
||||
}
|
||||
};
|
||||
|
||||
class MQTT {
|
||||
private:
|
||||
String homeId;
|
||||
WiFiClientSecure httpsSecureClient;
|
||||
espMqttClientSecure client;
|
||||
Ticker reconnectTimer;
|
||||
Ticker restartTimer;
|
||||
|
||||
void handleAdminOtaPayload(uint16_t packetId, const uint8_t* payload, size_t length, size_t index, size_t total);
|
||||
|
||||
uint16_t publish(const String& topic, uint8_t* payload, size_t length);
|
||||
uint16_t subscribe(const String& topic, uint8_t qos = 0);
|
||||
|
||||
void sendInitialDiagnostics();
|
||||
uint16_t sendOtaResponse(OTAResult status, uint8_t error_code = 0);
|
||||
|
||||
public:
|
||||
StopWatch diagnosticsStopWatch;
|
||||
OTAStatus ota;
|
||||
|
||||
MQTT();
|
||||
void connect();
|
||||
void disconnect();
|
||||
void reconnect();
|
||||
void loop();
|
||||
void sendDiagnostics();
|
||||
void sendTempHumData(double temp, double rh);
|
||||
};
|
||||
|
||||
struct DiagnosticsFlags {
|
||||
uint8_t state: 1;
|
||||
uint8_t config_changed_value_present: 1;
|
||||
uint8_t config_changed: 1;
|
||||
uint8_t reserved: 5;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct InitialDiagnosticsPayload {
|
||||
uint32_t ip;
|
||||
uint8_t fw_version;
|
||||
int8_t rssi;
|
||||
uint32_t free_heap;
|
||||
DiagnosticsFlags flags;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct DiagnosticsPayload {
|
||||
int8_t rssi;
|
||||
uint32_t free_heap;
|
||||
DiagnosticsFlags flags;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PowerPayload {
|
||||
char secret[12];
|
||||
uint8_t state;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct TempHumDataPayload {
|
||||
double temp;
|
||||
double rh;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct OTAResponse {
|
||||
OTAResult status;
|
||||
uint8_t error_code;
|
||||
} __attribute__((packed));
|
||||
|
||||
} }
|
450
platformio/temphum/src/static.cpp
Normal file
450
platformio/temphum/src/static.cpp
Normal file
@ -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};
|
||||
|
||||
}
|
22
platformio/temphum/src/static.h
Normal file
22
platformio/temphum/src/static.h
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* This file is autogenerated with make_static.sh script
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
namespace homekit::files {
|
||||
|
||||
typedef struct {
|
||||
size_t size;
|
||||
const uint8_t* content;
|
||||
} StaticFile;
|
||||
|
||||
extern const StaticFile index_html;
|
||||
extern const StaticFile app_js;
|
||||
extern const StaticFile md5_js;
|
||||
extern const StaticFile style_css;
|
||||
extern const StaticFile favicon_ico;
|
||||
|
||||
}
|
30
platformio/temphum/src/stopwatch.h
Normal file
30
platformio/temphum/src/stopwatch.h
Normal file
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
53
platformio/temphum/src/temphum.cpp
Normal file
53
platformio/temphum/src/temphum.cpp
Normal file
@ -0,0 +1,53 @@
|
||||
#include "temphum.h"
|
||||
#include "logging.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace homekit::temphum {
|
||||
|
||||
static const int addr = 0x40;
|
||||
|
||||
void setup() {
|
||||
pinMode(D2, OUTPUT);
|
||||
pinMode(D3, OUTPUT);
|
||||
|
||||
Wire.begin(D2, D3);
|
||||
|
||||
Wire.beginTransmission(addr);
|
||||
Wire.write(0xfe);
|
||||
Wire.endTransmission();
|
||||
|
||||
delay(500);
|
||||
}
|
||||
|
||||
struct data read() {
|
||||
// Request temperature measurement from the Si7021 sensor
|
||||
Wire.beginTransmission(addr);
|
||||
Wire.write(0xF3); // command to measure temperature
|
||||
Wire.endTransmission();
|
||||
|
||||
delay(500); // wait for the measurement to be ready
|
||||
|
||||
// Read the temperature measurement from the Si7021 sensor
|
||||
Wire.requestFrom(addr, 2);
|
||||
uint16_t temp_raw = Wire.read() << 8 | Wire.read();
|
||||
double temperature = ((175.72 * temp_raw) / 65536.0) - 46.85;
|
||||
|
||||
// Request humidity measurement from the Si7021 sensor
|
||||
Wire.beginTransmission(addr);
|
||||
Wire.write(0xF5); // command to measure humidity
|
||||
Wire.endTransmission();
|
||||
|
||||
delay(500); // wait for the measurement to be ready
|
||||
|
||||
// Read the humidity measurement from the Si7021 sensor
|
||||
Wire.requestFrom(addr, 2);
|
||||
uint16_t hum_raw = Wire.read() << 8 | Wire.read();
|
||||
double humidity = ((125.0 * hum_raw) / 65536.0) - 6.0;
|
||||
|
||||
return {
|
||||
.temp = temperature,
|
||||
.rh = humidity
|
||||
};
|
||||
}
|
||||
|
||||
}
|
15
platformio/temphum/src/temphum.h
Normal file
15
platformio/temphum/src/temphum.h
Normal file
@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <Wire.h>
|
||||
|
||||
namespace homekit::temphum {
|
||||
|
||||
struct data {
|
||||
double temp; // celsius
|
||||
double rh; // relative humidity percentage
|
||||
};
|
||||
|
||||
void setup();
|
||||
struct data read();
|
||||
|
||||
}
|
13
platformio/temphum/src/util.h
Normal file
13
platformio/temphum/src/util.h
Normal file
@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
namespace homekit {
|
||||
|
||||
inline size_t otaGetMaxUpdateSize() {
|
||||
return (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
|
||||
}
|
||||
|
||||
inline void restart() {
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
}
|
48
platformio/temphum/src/wifi.cpp
Normal file
48
platformio/temphum/src/wifi.cpp
Normal file
@ -0,0 +1,48 @@
|
||||
#include <pgmspace.h>
|
||||
#include "config.def.h"
|
||||
#include "wifi.h"
|
||||
#include "config.h"
|
||||
#include "logging.h"
|
||||
|
||||
namespace homekit::wifi {
|
||||
|
||||
using namespace homekit;
|
||||
using homekit::config::ConfigData;
|
||||
|
||||
const char NODE_ID[] = DEFAULT_NODE_ID;
|
||||
const char AP_SSID[] = DEFAULT_WIFI_AP_SSID;
|
||||
const char STA_SSID[] = DEFAULT_WIFI_STA_SSID;
|
||||
const char STA_PSK[] = DEFAULT_WIFI_STA_PSK;
|
||||
|
||||
void getConfig(ConfigData &cfg, const char** ssid, const char** psk, const char** hostname) {
|
||||
if (cfg.flags.wifi_configured) {
|
||||
*ssid = cfg.wifi_ssid;
|
||||
*psk = cfg.wifi_psk;
|
||||
*hostname = cfg.node_id;
|
||||
} else {
|
||||
*ssid = STA_SSID;
|
||||
*psk = STA_PSK;
|
||||
*hostname = NODE_ID;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<std::list<ScanResult>> scan() {
|
||||
if (WiFi.getMode() != WIFI_STA) {
|
||||
PRINTLN("wifi::scan: switching mode to STA");
|
||||
WiFi.mode(WIFI_STA);
|
||||
}
|
||||
|
||||
std::shared_ptr<std::list<ScanResult>> results(new std::list<ScanResult>);
|
||||
int count = WiFi.scanNetworks();
|
||||
for (int i = 0; i < count; i++) {
|
||||
results->push_back(ScanResult {
|
||||
.rssi = WiFi.RSSI(i),
|
||||
.ssid = WiFi.SSID(i)
|
||||
});
|
||||
}
|
||||
|
||||
WiFi.scanDelete();
|
||||
return results;
|
||||
}
|
||||
|
||||
}
|
36
platformio/temphum/src/wifi.h
Normal file
36
platformio/temphum/src/wifi.h
Normal file
@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include "config.h"
|
||||
|
||||
namespace homekit::wifi {
|
||||
|
||||
using homekit::config::ConfigData;
|
||||
|
||||
struct ScanResult {
|
||||
int rssi;
|
||||
String ssid;
|
||||
};
|
||||
|
||||
void getConfig(ConfigData& cfg, const char** ssid, const char** psk, const char** hostname);
|
||||
|
||||
std::shared_ptr<std::list<ScanResult>> scan();
|
||||
|
||||
inline uint32_t getIPAsInteger() {
|
||||
if (!WiFi.isConnected())
|
||||
return 0;
|
||||
return WiFi.localIP().v4();
|
||||
}
|
||||
|
||||
inline int8_t getRSSI() {
|
||||
return WiFi.RSSI();
|
||||
}
|
||||
|
||||
extern const char AP_SSID[];
|
||||
extern const char STA_SSID[];
|
||||
extern const char STA_PSK[];
|
||||
extern const char NODE_ID[];
|
||||
|
||||
}
|
42
src/esp_mqtt_util.py
Executable file
42
src/esp_mqtt_util.py
Executable file
@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import Optional
|
||||
from argparse import ArgumentParser
|
||||
from enum import Enum
|
||||
|
||||
from home.config import config
|
||||
from home.mqtt import MqttRelay
|
||||
from home.mqtt.esp import MqttEspBase
|
||||
from home.mqtt.temphum import MqttTempHum
|
||||
from home.mqtt.esp import MqttEspDevice
|
||||
|
||||
mqtt_client: Optional[MqttEspBase] = None
|
||||
|
||||
|
||||
class NodeType(Enum):
|
||||
RELAY = 'relay'
|
||||
TEMPHUM = 'temphum'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('--device-id', type=str, required=True)
|
||||
parser.add_argument('--type', type=str, required=True,
|
||||
choices=[i.name.lower() for i in NodeType])
|
||||
|
||||
config.load('mqtt_util', parser=parser)
|
||||
arg = parser.parse_args()
|
||||
|
||||
mqtt_node_type = NodeType(arg.type)
|
||||
devices = MqttEspDevice(id=arg.device_id)
|
||||
|
||||
if mqtt_node_type == NodeType.RELAY:
|
||||
mqtt_client = MqttRelay(devices=devices)
|
||||
elif mqtt_node_type == NodeType.TEMPHUM:
|
||||
mqtt_client = MqttTempHum(devices=devices)
|
||||
|
||||
mqtt_client.set_message_callback(lambda device_id, payload: print(payload))
|
||||
mqtt_client.configure_tls()
|
||||
try:
|
||||
mqtt_client.connect_and_loop()
|
||||
except KeyboardInterrupt:
|
||||
mqtt_client.disconnect()
|
@ -1,3 +1,4 @@
|
||||
from .mqtt import MQTTBase
|
||||
from .mqtt import MqttBase
|
||||
from .util import poll_tick
|
||||
from .relay import MQTTRelay, MQTTRelayState, MQTTRelayDevice
|
||||
from .relay import MqttRelay, MqttRelayState
|
||||
from .temphum import MqttTempHum
|
106
src/home/mqtt/esp.py
Normal file
106
src/home/mqtt/esp.py
Normal file
@ -0,0 +1,106 @@
|
||||
import re
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from .mqtt import MqttBase
|
||||
from typing import Optional, Union
|
||||
from .payload.esp import (
|
||||
OTAPayload,
|
||||
OTAResultPayload,
|
||||
DiagnosticsPayload,
|
||||
InitialDiagnosticsPayload
|
||||
)
|
||||
|
||||
|
||||
class MqttEspDevice:
|
||||
id: str
|
||||
secret: Optional[str]
|
||||
|
||||
def __init__(self, id: str, secret: Optional[str] = None):
|
||||
self.id = id
|
||||
self.secret = secret
|
||||
|
||||
|
||||
class MqttEspBase(MqttBase):
|
||||
_devices: list[MqttEspDevice]
|
||||
_message_callback: Optional[callable]
|
||||
_ota_publish_callback: Optional[callable]
|
||||
|
||||
TOPIC_LEAF = 'esp'
|
||||
|
||||
def __init__(self,
|
||||
devices: Union[MqttEspDevice, list[MqttEspDevice]],
|
||||
subscribe_to_updates=True):
|
||||
super().__init__(clean_session=True)
|
||||
if not isinstance(devices, list):
|
||||
devices = [devices]
|
||||
self._devices = devices
|
||||
self._message_callback = None
|
||||
self._ota_publish_callback = None
|
||||
self._subscribe_to_updates = subscribe_to_updates
|
||||
self._ota_mid = None
|
||||
|
||||
def on_connect(self, client: mqtt.Client, userdata, flags, rc):
|
||||
super().on_connect(client, userdata, flags, rc)
|
||||
|
||||
if self._subscribe_to_updates:
|
||||
for device in self._devices:
|
||||
topic = f'hk/{device.id}/{self.TOPIC_LEAF}/#'
|
||||
self._logger.debug(f"subscribing to {topic}")
|
||||
client.subscribe(topic, qos=1)
|
||||
|
||||
def on_publish(self, client: mqtt.Client, userdata, mid):
|
||||
if self._ota_mid is not None and mid == self._ota_mid and self._ota_publish_callback:
|
||||
self._ota_publish_callback()
|
||||
|
||||
def set_message_callback(self, callback: callable):
|
||||
self._message_callback = callback
|
||||
|
||||
def on_message(self, client: mqtt.Client, userdata, msg):
|
||||
try:
|
||||
match = re.match(self.get_mqtt_topics(), msg.topic)
|
||||
self._logger.debug(f'topic: {msg.topic}')
|
||||
if not match:
|
||||
return
|
||||
|
||||
device_id = match.group(1)
|
||||
subtopic = match.group(2)
|
||||
|
||||
# try:
|
||||
next(d for d in self._devices if d.id == device_id)
|
||||
# except StopIteration:h
|
||||
# return
|
||||
|
||||
message = None
|
||||
if subtopic == 'stat':
|
||||
message = DiagnosticsPayload.unpack(msg.payload)
|
||||
elif subtopic == 'stat1':
|
||||
message = InitialDiagnosticsPayload.unpack(msg.payload)
|
||||
elif subtopic == 'otares':
|
||||
message = OTAResultPayload.unpack(msg.payload)
|
||||
|
||||
if message and self._message_callback:
|
||||
self._message_callback(device_id, message)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self._logger.exception(str(e))
|
||||
|
||||
def push_ota(self,
|
||||
device_id,
|
||||
filename: str,
|
||||
publish_callback: callable,
|
||||
qos: int):
|
||||
device = next(d for d in self._devices if d.id == device_id)
|
||||
assert device.secret is not None, 'device secret not specified'
|
||||
|
||||
self._ota_publish_callback = publish_callback
|
||||
payload = OTAPayload(secret=device.secret, filename=filename)
|
||||
publish_result = self._client.publish(f'hk/{device.id}/{self.TOPIC_LEAF}/admin/ota',
|
||||
payload=payload.pack(),
|
||||
qos=qos)
|
||||
self._ota_mid = publish_result.mid
|
||||
self._client.loop_write()
|
||||
|
||||
@classmethod
|
||||
def get_mqtt_topics(cls, additional_topics: Optional[list[str]] = None):
|
||||
return rf'^hk/(.*?)/{cls.TOPIC_LEAF}/(stat|stat1|otares'+('|'+('|'.join(additional_topics)) if additional_topics else '')+')$'
|
@ -13,7 +13,7 @@ def username_and_password() -> Tuple[str, str]:
|
||||
return username, password
|
||||
|
||||
|
||||
class MQTTBase:
|
||||
class MqttBase:
|
||||
def __init__(self, clean_session=True):
|
||||
self._client = mqtt.Client(client_id=config['mqtt']['client_id'],
|
||||
protocol=mqtt.MQTTv311,
|
||||
|
@ -1 +1 @@
|
||||
from .base_payload import MQTTPayload
|
||||
from .base_payload import MqttPayload
|
@ -5,7 +5,21 @@ import re
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
class MQTTPayload(abc.ABC):
|
||||
def pldstr(self) -> str:
|
||||
attrs = []
|
||||
for field in self.__class__.__annotations__:
|
||||
if hasattr(self, field):
|
||||
attr = getattr(self, field)
|
||||
attrs.append(f'{field}={attr}')
|
||||
if attrs:
|
||||
attrs_s = ' '
|
||||
attrs_s += ', '.join(attrs)
|
||||
else:
|
||||
attrs_s = ''
|
||||
return f'<%s{attrs_s}>' % (self.__class__.__name__,)
|
||||
|
||||
|
||||
class MqttPayload(abc.ABC):
|
||||
FORMAT = ''
|
||||
PACKER = {}
|
||||
UNPACKER = {}
|
||||
@ -70,7 +84,7 @@ class MQTTPayload(abc.ABC):
|
||||
bf_number = -1
|
||||
i += 1
|
||||
|
||||
if issubclass(field_type, MQTTPayloadCustomField):
|
||||
if issubclass(field_type, MqttPayloadCustomField):
|
||||
kwargs[field] = field_type.unpack(data[i])
|
||||
else:
|
||||
kwargs[field] = cls._unpack_field(field, data[i])
|
||||
@ -87,15 +101,18 @@ class MQTTPayload(abc.ABC):
|
||||
|
||||
@classmethod
|
||||
def _unpack_field(cls, name, val):
|
||||
if isinstance(val, MQTTPayloadCustomField):
|
||||
if isinstance(val, MqttPayloadCustomField):
|
||||
return
|
||||
if cls.UNPACKER and name in cls.UNPACKER:
|
||||
return cls.UNPACKER[name](val)
|
||||
else:
|
||||
return val
|
||||
|
||||
def __str__(self):
|
||||
return pldstr(self)
|
||||
|
||||
class MQTTPayloadCustomField(abc.ABC):
|
||||
|
||||
class MqttPayloadCustomField(abc.ABC):
|
||||
def __init__(self, **kwargs):
|
||||
for field in self.__class__.__annotations__:
|
||||
setattr(self, field, kwargs[field])
|
||||
@ -109,6 +126,9 @@ class MQTTPayloadCustomField(abc.ABC):
|
||||
def unpack(cls, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return pldstr(self)
|
||||
|
||||
|
||||
def bit_field(seq_no: int, total_bits: int, bits: int):
|
||||
return type(f'MQTTPayloadBitField_{seq_no}_{total_bits}_{bits}', (object,), {
|
||||
|
78
src/home/mqtt/payload/esp.py
Normal file
78
src/home/mqtt/payload/esp.py
Normal file
@ -0,0 +1,78 @@
|
||||
import hashlib
|
||||
|
||||
from .base_payload import MqttPayload, MqttPayloadCustomField
|
||||
|
||||
|
||||
class OTAResultPayload(MqttPayload):
|
||||
FORMAT = '=BB'
|
||||
result: int
|
||||
error_code: int
|
||||
|
||||
|
||||
class OTAPayload(MqttPayload):
|
||||
secret: str
|
||||
filename: str
|
||||
|
||||
# structure of returned data:
|
||||
#
|
||||
# uint8_t[len(secret)] secret;
|
||||
# uint8_t[16] md5;
|
||||
# *uint8_t data
|
||||
|
||||
def pack(self):
|
||||
buf = bytearray(self.secret.encode())
|
||||
m = hashlib.md5()
|
||||
with open(self.filename, 'rb') as fd:
|
||||
content = fd.read()
|
||||
m.update(content)
|
||||
buf.extend(m.digest())
|
||||
buf.extend(content)
|
||||
return buf
|
||||
|
||||
def unpack(cls, buf: bytes):
|
||||
raise RuntimeError(f'{cls.__class__.__name__}.unpack: not implemented')
|
||||
# secret = buf[:12].decode()
|
||||
# filename = buf[12:].decode()
|
||||
# return OTAPayload(secret=secret, filename=filename)
|
||||
|
||||
|
||||
class DiagnosticsFlags(MqttPayloadCustomField):
|
||||
state: bool
|
||||
config_changed_value_present: bool
|
||||
config_changed: bool
|
||||
|
||||
@staticmethod
|
||||
def unpack(flags: int):
|
||||
# _logger.debug(f'StatFlags.unpack: flags={flags}')
|
||||
state = flags & 0x1
|
||||
ccvp = (flags >> 1) & 0x1
|
||||
cc = (flags >> 2) & 0x1
|
||||
# _logger.debug(f'StatFlags.unpack: state={state}')
|
||||
return DiagnosticsFlags(state=(state == 1),
|
||||
config_changed_value_present=(ccvp == 1),
|
||||
config_changed=(cc == 1))
|
||||
|
||||
def __index__(self):
|
||||
bits = 0
|
||||
bits |= (int(self.state) & 0x1)
|
||||
bits |= (int(self.config_changed_value_present) & 0x1) << 1
|
||||
bits |= (int(self.config_changed) & 0x1) << 2
|
||||
return bits
|
||||
|
||||
|
||||
class InitialDiagnosticsPayload(MqttPayload):
|
||||
FORMAT = '=IBbIB'
|
||||
|
||||
ip: int
|
||||
fw_version: int
|
||||
rssi: int
|
||||
free_heap: int
|
||||
flags: DiagnosticsFlags
|
||||
|
||||
|
||||
class DiagnosticsPayload(MqttPayload):
|
||||
FORMAT = '=bIB'
|
||||
|
||||
rssi: int
|
||||
free_heap: int
|
||||
flags: DiagnosticsFlags
|
@ -1,13 +1,13 @@
|
||||
import struct
|
||||
|
||||
from .base_payload import MQTTPayload, bit_field
|
||||
from .base_payload import MqttPayload, bit_field
|
||||
from typing import Tuple
|
||||
|
||||
_mult_10 = lambda n: int(n*10)
|
||||
_div_10 = lambda n: n/10
|
||||
|
||||
|
||||
class Status(MQTTPayload):
|
||||
class Status(MqttPayload):
|
||||
# 46 bytes
|
||||
FORMAT = 'IHHHHHHBHHHHHBHHHHHHHH'
|
||||
|
||||
@ -65,7 +65,7 @@ class Status(MQTTPayload):
|
||||
load_connected: bit_field(0, 16, 1)
|
||||
|
||||
|
||||
class Generation(MQTTPayload):
|
||||
class Generation(MqttPayload):
|
||||
# 8 bytes
|
||||
FORMAT = 'II'
|
||||
|
||||
|
@ -1,53 +1,13 @@
|
||||
import hashlib
|
||||
|
||||
from .base_payload import MQTTPayload, MQTTPayloadCustomField
|
||||
from .base_payload import MqttPayload
|
||||
from .esp import (
|
||||
OTAResultPayload,
|
||||
OTAPayload,
|
||||
InitialDiagnosticsPayload,
|
||||
DiagnosticsPayload
|
||||
)
|
||||
|
||||
|
||||
# _logger = logging.getLogger(__name__)
|
||||
|
||||
class StatFlags(MQTTPayloadCustomField):
|
||||
state: bool
|
||||
config_changed_value_present: bool
|
||||
config_changed: bool
|
||||
|
||||
@staticmethod
|
||||
def unpack(flags: int):
|
||||
# _logger.debug(f'StatFlags.unpack: flags={flags}')
|
||||
state = flags & 0x1
|
||||
ccvp = (flags >> 1) & 0x1
|
||||
cc = (flags >> 2) & 0x1
|
||||
# _logger.debug(f'StatFlags.unpack: state={state}')
|
||||
return StatFlags(state=(state == 1),
|
||||
config_changed_value_present=(ccvp == 1),
|
||||
config_changed=(cc == 1))
|
||||
|
||||
def __index__(self):
|
||||
bits = 0
|
||||
bits |= (int(self.state) & 0x1)
|
||||
bits |= (int(self.config_changed_value_present) & 0x1) << 1
|
||||
bits |= (int(self.config_changed) & 0x1) << 2
|
||||
return bits
|
||||
|
||||
|
||||
class InitialStatPayload(MQTTPayload):
|
||||
FORMAT = '=IBbIB'
|
||||
|
||||
ip: int
|
||||
fw_version: int
|
||||
rssi: int
|
||||
free_heap: int
|
||||
flags: StatFlags
|
||||
|
||||
|
||||
class StatPayload(MQTTPayload):
|
||||
FORMAT = '=bIB'
|
||||
|
||||
rssi: int
|
||||
free_heap: int
|
||||
flags: StatFlags
|
||||
|
||||
|
||||
class PowerPayload(MQTTPayload):
|
||||
class PowerPayload(MqttPayload):
|
||||
FORMAT = '=12sB'
|
||||
PACKER = {
|
||||
'state': lambda n: int(n),
|
||||
@ -60,37 +20,3 @@ class PowerPayload(MQTTPayload):
|
||||
|
||||
secret: str
|
||||
state: bool
|
||||
|
||||
|
||||
class OTAResultPayload(MQTTPayload):
|
||||
FORMAT = '=BB'
|
||||
result: int
|
||||
error_code: int
|
||||
|
||||
|
||||
class OTAPayload(MQTTPayload):
|
||||
secret: str
|
||||
filename: str
|
||||
|
||||
# structure of returned data:
|
||||
#
|
||||
# uint8_t[len(secret)] secret;
|
||||
# uint8_t[16] md5;
|
||||
# *uint8_t data
|
||||
|
||||
def pack(self):
|
||||
buf = bytearray(self.secret.encode())
|
||||
m = hashlib.md5()
|
||||
with open(self.filename, 'rb') as fd:
|
||||
content = fd.read()
|
||||
m.update(content)
|
||||
buf.extend(m.digest())
|
||||
buf.extend(content)
|
||||
return buf
|
||||
|
||||
def unpack(cls, buf: bytes):
|
||||
raise RuntimeError(f'{cls.__class__.__name__}.unpack: not implemented')
|
||||
# secret = buf[:12].decode()
|
||||
# filename = buf[12:].decode()
|
||||
# return OTAPayload(secret=secret, filename=filename)
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
from .base_payload import MQTTPayload
|
||||
from .base_payload import MqttPayload
|
||||
|
||||
_mult_100 = lambda n: int(n*100)
|
||||
_div_100 = lambda n: n/100
|
||||
|
||||
|
||||
class Temperature(MQTTPayload):
|
||||
class Temperature(MqttPayload):
|
||||
FORMAT = 'IhH'
|
||||
PACKER = {
|
||||
'temp': _mult_100,
|
||||
|
14
src/home/mqtt/payload/temphum.py
Normal file
14
src/home/mqtt/payload/temphum.py
Normal file
@ -0,0 +1,14 @@
|
||||
from .base_payload import MqttPayload
|
||||
|
||||
two_digits_precision = lambda x: round(x, 2)
|
||||
|
||||
|
||||
class TempHumDataPayload(MqttPayload):
|
||||
FORMAT = '=dd'
|
||||
UNPACKER = {
|
||||
'temp': two_digits_precision,
|
||||
'rh': two_digits_precision
|
||||
}
|
||||
|
||||
temp: float
|
||||
rh: float
|
@ -2,89 +2,14 @@ import paho.mqtt.client as mqtt
|
||||
import re
|
||||
import datetime
|
||||
|
||||
from .mqtt import MQTTBase
|
||||
from typing import Optional, Union
|
||||
from .payload.relay import (
|
||||
InitialStatPayload,
|
||||
StatPayload,
|
||||
PowerPayload,
|
||||
OTAPayload,
|
||||
OTAResultPayload
|
||||
)
|
||||
from .esp import MqttEspBase
|
||||
|
||||
|
||||
class MQTTRelayDevice:
|
||||
id: str
|
||||
secret: Optional[str]
|
||||
|
||||
def __init__(self, id: str, secret: Optional[str] = None):
|
||||
self.id = id
|
||||
self.secret = secret
|
||||
|
||||
|
||||
class MQTTRelay(MQTTBase):
|
||||
_devices: list[MQTTRelayDevice]
|
||||
_message_callback: Optional[callable]
|
||||
_ota_publish_callback: Optional[callable]
|
||||
|
||||
def __init__(self,
|
||||
devices: Union[MQTTRelayDevice, list[MQTTRelayDevice]],
|
||||
subscribe_to_updates=True):
|
||||
super().__init__(clean_session=True)
|
||||
if not isinstance(devices, list):
|
||||
devices = [devices]
|
||||
self._devices = devices
|
||||
self._message_callback = None
|
||||
self._ota_publish_callback = None
|
||||
self._subscribe_to_updates = subscribe_to_updates
|
||||
self._ota_mid = None
|
||||
|
||||
def on_connect(self, client: mqtt.Client, userdata, flags, rc):
|
||||
super().on_connect(client, userdata, flags, rc)
|
||||
|
||||
if self._subscribe_to_updates:
|
||||
for device in self._devices:
|
||||
topic = f'hk/{device.id}/relay/#'
|
||||
self._logger.debug(f"subscribing to {topic}")
|
||||
client.subscribe(topic, qos=1)
|
||||
|
||||
def on_publish(self, client: mqtt.Client, userdata, mid):
|
||||
if self._ota_mid is not None and mid == self._ota_mid and self._ota_publish_callback:
|
||||
self._ota_publish_callback()
|
||||
|
||||
def set_message_callback(self, callback: callable):
|
||||
self._message_callback = callback
|
||||
|
||||
def on_message(self, client: mqtt.Client, userdata, msg):
|
||||
try:
|
||||
match = re.match(r'^hk/(.*?)/relay/(stat|stat1|power|otares)$', msg.topic)
|
||||
self._logger.debug(f'topic: {msg.topic}')
|
||||
if not match:
|
||||
return
|
||||
|
||||
device_id = match.group(1)
|
||||
subtopic = match.group(2)
|
||||
|
||||
try:
|
||||
next(d for d in self._devices if d.id == device_id)
|
||||
except StopIteration:
|
||||
return
|
||||
|
||||
message = None
|
||||
if subtopic == 'stat':
|
||||
message = StatPayload.unpack(msg.payload)
|
||||
elif subtopic == 'stat1':
|
||||
message = InitialStatPayload.unpack(msg.payload)
|
||||
elif subtopic == 'power':
|
||||
message = PowerPayload.unpack(msg.payload)
|
||||
elif subtopic == 'otares':
|
||||
message = OTAResultPayload.unpack(msg.payload)
|
||||
|
||||
if message and self._message_callback:
|
||||
self._message_callback(device_id, message)
|
||||
|
||||
except Exception as e:
|
||||
self._logger.exception(str(e))
|
||||
class MqttRelay(MqttEspBase):
|
||||
TOPIC_LEAF = 'relay'
|
||||
|
||||
def set_power(self, device_id, enable: bool, secret=None):
|
||||
device = next(d for d in self._devices if d.id == device_id)
|
||||
@ -94,29 +19,35 @@ class MQTTRelay(MQTTBase):
|
||||
|
||||
payload = PowerPayload(secret=secret,
|
||||
state=enable)
|
||||
self._client.publish(f'hk/{device.id}/relay/power',
|
||||
self._client.publish(f'hk/{device.id}/{self.TOPIC_LEAF}/power',
|
||||
payload=payload.pack(),
|
||||
qos=1)
|
||||
self._client.loop_write()
|
||||
|
||||
def push_ota(self,
|
||||
device_id,
|
||||
filename: str,
|
||||
publish_callback: callable,
|
||||
qos: int):
|
||||
device = next(d for d in self._devices if d.id == device_id)
|
||||
assert device.secret is not None, 'device secret not specified'
|
||||
def on_message(self, client: mqtt.Client, userdata, msg):
|
||||
if super().on_message(client, userdata, msg):
|
||||
return
|
||||
|
||||
self._ota_publish_callback = publish_callback
|
||||
payload = OTAPayload(secret=device.secret, filename=filename)
|
||||
publish_result = self._client.publish(f'hk/{device.id}/relay/admin/ota',
|
||||
payload=payload.pack(),
|
||||
qos=qos)
|
||||
self._ota_mid = publish_result.mid
|
||||
self._client.loop_write()
|
||||
try:
|
||||
match = re.match(self.get_mqtt_topics(['power']), msg.topic)
|
||||
if not match:
|
||||
return
|
||||
|
||||
device_id = match.group(1)
|
||||
subtopic = match.group(2)
|
||||
|
||||
message = None
|
||||
if subtopic == 'power':
|
||||
message = PowerPayload.unpack(msg.payload)
|
||||
|
||||
if message and self._message_callback:
|
||||
self._message_callback(device_id, message)
|
||||
|
||||
except Exception as e:
|
||||
self._logger.exception(str(e))
|
||||
|
||||
|
||||
class MQTTRelayState:
|
||||
class MqttRelayState:
|
||||
enabled: bool
|
||||
update_time: datetime.datetime
|
||||
rssi: int
|
||||
|
33
src/home/mqtt/temphum.py
Normal file
33
src/home/mqtt/temphum.py
Normal file
@ -0,0 +1,33 @@
|
||||
import paho.mqtt.client as mqtt
|
||||
import re
|
||||
|
||||
from .payload.temphum import (
|
||||
TempHumDataPayload
|
||||
)
|
||||
from .esp import MqttEspBase
|
||||
|
||||
|
||||
class MqttTempHum(MqttEspBase):
|
||||
TOPIC_LEAF = 'temphum'
|
||||
|
||||
def on_message(self, client: mqtt.Client, userdata, msg):
|
||||
if super().on_message(client, userdata, msg):
|
||||
return
|
||||
|
||||
try:
|
||||
match = re.match(self.get_mqtt_topics(['data']), msg.topic)
|
||||
if not match:
|
||||
return
|
||||
|
||||
device_id = match.group(1)
|
||||
subtopic = match.group(2)
|
||||
|
||||
message = None
|
||||
if subtopic == 'data':
|
||||
message = TempHumDataPayload.unpack(msg.payload)
|
||||
|
||||
if message and self._message_callback:
|
||||
self._message_callback(device_id, message)
|
||||
|
||||
except Exception as e:
|
||||
self._logger.exception(str(e))
|
@ -1,15 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
import paho.mqtt.client as mqtt
|
||||
import re
|
||||
import logging
|
||||
|
||||
from home.mqtt import MQTTBase
|
||||
from home.mqtt import MqttBase
|
||||
from home.mqtt.payload.inverter import Status, Generation
|
||||
from home.database import InverterDatabase
|
||||
from home.config import config
|
||||
|
||||
|
||||
class MQTTReceiver(MQTTBase):
|
||||
class MqttReceiver(MqttBase):
|
||||
def __init__(self):
|
||||
super().__init__(clean_session=False)
|
||||
self.database = InverterDatabase()
|
||||
@ -70,6 +69,6 @@ class MQTTReceiver(MQTTBase):
|
||||
if __name__ == '__main__':
|
||||
config.load('inverter_mqtt_receiver')
|
||||
|
||||
server = MQTTReceiver()
|
||||
server = MqttReceiver()
|
||||
server.connect_and_loop()
|
||||
|
||||
|
@ -5,11 +5,11 @@ import json
|
||||
import inverterd
|
||||
|
||||
from home.config import config
|
||||
from home.mqtt import MQTTBase, poll_tick
|
||||
from home.mqtt import MqttBase, poll_tick
|
||||
from home.mqtt.payload.inverter import Status, Generation
|
||||
|
||||
|
||||
class MQTTClient(MQTTBase):
|
||||
class MqttClient(MqttBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
@ -66,7 +66,7 @@ class MQTTClient(MQTTBase):
|
||||
if __name__ == '__main__':
|
||||
config.load('inverter_mqtt_sender')
|
||||
|
||||
client = MQTTClient()
|
||||
client = MqttClient()
|
||||
client.configure_tls()
|
||||
client.connect_and_loop(loop_forever=False)
|
||||
client.poll_inverter()
|
@ -10,7 +10,7 @@ import paho.mqtt.client as mqtt
|
||||
|
||||
from home.telegram import bot
|
||||
from home.api.types import BotType
|
||||
from home.mqtt import MQTTBase
|
||||
from home.mqtt import MqttBase
|
||||
from home.config import config
|
||||
from home.util import chunks
|
||||
from syncleo import (
|
||||
@ -204,7 +204,7 @@ class KettleInfo:
|
||||
|
||||
|
||||
class KettleController(threading.Thread,
|
||||
MQTTBase,
|
||||
MqttBase,
|
||||
DeviceListener,
|
||||
IncomingMessageListener,
|
||||
KettleInfoListener,
|
||||
@ -224,7 +224,7 @@ class KettleController(threading.Thread,
|
||||
|
||||
def __init__(self):
|
||||
# basic setup
|
||||
MQTTBase.__init__(self, clean_session=False)
|
||||
MqttBase.__init__(self, clean_session=False)
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self._logger = logging.getLogger(self.__class__.__name__)
|
||||
|
@ -8,7 +8,7 @@ import paho.mqtt.client as mqtt
|
||||
from typing import Optional
|
||||
from argparse import ArgumentParser
|
||||
from queue import SimpleQueue
|
||||
from home.mqtt import MQTTBase
|
||||
from home.mqtt import MqttBase
|
||||
from home.config import config
|
||||
from syncleo import (
|
||||
Kettle,
|
||||
@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
|
||||
control_tasks = SimpleQueue()
|
||||
|
||||
|
||||
class MQTTServer(MQTTBase):
|
||||
class MqttServer(MqttBase):
|
||||
def __init__(self):
|
||||
super().__init__(clean_session=False)
|
||||
|
||||
@ -78,7 +78,7 @@ def main():
|
||||
arg = config.load('polaris_kettle_util', use_cli=True, parser=parser)
|
||||
|
||||
if arg.mode == 'mqtt':
|
||||
server = MQTTServer()
|
||||
server = MqttServer()
|
||||
try:
|
||||
server.connect_and_loop(loop_forever=True)
|
||||
except KeyboardInterrupt:
|
||||
|
@ -8,10 +8,10 @@ from telegram import ReplyKeyboardMarkup, User
|
||||
from home.config import config
|
||||
from home.telegram import bot
|
||||
from home.telegram._botutil import user_any_name
|
||||
from home.api.types import BotType
|
||||
from home.mqtt import MQTTRelay, MQTTRelayState, MQTTRelayDevice
|
||||
from home.mqtt.payload import MQTTPayload
|
||||
from home.mqtt.payload.relay import InitialStatPayload, StatPayload
|
||||
from home.mqtt.esp import MqttEspDevice
|
||||
from home.mqtt import MqttRelay, MqttRelayState
|
||||
from home.mqtt.payload import MqttPayload
|
||||
from home.mqtt.payload.relay import InitialDiagnosticsPayload, DiagnosticsPayload
|
||||
|
||||
|
||||
config.load('pump_mqtt_bot')
|
||||
@ -70,8 +70,8 @@ bot.lang.en(
|
||||
)
|
||||
|
||||
|
||||
mqtt_relay: Optional[MQTTRelay] = None
|
||||
relay_state = MQTTRelayState()
|
||||
mqtt_relay: Optional[MqttRelay] = None
|
||||
relay_state = MqttRelayState()
|
||||
|
||||
|
||||
class UserAction(Enum):
|
||||
@ -79,10 +79,10 @@ class UserAction(Enum):
|
||||
OFF = 'off'
|
||||
|
||||
|
||||
def on_mqtt_message(home_id, message: MQTTPayload):
|
||||
if isinstance(message, InitialStatPayload) or isinstance(message, StatPayload):
|
||||
def on_mqtt_message(home_id, message: MqttPayload):
|
||||
if isinstance(message, InitialDiagnosticsPayload) or isinstance(message, DiagnosticsPayload):
|
||||
kwargs = dict(rssi=message.rssi, enabled=message.flags.state)
|
||||
if isinstance(message, InitialStatPayload):
|
||||
if isinstance(message, InitialDiagnosticsPayload):
|
||||
kwargs['fw_version'] = message.fw_version
|
||||
relay_state.update(**kwargs)
|
||||
|
||||
@ -157,8 +157,8 @@ def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]:
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
mqtt_relay = MQTTRelay(devices=MQTTRelayDevice(id=config['mqtt']['home_id'],
|
||||
secret=config['mqtt']['home_secret']))
|
||||
mqtt_relay = MqttRelay(devices=MqttEspDevice(id=config['mqtt']['home_id'],
|
||||
secret=config['mqtt']['home_secret']))
|
||||
mqtt_relay.set_message_callback(on_mqtt_message)
|
||||
mqtt_relay.configure_tls()
|
||||
mqtt_relay.connect_and_loop(loop_forever=False)
|
||||
|
@ -6,10 +6,10 @@ from functools import partial
|
||||
|
||||
from home.config import config
|
||||
from home.telegram import bot
|
||||
from home.api.types import BotType
|
||||
from home.mqtt import MQTTRelay, MQTTRelayState, MQTTRelayDevice
|
||||
from home.mqtt.payload import MQTTPayload
|
||||
from home.mqtt.payload.relay import InitialStatPayload, StatPayload
|
||||
from home.mqtt import MqttRelay, MqttRelayState
|
||||
from home.mqtt.esp import MqttEspDevice
|
||||
from home.mqtt.payload import MqttPayload
|
||||
from home.mqtt.payload.relay import InitialDiagnosticsPayload, DiagnosticsPayload
|
||||
|
||||
|
||||
config.load('relay_mqtt_bot')
|
||||
@ -34,8 +34,8 @@ status_emoji = {
|
||||
'on': '✅',
|
||||
'off': '❌'
|
||||
}
|
||||
mqtt_relay: Optional[MQTTRelay] = None
|
||||
relay_states: dict[str, MQTTRelayState] = {}
|
||||
mqtt_relay: Optional[MqttRelay] = None
|
||||
relay_states: dict[str, MqttRelayState] = {}
|
||||
|
||||
|
||||
class UserAction(Enum):
|
||||
@ -43,13 +43,13 @@ class UserAction(Enum):
|
||||
OFF = 'off'
|
||||
|
||||
|
||||
def on_mqtt_message(home_id, message: MQTTPayload):
|
||||
if isinstance(message, InitialStatPayload) or isinstance(message, StatPayload):
|
||||
def on_mqtt_message(home_id, message: MqttPayload):
|
||||
if isinstance(message, InitialDiagnosticsPayload) or isinstance(message, DiagnosticsPayload):
|
||||
kwargs = dict(rssi=message.rssi, enabled=message.flags.state)
|
||||
if isinstance(message, InitialStatPayload):
|
||||
if isinstance(message, InitialDiagnosticsPayload):
|
||||
kwargs['fw_version'] = message.fw_version
|
||||
if home_id not in relay_states:
|
||||
relay_states[home_id] = MQTTRelayState()
|
||||
relay_states[home_id] = MqttRelayState()
|
||||
relay_states[home_id].update(**kwargs)
|
||||
|
||||
|
||||
@ -87,8 +87,8 @@ def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]:
|
||||
if __name__ == '__main__':
|
||||
devices = []
|
||||
for device_id, data in config['relays'].items():
|
||||
devices.append(MQTTRelayDevice(id=device_id,
|
||||
secret=data['secret']))
|
||||
devices.append(MqttEspDevice(id=device_id,
|
||||
secret=data['secret']))
|
||||
labels = data['labels']
|
||||
bot.lang.ru(**{device_id: labels['ru']})
|
||||
bot.lang.en(**{device_id: labels['en']})
|
||||
@ -101,7 +101,7 @@ if __name__ == '__main__':
|
||||
messages.append(f'{type_emoji}{status_emoji[action.value]} {labels[_lang]}')
|
||||
bot.handler(texts=messages)(partial(enable_handler if action == UserAction.ON else disable_handler, device_id))
|
||||
|
||||
mqtt_relay = MQTTRelay(devices=devices)
|
||||
mqtt_relay = MqttRelay(devices=devices)
|
||||
mqtt_relay.set_message_callback(on_mqtt_message)
|
||||
mqtt_relay.configure_tls()
|
||||
mqtt_relay.connect_and_loop(loop_forever=False)
|
||||
|
@ -1,20 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
from home import http
|
||||
from home.config import config
|
||||
from home.mqtt import MQTTRelay, MQTTRelayDevice, MQTTRelayState
|
||||
from home.mqtt.payload import MQTTPayload
|
||||
from home.mqtt.payload.relay import InitialStatPayload, StatPayload
|
||||
from home.mqtt import MqttRelay, MqttRelayState
|
||||
from home.mqtt.esp import MqttEspDevice
|
||||
from home.mqtt.payload import MqttPayload
|
||||
from home.mqtt.payload.relay import InitialDiagnosticsPayload, DiagnosticsPayload
|
||||
from typing import Optional
|
||||
|
||||
mqtt_relay: Optional[MQTTRelay] = None
|
||||
relay_states: dict[str, MQTTRelayState] = {}
|
||||
mqtt_relay: Optional[MqttRelay] = None
|
||||
relay_states: dict[str, MqttRelayState] = {}
|
||||
|
||||
|
||||
def on_mqtt_message(device_id, message: MQTTPayload):
|
||||
if isinstance(message, InitialStatPayload) or isinstance(message, StatPayload):
|
||||
def on_mqtt_message(device_id, message: MqttPayload):
|
||||
if isinstance(message, InitialDiagnosticsPayload) or isinstance(message, DiagnosticsPayload):
|
||||
kwargs = dict(rssi=message.rssi, enabled=message.flags.state)
|
||||
if device_id not in relay_states:
|
||||
relay_states[device_id] = MQTTRelayState()
|
||||
relay_states[device_id] = MqttRelayState()
|
||||
relay_states[device_id].update(**kwargs)
|
||||
|
||||
|
||||
@ -54,7 +55,7 @@ class RelayMqttHttpProxy(http.HTTPServer):
|
||||
if __name__ == '__main__':
|
||||
config.load('relay_mqtt_http_proxy')
|
||||
|
||||
mqtt_relay = MQTTRelay(devices=[MQTTRelayDevice(id=device_id) for device_id in config.get('relay.devices')])
|
||||
mqtt_relay = MqttRelay(devices=[MqttEspDevice(id=device_id) for device_id in config.get('relay.devices')])
|
||||
mqtt_relay.configure_tls()
|
||||
mqtt_relay.set_message_callback(on_mqtt_message)
|
||||
mqtt_relay.connect_and_loop(loop_forever=False)
|
||||
|
@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import Optional
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from home.config import config
|
||||
from home.mqtt import MQTTRelay, MQTTRelayDevice
|
||||
from home.mqtt.payload import MQTTPayload
|
||||
from home.mqtt.payload.relay import (
|
||||
InitialStatPayload, StatPayload, OTAResultPayload
|
||||
)
|
||||
|
||||
mqtt_relay: Optional[MQTTRelay] = None
|
||||
|
||||
|
||||
def on_mqtt_message(device_id, p: MQTTPayload):
|
||||
message = None
|
||||
|
||||
if isinstance(p, InitialStatPayload) or isinstance(p, StatPayload):
|
||||
message = f'[stat] state={"on" if p.flags.state else "off"}'
|
||||
message += f' rssi={p.rssi}'
|
||||
message += f' free_heap={p.free_heap}'
|
||||
if isinstance(p, InitialStatPayload):
|
||||
message += f' fw={p.fw_version}'
|
||||
|
||||
elif isinstance(p, OTAResultPayload):
|
||||
message = f'[otares] result={p.result} error_code={p.error_code}'
|
||||
|
||||
if message:
|
||||
print(message)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('--device-id', type=str, required=True)
|
||||
|
||||
config.load('relay_mqtt_util', parser=parser)
|
||||
arg = parser.parse_args()
|
||||
|
||||
mqtt_relay = MQTTRelay(devices=MQTTRelayDevice(id=arg.device_id))
|
||||
mqtt_relay.set_message_callback(on_mqtt_message)
|
||||
mqtt_relay.configure_tls()
|
||||
try:
|
||||
mqtt_relay.connect_and_loop()
|
||||
except KeyboardInterrupt:
|
||||
mqtt_relay.disconnect()
|
@ -2,7 +2,7 @@
|
||||
import paho.mqtt.client as mqtt
|
||||
import re
|
||||
|
||||
from home.mqtt import MQTTBase
|
||||
from home.mqtt import MqttBase
|
||||
from home.config import config
|
||||
from home.mqtt.payload.sensors import Temperature
|
||||
from home.api.types import TemperatureSensorLocation
|
||||
@ -16,7 +16,7 @@ def get_sensor_type(sensor: str) -> TemperatureSensorLocation:
|
||||
raise ValueError(f'unexpected sensor value: {sensor}')
|
||||
|
||||
|
||||
class MQTTServer(MQTTBase):
|
||||
class MqttServer(MqttBase):
|
||||
def __init__(self):
|
||||
super().__init__(clean_session=False)
|
||||
self.database = SensorsDatabase()
|
||||
@ -49,5 +49,5 @@ class MQTTServer(MQTTBase):
|
||||
if __name__ == '__main__':
|
||||
config.load('sensors_mqtt_receiver')
|
||||
|
||||
server = MQTTServer()
|
||||
server = MqttServer()
|
||||
server.connect_and_loop()
|
||||
|
@ -3,12 +3,12 @@ import time
|
||||
import json
|
||||
|
||||
from home.util import parse_addr, MySimpleSocketClient
|
||||
from home.mqtt import MQTTBase, poll_tick
|
||||
from home.mqtt import MqttBase, poll_tick
|
||||
from home.mqtt.payload.sensors import Temperature
|
||||
from home.config import config
|
||||
|
||||
|
||||
class MQTTClient(MQTTBase):
|
||||
class MqttClient(MqttBase):
|
||||
def __init__(self):
|
||||
super().__init__(self)
|
||||
self._home_id = config['mqtt']['home_id']
|
||||
@ -52,7 +52,7 @@ class MQTTClient(MQTTBase):
|
||||
if __name__ == '__main__':
|
||||
config.load('sensors_mqtt_sender')
|
||||
|
||||
client = MQTTClient()
|
||||
client = MqttClient()
|
||||
client.configure_tls()
|
||||
client.connect_and_loop(loop_forever=False)
|
||||
client.poll()
|
||||
|
1
src/temphum.py
Normal file → Executable file
1
src/temphum.py
Normal file → Executable file
@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
from argparse import ArgumentParser
|
||||
from home.temphum import SensorType, create_sensor
|
||||
|
||||
|
@ -10,7 +10,7 @@ sys.path.extend([
|
||||
from time import sleep
|
||||
from argparse import ArgumentParser
|
||||
from src.home.config import config
|
||||
from src.home.mqtt import MQTTRelay, MQTTRelayDevice
|
||||
from src.home.mqtt import MqttRelay, MQTTESPDevice
|
||||
|
||||
|
||||
def guess_filename(product: str, build_target: str):
|
||||
@ -34,7 +34,7 @@ def relayctl_publish_ota(filename: str,
|
||||
global stop
|
||||
stop = True
|
||||
|
||||
mqtt_relay = MQTTRelay(devices=MQTTRelayDevice(id=device_id, secret=home_secret))
|
||||
mqtt_relay = MqttRelay(devices=MQTTESPDevice(id=device_id, secret=home_secret))
|
||||
mqtt_relay.configure_tls()
|
||||
mqtt_relay.connect_and_loop(loop_forever=False)
|
||||
mqtt_relay.push_ota(device_id, filename, published, qos)
|
||||
|
Loading…
x
Reference in New Issue
Block a user