--- /dev/null
+#pragma once
+#include <ESP8266WiFi.h>
+#include <Printable.h>
+#include <LittleFS.h>
+#include <RTClib.h>
+
+// net.cpp
+
+extern bool isNetConnected;
+
+void setupNet(bool AP = false);
+void tickNet();
+
+// time.cpp
+
+extern bool isTimeSet;
+extern time_t now;
+extern time_t last_sync;
+
+extern int hh;
+extern int mi;
+extern int ss;
+
+extern int dw;
+
+extern int dd;
+extern int mm;
+extern int yy;
+
+void registerTimeHandler(const char* handlerName, const char handlerType, std::function<void()> timeHandler);
+void unregisterTimeHandler(const char* handlerName);
+void registerTimeHandler(const __FlashStringHelper* handlerName, const char handlerType, std::function<void()> timeHandler);
+void unregisterTimeHandler(const __FlashStringHelper* handlerName);
+void setupHandlers();
+
+void setupTime();
+void tickTime();
+
+bool isNight();
+
+// config.cpp
+
+char* copystr(const char* s);
+char* copystr(const __FlashStringHelper* s);
+
+union ValueType {
+ bool boolValue;
+ int intValue;
+ double floatValue;
+ char *charValue;
+};
+
+class ConfigParameter: public Printable {
+ public:
+
+ ConfigParameter(const char *id, const char* value);
+ ConfigParameter(const char *id, int value);
+ ConfigParameter(const char *id, double value);
+ ConfigParameter(const char *id, bool value);
+ ConfigParameter(const __FlashStringHelper* id, const char* value);
+ ConfigParameter(const __FlashStringHelper* id, int value);
+ ConfigParameter(const __FlashStringHelper* id, double value);
+ ConfigParameter(const __FlashStringHelper* id, bool value);
+ ~ConfigParameter();
+
+ const char *getID() const;
+ char getType() const;
+ const bool getBoolValue() const;
+ const int getIntValue() const ;
+ const double getFloatValue() const;
+ const char *getCharValue() const;
+
+ void setValue(const bool value);
+ void setValue(const int value);
+ void setValue(const double value);
+ void setValue(const char *value);
+
+ virtual size_t printTo(Print& p) const;
+
+ protected:
+ void init(const char *id, char type);
+ void init(const __FlashStringHelper *id, char type);
+
+ private:
+ const char *_id;
+ char _type; // _B_oolean, _I_nt, double _F_loat, _S_tring
+ ValueType _value;
+};
+
+class Config: public Printable {
+ public:
+
+ Config();
+ ~Config();
+
+ ConfigParameter* getParameter(int i);
+ int getParametersCount() const;
+
+ void setValue(const char *id, const char *value);
+ void setValue(const char *id, int value);
+ void setValue(const char *id, double value);
+ void setValue(const char *id, bool value);
+ int getIntValue(const char *id) const;
+ double getFloatValue(const char *id) const;
+ bool getBoolValue(const char *id) const;
+ const char* getCharValue(const char *id) const;
+
+ void setValue(const __FlashStringHelper* id, const char *value);
+ void setValue(const __FlashStringHelper* id, int value);
+ void setValue(const __FlashStringHelper* id, double value);
+ void setValue(const __FlashStringHelper* id, bool value);
+ int getIntValue(const __FlashStringHelper* id) const;
+ double getFloatValue(const __FlashStringHelper* id) const ;
+ bool getBoolValue(const __FlashStringHelper* id) const;
+ const char* getCharValue(const __FlashStringHelper* id) const;
+
+ ConfigParameter* getParam(const char *id) const;
+ ConfigParameter* getParam(const __FlashStringHelper* id) const;
+
+ virtual size_t printTo(Print& p) const;
+
+ void readFrom(Stream& s);
+ void dumpJson(Stream& s) const;
+
+ void clear();
+
+ unsigned long getTimestamp();
+ void resetTimestamp();
+
+ protected:
+ void init();
+ void addParameter(ConfigParameter* p);
+
+ private:
+ int _paramsCount;
+ int _max_params;
+ unsigned long _timestamp;
+ ConfigParameter** _params;
+
+};
+
+extern Config cfg;
+void setupConfig();
+void saveConfig(bool force = false);
+void reboot();
+void reset();
+
+// hardware.cpp
+
+extern bool isRTCEnabled;
+extern RTC_DS3231 RTC;
+
+void setupHardware();
+void tickHardware();
+
+void beep(int tone, int length, int beep_ms = 60000, int silent_ms = 0);
+
+// panel.cpp
+
+#define mDefault 0
+#define mTime 1
+#define mDate 2
+#define mWeather 3
+#define mBarrier 4
+#define mMessage 5
+#define mLast 6
+
+extern int screenMode;
+extern int screenModeRequested;
+
+void setupPanel();
+void tickPanel();
+
+void utf8rus(const char* source, char* target, int maxLen = 255);
+
+void message(const char* str, int priority=10);
+void message(const __FlashStringHelper* str, int priority=10);
+void messageModal(const char* str);
+void messageModal(const __FlashStringHelper* str);
+void scroll(const char* str, bool force = false);
+void setPanelBrightness();
+
+// alarm.cpp
+
+void setupAlarm();
+
+// weather.cpp
+
+extern char weatherData[256];
+
+void setupWeatherRequest();
+
+// web.cpp
+
+extern bool isApEnabled;
+
+void setupWeb();
+void tickWeb();
+void reportChange(const __FlashStringHelper* name);
+void reportMessage(const __FlashStringHelper* msg);
+void sendWeather();
--- /dev/null
+#include "Clock.h"
+#include <LittleFS.h>
+
+void setup() {
+ // put your setup code here, to run once:
+
+ Serial.begin(115200);
+ Serial.println();
+ Serial.println(F("Starting..."));
+
+ setupConfig();
+ Serial.println(cfg);
+
+ setupHandlers();
+
+ setupHardware();
+ setupPanel();
+
+ setupNet();
+ setupTime();
+
+ setupAlarm();
+ setupWeatherRequest();
+
+ setupWeb();
+}
+
+void mem() {
+ Serial.println(F("-------------------------------------------------------------"));
+ Serial.print("Heap:"); Serial.print(ESP.getFreeHeap());
+ Serial.print(" Largest chunk:"); Serial.print(ESP.getMaxFreeBlockSize());
+ Serial.print(" Fragmentation:"); Serial.print(ESP.getHeapFragmentation());
+ Serial.print(" Stack:"); Serial.println(ESP.getFreeContStack());
+ Serial.println(F("-------------------------------------------------------------"));
+}
+
+void loop() {
+ static unsigned long lastMillis = 0;
+ int interval = 15000;
+ // put your main code here, to run repeatedly:
+ if (millis() - lastMillis > interval) {
+ lastMillis = millis();
+ mem();
+ }
+ tickNet();
+ tickTime();
+ tickHardware();
+ tickPanel();
+ tickWeb();
+}
--- /dev/null
+#include "Clock.h"
+
+void hourlyBeep() {
+ bool enable_hourly = cfg.getBoolValue(F("enable_hourly"));
+ int hourly_count = cfg.getIntValue(F("hourly_count"));
+ bool hourly_night = cfg.getBoolValue(F("hourly_night"));
+ int hourly_beep_ms = cfg.getIntValue(F("hourly_beep_ms"));
+ int hourly_silent_ms = cfg.getIntValue(F("hourly_silent_ms"));
+ int hourly_tone = cfg.getIntValue(F("hourly_tone"));
+ if ( enable_hourly && hourly_count && (!isNight() || hourly_night)) {
+ int length = hourly_count * hourly_beep_ms + (hourly_count - 1) * hourly_silent_ms;
+ beep(hourly_tone, length, hourly_beep_ms, hourly_silent_ms);
+ }
+}
+
+void checkAlarm() {
+ bool enable_alarm = cfg.getBoolValue(F("enable_alarm"));
+ int alarm_hour = cfg.getIntValue(F("alarm_hour"));
+ int alarm_minute = cfg.getIntValue(F("alarm_minute"));
+ const char* alarm_days = cfg.getCharValue(F("alarm_days"));
+ int alarm_tone = cfg.getIntValue(F("alarm_tone"));
+ int alarm_length = cfg.getIntValue(F("alarm_length"));
+ int alarm_beep_ms = cfg.getIntValue(F("alarm_beep_ms"));
+ int alarm_silent_ms = cfg.getIntValue(F("alarm_silent_ms"));
+ if ( enable_alarm && (hh == alarm_hour) && (mi == alarm_minute) && (alarm_days[dw?dw-1:6]!='0')) {
+ beep(alarm_tone, alarm_length * 1000, alarm_beep_ms, alarm_silent_ms);
+ reportMessage(F("Будильник сработал!"));
+ }
+}
+
+void setupAlarm() {
+ unregisterTimeHandler(F("hourly-beep"));
+ unregisterTimeHandler(F("check-alarm"));
+ registerTimeHandler(F("hourly-beep"),'h',hourlyBeep);
+ registerTimeHandler(F("check-alarm"),'m',checkAlarm);
+}
+
--- /dev/null
+#include "Clock.h"
+#include <Print.h>
+
+#define MAX_PARAMS 64
+
+Config cfg;
+
+char* copystr(const char* s) {
+ int len = strlen(s);
+ char* res = new char[len+1];
+ strcpy(res, s);
+ return res;
+}
+
+char* copystr(const __FlashStringHelper* s) {
+ int len = strlen_P((PGM_P)s);
+ char* res = new char[len+1];
+ strcpy_P(res, (PGM_P)s);
+ return res;
+}
+
+ConfigParameter::ConfigParameter(const char *id, const char* value) {
+ init(id, 'S');
+ setValue(value);
+};
+
+ConfigParameter::ConfigParameter(const char *id, int value) {
+ init(id, 'I');
+ setValue(value);
+};
+
+ConfigParameter::ConfigParameter(const char *id, double value) {
+ init(id, 'F');
+ setValue(value);
+};
+
+ConfigParameter::ConfigParameter(const char *id, bool value) {
+ init(id, 'B');
+ setValue(value);
+};
+
+ConfigParameter::ConfigParameter(const __FlashStringHelper *id, const char* value) {
+ init(id, 'S');
+ setValue(value);
+};
+
+ConfigParameter::ConfigParameter(const __FlashStringHelper *id, int value) {
+ init(id, 'I');
+ setValue(value);
+};
+
+ConfigParameter::ConfigParameter(const __FlashStringHelper *id, double value) {
+ init(id, 'F');
+ setValue(value);
+};
+
+ConfigParameter::ConfigParameter(const __FlashStringHelper *id, bool value) {
+ init(id, 'B');
+ setValue(value);
+};
+
+ConfigParameter::~ConfigParameter() {
+ delete[] _id;
+ if (_type=='S' && _value.charValue) {
+ delete[] _value.charValue;
+ }
+};
+
+const char *ConfigParameter::getID() const {
+ return _id;
+};
+
+char ConfigParameter::getType() const {
+ return _type;
+};
+
+const bool ConfigParameter::getBoolValue() const {
+ return (_type=='B')?_value.boolValue:false;
+};
+
+const int ConfigParameter::getIntValue() const {
+ return (_type=='I')?_value.intValue:0;
+};
+
+const double ConfigParameter::getFloatValue() const {
+ return (_type=='F')?_value.floatValue:0.0;
+};
+
+const char *ConfigParameter::getCharValue() const {
+ return (_type=='S')?_value.charValue:"";
+};
+
+void ConfigParameter::setValue(const bool value) {
+ if (_type=='B') {
+ _value.boolValue = value;
+ }
+};
+
+void ConfigParameter::setValue(const int value) {
+ if (_type=='I') {
+ _value.intValue = value;
+ }
+};
+
+void ConfigParameter::setValue(const double value) {
+ if (_type=='F') {
+ _value.floatValue = value;
+ }
+};
+
+void ConfigParameter::setValue(const char *value) {
+ if (_type=='S') {
+ int length = strlen(value);
+ if (!_value.charValue) {
+ _value.charValue = new char[length + 1];
+ } else if (strlen(_value.charValue) != length) {
+ delete[] _value.charValue;
+ _value.charValue = new char[length + 1];
+ }
+ strcpy(_value.charValue,value);
+ }
+};
+
+void ConfigParameter::init(const char *id, char type) {
+ _id = copystr(id);
+ _type = type;
+ _value.charValue = nullptr;
+};
+
+void ConfigParameter::init(const __FlashStringHelper *id, char type) {
+ _id = copystr(id);
+ _type = type;
+ _value.charValue = nullptr;
+};
+
+size_t ConfigParameter::printTo(Print& p) const {
+ size_t n = 0;
+ n += p.print(_id); n += p.print(":"); n += p.print(_type); n += p.print(":");
+ switch (_type) {
+ case 'B':
+ if (_value.boolValue) { n += p.print("true"); }
+ else { n += p.print("false"); }
+ break;
+ case 'I':
+ n += p.print(_value.intValue);
+ break;
+ case 'F':
+ n += p.print(_value.floatValue);
+ break;
+ case 'S':
+ n += p.print(_value.charValue);
+ break;
+ }
+ return n;
+}
+
+Config::Config() {
+ init();
+}
+
+void Config::init() {
+ _paramsCount = 0;
+ _max_params = MAX_PARAMS;
+ _params = NULL;
+ _timestamp = 0;
+}
+
+Config::~Config() {
+ if (_params != NULL) {
+ for (int i = 0; i < _paramsCount; i++) {
+ delete _params[i];
+ _params[i] = nullptr;
+ }
+ free(_params);
+ _params = NULL;
+ }
+}
+
+ConfigParameter* Config::getParameter(int i) {
+ return _params[i];
+}
+
+int Config::getParametersCount() const {
+ return _paramsCount;
+}
+
+ConfigParameter* Config::getParam(const char *id) const {
+ for (int j = 0; j < _paramsCount; j++) {
+ if (strcmp(id, _params[j]->getID()) == 0) {
+ return _params[j];
+ }
+ }
+ return nullptr;
+}
+
+ConfigParameter* Config::getParam(const __FlashStringHelper *id) const {
+ for (int j = 0; j < _paramsCount; j++) {
+ if (strcmp_P(_params[j]->getID(),(PGM_P)id) == 0) {
+ return _params[j];
+ }
+ }
+ return nullptr;
+}
+
+void Config::addParameter(ConfigParameter* p) {
+ if (_params == NULL) {
+ _params = (ConfigParameter**)malloc(_max_params * sizeof(ConfigParameter*));
+ }
+ if (_paramsCount == _max_params) {
+ _max_params += MAX_PARAMS;
+ ConfigParameter** new_params = (ConfigParameter**)realloc(_params, _max_params * sizeof(ConfigParameter*));
+ if (new_params != NULL) {
+ _params = new_params;
+ } else {
+ Serial.println(F("Не удалось расширить массив параметров"));
+ return;
+ }
+ }
+ _params[_paramsCount] = p;
+ _paramsCount++;
+}
+
+
+void Config::setValue(const char *id, const char *value) {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ p->setValue(value);
+ } else {
+ addParameter(new ConfigParameter(id,value));
+ }
+ _timestamp = now;
+}
+
+void Config::setValue(const __FlashStringHelper *id, const char *value) {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ p->setValue(value);
+ } else {
+ addParameter(new ConfigParameter(copystr(id),value));
+ }
+ _timestamp = now;
+}
+
+void Config::setValue(const char *id, int value) {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ p->setValue(value);
+ } else {
+ addParameter(new ConfigParameter(id,value));
+ }
+ _timestamp = now;
+}
+
+void Config::setValue(const __FlashStringHelper *id, int value) {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ p->setValue(value);
+ } else {
+ addParameter(new ConfigParameter(copystr(id),value));
+ }
+ _timestamp = now;
+}
+
+void Config::setValue(const char *id, const double value) {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ p->setValue(value);
+ } else {
+ addParameter(new ConfigParameter(id,value));
+ }
+ _timestamp = now;
+}
+
+void Config::setValue(const __FlashStringHelper *id, const double value) {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ p->setValue(value);
+ } else {
+ addParameter(new ConfigParameter(copystr(id),value));
+ }
+ _timestamp = now;
+}
+
+void Config::setValue(const char *id, const bool value) {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ p->setValue(value);
+ } else {
+ addParameter(new ConfigParameter(id,value));
+ }
+ _timestamp = now;
+}
+
+void Config::setValue(const __FlashStringHelper *id, const bool value) {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ p->setValue(value);
+ } else {
+ addParameter(new ConfigParameter(copystr(id),value));
+ }
+ _timestamp = now;
+}
+
+size_t Config::printTo(Print& p) const {
+ size_t n = 0;
+ for (int i=0; i<_paramsCount; i++) {
+ ConfigParameter* param = _params[i];
+ if (param) {
+ n += p.print(*param); n+= p.println();
+ }
+ }
+ return n;
+}
+
+int Config::getIntValue(const char *id) const {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ return p->getIntValue();
+ } else {
+ return 0;
+ }
+}
+
+int Config::getIntValue(const __FlashStringHelper *id) const {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ return p->getIntValue();
+ } else {
+ return 0;
+ }
+}
+
+double Config::getFloatValue(const char *id) const {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ return p->getFloatValue();
+ } else {
+ return 0.0;
+ }
+};
+
+double Config::getFloatValue(const __FlashStringHelper *id) const {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ return p->getFloatValue();
+ } else {
+ return 0.0;
+ }
+};
+
+bool Config::getBoolValue(const char *id) const {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ return p->getBoolValue();
+ } else {
+ return false;
+ }
+};
+
+bool Config::getBoolValue(const __FlashStringHelper *id) const {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ return p->getBoolValue();
+ } else {
+ return false;
+ }
+};
+
+const char* Config::getCharValue(const char *id) const {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ return p->getCharValue();
+ } else {
+ return "";
+ }
+};
+
+const char* Config::getCharValue(const __FlashStringHelper *id) const {
+ ConfigParameter* p = getParam(id);
+ if (p) {
+ return p->getCharValue();
+ } else {
+ return "";
+ }
+};
+
+void Config::readFrom(Stream& s) {
+ char buf[256]; // name + type + value + separators
+ while (s.available()) {
+ int x = s.readBytesUntil('\n',buf,255);
+ buf[x] = 0;
+ if (x>=3) {
+ char* origName = strtok(buf,":\r\n");
+ char* origType = strtok(NULL,":\r\n");
+ char* origValue = strtok(NULL,"\r\n");
+ if (origType && origName && origName[0]) {
+ if (origType[0]=='B') {
+ if (origValue) {
+ setValue(copystr(origName),(strcmp(origValue,"true")==0));
+ } else {
+ setValue(copystr(origName),false);
+ }
+ } else if (origType[0]=='I') {
+ if (origValue) {
+ setValue(copystr(origName),atoi(origValue));
+ } else {
+ setValue(copystr(origName),0);
+ }
+ } else if (origType[0]=='F') {
+ if (origValue) {
+ setValue(copystr(origName),atof(origValue));
+ } else {
+ setValue(copystr(origName),0.0);
+ }
+ } else if (origType[0]=='S') {
+ if (origValue) {
+ setValue(copystr(origName),origValue);
+ } else {
+ setValue(copystr(origName),"");
+ }
+ }
+ }
+ }
+ }
+}
+
+void Config::clear() {
+ if (_params != NULL) {
+ for (int i = 0; i < _paramsCount; i++) {
+ delete _params[i];
+ _params[i] = nullptr;
+ }
+ free(_params);
+ }
+ init();
+}
+
+void Config::dumpJson(Stream& s) const {
+ s.print("{");
+ for (int i=0; i<_paramsCount; i++) {
+ ConfigParameter* param = _params[i];
+ if (i) s.print(",");
+ s.print("\"");
+ s.print(param->getID());
+ s.print("\":");
+ switch (param->getType()) {
+ case 'B':
+ s.print(param->getBoolValue()?"true":"false");
+ break;
+ case 'I':
+ s.print(param->getIntValue());
+ break;
+ case 'F':
+ s.print(param->getFloatValue());
+ break;
+ case 'S':
+ s.print("\"");
+ s.print(param->getCharValue());
+ s.print("\"");
+ break;
+ }
+ }
+ s.print("}");
+}
+
+unsigned long Config::getTimestamp() {
+ return _timestamp;
+}
+
+void Config::resetTimestamp() {
+ _timestamp = 0;
+}
+
+void setupConfig() {
+ if (LittleFS.begin()) {
+ if (File f = LittleFS.open(F("/config.txt"),"r")) {
+ Serial.println('Открываю файл конфигурации');
+ cfg.readFrom(f);
+ f.close();
+ cfg.resetTimestamp();
+ }
+ } else {
+ Serial.println(F("Конфигурация не найдена"));
+ }
+}
+
+void saveConfig(bool force) {
+ if (cfg.getTimestamp() || force) {
+ if (File f = LittleFS.open(F("/config.txt"),"w")) {
+ f.print(cfg);
+ f.close();
+ Serial.println(F("Конфигурация сохранена"));
+ cfg.resetTimestamp();
+ }
+ }
+}
+
+void reboot() {
+ saveConfig();
+ LittleFS.end();
+ messageModal(F("Перезагружаюсь"));
+ ESP.restart();
+}
+
+char* distConfig PROGMEM =
+"sta_ssid:S:\n"
+"sta_psk:S:\n"
+"sta_wait:I:120\n"
+"ap_ssid:S:WiFi_Clock\n"
+"ap_psk:S:1234-5678\n"
+"ntp_server:S:ru.pool.ntp.org\n"
+"tz:S:MSK-3\n"
+"pin_sda:I:4\n"
+"pin_scl:I:5\n"
+"i2c_speed:I:400000\n"
+"enable_rtc:B:true\n"
+"flash_dots:B:true\n"
+"pin_din:I:12\n"
+"pin_clk:I:15\n"
+"pin_cs:I:13\n"
+"led_modules:I:4\n"
+"panel_font:I:1\n"
+"panel_speed:I:15\n"
+"panel_brightness_day:I:1\n"
+"panel_brightness_night:I:0\n"
+"panel_zero:B:true\n"
+"panel_seconds:B:false\n"
+"enable_button:B:true\n"
+"button_pin:I:0\n"
+"enable_buzzer:B:true\n"
+"buzzer_pin:I:14\n"
+"buzzer_passive:B:true\n"
+"day_from:I:8\n"
+"night_from:I:22\n"
+"enable_hourly:B:true\n"
+"hourly_night:B:false\n"
+"hourly_count:I:2\n"
+"hourly_tone:I:800\n"
+"hourly_beep_ms:I:100\n"
+"hourly_silent_ms:I:100\n"
+"enable_alarm:B:false\n"
+"alarm_hour:I:13\n"
+"alarm_minute:I:12\n"
+"alarm_days:S:1111111\n"
+"alarm_tone:I:1500\n"
+"alarm_length:I:60\n"
+"alarm_beep_ms:I:300\n"
+"alarm_silent_ms:I:200\n"
+"enable_weather:B:false\n"
+"weather_url:S:http://api.openweathermap.org/data/2.5/weather?lat=5{LATITUDE}&lon={LONGITUDE}&appid={API_KEY}&lang=ru&units=metric\n"
+"weather_template:S:Сегодня %weather.[0].description%, температура %main.temp% (%main.feels_like%)°C, влажность %main.humidity%%%, ветер %wind.speed% м/с\n"
+"weather_min:I:15\n"
+"auth_user:S:\n"
+"auth_pwd:S:\n";
+
+
+void reset() {
+ messageModal(F("Сбрасываю настройки"));
+ delay(2000);
+ if (File f = LittleFS.open(F("/config.txt"),"w")) {
+ f.print(distConfig);
+ f.close();
+ }
+ LittleFS.end();
+ ESP.restart();
+}
--- /dev/null
+sta_ssid:S:
+sta_psk:S:
+sta_wait:I:120
+ap_ssid:S:WiFi_Clock
+ap_psk:S:1234-5678
+ntp_server:S:ru.pool.ntp.org
+tz:S:MSK-3
+pin_sda:I:4
+pin_scl:I:5
+i2c_speed:I:400000
+enable_rtc:B:true
+flash_dots:B:true
+pin_din:I:12
+pin_clk:I:15
+pin_cs:I:13
+led_modules:I:4
+panel_font:I:1
+panel_speed:I:15
+panel_brightness_day:I:1
+panel_brightness_night:I:0
+panel_zero:B:true
+panel_seconds:B:false
+enable_button:B:true
+button_pin:I:0
+button_inversed:B:false
+enable_buzzer:B:true
+buzzer_pin:I:14
+buzzer_passive:B:true
+day_from:I:8
+night_from:I:22
+enable_hourly:B:true
+hourly_night:B:false
+hourly_count:I:2
+hourly_tone:I:800
+hourly_beep_ms:I:100
+hourly_silent_ms:I:100
+enable_alarm:B:true
+alarm_hour:I:13
+alarm_minute:I:12
+alarm_days:S:1111100
+alarm_tone:I:1500
+alarm_length:I:60
+alarm_beep_ms:I:300
+alarm_silent_ms:I:200
+enable_weather:B:true
+weather_url:S:http://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={ap-key}&lang=ru&units=metric
+weather_template:S:Сегодня %weather.[0].description%, температура %main.temp% (%main.feels_like%)°C, влажность %main.humidity%%%, ветер %wind.speed% м/с
+weather_min:I:15
+auth_user:S:
+auth_pwd:S:
--- /dev/null
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve">
+<g>
+ <path style="fill:#8BB7F0;" d="M20,38.5C9.799,38.5,1.5,30.201,1.5,20S9.799,1.5,20,1.5S38.5,9.799,38.5,20S30.201,38.5,20,38.5z"
+ />
+ <g>
+ <path style="fill:#4E7AB5;" d="M20,2c9.925,0,18,8.075,18,18s-8.075,18-18,18S2,29.925,2,20S10.075,2,20,2 M20,1
+ C9.507,1,1,9.507,1,20s8.507,19,19,19s19-8.507,19-19S30.493,1,20,1L20,1z"/>
+ </g>
+</g>
+<g>
+ <path style="fill:#FFFFFF;" d="M20,35.5c-8.547,0-15.5-6.953-15.5-15.5S11.453,4.5,20,4.5S35.5,11.453,35.5,20S28.547,35.5,20,35.5
+ z"/>
+ <path style="fill:#4E7AB5;" d="M20,5c8.271,0,15,6.729,15,15s-6.729,15-15,15S5,28.271,5,20S11.729,5,20,5 M20,4
+ C11.163,4,4,11.163,4,20s7.163,16,16,16s16-7.163,16-16S28.837,4,20,4L20,4z"/>
+</g>
+<polyline style="fill:none;stroke:#66798F;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;" points="26.995,9.665 20,20
+ 25.846,25.846 "/>
+<g>
+ <circle style="fill:#66798F;" cx="20" cy="20" r="1.5"/>
+</g>
+<g>
+ <circle style="fill:#C5D4DE;" cx="20" cy="7" r="1"/>
+</g>
+<g>
+ <circle style="fill:#C5D4DE;" cx="20" cy="33" r="1"/>
+</g>
+<g>
+ <circle style="fill:#C5D4DE;" cx="33" cy="20" r="1"/>
+</g>
+<g>
+ <circle style="fill:#C5D4DE;" cx="7" cy="20" r="1"/>
+</g>
+</svg>
--- /dev/null
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="">
+ <title></title>
+ <link rel="stylesheet" href="/style.css">
+ <link rel="shortcut icon" href="/icon.svg">
+</head>
+<body>
+
+<div id="layout">
+ <!-- Menu toggle -->
+ <a href="#menu" id="menuLink" class="menu-link">
+ <!-- Hamburger icon -->
+ <span id="menuBtn"></span>
+ </a>
+
+ <div id="menu">
+ <div class="pure-menu">
+ <a id="_ui_menu_header" class="pure-menu-heading" href="/"></a>
+ <ul id="_ui_menu_list" class="pure-menu-list">
+ </ul>
+ </div>
+ <footer id="_ui_contacts"></footer>
+ </div>
+
+ <div id="_ui_notification" class="notification" hidden></div>
+ <div class="message fadeout" id="_ui_message" hidden>
+ <div class="message-text" id="_ui_message_text"></div>
+ <div class="close-button" onclick="closeMsg()" title="Закрыть">Х</div>
+ </div>
+
+ <div id="main">
+ <div class="header">
+ <h1 id="_ui_page_header"></h1>
+ </div>
+ <div class="content">
+ <form class="pure-form pure-form-stacked">
+ <fieldset>
+ <div class="pure-g" id="_ui_page_content">
+ </div>
+ </fieldset>
+ </div>
+ </div>
+ </div>
+</div>
+
+<script src="/script.js"></script>
+
+</body>
+</html>
--- /dev/null
+{
+ "background_color":"#fff",
+ "display":"standalone",
+ "start_url":"/",
+ "name":"IoT Clock",
+ "short_name":"IoT Clock",
+ "icons":[
+ {"src":"/icon.svg","sizes":"192x192","type":"image/svg"}
+ ]
+}
--- /dev/null
+var pages
+var parameters = {}
+var daynames = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']
+var msgT
+
+function toggleMenu(e) {
+ active = (document.getElementById('menuLink').className.indexOf('active') !== -1)
+ if (active || e.target.id == 'menuLink' || e.target.id == 'menuBtn') {
+ elements = [ document.getElementById('layout'), document.getElementById('menu'), document.getElementById('menuLink') ]
+ for (const element of elements) {
+ if (!active) {
+ element.classList.add('active')
+ } else {
+ element.classList.remove('active')
+ }
+ }
+ if (e.target.id == 'menuLink' || e.target.id == 'menuBtn') {
+ e.preventDefault()
+ }
+ e.stopPropagation()
+ }
+}
+
+document.getElementById('layout').addEventListener('click', toggleMenu)
+
+function encode(r){
+ r = String(r)
+ return r.replace(/[\x26\x0A\<>'"]/g,function(r){return"&#"+r.charCodeAt(0)+";"})
+}
+
+function getAnchor() {
+ return window.location.hash.slice(1);
+}
+
+function drawHeader(project) {
+ var menu_header = document.getElementById('_ui_menu_header')
+ menu_header.innerHTML = project.name
+ document.title = project.name + '/' + project.version
+}
+
+function parseJsonQ(url, callback) {
+ var req = new XMLHttpRequest();
+
+ req.onreadystatechange = function () {
+ if (this.readyState != 4) return;
+ if (this.status != 200 && this.status != 500 && this.status != 404) {
+ setTimeout(parseJsonQ(url, callback),30000);
+ return;
+ }
+ var json = JSON.parse(this.responseText)
+ callback(json);
+ };
+
+ req.open("GET", url, true);
+ req.send()
+
+}
+
+function updateElement(id, value) {
+ var element = document.getElementById("_ui_element_"+id)
+ if (!element) return;
+ var ui_class = element.dataset.ui_class;
+ switch (ui_class) {
+ case "table":
+ element.innerText = value
+ break
+ case "input":
+ case "password":
+ case "select":
+ case "number":
+ case "range":
+ element.value = value
+ break
+ case "checkbox":
+ element.checked = value
+ break
+ case "week":
+ for (i=0; i<7; i++) {
+ var subname = '_ui_elpart_'+i+'_'+id
+ var subelement = document.getElementById(subname)
+ if (value[i]==1) {
+ subelement.className = "weekday-selected"
+ } else {
+ subelement.className = "weekday"
+ }
+ }
+ break
+ }
+}
+
+function updateValues(json) {
+ for (var key in json) {
+ var obj = document.getElementById("_ui_element_"+key)
+ if (obj) {
+ updateElement(key, json[key])
+ }
+ parameters[key] = json[key]
+ }
+ var notification = document.getElementById('_ui_notification');
+ if (parameters['_changed']) {
+ notification.innerHTML = '<input type="button" id="save" value="Сохранить" class="pure-button" onclick="sendAction(\'save\')">'
+ notification.removeAttribute('hidden')
+ } else {
+ notification.innerHTML = ''
+ notification.hidden = true
+ }
+}
+
+function sendUpdate(id) {
+ var input = document.getElementById('_ui_element_'+id)
+ var ui_class = input.dataset.ui_class;
+ switch (ui_class) {
+ case 'input':
+ case 'password':
+ case 'number':
+ case 'range':
+ if (input.checkValidity() && input.value != parameters[id]) {
+ parseJsonQ('/config/set?name=' + id + '&value=' + encodeURIComponent(input.value), function(json) {
+ updateValues(json)
+ })
+ }
+ break
+ case 'select':
+ parseJsonQ('/config/set?name=' + id + '&value=' + encodeURIComponent(input.selectedOptions[0].value), function(json) {
+ updateValues(json)
+ })
+ break;
+ case 'checkbox':
+ parseJsonQ('/config/set?name=' + id + '&value=' + (input.checked?'true':'false'), function(json) {
+ updateValues(json)
+ })
+ break;
+ case 'week':
+ parseJsonQ('/config/set?name=' + id + '&value=' + input.dataset.value, function(json) {
+ updateValues(json)
+ })
+ break;
+ }
+}
+
+function sendAction(name, params = {}) {
+ var url = '/action?name=' + name
+ for (var param in params) {
+ url += '&'+param+'='+encodeURIComponent(params[param])
+ }
+ parseJsonQ(url, function(json) {
+ if (json.result == 'FAILED') {
+ alert(json.message)
+ if (json.page) {
+ drawPage(json.page)
+ }
+ } else {
+ location.reload()
+ }
+ })
+}
+
+function showPwd(id) {
+ var x = document.getElementById('_ui_element_' + id)
+ if (x.type === "password") {
+ x.type = "text";
+ } else {
+ x.type = "password";
+ }
+}
+
+function openSelect(id) {
+ var selector = document.getElementById('_ui_elemmodal_'+id);
+ selector.removeAttribute("hidden")
+}
+
+function closeSelect(id) {
+ var selector = document.getElementById('_ui_elemmodal_'+id);
+ selector.hidden = true
+}
+
+function closeMsg() {
+ document.getElementById("_ui_message").hidden = true;
+}
+
+function fadeMsg() {
+ var msg = document.getElementById('_ui_message');
+ msg.classList.add("fadeout")
+ msgT = setTimeout(()=> { closeMsg() }, 5000);
+}
+
+function openMsg(msgText) {
+ document.getElementById("_ui_message_text").innerText = msgText;
+ document.getElementById("_ui_message").classList.remove("fadeout");
+ document.getElementById("_ui_message").removeAttribute('hidden');
+
+ if (msgT) {
+ window.clearTimeout(msgT);
+ }
+ msgT = setTimeout(()=> { fadeMsg(); }, 5000);
+}
+
+function selectWiFi(id, ssid) {
+ closeSelect(id);
+ var x = document.getElementById('_ui_element_' + id)
+ updateElement(x,ssid);
+ sendUpdate(id)
+}
+
+function getWiFi(id) {
+ var list = document.getElementById('_ui_elemselect_'+id)
+
+ var req = new XMLHttpRequest();
+
+ req.onreadystatechange = function () {
+ if (this.readyState != 4) return;
+ if (this.status != 200 && this.status != 500 && this.status != 404) {
+ setTimeout(getWiFi(id),30000);
+ return;
+ }
+ var json = JSON.parse(this.responseText)
+ var table = '<table cellpadding="5" border="0" align="center"><thead class="table-header"><tr><td>SSID</td><td>BSSID</td><td>RSSI</td><td>Канал</td><td>Защита</td></tr></thead><tbody>'
+ if (!json.length) {
+ setTimeout(getWiFi(id),5000);
+ }
+ for (idx in json) {
+ var encryption = json[idx].secure == 2? "TKIP" : json[idx].secure == 5? "WEP" : json[idx].secure == 4? "CCMP" : json[idx].secure == 7? "нет" : json[idx].secure == 8? "Автоматически" : "Не определено";
+ table += '<tr onclick="selectWiFi(\''+id+'\',\''+json[idx].ssid+'\')"><td>'+json[idx].ssid+'</td><td>'+json[idx].bssid+'</td><td>'+json[idx].rssi+'</td><td>'+json[idx].channel+'</td><td>'+encryption+'</td></tr>'
+ }
+
+ table += '</tbody></table>'
+ list.innerHTML = table;
+ };
+
+ req.open("GET", "/wifi/scan", true);
+ req.send()
+}
+
+function clickDay(id, day) {
+ value = parameters[id].split('')
+ day_value = value[day]
+ day_value = (day_value=='0')?'1':'0'
+ value[day] = day_value
+ element = document.getElementById('_ui_element_' + id)
+ value = value.join('')
+ element.dataset.value = value;
+ sendUpdate(id);
+}
+
+function sendTime(id) {
+ var value = document.getElementById('_ui_element_' + id).value
+ if (value) {
+ var date = new Date(value)
+ var timestamp = Math.floor(date.getTime()/1000);
+ sendAction('time',{"timestamp":timestamp})
+ }
+}
+
+function downloadFile(url, name) {
+ const a = document.createElement('a')
+ a.href = url
+ a.download = name?name:url.split('/').pop()
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+}
+
+function uploadConfig() {
+ var elem = document.getElementById('_config_file')
+ if (elem.value) {
+ var data = elem.files[0]
+ console.log(data)
+ fetch('/config/put', {method:'PUT',body:data});
+ elem.value = null
+ }
+}
+
+function elementHTML(element) {
+ var value
+ if (parameters[element.id] || !isNaN(parameters[element.id])) {
+ value = parameters[element.id]
+ } else if (element.value) {
+ value = element.value
+ } else {
+ value = ""
+ }
+ switch (element.type) {
+ case 'hr':
+ return '<div class="pure-u-1 pure-u-md-1-3"><hr></div>'
+ case 'button':
+ return '<div class="pure-u-1 pure-u-md-1-3"><div align="center"><input type="button" id="'
+ + element.id + '" value="' + encode(element.label) + '" class="pure-button" onclick="sendAction(\'' + element.id + '\')" /></div></div>'
+ case 'password':
+ return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
+ + '<div class="hinted"><input data-ui_class="password" type="password" id="_ui_element_' + element.id + '" value="' + encode(value)
+ + '" class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" />'
+ + '<z class="hint" onclick="showPwd(\'' + element.id + '\')"></z></div></div>'
+ case 'input':
+ var pattern = ""
+ if (element.pattern) {
+ pattern = ' pattern="' + encode(element.pattern) + '"'
+ }
+ return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
+ + '<input data-ui_class="input" type="text" id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
+ + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" /></div>'
+ case 'input-wifi':
+ var pattern = ""
+ if (element.pattern) {
+ pattern = ' pattern="' + encode(element.pattern) + '"'
+ }
+ return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
+ + '<div class="hinted"><input data-ui_class="input" type="text" id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
+ + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" />'
+ + '<z class="hint" onclick="openSelect(\'' + element.id+ '\'); getWiFi(\'' + element.id + '\')"></z></div>'
+ + '<div class="modal" id="_ui_elemmodal_' + element.id + '" hidden>'
+ + '<div class="modal-content">'
+ + '<div id="_ui_elemselect_' + element.id + '"></div>'
+ + '<div class="pure-u-1 pure-u-md-1-3"><div align="center"><input type="button" id="_ui_button_'
+ + element.id + '" value="Закрыть" class="pure-button" onclick="closeSelect(\'' + element.id + '\')"></div>'
+ + '</div></div></div></div>'
+ case 'checkbox':
+ return '<div class="pure-u-1 pure-u-md-1-3"><label class="switch socket" for="_ui_element_' + element.id + '">'
+ + '<input class="switch" data-ui_class="checkbox" type="checkbox" id="_ui_element_' + element.id + '"' + (parameters[element.id]?' checked':'') + ' onchange="sendUpdate(\'' + element.id + '\')" />'
+ + '<span class="switch slider">'+ encode(element.label) + '</span>'
+ + '</label></div>'
+ case 'select':
+ var options = '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
+ + '<select class="pure-u-24-24" data-ui_class="select" id="_ui_element_' + element.id + '" onchange="sendUpdate(\'' + element.id + '\')">'
+ for (const option of element.options) {
+ var list_option = '<option value="' + encode(option.value) + '" ';
+ if (option.value == parameters[element.id]) {
+ list_option += 'selected '
+ }
+ list_option += '>' + encode(option.text) + '</option>'
+ options += list_option
+ }
+ options += '</select></div>'
+ return options
+ case 'week':
+ days = '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
+ + '<table data-ui_class="week" data-value="' + value + '" id="_ui_element_' + element.id + '" cellpadding="5" border="0" class="week"><tbody><tr>'
+ for (i=0; i<7; i++) {
+ a_enabled = (value[i] == "1")
+ days = days + '<td><div class="weekday' + (a_enabled?"-selected":"") + '" id="_ui_elpart_'+i+'_'+element.id+'" onclick="clickDay(\'' + element.id + '\', ' + i + ')">'+ daynames[i] + '</div></td>'
+ }
+ days += '</tr></tbody></table></div>'
+ return days
+ case 'timeset':
+ var now = new Date()
+ now.setMinutes(now.getMinutes() - now.getTimezoneOffset())
+ value = now.toISOString().slice(0, -1);
+ return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
+ +'<input id="_ui_element_'+element.id+'" data-ui_class="timeset" class="inline-input" type="datetime-local" value="'+value+'">'
+ + '<div class="send-button" onclick="sendTime(\''+element.id+'\')">-></div></div>'
+ case 'text':
+ return '<div class="pure-u-1 pure-u-md-1-3"><h2 id="_ui_element_'+ element.id +'" ' + (element.color?'style="color:'+ element.color+'" ':'')+ '>' + encode(value) + '</h2></div>'
+ case 'number':
+ return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
+ + '<input data-ui_class="number" type="number" '+ (!isNaN(element.min)?'min="'+element.min+'" ':'') + (!isNaN(element.max)?'max="'+element.max+'" ':'') + (!isNaN(element.step)?'step="'+element.step+'" ':'')
+ + 'id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
+ + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" /></div>'
+ case 'range':
+ return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
+ + '<input data-ui_class="range" type="range" '+ (!isNaN(element.min)?'min="'+element.min+'" ':'') + (!isNaN(element.max)?'max="'+element.max+'" ':'') + (!isNaN(element.step)?'step="'+element.step+'" ':'')
+ + 'id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
+ + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" /></div>'
+ case 'config':
+ return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
+ + '<div align="center"><input type="button" class="pure-button row-button" value="Сохранить..." onclick="downloadFile(\'/config/get\',\'config.json\')">'
+ + '<label for="_config_file" class="pure-button row-button">Восстановить...</label>'
+ + '<input type="button" class="pure-button row-button" value="Настройки по умолчанию" onclick="confirm(\'Вы точно хотите сбросить настройки?\')?sendAction(\'reset\'):console.log(\'Не надо так не надо...\')">'
+ + '<input type="file" id="_config_file" onchange="uploadConfig()" style="visibility:hidden">'
+ + '</div></div>'
+ case 'table':
+ default:
+ return '<div class="pure-u-1 pure-u-md-1-3"><table class="texttable" cellpadding="5" border="0" align="center"><tbody><tr><td class="value-name" align="right">'
+ + encode(element.label)+ '</td><td id="_ui_element_'
+ + element.id + '" data-ui_class="table" ' + (element.color?'style="color:'+ element.color+'" ':'')+ '>' + encode(value) + '</td></tr></tbody></table></div>'
+ }
+}
+
+function drawPage(id) {
+ var idx =0, i=0
+ for (const page of pages) {
+ var menu_link = document.getElementById('_ui_pglink_' + page.id)
+ if (page.id != id) {
+ menu_link.classList.remove("pure-menu-selected")
+ } else {
+ menu_link.classList.add("pure-menu-selected")
+ idx = i
+ }
+ i++
+ }
+ var page_header = document.getElementById("_ui_page_header")
+ page_header.innerHTML = pages[idx].title
+ var page_content = document.getElementById("_ui_page_content");
+ var content = ''
+ for (const element of pages[idx].elements) {
+ content = content + elementHTML(element) + '\n'
+ }
+ page_content.innerHTML = content
+ window.location.hash = id
+}
+
+function drawNavigator(project, pages) {
+ var menu = document.getElementById('_ui_menu_list');
+ var list = ''
+ for (const page of pages) {
+ list = list + '<li id="_ui_pglink_' + page.id
+ + '" class="pure-menu-item"><a class="pure-menu-link" onclick="drawPage(\''+ page.id +'\')" href="#' + page.id + '">'
+ + (page.icon?'<span class="icon">'+page.icon+'</span>':'')
+ + page.title+'</a></li>'
+ }
+ menu.innerHTML = list
+}
+
+function drawContacts(contacts) {
+ if (!contacts) return;
+ var contact_list = '<hr><h4 class="pure-u1">Контакты</h4>'
+ for (contact of contacts) {
+ const url = new URL(contact)
+ var ref
+ switch (url.protocol) {
+ case 'http':
+ case 'https:':
+ ref = '<span class="icon"></span>'+url.hostname
+ break
+ case 'mailto:':
+ ref = '<span class="icon"></span>'+url.pathname
+ break
+ case 'tg:':
+ ref = '<span class="icon"></span>'+url.pathname
+ contact = 'tg://resolve?domain='+url.pathname
+ break
+ default:
+ ref = '<span class="icon"></span>'+url.pathname
+ }
+ contact_list += '<a href="'+contact+'">'+ref+'</a>'
+ }
+ var footer = document.getElementById('_ui_contacts');
+ footer.innerHTML = contact_list
+}
+
+function drawUI(ui) {
+ drawHeader(ui.project)
+ pages = ui.pages
+ drawNavigator(ui.project, pages)
+ drawContacts(ui.project.contacts)
+ var anchor = getAnchor()
+ if (anchor) {
+ drawPage(anchor)
+ } else {
+ drawPage(pages[0].id)
+ }
+}
+
+function GetUI() {
+ parseJsonQ("/ui", drawUI);
+}
+
+GetUI()
+
+function initES() {
+ if (!!window.EventSource) {
+ var source = new EventSource('/events');
+ openMsg('Соединение установлено')
+
+ source.onerror = function(e) {
+ if (source.readyState == 2) {
+ openMsg('Соединение прервано')
+ setTimeout(initES, 5000);
+ }
+ };
+
+ source.addEventListener('update', function(e) {
+ updateValues(JSON.parse(e.data));
+ }, false);
+ source.addEventListener('message', function(e) {
+ openMsg(e.data);
+ }, false);
+ }
+}
+
+initES();
+
+function drawConfig(cfg) {
+ updateValues(cfg)
+}
+
+function GetCfg() {
+ parseJsonQ("/config/get", drawConfig);
+}
+
+GetCfg()
--- /dev/null
+/*!
+Pure v3.0.0
+Copyright 2013 Yahoo!
+Licensed under the BSD License.
+https://github.com/pure-css/pure/blob/master/LICENSE
+*/
+/*!
+normalize.css v | MIT License | https://necolas.github.io/normalize.css/
+Copyright (c) Nicolas Gallagher and Jonathan Neal
+*/
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html {
+ line-height:1.15;
+ -webkit-text-size-adjust:100%
+}
+body {
+ margin:0
+}
+main {
+ display:block
+}
+h1 {
+ font-size:2em;
+ margin:.67em 0
+}
+hr {
+ box-sizing:content-box;
+ height:0;
+ overflow:visible
+}
+pre {
+ font-family:monospace,monospace;
+ font-size:1em
+}
+a {
+ background-color:transparent
+}
+abbr[title] {
+ border-bottom:none;
+ text-decoration:underline;
+ -webkit-text-decoration:underline dotted;
+ text-decoration:underline dotted
+}
+b,
+strong {
+ font-weight:bolder
+}
+code,
+kbd,
+samp {
+ font-family:monospace,monospace;
+ font-size:1em
+}
+small {
+ font-size:80%
+}
+sub,
+sup {
+ font-size:75%;
+ line-height:0;
+ position:relative;
+ vertical-align:baseline
+}
+sub {
+ bottom:-.25em
+}
+sup {
+ top:-.5em
+}
+img {
+ border-style:none
+}
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family:inherit;
+ font-size:100%;
+ line-height:1.15;
+ margin:0
+}
+button,
+input {
+ overflow:visible
+}
+button,
+select {
+ text-transform:none
+}
+[type=button],
+[type=reset],
+[type=submit],
+button {
+ -webkit-appearance:button
+}
+[type=button]::-moz-focus-inner,
+[type=reset]::-moz-focus-inner,
+[type=submit]::-moz-focus-inner,
+button::-moz-focus-inner {
+ border-style:none;
+ padding:0
+}
+[type=button]:-moz-focusring,
+[type=reset]:-moz-focusring,
+[type=submit]:-moz-focusring,
+button:-moz-focusring {
+ outline:1px dotted ButtonText
+}
+fieldset {
+ padding:.35em .75em .625em
+}
+legend {
+ box-sizing:border-box;
+ color:inherit;
+ display:table;
+ max-width:100%;
+ padding:0;
+ white-space:normal
+}
+progress {
+ vertical-align:baseline
+}
+textarea {
+ overflow:auto
+}
+[type=checkbox],
+[type=radio] {
+ box-sizing:border-box;
+ padding:0
+}
+[type=number]::-webkit-inner-spin-button,
+[type=number]::-webkit-outer-spin-button {
+ height:auto
+}
+[type=search] {
+ -webkit-appearance:textfield;
+ outline-offset:-2px
+}
+[type=search]::-webkit-search-decoration {
+ -webkit-appearance:none
+}
+::-webkit-file-upload-button {
+ -webkit-appearance:button;
+ font:inherit
+}
+details {
+ display:block
+}
+summary {
+ display:list-item
+}
+template {
+ display:none
+}
+[hidden] {
+ display:none
+}
+html {
+ font-family:sans-serif
+}
+.hidden,
+[hidden] {
+ display:none!important
+}
+.pure-img {
+ max-width:100%;
+ height:auto;
+ display:block
+}
+.pure-g {
+ display:flex;
+ flex-flow:row wrap;
+ align-content:flex-start
+}
+.pure-u {
+ display:inline-block;
+ vertical-align:top
+}
+.pure-u-1,
+.pure-u-1-1,
+.pure-u-1-12,
+.pure-u-1-2,
+.pure-u-1-24,
+.pure-u-1-3,
+.pure-u-1-4,
+.pure-u-1-5,
+.pure-u-1-6,
+.pure-u-1-8,
+.pure-u-10-24,
+.pure-u-11-12,
+.pure-u-11-24,
+.pure-u-12-24,
+.pure-u-13-24,
+.pure-u-14-24,
+.pure-u-15-24,
+.pure-u-16-24,
+.pure-u-17-24,
+.pure-u-18-24,
+.pure-u-19-24,
+.pure-u-2-24,
+.pure-u-2-3,
+.pure-u-2-5,
+.pure-u-20-24,
+.pure-u-21-24,
+.pure-u-22-24,
+.pure-u-23-24,
+.pure-u-24-24,
+.pure-u-3-24,
+.pure-u-3-4,
+.pure-u-3-5,
+.pure-u-3-8,
+.pure-u-4-24,
+.pure-u-4-5,
+.pure-u-5-12,
+.pure-u-5-24,
+.pure-u-5-5,
+.pure-u-5-6,
+.pure-u-5-8,
+.pure-u-6-24,
+.pure-u-7-12,
+.pure-u-7-24,
+.pure-u-7-8,
+.pure-u-8-24,
+.pure-u-9-24 {
+ display:inline-block;
+ letter-spacing:normal;
+ word-spacing:normal;
+ vertical-align:top;
+ text-rendering:auto
+}
+.pure-u-1-24 {
+ width:4.1667%
+}
+.pure-u-1-12,
+.pure-u-2-24 {
+ width:8.3333%
+}
+.pure-u-1-8,
+.pure-u-3-24 {
+ width:12.5%
+}
+.pure-u-1-6,
+.pure-u-4-24 {
+ width:16.6667%
+}
+.pure-u-1-5 {
+ width:20%
+}
+.pure-u-5-24 {
+ width:20.8333%
+}
+.pure-u-1-4,
+.pure-u-6-24 {
+ width:25%
+}
+.pure-u-7-24 {
+ width:29.1667%
+}
+.pure-u-1-3,
+.pure-u-8-24 {
+ width:33.3333%
+}
+.pure-u-3-8,
+.pure-u-9-24 {
+ width:37.5%
+}
+.pure-u-2-5 {
+ width:40%
+}
+.pure-u-10-24,
+.pure-u-5-12 {
+ width:41.6667%
+}
+.pure-u-11-24 {
+ width:45.8333%
+}
+.pure-u-1-2,
+.pure-u-12-24 {
+ width:50%
+}
+.pure-u-13-24 {
+ width:54.1667%
+}
+.pure-u-14-24,
+.pure-u-7-12 {
+ width:58.3333%
+}
+.pure-u-3-5 {
+ width:60%
+}
+.pure-u-15-24,
+.pure-u-5-8 {
+ width:62.5%
+}
+.pure-u-16-24,
+.pure-u-2-3 {
+ width:66.6667%
+}
+.pure-u-17-24 {
+ width:70.8333%
+}
+.pure-u-18-24,
+.pure-u-3-4 {
+ width:75%
+}
+.pure-u-19-24 {
+ width:79.1667%
+}
+.pure-u-4-5 {
+ width:80%
+}
+.pure-u-20-24,
+.pure-u-5-6 {
+ width:83.3333%
+}
+.pure-u-21-24,
+.pure-u-7-8 {
+ width:87.5%
+}
+.pure-u-11-12,
+.pure-u-22-24 {
+ width:91.6667%
+}
+.pure-u-23-24 {
+ width:95.8333%
+}
+.pure-u-1,
+.pure-u-1-1,
+.pure-u-24-24,
+.pure-u-5-5 {
+ width:100%
+}
+.pure-button {
+ display:inline-block;
+ line-height:normal;
+ white-space:nowrap;
+ vertical-align:middle;
+ text-align:center;
+ cursor:pointer;
+ -webkit-user-drag:none;
+ -webkit-user-select:none;
+ user-select:none;
+ box-sizing:border-box
+}
+.pure-button::-moz-focus-inner {
+ padding:0;
+ border:0
+}
+.pure-button-group {
+ letter-spacing:-.31em;
+ text-rendering:optimizespeed
+}
+.opera-only :-o-prefocus,
+.pure-button-group {
+ word-spacing:-0.43em
+}
+.pure-button-group .pure-button {
+ letter-spacing:normal;
+ word-spacing:normal;
+ vertical-align:top;
+ text-rendering:auto
+}
+.pure-button {
+ font-family:inherit;
+ font-size:100%;
+ padding:.5em 1em;
+ color:rgba(0,0,0,.8);
+ border:none transparent;
+ background-color:#e6e6e6;
+ text-decoration:none;
+ border-radius:2px
+}
+.pure-button-hover,
+.pure-button:focus,
+.pure-button:hover {
+ background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))
+}
+.pure-button:focus {
+ outline:0
+}
+.pure-button-active,
+.pure-button:active {
+ box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;
+ border-color:#000
+}
+.pure-button-disabled,
+.pure-button-disabled:active,
+.pure-button-disabled:focus,
+.pure-button-disabled:hover,
+.pure-button[disabled] {
+ border:none;
+ background-image:none;
+ opacity:.4;
+ cursor:not-allowed;
+ box-shadow:none;
+ pointer-events:none
+}
+.pure-button-hidden {
+ display:none
+}
+.pure-button-primary,
+.pure-button-selected,
+a.pure-button-primary,
+a.pure-button-selected {
+ background-color:#0078e7;
+ color:#fff
+}
+.pure-button-group .pure-button {
+ margin:0;
+ border-radius:0;
+ border-right:1px solid rgba(0,0,0,.2)
+}
+.pure-button-group .pure-button:first-child {
+ border-top-left-radius:2px;
+ border-bottom-left-radius:2px
+}
+.pure-button-group .pure-button:last-child {
+ border-top-right-radius:2px;
+ border-bottom-right-radius:2px;
+ border-right:none
+}
+.pure-form input[type=color],
+.pure-form input[type=date],
+.pure-form input[type=datetime-local],
+.pure-form input[type=datetime],
+.pure-form input[type=email],
+.pure-form input[type=month],
+.pure-form input[type=number],
+.pure-form input[type=password],
+.pure-form input[type=search],
+.pure-form input[type=tel],
+.pure-form input[type=text],
+.pure-form input[type=time],
+.pure-form input[type=url],
+.pure-form input[type=week],
+.pure-form select,
+.pure-form textarea {
+ padding:.5em .6em;
+ display:inline-block;
+ border:1px solid #ccc;
+ box-shadow:inset 0 1px 3px #ddd;
+ border-radius:4px;
+ vertical-align:middle;
+ box-sizing:border-box
+}
+.pure-form input:not([type]) {
+ padding:.5em .6em;
+ display:inline-block;
+ border:1px solid #ccc;
+ box-shadow:inset 0 1px 3px #ddd;
+ border-radius:4px;
+ box-sizing:border-box
+}
+.pure-form input[type=color] {
+ padding:.2em .5em
+}
+.pure-form input[type=color]:focus,
+.pure-form input[type=date]:focus,
+.pure-form input[type=datetime-local]:focus,
+.pure-form input[type=datetime]:focus,
+.pure-form input[type=email]:focus,
+.pure-form input[type=month]:focus,
+.pure-form input[type=number]:focus,
+.pure-form input[type=password]:focus,
+.pure-form input[type=search]:focus,
+.pure-form input[type=tel]:focus,
+.pure-form input[type=text]:focus,
+.pure-form input[type=time]:focus,
+.pure-form input[type=url]:focus,
+.pure-form input[type=week]:focus,
+.pure-form select:focus,
+.pure-form textarea:focus {
+ outline:0;
+ border-color:#129fea
+}
+.pure-form input:not([type]):focus {
+ outline:0;
+ border-color:#129fea
+}
+.pure-form input[type=checkbox]:focus,
+.pure-form input[type=file]:focus,
+.pure-form input[type=radio]:focus {
+ outline:thin solid #129FEA;
+ outline:1px auto #129FEA
+}
+.pure-form .pure-checkbox,
+.pure-form .pure-radio {
+ margin:.5em 0;
+ display:block
+}
+.pure-form input[type=color][disabled],
+.pure-form input[type=date][disabled],
+.pure-form input[type=datetime-local][disabled],
+.pure-form input[type=datetime][disabled],
+.pure-form input[type=email][disabled],
+.pure-form input[type=month][disabled],
+.pure-form input[type=number][disabled],
+.pure-form input[type=password][disabled],
+.pure-form input[type=search][disabled],
+.pure-form input[type=tel][disabled],
+.pure-form input[type=text][disabled],
+.pure-form input[type=time][disabled],
+.pure-form input[type=url][disabled],
+.pure-form input[type=week][disabled],
+.pure-form select[disabled],
+.pure-form textarea[disabled] {
+ cursor:not-allowed;
+ background-color:#eaeded;
+ color:#cad2d3
+}
+.pure-form input:not([type])[disabled] {
+ cursor:not-allowed;
+ background-color:#eaeded;
+ color:#cad2d3
+}
+.pure-form input[readonly],
+.pure-form select[readonly],
+.pure-form textarea[readonly] {
+ background-color:#eee;
+ color:#777;
+ border-color:#ccc
+}
+.pure-form input:focus:invalid,
+.pure-form select:focus:invalid,
+.pure-form textarea:focus:invalid {
+ color:#b94a48;
+ border-color:#e9322d
+}
+.pure-form input[type=checkbox]:focus:invalid:focus,
+.pure-form input[type=file]:focus:invalid:focus,
+.pure-form input[type=radio]:focus:invalid:focus {
+ outline-color:#e9322d
+}
+.pure-form select {
+ height:2.25em;
+ border:1px solid #ccc;
+ background-color:#fff
+}
+.pure-form select[multiple] {
+ height:auto
+}
+.pure-form label {
+ margin:.5em 0 .2em
+}
+.pure-form fieldset {
+ margin:0;
+ padding:.35em 0 .75em;
+ border:0
+}
+.pure-form legend {
+ display:block;
+ width:100%;
+ padding:.3em 0;
+ margin-bottom:.3em;
+ color:#333;
+ border-bottom:1px solid #e5e5e5
+}
+.pure-form-stacked input[type=color],
+.pure-form-stacked input[type=date],
+.pure-form-stacked input[type=datetime-local],
+.pure-form-stacked input[type=datetime],
+.pure-form-stacked input[type=email],
+.pure-form-stacked input[type=file],
+.pure-form-stacked input[type=month],
+.pure-form-stacked input[type=number],
+.pure-form-stacked input[type=password],
+.pure-form-stacked input[type=search],
+.pure-form-stacked input[type=tel],
+.pure-form-stacked input[type=text],
+.pure-form-stacked input[type=time],
+.pure-form-stacked input[type=url],
+.pure-form-stacked input[type=week],
+.pure-form-stacked label,
+.pure-form-stacked select,
+.pure-form-stacked textarea {
+ display:block;
+ margin:.25em 0
+}
+.pure-form-stacked input:not([type]) {
+ display:block;
+ margin:.25em 0
+}
+.pure-form-aligned input,
+.pure-form-aligned select,
+.pure-form-aligned textarea,
+.pure-form-message-inline {
+ display:inline-block;
+ vertical-align:middle
+}
+.pure-form-aligned textarea {
+ vertical-align:top
+}
+.pure-form-aligned .pure-control-group {
+ margin-bottom:.5em
+}
+.pure-form-aligned .pure-control-group label {
+ text-align:right;
+ display:inline-block;
+ vertical-align:middle;
+ width:10em;
+ margin:0 1em 0 0
+}
+.pure-form-aligned .pure-controls {
+ margin:1.5em 0 0 11em
+}
+.pure-form .pure-input-rounded,
+.pure-form input.pure-input-rounded {
+ border-radius:2em;
+ padding:.5em 1em
+}
+.pure-form .pure-group fieldset {
+ margin-bottom:10px
+}
+.pure-form .pure-group input,
+.pure-form .pure-group textarea {
+ display:block;
+ padding:10px;
+ margin:0 0 -1px;
+ border-radius:0;
+ position:relative;
+ top:-1px
+}
+.pure-form .pure-group input:focus,
+.pure-form .pure-group textarea:focus {
+ z-index:3
+}
+.pure-form .pure-group input:first-child,
+.pure-form .pure-group textarea:first-child {
+ top:1px;
+ border-radius:4px 4px 0 0;
+ margin:0
+}
+.pure-form .pure-group input:first-child:last-child,
+.pure-form .pure-group textarea:first-child:last-child {
+ top:1px;
+ border-radius:4px;
+ margin:0
+}
+.pure-form .pure-group input:last-child,
+.pure-form .pure-group textarea:last-child {
+ top:-2px;
+ border-radius:0 0 4px 4px;
+ margin:0
+}
+.pure-form .pure-group button {
+ margin:.35em 0
+}
+.pure-form .pure-input-1 {
+ width:100%
+}
+.pure-form .pure-input-3-4 {
+ width:75%
+}
+.pure-form .pure-input-2-3 {
+ width:66%
+}
+.pure-form .pure-input-1-2 {
+ width:50%
+}
+.pure-form .pure-input-1-3 {
+ width:33%
+}
+.pure-form .pure-input-1-4 {
+ width:25%
+}
+.pure-form-message-inline {
+ display:inline-block;
+ padding-left:.3em;
+ color:#666;
+ vertical-align:middle;
+ font-size:.875em
+}
+.pure-form-message {
+ display:block;
+ color:#666;
+ font-size:.875em
+}
+@media only screen and (max-width :480px) {
+ .pure-form button[type=submit] {
+ margin:.7em 0 0
+ }
+ .pure-form input:not([type]),
+ .pure-form input[type=color],
+ .pure-form input[type=date],
+ .pure-form input[type=datetime-local],
+ .pure-form input[type=datetime],
+ .pure-form input[type=email],
+ .pure-form input[type=month],
+ .pure-form input[type=number],
+ .pure-form input[type=password],
+ .pure-form input[type=search],
+ .pure-form input[type=tel],
+ .pure-form input[type=text],
+ .pure-form input[type=time],
+ .pure-form input[type=url],
+ .pure-form input[type=week],
+ .pure-form label {
+ margin-bottom:.3em;
+ display:block
+ }
+ .pure-group input:not([type]),
+ .pure-group input[type=color],
+ .pure-group input[type=date],
+ .pure-group input[type=datetime-local],
+ .pure-group input[type=datetime],
+ .pure-group input[type=email],
+ .pure-group input[type=month],
+ .pure-group input[type=number],
+ .pure-group input[type=password],
+ .pure-group input[type=search],
+ .pure-group input[type=tel],
+ .pure-group input[type=text],
+ .pure-group input[type=time],
+ .pure-group input[type=url],
+ .pure-group input[type=week] {
+ margin-bottom:0
+ }
+ .pure-form-aligned .pure-control-group label {
+ margin-bottom:.3em;
+ text-align:left;
+ display:block;
+ width:100%
+ }
+ .pure-form-aligned .pure-controls {
+ margin:1.5em 0 0 0
+ }
+ .pure-form-message,
+ .pure-form-message-inline {
+ display:block;
+ font-size:.75em;
+ padding:.2em 0 .8em
+ }
+}
+.pure-menu {
+ box-sizing:border-box
+}
+.pure-menu-fixed {
+ position:fixed;
+ left:0;
+ top:0;
+ z-index:3
+}
+.pure-menu-item,
+.pure-menu-list {
+ position:relative
+}
+.pure-menu-list {
+ list-style:none;
+ margin:0;
+ padding:0
+}
+.pure-menu-item {
+ padding:0;
+ margin:0;
+ height:100%
+}
+.pure-menu-heading,
+.pure-menu-link {
+ display:block;
+ text-decoration:none;
+ white-space:nowrap
+}
+.pure-menu-horizontal {
+ width:100%;
+ white-space:nowrap
+}
+.pure-menu-horizontal .pure-menu-list {
+ display:inline-block
+}
+.pure-menu-horizontal .pure-menu-heading,
+.pure-menu-horizontal .pure-menu-item,
+.pure-menu-horizontal .pure-menu-separator {
+ display:inline-block;
+ vertical-align:middle
+}
+.pure-menu-item .pure-menu-item {
+ display:block
+}
+.pure-menu-children {
+ display:none;
+ position:absolute;
+ left:100%;
+ top:0;
+ margin:0;
+ padding:0;
+ z-index:3
+}
+.pure-menu-horizontal .pure-menu-children {
+ left:0;
+ top:auto;
+ width:inherit
+}
+.pure-menu-active>.pure-menu-children,
+.pure-menu-allow-hover:hover>.pure-menu-children {
+ display:block;
+ position:absolute
+}
+.pure-menu-has-children>.pure-menu-link:after {
+ padding-left:.5em;
+ content:"\25B8";
+ font-size:small
+}
+.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after {
+ content:"\25BE"
+}
+.pure-menu-scrollable {
+ overflow-y:scroll;
+ overflow-x:hidden
+}
+.pure-menu-scrollable .pure-menu-list {
+ display:block
+}
+.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list {
+ display:inline-block
+}
+.pure-menu-horizontal.pure-menu-scrollable {
+ white-space:nowrap;
+ overflow-y:hidden;
+ overflow-x:auto;
+ padding:.5em 0
+}
+.pure-menu-horizontal .pure-menu-children .pure-menu-separator,
+.pure-menu-separator {
+ background-color:#ccc;
+ height:1px;
+ margin:.3em 0
+}
+.pure-menu-horizontal .pure-menu-separator {
+ width:1px;
+ height:1.3em;
+ margin:0 .3em
+}
+.pure-menu-horizontal .pure-menu-children .pure-menu-separator {
+ display:block;
+ width:auto
+}
+.pure-menu-heading {
+ text-transform:uppercase;
+ color:#565d64
+}
+.pure-menu-link {
+ color:#777
+}
+.pure-menu-children {
+ background-color:#fff
+}
+.pure-menu-heading,
+.pure-menu-link {
+ padding:.5em 1em
+}
+.pure-menu-disabled {
+ opacity:.5
+}
+.pure-menu-disabled .pure-menu-link:hover {
+ background-color:transparent;
+ cursor:default
+}
+.pure-menu-active>.pure-menu-link,
+.pure-menu-link:focus,
+.pure-menu-link:hover {
+ background-color:#eee
+}
+.pure-menu-selected>.pure-menu-link,
+.pure-menu-selected>.pure-menu-link:visited {
+ color:#000
+}
+.pure-table {
+ border-collapse:collapse;
+ border-spacing:0;
+ empty-cells:show;
+ border:1px solid #cbcbcb
+}
+.pure-table caption {
+ color:#000;
+ font:italic 85%/1 arial,sans-serif;
+ padding:1em 0;
+ text-align:center
+}
+.pure-table td,
+.pure-table th {
+ border-left:1px solid #cbcbcb;
+ border-width:0 0 0 1px;
+ font-size:inherit;
+ margin:0;
+ overflow:visible;
+ padding:.5em 1em
+}
+.pure-table thead {
+ background-color:#e0e0e0;
+ color:#000;
+ text-align:left;
+ vertical-align:bottom
+}
+.pure-table td {
+ background-color:transparent
+}
+.pure-table-odd td {
+ background-color:#f2f2f2
+}
+.pure-table-striped tr:nth-child(2n-1) td {
+ background-color:#f2f2f2
+}
+.pure-table-bordered td {
+ border-bottom:1px solid #cbcbcb
+}
+.pure-table-bordered tbody>tr:last-child>td {
+ border-bottom-width:0
+}
+.pure-table-horizontal td,
+.pure-table-horizontal th {
+ border-width:0 0 1px 0;
+ border-bottom:1px solid #cbcbcb
+}
+.pure-table-horizontal tbody>tr:last-child>td {
+ border-bottom-width:0
+}
+
+/* custom styles */
+
+:root {
+ --slide: 25em;
+ --blue: #129fea;
+ --darkgray: #242424;
+ --lightgray: #cad2d3;
+ --darkergray: #191919;
+}
+
+body {
+ color:#aaa;
+ background: var(--darkgray);
+}
+
+.pure-img-responsive {
+ max-width:100%;
+ height:auto
+}
+
+#layout,
+#menu,
+.menu-link {
+ -webkit-transition:all 0.2s ease-out;
+ -moz-transition:all 0.2s ease-out;
+ -ms-transition:all 0.2s ease-out;
+ -o-transition:all 0.2s ease-out;
+ transition:all 0.2s ease-out
+}
+#layout {
+ position:relative;
+ left:0;
+ padding-left:0
+}
+#layout.active #menu {
+ left:var(--slide);
+ width:var(--slide)
+}
+#layout.active .menu-link {
+ left:var(--slide)
+}
+.content {
+ margin:0 auto;
+ padding:0 2em;
+ max-width: 100em;
+ margin-bottom: 5em;
+ line-height:1.6em
+}
+.header {
+ margin:0;
+ color: var(--darkgray);
+ text-align:center
+}
+.header h1 {
+ color: var(--blue);
+ margin:.4em 0;
+ padding-top: 0.5em;
+ font-size: 2em;
+ font-weight: bold;
+ font-variant-caps: small-caps
+}
+.header h2 {
+ font-weight:300;
+ color:#ccc;
+ padding:0;
+ margin-top:0
+}
+.content-subhead {
+ margin: 5em 0 2em 0;
+ font-weight:300;
+ color: var(--lightgray)
+}
+#menu {
+ margin-left:calc(var(--slide)* -1);
+ width:var(--slide);
+ height: 100vh;
+ position:fixed;
+ top:0;
+ left:0;
+ bottom:0;
+ z-index:1000;
+ background: var(--darkergray);
+ overflow-y:auto;
+ -webkit-overflow-scrolling:touch
+}
+#menu a {
+ color: var(--lightgray);
+ border:none;
+ padding:.6em 0.3em .6em 0.6em
+}
+#menu .pure-menu,
+#menu .pure-menu ul {
+ border:none;
+ background:transparent
+}
+#menu .pure-menu ul,
+#menu .pure-menu .menu-item-divided {
+ border-top: 0.1em solid var(--darkgray)
+}
+#menu .pure-menu li a:hover,
+#menu .pure-menu li a:focus {
+ background: var(--darkgray);
+ color: var(--lightgray);
+}
+#menu .pure-menu-selected,
+#menu .pure-menu-heading {
+ background: var(--blue);
+}
+#menu .pure-menu-selected a {
+ color: white
+}
+#menu .pure-menu-heading {
+ letter-spacing:.15em;
+ text-transform:uppercase;
+ color: white;
+ margin:0;
+ text-align:center;
+ padding:.6em 0.3em .6em 0.3em
+}
+.menu-link {
+ position:fixed;
+ display:block;
+ top:0;
+ left:0;
+ background: black;
+ background:rgba(0,0,0,0.42);
+ font-size: 1em;
+ z-index:10;
+ width:2em;
+ height:auto;
+ padding:2.1em 1.6em
+}
+.menu-link:hover,
+.menu-link:focus {
+ background: black;
+}
+.menu-link span {
+ position:relative;
+ display:block
+}
+.menu-link span,
+.menu-link span:before,
+.menu-link span:after {
+ background-color: var(--blue);
+ width:100%;
+ height:.2em
+}
+.menu-link span:before,
+.menu-link span:after {
+ position:absolute;
+ margin-top:-.6em;
+ content:" "
+}
+.menu-link span:after {
+ margin-top:.6em
+}
+@media(min-width:800px) {
+ .header,
+ .content {
+ padding-left:2em;
+ padding-right:2em
+ }
+ #layout {
+ padding-left:var(--slide);
+ left:0
+ }
+ #menu {
+ left:var(--slide)
+ }
+ .menu-link {
+ position:fixed;
+ left:var(--slide);
+ display:none
+ }
+ #layout.active .menu-link {
+ left:var(--slide)
+ }
+}
+
+/* Sliders */
+
+input[type=range] {
+ -webkit-appearance:none;
+ margin:0 0 0 0;
+ width:100%;
+ background: transparent;
+}
+
+input[type=range]:focus {
+ outline:none
+}
+
+input[type=range]::-webkit-slider-runnable-track {
+ width:100%;
+ height: 0.4em;
+ cursor:pointer;
+ animate:0.2s;
+ box-shadow: none
+ background: var(--blue);
+ border-radius: 0.3em;
+ border: none
+}
+
+input[type=range]::-webkit-slider-thumb {
+ box-shadow: none;
+ border: 0.1em solid var(--lightgray)
+ height: 2em;
+ width: 2em;
+ border-radius: 2em;
+ background: var(--lightgray)
+ cursor:pointer;
+ -webkit-appearance:none;
+ margin-top: 0.85em
+}
+
+input[type=range]:focus::-webkit-slider-runnable-track {
+ background: var(--blue)
+}
+
+input[type=range]::-moz-range-track {
+ width: 100%;
+ height: 0.4em;
+ cursor: pointer;
+ animate:0.2s;
+ box-shadow: none;
+ background: var(--blue);
+ border-radius: 0.3em;
+ border: none
+}
+
+input[type=range]::-moz-range-thumb {
+ box-shadow: none;
+ border: 0.1em solid var(--blue);
+ height: 2em;
+ width: 2em;
+ border-radius: 2em;
+ background: var(--lightgray);
+ cursor:pointer
+}
+
+input[type=range]::-ms-track {
+ width:100%;
+ height: 0.4em;
+ cursor:pointer;
+ animate:0.2s;
+ background:transparent;
+ border-color:transparent;
+ color:transparent
+}
+
+input[type=range]::-ms-fill-lower {
+ background: var(--lightgray);
+ border: none;
+ border-radius: 0.6em;
+ box-shadow: none
+}
+
+input[type=range]::-ms-fill-upper {
+ background: var(--lightgray);
+ border: none;
+ border-radius: 0.6em;
+ box-shadow: none
+}
+
+input[type=range]::-ms-thumb {
+ margin-top:0.1em;
+ box-shadow: nonel
+ border: 0.1em solid var(--blue);
+ height: 2em;
+ width: 2em;
+ border-radius: 2em;
+ background: var(--lightgray);
+ cursor:pointer
+}
+
+input[type=range]:focus::-ms-fill-lower {
+ background: var(--lightgray);
+}
+
+input[type=range]:focus::-ms-fill-upper {
+ background: var(--lightgray);
+}
+
+
+.hide {
+ display:none
+}
+
+/* footer */
+
+footer {
+ text-align:center;
+ position: absolute;
+ bottom: 1rem;
+ width: 100%
+}
+footer a {
+ display: inline-block;
+ text-decoration: none
+}
+
+/* notification button */
+
+.notification {
+ position: fixed;
+ right: 0.5em;
+ top: 0.5em;
+ background-color: var(--darkgray);
+ z-index:200;
+ word-wrap: anywhere;
+ float: right;
+ }
+
+/* notification message */
+
+.message {
+ position: fixed;
+ right: 1em;
+ bottom: 0.5em;
+ z-index:200;
+ float: right;
+ color:#fff;
+}
+
+.message-text {
+ background-color: var(--darkgray);
+ border:0.2em solid var(--blue);
+ box-shadow:inset 0 0.1em 1.1em 0 var(--blue);
+ word-wrap: anywhere;
+ font-weight: bolder;
+ font-size: 120%;
+ padding: 1em;
+ border-radius: 0.5em;
+ position: relative;
+}
+
+.close-button {
+ content: 'X';
+ position: absolute;
+ right: -1em;
+ top: -1em;
+ width: 1.2em;
+ padding: 0.1em 0.1em 0.1em 0.1em;
+ text-decoration: none;
+ text-shadow: none;
+ text-align: center;
+ font-weight: bold;
+ background: var(--darkgray);
+ color: white;
+ border: 0.2em solid var(--blue);
+ border-radius: 1em;
+ box-shadow:inset 0 0.1em 1.1em 0 var(--blue);
+}
+
+.fadeout {
+ animation: fadeOut 5s ease;
+ opacity:0;
+}
+
+@keyframes fadeOut {
+ 0% {opacity:1;}
+ 100% {opacity:0;}
+}
+
+/* modal dialog */
+
+.modal {
+ position: fixed; /* Stay in place */
+ z-index: 2000; /* Sit on top */
+ left: 0;
+ top: 0;
+ width: 100%; /* Full width */
+ height: 100%; /* Full height */
+ overflow: auto; /* Enable scroll if needed */
+ background-color: rgba(0,0,0,0.3); /* Black w/ opacity */
+}
+.modal-content {
+ -webkit-appearance:button;
+ cursor:pointer;
+ letter-spacing:.05em;
+ text-transform:uppercase;
+ color:var(--lightgray);
+ font-size:90%;
+ border:0.2em solid var(--blue);
+ border-radius:6px;
+ box-shadow:inset 0 0.1em 1.1em 0 var(--blue);
+ padding: 1em;
+ width: max-content;
+ min-width: 30%;
+ background-color: var(--darkgray);
+ margin: 10% auto;
+ padding: 1rem
+}
+.table-header {
+ font-weight: bold;
+ font-size: 120%;
+ text-align: center;
+ border-bottom: var(--blue) solid 0.1em;
+}
+
+/* overlapping hint button */
+
+.hinted {
+ position: relative;
+}
+
+.hint {
+ cursor: pointer;
+ width: 1rem;
+ background: transparent;
+ color: var(--darkgray);
+ padding: .5em .6em;
+ border: none;
+ box-shadow: none;
+ position: absolute;
+ top: 0em;
+ right: 0em;
+ line-height: 1.15em;
+ font-size: 100%;
+}
+
+/* Spacing */
+fieldset .pure-u-1 {
+ margin-top: 1.5em
+}
+
+/* checkbox as a switch */
+
+input[type=checkbox].switch {
+ display: none;
+}
+label.switch.socket {
+ position: relative;
+ width: auto;
+ padding-left: 4em;
+}
+span.switch.slider {
+ display: inline-block;
+}
+label.switch.socket span.switch.slider:before {
+ content: "";
+ color: gray;
+ width: 3em;
+ height: 1.4em;
+ border-radius: 1em;
+ position: absolute;
+ left: 0;
+ top: 0;
+ border:0.2em solid var(--blue);
+ box-shadow:inset 0 0.1em 1.1em 0 var(--blue)
+}
+label.switch span.switch.slider:after {
+ content: "";
+ width: 1rem;
+ height: 1rem;
+ background: var(--blue);
+ border-radius: 1em;
+ position: absolute;
+ left: 0.4rem;
+ top: 0.4rem;
+ transition: 0.2s;
+}
+label.switch.socket input[type=checkbox]:checked + span.slider:before {
+ background: var(--lightgray);
+}
+
+label.switch.socket input[type=checkbox]:checked + span.slider:after {
+ background: white;
+ left: 1.9em;
+}
+
+/* Time setter */
+
+.inline-input.inline-input.inline-input {
+ display: inline-block;
+ width: calc(100% - 5em)
+}
+
+.send-button {
+ display:inline-block;
+ font-family:inherit;
+ font-size:100%;
+ line-height: 1.15em;
+ padding: .5em 1em;
+ color:rgba(0,0,0,.8);
+ border:none transparent;
+ background-color:#e6e6e6;
+ text-decoration:none;
+ border-radius:2px;
+ vertical-align: middle;
+ margin-left: 1em;
+ width: 2em;
+ text-align: center;
+}
+
+/* weekdays */
+
+.week {
+ width: 100%;
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+.week td {
+ padding: 0
+}
+.weekday, .weekday-selected {
+ font-size: min(4vw, 100%rem);
+ text-align: center;
+ margin: 0.2vw;
+ padding: 0.2em;
+ border-radius: 5em;
+ border:0.2em solid var(--blue);
+ box-shadow:inset 0 0.1em 1.1em 0 var(--blue);
+ color: gray
+}
+.weekday-selected {
+ background: var(--lightgray);
+ color: black
+}
+
+/* Hide arrows */
+
+input[type=number] {
+ appearance: textfield;
+}
+
+/* fill all screen */
+
+#layout {
+ min-height: 100vh;
+}
+
+/* Text table */
+
+.texttable {
+ border-collapse: collapse;
+ border-spacing: 0;
+ width: 100%;
+}
+
+.texttable td {
+ padding: 0
+}
+
+.value-name.value-name {
+ width: 50%;
+ padding-right: 1em;
+}
+
+/* Config */
+
+input[type=file]::file-selector-button {
+ display: none;
+}
+
+input[type=file]::-webkit-file-upload-button {
+ display: block;
+ width: 0;
+ height: 0;
+ margin-left: -100%;
+}
+
+input[type=file]::-ms-browse {
+ display: none;
+}
+
+.row-button.row-button.row-button {
+ display: inline-block;
+ margin: 0 2em 0 2em
+}
+
+/* fonts */
+
+@font-face {
+ font-family: "icons";
+ font-style: normal;
+ font-weight: 400;
+ src: url("/icons.woff2") format("woff2");
+}
+.hint, .icon {
+ font-family: icons;
+}
+.icon {
+ margin-right: 0.5em;
+}
--- /dev/null
+description = "ESP8266 alarm clock"
+owner = "rvb@rvb.name"
--- /dev/null
+#include "Clock.h"
+#include <LittleFS.h>
+
+void setup() {
+ // put your setup code here, to run once:
+
+ Serial.begin(115200);
+ Serial.println();
+ Serial.println(F("Starting..."));
+
+ setupConfig();
+ Serial.println(cfg);
+
+ setupHandlers();
+
+ setupHardware();
+ setupPanel();
+
+ setupNet();
+ setupTime();
+
+ setupAlarm();
+ setupWeatherRequest();
+
+ setupWeb();
+}
+
+void mem() {
+ Serial.println(F("-------------------------------------------------------------"));
+ Serial.print("Heap:"); Serial.print(ESP.getFreeHeap());
+ Serial.print(" Largest chunk:"); Serial.print(ESP.getMaxFreeBlockSize());
+ Serial.print(" Fragmentation:"); Serial.print(ESP.getHeapFragmentation());
+ Serial.print(" Stack:"); Serial.println(ESP.getFreeContStack());
+ Serial.println(F("-------------------------------------------------------------"));
+}
+
+void loop() {
+ static unsigned long lastMillis = 0;
+ int interval = 5000;
+ // put your main code here, to run repeatedly:
+ if (millis() - lastMillis > interval) {
+ lastMillis = millis();
+ mem();
+ }
+ tickNet();
+ tickTime();
+ tickHardware();
+ tickPanel();
+ tickWeb();
+}
--- /dev/null
+#pragma once
+
+MD_MAX72XX::fontType_t NumbersSinclair[] PROGMEM =
+{
+ 0, // 0
+ 0, // 1
+ 0, // 2
+ 0, // 3
+ 0, // 4
+ 0, // 5
+ 0, // 6
+ 0, // 7
+ 0, // 8
+ 0, // 9
+ 0, // 10
+ 0, // 11
+ 0, // 12
+ 0, // 13
+ 0, // 14
+ 0, // 15
+ 0, // 16
+ 0, // 17
+ 0, // 18
+ 0, // 19
+ 0, // 20
+ 0, // 21
+ 0, // 22
+ 0, // 23
+ 0, // 24
+ 0, // 25
+ 0, // 26
+ 0, // 27
+ 0, // 28
+ 0, // 29
+ 0, // 30
+ 0, // 31
+ 2, 0, 0, // 32
+ 0, // 33
+ 0, // 34
+ 0, // 35
+ 0, // 36
+ 0, // 37
+ 0, // 38
+ 0, // 39
+ 0, // 40
+ 0, // 41
+ 0, // 42
+ 0, // 43
+ 0, // 44
+ 3, 24, 24, 24, // 45
+ 2, 192, 192, // 46
+ 0, // 47
+ 7, 255, 255, 145, 137, 255, 255, 255, // 48
+ 7, 0, 130, 255, 255, 255, 128, 0, // 49
+ 7, 243, 243, 145, 145, 159, 223, 223, // 50
+ 7, 129, 129, 145, 145, 255, 255, 255, // 51
+ 7, 31, 31, 16, 16, 255, 255, 255, // 52
+ 7, 223, 223, 145, 145, 241, 243, 243, // 53
+ 7, 255, 255, 137, 137, 249, 249, 249, // 54
+ 7, 1, 1, 1, 1, 255, 255, 255, // 55
+ 7, 247, 255, 137, 137, 255, 255, 247, // 56
+ 7, 223, 223, 145, 145, 255, 255, 255, // 57
+ 2, 102, 102, // 58
+ 0, // 59
+ 0, // 60
+ 0, // 61
+ 0, // 62
+ 0, // 63
+ 0, // 64
+ 0, // 65
+ 0, // 66
+ 0, // 67
+ 0, // 68
+ 0, // 69
+ 0, // 70
+ 0, // 71
+ 0, // 72
+ 0, // 73
+ 0, // 74
+ 0, // 75
+ 0, // 76
+ 0, // 77
+ 0, // 78
+ 0, // 79
+ 0, // 80
+ 0, // 81
+ 0, // 82
+ 0, // 83
+ 0, // 84
+ 0, // 85
+ 0, // 86
+ 0, // 87
+ 0, // 88
+ 0, // 89
+ 0, // 90
+ 0, // 91
+ 0, // 92
+ 0, // 93
+ 0, // 94
+ 0, // 95
+ 0, // 96
+ 0, // 97
+ 0, // 98
+ 0, // 99
+ 0, // 100
+ 0, // 101
+ 0, // 102
+ 0, // 103
+ 0, // 104
+ 0, // 105
+ 0, // 106
+ 0, // 107
+ 0, // 108
+ 0, // 109
+ 0, // 110
+ 0, // 111
+ 0, // 112
+ 0, // 113
+ 0, // 114
+ 0, // 115
+ 0, // 116
+ 0, // 117
+ 0, // 118
+ 0, // 119
+ 0, // 120
+ 0, // 121
+ 0, // 122
+ 0, // 123
+ 0, // 124
+ 0, // 125
+ 0, // 126
+ 0, // 127
+ 0, // 128
+ 0, // 129
+ 0, // 130
+ 0, // 131
+ 0, // 132
+ 0, // 133
+ 0, // 134
+ 0, // 135
+ 0, // 136
+ 0, // 137
+ 0, // 138
+ 0, // 139
+ 0, // 140
+ 0, // 141
+ 0, // 142
+ 0, // 143
+ 0, // 144
+ 0, // 145
+ 0, // 146
+ 0, // 147
+ 0, // 148
+ 0, // 149
+ 0, // 150
+ 0, // 151
+ 0, // 152
+ 0, // 153
+ 0, // 154
+ 0, // 155
+ 0, // 156
+ 0, // 157
+ 0, // 158
+ 0, // 159
+ 0, // 160
+ 0, // 161
+ 0, // 162
+ 0, // 163
+ 0, // 164
+ 0, // 165
+ 0, // 166
+ 0, // 167
+ 0, // 168
+ 0, // 169
+ 0, // 170
+ 0, // 171
+ 0, // 172
+ 0, // 173
+ 0, // 174
+ 0, // 175
+ 0, // 176
+ 0, // 177
+ 0, // 178
+ 0, // 179
+ 0, // 180
+ 0, // 181
+ 0, // 182
+ 0, // 183
+ 0, // 184
+ 0, // 185
+ 0, // 186
+ 0, // 187
+ 0, // 188
+ 0, // 189
+ 0, // 190
+ 0, // 191
+ 0, // 192
+ 0, // 193
+ 0, // 194
+ 0, // 195
+ 0, // 196
+ 0, // 197
+ 0, // 198
+ 0, // 199
+ 0, // 200
+ 0, // 201
+ 0, // 202
+ 0, // 203
+ 0, // 204
+ 0, // 205
+ 0, // 206
+ 0, // 207
+ 0, // 208
+ 0, // 209
+ 0, // 210
+ 0, // 211
+ 0, // 212
+ 0, // 213
+ 0, // 214
+ 0, // 215
+ 0, // 216
+ 0, // 217
+ 0, // 218
+ 0, // 219
+ 0, // 220
+ 0, // 221
+ 0, // 222
+ 0, // 223
+ 0, // 224
+ 0, // 225
+ 0, // 226
+ 0, // 227
+ 0, // 228
+ 0, // 229
+ 0, // 230
+ 0, // 231
+ 0, // 232
+ 0, // 233
+ 0, // 234
+ 0, // 235
+ 0, // 236
+ 0, // 237
+ 0, // 238
+ 0, // 239
+ 0, // 240
+ 0, // 241
+ 0, // 242
+ 0, // 243
+ 0, // 244
+ 0, // 245
+ 0, // 246
+ 0, // 247
+ 0, // 248
+ 0, // 249
+ 0, // 250
+ 0, // 251
+ 0, // 252
+ 0, // 253
+ 0, // 254
+ 0, // 255
+};
+
+MD_MAX72XX::fontType_t NumbersBoldStraight[] PROGMEM =
+{
+ 0, // 0
+ 0, // 1
+ 0, // 2
+ 0, // 3
+ 0, // 4
+ 0, // 5
+ 0, // 6
+ 0, // 7
+ 0, // 8
+ 0, // 9
+ 0, // 10
+ 0, // 11
+ 0, // 12
+ 0, // 13
+ 0, // 14
+ 0, // 15
+ 0, // 16
+ 0, // 17
+ 0, // 18
+ 0, // 19
+ 0, // 20
+ 0, // 21
+ 0, // 22
+ 0, // 23
+ 0, // 24
+ 0, // 25
+ 0, // 26
+ 0, // 27
+ 0, // 28
+ 0, // 29
+ 0, // 30
+ 0, // 31
+ 2, 0, 0, // 32
+ 0, // 33
+ 0, // 34
+ 0, // 35
+ 0, // 36
+ 0, // 37
+ 0, // 38
+ 0, // 39
+ 0, // 40
+ 0, // 41
+ 0, // 42
+ 0, // 43
+ 0, // 44
+ 3, 24, 24, 24, // 45
+ 2, 192, 192, // 46
+ 0, // 47
+ 6, 255, 255, 129, 129, 255, 255, // 48
+ 6, 0, 0, 255, 255, 0, 0, // 49
+ 6, 241, 241, 145, 145, 159, 159, // 50
+ 6, 129, 129, 137, 137, 255, 255, // 51
+ 6, 31, 31, 16, 16, 255, 255, // 52
+ 6, 143, 143, 137, 137, 249, 249, // 53
+ 6, 255, 255, 137, 137, 249, 249, // 54
+ 6, 1, 1, 1, 1, 255, 255, // 55
+ 6, 255, 255, 137, 137, 255, 255, // 56
+ 6, 223, 159, 145, 145, 255, 255, // 57
+ 2, 102, 102, // 58
+ 0, // 59
+ 0, // 60
+ 0, // 61
+ 0, // 62
+ 0, // 63
+ 0, // 64
+ 0, // 65
+ 0, // 66
+ 0, // 67
+ 0, // 68
+ 0, // 69
+ 0, // 70
+ 0, // 71
+ 0, // 72
+ 0, // 73
+ 0, // 74
+ 0, // 75
+ 0, // 76
+ 0, // 77
+ 0, // 78
+ 0, // 79
+ 0, // 80
+ 0, // 81
+ 0, // 82
+ 0, // 83
+ 0, // 84
+ 0, // 85
+ 0, // 86
+ 0, // 87
+ 0, // 88
+ 0, // 89
+ 0, // 90
+ 0, // 91
+ 0, // 92
+ 0, // 93
+ 0, // 94
+ 0, // 95
+ 0, // 96
+ 0, // 97
+ 0, // 98
+ 0, // 99
+ 0, // 100
+ 0, // 101
+ 0, // 102
+ 0, // 103
+ 0, // 104
+ 0, // 105
+ 0, // 106
+ 0, // 107
+ 0, // 108
+ 0, // 109
+ 0, // 110
+ 0, // 111
+ 0, // 112
+ 0, // 113
+ 0, // 114
+ 0, // 115
+ 0, // 116
+ 0, // 117
+ 0, // 118
+ 0, // 119
+ 0, // 120
+ 0, // 121
+ 0, // 122
+ 0, // 123
+ 0, // 124
+ 0, // 125
+ 0, // 126
+ 0, // 127
+ 0, // 128
+ 0, // 129
+ 0, // 130
+ 0, // 131
+ 0, // 132
+ 0, // 133
+ 0, // 134
+ 0, // 135
+ 0, // 136
+ 0, // 137
+ 0, // 138
+ 0, // 139
+ 0, // 140
+ 0, // 141
+ 0, // 142
+ 0, // 143
+ 0, // 144
+ 0, // 145
+ 0, // 146
+ 0, // 147
+ 0, // 148
+ 0, // 149
+ 0, // 150
+ 0, // 151
+ 0, // 152
+ 0, // 153
+ 0, // 154
+ 0, // 155
+ 0, // 156
+ 0, // 157
+ 0, // 158
+ 0, // 159
+ 0, // 160
+ 0, // 161
+ 0, // 162
+ 0, // 163
+ 0, // 164
+ 0, // 165
+ 0, // 166
+ 0, // 167
+ 0, // 168
+ 0, // 169
+ 0, // 170
+ 0, // 171
+ 0, // 172
+ 0, // 173
+ 0, // 174
+ 0, // 175
+ 0, // 176
+ 0, // 177
+ 0, // 178
+ 0, // 179
+ 0, // 180
+ 0, // 181
+ 0, // 182
+ 0, // 183
+ 0, // 184
+ 0, // 185
+ 0, // 186
+ 0, // 187
+ 0, // 188
+ 0, // 189
+ 0, // 190
+ 0, // 191
+ 0, // 192
+ 0, // 193
+ 0, // 194
+ 0, // 195
+ 0, // 196
+ 0, // 197
+ 0, // 198
+ 0, // 199
+ 0, // 200
+ 0, // 201
+ 0, // 202
+ 0, // 203
+ 0, // 204
+ 0, // 205
+ 0, // 206
+ 0, // 207
+ 0, // 208
+ 0, // 209
+ 0, // 210
+ 0, // 211
+ 0, // 212
+ 0, // 213
+ 0, // 214
+ 0, // 215
+ 0, // 216
+ 0, // 217
+ 0, // 218
+ 0, // 219
+ 0, // 220
+ 0, // 221
+ 0, // 222
+ 0, // 223
+ 0, // 224
+ 0, // 225
+ 0, // 226
+ 0, // 227
+ 0, // 228
+ 0, // 229
+ 0, // 230
+ 0, // 231
+ 0, // 232
+ 0, // 233
+ 0, // 234
+ 0, // 235
+ 0, // 236
+ 0, // 237
+ 0, // 238
+ 0, // 239
+ 0, // 240
+ 0, // 241
+ 0, // 242
+ 0, // 243
+ 0, // 244
+ 0, // 245
+ 0, // 246
+ 0, // 247
+ 0, // 248
+ 0, // 249
+ 0, // 250
+ 0, // 251
+ 0, // 252
+ 0, // 253
+ 0, // 254
+ 0, // 255
+};
+
+MD_MAX72XX::fontType_t NumbersBold[] PROGMEM =
+{
+ 0, // 0
+ 0, // 1
+ 0, // 2
+ 0, // 3
+ 0, // 4
+ 0, // 5
+ 0, // 6
+ 0, // 7
+ 0, // 8
+ 0, // 9
+ 0, // 10
+ 0, // 11
+ 0, // 12
+ 0, // 13
+ 0, // 14
+ 0, // 15
+ 0, // 16
+ 0, // 17
+ 0, // 18
+ 0, // 19
+ 0, // 20
+ 0, // 21
+ 0, // 22
+ 0, // 23
+ 0, // 24
+ 0, // 25
+ 0, // 26
+ 0, // 27
+ 0, // 28
+ 0, // 29
+ 0, // 30
+ 0, // 31
+ 2, 0, 0, // 32
+ 0, // 33
+ 0, // 34
+ 0, // 35
+ 0, // 36
+ 0, // 37
+ 0, // 38
+ 0, // 39
+ 0, // 40
+ 0, // 41
+ 0, // 42
+ 0, // 43
+ 0, // 44
+ 3, 24, 24, 24, // 45
+ 2, 192, 192, // 46
+ 0, // 47
+ 6, 126, 255, 129, 129, 255, 126, // 48
+ 6, 132, 130, 255, 255, 128, 128, // 49
+ 6, 194, 227, 177, 153, 143, 134, // 50
+ 6, 66, 195, 137, 137, 255, 118, // 51
+ 6, 30, 31, 16, 16, 254, 254, // 52
+ 6, 79, 207, 137, 137, 249, 113, // 53
+ 6, 126, 251, 137, 137, 251, 114, // 54
+ 6, 1, 1, 241, 249, 15, 7, // 55
+ 6, 118, 255, 137, 137, 255, 118, // 56
+ 6, 78, 159, 145, 145, 255, 126, // 57
+ 2, 102, 102, // 58
+ 0, // 59
+ 0, // 60
+ 0, // 61
+ 0, // 62
+ 0, // 63
+ 0, // 64
+ 0, // 65
+ 0, // 66
+ 0, // 67
+ 0, // 68
+ 0, // 69
+ 0, // 70
+ 0, // 71
+ 0, // 72
+ 0, // 73
+ 0, // 74
+ 0, // 75
+ 0, // 76
+ 0, // 77
+ 0, // 78
+ 0, // 79
+ 0, // 80
+ 0, // 81
+ 0, // 82
+ 0, // 83
+ 0, // 84
+ 0, // 85
+ 0, // 86
+ 0, // 87
+ 0, // 88
+ 0, // 89
+ 0, // 90
+ 0, // 91
+ 0, // 92
+ 0, // 93
+ 0, // 94
+ 0, // 95
+ 0, // 96
+ 0, // 97
+ 0, // 98
+ 0, // 99
+ 0, // 100
+ 0, // 101
+ 0, // 102
+ 0, // 103
+ 0, // 104
+ 0, // 105
+ 0, // 106
+ 0, // 107
+ 0, // 108
+ 0, // 109
+ 0, // 110
+ 0, // 111
+ 0, // 112
+ 0, // 113
+ 0, // 114
+ 0, // 115
+ 0, // 116
+ 0, // 117
+ 0, // 118
+ 0, // 119
+ 0, // 120
+ 0, // 121
+ 0, // 122
+ 0, // 123
+ 0, // 124
+ 0, // 125
+ 0, // 126
+ 0, // 127
+ 0, // 128
+ 0, // 129
+ 0, // 130
+ 0, // 131
+ 0, // 132
+ 0, // 133
+ 0, // 134
+ 0, // 135
+ 0, // 136
+ 0, // 137
+ 0, // 138
+ 0, // 139
+ 0, // 140
+ 0, // 141
+ 0, // 142
+ 0, // 143
+ 0, // 144
+ 0, // 145
+ 0, // 146
+ 0, // 147
+ 0, // 148
+ 0, // 149
+ 0, // 150
+ 0, // 151
+ 0, // 152
+ 0, // 153
+ 0, // 154
+ 0, // 155
+ 0, // 156
+ 0, // 157
+ 0, // 158
+ 0, // 159
+ 0, // 160
+ 0, // 161
+ 0, // 162
+ 0, // 163
+ 0, // 164
+ 0, // 165
+ 0, // 166
+ 0, // 167
+ 0, // 168
+ 0, // 169
+ 0, // 170
+ 0, // 171
+ 0, // 172
+ 0, // 173
+ 0, // 174
+ 0, // 175
+ 0, // 176
+ 0, // 177
+ 0, // 178
+ 0, // 179
+ 0, // 180
+ 0, // 181
+ 0, // 182
+ 0, // 183
+ 0, // 184
+ 0, // 185
+ 0, // 186
+ 0, // 187
+ 0, // 188
+ 0, // 189
+ 0, // 190
+ 0, // 191
+ 0, // 192
+ 0, // 193
+ 0, // 194
+ 0, // 195
+ 0, // 196
+ 0, // 197
+ 0, // 198
+ 0, // 199
+ 0, // 200
+ 0, // 201
+ 0, // 202
+ 0, // 203
+ 0, // 204
+ 0, // 205
+ 0, // 206
+ 0, // 207
+ 0, // 208
+ 0, // 209
+ 0, // 210
+ 0, // 211
+ 0, // 212
+ 0, // 213
+ 0, // 214
+ 0, // 215
+ 0, // 216
+ 0, // 217
+ 0, // 218
+ 0, // 219
+ 0, // 220
+ 0, // 221
+ 0, // 222
+ 0, // 223
+ 0, // 224
+ 0, // 225
+ 0, // 226
+ 0, // 227
+ 0, // 228
+ 0, // 229
+ 0, // 230
+ 0, // 231
+ 0, // 232
+ 0, // 233
+ 0, // 234
+ 0, // 235
+ 0, // 236
+ 0, // 237
+ 0, // 238
+ 0, // 239
+ 0, // 240
+ 0, // 241
+ 0, // 242
+ 0, // 243
+ 0, // 244
+ 0, // 245
+ 0, // 246
+ 0, // 247
+ 0, // 248
+ 0, // 249
+ 0, // 250
+ 0, // 251
+ 0, // 252
+ 0, // 253
+ 0, // 254
+ 0, // 255
+};
+
+MD_MAX72XX::fontType_t NumbersThin[] PROGMEM =
+{
+ 0, // 0
+ 0, // 1
+ 0, // 2
+ 0, // 3
+ 0, // 4
+ 0, // 5
+ 0, // 6
+ 0, // 7
+ 0, // 8
+ 0, // 9
+ 0, // 10
+ 0, // 11
+ 0, // 12
+ 0, // 13
+ 0, // 14
+ 0, // 15
+ 0, // 16
+ 0, // 17
+ 0, // 18
+ 0, // 19
+ 0, // 20
+ 0, // 21
+ 0, // 22
+ 0, // 23
+ 0, // 24
+ 0, // 25
+ 0, // 26
+ 0, // 27
+ 0, // 28
+ 0, // 29
+ 0, // 30
+ 0, // 31
+ 1, 0, // 32
+ 0, // 33
+ 0, // 34
+ 0, // 35
+ 0, // 36
+ 0, // 37
+ 0, // 38
+ 0, // 39
+ 0, // 40
+ 0, // 41
+ 0, // 42
+ 0, // 43
+ 0, // 44
+ 2, 16, 16, // 45
+ 1, 128, // 46
+ 0, // 47
+ 3, 255, 129, 255, // 48
+ 3, 0, 255, 0, // 49
+ 3, 243, 145, 159, // 50
+ 3, 195, 137, 255, // 51
+ 3, 31, 16, 255, // 52
+ 3, 143, 137, 249, // 53
+ 3, 255, 137, 249, // 54
+ 3, 1, 1, 255, // 55
+ 3, 255, 137, 255, // 56
+ 3, 159, 145, 255, // 57
+ 1, 36, // 58
+ 0, // 59
+ 0, // 60
+ 0, // 61
+ 0, // 62
+ 0, // 63
+ 0, // 64
+ 0, // 65
+ 0, // 66
+ 0, // 67
+ 0, // 68
+ 0, // 69
+ 0, // 70
+ 0, // 71
+ 0, // 72
+ 0, // 73
+ 0, // 74
+ 0, // 75
+ 0, // 76
+ 0, // 77
+ 0, // 78
+ 0, // 79
+ 0, // 80
+ 0, // 81
+ 0, // 82
+ 0, // 83
+ 0, // 84
+ 0, // 85
+ 0, // 86
+ 0, // 87
+ 0, // 88
+ 0, // 89
+ 0, // 90
+ 0, // 91
+ 0, // 92
+ 0, // 93
+ 0, // 94
+ 0, // 95
+ 0, // 96
+ 0, // 97
+ 0, // 98
+ 0, // 99
+ 0, // 100
+ 0, // 101
+ 0, // 102
+ 0, // 103
+ 0, // 104
+ 0, // 105
+ 0, // 106
+ 0, // 107
+ 0, // 108
+ 0, // 109
+ 0, // 110
+ 0, // 111
+ 0, // 112
+ 0, // 113
+ 0, // 114
+ 0, // 115
+ 0, // 116
+ 0, // 117
+ 0, // 118
+ 0, // 119
+ 0, // 120
+ 0, // 121
+ 0, // 122
+ 0, // 123
+ 0, // 124
+ 0, // 125
+ 0, // 126
+ 0, // 127
+ 0, // 128
+ 0, // 129
+ 0, // 130
+ 0, // 131
+ 0, // 132
+ 0, // 133
+ 0, // 134
+ 0, // 135
+ 0, // 136
+ 0, // 137
+ 0, // 138
+ 0, // 139
+ 0, // 140
+ 0, // 141
+ 0, // 142
+ 0, // 143
+ 0, // 144
+ 0, // 145
+ 0, // 146
+ 0, // 147
+ 0, // 148
+ 0, // 149
+ 0, // 150
+ 0, // 151
+ 0, // 152
+ 0, // 153
+ 0, // 154
+ 0, // 155
+ 0, // 156
+ 0, // 157
+ 0, // 158
+ 0, // 159
+ 0, // 160
+ 0, // 161
+ 0, // 162
+ 0, // 163
+ 0, // 164
+ 0, // 165
+ 0, // 166
+ 0, // 167
+ 0, // 168
+ 0, // 169
+ 0, // 170
+ 0, // 171
+ 0, // 172
+ 0, // 173
+ 0, // 174
+ 0, // 175
+ 0, // 176
+ 0, // 177
+ 0, // 178
+ 0, // 179
+ 0, // 180
+ 0, // 181
+ 0, // 182
+ 0, // 183
+ 0, // 184
+ 0, // 185
+ 0, // 186
+ 0, // 187
+ 0, // 188
+ 0, // 189
+ 0, // 190
+ 0, // 191
+ 0, // 192
+ 0, // 193
+ 0, // 194
+ 0, // 195
+ 0, // 196
+ 0, // 197
+ 0, // 198
+ 0, // 199
+ 0, // 200
+ 0, // 201
+ 0, // 202
+ 0, // 203
+ 0, // 204
+ 0, // 205
+ 0, // 206
+ 0, // 207
+ 0, // 208
+ 0, // 209
+ 0, // 210
+ 0, // 211
+ 0, // 212
+ 0, // 213
+ 0, // 214
+ 0, // 215
+ 0, // 216
+ 0, // 217
+ 0, // 218
+ 0, // 219
+ 0, // 220
+ 0, // 221
+ 0, // 222
+ 0, // 223
+ 0, // 224
+ 0, // 225
+ 0, // 226
+ 0, // 227
+ 0, // 228
+ 0, // 229
+ 0, // 230
+ 0, // 231
+ 0, // 232
+ 0, // 233
+ 0, // 234
+ 0, // 235
+ 0, // 236
+ 0, // 237
+ 0, // 238
+ 0, // 239
+ 0, // 240
+ 0, // 241
+ 0, // 242
+ 0, // 243
+ 0, // 244
+ 0, // 245
+ 0, // 246
+ 0, // 247
+ 0, // 248
+ 0, // 249
+ 0, // 250
+ 0, // 251
+ 0, // 252
+ 0, // 253
+ 0, // 254
+ 0, // 255
+};
+
+MD_MAX72XX::fontType_t NumbersStandard[] PROGMEM = {
+0, // 0
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1, 0x00, // 32 - 'Space'
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 3, 0x80, 0x70, 0x30, // 44 - ','
+ 0,
+ 2, 0x60, 0x60, // 46 - '.'
+ 0,
+ 5, 62, 65, 65, 65, 62, // 48 - '0'
+ 3, 0x42, 0x7f, 0x40, // 49 - '1'
+ 5, 0x72, 0x49, 0x49, 0x49, 0x46, // 50 - '2'
+ 5, 0x21, 0x41, 0x49, 0x4d, 0x33, // 51 - '3'
+ 5, 0x18, 0x14, 0x12, 0x7f, 0x10, // 52 - '4'
+ 5, 0x27, 0x45, 0x45, 0x45, 0x39, // 53 - '5'
+ 5, 0x3c, 0x4a, 0x49, 0x49, 0x31, // 54 - '6'
+ 5, 0x41, 0x21, 0x11, 0x09, 0x07, // 55 - '7'
+ 5, 0x36, 0x49, 0x49, 0x49, 0x36, // 56 - '8'
+ 5, 0x46, 0x49, 0x49, 0x29, 0x1e, // 57 - '9'
+ 1, 0x14, // 58 - ':'
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ };
+
+MD_MAX72XX::fontType_t Numbers5x8[] PROGMEM =
+{
+ 0, // 0
+ 0, // 1
+ 0, // 2
+ 0, // 3
+ 0, // 4
+ 0, // 5
+ 0, // 6
+ 0, // 7
+ 0, // 8
+ 0, // 9
+ 0, // 10
+ 0, // 11
+ 0, // 12
+ 0, // 13
+ 0, // 14
+ 0, // 15
+ 0, // 16
+ 0, // 17
+ 0, // 18
+ 0, // 19
+ 0, // 20
+ 0, // 21
+ 0, // 22
+ 0, // 23
+ 0, // 24
+ 0, // 25
+ 0, // 26
+ 0, // 27
+ 0, // 28
+ 0, // 29
+ 0, // 30
+ 0, // 31
+ 1, 0x00, // 32 - 'Space'
+ 0, // 33
+ 0, // 34
+ 0, // 35
+ 0, // 36
+ 0, // 37
+ 0, // 38
+ 0, // 39
+ 0, // 40
+ 0, // 41
+ 0, // 42
+ 0, // 43
+ 0, // 44
+ 3, 16, 16, 16, // 45
+ 1, 128, // 46
+ 0, // 47
+ 5, 126, 129, 129, 129, 126, // 48
+ 5, 132, 130, 255, 128, 128, // 49
+ 5, 130, 193, 161, 145, 142, // 50
+ 5, 66, 129, 137, 137, 118, // 51
+ 5, 48, 40, 36, 34, 255, // 52
+ 5, 79, 137, 137, 137, 113, // 53
+ 5, 126, 137, 137, 137, 114, // 54
+ 5, 1, 225, 17, 9, 7, // 55
+ 5, 118, 137, 137, 137, 118, // 56
+ 5, 78, 145, 145, 145, 126, // 57
+ 1, 36, // 58
+ 0, // 59
+ 0, // 60
+ 0, // 61
+ 0, // 62
+ 0, // 63
+ 0, // 64
+ 0, // 65
+ 0, // 66
+ 0, // 67
+ 0, // 68
+ 0, // 69
+ 0, // 70
+ 0, // 71
+ 0, // 72
+ 0, // 73
+ 0, // 74
+ 0, // 75
+ 0, // 76
+ 0, // 77
+ 0, // 78
+ 0, // 79
+ 0, // 80
+ 0, // 81
+ 0, // 82
+ 0, // 83
+ 0, // 84
+ 0, // 85
+ 0, // 86
+ 0, // 87
+ 0, // 88
+ 0, // 89
+ 0, // 90
+ 0, // 91
+ 0, // 92
+ 0, // 93
+ 0, // 94
+ 0, // 95
+ 0, // 96
+ 0, // 97
+ 0, // 98
+ 0, // 99
+ 0, // 100
+ 0, // 101
+ 0, // 102
+ 0, // 103
+ 0, // 104
+ 0, // 105
+ 0, // 106
+ 0, // 107
+ 0, // 108
+ 0, // 109
+ 0, // 110
+ 0, // 111
+ 0, // 112
+ 0, // 113
+ 0, // 114
+ 0, // 115
+ 0, // 116
+ 0, // 117
+ 0, // 118
+ 0, // 119
+ 0, // 120
+ 0, // 121
+ 0, // 122
+ 0, // 123
+ 0, // 124
+ 0, // 125
+ 0, // 126
+ 0, // 127
+ 0, // 128
+ 0, // 129
+ 0, // 130
+ 0, // 131
+ 0, // 132
+ 0, // 133
+ 0, // 134
+ 0, // 135
+ 0, // 136
+ 0, // 137
+ 0, // 138
+ 0, // 139
+ 0, // 140
+ 0, // 141
+ 0, // 142
+ 0, // 143
+ 0, // 144
+ 0, // 145
+ 0, // 146
+ 0, // 147
+ 0, // 148
+ 0, // 149
+ 0, // 150
+ 0, // 151
+ 0, // 152
+ 0, // 153
+ 0, // 154
+ 0, // 155
+ 0, // 156
+ 0, // 157
+ 0, // 158
+ 0, // 159
+ 0, // 160
+ 0, // 161
+ 0, // 162
+ 0, // 163
+ 0, // 164
+ 0, // 165
+ 0, // 166
+ 0, // 167
+ 0, // 168
+ 0, // 169
+ 0, // 170
+ 0, // 171
+ 0, // 172
+ 0, // 173
+ 0, // 174
+ 0, // 175
+ 0, // 176
+ 0, // 177
+ 0, // 178
+ 0, // 179
+ 0, // 180
+ 0, // 181
+ 0, // 182
+ 0, // 183
+ 0, // 184
+ 0, // 185
+ 0, // 186
+ 0, // 187
+ 0, // 188
+ 0, // 189
+ 0, // 190
+ 0, // 191
+ 0, // 192
+ 0, // 193
+ 0, // 194
+ 0, // 195
+ 0, // 196
+ 0, // 197
+ 0, // 198
+ 0, // 199
+ 0, // 200
+ 0, // 201
+ 0, // 202
+ 0, // 203
+ 0, // 204
+ 0, // 205
+ 0, // 206
+ 0, // 207
+ 0, // 208
+ 0, // 209
+ 0, // 210
+ 0, // 211
+ 0, // 212
+ 0, // 213
+ 0, // 214
+ 0, // 215
+ 0, // 216
+ 0, // 217
+ 0, // 218
+ 0, // 219
+ 0, // 220
+ 0, // 221
+ 0, // 222
+ 0, // 223
+ 0, // 224
+ 0, // 225
+ 0, // 226
+ 0, // 227
+ 0, // 228
+ 0, // 229
+ 0, // 230
+ 0, // 231
+ 0, // 232
+ 0, // 233
+ 0, // 234
+ 0, // 235
+ 0, // 236
+ 0, // 237
+ 0, // 238
+ 0, // 239
+ 0, // 240
+ 0, // 241
+ 0, // 242
+ 0, // 243
+ 0, // 244
+ 0, // 245
+ 0, // 246
+ 0, // 247
+ 0, // 248
+ 0, // 249
+ 0, // 250
+ 0, // 251
+ 0, // 252
+ 0, // 253
+ 0, // 254
+ 0, // 255
+};
+
+MD_MAX72XX::fontType_t RomanCyrillic[] PROGMEM = {
+ 0, // 0
+ 5, 0x3e, 0x5b, 0x4f, 0x5b, 0x3e, // 1 - 'Sad Smiley'
+ 5, 0x3e, 0x6b, 0x4f, 0x6b, 0x3e, // 2 - 'Happy Smiley'
+ 5, 0x1c, 0x3e, 0x7c, 0x3e, 0x1c, // 3 - 'Heart'
+ 5, 0x18, 0x3c, 0x7e, 0x3c, 0x18, // 4 - 'Diamond'
+ 5, 0x1c, 0x57, 0x7d, 0x57, 0x1c, // 5 - 'Clubs'
+ 5, 0x1c, 0x5e, 0x7f, 0x5e, 0x1c, // 6 - 'Spades'
+ 4, 0x00, 0x18, 0x3c, 0x18, // 7 - 'Bullet Point'
+ 5, 0xff, 0xe7, 0xc3, 0xe7, 0xff, // 8 - 'Rev Bullet Point'
+ 4, 0x00, 0x18, 0x24, 0x18, // 9 - 'Hollow Bullet Point'
+ 5, 0xff, 0xe7, 0xdb, 0xe7, 0xff, // 10 - 'Rev Hollow BP'
+ 5, 0x30, 0x48, 0x3a, 0x06, 0x0e, // 11 - 'Male'
+ 5, 0x26, 0x29, 0x79, 0x29, 0x26, // 12 - 'Female'
+ 5, 0x40, 0x7f, 0x05, 0x05, 0x07, // 13 - 'Music Note 1'
+ 5, 0x40, 0x7f, 0x05, 0x25, 0x3f, // 14 - 'Music Note 2'
+ 3, 2, 5, 2, // 15 - degree sign
+ 5, 0x7f, 0x3e, 0x1c, 0x1c, 0x08, // 16 - 'Right Pointer'
+ 5, 0x08, 0x1c, 0x1c, 0x3e, 0x7f, // 17 - 'Left Pointer'
+ 5, 0x14, 0x22, 0x7f, 0x22, 0x14, // 18 - 'UpDown Arrows'
+ 5, 0x5f, 0x5f, 0x00, 0x5f, 0x5f, // 19 - 'Double Exclamation'
+ 5, 0x06, 0x09, 0x7f, 0x01, 0x7f, // 20 - 'Paragraph Mark'
+ 4, 0x66, 0x89, 0x95, 0x6a, // 21 - 'Section Mark'
+ 5, 0x60, 0x60, 0x60, 0x60, 0x60, // 22 - 'Double Underline'
+ 5, 0x94, 0xa2, 0xff, 0xa2, 0x94, // 23 - 'UpDown Underlined'
+ 5, 0x08, 0x04, 0x7e, 0x04, 0x08, // 24 - 'Up Arrow'
+ 5, 0x10, 0x20, 0x7e, 0x20, 0x10, // 25 - 'Down Arrow'
+ 5, 0x08, 0x08, 0x2a, 0x1c, 0x08, // 26 - 'Right Arrow'
+ 5, 0x08, 0x1c, 0x2a, 0x08, 0x08, // 27 - 'Left Arrow'
+ 5, 0x1e, 0x10, 0x10, 0x10, 0x10, // 28 - 'Angled'
+ 5, 0x0c, 0x1e, 0x0c, 0x1e, 0x0c, // 29 - 'Squashed #'
+ 5, 0x30, 0x38, 0x3e, 0x38, 0x30, // 30 - 'Up Pointer'
+ 5, 0x06, 0x0e, 0x3e, 0x0e, 0x06, // 31 - 'Down Pointer'
+ 2, 0x00, 0x00, // 32 - 'Space'
+ 1, 0x5f, // 33 - '!'
+ 3, 0x07, 0x00, 0x07, // 34 - '"'
+ 5, 0x14, 0x7f, 0x14, 0x7f, 0x14, // 35 - '#'
+ 5, 0x24, 0x2a, 0x7f, 0x2a, 0x12, // 36 - '$'
+ 5, 0x23, 0x13, 0x08, 0x64, 0x62, // 37 - '%'
+ 5, 0x36, 0x49, 0x56, 0x20, 0x50, // 38 - '&'
+ 3, 0x08, 0x07, 0x03, // 39 - '''
+ 3, 0x1c, 0x22, 0x41, // 40 - '('
+ 3, 0x41, 0x22, 0x1c, // 41 - ')'
+ 5, 0x2a, 0x1c, 0x7f, 0x1c, 0x2a, // 42 - '*'
+ 5, 0x08, 0x08, 0x3e, 0x08, 0x08, // 43 - '+'
+ 3, 0x80, 0x70, 0x30, // 44 - ','
+ 5, 0x08, 0x08, 0x08, 0x08, 0x08, // 45 - '-'
+ 2, 0x60, 0x60, // 46 - '.'
+ 5, 0x20, 0x10, 0x08, 0x04, 0x02, // 47 - '/'
+ 5, 0x3e, 0x51, 0x49, 0x45, 0x3e, // 48 - '0'
+ 3, 0x42, 0x7f, 0x40, // 49 - '1'
+ 5, 0x72, 0x49, 0x49, 0x49, 0x46, // 50 - '2'
+ 5, 0x21, 0x41, 0x49, 0x4d, 0x33, // 51 - '3'
+ 5, 0x18, 0x14, 0x12, 0x7f, 0x10, // 52 - '4'
+ 5, 0x27, 0x45, 0x45, 0x45, 0x39, // 53 - '5'
+ 5, 0x3c, 0x4a, 0x49, 0x49, 0x31, // 54 - '6'
+ 5, 0x41, 0x21, 0x11, 0x09, 0x07, // 55 - '7'
+ 5, 0x36, 0x49, 0x49, 0x49, 0x36, // 56 - '8'
+ 5, 0x46, 0x49, 0x49, 0x29, 0x1e, // 57 - '9'
+ 1, 0x14, // 58 - ':'
+ 2, 0x80, 0x68, // 59 - ';'
+ 4, 0x08, 0x14, 0x22, 0x41, // 60 - '<'
+ 5, 0x14, 0x14, 0x14, 0x14, 0x14, // 61 - '='
+ 4, 0x41, 0x22, 0x14, 0x08, // 62 - '>'
+ 5, 0x02, 0x01, 0x59, 0x09, 0x06, // 63 - '?'
+ 5, 0x3e, 0x41, 0x5d, 0x59, 0x4e, // 64 - '@'
+ 5, 0x7c, 0x12, 0x11, 0x12, 0x7c, // 65 - 'A'
+ 5, 0x7f, 0x49, 0x49, 0x49, 0x36, // 66 - 'B'
+ 5, 0x3e, 0x41, 0x41, 0x41, 0x22, // 67 - 'C'
+ 5, 0x7f, 0x41, 0x41, 0x41, 0x3e, // 68 - 'D'
+ 5, 0x7f, 0x49, 0x49, 0x49, 0x41, // 69 - 'E'
+ 5, 0x7f, 0x09, 0x09, 0x09, 0x01, // 70 - 'F'
+ 5, 0x3e, 0x41, 0x41, 0x51, 0x73, // 71 - 'G'
+ 5, 0x7f, 0x08, 0x08, 0x08, 0x7f, // 72 - 'H'
+ 3, 0x41, 0x7f, 0x41, // 73 - 'I'
+ 5, 0x20, 0x40, 0x41, 0x3f, 0x01, // 74 - 'J'
+ 5, 0x7f, 0x08, 0x14, 0x22, 0x41, // 75 - 'K'
+ 5, 0x7f, 0x40, 0x40, 0x40, 0x40, // 76 - 'L'
+ 5, 0x7f, 0x02, 0x1c, 0x02, 0x7f, // 77 - 'M'
+ 5, 0x7f, 0x04, 0x08, 0x10, 0x7f, // 78 - 'N'
+ 5, 0x3e, 0x41, 0x41, 0x41, 0x3e, // 79 - 'O'
+ 5, 0x7f, 0x09, 0x09, 0x09, 0x06, // 80 - 'P'
+ 5, 0x3e, 0x41, 0x51, 0x21, 0x5e, // 81 - 'Q'
+ 5, 0x7f, 0x09, 0x19, 0x29, 0x46, // 82 - 'R'
+ 5, 0x26, 0x49, 0x49, 0x49, 0x32, // 83 - 'S'
+ 5, 0x03, 0x01, 0x7f, 0x01, 0x03, // 84 - 'T'
+ 5, 0x3f, 0x40, 0x40, 0x40, 0x3f, // 85 - 'U'
+ 5, 0x1f, 0x20, 0x40, 0x20, 0x1f, // 86 - 'V'
+ 5, 0x3f, 0x40, 0x38, 0x40, 0x3f, // 87 - 'W'
+ 5, 0x63, 0x14, 0x08, 0x14, 0x63, // 88 - 'X'
+ 5, 0x03, 0x04, 0x78, 0x04, 0x03, // 89 - 'Y'
+ 5, 0x61, 0x59, 0x49, 0x4d, 0x43, // 90 - 'Z'
+ 3, 0x7f, 0x41, 0x41, // 91 - '['
+ 5, 0x02, 0x04, 0x08, 0x10, 0x20, // 92 - '\'
+ 3, 0x41, 0x41, 0x7f, // 93 - ']'
+ 5, 0x04, 0x02, 0x01, 0x02, 0x04, // 94 - '^'
+ 5, 0x40, 0x40, 0x40, 0x40, 0x40, // 95 - '_'
+ 3, 0x03, 0x07, 0x08, // 96 - '`'
+ 5, 0x20, 0x54, 0x54, 0x78, 0x40, // 97 - 'a'
+ 5, 0x7f, 0x28, 0x44, 0x44, 0x38, // 98 - 'b'
+ 5, 0x38, 0x44, 0x44, 0x44, 0x28, // 99 - 'c'
+ 5, 0x38, 0x44, 0x44, 0x28, 0x7f, // 100 - 'd'
+ 5, 0x38, 0x54, 0x54, 0x54, 0x18, // 101 - 'e'
+ 4, 0x08, 0x7e, 0x09, 0x02, // 102 - 'f'
+ 5, 0x18, 0xa4, 0xa4, 0x9c, 0x78, // 103 - 'g'
+ 5, 0x7f, 0x08, 0x04, 0x04, 0x78, // 104 - 'h'
+ 3, 0x44, 0x7d, 0x40, // 105 - 'i'
+ 4, 0x40, 0x80, 0x80, 0x7a, // 106 - 'j'
+ 4, 0x7f, 0x10, 0x28, 0x44, // 107 - 'k'
+ 3, 0x41, 0x7f, 0x40, // 108 - 'l'
+ 5, 0x7c, 0x04, 0x78, 0x04, 0x78, // 109 - 'm'
+ 5, 0x7c, 0x08, 0x04, 0x04, 0x78, // 110 - 'n'
+ 5, 0x38, 0x44, 0x44, 0x44, 0x38, // 111 - 'o'
+ 5, 0xfc, 0x18, 0x24, 0x24, 0x18, // 112 - 'p'
+ 5, 0x18, 0x24, 0x24, 0x18, 0xfc, // 113 - 'q'
+ 5, 0x7c, 0x08, 0x04, 0x04, 0x08, // 114 - 'r'
+ 5, 0x48, 0x54, 0x54, 0x54, 0x24, // 115 - 's'
+ 4, 0x04, 0x3f, 0x44, 0x24, // 116 - 't'
+ 5, 0x3c, 0x40, 0x40, 0x20, 0x7c, // 117 - 'u'
+ 5, 0x1c, 0x20, 0x40, 0x20, 0x1c, // 118 - 'v'
+ 5, 0x3c, 0x40, 0x30, 0x40, 0x3c, // 119 - 'w'
+ 5, 0x44, 0x28, 0x10, 0x28, 0x44, // 120 - 'x'
+ 5, 0x4c, 0x90, 0x90, 0x90, 0x7c, // 121 - 'y'
+ 5, 0x44, 0x64, 0x54, 0x4c, 0x44, // 122 - 'z'
+ 3, 0x08, 0x36, 0x41, // 123 - '{'
+ 1, 0x77, // 124 - '|'
+ 3, 0x41, 0x36, 0x08, // 125 - '}'
+ 5, 0x02, 0x01, 0x02, 0x04, 0x02, // 126 - '~'
+ 5, 0x3c, 0x26, 0x23, 0x26, 0x3c, // 127 - 'Hollow Up Arrow'
+ 5, 0x1e, 0xa1, 0xa1, 0x61, 0x12, // 128 - 'C sedilla'
+ 5, 0x38, 0x42, 0x40, 0x22, 0x78, // 129 - 'u umlaut'
+ 5, 0x38, 0x54, 0x54, 0x55, 0x59, // 130 - 'e acute'
+ 5, 0x21, 0x55, 0x55, 0x79, 0x41, // 131 - 'a accent'
+ 5, 0x21, 0x54, 0x54, 0x78, 0x41, // 132 - 'a umlaut'
+ 5, 0x21, 0x55, 0x54, 0x78, 0x40, // 133 - 'a grave'
+ 5, 0x20, 0x54, 0x55, 0x79, 0x40, // 134 - 'a acute'
+ 5, 0x18, 0x3c, 0xa4, 0xe4, 0x24, // 135 - 'c sedilla'
+ 5, 0x39, 0x55, 0x55, 0x55, 0x59, // 136 - 'e accent'
+ 5, 0x38, 0x55, 0x54, 0x55, 0x58, // 137 - 'e umlaut'
+ 5, 0x39, 0x55, 0x54, 0x54, 0x58, // 138 - 'e grave'
+ 3, 0x45, 0x7c, 0x41, // 139 - 'i umlaut'
+ 4, 0x02, 0x45, 0x7d, 0x42, // 140 - 'i hat'
+ 4, 0x01, 0x45, 0x7c, 0x40, // 141 - 'i grave'
+ 5, 0xf0, 0x29, 0x24, 0x29, 0xf0, // 142 - 'A umlaut'
+ 5, 0xf0, 0x28, 0x25, 0x28, 0xf0, // 143 - 'A dot'
+ 4, 0x7c, 0x54, 0x55, 0x45, // 144 - 'E grave'
+ 7, 0x20, 0x54, 0x54, 0x7c, 0x54, 0x54, 0x08, // 145 - 'ae'
+ 6, 0x7c, 0x0a, 0x09, 0x7f, 0x49, 0x49, // 146 - 'AE'
+ 5, 0x32, 0x49, 0x49, 0x49, 0x32, // 147 - 'o hat'
+ 5, 0x30, 0x4a, 0x48, 0x4a, 0x30, // 148 - 'o umlaut'
+ 5, 0x32, 0x4a, 0x48, 0x48, 0x30, // 149 - 'o grave'
+ 5, 0x3a, 0x41, 0x41, 0x21, 0x7a, // 150 - 'u hat'
+ 5, 0x3a, 0x42, 0x40, 0x20, 0x78, // 151 - 'u grave'
+ 4, 0x9d, 0xa0, 0xa0, 0x7d, // 152 - 'y umlaut'
+ 5, 0x38, 0x45, 0x44, 0x45, 0x38, // 153 - 'O umlaut'
+ 5, 0x3c, 0x41, 0x40, 0x41, 0x3c, // 154 - 'U umlaut'
+ 5, 0x3c, 0x24, 0xff, 0x24, 0x24, // 155 - 'Cents'
+ 5, 0x48, 0x7e, 0x49, 0x43, 0x66, // 156 - 'Pounds'
+ 5, 0x2b, 0x2f, 0xfc, 0x2f, 0x2b, // 157 - 'Yen'
+ 5, 0xff, 0x09, 0x29, 0xf6, 0x20, // 158 - 'R +'
+ 5, 0xc0, 0x88, 0x7e, 0x09, 0x03, // 159 - 'f notation'
+ 5, 0x20, 0x54, 0x54, 0x79, 0x41, // 160 - 'a acute'
+ 3, 0x44, 0x7d, 0x41, // 161 - 'i acute'
+ 5, 0x30, 0x48, 0x48, 0x4a, 0x32, // 162 - 'o acute'
+ 5, 0x38, 0x40, 0x40, 0x22, 0x7a, // 163 - 'u acute'
+ 4, 0x7a, 0x0a, 0x0a, 0x72, // 164 - 'n accent'
+ 5, 0x7d, 0x0d, 0x19, 0x31, 0x7d, // 165 - 'N accent'
+ 5, 0x26, 0x29, 0x29, 0x2f, 0x28, // 166
+ 5, 0x26, 0x29, 0x29, 0x29, 0x26, // 167
+ 5, 0x30, 0x48, 0x4d, 0x40, 0x20, // 168 - 'Inverted ?'
+ 5, 0x38, 0x08, 0x08, 0x08, 0x08, // 169 - 'LH top corner'
+ 5, 0x08, 0x08, 0x08, 0x08, 0x38, // 170 - 'RH top corner'
+ 5, 0x2f, 0x10, 0xc8, 0xac, 0xba, // 171 - '1/2'
+ 5, 0x2f, 0x10, 0x28, 0x34, 0xfa, // 172 - '1/4'
+ 1, 0x7b, // 173 - '| split'
+ 5, 0x08, 0x14, 0x2a, 0x14, 0x22, // 174 - '<<'
+ 5, 0x22, 0x14, 0x2a, 0x14, 0x08, // 175 - '>>'
+ 5, 0xaa, 0x00, 0x55, 0x00, 0xaa, // 176 - '30% shading'
+ 5, 0xaa, 0x55, 0xaa, 0x55, 0xaa, // 177 - '50% shading'
+ 5, 0x00, 0x00, 0x00, 0x00, 0xff, // 178 - 'Right side'
+ 5, 0x10, 0x10, 0x10, 0x10, 0xff, // 179 - 'Right T'
+ 5, 0x14, 0x14, 0x14, 0x14, 0xff, // 180 - 'Right T double H'
+ 5, 0x10, 0x10, 0xff, 0x00, 0xff, // 181 - 'Right T double V'
+ 5, 0x10, 0x10, 0xf0, 0x10, 0xf0, // 182 - 'Top Right double V'
+ 5, 0x14, 0x14, 0x14, 0x14, 0xfc, // 183 - 'Top Right double H'
+ 5, 0x14, 0x14, 0xf7, 0x00, 0xff, // 184 - 'Right T double all'
+ 5, 0x00, 0x00, 0xff, 0x00, 0xff, // 185 - 'Right side double'
+ 5, 0x14, 0x14, 0xf4, 0x04, 0xfc, // 186 - 'Top Right double'
+ 5, 0x14, 0x14, 0x17, 0x10, 0x1f, // 187 - 'Bot Right double'
+ 5, 0x10, 0x10, 0x1f, 0x10, 0x1f, // 188 - 'Bot Right double V'
+ 5, 0x14, 0x14, 0x14, 0x14, 0x1f, // 189 - 'Bot Right double H'
+ 5, 0x10, 0x10, 0x10, 0x10, 0xf0, // 190 - 'Top Right'
+ 5, 0x00, 0x00, 0x00, 0x1f, 0x10, // 191 - 'Bot Left'
+ 5, 0x7e, 0x11, 0x11, 0x11, 0x7e, // 192 - '?' x0c0
+ 5, 0x7f, 0x49, 0x49, 0x49, 0x31, // 193 - '?' x0c1
+ 5, 0x7f, 0x49, 0x49, 0x49, 0x36, // 194 - 'B' x0c2
+ 5, 0x7f, 0x01, 0x01, 0x01, 0x03, // 195 - '?' x0c3
+ 6, 0xc0, 0x7e, 0x41, 0x41, 0x7e, 0xc0, // 196 - '?' x0c4
+ 5, 0x7f, 0x49, 0x49, 0x49, 0x41, // 197 - 'E' x0c5
+ 5, 0x77, 0x08, 0x7f, 0x08, 0x77, // 198 - '?' x0c6
+ 5, 0x41, 0x49, 0x49, 0x49, 0x36, // 199 - '?' x0c7
+ 5, 0x7f, 0x10, 0x08, 0x04, 0x7f, // 200 - '?' x0c8
+ 5, 0x7f, 0x10, 0x09, 0x04, 0x7f, // 201 - '?' x0c9
+ 5, 0x7f, 0x08, 0x14, 0x22, 0x41, // 202 - 'K' x0ca
+ 5, 0x40, 0x3e, 0x01, 0x01, 0x7f, // 203 - '?' x0cb
+ 5, 0x7f, 0x02, 0x0c, 0x02, 0x7f, // 204 - 'M' x0cc
+ 5, 0x7f, 0x08, 0x08, 0x08, 0x7f, // 205 - 'H' x0cd
+ 5, 0x3e, 0x41, 0x41, 0x41, 0x3e, // 206 - 'O' x0ce
+ 5, 0x7f, 0x01, 0x01, 0x01, 0x7f, // 207 - '?' x0cf
+ 5, 0x7f, 0x09, 0x09, 0x09, 0x06, // 208 - '?' x0d0
+ 5, 0x3e, 0x41, 0x41, 0x41, 0x22, // 209 - 'C' x0d1
+ 5, 0x03, 0x01, 0x7f, 0x01, 0x03, // 210 - 'T' x0d2
+ 5, 0x27, 0x48, 0x48, 0x48, 0x3f, // 211 - '?' x0d3
+ 7, 0x1c, 0x22, 0x22, 0x7f, 0x22, 0x22, 0x1c, // 212 - '?' x0d4
+ 5, 0x63, 0x14, 0x08, 0x14, 0x63, // 213 - 'X' x0d5
+ 6, 0x7f, 0x40, 0x40, 0x40, 0x7f, 0xc0, // 214 - '?' x0d6
+ 5, 0x07, 0x08, 0x08, 0x08, 0x7f, // 215 - '?' x0d7
+ 5, 0x7f, 0x40, 0x7f, 0x40, 0x7f, // 216 - '?' x0d8
+ 6, 0x7f, 0x40, 0x7f, 0x40, 0x7f, 0xc0, // 217 - '?' x0d9
+ 7, 0x01, 0x01, 0x7f, 0x48, 0x48, 0x48, 0x30, // 218 - '?' x0da
+ 5, 0x7f, 0x48, 0x48, 0x30, 0x7f, // 219 - '?' x0db
+ 5, 0x7f, 0x48, 0x48, 0x48, 0x30, // 220 - '?' x0dc
+ 5, 0x22, 0x41, 0x49, 0x49, 0x3e, // 221 - '?' x0dd
+ 6, 0x7f, 0x08, 0x3e, 0x41, 0x41, 0x3e, // 222 - '?' x0de
+ 5, 0x46, 0x29, 0x19, 0x09, 0x7f, // 223 - '?' x0df
+ 5, 0x20, 0x54, 0x54, 0x54, 0x78, // 224 - '?' x0e0
+ 5, 0x3c, 0x4a, 0x4a, 0x49, 0x31, // 225 - '?' x0e1
+ 5, 0x7c, 0x54, 0x54, 0x54, 0x28, // 226 - '?' x0e2
+ 4, 0x7c, 0x04, 0x04, 0x0c, // 227 - '?' x0e3
+ 6, 0xc0, 0x78, 0x44, 0x44, 0x78, 0xc0, // 228 - '?' x0e4
+ 5, 0x38, 0x54, 0x54, 0x54, 0x18, // 229 - 'e' x0e5
+ 5, 0x6c, 0x10, 0x7c, 0x10, 0x6c, // 230 - '?' x0e6
+ 5, 0x44, 0x54, 0x54, 0x54, 0x28, // 231 - '?' x0e7
+ 5, 0x7c, 0x20, 0x10, 0x08, 0x7c, // 232 - '?' x0e8
+ 5, 0x7c, 0x40, 0x26, 0x10, 0x7c, // 233 - '?' x0e9
+ 5, 0x7c, 0x10, 0x10, 0x28, 0x44, // 234 - '?' x0ea
+ 5, 0x40, 0x38, 0x04, 0x04, 0x7c, // 235 - '?' x0eb
+ 5, 0x7c, 0x08, 0x10, 0x08, 0x7c, // 236 - '?' x0ec
+ 5, 0x7c, 0x10, 0x10, 0x10, 0x7c, // 237 - '?' x0ed
+ 5, 0x38, 0x44, 0x44, 0x44, 0x38, // 238 - 'o' x0ee
+ 5, 0x7c, 0x04, 0x04, 0x04, 0x7c, // 239 - '?' x0ef
+ 5, 0xfc, 0x24, 0x24, 0x24, 0x18, // 240 - '?' x0f0
+ 4, 0x38, 0x44, 0x44, 0x44, // 241 - '?' x0f1
+ 5, 0x04, 0x04, 0x7c, 0x04, 0x04, // 242 - '?' x0f2
+ 5, 0x0c, 0x90, 0x90, 0x90, 0x7c, // 243 - '?' x0f3
+ 7, 0x10, 0x28, 0x28, 0xfc, 0x28, 0x28, 0x10, // 244 - '?' x0f4
+ 5, 0x44, 0x28, 0x10, 0x28, 0x44, // 245 - 'x' x0f5
+ 5, 0x7c, 0x40, 0x40, 0x7c, 0xc0, // 246 - '?' x0f6
+ 5, 0x0c, 0x10, 0x10, 0x10, 0x7c, // 247 - '?' x0f7
+ 5, 0x7c, 0x40, 0x7c, 0x40, 0x7c, // 248 - '?' x0f8
+ 6, 0x7c, 0x40, 0x7c, 0x40, 0x7c, 0xc0, // 249 - '?' x0f9
+ 6, 0x04, 0x7c, 0x50, 0x50, 0x50, 0x20, // 250 - '?' x0fa
+ 5, 0x7c, 0x50, 0x50, 0x20, 0x7c, // 251 - '?' x0fb
+ 5, 0x7c, 0x50, 0x50, 0x50, 0x20, // 252 - '?' x0fc
+ 5, 0x28, 0x44, 0x54, 0x54, 0x38, // 253 - '?' x0fd
+ 6, 0x7c, 0x10, 0x38, 0x44, 0x44, 0x38, // 254 - '?' x0fe
+ 5, 0x48, 0x34, 0x14, 0x14, 0x7c // 255 - '?' x0ff
+};
+
+MD_MAX72XX::fontType_t* fonts[] PROGMEM = {
+ (MD_MAX72XX::fontType_t*)&NumbersStandard,
+ (MD_MAX72XX::fontType_t*)&Numbers5x8,
+ (MD_MAX72XX::fontType_t*)&NumbersThin,
+ (MD_MAX72XX::fontType_t*)&NumbersBold,
+ (MD_MAX72XX::fontType_t*)&NumbersBoldStraight,
+ (MD_MAX72XX::fontType_t*)&NumbersSinclair
+};
--- /dev/null
+#include "Clock.h"
+#include <Wire.h>
+#include "time.h"
+#include <Button2.h>
+#include <Arduino.h>
+#include <coredecls.h>
+
+RTC_DS3231 RTC;
+bool isRTCEnabled = false;
+
+Button2 btn;
+bool isButtonEnabled = false;
+
+bool isBuzzerEnabled = false;
+int buzzer_pin;
+bool buzzer_passive;
+
+bool beepState = false;
+bool beepStateRequested = false;
+
+int beepToneRequested;
+int beepLengthRequested;
+
+int beepMs = 400;
+int silentMs = 200;
+int beepTone = 1000;
+
+void beep(int tone, int length, int beep, int silent) {
+ beepStateRequested = true;
+ beepToneRequested = tone;
+ beepLengthRequested = length;
+ if (!silent && beep>length) {
+ beepMs = length;
+ } else {
+ beepMs = beep;
+ }
+ silentMs = silent;
+}
+
+unsigned long first_click_millis = 0;
+
+void buttonHandler(Button2& btn) {
+ if (!isButtonEnabled) return;
+ if (beepStateRequested) {
+ beepStateRequested = false;
+ return;
+ }
+ clickType click = btn.read();
+ Serial.println(btn.clickToString(click));
+ switch (click) {
+ case single_click:
+ screenModeRequested++;
+ if (screenModeRequested == mBarrier) { screenModeRequested = mDefault; }
+ if (screenModeRequested == mLast) { screenModeRequested = mDefault; }
+ break;
+ case double_click:
+ screenModeRequested--;
+ if (screenModeRequested <= mDefault) { screenModeRequested = mBarrier - 1; }
+ break;
+ case triple_click:
+ static int click_counter = 0;
+ if (millis() - first_click_millis > 6000) {
+ first_click_millis = millis();
+ click_counter = 1;
+ beep(400,800,200,100);
+ message(F("Длинное - точка доступа, тройное - сброс"));
+ } else {
+ message(F("Еще раз для сброса"));
+ beep(800,800,200,100);
+ click_counter++;
+ if (click_counter>2) {
+ reportMessage(F("Сбрасываю настройки"));
+ tone(1200,1000);
+ Serial.println(F("Три тройных нажатия - сбрасываю конфигурацию"));
+ reset();
+ }
+ }
+ break;
+ }
+}
+
+void buttonLongClickHandler(Button2& btn) {
+ if (!isButtonEnabled) return;
+ if (beepStateRequested) {
+ beepStateRequested = false;
+ return;
+ }
+ if (millis() - first_click_millis < 6000) {
+ if (isApEnabled) {
+ setupNet(false);
+ } else {
+ setupNet(true);
+ }
+ return;
+ }
+ bool enable_alarm = cfg.getBoolValue(F("enable_alarm"));
+ enable_alarm = !enable_alarm;
+ cfg.setValue(F("enable_alarm"), enable_alarm);
+ Serial.println(F("Alarm = ")); Serial.println(enable_alarm);
+ if (enable_alarm) {
+ message(F("Будильник включен"));
+ reportChange(F("enable_alarm"));
+ reportMessage(F("Будильник включен"));
+ } else {
+ message(F("Будильник выключен"));
+ reportChange(F("enable_alarm"));
+ reportMessage(F("Будильник выключен"));
+ }
+
+}
+
+void setupHardware() {
+ int pin_sda = cfg.getIntValue(F("pin_sda"));
+ int pin_scl = cfg.getIntValue(F("pin_scl"));
+ if (pin_sda && pin_scl && cfg.getBoolValue(F("enable_rtc"))) {
+ Serial.println(F("Нестандартные пины i2c"));
+ Wire.begin(pin_sda,pin_scl);
+ }
+ int i2c_speed = cfg.getIntValue(F("i2c_speed"));
+ if (i2c_speed) Wire.setClock(i2c_speed);
+ if (cfg.getBoolValue(F("enable_rtc"))) {
+ RTC.begin();
+ time_t rtc = RTC.now().unixtime();
+ timeval tv = { rtc, 0 };
+ settimeofday(&tv, nullptr);
+ isRTCEnabled = true;
+ Serial.println(F("Время установлено по встроенным часам"));
+ }
+ if (cfg.getBoolValue(F("enable_button"))) {
+ int button_pin = cfg.getIntValue(F("button_pin"));
+ btn.begin(button_pin, INPUT_PULLUP, !cfg.getBoolValue("button_inversed"));
+ btn.setLongClickTime(700);
+ btn.setDoubleClickTime(500);
+ btn.setClickHandler(&buttonHandler);
+ btn.setDoubleClickHandler(&buttonHandler);
+ btn.setTripleClickHandler(&buttonHandler);
+ btn.setLongClickDetectedHandler(&buttonLongClickHandler);
+ isButtonEnabled = true;
+ } else {
+ isButtonEnabled = false;
+ }
+ if (cfg.getBoolValue(F("enable_buzzer"))) {
+ isBuzzerEnabled = true;
+ buzzer_pin = cfg.getIntValue(F("buzzer_pin"));
+ pinMode(buzzer_pin,OUTPUT);
+ buzzer_passive = cfg.getBoolValue(F("buzzer_passive"));
+ } else {
+ isBuzzerEnabled = true;
+ }
+}
+
+void doBeep(int note, int duration) {
+ tone(buzzer_pin, note, duration);
+}
+
+void tickHardware() {
+ if (isButtonEnabled) {
+ btn.loop();
+ }
+ if (isBuzzerEnabled) {
+ static unsigned long stopBeepMillis = 0;
+ if (stopBeepMillis && millis() >= stopBeepMillis) {
+ beepStateRequested = false;
+ stopBeepMillis = 0;
+ }
+ if (beepStateRequested != beepState && beepStateRequested) {
+ stopBeepMillis = millis() + beepLengthRequested;
+ }
+ if (buzzer_passive) {
+ static unsigned long nextmillis;
+ if (beepStateRequested != beepState) {
+ beepState = beepStateRequested;
+ nextmillis = 0;
+ }
+ if (beepState) {
+ if (millis() > nextmillis) {
+ doBeep(beepToneRequested, beepMs);
+ nextmillis = millis() + beepMs + silentMs;
+ }
+ }
+ } else {
+ static unsigned long nextonmillis;
+ static unsigned long nextoffmillis;
+ if (beepStateRequested != beepState) {
+ beepState = beepStateRequested;
+ nextonmillis = 0;
+ nextoffmillis = 0;
+ }
+ if (beepState) {
+ static unsigned pinState = 0;
+ if (millis() > nextonmillis) {
+ digitalWrite(buzzer_pin, HIGH);
+ nextoffmillis = nextonmillis + beepMs;
+ nextonmillis = nextonmillis + beepMs + silentMs;
+ } else if (millis() > nextoffmillis) {
+ digitalWrite(buzzer_pin, LOW);
+ nextoffmillis = nextonmillis;
+ }
+ } else {
+ digitalWrite(buzzer_pin, LOW);
+ }
+ }
+ }
+}
--- /dev/null
+#!/bin/sh
+
+PORT=/dev/ttyUSB1
+
+mkdir -p ./.data-release
+cp -r ./data/* ./.data-release/
+
+cat ui.yml | yq -c > ./.data-release/ui.json
+
+find ./.data-release/ -name "*~" -type f -delete
+find ./.data-release/web/ -type f -name "*.css" ! -name "*.min.*" -exec echo {} \; -exec uglifycss --output {}.min {} \; -exec rm {} \; -exec mv {}.min {} \;
+find ./.data-release/web/ -type f -name "*.js" ! -name "*.min.*" ! -name "vfs_fonts*" -exec echo {} \; -exec uglifyjs -o {}.min {} \; -exec rm {} \; -exec mv {}.min {} \;
+find ./.data-release/web -type f -exec gzip {} +
+
+mklittlefs -c ./.data-release littlefs.img -d 5 -b 8192 -p 256 -s 0xfb000
+esptool.py --port $PORT write_flash 0x300000 littlefs.img
+rm -r ./.data-release
--- /dev/null
+#include "Clock.h"
+
+bool isNetConnected = false;
+const char* SSID;
+const char* PSK;
+
+const char* apMsg1 PROGMEM = "Активирована точка доступа ";
+const char* apMsg2 PROGMEM = ", пароль ";
+const char* apMsg3 PROGMEM = ", IP-адрес ";
+
+void setupNet(bool AP) {
+ SSID = cfg.getCharValue(F("sta_ssid"));
+ PSK = cfg.getCharValue(F("sta_psk"));
+ if (SSID && SSID[0] && !AP) {
+ WiFi.mode(WIFI_STA);
+ WiFi.begin(SSID,PSK);
+ isApEnabled = false;
+ Serial.println(F("Режим беспроводного клиента"));
+ } else {
+ SSID = cfg.getCharValue(F("ap_ssid"));
+ PSK = cfg.getCharValue(F("ap_psk"));
+ if (!SSID || !SSID[0]) {
+ SSID = "WiFi Clock";
+ }
+ WiFi.mode(WIFI_AP);
+ WiFi.softAP(SSID,PSK);
+ Serial.println(F("Режим точки доступа"));
+ isApEnabled = true;
+ String IP = WiFi.softAPIP().toString();
+ char buf[256];
+ strcpy_P(buf, apMsg1);
+ strcat(buf, SSID);
+ if (PSK && PSK[0]) {
+ strcat_P(buf, apMsg2);
+ strcat(buf, PSK);
+ }
+ strcat_P(buf, apMsg3);
+ strcat(buf, IP.c_str());
+ message(buf);
+ }
+}
+
+void tickNet() {
+ if (isApEnabled) {
+ if (WiFi.status() == WL_CONNECTED && !isNetConnected) {
+ isNetConnected = true;
+ Serial.print(F("Выполнено подключение к сети ")); Serial.println(SSID);
+ message(F("Соединение установлено"));
+ } else if (WiFi.status() != WL_CONNECTED && isNetConnected) {
+ isNetConnected = false;
+ Serial.println(F("Сеть потеряна"));
+ message(F("Сеть потеряна"));
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+#include "Clock.h"
+#include <MD_Parola.h>
+#include <MD_MAX72xx.h>
+#include "fonts.h"
+
+#define FONT_NARROW 2
+
+int screenMode = mTime;
+int screenModeRequested = mTime;
+int currentPriority = 255;
+bool scrollFinished = true;
+char msgBuf[256]; // one-time messages
+char scrollBuf[256] = "- - - - -"; // retained
+
+const char mn1[] PROGMEM = "января";
+const char mn2[] PROGMEM = "февраля";
+const char mn3[] PROGMEM = "марта";
+const char mn4[] PROGMEM = "апреля";
+const char mn5[] PROGMEM = "мая";
+const char mn6[] PROGMEM = "июня";
+const char mn7[] PROGMEM = "июля";
+const char mn8[] PROGMEM = "августа";
+const char mn9[] PROGMEM = "сентября";
+const char mn10[] PROGMEM = "октября";
+const char mn11[] PROGMEM = "ноября";
+const char mn12[] PROGMEM = "декабря";
+
+const char *const months[] PROGMEM = {
+ mn1, mn2, mn3, mn4, mn5, mn6, mn7, mn8, mn9, mn10, mn11, mn12
+};
+
+const char day0[] PROGMEM = "Воскресенье";
+const char day1[] PROGMEM = "Понедельник";
+const char day2[] PROGMEM = "Вторник";
+const char day3[] PROGMEM = "Среда";
+const char day4[] PROGMEM = "Четверг";
+const char day5[] PROGMEM = "Пятница";
+const char day6[] PROGMEM = "Суббота";
+
+const char* const days[] PROGMEM = {
+ day0, day1, day2, day3, day4, day5, day6
+};
+
+MD_Parola* Panel = nullptr;
+
+void utf8rus(const char* source, char* target, int maxLen) {
+ int i = 0, k , idx =0;
+ unsigned char n;
+ k = strlen(source);
+ while (i < k) {
+ n = source[i]; i++;
+ if (n >= 0xC0) {
+ switch (n) {
+ case 0xD0: {
+ n = source[i]; i++;
+ if (n == 0x81) { n = 0xA8; break; }
+ if (n >= 0x90 && n <= 0xBF) n = n + 0x30;
+ break;
+ }
+ case 0xD1: {
+ n = source[i]; i++;
+ if (n == 0x91) { n = 0xB8; break; }
+ if (n >= 0x80 && n <= 0x8F) n = n + 0x70;
+ break;
+ }
+ case 0xC2: {
+ n = source[i]; i++;
+ if (n == 0xb0) { n = 0xf; break; } // degree sign, hack
+ break;
+ }
+ default: {
+ n = source[i]; i++;
+ }
+ }
+ }
+ target[idx++] = n;
+ if (idx>=maxLen) break;
+ }
+ target[idx] = 0;
+}
+
+void setPanelBrightness() {
+ if (Panel) {
+ int br;
+ if (isNight()) {
+ br = cfg.getIntValue(F("panel_brightness_night"));
+ } else {
+ br = cfg.getIntValue(F("panel_brightness_day"));
+ }
+ Panel->setIntensity(br);
+ }
+}
+
+void setupPanel() {
+ if (Panel) {
+ Panel->displayClear();
+ delete Panel;
+ Panel=nullptr;
+ }
+ int pin_din = cfg.getIntValue(F("pin_din"));
+ int pin_clk = cfg.getIntValue(F("pin_clk"));
+ int pin_cs = cfg.getIntValue(F("pin_cs"));
+ if (!pin_din || !pin_clk || !pin_cs) {
+ pin_din = D6;
+ pin_clk = D8;
+ pin_cs = D7;
+ }
+ int led_modules = cfg.getIntValue(F("led_modules"));
+ if (!led_modules) led_modules = 4;
+ Panel = new MD_Parola(MD_MAX72XX::FC16_HW,pin_din,pin_clk,pin_cs,led_modules);
+ Panel->begin();
+ Panel->setFont(RomanCyrillic);
+ unregisterTimeHandler(F("brightness"));
+ registerTimeHandler(F("brightness"),'h',setPanelBrightness);
+}
+
+int panelSpeed() {
+ int panel_speed = cfg.getIntValue(F("panel_speed"));
+ if (panel_speed>0 && panel_speed<=20) {
+ panel_speed = 500/panel_speed;
+ } else {
+ panel_speed = 50;
+ }
+ return panel_speed;
+}
+
+void drawScroll() {
+ Panel->displayClear();
+ Panel->setFont(RomanCyrillic);
+ Panel->displayScroll(scrollBuf, PA_CENTER, PA_SCROLL_LEFT, panelSpeed());
+ screenMode = mWeather;
+}
+
+void message(const char* str, int priority) {
+ if (priority > currentPriority) return;
+ currentPriority = priority;
+ utf8rus(str, msgBuf, 255);
+ Panel->displayClear();
+ Panel->setFont(RomanCyrillic);
+ Panel->displayScroll(msgBuf, PA_CENTER, PA_SCROLL_LEFT, panelSpeed());
+ screenModeRequested = mMessage;
+ screenMode = mMessage;
+}
+
+void message(const __FlashStringHelper* str, int priority) {
+ if (priority > currentPriority) return;
+ currentPriority = priority;
+ char buf[256];
+ strncpy_P(buf, (PGM_P)str, 255);
+ utf8rus(buf, msgBuf, 255);
+ Panel->displayClear();
+ Panel->setFont(RomanCyrillic);
+ Panel->displayScroll(msgBuf, PA_CENTER, PA_SCROLL_LEFT, panelSpeed());
+ screenModeRequested = mMessage;
+ screenMode = mMessage;
+}
+
+void messageModal(const char* str) {
+ message(str);
+ while (!(Panel->displayAnimate())) { delay(10); }
+}
+
+void messageModal(const __FlashStringHelper* str) {
+ message(str);
+ while (!(Panel->displayAnimate())) { delay(10); }
+}
+
+void scroll(const char* str, bool force) {
+ utf8rus(str, scrollBuf, 255);
+ if (force && currentPriority>5) {
+ drawScroll();
+ screenModeRequested = mWeather;
+ }
+}
+
+const char* templateSZ PROGMEM = "%02d%c%02d%c%02d";
+const char* templateSNZ PROGMEM = "%2d%c%02d%c%02d";
+const char* templateMZ PROGMEM = "%02d%c%02d";
+const char* templateMNZ PROGMEM = "%2d%c%02d";
+
+void displayTime(int hh, int mi, int ss, int dw, int dd, int mm, int yy, bool dots) {
+ char divider;
+ char templateStr[32];
+ Panel->setFont(fonts[
+ (cfg.getBoolValue(F("panel_seconds")) && (cfg.getIntValue(F("led_modules"))<6))
+ ?FONT_NARROW:cfg.getIntValue(F("panel_font")) ]);
+ if (screenMode !=mTime) {
+ screenModeRequested = mTime;
+ screenMode = mTime;
+ }
+ if (dots || !cfg.getBoolValue(F("flash_dots"))) {
+ divider = ':';
+ } else {
+ divider = ' ';
+ }
+ if (cfg.getBoolValue(F("panel_seconds"))) {
+ if (cfg.getBoolValue(F("panel_zero"))) {
+ strcpy_P(templateStr,templateSZ);
+ } else {
+ strcpy_P(templateStr,templateSNZ);
+ }
+ } else {
+ if (cfg.getBoolValue(F("panel_zero"))) {
+ strcpy_P(templateStr,templateMZ);
+ } else {
+ strcpy_P(templateStr,templateMNZ);
+ }
+ }
+ snprintf(msgBuf, 255, templateStr, hh, divider, mi, divider, ss);
+ Panel->displayText(msgBuf, PA_CENTER,100,0,PA_NO_EFFECT,PA_NO_EFFECT);
+ Panel->displayAnimate();
+ scrollFinished = true;
+}
+
+void displayTime() {
+
+ static bool newsec = false;
+ static bool newsec06 = false;
+ static int prevss;
+ static unsigned long prevmilli;
+
+ if (ss != prevss) {
+ prevmilli=millis();
+ prevss = ss;
+ newsec = true;
+ newsec06 = true;
+ }
+
+ if (newsec) {
+ displayTime(hh,mi,ss,dw,dd,mm,yy,true);
+ newsec = false;
+ } else {
+ int delta = millis() - prevmilli;
+ if (delta>600 && newsec06) {
+ displayTime(hh,mi,ss,dw,dd,mm,yy,false);
+ newsec06 = false;
+ }
+ }
+
+}
+
+void displayDate(int dd, int mm, int yy) {
+
+ String msg;
+
+ if (screenMode !=mDate) {
+ screenMode = mDate;
+
+ PGM_P day = days[dw];
+ PGM_P month = months[mm];
+
+ sprintf(msgBuf,"%s, %02d %s %04d г",day, dd, month, yy);
+ utf8rus(msgBuf, msgBuf, 255);
+ Panel->displayClear();
+ Panel->setFont(RomanCyrillic);
+
+ Panel->displayScroll(msgBuf, PA_CENTER, PA_SCROLL_LEFT, panelSpeed());
+ }
+}
+
+void displayDate() {
+ displayDate(dd,mm,yy);
+}
+
+void tickPanel() {
+ if (screenMode == mTime || screenModeRequested != screenMode) { // needs to redraw
+ if (now<1668332133) { message(F("Время еще неизвестно...")); }
+ switch (screenModeRequested) {
+ case mDefault:
+ case mBarrier:
+ case mLast:
+ case mTime:
+ displayTime();
+ break;
+ case mDate:
+ displayDate();
+ break;
+ case mWeather:
+ drawScroll();
+ break;
+ case mMessage:
+ break;
+ }
+ } else if (Panel->displayAnimate()) {
+ screenModeRequested = mTime;
+ currentPriority = 255;
+ }
+}
+
+
--- /dev/null
+#include "Clock.h"
+#include <time.h>
+#include <coredecls.h>
+
+bool isTimeSet = false;
+time_t now;
+time_t last_sync;
+
+int hh;
+int mi;
+int ss;
+
+int dw;
+
+int dd;
+int mm;
+int yy;
+
+#define maxTimeHandlers 8
+const char* tHandlerNames[maxTimeHandlers];
+char tHandlerTypes[maxTimeHandlers];
+std::function<void()> tTimeHandlers[maxTimeHandlers];
+
+void timeIsSet(bool ntp) {
+ if (ntp) {
+ Serial.println(F("Время синхронизировано"));
+ message(F("Время синхронизировано"));
+ reportMessage(F("Время синхронизировано"));
+ if (isRTCEnabled) {
+ RTC.adjust(DateTime(now));
+ }
+ }
+ isTimeSet = true;
+ last_sync = now;
+}
+
+void setupHandlers() {
+ for (int i=0; i<maxTimeHandlers; i++) {
+ tHandlerNames[i] = nullptr;
+ tHandlerTypes[i] = ' ';
+ tTimeHandlers[i] = nullptr;
+ }
+}
+
+void setupTime() {
+ configTime(cfg.getCharValue(F("tz")),cfg.getCharValue(F("ntp_server")));
+ settimeofday_cb(&timeIsSet);
+}
+
+void registerTimeHandler(const char* handlerName, const char handlerType, std::function<void()> timeHandler) {
+ for (int i=0; i<maxTimeHandlers; i++) {
+ if (!tHandlerNames[i]) {
+ // empty slot found!
+ tHandlerNames[i] = handlerName;
+ tHandlerTypes[i] = handlerType;
+ tTimeHandlers[i] = timeHandler;
+ break;
+ }
+ }
+}
+
+void registerTimeHandler(const __FlashStringHelper* handlerName, const char handlerType, std::function<void()> timeHandler) {
+ for (int i=0; i<maxTimeHandlers; i++) {
+ if (!tHandlerNames[i]) {
+ // empty slot found!
+ tHandlerNames[i] = copystr(handlerName);
+ tHandlerTypes[i] = handlerType;
+ tTimeHandlers[i] = timeHandler;
+ break;
+ }
+ }
+}
+
+void unregisterTimeHandler(const char* handlerName) {
+ for (int i=0; i<maxTimeHandlers; i++) {
+ if (tHandlerNames[i] && strcmp(tHandlerNames[i],handlerName) == 0) {
+ tHandlerNames[i] = nullptr;
+ tHandlerTypes[i] = ' ';
+ tTimeHandlers[i] = nullptr;
+ break;
+ }
+ }
+}
+
+void unregisterTimeHandler(const __FlashStringHelper* handlerName) {
+ for (int i=0; i<maxTimeHandlers; i++) {
+ if (tHandlerNames[i] && strcmp_P(tHandlerNames[i],(PGM_P)handlerName) == 0) {
+ tHandlerNames[i] = nullptr;
+ tHandlerTypes[i] = ' ';
+ tTimeHandlers[i] = nullptr;
+ break;
+ }
+ }
+}
+
+void runHandlers(char handlerType) {
+ for (int i=0; i<maxTimeHandlers; i++) {
+ if (tTimeHandlers[i] && tHandlerTypes[i]==handlerType) {
+ tTimeHandlers[i]();
+ }
+ }
+}
+
+void tickTime() {
+ static int prevD = 0, prevH = 0, prevM = 0, prevS = 0;
+ time(&now);
+ struct tm* timeinfo = localtime(&now);
+ hh = timeinfo->tm_hour;
+ mi = timeinfo->tm_min;
+ ss = timeinfo->tm_sec;
+ dw = timeinfo->tm_wday;
+ dd = timeinfo->tm_mday;
+ mm = timeinfo->tm_mon+1;
+ yy = timeinfo->tm_year+1900;
+ if (dd != prevD) {
+ prevD = dd;
+ runHandlers('d');
+ }
+ if (hh != prevH) {
+ prevH = hh;
+ runHandlers('h');
+ }
+ if (mi != prevM) {
+ prevM = mi;
+ runHandlers('m');
+ }
+ if (ss != prevS) {
+ prevS = ss;
+ runHandlers('s');
+ }
+}
+
+bool isNight() {
+ int day_from = cfg.getIntValue(F("day_from"));
+ int night_from = cfg.getIntValue(F("night_from"));
+ if (day_from<night_from) { // night ... day ... night
+ return hh<day_from || hh>=night_from;
+ } else { /// late day ... night .... day till mindnight
+ return (hh>=night_from && hh<day_from);
+ }
+}
+
--- /dev/null
+project:
+ name: WiFi Clock
+ version: 0.2.0
+ contacts:
+ - mailto:rvb@rvb.name
+ - tg:rvbglas
+ - https://git.rvb.name
+ - xmpp:rvb@rvb.name
+pages:
+ - id: main
+ title: "Главная"
+ icon: ""
+ elements:
+ - type: table
+ label: Имя устройства
+ value: WiFi Clock
+ - type: hr
+ - id: _weather
+ type: table
+ label: Погода
+ color: lightblue
+ - type: hr
+ - id: _time
+ type: table
+ label: Время
+ color: green
+ - id: _date
+ type: table
+ label: Дата
+ color: green
+ - id: _last_sync
+ type: table
+ label: Синхронизировано
+ - type: hr
+ - id: _uptime
+ type: table
+ label: Uptime
+ - id: _mac
+ type: table
+ label: MAC-адрес
+ - id: _rssi
+ type: table
+ label: Уровень сигнала
+ - id: _heap
+ type: table
+ label: Свободная память
+ - id: clock
+ title: Часы
+ icon: ""
+ elements:
+ - type: text
+ value: Внешний вид
+ - id: panel_font
+ label: Шрифт часов
+ type: select
+ options:
+ - value: 0
+ text: Стандартный
+ - value: 1
+ text: Высокий
+ - value: 2
+ text: Узкий
+ - value: 3
+ text: Жирный
+ - value: 4
+ text: Жирный прямой
+ - value: 5
+ text: Синклер
+ - id: panel_seconds
+ label: Секунды
+ type: checkbox
+ - id: panel_zero
+ label: Ведущий ноль
+ type: checkbox
+ - id: flash_dots
+ label: Мигать точками
+ type: checkbox
+ - id: panel_speed
+ label: Скорость
+ type: range
+ min: 1
+ max: 20
+ step: 1
+ - type: hr
+ - type: text
+ value: Яркость
+ - id: day_from
+ label: Дневной режим с
+ type: number
+ min: 0
+ max: 24
+ step: 1
+ - id: night_from
+ label: Ночной режим с
+ type: number
+ min: 0
+ max: 24
+ step: 1
+ - id: panel_brightness_day
+ label: Яркость днем
+ type: range
+ min: 0
+ max: 14
+ step: 1
+ - id: panel_brightness_night
+ label: Яркость ночью
+ type: range
+ min: 0
+ max: 14
+ step: 1
+ - id: alarm
+ title: Будильник
+ icon: ""
+ elements:
+ - type: text
+ value: Будильник
+ - id: enable_alarm
+ type: checkbox
+ label: Включить
+ - id: alarm_hour
+ label: Час
+ type: number
+ min: 0
+ max: 23
+ step: 1
+ - id: alarm_minute
+ label: Минуты
+ type: number
+ min: 0
+ max: 59
+ step: 1
+ - id: alarm_days
+ label: Дни
+ type: week
+ - type: hr
+ - id: alarm_length
+ label: Длительность сигнала, с
+ type: number
+ min: 5
+ max: 120
+ step: 5
+ - id: alarm_tone
+ label: Тон сигнала
+ type: number
+ min: 100
+ max: 5000
+ - id: alarm_beep_ms
+ label: Длительность гудка, мс
+ type: number
+ min: 100
+ max: 2000
+ - id: alarm_silent_ms
+ label: Длительность паузы, мс
+ type: number
+ min: 100
+ max: 2000
+ - type: hr
+ - type: text
+ value: Сигнал каждый час
+ - id: enable_hourly
+ type: checkbox
+ label: Включить
+ - id: hourly_night
+ label: В том числе ночью
+ type: checkbox
+ - id: hourly_count
+ label: Число сигналов
+ type: number
+ min: 1
+ max: 5
+ - id: hourly_tone
+ label: Тон сигнала
+ type: number
+ min: 100
+ max: 5000
+ - id: hourly_beep_ms
+ label: Длительность гудка, мс
+ type: number
+ min: 100
+ max: 2000
+ - id: hourly_silent_ms
+ label: Длительность паузы, мс
+ type: number
+ min: 100
+ max: 2000
+ - id: hw
+ title: Оборудование
+ icon: ""
+ elements:
+ - type: text
+ value: Часы реального времени
+ - id: enable_rtc
+ label: Использовать
+ type: checkbox
+ - id: pin_sda
+ label: Пин SDA
+ type: select
+ options:
+ - value: 16
+ text: D0
+ - value: 5
+ text: D1
+ - value: 4
+ text: D2
+ - value: 0
+ text: D3
+ - value: 2
+ text: D4
+ - value: 14
+ text: D5
+ - value: 12
+ text: D6
+ - value: 13
+ text: D7
+ - value: 15
+ text: D8
+ - value: 3
+ text: "RX*"
+ - value: 1
+ text: "TX*"
+ - id: pin_scl
+ label: Пин SCL
+ type: select
+ options:
+ - value: 16
+ text: D0
+ - value: 5
+ text: D1
+ - value: 4
+ text: D2
+ - value: 0
+ text: D3
+ - value: 2
+ text: D4
+ - value: 14
+ text: D5
+ - value: 12
+ text: D6
+ - value: 13
+ text: D7
+ - value: 15
+ text: D8
+ - value: 3
+ text: "RX*"
+ - value: 1
+ text: "TX*"
+ - id: i2c_speed
+ label: Скорость шины I2C
+ type: select
+ options:
+ - value: 100000
+ text: 100000 (стандартная)
+ - value: 400000
+ text: 400000 (быстрая)
+ - type: hr
+ - type: text
+ value: Кнопка
+ - id: enable_button
+ label: Использовать
+ type: checkbox
+ - id: button_pin
+ label: Пин кнопки
+ type: select
+ options:
+ - value: 16
+ text: D0
+ - value: 5
+ text: D1
+ - value: 4
+ text: D2
+ - value: 0
+ text: D3
+ - value: 2
+ text: D4
+ - value: 14
+ text: D5
+ - value: 12
+ text: D6
+ - value: 13
+ text: D7
+ - value: 15
+ text: D8
+ - value: 3
+ text: "RX*"
+ - value: 1
+ text: "TX*"
+ - id: button_inversed
+ label: Кнопка на размыкание
+ type: checkbox
+ - type: hr
+ - type: text
+ value: Зуммер
+ - id: enable_buzzer
+ label: Использовать
+ type: checkbox
+ - id: buzzer_pin
+ label: Пин зуммера
+ type: select
+ options:
+ - value: 16
+ text: D0
+ - value: 5
+ text: D1
+ - value: 4
+ text: D2
+ - value: 0
+ text: D3
+ - value: 2
+ text: D4
+ - value: 14
+ text: D5
+ - value: 12
+ text: D6
+ - value: 13
+ text: D7
+ - value: 15
+ text: D8
+ - value: 3
+ text: "RX*"
+ - value: 1
+ text: "TX*"
+ - id: buzzer_passive
+ label: Пассивный зуммер
+ type: checkbox
+ - type: hr
+ - type: text
+ value: Светодиодная панель
+ - id: pin_din
+ label: Пин DIN
+ type: select
+ options:
+ - value: 16
+ text: D0
+ - value: 5
+ text: D1
+ - value: 4
+ text: D2
+ - value: 0
+ text: D3
+ - value: 2
+ text: D4
+ - value: 14
+ text: D5
+ - value: 12
+ text: D6
+ - value: 13
+ text: D7
+ - value: 15
+ text: D8
+ - value: 3
+ text: "RX*"
+ - value: 1
+ text: "TX*"
+ - id: pin_clk
+ label: Пин CLK
+ type: select
+ options:
+ - value: 16
+ text: D0
+ - value: 5
+ text: D1
+ - value: 4
+ text: D2
+ - value: 0
+ text: D3
+ - value: 2
+ text: D4
+ - value: 14
+ text: D5
+ - value: 12
+ text: D6
+ - value: 13
+ text: D7
+ - value: 15
+ text: D8
+ - value: 3
+ text: "RX*"
+ - value: 1
+ text: "TX*"
+ - id: pin_cs
+ label: Пин CS
+ type: select
+ options:
+ - value: 16
+ text: D0
+ - value: 5
+ text: D1
+ - value: 4
+ text: D2
+ - value: 0
+ text: D3
+ - value: 2
+ text: D4
+ - value: 14
+ text: D5
+ - value: 12
+ text: D6
+ - value: 13
+ text: D7
+ - value: 15
+ text: D8
+ - value: 3
+ text: "RX*"
+ - value: 1
+ text: "TX*"
+ - id: led_modules
+ label: Число модулей 8*8
+ type: number
+ min: 4
+ max: 8
+ - id: weather
+ title: Погода
+ icon: ""
+ elements:
+ - id: enable_weather
+ label: Использовать погодный сервис
+ type: checkbox
+ - id: weather_url
+ label: URL погодного сервиса
+ type: input
+ - id: weather_template
+ label: Шаблон вывода
+ type: input
+ - id: weather_min
+ label: Частота обновления, мин
+ type: number
+ min: 5
+ - id: wifi
+ title: WiFi
+ icon: ""
+ elements:
+ - type: text
+ value: Клиент сети
+ - id: sta_ssid
+ type: input-wifi
+ label: 🗘Имя беспроводной сети
+ pattern: ^[^!#;+\]/"\t][^+\]/"\t]{0,31}$
+ - id: sta_psk
+ type: password
+ label: 🗘Ключевая фраза
+ - id: sta_wait
+ type: input
+ label: Ожидать подключения не дольше, с
+ - type: hr
+ - type: text
+ value: Точка доступа
+ - id: ap_ssid
+ type: input
+ label: 🗘Имя беспроводной сети
+ pattern: ^[^!#;+\]/"\t][^+\]/"\t]{0,31}$
+ - id: ap_psk
+ type: password
+ label: 🗘Ключевая фраза
+ - id: wifi
+ type: button
+ label: Применить настройки WiFi
+ - id: system
+ title: Система
+ icon: ""
+ elements:
+ - type: text
+ value: Авторизация
+ - id: auth_user
+ type: input
+ label: Логин
+ pattern: ^[A-Za-z0-9]{3,16}$
+ - id: auth_pwd
+ label: Пароль
+ type: password
+ pattern: ^.{4,16}$
+ - id: auth
+ type: button
+ label: Сменить пароль
+ - type: hr
+ - type: text
+ value: Конфигурация
+ - id: _config
+ type: config
+ label: Сохранение и восстановление настроек
+ - type: hr
+ - type: text
+ value: Синхронизация времени
+ - id: _timeset
+ label: Установить время вручную
+ type: timeset
+ - id: ntp_server
+ type: input
+ label: NTP-сервер
+ pattern: ^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$
+ - id: tz
+ type: input
+ label: Часовой пояс (POSIX)
+ pattern: ^([A-Za-z]*)(-?[0-9]*(:[0-9]*)?)((([A-Za-z]*)(-?[0-9]*)?)?(,M[0-9]*\.[0-9]*\.[0-9]*/([0-9]*:?){3}){2})?$
+ - id: restart
+ type: button
+ label: Перезагрузить
--- /dev/null
+#include "Clock.h"
+#include "AsyncHTTPRequest_Generic.h"
+#include <Ticker.h>
+#include <ArduinoJson.h>
+
+Ticker* weatherTicker = nullptr;
+AsyncHTTPRequest request;
+Config weather;
+char weatherData[256];
+
+void executeWeatherRequest();
+
+void IterateJson(JsonVariant json, const String& rootName, Config& lCfg);
+
+void IterateJsonObject(JsonObject json, String rootName, Config& lCfg) {
+ for (JsonPair kv : json) {
+ IterateJson(kv.value(), rootName == "" ? String(kv.key().c_str()) : rootName + "." + String(kv.key().c_str()),lCfg);
+ }
+}
+
+void IterateJsonArray(JsonArray json, const String& rootName, Config& lCfg) {
+ for (int i = 0; i < json.size(); i++) {
+ IterateJson(json[i], rootName == "" ? "[" + String(i) + "]" : rootName + "." + "[" + String(i) + "]", lCfg);
+ }
+}
+
+void IterateJson(JsonVariant json, const String& rootName, Config& lCfg) {
+ if (json.is<JsonObject>()) {
+ IterateJsonObject(json.as<JsonObject>(), rootName, lCfg);
+ } else if (json.is<JsonArray>()) {
+ IterateJsonArray(json.as<JsonArray>(), rootName, lCfg);
+ } else {
+ lCfg.setValue(rootName.c_str(), json.as<String>().c_str());
+ }
+}
+
+void processTemplates(char* buf, const char* strTemplate, const Config& cfg, int maxLen) {
+ buf[0] = 0;
+ char varName[34];
+ const char* ptr = strTemplate;
+ while (true) {
+ char* pos = strchr(ptr, '%');
+ if (!pos) {
+ strncat(buf, ptr, maxLen);
+ break;
+ }
+ int len = pos - ptr;
+ if (len + strlen(buf) >= maxLen) {
+ len = maxLen - strlen(buf);
+ }
+ int bufLen = strlen(buf);
+ strncpy(buf + bufLen, ptr, len);
+ buf[bufLen+len] = 0;
+ char* endpos = strchr(pos+1, '%');
+ if (!endpos) {
+ Serial.println(F("Незавершенное имя переменной"));
+ break;
+ }
+ int varLen = (endpos-1) - (pos);
+ if (varLen>64) {
+ Serial.println(F("Имя переменной слишком длинное"));
+ break;
+ }
+ if (varLen) {
+ strncpy(varName,pos+1,varLen);
+ varName[varLen] = 0;
+ const char* value = cfg.getCharValue(varName);
+ if (value) {
+ strncat(buf, value, maxLen);
+ } else {
+ strncat(buf, "??", maxLen);
+ }
+ } else {
+ strncat(buf, "%", maxLen);
+ }
+ ptr = endpos+1;
+ }
+}
+
+void requestCB(void* optParm, AsyncHTTPRequest* request, int readyState) {
+ const char* weather_template = cfg.getCharValue(F("weather_template"));
+
+ if (readyState == readyStateDone) {
+ if (request->responseHTTPcode() == 200) {
+ String weather_json = request->responseText();
+ DynamicJsonDocument* current_weather = new DynamicJsonDocument(2048);
+ DeserializationError error = deserializeJson(*current_weather, weather_json);
+ if (error) {
+ Serial.print(F("Ошибка разбора ответа: "));
+ Serial.println(error.c_str());
+ Serial.println(weather_json);
+ reportMessage(F("Ошибка обновления погоды"));
+ return;
+ }
+ weather.clear();
+ IterateJson(current_weather->as<JsonObject>(), "", weather);
+ weather_json = String();
+ delete current_weather;
+ processTemplates(weatherData,weather_template,weather,255);
+ sendWeather();
+ reportMessage(F("Погода обновлена"));
+ scroll(weatherData, !isNight());
+ }
+ }
+}
+
+void weatherRequest() {
+ if (!WiFi.isConnected()) { return; }
+ if (WiFi.isConnected() && weatherTicker) {
+ weatherTicker->detach();
+ int weather_min = cfg.getIntValue(F("weather_min"));
+ weather_min = weather_min ? weather_min : 60;
+ weatherTicker->attach(weather_min * 60, executeWeatherRequest); // reschedule to requested interval
+ }
+ static bool requestOpenResult;
+ if (request.readyState() == readyStateUnsent || request.readyState() == readyStateDone) {
+ const char* weather_url = cfg.getCharValue(F("weather_url"));
+ requestOpenResult = request.open("GET", weather_url);
+
+ if (requestOpenResult) {
+ request.send();
+ } else {
+ Serial.println(F("Не удалось отправить запрос"));
+ }
+ } else {
+ Serial.println(F("Не удалось отправить запрос"));
+ }
+}
+
+void executeWeatherRequest() {
+ if (cfg.getBoolValue(F("enable_weather"))) {
+ request.onReadyStateChange(requestCB);
+ weatherRequest();
+ }
+}
+
+void setupWeatherRequest() {
+ if (weatherTicker) {
+ weatherTicker->detach();
+ delete weatherTicker;
+ weatherTicker = nullptr;
+ }
+ if (cfg.getBoolValue(F("enable_weather"))) {
+ weatherTicker = new Ticker;
+ weatherTicker->attach(30, executeWeatherRequest); // before connection - every 30s
+ }
+}
\ No newline at end of file
--- /dev/null
+#include "Clock.h"
+#include <LittleFS.h>
+#include <ESPAsyncWebServer.h>
+#include <StreamString.h>
+#include <Ticker.h>
+#include "ArduinoJson.h"
+#include "AsyncJson.h"
+
+bool isApEnabled = false;
+bool isWebStarted = false;
+
+bool pendingWiFi = false;
+bool pendingAuth = false;
+
+AsyncWebServer server(80);
+AsyncEventSource events("/events");
+
+Ticker tKeepalive;
+
+char auth_user[32];
+char auth_pwd[32];
+
+void reportChange(const __FlashStringHelper* name) {
+ char buf[256];
+ ConfigParameter* param = cfg.getParam(name);
+ if (param) {
+ switch (param->getType()) {
+ case 'B':
+ sprintf(buf,"{\"%s\":%s}", name, param->getBoolValue()?"true":"false");
+ break;
+ case 'I':
+ sprintf(buf,"{\"%s\":%d}", name, param->getIntValue());
+ break;
+ case 'F':
+ sprintf(buf,"{\"%s\":%f}", name, param->getFloatValue());
+ break;
+ case 'S':
+ sprintf(buf,"{\"%s\":\"%s\"}", name, param->getCharValue());
+ break;
+ }
+ events.send(buf,"update",millis());
+ }
+}
+
+void reportMessage(const __FlashStringHelper* msg) {
+ char buf[256];
+ strcpy_P(buf, (PGM_P)msg);
+ events.send(buf,"message",millis());
+}
+
+void sendInitial(AsyncEventSourceClient *client) {
+ String mac = WiFi.macAddress();
+ char buf[256];
+ sprintf(buf,"{\"_mac\":\"%s\",\"_weather\":\"%s\"}", mac.c_str(), weatherData);
+ client->send(buf,"update",millis());
+ mac = String();
+}
+
+void sendWeather() {
+ char buf[256];
+ sprintf(buf,"{\"_weather\":\"%s\"}",weatherData);
+ events.send(buf,"update",millis());
+}
+
+void sendKeepalive() {
+ static unsigned long lastMillis = 0;
+ static unsigned long uptimeCorrection = 0;
+ unsigned long currentMillis = millis();
+ unsigned long uptime = millis()/1000;
+ if (currentMillis < lastMillis) {
+ uptimeCorrection += lastMillis/1000;
+ }
+ lastMillis = millis();
+ uptime = uptime + uptimeCorrection;
+ int days = uptime / 86400;
+ uptime = uptime % 86400;
+ int hrs = uptime / 3600;
+ uptime = uptime % 3600;
+ int mins = uptime / 60;
+ uptime = uptime % 60;
+ int heap = ESP.getFreeHeap();
+ int rssi = WiFi.RSSI();
+ struct tm* timeinfo = localtime(&last_sync);
+ bool changed = cfg.getTimestamp() != 0;
+ char sync[16] = "--:--:--";
+ if (last_sync) {
+ sprintf(sync, "%02d:%02d:%02d", timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
+ }
+ char buf[256];
+ if (days) {
+ sprintf(buf,"{\"_uptime\":\"%d д %d ч %d % м %d с\", \"_date\":\"%02d.%2d.%04d\", \"_time\":\"%02d:%02d\",\"_heap\":\"%d б\", \"_rssi\":\"%d\", \"_last_sync\":\"%s\", \"_changed\":%s}", days, hrs, mins, uptime, dd, mm, yy, hh, mi, heap, rssi, sync, changed?"true":"false");
+ } else if (hrs) {
+ sprintf(buf,"{\"_uptime\":\"%d ч %d % м %d с\", \"_date\":\"%02d.%2d.%04d\", \"_time\":\"%02d:%02d\",\"_heap\":\"%d б\", \"_rssi\":\"%d\", \"_last_sync\":\"%s\", \"_changed\":%s}", hrs, mins, uptime, dd, mm, yy, hh, mi, heap, rssi, sync, changed?"true":"false");
+ } else {
+ sprintf(buf,"{\"_uptime\":\"%d м %d с\", \"_date\":\"%02d.%2d.%04d\", \"_time\":\"%02d:%02d\",\"_heap\":\"%d б\", \"_rssi\":\"%d\", \"_last_sync\":\"%s\", \"_changed\":%s}", mins, uptime, dd, mm, yy, hh, mi, heap, rssi, sync, changed?"true":"false");
+ }
+ events.send(buf,"update",millis());
+}
+
+void apply(const char* name) {
+ if (strcmp(name,"sta_ssid") == 0 || strcmp(name,"sta_psk") == 0) {
+ if (!isApEnabled) {
+ pendingWiFi = true;
+ }
+ } else if (strcmp(name,"ap_ssid") == 0 || strcmp(name,"ap_psk") == 0) {
+ if (isApEnabled) {
+ pendingWiFi = true;
+ }
+ } else if (strcmp(name,"auth_user") == 0 || strcmp(name,"auth_pwd") == 0) {
+ pendingAuth = true;
+ } else if (strcmp(name,"ntp_server") == 0 || strcmp(name,"tz") == 0) {
+ setupTime();
+ } else if (strcmp(name,"pin_sda") == 0 || strcmp(name,"pin_scl") == 0 ||
+ strcmp(name,"i2c_speed") == 0 || strcmp(name,"enable_rtc") == 0 ||
+ strcmp(name,"enable_button") == 0 || strcmp(name,"button_pin") == 0 || strcmp(name,"button_inversed") == 0 ||
+ strcmp(name,"enable_buzzer") == 0 || strcmp(name,"buzzer_pin") == 0) {
+ setupHardware();
+ } else if (strcmp(name,"pin_din") == 0 || strcmp(name,"pin_clk") == 0 ||
+ strcmp_P(name,"pin_cs") == 0 || strcmp(name,"led_modules") == 0){
+ setupPanel();
+ } else if (strcmp(name,"panel_brightness_day") == 0 || strcmp_P(name,"panel_brightness_night") == 0 ||
+ strcmp_P(name,"day_from") == 0 || strcmp(name,"night_from") == 0){
+ setPanelBrightness();
+ }
+}
+
+char* actionScheduled = nullptr;
+unsigned long millisScheduled = 0;
+
+void setupWeb() {
+ char buf[256];
+
+ if (isWebStarted) {
+ tKeepalive.detach();
+ server.end();
+ }
+
+ isWebStarted = true;
+
+ strncpy(auth_user,cfg.getCharValue(F("auth_user")),31);
+ strncpy(auth_pwd,cfg.getCharValue(F("auth_pwd")),31);
+
+ server.on("/action", HTTP_GET, [](AsyncWebServerRequest* request) {
+ if (auth_user && auth_pwd && auth_user[0] && auth_pwd[0] && !request->authenticate(auth_user, auth_pwd)) {
+ return request-> requestAuthentication();
+ }
+ if(request->hasParam("name")) {
+ const char* action = request->getParam("name")->value().c_str();
+ if (strcmp(action,"reset") == 0) {
+ millisScheduled = millis();
+ actionScheduled = "reset";
+ request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Сбрасываю настройки и перезагружаюсь\"}");
+ } else if (strcmp(action,"restart") == 0) {
+ if (pendingWiFi) {
+ request->send(200,"application/json", "{\"result\":\"FAILED\",\"message\":\"Не применены настройки WiFi\", \"page\":\"wifi\"}");
+ } else if (pendingAuth) {
+ request->send(200,"application/json", "{\"result\":\"FAILED\",\"message\":\"Не применены настройки авторизации\", \"page\":\"system\"}");
+ } else {
+ request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Перезагружаюсь\"}");
+ millisScheduled = millis();
+ actionScheduled = "restart";
+ }
+ } else if (strcmp(action,"wifi") == 0) {
+ if (pendingAuth) {
+ request->send(200,"application/json", "{\"result\":\"FAILED\",\"message\":\"Не применены настройки авторизации\", \"page\":\"system\"}");
+ } else {
+ request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Применяю настройки\"}");
+ millisScheduled = millis();
+ actionScheduled = "wifi";
+ }
+ } else if (strcmp(action,"auth") == 0) {
+ request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Применяю настройки\"}");
+ millisScheduled = millis();
+ actionScheduled = "auth";
+ } else if (strcmp(action,"save") == 0) {
+ if (pendingWiFi) {
+ request->send(200,"application/json", "{\"result\":\"FAILED\",\"message\":\"Не применены настройки WiFi\", \"page\":\"wifi\"}");
+ } else if (pendingAuth) {
+ request->send(200,"application/json", "{\"result\":\"FAILED\",\"message\":\"Не применены настройки авторизации\", \"page\":\"system\"}");
+ } else {
+ request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Сохраняю настройки\"}");
+ millisScheduled = millis();
+ actionScheduled = "save";
+ }
+ } else if (strcmp(action,"time") == 0) {
+ if(request->hasParam("timestamp")) {
+ unsigned long timestamp = atoi(request->getParam("timestamp")->value().c_str());
+ if (timestamp) {
+ timeval tv = { timestamp, 0 };
+ settimeofday(&tv, nullptr);
+ if (isRTCEnabled) {
+ Serial.println(F("Время установлено вручную"));
+ RTC.adjust(DateTime(timestamp));
+ }
+ }
+ request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Устанавливаю время\"}");
+ } else {
+ request->send(500, "text/plain", "{\"result\":\"FAILED\",\"message\":\"Not all parameters set\"}");
+ }
+ } else {
+ request->send(500, "text/plain", "{\"result\":\"FAILED\",\"message\":\"Unsupported action\"}");
+ }
+ } else {
+ request->send(500, "text/plain", "{\"result\":\"FAILED\",\"message\":\"Not all parameters set\"}");
+ }
+ });
+
+ server.on("/wifi/scan", HTTP_GET, [](AsyncWebServerRequest* request) {
+ if (auth_user && auth_pwd && auth_user[0] && auth_pwd[0] && !request->authenticate(auth_user, auth_pwd)) {
+ return request-> requestAuthentication();
+ }
+ String json = "[";
+ int n = WiFi.scanComplete();
+ if (n == -2) {
+ WiFi.scanNetworks(true);
+ } else if (n) {
+ for (int i = 0; i < n; ++i) {
+ if (i) json += ",";
+ json += "{";
+ json += "\"rssi\":" + String(WiFi.RSSI(i));
+ json += ",\"ssid\":\"" + WiFi.SSID(i) + "\"";
+ json += ",\"bssid\":\"" + WiFi.BSSIDstr(i) + "\"";
+ json += ",\"channel\":" + String(WiFi.channel(i));
+ json += ",\"secure\":" + String(WiFi.encryptionType(i));
+ json += ",\"hidden\":" + String(WiFi.isHidden(i) ? "true" : "false");
+ json += "}";
+ }
+ WiFi.scanDelete();
+ if (WiFi.scanComplete() == -2) {
+ WiFi.scanNetworks(true);
+ }
+ }
+ json += "]";
+ request->send(200, "application/json", json);
+ json = String();
+ });
+
+ server.on("/config/get", HTTP_GET, [](AsyncWebServerRequest* request) {
+ if (auth_user && auth_pwd && auth_user[0] && auth_pwd[0] && !request->authenticate(auth_user, auth_pwd)) {
+ return request-> requestAuthentication();
+ }
+ AsyncResponseStream* s = request->beginResponseStream("application/json");
+ s->print("{");
+ for (int i = 0; i < cfg.getParametersCount(); i++) {
+ ConfigParameter* param = cfg.getParameter(i);
+ if (i) s->print(",");
+ s->print("\"");
+ s->print(param->getID());
+ s->print("\":");
+ switch (param->getType()) {
+ case 'B':
+ s->print(param->getBoolValue() ? "true" : "false");
+ break;
+ case 'I':
+ s->print(param->getIntValue());
+ break;
+ case 'F':
+ s->print(param->getFloatValue());
+ break;
+ case 'S':
+ s->print("\"");
+ s->print(param->getCharValue());
+ s->print("\"");
+ break;
+ }
+ }
+ s->print("}");
+ request->send(s);
+ });
+
+ server.on("/config/set", HTTP_GET, [](AsyncWebServerRequest* request) {
+ if (auth_user && auth_pwd && auth_user[0] && auth_pwd[0] && !request->authenticate(auth_user, auth_pwd)) {
+ return request-> requestAuthentication();
+ }
+ if(request->hasParam("name") && (request->hasParam("value"))) {
+ const char* name = request->getParam("name")->value().c_str();
+ const char* value = request->getParam("value")->value().c_str();
+ Serial.print(name); Serial.print(" = "); Serial.println(value);
+ char buf[256];
+ ConfigParameter* param = cfg.getParam(name);
+ if (param) {
+ switch (param->getType()) {
+ case 'B':
+ cfg.setValue(name, strcmp(value, "true")==0);
+ sprintf(buf,"{\"%s\":%s}", name, param->getBoolValue()?"true":"false");
+ break;
+ case 'I':
+ cfg.setValue(name, atoi(value));
+ sprintf(buf,"{\"%s\":%d}", name, param->getIntValue());
+ break;
+ case 'F':
+ cfg.setValue(name, atof(value));
+ sprintf(buf,"{\"%s\":%f}", name, param->getFloatValue());
+ break;
+ case 'S':
+ cfg.setValue(name, value);
+ sprintf(buf,"{\"%s\":\"%s\"}", name, param->getCharValue());
+ break;
+ }
+ apply(name);
+ } else {
+ request->send(500, "text/plain", "Unknown parameter name");
+ return;
+ }
+ request->send(200,"application/json",buf);
+ } else {
+ request->send(500, "text/plain", "Not all parameters set");
+ }
+ });
+
+ AsyncCallbackJsonWebHandler* configUploadHandler = new AsyncCallbackJsonWebHandler("/config/put", [](AsyncWebServerRequest *request, JsonVariant &json) {
+ cfg.clear();
+ // first - set values
+ for( JsonPair kv : json.as<JsonObject>() ) {
+ const char* name = kv.key().c_str();
+ if (kv.value().is<bool>()) {
+ cfg.setValue(name, kv.value().as<bool>());
+ } else if (kv.value().is<int>()) {
+ cfg.setValue(name, kv.value().as<int>());
+ } else if (kv.value().is<double>()) {
+ cfg.setValue(name, kv.value().as<double>());
+ } else if (kv.value().is<char*>()) {
+ cfg.setValue(name, kv.value().as<char*>());
+ } else {
+ Serial.print(F("Неопознанный тип значения параметра ")); Serial.print(name); Serial.print(": "); Serial.println(kv.value().as<String>().c_str());
+ cfg.clear();
+ setupConfig();
+ request->send(500, "text/plain", "Unknown parameter type");
+ }
+ }
+ // second - handle all changes
+ for( JsonPair kv : json.as<JsonObject>() ) {
+ apply(kv.key().c_str());
+ }
+ pendingWiFi = false;
+ pendingAuth = false;
+ saveConfig();
+ message(F("Применены сохраненные настройки"),5);
+ reportMessage(F("Применены сохраненные настройки"));
+ millisScheduled = millis() + 10000;
+ actionScheduled = "restart";
+ request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Настройки восстановлены из резервной копии\"}");
+ });
+
+ server.addHandler(configUploadHandler).setAuthentication(auth_user,auth_pwd);
+
+ server.serveStatic("ui", LittleFS, "/ui.json").setAuthentication(auth_user,auth_pwd);
+
+ server.serveStatic("/", LittleFS, "/web/").setDefaultFile("index.html").setAuthentication(auth_user,auth_pwd);
+
+ server.onNotFound([](AsyncWebServerRequest *request){
+ request->send(404,"text/plain","Not found");
+ });
+
+ events.onConnect([](AsyncEventSourceClient *client){
+ sendInitial(client);
+ });
+
+ events.setAuthentication(auth_user,auth_pwd);
+
+ server.addHandler(&events);
+
+ server.begin();
+
+ tKeepalive.attach(2, sendKeepalive);
+
+}
+
+#define CFG_AUTOSAVE 15
+
+void tickWeb() {
+ static char storedSSID[64];
+ static char storedPSK[64];
+ static bool connectInProgress = false;
+ static unsigned long connectMillis = 0;
+ if (actionScheduled && millis()>millisScheduled+300) {
+ Serial.print(F("Запланированная операция ")); Serial.println(actionScheduled);
+ //
+ if (strcmp(actionScheduled,"reset") == 0) {
+ server.end();
+ reset();
+ } else if (strcmp(actionScheduled,"restart") == 0) {
+ server.end();
+ reboot();
+ } else if (strcmp(actionScheduled,"auth") == 0) {
+ Serial.println("Логин/пароль изменены");
+ strncpy(auth_user,cfg.getCharValue(F("auth_user")),31);
+ strncpy(auth_pwd,cfg.getCharValue(F("auth_pwd")),31);
+ pendingAuth = false;
+ } else if (strcmp(actionScheduled,"wifi") == 0) {
+ Serial.println("Применяю настройки сети");
+ strcpy(storedSSID,WiFi.SSID().c_str());
+ strcpy(storedPSK,WiFi.psk().c_str());
+ WiFi.mode(WIFI_STA);
+ WiFi.begin(cfg.getCharValue("sta_ssid"),cfg.getCharValue("sta_psk"));
+ connectInProgress = true;
+ connectMillis = millis();
+ } else if (strcmp(actionScheduled,"save") == 0 && !pendingWiFi && !pendingAuth) {
+ saveConfig();
+ }
+ actionScheduled = nullptr;
+ }
+ if (connectInProgress && (millis() > connectMillis + 1000)) {
+ if (WiFi.status() == WL_CONNECTED) {
+ char buf[64];
+ sprintf(buf,"Подключен к %s, IP=%s", WiFi.SSID(), WiFi.localIP().toString().c_str());
+ Serial.println(buf);
+ message(buf,1);
+ pendingWiFi = false;
+ connectInProgress = false;
+ } else if (WiFi.status() == WL_CONNECT_FAILED || WiFi.status() == WL_NO_SSID_AVAIL || ((WiFi.status() == WL_CONNECTION_LOST || WiFi.status() == WL_DISCONNECTED) && (millis()>connectMillis+1000*cfg.getIntValue("sta_wait")))) {
+ Serial.println(F("Подключение не удалось, возвращаю прежние настройки"));
+ message(F("Подключение не удалось, возвращаю прежние настройки"),1);
+ WiFi.begin(storedSSID, storedPSK);
+ connectInProgress = false;
+ }
+ }
+
+ if (!pendingWiFi && !pendingAuth && cfg.getTimestamp() && cfg.getTimestamp() < now - CFG_AUTOSAVE) {
+ saveConfig();
+ reportMessage(F("Настройки сохранены"));
+ Serial.println(F("Настройки сохранены"));
+ }
+}