esp8266 relay controller wip
This commit is contained in:
parent
16d47968b4
commit
7bb6daa4bf
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,12 +1,17 @@
|
||||
.idea
|
||||
/venv
|
||||
/node_modules
|
||||
*.pyc
|
||||
config.def.h
|
||||
__pycache__
|
||||
.DS_Store
|
||||
/src/test/test_inverter_monitor.log
|
||||
/youtrack-certificate
|
||||
/cpp
|
||||
/esp32-cam/CameraWebServer/wifi_password.h
|
||||
cmake-build-*
|
||||
.pio
|
||||
CMakeListsPrivate.txt
|
||||
|
||||
*.swp
|
||||
|
||||
|
14
package.json
Normal file
14
package.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "homekit",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"repository": "git@ch1p.io:homekit.git",
|
||||
"author": "Evgeny Zinoviev <me@ch1p.io>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"clean-css": "^5.3.1",
|
||||
"html-minifier-terser": "^7.1.0",
|
||||
"minimist": "^1.2.7",
|
||||
"terser": "^5.16.1"
|
||||
}
|
||||
}
|
3
platformio/relayctl/.gitignore
vendored
Normal file
3
platformio/relayctl/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.pio
|
||||
CMakeListsPrivate.txt
|
||||
cmake-build-*/
|
33
platformio/relayctl/CMakeLists.txt
Normal file
33
platformio/relayctl/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("relayctl" 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})
|
87
platformio/relayctl/make_static.sh
Executable file
87
platformio/relayctl/make_static.sh
Executable file
@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
|
||||
#set -x
|
||||
#set -e
|
||||
|
||||
DIR="$(dirname "$(realpath "$0")")"
|
||||
|
||||
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"
|
||||
|
||||
[ -f "$header" ] && rm "$header"
|
||||
[ -f "$source" ] && rm "$source"
|
||||
|
||||
is_minifyable() {
|
||||
local ext="$1"
|
||||
[ "$ext" = "html" ] || [ "$ext" = "css" ] || [ "$ext" = "js" ]
|
||||
}
|
||||
|
||||
minify() {
|
||||
local ext="$1"
|
||||
local bin="$(realpath "$DIR"/../../tools/minify.js)"
|
||||
"$bin" --type "$ext"
|
||||
}
|
||||
|
||||
# .h header
|
||||
cat <<EOF >> "$header"
|
||||
/**
|
||||
* This file is autogenerated with make_static.sh script
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
namespace homekit::files {
|
||||
|
||||
typedef struct {
|
||||
size_t size;
|
||||
const uint8_t* content;
|
||||
} StaticFile;
|
||||
|
||||
EOF
|
||||
|
||||
cat <<EOF >> "$source"
|
||||
/**
|
||||
* This file is autogenerated with make_static.sh script
|
||||
*/
|
||||
|
||||
#include "static.h"
|
||||
|
||||
namespace homekit::files {
|
||||
|
||||
EOF
|
||||
|
||||
# loop over files
|
||||
for ext in html js css ico; do
|
||||
for f in "$DIR"/static/*.$ext; do
|
||||
filename="$(basename "$f")"
|
||||
echo "processing ${filename}..."
|
||||
filename="${filename/./_}"
|
||||
|
||||
# write .h
|
||||
echo "extern const StaticFile $filename;" >> "$header"
|
||||
|
||||
# write .c
|
||||
{
|
||||
echo "static const uint8_t ${filename}_content[] PROGMEM = {"
|
||||
|
||||
cat "$f" |
|
||||
( [ "$ext" = "html" ] && sed "s/{version}/$fw_version/" || cat ) |
|
||||
( is_minifyable "$ext" && minify "$ext" || cat ) |
|
||||
gzip |
|
||||
xxd -ps -c 16 |
|
||||
sed 's/.\{2\}/0x&, /g' |
|
||||
sed 's/^/ /'
|
||||
|
||||
echo "};"
|
||||
echo "const StaticFile $filename PROGMEM = {(sizeof(${filename}_content)/sizeof(${filename}_content[0])), ${filename}_content};"
|
||||
echo ""
|
||||
} >> "$source"
|
||||
done
|
||||
done
|
||||
|
||||
# end of homekit::files
|
||||
( echo ""; echo "}" ) >> "$header"
|
||||
echo "}" >> "$source"
|
20
platformio/relayctl/platformio.ini
Normal file
20
platformio/relayctl/platformio.ini
Normal file
@ -0,0 +1,20 @@
|
||||
; 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 =
|
||||
ESP Async WebServer
|
||||
knolleary/PubSubClient@^2.8
|
||||
me-no-dev/ESPAsyncTCP@^1.2.2
|
88
platformio/relayctl/src/config.cpp
Normal file
88
platformio/relayctl/src/config.cpp
Normal file
@ -0,0 +1,88 @@
|
||||
#include <EEPROM.h>
|
||||
#include <strings.h>
|
||||
#include "config.h"
|
||||
#include "config.def.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] = {
|
||||
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 = crc_table[(crc ^ data[index]) & 0x0f] ^ (crc >> 4);
|
||||
crc = crc_table[(crc ^ (data[index] >> 4)) & 0x0f] ^ (crc >> 4);
|
||||
crc = ~crc;
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
ConfigData read() {
|
||||
ConfigData data {0};
|
||||
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;
|
||||
}
|
||||
|
||||
bool write(ConfigData& data) {
|
||||
EEPROM.begin(sizeof(ConfigData));
|
||||
data.magic = magic;
|
||||
data.crc = GET_DATA_CRC(data);
|
||||
EEPROM.put(0, data);
|
||||
return EEPROM.end();
|
||||
}
|
||||
|
||||
bool erase() {
|
||||
ConfigData data;
|
||||
return erase(data);
|
||||
}
|
||||
|
||||
bool erase(ConfigData& data) {
|
||||
bzero(reinterpret_cast<uint8_t*>(&data), sizeof(data));
|
||||
data.magic = magic;
|
||||
EEPROM.begin(sizeof(data));
|
||||
EEPROM.put(0, data);
|
||||
return EEPROM.end();
|
||||
}
|
||||
|
||||
bool isValid(ConfigData& data) {
|
||||
return data.crc == GET_DATA_CRC(data);
|
||||
}
|
||||
|
||||
bool isDirty(ConfigData& data) {
|
||||
return data.magic != magic;
|
||||
}
|
||||
|
||||
char* ConfigData::escapeNodeId(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;
|
||||
}
|
||||
|
||||
}
|
32
platformio/relayctl/src/config.def.h.example
Normal file
32
platformio/relayctl/src/config.def.h.example
Normal file
@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#define FW_VERSION 3
|
||||
|
||||
#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
|
||||
|
||||
// 12 bytes string
|
||||
#define SECRET ""
|
||||
|
||||
#define DEBUG
|
||||
|
||||
#define ARRAY_SIZE(X) sizeof((X))/sizeof((X)[0])
|
34
platformio/relayctl/src/config.h
Normal file
34
platformio/relayctl/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* escapeNodeId(char* buf, size_t len);
|
||||
} __attribute__((packed));
|
||||
|
||||
|
||||
ConfigData read();
|
||||
bool write(ConfigData& data);
|
||||
bool erase();
|
||||
bool erase(ConfigData& data);
|
||||
bool isValid(ConfigData& data);
|
||||
bool isDirty(ConfigData& data);
|
||||
|
||||
}
|
249
platformio/relayctl/src/http_server.cpp
Normal file
249
platformio/relayctl/src/http_server.cpp
Normal file
@ -0,0 +1,249 @@
|
||||
#include <Arduino.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "http_server.h"
|
||||
#include "config.h"
|
||||
#include "wifi.h"
|
||||
#include "config.def.h"
|
||||
#include "logging.h"
|
||||
|
||||
namespace homekit {
|
||||
|
||||
static const char CONTENT_TYPE_HTML[] = "text/html; charset=utf-8";
|
||||
static const char CONTENT_TYPE_CSS[] = "text/css";
|
||||
static const char CONTENT_TYPE_JS[] = "application/javascript";
|
||||
static const char CONTENT_TYPE_JSON[] = "application/json";
|
||||
static const char CONTENT_TYPE_FAVICON[] = "image/x-icon";
|
||||
|
||||
static const char JSON_STATUS_FMT[] = "{\"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 NODE_ID_ERROR[] = "?";
|
||||
|
||||
static const char FIELD_NODE_ID[] = "nid";
|
||||
static const char FIELD_SSID[] = "ssid";
|
||||
static const char FIELD_PSK[] = "psk";
|
||||
|
||||
static const char MSG_IS_INVALID[] = " is invalid";
|
||||
static const char MSG_IS_MISSING[] = " is missing";
|
||||
|
||||
static const char GZIP[] = "gzip";
|
||||
static const char CONTENT_ENCODING[] = "Content-Encoding";
|
||||
static const char NOT_FOUND[] = "Not Found";
|
||||
|
||||
static void do_restart() {
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
void HttpServer::start() {
|
||||
_server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest* req) { sendGzip(req, files::style_css, CONTENT_TYPE_CSS); });
|
||||
_server.on("/app.js", HTTP_GET, [](AsyncWebServerRequest* req) { sendGzip(req, files::app_js, CONTENT_TYPE_JS); });
|
||||
_server.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest* req) { sendGzip(req, files::favicon_ico, CONTENT_TYPE_FAVICON); });
|
||||
_server.on("/", HTTP_GET, [&](AsyncWebServerRequest* req) { sendGzip(req, files::index_html, CONTENT_TYPE_HTML); });
|
||||
|
||||
_server.on("/status", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
char json_buf[JSON_BUF_SIZE];
|
||||
auto cfg = config::read();
|
||||
char *ssid, *psk;
|
||||
wifi::getConfig(cfg, &ssid, &psk, nullptr);
|
||||
|
||||
if (!isValid(cfg) || !cfg.flags.node_configured) {
|
||||
sprintf(json_buf, JSON_STATUS_FMT
|
||||
, DEFAULT_NODE_ID
|
||||
#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.escapeNodeId(escaped_node_id, 32);
|
||||
sprintf(json_buf, JSON_STATUS_FMT
|
||||
, escaped_node_id_res == nullptr ? NODE_ID_ERROR : escaped_node_id
|
||||
#ifdef DEBUG
|
||||
, 1
|
||||
, cfg.crc
|
||||
, cfg.flags.node_configured
|
||||
, cfg.flags.wifi_configured
|
||||
#endif
|
||||
);
|
||||
}
|
||||
req->send(200, CONTENT_TYPE_JSON, json_buf);
|
||||
});
|
||||
|
||||
_server.on("/status", HTTP_POST, [&](AsyncWebServerRequest* req) {
|
||||
auto cfg = config::read();
|
||||
char *s;
|
||||
|
||||
if (!handleInputStr(req, FIELD_SSID, 32, &s)) return;
|
||||
strncpy(cfg.wifi_ssid, s, 32);
|
||||
PRINTF("saving ssid: %s\n", cfg.wifi_ssid);
|
||||
|
||||
if (!handleInputStr(req, FIELD_PSK, 63, &s)) return;
|
||||
strncpy(cfg.wifi_psk, s, 63);
|
||||
PRINTF("saving psk: %s\n", cfg.wifi_psk);
|
||||
|
||||
if (!handleInputStr(req, FIELD_NODE_ID, 16, &s)) return;
|
||||
strcpy(cfg.node_id, s);
|
||||
PRINTF("saving node id: %s\n", cfg.node_id);
|
||||
|
||||
cfg.flags.node_configured = 1;
|
||||
cfg.flags.wifi_configured = 1;
|
||||
|
||||
if (!config::write(cfg)) {
|
||||
PRINTLN("eeprom write error");
|
||||
return sendError(req, "eeprom error");
|
||||
}
|
||||
|
||||
restartTimer.once(0, do_restart);
|
||||
});
|
||||
|
||||
_server.on("/reset", HTTP_POST, [&](AsyncWebServerRequest* req) {
|
||||
config::erase();
|
||||
restartTimer.once(0, do_restart);
|
||||
});
|
||||
|
||||
_server.on("/heap", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
req->send(200, CONTENT_TYPE_HTML, String(ESP.getFreeHeap()));
|
||||
});
|
||||
|
||||
_server.on("/scan", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||
int i = 0;
|
||||
size_t len;
|
||||
const char* ssid;
|
||||
bool enough = false;
|
||||
|
||||
bzero(reinterpret_cast<uint8_t*>(buf1k), ARRAY_SIZE(buf1k));
|
||||
char* cur = buf1k;
|
||||
|
||||
strncpy(cur, "{\"list\":[", ARRAY_SIZE(buf1k));
|
||||
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 (cur - buf1k >= ARRAY_SIZE(buf1k)-40)
|
||||
enough = true;
|
||||
|
||||
if (i < _scanResults->size()-1 || enough)
|
||||
*cur++ = ',';
|
||||
|
||||
if (enough)
|
||||
break;
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
*cur++ = ']';
|
||||
*cur++ = '}';
|
||||
*cur++ = '\0';
|
||||
|
||||
req->send(200, CONTENT_TYPE_JSON, buf1k);
|
||||
});
|
||||
|
||||
_server.on("/update", HTTP_POST, [&](AsyncWebServerRequest* req) {
|
||||
char json_buf[16];
|
||||
bool should_reboot = !Update.hasError();
|
||||
|
||||
sprintf(json_buf, "{\"result\":%d}", should_reboot ? 1 : 0);
|
||||
|
||||
auto resp = req->beginResponse(200, CONTENT_TYPE_JSON, json_buf);
|
||||
req->send(resp);
|
||||
|
||||
if (should_reboot) restartTimer.once(1, do_restart);
|
||||
}, [&](AsyncWebServerRequest *req, const String& filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||
if (!index) {
|
||||
PRINTF("update start: %s\n", filename.c_str());
|
||||
Update.runAsync(true);
|
||||
if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000))
|
||||
Update.printError(Serial);
|
||||
}
|
||||
|
||||
if (!Update.hasError() && len) {
|
||||
if (Update.write(data, len) != len) {
|
||||
Update.printError(Serial);
|
||||
}
|
||||
}
|
||||
|
||||
if (final) { // if the final flag is set then this is the last frame of data
|
||||
if (Update.end(true)) {
|
||||
PRINTF("update success: %uB\n", index+len);
|
||||
} else {
|
||||
Update.printError(Serial);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_server.onNotFound([](AsyncWebServerRequest* req) {
|
||||
req->send(404, CONTENT_TYPE_HTML, NOT_FOUND);
|
||||
});
|
||||
|
||||
_server.begin();
|
||||
}
|
||||
|
||||
void HttpServer::sendGzip(AsyncWebServerRequest* req, StaticFile file, const char* content_type) {
|
||||
auto resp = req->beginResponse_P(200, content_type, file.content, file.size);
|
||||
resp->addHeader(CONTENT_ENCODING, GZIP);
|
||||
req->send(resp);
|
||||
}
|
||||
|
||||
void HttpServer::sendError(AsyncWebServerRequest* req, const String& message) {
|
||||
char buf[32];
|
||||
if (snprintf(buf, 32, "error: %s", message.c_str()) == 32)
|
||||
buf[31] = '\0';
|
||||
req->send(400, CONTENT_TYPE_HTML, buf);
|
||||
}
|
||||
|
||||
bool HttpServer::handleInputStr(AsyncWebServerRequest *req,
|
||||
const char *field_name,
|
||||
size_t max_len,
|
||||
char **dst) {
|
||||
const char* s;
|
||||
size_t len;
|
||||
|
||||
if (!req->hasParam(field_name, true)) {
|
||||
sendError(req, String(field_name) + String(MSG_IS_MISSING));
|
||||
return false;
|
||||
}
|
||||
|
||||
s = req->getParam(field_name, true)->value().c_str();
|
||||
len = strlen(s);
|
||||
if (!len || len > max_len) {
|
||||
sendError(req, String(FIELD_NODE_ID) + String(MSG_IS_INVALID));
|
||||
return false;
|
||||
}
|
||||
|
||||
*dst = (char*)s;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
37
platformio/relayctl/src/http_server.h
Normal file
37
platformio/relayctl/src/http_server.h
Normal file
@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <list>
|
||||
#include <Ticker.h>
|
||||
#include <utility>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include "static.h"
|
||||
#include "config.h"
|
||||
#include "wifi.h"
|
||||
|
||||
namespace homekit {
|
||||
|
||||
using files::StaticFile;
|
||||
|
||||
class HttpServer {
|
||||
private:
|
||||
AsyncWebServer _server;
|
||||
Ticker restartTimer;
|
||||
std::shared_ptr<std::list<wifi::ScanResult>> _scanResults;
|
||||
char buf1k[1024];
|
||||
|
||||
static void sendGzip(AsyncWebServerRequest* req, StaticFile file, const char* content_type);
|
||||
static void sendError(AsyncWebServerRequest* req, const String& message);
|
||||
|
||||
static bool handleInputStr(AsyncWebServerRequest* req, const char* field_name, size_t max_len, char** dst);
|
||||
// static bool handle_input_addr(AsyncWebServerRequest* req, const char* field_name, ConfigIPv4Addr* addr_dst);
|
||||
|
||||
public:
|
||||
explicit HttpServer(std::shared_ptr<std::list<wifi::ScanResult>> scanResults)
|
||||
: _server(80)
|
||||
, _scanResults(std::move(scanResults)) {};
|
||||
|
||||
void start();
|
||||
};
|
||||
|
||||
}
|
20
platformio/relayctl/src/led.cpp
Normal file
20
platformio/relayctl/src/led.cpp
Normal file
@ -0,0 +1,20 @@
|
||||
#include "led.h"
|
||||
|
||||
namespace homekit {
|
||||
|
||||
void Led::blink(uint8_t count, uint16_t delay_ms) const {
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
on_off(delay_ms, i < count-1);
|
||||
}
|
||||
}
|
||||
|
||||
void Led::on_off(uint16_t delay_ms, bool last_delay) const {
|
||||
on();
|
||||
delay(delay_ms);
|
||||
|
||||
off();
|
||||
if (last_delay)
|
||||
delay(delay_ms);
|
||||
}
|
||||
|
||||
}
|
24
platformio/relayctl/src/led.h
Normal file
24
platformio/relayctl/src/led.h
Normal file
@ -0,0 +1,24 @@
|
||||
#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;
|
||||
void blink(uint8_t count, uint16_t delay_ms) const;
|
||||
};
|
||||
|
||||
}
|
37
platformio/relayctl/src/logging.cpp
Normal file
37
platformio/relayctl/src/logging.cpp
Normal file
@ -0,0 +1,37 @@
|
||||
#include <stdio.h>
|
||||
#include "logging.h"
|
||||
|
||||
#ifdef DEBUG
|
||||
namespace homekit {
|
||||
|
||||
void hexdump(const void* data, size_t size) {
|
||||
char ascii[17];
|
||||
size_t i, j;
|
||||
ascii[16] = '\0';
|
||||
for (i = 0; i < size; ++i) {
|
||||
printf("%02X ", ((unsigned char*)data)[i]);
|
||||
if (((unsigned char*)data)[i] >= ' ' && ((unsigned char*)data)[i] <= '~') {
|
||||
ascii[i % 16] = ((unsigned char*)data)[i];
|
||||
} else {
|
||||
ascii[i % 16] = '.';
|
||||
}
|
||||
if ((i+1) % 8 == 0 || i+1 == size) {
|
||||
printf(" ");
|
||||
if ((i+1) % 16 == 0) {
|
||||
printf("| %s \n", ascii);
|
||||
} else if (i+1 == size) {
|
||||
ascii[(i+1) % 16] = '\0';
|
||||
if ((i+1) % 16 <= 8) {
|
||||
printf(" ");
|
||||
}
|
||||
for (j = (i+1) % 16; j < 16; ++j) {
|
||||
printf(" ");
|
||||
}
|
||||
printf("| %s \n", ascii);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
24
platformio/relayctl/src/logging.h
Normal file
24
platformio/relayctl/src/logging.h
Normal file
@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
#include "config.def.h"
|
||||
|
||||
#ifdef DEBUG
|
||||
|
||||
namespace homekit {
|
||||
void hexdump(const void* data, size_t size);
|
||||
}
|
||||
|
||||
#define PRINTLN(s) Serial.println(s)
|
||||
#define PRINT(s) Serial.print(s)
|
||||
#define PRINTF(fmt, ...) Serial.printf(fmt, ##__VA_ARGS__)
|
||||
#define HEXDUMP(data, size) homekit::hexdump((data), (size));
|
||||
|
||||
#else
|
||||
|
||||
#define PRINTLN(s)
|
||||
#define PRINT(s)
|
||||
#define PRINTF(a)
|
||||
#define HEXDUMP(data, size)
|
||||
|
||||
#endif
|
170
platformio/relayctl/src/main.cpp
Normal file
170
platformio/relayctl/src/main.cpp
Normal file
@ -0,0 +1,170 @@
|
||||
#include <Arduino.h>
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <DNSServer.h>
|
||||
#include <Ticker.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 "relay.h"
|
||||
#include "stopwatch.h"
|
||||
|
||||
using namespace homekit;
|
||||
|
||||
static Led board_led(BOARD_LED_PIN);
|
||||
static Led esp_led(ESP_LED_PIN);
|
||||
|
||||
enum class WorkingMode {
|
||||
RECOVERY, // AP mode, http server with configuration
|
||||
NORMAL, // MQTT client
|
||||
};
|
||||
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;
|
||||
static StopWatch blinkStopWatch;
|
||||
|
||||
static DNSServer* dnsServer;
|
||||
|
||||
static void onWifiConnected(const WiFiEventStationModeGotIP& event);
|
||||
static void onWifiDisconnected(const WiFiEventStationModeDisconnected& event);
|
||||
|
||||
static void wifiConnect() {
|
||||
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.setHostname(hostname);
|
||||
WiFi.begin(ssid, psk);
|
||||
|
||||
PRINT("connecting to wifi..");
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
esp_led.blink(2, 50);
|
||||
delay(1000);
|
||||
PRINT('.');
|
||||
}
|
||||
PRINT(' ');
|
||||
}
|
||||
|
||||
static void wifiHotspot() {
|
||||
esp_led.on();
|
||||
|
||||
auto scanResults = wifi::scan();
|
||||
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP(wifi::WIFI_AP_SSID);
|
||||
|
||||
dnsServer = new DNSServer();
|
||||
dnsServer->start(53, "*", WiFi.softAPIP());
|
||||
|
||||
service = new HttpServer(scanResults);
|
||||
((HttpServer*)service)->start();
|
||||
}
|
||||
|
||||
void setup() {
|
||||
#ifdef DEBUG
|
||||
Serial.begin(115200);
|
||||
#endif
|
||||
|
||||
relay::init();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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::JUST_CONNECTED) {
|
||||
board_led.blink(3, 300);
|
||||
wifi_state = WiFiConnectionState::CONNECTED;
|
||||
|
||||
if (service == nullptr)
|
||||
service = new mqtt::MQTT();
|
||||
|
||||
((mqtt::MQTT*)service)->connect();
|
||||
blinkStopWatch.save();
|
||||
}
|
||||
|
||||
auto mqtt = (mqtt::MQTT*)service;
|
||||
if (static_cast<int>(wifi_state) >= 1
|
||||
&& mqtt != nullptr) {
|
||||
if (!mqtt->loop()) {
|
||||
PRINTLN("mqtt::loop() returned false");
|
||||
// FIXME do something here
|
||||
}
|
||||
|
||||
if (mqtt->statStopWatch.elapsed(10000)) {
|
||||
mqtt->sendStat();
|
||||
}
|
||||
|
||||
// periodically blink board led
|
||||
if (blinkStopWatch.elapsed(5000)) {
|
||||
board_led.blink(1, 10);
|
||||
blinkStopWatch.save();
|
||||
}
|
||||
}
|
||||
|
||||
delay(500);
|
||||
} else {
|
||||
if (dnsServer != nullptr)
|
||||
dnsServer->processNextRequest();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
172
platformio/relayctl/src/mqtt.cpp
Normal file
172
platformio/relayctl/src/mqtt.cpp
Normal file
@ -0,0 +1,172 @@
|
||||
#include "mqtt.h"
|
||||
#include "logging.h"
|
||||
#include "wifi.h"
|
||||
#include "config.def.h"
|
||||
#include "relay.h"
|
||||
#include "config.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[] = SECRET;
|
||||
static const char TOPIC_RELAY_POWER[] = "relay/power";
|
||||
static const char TOPIC_STAT[] = "stat";
|
||||
static const char TOPIC_STAT1[] = "stat1";
|
||||
static const char TOPIC_ADMIN[] = "admin";
|
||||
static const char TOPIC_RELAY[] = "relay";
|
||||
|
||||
|
||||
using namespace homekit;
|
||||
|
||||
MQTT::MQTT() : client(wifiClient) {
|
||||
randomSeed(micros());
|
||||
|
||||
wifiClient.setFingerprint(MQTT_CA_FINGERPRINT);
|
||||
|
||||
client.setServer(MQTT_SERVER, MQTT_PORT);
|
||||
client.setCallback([&](char* topic, byte* payload, unsigned int length) {
|
||||
this->callback(topic, payload, length);
|
||||
});
|
||||
}
|
||||
|
||||
void MQTT::connect() {
|
||||
reconnect();
|
||||
}
|
||||
|
||||
void MQTT::reconnect() {
|
||||
char buf[128] {0};
|
||||
|
||||
if (client.connected()) {
|
||||
PRINTLN("warning: already connected");
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to connect
|
||||
if (client.connect(MQTT_CLIENT_ID, MQTT_USERNAME, MQTT_PASSWORD)) {
|
||||
PRINTLN("mqtt: connected");
|
||||
|
||||
sendInitialStat();
|
||||
|
||||
subscribe(TOPIC_RELAY);
|
||||
subscribe(TOPIC_ADMIN);
|
||||
} else {
|
||||
PRINTF("mqtt: failed to connect, rc=%d\n", client.state());
|
||||
wifiClient.getLastSSLError(buf, sizeof(buf));
|
||||
PRINTF("SSL error: %s\n", buf);
|
||||
|
||||
reconnectTimer.once(2, [&]() {
|
||||
reconnect();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void MQTT::disconnect() {
|
||||
// TODO test how this works???
|
||||
reconnectTimer.detach();
|
||||
client.disconnect();
|
||||
wifiClient.stop();
|
||||
}
|
||||
|
||||
bool MQTT::loop() {
|
||||
return client.loop();
|
||||
}
|
||||
|
||||
bool MQTT::publish(const char* topic, uint8_t *payload, size_t length) {
|
||||
char full_topic[40] {0};
|
||||
strcpy(full_topic, "/hk/");
|
||||
strcat(full_topic, wifi::NODE_ID);
|
||||
strcat(full_topic, "/");
|
||||
strcat(full_topic, topic);
|
||||
return client.publish(full_topic, payload, length);
|
||||
}
|
||||
|
||||
bool MQTT::subscribe(const char *topic) {
|
||||
char full_topic[40] {0};
|
||||
strcpy(full_topic, "/hk/");
|
||||
strcat(full_topic, wifi::NODE_ID);
|
||||
strcat(full_topic, "/");
|
||||
strcat(full_topic, topic);
|
||||
strcat(full_topic, "/#");
|
||||
bool res = client.subscribe(full_topic, 1);
|
||||
if (!res)
|
||||
PRINTF("error: failed to subscribe to %s\n", full_topic);
|
||||
return res;
|
||||
}
|
||||
|
||||
void MQTT::sendInitialStat() {
|
||||
auto cfg = config::read();
|
||||
InitialStatPayload stat {
|
||||
.ip = wifi::getIPAsInteger(),
|
||||
.fw_version = FW_VERSION,
|
||||
.rssi = wifi::getRSSI(),
|
||||
.free_heap = ESP.getFreeHeap(),
|
||||
.flags = StatFlags {
|
||||
.state = static_cast<uint8_t>(relay::getState() ? 1 : 0),
|
||||
.config_changed_value_present = 1,
|
||||
.config_changed = static_cast<uint8_t>(cfg.flags.node_configured || cfg.flags.wifi_configured ? 1 : 0)
|
||||
}
|
||||
};
|
||||
publish(TOPIC_STAT1, reinterpret_cast<uint8_t*>(&stat), sizeof(stat));
|
||||
statStopWatch.save();
|
||||
}
|
||||
|
||||
void MQTT::sendStat() {
|
||||
StatPayload stat {
|
||||
.rssi = wifi::getRSSI(),
|
||||
.free_heap = ESP.getFreeHeap(),
|
||||
.flags = StatFlags {
|
||||
.state = static_cast<uint8_t>(relay::getState() ? 1 : 0),
|
||||
.config_changed_value_present = 0,
|
||||
.config_changed = 0
|
||||
}
|
||||
};
|
||||
|
||||
PRINTF("free heap: %d\n", ESP.getFreeHeap());
|
||||
|
||||
publish(TOPIC_STAT, reinterpret_cast<uint8_t*>(&stat), sizeof(stat));
|
||||
statStopWatch.save();
|
||||
}
|
||||
|
||||
void MQTT::callback(char* topic, uint8_t* payload, uint32_t length) {
|
||||
const size_t bufsize = 16;
|
||||
char relevant_topic[bufsize];
|
||||
strncpy(relevant_topic, topic+strlen(wifi::NODE_ID)+5, bufsize);
|
||||
|
||||
if (strncmp(TOPIC_RELAY_POWER, relevant_topic, bufsize) == 0) {
|
||||
handleRelayPowerPayload(payload, length);
|
||||
} else {
|
||||
PRINTF("error: invalid topic %s\n", topic);
|
||||
}
|
||||
}
|
||||
|
||||
void MQTT::handleRelayPowerPayload(uint8_t *payload, uint32_t length) {
|
||||
if (length != sizeof(PowerPayload)) {
|
||||
PRINTF("error: size of payload (%ul) does not match expected (%ul)\n",
|
||||
length, sizeof(PowerPayload));
|
||||
return;
|
||||
}
|
||||
|
||||
auto pd = reinterpret_cast<struct PowerPayload*>(payload);
|
||||
if (strncmp(pd->secret, MQTT_SECRET, sizeof(pd->secret)) != 0) {
|
||||
PRINTLN("error: invalid secret");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pd->state == 1) {
|
||||
relay::setOn();
|
||||
} else if (pd->state == 0) {
|
||||
relay::setOff();
|
||||
} else {
|
||||
PRINTLN("error: unexpected state value");
|
||||
}
|
||||
|
||||
sendStat();
|
||||
}
|
||||
|
||||
}
|
57
platformio/relayctl/src/mqtt.h
Normal file
57
platformio/relayctl/src/mqtt.h
Normal file
@ -0,0 +1,57 @@
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <PubSubClient.h>
|
||||
#include <Ticker.h>
|
||||
#include "stopwatch.h"
|
||||
|
||||
namespace homekit::mqtt {
|
||||
|
||||
class MQTT {
|
||||
private:
|
||||
WiFiClientSecure wifiClient;
|
||||
PubSubClient client;
|
||||
Ticker reconnectTimer;
|
||||
|
||||
void callback(char* topic, uint8_t* payload, size_t length);
|
||||
void handleRelayPowerPayload(uint8_t* payload, uint32_t length);
|
||||
bool publish(const char* topic, uint8_t* payload, size_t length);
|
||||
bool subscribe(const char* topic);
|
||||
void sendInitialStat();
|
||||
|
||||
public:
|
||||
StopWatch statStopWatch;
|
||||
|
||||
MQTT();
|
||||
void connect();
|
||||
void disconnect();
|
||||
void reconnect();
|
||||
bool loop();
|
||||
void sendStat();
|
||||
};
|
||||
|
||||
struct StatFlags {
|
||||
uint8_t state: 1;
|
||||
uint8_t config_changed_value_present: 1;
|
||||
uint8_t config_changed: 1;
|
||||
uint8_t reserved: 5;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct InitialStatPayload {
|
||||
uint32_t ip;
|
||||
uint8_t fw_version;
|
||||
int8_t rssi;
|
||||
uint32_t free_heap;
|
||||
StatFlags flags;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct StatPayload {
|
||||
int8_t rssi;
|
||||
uint32_t free_heap;
|
||||
StatFlags flags;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PowerPayload {
|
||||
char secret[12];
|
||||
uint8_t state;
|
||||
} __attribute__((packed));
|
||||
|
||||
}
|
24
platformio/relayctl/src/relay.h
Normal file
24
platformio/relayctl/src/relay.h
Normal file
@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "config.def.h"
|
||||
|
||||
namespace homekit::relay {
|
||||
|
||||
inline void init() {
|
||||
pinMode(RELAY_PIN, OUTPUT);
|
||||
}
|
||||
|
||||
inline bool getState() {
|
||||
return digitalRead(RELAY_PIN) == 1;
|
||||
}
|
||||
|
||||
inline void setOn() {
|
||||
digitalWrite(RELAY_PIN, HIGH);
|
||||
}
|
||||
|
||||
inline void setOff() {
|
||||
digitalWrite(RELAY_PIN, LOW);
|
||||
}
|
||||
|
||||
}
|
276
platformio/relayctl/src/static.cpp
Normal file
276
platformio/relayctl/src/static.cpp
Normal file
@ -0,0 +1,276 @@
|
||||
/**
|
||||
* 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, 0xdb, 0x8a, 0xdc, 0x38,
|
||||
0x10, 0x7d, 0x9f, 0xaf, 0x50, 0xf4, 0xb0, 0xcc, 0xc0, 0xb4, 0xbd, 0xd3, 0x81, 0x21, 0x6c, 0x6c,
|
||||
0xc3, 0x92, 0x0b, 0x04, 0xc2, 0x32, 0x4c, 0x13, 0x02, 0x79, 0x69, 0x64, 0xb9, 0xdc, 0x56, 0x5a,
|
||||
0x96, 0xb4, 0x56, 0xd9, 0x3d, 0xbd, 0x5f, 0x9f, 0x92, 0x6c, 0xf7, 0x65, 0xd3, 0x4c, 0x2e, 0x2f,
|
||||
0x6d, 0x57, 0xa9, 0xea, 0xd4, 0xa9, 0xa3, 0x92, 0xd5, 0xd9, 0x8b, 0xca, 0x4a, 0xdc, 0x3b, 0x60,
|
||||
0x0d, 0xb6, 0xba, 0xb8, 0xca, 0xc2, 0x83, 0x69, 0x61, 0x36, 0x39, 0x07, 0xc3, 0x83, 0x03, 0x44,
|
||||
0x45, 0x8f, 0x16, 0x50, 0x50, 0x0c, 0xba, 0x05, 0xfc, 0xdb, 0xab, 0x21, 0xe7, 0xd2, 0x1a, 0x04,
|
||||
0x83, 0x8b, 0x90, 0xcc, 0xd9, 0x64, 0xe5, 0x1c, 0xe1, 0x09, 0xd3, 0x00, 0xf2, 0x9a, 0xc9, 0x46,
|
||||
0x74, 0x1e, 0x30, 0xef, 0xb1, 0x5e, 0xbc, 0xe2, 0x33, 0x86, 0x11, 0x2d, 0xe4, 0x7c, 0x50, 0xb0,
|
||||
0x73, 0xb6, 0xc3, 0x93, 0xcc, 0x9d, 0xaa, 0xb0, 0xc9, 0x2b, 0x18, 0x94, 0x84, 0x45, 0x34, 0x6e,
|
||||
0x95, 0x51, 0xa8, 0x84, 0x5e, 0x78, 0x29, 0x34, 0xe4, 0x77, 0xb7, 0x2d, 0x39, 0xda, 0xbe, 0x3d,
|
||||
0xda, 0xe2, 0xe9, 0xcc, 0xee, 0x3d, 0x74, 0xd1, 0x10, 0x25, 0xd9, 0xc6, 0x86, 0xa2, 0xa8, 0x50,
|
||||
0x43, 0xf1, 0xc6, 0x9a, 0x5a, 0x6d, 0xfa, 0x4e, 0xa0, 0xb2, 0x26, 0x4b, 0x47, 0xe7, 0x55, 0xa6,
|
||||
0x95, 0xd9, 0xb2, 0x0e, 0x74, 0xce, 0x7d, 0x43, 0x6c, 0x64, 0x8f, 0x4c, 0x11, 0x21, 0xce, 0x9a,
|
||||
0x0e, 0xea, 0x9c, 0xa7, 0xb5, 0x18, 0x82, 0x9d, 0xd0, 0x0f, 0x67, 0xa1, 0xd3, 0x9c, 0xab, 0x56,
|
||||
0x6c, 0x20, 0x7d, 0x5a, 0xc4, 0xb8, 0x73, 0x08, 0xdc, 0x6b, 0xf0, 0x0d, 0x00, 0xce, 0xb1, 0x51,
|
||||
0x0c, 0xe9, 0xfd, 0x01, 0x2f, 0x86, 0x24, 0xc1, 0x43, 0x99, 0x5e, 0x76, 0xca, 0x21, 0xf3, 0x9d,
|
||||
0xa4, 0x15, 0xe1, 0x5c, 0xf2, 0x95, 0xdc, 0x59, 0x3a, 0xba, 0x69, 0x3d, 0x9d, 0xa4, 0x2f, 0x6d,
|
||||
0xb5, 0x67, 0xd6, 0x68, 0x2b, 0x2a, 0x2a, 0x4f, 0x92, 0xfc, 0xed, 0xdc, 0xf5, 0x4d, 0x40, 0xa8,
|
||||
0xd4, 0xc0, 0xa4, 0x16, 0xde, 0x53, 0xa9, 0xd0, 0x11, 0x2f, 0x56, 0x80, 0xa8, 0xcc, 0xc6, 0xb3,
|
||||
0xcc, 0x3b, 0x61, 0x98, 0xa2, 0x8c, 0x90, 0x47, 0xae, 0x35, 0x89, 0x02, 0x9a, 0x17, 0xd7, 0x93,
|
||||
0x9d, 0x24, 0xc9, 0x0d, 0x15, 0xa3, 0x28, 0xaa, 0x49, 0x40, 0xe7, 0x70, 0xa5, 0xb6, 0x72, 0x1b,
|
||||
0x4a, 0xd4, 0xb6, 0x6b, 0x19, 0x6d, 0x5c, 0x63, 0x09, 0xca, 0x59, 0x4f, 0xbd, 0x09, 0x19, 0x44,
|
||||
0x8c, 0xdd, 0x08, 0xec, 0xa9, 0xb9, 0x71, 0x4b, 0x0d, 0xe0, 0xce, 0x76, 0xdb, 0xb5, 0x9f, 0x28,
|
||||
0xfc, 0x8f, 0x60, 0x00, 0x9a, 0x39, 0x7c, 0x56, 0xef, 0x15, 0x5b, 0xad, 0x3e, 0xbc, 0xbd, 0x50,
|
||||
0x39, 0xc6, 0x29, 0xe3, 0x7a, 0x8c, 0x1a, 0x81, 0x06, 0x89, 0xb1, 0x0f, 0xef, 0x55, 0xb5, 0x1e,
|
||||
0xed, 0xb9, 0x64, 0x70, 0xf1, 0x43, 0x62, 0xaf, 0xf5, 0x38, 0x37, 0x21, 0xd1, 0xba, 0x40, 0x92,
|
||||
0x0d, 0x42, 0xf7, 0x14, 0xc8, 0x8b, 0x8f, 0x87, 0xae, 0xb3, 0x74, 0x5c, 0x0b, 0x0a, 0x8f, 0x70,
|
||||
0xe1, 0xed, 0x32, 0x8f, 0x53, 0xbe, 0x0f, 0xe4, 0xa6, 0x06, 0xab, 0x1f, 0x72, 0x8e, 0x2f, 0xd3,
|
||||
0x04, 0xb8, 0x29, 0x89, 0x1f, 0x98, 0x4c, 0xd4, 0x9d, 0xdf, 0x5e, 0x62, 0x1e, 0x3b, 0xad, 0x75,
|
||||
0xb5, 0x8e, 0xeb, 0x34, 0xdf, 0x1a, 0xcc, 0x86, 0x8e, 0x05, 0xbf, 0x7f, 0xc9, 0x59, 0xa5, 0x7c,
|
||||
0x18, 0xec, 0xea, 0x42, 0x71, 0xd9, 0x80, 0xdc, 0x96, 0xf6, 0x29, 0x4e, 0x64, 0x20, 0xcd, 0xc8,
|
||||
0x1d, 0xa7, 0x7a, 0x17, 0xa1, 0x8a, 0x33, 0x56, 0x87, 0xe8, 0x59, 0xc7, 0x39, 0x6c, 0x14, 0xfa,
|
||||
0x90, 0xc4, 0xc2, 0x2b, 0x73, 0x87, 0xc6, 0x23, 0xf2, 0x51, 0xad, 0xe7, 0x45, 0xfb, 0xc7, 0x56,
|
||||
0xc0, 0x7e, 0x62, 0x8b, 0x4f, 0x89, 0x85, 0x03, 0x73, 0x22, 0xd5, 0x49, 0xff, 0x77, 0xf7, 0x87,
|
||||
0x39, 0x0b, 0x7b, 0x3e, 0xcb, 0x64, 0x2e, 0x0f, 0xc0, 0xa9, 0x54, 0x53, 0xfd, 0xb2, 0x47, 0xa4,
|
||||
0x81, 0x18, 0xeb, 0xf8, 0xbe, 0x6c, 0x15, 0x1e, 0xc3, 0x66, 0x1d, 0x46, 0x77, 0xb1, 0x12, 0x03,
|
||||
0x30, 0x61, 0x2a, 0xf6, 0x08, 0xa5, 0xb5, 0x98, 0xa5, 0x63, 0x72, 0x00, 0x0b, 0xdc, 0x2f, 0xb6,
|
||||
0x3e, 0x1d, 0xc0, 0x4f, 0xae, 0x12, 0x08, 0xac, 0x56, 0x5d, 0xbb, 0x13, 0x1d, 0xb0, 0xeb, 0xa4,
|
||||
0x54, 0xe6, 0xe6, 0x77, 0x4f, 0x58, 0x1f, 0xd1, 0x38, 0x03, 0x23, 0x47, 0xe2, 0x6d, 0xaf, 0x51,
|
||||
0x39, 0xd1, 0x61, 0x24, 0xb2, 0xa0, 0x55, 0x31, 0xeb, 0x32, 0xc6, 0x3e, 0x7b, 0xfc, 0x2e, 0x6a,
|
||||
0x5e, 0x2b, 0xe2, 0x4d, 0x25, 0x25, 0x38, 0xfa, 0x0a, 0x07, 0xba, 0xb7, 0xe1, 0x27, 0xd9, 0xfc,
|
||||
0x37, 0x23, 0xc7, 0x88, 0x1f, 0x28, 0x79, 0x2e, 0xe0, 0x27, 0x17, 0xbe, 0x32, 0xbf, 0xa2, 0xdb,
|
||||
0x23, 0x10, 0x71, 0x36, 0x93, 0xff, 0x5d, 0xbd, 0xba, 0x80, 0xc2, 0x7f, 0x8a, 0xe3, 0x8c, 0xab,
|
||||
0xfc, 0x7a, 0xca, 0x8a, 0x14, 0x7e, 0x85, 0xf3, 0x07, 0x53, 0xdb, 0x67, 0x98, 0xbe, 0x5b, 0x3d,
|
||||
0xbc, 0x5a, 0xde, 0xdf, 0x2f, 0x4a, 0xe1, 0x69, 0xc2, 0xb2, 0xb2, 0xa0, 0x5b, 0x42, 0xec, 0x25,
|
||||
0x6a, 0xaa, 0x51, 0xdc, 0x1e, 0x47, 0x64, 0x58, 0x66, 0x65, 0x57, 0x5c, 0x3d, 0xd0, 0xae, 0x32,
|
||||
0x5b, 0xb3, 0x4c, 0x4c, 0xb7, 0x45, 0xb8, 0x6d, 0xfd, 0x5f, 0x69, 0xba, 0x51, 0x98, 0xc8, 0xe6,
|
||||
0xce, 0x25, 0xca, 0xa6, 0x8d, 0x6d, 0x61, 0x4b, 0x36, 0xf9, 0x52, 0x5e, 0x4c, 0x56, 0x96, 0x8a,
|
||||
0x82, 0x95, 0xfb, 0xef, 0x33, 0xa7, 0x2c, 0x5e, 0xbc, 0x1b, 0x36, 0x60, 0xf6, 0xec, 0x8b, 0x32,
|
||||
0x96, 0x6e, 0xde, 0x21, 0x26, 0xfc, 0x21, 0xad, 0xdb, 0xbf, 0x66, 0xcb, 0x3f, 0x97, 0xcb, 0xe3,
|
||||
0x89, 0x0e, 0x77, 0x4d, 0xbc, 0x7a, 0xe2, 0xbf, 0x81, 0x6f, 0xa3, 0xf2, 0xc7, 0xe5, 0x1e, 0x08,
|
||||
0x00, 0x00,
|
||||
};
|
||||
const StaticFile index_html PROGMEM = {(sizeof(index_html_content)/sizeof(index_html_content[0])), index_html_content};
|
||||
|
||||
static const uint8_t app_js_content[] PROGMEM = {
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x9d, 0x56, 0x5b, 0x6f, 0xdc, 0x44,
|
||||
0x14, 0x7e, 0xe7, 0x57, 0x78, 0x47, 0x22, 0xf2, 0xb0, 0x8e, 0x73, 0x81, 0x87, 0x6a, 0x5d, 0x6b,
|
||||
0xd5, 0x2b, 0x2d, 0x6a, 0x9b, 0xaa, 0x49, 0x11, 0x52, 0x14, 0xa2, 0x59, 0xfb, 0x6c, 0xd6, 0x8d,
|
||||
0x77, 0xc6, 0x8c, 0xc7, 0xd9, 0x84, 0x6d, 0x24, 0xda, 0x3e, 0x80, 0x04, 0x52, 0x25, 0xde, 0xe1,
|
||||
0x89, 0x1f, 0x90, 0x06, 0x02, 0x2d, 0x6d, 0xc3, 0x5f, 0xf0, 0xfe, 0x23, 0xce, 0x19, 0x7b, 0xaf,
|
||||
0x89, 0x44, 0xc5, 0x43, 0x36, 0xf6, 0x78, 0xce, 0xed, 0x3b, 0xe7, 0xfb, 0x66, 0xba, 0x85, 0x8c,
|
||||
0x4c, 0xa2, 0xa4, 0x93, 0xe4, 0x1b, 0x9d, 0x27, 0x10, 0x19, 0x17, 0xf8, 0x50, 0x83, 0x29, 0xb4,
|
||||
0x64, 0xdb, 0xca, 0xae, 0x38, 0xd5, 0x87, 0x1d, 0x16, 0x86, 0x61, 0xf5, 0xe8, 0x67, 0x5a, 0x19,
|
||||
0x65, 0x8e, 0x32, 0xf0, 0x8d, 0xda, 0x34, 0x3a, 0x91, 0x7b, 0x7e, 0x24, 0xd2, 0x14, 0x6d, 0x8f,
|
||||
0xbb, 0x63, 0x87, 0x7b, 0x30, 0x75, 0xe5, 0xc4, 0x2a, 0x2a, 0xfa, 0x20, 0x8d, 0xbf, 0x07, 0xe6,
|
||||
0x56, 0x0a, 0xf4, 0x78, 0xfd, 0xe8, 0x6e, 0x3c, 0x67, 0x10, 0x09, 0x19, 0x41, 0x7a, 0xeb, 0x00,
|
||||
0xbf, 0xcd, 0x58, 0x02, 0x06, 0x03, 0x5a, 0xbb, 0x09, 0x5d, 0x51, 0xa4, 0x66, 0x69, 0x69, 0x71,
|
||||
0xc5, 0xe5, 0x1e, 0xf8, 0xb9, 0x51, 0xd9, 0x43, 0xad, 0x32, 0xb1, 0x27, 0xc8, 0x19, 0xed, 0x5a,
|
||||
0x58, 0xb2, 0xdb, 0xaa, 0x18, 0xd7, 0x8b, 0x4e, 0x27, 0x85, 0xb0, 0xb1, 0x8a, 0x2b, 0x55, 0x98,
|
||||
0x2f, 0x45, 0x5a, 0xe0, 0xc2, 0x9a, 0xd7, 0x58, 0x9b, 0x26, 0x04, 0x5a, 0x2b, 0xbd, 0x05, 0x87,
|
||||
0x73, 0xe9, 0x38, 0x89, 0xcc, 0x0d, 0x79, 0x51, 0x5d, 0xe7, 0x16, 0x6d, 0x68, 0x83, 0xdf, 0x87,
|
||||
0x3c, 0x17, 0x7b, 0xd0, 0x82, 0x26, 0x63, 0x53, 0xf3, 0x54, 0x45, 0xfb, 0x64, 0x89, 0x99, 0x80,
|
||||
0xb9, 0x66, 0x10, 0xa6, 0x4e, 0x61, 0xc0, 0x65, 0x71, 0x92, 0x0b, 0x0c, 0x1f, 0x33, 0x6f, 0xfa,
|
||||
0x38, 0x03, 0x43, 0x21, 0xa7, 0x86, 0x1a, 0xfa, 0xea, 0x00, 0x2e, 0xb3, 0x9d, 0x31, 0x48, 0x64,
|
||||
0x62, 0x1e, 0x80, 0x19, 0x28, 0xbd, 0xbf, 0x09, 0xc6, 0x60, 0x33, 0x72, 0x97, 0x0f, 0xa7, 0x55,
|
||||
0xb8, 0xe0, 0x19, 0x3e, 0x94, 0x45, 0x9a, 0x36, 0xc2, 0x10, 0xf1, 0x73, 0xc1, 0x3f, 0xb0, 0xe5,
|
||||
0x1a, 0xee, 0x4d, 0x82, 0x1d, 0x1f, 0x08, 0xed, 0x98, 0x70, 0x35, 0x98, 0xd8, 0x21, 0x60, 0xc3,
|
||||
0x75, 0x6c, 0x79, 0xb3, 0x49, 0x36, 0xd8, 0x4d, 0x96, 0x2a, 0x11, 0xa3, 0xf7, 0xdd, 0x54, 0x74,
|
||||
0x20, 0x65, 0x1c, 0x01, 0x3e, 0x4a, 0xc1, 0xc7, 0x94, 0xb2, 0x54, 0x1c, 0x85, 0x4c, 0x2a, 0x09,
|
||||
0xac, 0x72, 0xa4, 0xc3, 0x49, 0xc3, 0xbb, 0x4a, 0xf7, 0x73, 0x5f, 0x56, 0xf9, 0xed, 0xe6, 0x75,
|
||||
0x82, 0x81, 0xf6, 0x45, 0x1c, 0xdb, 0x5e, 0xdf, 0x4b, 0x72, 0x03, 0x12, 0xb4, 0xcb, 0xf2, 0xa2,
|
||||
0xd3, 0x4f, 0x0c, 0xf3, 0xdc, 0x71, 0x0e, 0x33, 0xb8, 0x6b, 0x5f, 0x26, 0x71, 0x95, 0xb7, 0x8f,
|
||||
0x68, 0xf4, 0x5d, 0xde, 0xd6, 0x7e, 0x96, 0xef, 0xd7, 0x4b, 0x29, 0xc8, 0x3d, 0xd3, 0xbb, 0x7a,
|
||||
0xa5, 0xed, 0x8a, 0x14, 0xb4, 0x71, 0x59, 0xf9, 0x4b, 0x79, 0x56, 0x9e, 0x96, 0x67, 0xa3, 0xef,
|
||||
0xca, 0xf7, 0xa3, 0x1f, 0xcb, 0x37, 0x4e, 0xf9, 0x4f, 0x79, 0x82, 0x2f, 0xe7, 0xe5, 0xdb, 0xd1,
|
||||
0x4f, 0x8e, 0x5b, 0xbe, 0x2b, 0x5f, 0x97, 0xef, 0xf1, 0xef, 0x5d, 0x79, 0x42, 0x2b, 0xf8, 0x7c,
|
||||
0x32, 0x7a, 0xe9, 0x94, 0x7f, 0x94, 0x6f, 0xed, 0x87, 0x13, 0x67, 0xd9, 0xb9, 0xe2, 0x8c, 0x9e,
|
||||
0xd9, 0x1d, 0xa7, 0x64, 0x85, 0x7f, 0xa7, 0x9c, 0x71, 0x6f, 0x7e, 0x4a, 0x79, 0x6b, 0x79, 0x2d,
|
||||
0x0c, 0xb5, 0x9f, 0xe7, 0x98, 0x5d, 0x0e, 0x29, 0x92, 0x03, 0xe2, 0xbb, 0x32, 0x86, 0xc3, 0xb9,
|
||||
0x4c, 0x9c, 0xf2, 0x14, 0x93, 0x78, 0x85, 0xf1, 0x4f, 0xac, 0xf3, 0xd1, 0xf3, 0xf2, 0x7c, 0xf4,
|
||||
0x7d, 0xf9, 0x37, 0x3e, 0x62, 0xc8, 0xf3, 0xd1, 0xb3, 0xd1, 0xf3, 0xd1, 0x0b, 0xca, 0xf0, 0x92,
|
||||
0x00, 0x07, 0x2a, 0x89, 0xab, 0x59, 0xc2, 0x30, 0x16, 0x21, 0xde, 0x9a, 0xf8, 0xfe, 0x99, 0x6a,
|
||||
0x44, 0x17, 0xaf, 0xd1, 0xe3, 0x99, 0x23, 0x55, 0x8c, 0xf3, 0x19, 0x5f, 0x74, 0x72, 0xcc, 0xb9,
|
||||
0x87, 0xd6, 0x3d, 0x35, 0xd8, 0x25, 0xcc, 0x2e, 0x42, 0x1f, 0xf5, 0x84, 0xdc, 0x83, 0x45, 0xe8,
|
||||
0x2d, 0xc0, 0xf3, 0xc3, 0x4b, 0xa4, 0x67, 0x48, 0x1b, 0x23, 0x34, 0x52, 0xd9, 0x8f, 0x7a, 0x10,
|
||||
0xed, 0x43, 0xdc, 0x66, 0x06, 0x69, 0xc2, 0x5a, 0x2c, 0x13, 0x79, 0x8e, 0x8d, 0xa6, 0xf9, 0xac,
|
||||
0x42, 0x12, 0x2e, 0x1f, 0x18, 0xae, 0x9a, 0xc0, 0x89, 0xeb, 0x39, 0x34, 0x83, 0xe5, 0xb5, 0xc9,
|
||||
0xf0, 0xd6, 0xdf, 0x55, 0x46, 0x86, 0xf9, 0xb6, 0xd9, 0xc1, 0xd5, 0x99, 0x59, 0x08, 0x19, 0xab,
|
||||
0x0a, 0x16, 0x4f, 0xc4, 0x21, 0xe9, 0x8d, 0xcb, 0x56, 0x90, 0xb1, 0xa6, 0xc8, 0x99, 0x37, 0x3c,
|
||||
0x9e, 0x09, 0x69, 0x3c, 0xc5, 0x87, 0x46, 0x1f, 0x0d, 0x93, 0xae, 0x6b, 0xb8, 0xe9, 0x69, 0x35,
|
||||
0x70, 0x4c, 0x00, 0xae, 0x1d, 0x35, 0x4f, 0xf9, 0x84, 0xe6, 0x6e, 0x12, 0x3f, 0x7d, 0x4a, 0xd4,
|
||||
0x41, 0xf1, 0xa8, 0x82, 0x78, 0xd3, 0xb7, 0xaa, 0x1b, 0xf5, 0x02, 0xd2, 0xe5, 0x38, 0x12, 0x26,
|
||||
0xea, 0xa1, 0xaf, 0x61, 0xd5, 0x9e, 0xa9, 0x7e, 0x18, 0xcc, 0x68, 0x31, 0x25, 0x6c, 0xd1, 0x42,
|
||||
0x42, 0x96, 0xa7, 0x75, 0x42, 0x50, 0x27, 0x04, 0x41, 0x0d, 0x62, 0x22, 0x11, 0xb9, 0x3b, 0x5b,
|
||||
0xf7, 0xef, 0x61, 0x7d, 0x01, 0x92, 0xca, 0x25, 0xb8, 0x14, 0x12, 0x56, 0x5d, 0x35, 0x7e, 0x8a,
|
||||
0xc8, 0xd6, 0x1c, 0x08, 0x54, 0xb3, 0x59, 0x61, 0x99, 0x84, 0xd5, 0x87, 0x6d, 0xb5, 0xb3, 0xbd,
|
||||
0xba, 0xe3, 0x89, 0x99, 0xd7, 0xb5, 0x9d, 0xb1, 0x5b, 0x91, 0x65, 0x20, 0x63, 0x57, 0xc2, 0xc0,
|
||||
0xd9, 0xb0, 0x80, 0xba, 0x49, 0x93, 0x39, 0x2e, 0x6b, 0x0a, 0xfc, 0x17, 0x5f, 0xef, 0x73, 0xe6,
|
||||
0x25, 0x98, 0x7c, 0xad, 0x13, 0x95, 0xd1, 0x6c, 0xb1, 0x70, 0xb1, 0x58, 0xa8, 0x8a, 0x9d, 0x17,
|
||||
0xa8, 0xc7, 0x59, 0x2c, 0x0c, 0xdc, 0x46, 0x31, 0x70, 0xab, 0xec, 0x60, 0x51, 0x22, 0x0a, 0xbb,
|
||||
0x63, 0xaa, 0x10, 0xf0, 0x41, 0x0a, 0x81, 0x88, 0x21, 0x5a, 0xb3, 0xe3, 0x8e, 0xaa, 0xd6, 0x00,
|
||||
0xbf, 0x9b, 0xa4, 0xd5, 0x4f, 0x5e, 0xe3, 0xc2, 0x6b, 0x21, 0x19, 0x33, 0xe7, 0x37, 0xa4, 0xe2,
|
||||
0x9b, 0xf2, 0xad, 0x83, 0xbc, 0x7e, 0x85, 0x9c, 0x44, 0x76, 0x23, 0xc7, 0xcf, 0x48, 0x13, 0x48,
|
||||
0x07, 0xde, 0x2f, 0x70, 0x16, 0x29, 0xd5, 0x58, 0x0b, 0x2a, 0xad, 0x1c, 0xb3, 0x30, 0xa0, 0x32,
|
||||
0x64, 0x48, 0xd0, 0x7d, 0x75, 0xff, 0xde, 0x1d, 0x63, 0xb2, 0x47, 0xf0, 0x4d, 0x01, 0xb9, 0xf1,
|
||||
0xb4, 0x5d, 0xa4, 0x62, 0x6f, 0x0a, 0x23, 0x82, 0x89, 0x84, 0xd5, 0x60, 0x33, 0xca, 0x8b, 0x48,
|
||||
0x34, 0x4d, 0x12, 0x1b, 0x84, 0xa8, 0x22, 0x06, 0xa4, 0xb1, 0x97, 0x14, 0x8e, 0x07, 0xee, 0x9e,
|
||||
0xc6, 0x53, 0x66, 0xa1, 0x74, 0x9b, 0x01, 0x86, 0x5b, 0x70, 0xe5, 0xe7, 0xc9, 0xb7, 0x10, 0x48,
|
||||
0x6a, 0x38, 0xba, 0x83, 0xf8, 0xaa, 0x6e, 0xdf, 0x17, 0xa6, 0xe7, 0x6b, 0x55, 0x60, 0xf8, 0xf1,
|
||||
0xea, 0x8a, 0xfe, 0x64, 0x6d, 0x75, 0x95, 0xe3, 0x11, 0x7e, 0x3b, 0x39, 0x84, 0xd8, 0x5d, 0xe7,
|
||||
0x2d, 0x7c, 0xf7, 0xc6, 0xf5, 0xcd, 0x4c, 0x9c, 0x6c, 0xb2, 0x8f, 0x19, 0xcd, 0xaf, 0xf4, 0x95,
|
||||
0xd4, 0x20, 0xe2, 0x23, 0x22, 0x14, 0x54, 0x2c, 0x0e, 0x27, 0x09, 0xf1, 0xe1, 0x67, 0x78, 0x5e,
|
||||
0x48, 0xdf, 0xee, 0xd8, 0xa4, 0x1d, 0xc8, 0x4c, 0x94, 0xc7, 0xf0, 0x8b, 0xcd, 0x8d, 0x07, 0x7e,
|
||||
0x26, 0x74, 0x0e, 0x2e, 0x7d, 0xcd, 0x33, 0xe4, 0x2d, 0xd0, 0xac, 0x70, 0x7a, 0xc3, 0x23, 0xbc,
|
||||
0x3d, 0xee, 0xca, 0xaf, 0x8b, 0xcd, 0xa0, 0x3e, 0xfc, 0x85, 0x1d, 0xb0, 0x52, 0x3e, 0xfa, 0xc1,
|
||||
0x2e, 0x9e, 0x7b, 0xce, 0xe8, 0x85, 0xd5, 0x4c, 0xd2, 0xf3, 0x37, 0xf4, 0x44, 0x1a, 0x4d, 0x12,
|
||||
0x4f, 0x7a, 0x7f, 0x66, 0x0d, 0x7e, 0xc7, 0xed, 0x2f, 0xca, 0x3f, 0xf1, 0xe9, 0x0c, 0x37, 0x3e,
|
||||
0x1b, 0xbd, 0x64, 0xbc, 0x35, 0x89, 0x82, 0x8e, 0x5e, 0x63, 0x24, 0xab, 0xbf, 0x97, 0xf4, 0x9f,
|
||||
0x04, 0xc4, 0xd6, 0x6a, 0x87, 0x3a, 0x9c, 0x15, 0xa9, 0xcb, 0x86, 0x9d, 0xb6, 0x62, 0x63, 0x5d,
|
||||
0xf6, 0x70, 0x63, 0x73, 0x8b, 0x79, 0x66, 0xac, 0x50, 0xc2, 0x5a, 0x11, 0x6a, 0x39, 0xb5, 0x5d,
|
||||
0xd3, 0x0c, 0x5d, 0x20, 0xc6, 0xb5, 0x2c, 0x43, 0xe0, 0x2e, 0x3d, 0xc3, 0xbd, 0x45, 0xe2, 0x1c,
|
||||
0x37, 0x66, 0xb0, 0x9e, 0x3f, 0xe0, 0x3d, 0x1c, 0x03, 0x4b, 0x05, 0x1c, 0xc8, 0x4a, 0xb1, 0x3c,
|
||||
0x36, 0xde, 0xc1, 0x1a, 0x21, 0xc9, 0x36, 0x5e, 0x58, 0x74, 0xad, 0x2a, 0x34, 0x9e, 0xf6, 0xf2,
|
||||
0x82, 0x52, 0x8c, 0xf7, 0xb6, 0x8e, 0x88, 0xf6, 0x9d, 0x7e, 0x91, 0x1b, 0xa7, 0x03, 0x8e, 0x70,
|
||||
0x26, 0x76, 0x3c, 0x40, 0x87, 0x0d, 0x73, 0xd1, 0x48, 0x2a, 0xa7, 0xd0, 0xa9, 0x93, 0x67, 0x10,
|
||||
0x25, 0xdd, 0x84, 0xae, 0x23, 0x41, 0x3e, 0x48, 0x6a, 0x3d, 0x88, 0x44, 0x0e, 0xec, 0xf3, 0x5b,
|
||||
0x5b, 0xac, 0x85, 0xd6, 0x93, 0x6b, 0xa5, 0xe4, 0x7c, 0xa2, 0x5a, 0x58, 0xb8, 0x23, 0xb9, 0xf4,
|
||||
0x7b, 0x22, 0xdf, 0x18, 0x48, 0xba, 0xa3, 0x21, 0xa8, 0x47, 0xae, 0xe2, 0x38, 0x2f, 0xa6, 0x19,
|
||||
0xba, 0x74, 0xa6, 0x86, 0x34, 0x80, 0xa8, 0xfc, 0x1b, 0x5d, 0x97, 0xb5, 0x19, 0x6f, 0xe3, 0x4f,
|
||||
0x8b, 0x2d, 0x31, 0xde, 0x04, 0x19, 0xa1, 0x30, 0x3f, 0x7e, 0x74, 0xf7, 0x86, 0xea, 0xe3, 0x20,
|
||||
0x11, 0xe5, 0x15, 0x6f, 0xb2, 0x90, 0x5d, 0xf6, 0x45, 0xa2, 0xda, 0x71, 0x1e, 0x74, 0x70, 0x24,
|
||||
0xf7, 0x03, 0x9b, 0x98, 0xed, 0xd1, 0x62, 0x66, 0xb5, 0x5e, 0x6e, 0xef, 0x04, 0x1f, 0x92, 0x63,
|
||||
0xe2, 0x67, 0x45, 0x8e, 0xa5, 0xfe, 0x8f, 0x44, 0x64, 0x98, 0xf8, 0x4f, 0x54, 0x82, 0xb3, 0x82,
|
||||
0xa5, 0x1c, 0xdb, 0xbb, 0x92, 0xb8, 0x44, 0x41, 0xc6, 0x92, 0x21, 0xaa, 0xc1, 0xa2, 0x83, 0xc1,
|
||||
0xab, 0x32, 0x47, 0x60, 0x90, 0x55, 0x82, 0x4e, 0xe5, 0x7a, 0xef, 0x1d, 0x64, 0x1b, 0x49, 0xc4,
|
||||
0x0d, 0x25, 0x51, 0x2c, 0xcc, 0x72, 0x75, 0x42, 0x33, 0xd4, 0x9a, 0x34, 0x89, 0xec, 0xcd, 0x77,
|
||||
0xe5, 0x70, 0x79, 0x30, 0x18, 0x2c, 0x93, 0xcc, 0x2e, 0x63, 0xdb, 0xaa, 0xec, 0xe8, 0x96, 0x20,
|
||||
0xfe, 0x83, 0xce, 0x88, 0x12, 0x31, 0x5a, 0xcc, 0x30, 0xda, 0x2e, 0xb2, 0xfa, 0x3c, 0x45, 0x88,
|
||||
0xc4, 0xd2, 0x52, 0x63, 0xe5, 0xeb, 0xf5, 0xa7, 0x6b, 0xeb, 0xeb, 0x9f, 0xae, 0xf8, 0x06, 0xf3,
|
||||
0x71, 0x31, 0x39, 0xfb, 0x99, 0x5f, 0x9c, 0x9b, 0x1e, 0x96, 0xe8, 0x50, 0x74, 0x07, 0x0f, 0x98,
|
||||
0xf1, 0xb6, 0x40, 0xbb, 0x76, 0x5e, 0x67, 0x54, 0x42, 0xcc, 0xab, 0x04, 0x22, 0x65, 0x73, 0xbd,
|
||||
0x48, 0x47, 0x8d, 0xd8, 0xd8, 0xf3, 0x97, 0x36, 0x58, 0x96, 0xd9, 0xc9, 0x23, 0x94, 0xda, 0xb4,
|
||||
0xde, 0x42, 0xfa, 0x89, 0xe3, 0x01, 0x4e, 0x92, 0x1a, 0xf8, 0x74, 0xfa, 0x86, 0x43, 0x64, 0x66,
|
||||
0x0b, 0xfc, 0x0e, 0x2e, 0xa1, 0xad, 0xdd, 0xcd, 0xbd, 0x4c, 0xe5, 0xb3, 0x8b, 0x16, 0x69, 0x0c,
|
||||
0xea, 0xf2, 0xe0, 0xa3, 0x7f, 0x01, 0x1a, 0x08, 0xa7, 0x18, 0x21, 0x0d, 0x00, 0x00,
|
||||
};
|
||||
const StaticFile app_js PROGMEM = {(sizeof(app_js_content)/sizeof(app_js_content[0])), app_js_content};
|
||||
|
||||
static const uint8_t style_css_content[] PROGMEM = {
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x9d, 0x53, 0x5d, 0x6b, 0xdb, 0x30,
|
||||
0x14, 0x7d, 0xdf, 0xaf, 0x08, 0x94, 0x41, 0x0b, 0x76, 0xb0, 0x9b, 0x26, 0x59, 0x64, 0xf6, 0xb0,
|
||||
0x3d, 0x8c, 0xed, 0x61, 0x4f, 0x65, 0x4f, 0xa3, 0x14, 0x7d, 0x5c, 0xd9, 0x22, 0xb2, 0x25, 0xa4,
|
||||
0xeb, 0x26, 0x99, 0xf1, 0x7f, 0x9f, 0xfc, 0x95, 0x26, 0x4d, 0x06, 0x65, 0x18, 0x04, 0xf7, 0x43,
|
||||
0x3a, 0xe7, 0x9e, 0x7b, 0xcc, 0x8c, 0x38, 0x44, 0x05, 0x96, 0xba, 0xb1, 0x54, 0x08, 0x55, 0xe5,
|
||||
0x24, 0xc9, 0x4a, 0xea, 0x72, 0x55, 0x91, 0xa4, 0x65, 0x5d, 0x91, 0xd5, 0x88, 0xa6, 0x8a, 0x54,
|
||||
0x65, 0x6b, 0xfc, 0x8d, 0x07, 0x0b, 0x9f, 0x2d, 0xf5, 0x7e, 0x67, 0x9c, 0x78, 0x3a, 0x4d, 0x22,
|
||||
0xec, 0xf1, 0xa9, 0x91, 0xa6, 0xc2, 0xd8, 0xab, 0x3f, 0x40, 0xd2, 0x95, 0xdd, 0x67, 0x7d, 0x28,
|
||||
0x69, 0xa9, 0xf4, 0x81, 0xc4, 0xd4, 0x5a, 0x0d, 0xb1, 0x3f, 0x78, 0x84, 0x32, 0xfa, 0xaa, 0x55,
|
||||
0xb5, 0xfd, 0x49, 0xf9, 0x63, 0x1f, 0x7e, 0x0b, 0x7d, 0xd1, 0x23, 0xe4, 0x06, 0x66, 0xbf, 0x7e,
|
||||
0x44, 0xdf, 0x41, 0xbf, 0x00, 0x2a, 0x4e, 0xa3, 0x2f, 0x4e, 0x51, 0x1d, 0x79, 0x5a, 0xf9, 0xd8,
|
||||
0x83, 0x53, 0xb2, 0x9d, 0xa3, 0x42, 0x0d, 0x47, 0xae, 0x69, 0x62, 0xf7, 0xb3, 0xfe, 0x38, 0xa2,
|
||||
0xed, 0x40, 0xe5, 0x05, 0x92, 0x55, 0x92, 0x64, 0x8c, 0xf2, 0x6d, 0xee, 0x4c, 0x5d, 0x89, 0x98,
|
||||
0x1b, 0x6d, 0x1c, 0xb9, 0x01, 0x29, 0xef, 0xe5, 0x32, 0x63, 0x81, 0x3c, 0xb8, 0x98, 0x99, 0x30,
|
||||
0x59, 0x49, 0xd2, 0x70, 0xdd, 0x1b, 0xad, 0xc4, 0xec, 0x46, 0x6c, 0x20, 0x81, 0x75, 0x36, 0x76,
|
||||
0xdf, 0xaf, 0x57, 0xc0, 0x1e, 0xb2, 0x93, 0x99, 0x96, 0x76, 0xdf, 0xce, 0x99, 0x36, 0x7c, 0x7b,
|
||||
0x46, 0xa1, 0x9d, 0xcb, 0x5a, 0xeb, 0x78, 0xa7, 0x04, 0x16, 0x4d, 0x7f, 0x86, 0x74, 0xf2, 0x31,
|
||||
0xe0, 0xec, 0xbb, 0x8b, 0x5d, 0xdb, 0x11, 0xb2, 0x6b, 0x36, 0xae, 0x7c, 0xd6, 0x94, 0xc1, 0x89,
|
||||
0xe8, 0xb3, 0x64, 0xb6, 0xb8, 0x1c, 0x61, 0xec, 0xed, 0x65, 0x6e, 0x86, 0xb5, 0x1c, 0x59, 0xf7,
|
||||
0x5c, 0xfa, 0x32, 0x2f, 0x80, 0x6f, 0xc3, 0xcb, 0xd3, 0x6b, 0x31, 0x1a, 0x4b, 0xc2, 0x6b, 0xed,
|
||||
0xbb, 0x76, 0x16, 0x79, 0xd0, 0xc0, 0xb1, 0x19, 0x19, 0x3a, 0x2a, 0x54, 0xed, 0xc9, 0x43, 0x20,
|
||||
0x33, 0x64, 0x4e, 0xf5, 0xe1, 0x1b, 0xce, 0xb9, 0xcc, 0x26, 0xd6, 0xeb, 0x50, 0xd9, 0x84, 0x46,
|
||||
0x53, 0x63, 0x58, 0x27, 0x04, 0xc7, 0x5c, 0x43, 0x24, 0xd2, 0xf0, 0xda, 0x5f, 0xe0, 0x8e, 0xe9,
|
||||
0x01, 0x7d, 0x08, 0x9a, 0x5e, 0xb0, 0x82, 0x0a, 0xb3, 0xeb, 0x15, 0xe9, 0x37, 0xeb, 0x72, 0x46,
|
||||
0x6f, 0x93, 0xa8, 0xfb, 0xe6, 0xe9, 0xf2, 0xae, 0x1d, 0xfc, 0x48, 0x84, 0xf2, 0x94, 0x69, 0x10,
|
||||
0x57, 0x8d, 0x79, 0xb5, 0x3a, 0xa0, 0x1e, 0x2b, 0x23, 0xf0, 0x14, 0x37, 0x97, 0x6e, 0x91, 0x69,
|
||||
0x70, 0xcb, 0x62, 0x72, 0xcb, 0x59, 0x72, 0x64, 0xf1, 0xbf, 0xa2, 0x75, 0xcb, 0x7b, 0x55, 0xed,
|
||||
0xc4, 0xa8, 0xe1, 0x7d, 0x29, 0x27, 0xff, 0x25, 0xc1, 0xc3, 0xd6, 0x78, 0x85, 0x2a, 0xcc, 0xeb,
|
||||
0x40, 0x53, 0x54, 0x2f, 0x90, 0x75, 0x77, 0xe2, 0x62, 0xb0, 0x48, 0xfa, 0xe9, 0x8a, 0x67, 0x46,
|
||||
0x81, 0x2a, 0x83, 0xb7, 0xc7, 0xe9, 0xee, 0x48, 0x61, 0x5e, 0xc0, 0xbd, 0x47, 0xe0, 0x8c, 0xd7,
|
||||
0xce, 0x07, 0x78, 0x6b, 0x54, 0x85, 0xe0, 0xde, 0x8c, 0xcf, 0x96, 0x9c, 0xc3, 0xe2, 0xfc, 0x0f,
|
||||
0xf9, 0x07, 0x22, 0xe5, 0x1d, 0xdf, 0xa6, 0x33, 0x63, 0x50, 0x63, 0x6c, 0x9a, 0x2b, 0xff, 0xec,
|
||||
0xc0, 0x03, 0x46, 0x6f, 0xe2, 0xeb, 0x7c, 0xa7, 0xff, 0x76, 0xb5, 0xd8, 0xa4, 0xeb, 0xf6, 0xc3,
|
||||
0x5f, 0xa4, 0x85, 0xb2, 0x78, 0xae, 0x04, 0x00, 0x00,
|
||||
};
|
||||
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};
|
||||
|
||||
}
|
21
platformio/relayctl/src/static.h
Normal file
21
platformio/relayctl/src/static.h
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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 style_css;
|
||||
extern const StaticFile favicon_ico;
|
||||
|
||||
}
|
30
platformio/relayctl/src/stopwatch.h
Normal file
30
platformio/relayctl/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;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
49
platformio/relayctl/src/wifi.cpp
Normal file
49
platformio/relayctl/src/wifi.cpp
Normal file
@ -0,0 +1,49 @@
|
||||
#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 WIFI_AP_SSID[] = DEFAULT_WIFI_AP_SSID;
|
||||
const char WIFI_STA_SSID[] = DEFAULT_WIFI_STA_SSID;
|
||||
const char WIFI_STA_PSK[] = DEFAULT_WIFI_STA_PSK;
|
||||
|
||||
void getConfig(ConfigData& cfg, char** ssid_dst, char** psk_dst, char** hostname_dst) {
|
||||
if (cfg.flags.wifi_configured) {
|
||||
*ssid_dst = cfg.wifi_ssid;
|
||||
*psk_dst = cfg.wifi_psk;
|
||||
if (hostname_dst != nullptr)
|
||||
*hostname_dst = cfg.node_id;
|
||||
} else {
|
||||
*ssid_dst = (char*)WIFI_STA_SSID;
|
||||
*psk_dst = (char*)WIFI_STA_PSK;
|
||||
if (hostname_dst != nullptr)
|
||||
*hostname_dst = (char*)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;
|
||||
}
|
||||
|
||||
}
|
35
platformio/relayctl/src/wifi.h
Normal file
35
platformio/relayctl/src/wifi.h
Normal file
@ -0,0 +1,35 @@
|
||||
#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, char **ssid_dst, char **psk_dst, char **hostname_dst);
|
||||
std::shared_ptr<std::list<ScanResult>> scan();
|
||||
|
||||
inline int8_t getRSSI() {
|
||||
return WiFi.RSSI();
|
||||
}
|
||||
|
||||
inline uint32_t getIPAsInteger() {
|
||||
if (!WiFi.isConnected())
|
||||
return 0;
|
||||
return WiFi.localIP().v4();
|
||||
}
|
||||
|
||||
extern const char WIFI_AP_SSID[];
|
||||
extern const char WIFI_STA_SSID[];
|
||||
extern const char WIFI_STA_PSK[];
|
||||
extern const char NODE_ID[];
|
||||
|
||||
}
|
222
platformio/relayctl/static/app.js
Normal file
222
platformio/relayctl/static/app.js
Normal file
@ -0,0 +1,222 @@
|
||||
function isObject(o) {
|
||||
return Object.prototype.toString.call(o) === '[object Object]';
|
||||
}
|
||||
|
||||
function ge(id) {
|
||||
return document.getElementById(id)
|
||||
}
|
||||
|
||||
function cancelEvent(evt) {
|
||||
if (evt.preventDefault) evt.preventDefault();
|
||||
if (evt.stopPropagation) evt.stopPropagation();
|
||||
|
||||
evt.cancelBubble = true;
|
||||
evt.returnValue = false;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function errorText(e) {
|
||||
return e instanceof Error ? e.message : e+''
|
||||
}
|
||||
|
||||
(function() {
|
||||
function request(method, url, data, callback) {
|
||||
data = data || null;
|
||||
|
||||
if (typeof callback != 'function') {
|
||||
throw new Error('callback must be a function');
|
||||
}
|
||||
|
||||
if (!url)
|
||||
throw new Error('no url specified');
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
if (isObject(data)) {
|
||||
for (var k in data) {
|
||||
if (data.hasOwnProperty(k))
|
||||
url += (url.indexOf('?') === -1 ? '?' : '&')+encodeURIComponent(k)+'='+encodeURIComponent(data[k])
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
if (isObject(data)) {
|
||||
var sdata = [];
|
||||
for (var k in data) {
|
||||
if (data.hasOwnProperty(k))
|
||||
sdata.push(encodeURIComponent(k)+'='+encodeURIComponent(data[k]));
|
||||
}
|
||||
data = sdata.join('&');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open(method, url);
|
||||
|
||||
if (method === 'POST')
|
||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4) {
|
||||
if ('status' in xhr && !/^2|1223/.test(xhr.status))
|
||||
throw new Error('http code '+xhr.status)
|
||||
callback(null, JSON.parse(xhr.responseText));
|
||||
}
|
||||
};
|
||||
xhr.onerror = function(e) {
|
||||
callback(e, null);
|
||||
};
|
||||
|
||||
xhr.send(method === 'GET' ? null : data);
|
||||
return xhr;
|
||||
}
|
||||
|
||||
window.ajax = {
|
||||
get: request.bind(request, 'GET'),
|
||||
post: request.bind(request, 'POST')
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
function lock(el) {
|
||||
el.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
|
||||
function unlock(el) {
|
||||
el.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
function initNetworkSettings() {
|
||||
function setupField(el, value) {
|
||||
if (value !== null)
|
||||
el.value = value;
|
||||
unlock(el);
|
||||
}
|
||||
|
||||
var doneRequestsCount = 0;
|
||||
function onRequestDone() {
|
||||
doneRequestsCount++;
|
||||
if (doneRequestsCount === 2) {
|
||||
ge('loading_label').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
var form = document.forms.network_settings;
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (!form.nid.value.trim()) {
|
||||
alert('Введите node id');
|
||||
return cancelEvent(e);
|
||||
}
|
||||
|
||||
if (form.psk.value.length < 8) {
|
||||
alert('Неверный пароль (минимальная длина - 8 символов)');
|
||||
return cancelEvent(e);
|
||||
}
|
||||
|
||||
if (form.ssid.selectedIndex == -1) {
|
||||
alert('Не выбрана точка доступа');
|
||||
return cancelEvent(e);
|
||||
}
|
||||
|
||||
lock(form.submit)
|
||||
})
|
||||
form.show_psk.addEventListener('change', function(e) {
|
||||
form.psk.setAttribute('type', e.target.checked ? 'text' : 'password');
|
||||
});
|
||||
form.ssid.addEventListener('change', function(e) {
|
||||
var i = e.target.selectedIndex;
|
||||
if (i !== -1) {
|
||||
var opt = e.target.options[i];
|
||||
if (opt)
|
||||
form.psk.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
ajax.get('/status', {}, function(error, response) {
|
||||
try {
|
||||
if (error)
|
||||
throw error;
|
||||
|
||||
setupField(form.nid, response.node_id || null);
|
||||
setupField(form.psk, null);
|
||||
setupField(form.submit, null);
|
||||
|
||||
onRequestDone();
|
||||
} catch (error) {
|
||||
alert(errorText(error));
|
||||
}
|
||||
});
|
||||
|
||||
ajax.get('/scan', {}, function(error, response) {
|
||||
try {
|
||||
if (error)
|
||||
throw error;
|
||||
|
||||
form.ssid.innerHTML = '';
|
||||
for (var i = 0; i < response.list.length; i++) {
|
||||
var ssid = response.list[i][0];
|
||||
var rssi = response.list[i][1];
|
||||
form.ssid.append(new Option(ssid + ' (' + rssi + ' dBm)', ssid));
|
||||
}
|
||||
unlock(form.ssid);
|
||||
|
||||
onRequestDone();
|
||||
} catch (error) {
|
||||
alert(errorText(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initUpdateForm() {
|
||||
var form = document.forms.update_settings;
|
||||
form.addEventListener('submit', function(e) {
|
||||
cancelEvent(e);
|
||||
if (!form.file.files.length) {
|
||||
alert('Файл обновления не выбран');
|
||||
return false;
|
||||
}
|
||||
|
||||
lock(form.submit);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
var fd = new FormData();
|
||||
fd.append('file', form.file.files[0]);
|
||||
|
||||
xhr.upload.addEventListener('progress', function (e) {
|
||||
var total = form.file.files[0].size;
|
||||
var progress;
|
||||
if (e.loaded < total) {
|
||||
progress = Math.round(e.loaded / total * 100).toFixed(2);
|
||||
} else {
|
||||
progress = 100;
|
||||
}
|
||||
form.submit.innerHTML = progress + '%';
|
||||
});
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4) {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
if (response.result === 1) {
|
||||
alert('Обновление завершено, устройство перезагружается');
|
||||
} else {
|
||||
alert('Ошибка обновления');
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.onerror = function(e) {
|
||||
alert(errorText(e))
|
||||
};
|
||||
|
||||
xhr.open('POST', e.target.action);
|
||||
xhr.send(fd);
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function initApp() {
|
||||
initNetworkSettings();
|
||||
initUpdateForm();
|
||||
}
|
BIN
platformio/relayctl/static/favicon.ico
Normal file
BIN
platformio/relayctl/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.7 KiB |
62
platformio/relayctl/static/index.html
Normal file
62
platformio/relayctl/static/index.html
Normal file
@ -0,0 +1,62 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Configuration</title>
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||
<script src="/app.js"></script>
|
||||
</head>
|
||||
<body onload="initApp()">
|
||||
<div class="title">Settings <span id="loading_label">(loading...)</span></div>
|
||||
<div class="block">
|
||||
<form method="post" action="/status" name="network_settings">
|
||||
<div class="form_label">WiFi SSID</div>
|
||||
<div class="form_input">
|
||||
<select id="ssid_select" name="ssid" class="full-width">
|
||||
<option value="">Loading...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form_label">WiFi Password</div>
|
||||
<div class="form_input">
|
||||
<input type="password" value="" name="psk" class="full-width" id="fld_psk" maxlength="63" disabled>
|
||||
<div class="form_checkbox">
|
||||
<label for="show_psk"><input type="checkbox" name="show_psk" id="show_psk"> show password</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form_label">Node ID</div>
|
||||
<div class="form_input">
|
||||
<input type="text" value="" maxlength="16" name="nid" id="fld_nid" class="full-width" disabled>
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled="disabled" name="submit">Save and Reboot</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="title">Update firmware (.bin)</div>
|
||||
<div class="block">
|
||||
<form method="post" action="/update" enctype="multipart/form-data" name="update_settings">
|
||||
<div class="form_input">
|
||||
<input type="file" accept=".bin,.bin.gz" name="file">
|
||||
</div>
|
||||
<button type="submit" name="submit">Upload</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="title">Reset settings</div>
|
||||
<div class="block">
|
||||
<form method="post" action="/reset">
|
||||
<button type="submit" name="submit" class="is_reset">Reset</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="title">Info</div>
|
||||
<div class="block">
|
||||
ESP8266-based <b>relayctl</b>, firmware v{version}<br>
|
||||
Part of <a href="https://git.ch1p.io/homekit.git/">homekit</a> by <a href="https://ch1p.io">Evgeny Zinoviev</a> © 2022
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
85
platformio/relayctl/static/style.css
Normal file
85
platformio/relayctl/static/style.css
Normal file
@ -0,0 +1,85 @@
|
||||
body, html {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
body, button, input[type="text"], input[type="password"] {
|
||||
font-size: 16px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 10px 10px 6px;
|
||||
font-weight: 600;
|
||||
background-color: #eff2f5;
|
||||
border-bottom: 1px #d9e0e7 solid;
|
||||
color: #276eb4;
|
||||
font-size: 15px;
|
||||
}
|
||||
.block {
|
||||
padding: 10px;
|
||||
}
|
||||
.full-width {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form_label {
|
||||
padding: 0 0 3px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.form_input {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.form_checkbox {
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
select {
|
||||
border-radius: 4px;
|
||||
border: 1px #c9cccf solid;
|
||||
padding: 7px 9px;
|
||||
outline: none;
|
||||
}
|
||||
input[type="text"]:focus,
|
||||
input[type="password"]:focus,
|
||||
select:focus {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
input[type="text"]:disabled,
|
||||
input[type="password"]:disabled,
|
||||
select:disabled {
|
||||
background-color: #f1f2f3;
|
||||
border-color: #f1f2f3;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 4px;
|
||||
border: 1px #c9cccf solid;
|
||||
padding: 7px 15px;
|
||||
outline: none;
|
||||
background: #fff;
|
||||
color: #000; /* fix for iOS */
|
||||
position: relative;
|
||||
line-height: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
button:disabled {
|
||||
background-color: #f1f2f3;
|
||||
border-color: #f1f2f3;
|
||||
}
|
||||
button:not(:disabled):hover {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
|
||||
cursor: pointer;
|
||||
border-color: #b5cce3;
|
||||
color: #276eb4;
|
||||
}
|
||||
button:not(:disabled):active {
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
button.is_reset,
|
||||
button.is_reset:not(:disabled):hover {
|
||||
color: #e63917;
|
||||
}
|
64
tools/minify.js
Executable file
64
tools/minify.js
Executable file
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {minify: minifyJs} = require('terser')
|
||||
const {minify: minifyHtml} = require('html-minifier-terser')
|
||||
const CleanCSS = require('clean-css');
|
||||
const parseArgs = require('minimist')
|
||||
const {promises: fs} = require('fs')
|
||||
|
||||
const argv = process.argv.slice(2)
|
||||
if (!argv.length) {
|
||||
console.log(`usage: ${process.argv[1]} --type js|css|html filename`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
async function read() {
|
||||
const chunks = []
|
||||
for await (const chunk of process.stdin)
|
||||
chunks.push(chunk)
|
||||
return Buffer.concat(chunks).toString('utf-8')
|
||||
}
|
||||
|
||||
const args = parseArgs(argv, {
|
||||
string: ['type'],
|
||||
})
|
||||
|
||||
;(async () => {
|
||||
if (!['js', 'css', 'html'].includes(args.type))
|
||||
throw new Error('invalid type')
|
||||
|
||||
const content = await read()
|
||||
|
||||
switch (args.type) {
|
||||
case 'html':
|
||||
console.log(await minifyHtml(content, {
|
||||
collapseBooleanAttributes: true,
|
||||
collapseInlineTagWhitespace: true,
|
||||
collapseWhitespace: true,
|
||||
conservativeCollapse: true,
|
||||
html5: true,
|
||||
includeAutoGeneratedTags: true,
|
||||
keepClosingSlash: false,
|
||||
minifyCSS: true,
|
||||
minifyJS: true,
|
||||
minifyURLs: false,
|
||||
preserveLineBreaks: true,
|
||||
removeComments: true,
|
||||
removeAttributeQuotes: false,
|
||||
sortAttributes: false,
|
||||
sortClassName: false,
|
||||
useShortDoctype: true,
|
||||
}))
|
||||
break
|
||||
|
||||
case 'css':
|
||||
console.log(new CleanCSS({level:2}).minify(content).styles)
|
||||
break
|
||||
|
||||
case 'js':
|
||||
console.log((await minifyJs(content, {
|
||||
ecma: 5
|
||||
})).code)
|
||||
break
|
||||
}
|
||||
})()
|
180
yarn.lock
Normal file
180
yarn.lock
Normal file
@ -0,0 +1,180 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@jridgewell/gen-mapping@^0.3.0":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
|
||||
integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
|
||||
dependencies:
|
||||
"@jridgewell/set-array" "^1.0.1"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
"@jridgewell/trace-mapping" "^0.3.9"
|
||||
|
||||
"@jridgewell/resolve-uri@3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
|
||||
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
|
||||
|
||||
"@jridgewell/set-array@^1.0.1":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
|
||||
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
|
||||
|
||||
"@jridgewell/source-map@^0.3.2":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb"
|
||||
integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==
|
||||
dependencies:
|
||||
"@jridgewell/gen-mapping" "^0.3.0"
|
||||
"@jridgewell/trace-mapping" "^0.3.9"
|
||||
|
||||
"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10":
|
||||
version "1.4.14"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
|
||||
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
|
||||
|
||||
"@jridgewell/trace-mapping@^0.3.9":
|
||||
version "0.3.17"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985"
|
||||
integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==
|
||||
dependencies:
|
||||
"@jridgewell/resolve-uri" "3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "1.4.14"
|
||||
|
||||
acorn@^8.5.0:
|
||||
version "8.8.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73"
|
||||
integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==
|
||||
|
||||
buffer-from@^1.0.0:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
||||
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||
|
||||
camel-case@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a"
|
||||
integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==
|
||||
dependencies:
|
||||
pascal-case "^3.1.2"
|
||||
tslib "^2.0.3"
|
||||
|
||||
clean-css@5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.2.0.tgz#44e4a04e8873ff0041df97acecf23a4a6519844e"
|
||||
integrity sha512-2639sWGa43EMmG7fn8mdVuBSs6HuWaSor+ZPoFWzenBc6oN+td8YhTfghWXZ25G1NiiSvz8bOFBS7PdSbTiqEA==
|
||||
dependencies:
|
||||
source-map "~0.6.0"
|
||||
|
||||
clean-css@^5.3.1:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.1.tgz#d0610b0b90d125196a2894d35366f734e5d7aa32"
|
||||
integrity sha512-lCr8OHhiWCTw4v8POJovCoh4T7I9U11yVsPjMWWnnMmp9ZowCxyad1Pathle/9HjaDp+fdQKjO9fQydE6RHTZg==
|
||||
dependencies:
|
||||
source-map "~0.6.0"
|
||||
|
||||
commander@^2.20.0:
|
||||
version "2.20.3"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||
|
||||
commander@^9.4.1:
|
||||
version "9.4.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.1.tgz#d1dd8f2ce6faf93147295c0df13c7c21141cfbdd"
|
||||
integrity sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==
|
||||
|
||||
dot-case@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
|
||||
integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==
|
||||
dependencies:
|
||||
no-case "^3.0.4"
|
||||
tslib "^2.0.3"
|
||||
|
||||
entities@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174"
|
||||
integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==
|
||||
|
||||
html-minifier-terser@^7.1.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-7.1.0.tgz#cd62d42158be9a6bef0fcd40f94127345743d9b5"
|
||||
integrity sha512-BvPO2S7Ip0Q5qt+Y8j/27Vclj6uHC6av0TMoDn7/bJPhMWHI2UtR2e/zEgJn3/qYAmxumrGp9q4UHurL6mtW9Q==
|
||||
dependencies:
|
||||
camel-case "^4.1.2"
|
||||
clean-css "5.2.0"
|
||||
commander "^9.4.1"
|
||||
entities "^4.4.0"
|
||||
param-case "^3.0.4"
|
||||
relateurl "^0.2.7"
|
||||
terser "^5.15.1"
|
||||
|
||||
lower-case@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
|
||||
integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
|
||||
dependencies:
|
||||
tslib "^2.0.3"
|
||||
|
||||
minimist@^1.2.7:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
|
||||
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
|
||||
|
||||
no-case@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
|
||||
integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
|
||||
dependencies:
|
||||
lower-case "^2.0.2"
|
||||
tslib "^2.0.3"
|
||||
|
||||
param-case@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"
|
||||
integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==
|
||||
dependencies:
|
||||
dot-case "^3.0.4"
|
||||
tslib "^2.0.3"
|
||||
|
||||
pascal-case@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb"
|
||||
integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==
|
||||
dependencies:
|
||||
no-case "^3.0.4"
|
||||
tslib "^2.0.3"
|
||||
|
||||
relateurl@^0.2.7:
|
||||
version "0.2.7"
|
||||
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
|
||||
integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==
|
||||
|
||||
source-map-support@~0.5.20:
|
||||
version "0.5.21"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
|
||||
integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
|
||||
dependencies:
|
||||
buffer-from "^1.0.0"
|
||||
source-map "^0.6.0"
|
||||
|
||||
source-map@^0.6.0, source-map@~0.6.0:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
terser@^5.15.1, terser@^5.16.1:
|
||||
version "5.16.1"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.1.tgz#5af3bc3d0f24241c7fb2024199d5c461a1075880"
|
||||
integrity sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw==
|
||||
dependencies:
|
||||
"@jridgewell/source-map" "^0.3.2"
|
||||
acorn "^8.5.0"
|
||||
commander "^2.20.0"
|
||||
source-map-support "~0.5.20"
|
||||
|
||||
tslib@^2.0.3:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
|
||||
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
|
Loading…
x
Reference in New Issue
Block a user