esp8266 relay controller wip

This commit is contained in:
Evgeny Zinoviev 2022-11-28 05:45:20 +03:00
parent 16d47968b4
commit 7bb6daa4bf
30 changed files with 2154 additions and 0 deletions

5
.gitignore vendored
View File

@ -1,12 +1,17 @@
.idea .idea
/venv /venv
/node_modules
*.pyc *.pyc
config.def.h
__pycache__ __pycache__
.DS_Store .DS_Store
/src/test/test_inverter_monitor.log /src/test/test_inverter_monitor.log
/youtrack-certificate /youtrack-certificate
/cpp /cpp
/esp32-cam/CameraWebServer/wifi_password.h /esp32-cam/CameraWebServer/wifi_password.h
cmake-build-*
.pio
CMakeListsPrivate.txt
*.swp *.swp

14
package.json Normal file
View 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
View File

@ -0,0 +1,3 @@
.pio
CMakeListsPrivate.txt
cmake-build-*/

View 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})

View 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"

View 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

View 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;
}
}

View 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])

View 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);
}

View 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;
}
}

View 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();
};
}

View 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);
}
}

View 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;
};
}

View 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

View 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

View 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);
}

View 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();
}
}

View 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));
}

View 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);
}
}

View 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};
}

View 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;
}

View 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;
}
};
}

View 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;
}
}

View 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[];
}

View 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();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View 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> &copy; 2022
</div>
</body>
</html>

View 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
View 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
View 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==