From: Roman Bazalevskiy Date: Fri, 18 Nov 2022 17:05:44 +0000 (+0300) Subject: Merge branch 'main' of github.com:rvbglas/esp_clock X-Git-Url: https://git.rvb.name/esp-clock.git/commitdiff_plain/bb98586f5554bf3f73c10a48cc5aaf4b1c33badb?hp=45eb46328c98899c3125c80ef2ac6fc6f53a176a Merge branch 'main' of github.com:rvbglas/esp_clock --- diff --git a/Clock.h b/Clock.h new file mode 100644 index 0000000..cd3620d --- /dev/null +++ b/Clock.h @@ -0,0 +1,201 @@ +#pragma once +#include +#include +#include +#include + +// 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 timeHandler); +void unregisterTimeHandler(const char* handlerName); +void registerTimeHandler(const __FlashStringHelper* handlerName, const char handlerType, std::function 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(); diff --git a/ESP8266_clock.ino b/ESP8266_clock.ino new file mode 100644 index 0000000..b8e8c4a --- /dev/null +++ b/ESP8266_clock.ino @@ -0,0 +1,50 @@ +#include "Clock.h" +#include + +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(); +} diff --git a/alarm.cpp b/alarm.cpp new file mode 100644 index 0000000..2964418 --- /dev/null +++ b/alarm.cpp @@ -0,0 +1,37 @@ +#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); +} + diff --git a/config.cpp b/config.cpp new file mode 100644 index 0000000..9c46b95 --- /dev/null +++ b/config.cpp @@ -0,0 +1,566 @@ +#include "Clock.h" +#include + +#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(); +} diff --git a/data/config.txt b/data/config.txt new file mode 100644 index 0000000..c8442a8 --- /dev/null +++ b/data/config.txt @@ -0,0 +1,50 @@ +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: diff --git a/data/web/icon.svg b/data/web/icon.svg new file mode 100644 index 0000000..913fff7 --- /dev/null +++ b/data/web/icon.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/web/icons.woff2 b/data/web/icons.woff2 new file mode 100644 index 0000000..85a75f5 Binary files /dev/null and b/data/web/icons.woff2 differ diff --git a/data/web/index.html b/data/web/index.html new file mode 100644 index 0000000..0688a49 --- /dev/null +++ b/data/web/index.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+

+
+
+
+
+
+
+
+
+
+
+ + + + + + diff --git a/data/web/manifest.json b/data/web/manifest.json new file mode 100644 index 0000000..50f32fd --- /dev/null +++ b/data/web/manifest.json @@ -0,0 +1,10 @@ +{ + "background_color":"#fff", + "display":"standalone", + "start_url":"/", + "name":"IoT Clock", + "short_name":"IoT Clock", + "icons":[ + {"src":"/icon.svg","sizes":"192x192","type":"image/svg"} + ] +} diff --git a/data/web/script.js b/data/web/script.js new file mode 100644 index 0000000..ee43439 --- /dev/null +++ b/data/web/script.js @@ -0,0 +1,489 @@ +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 = '' + 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 = '' + 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 += '' + } + + table += '
SSIDBSSIDRSSIКаналЗащита
'+json[idx].ssid+''+json[idx].bssid+''+json[idx].rssi+''+json[idx].channel+''+encryption+'
' + 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 '

' + case 'button': + return '
' + case 'password': + return '
' + + '
' + + '
' + case 'input': + var pattern = "" + if (element.pattern) { + pattern = ' pattern="' + encode(element.pattern) + '"' + } + return '
' + + '
' + case 'input-wifi': + var pattern = "" + if (element.pattern) { + pattern = ' pattern="' + encode(element.pattern) + '"' + } + return '
' + + '
' + + '
' + + '
' + case 'checkbox': + return '
' + case 'select': + var options = '
' + + '
' + return options + case 'week': + days = '
' + + '' + for (i=0; i<7; i++) { + a_enabled = (value[i] == "1") + days = days + '' + } + days += '
'+ daynames[i] + '
' + return days + case 'timeset': + var now = new Date() + now.setMinutes(now.getMinutes() - now.getTimezoneOffset()) + value = now.toISOString().slice(0, -1); + return '
' + +'' + + '
->
' + case 'text': + return '

' + encode(value) + '

' + case 'number': + return '
' + + '
' + case 'range': + return '
' + + '
' + case 'config': + return '
' + + '
' + + '' + + '' + + '' + + '
' + case 'table': + default: + return '
' + + encode(element.label)+ '' + encode(value) + '
' + } +} + +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 + '' + } + menu.innerHTML = list +} + +function drawContacts(contacts) { + if (!contacts) return; + var contact_list = '

Контакты

' + for (contact of contacts) { + const url = new URL(contact) + var ref + switch (url.protocol) { + case 'http': + case 'https:': + ref = ''+url.hostname + break + case 'mailto:': + ref = ''+url.pathname + break + case 'tg:': + ref = ''+url.pathname + contact = 'tg://resolve?domain='+url.pathname + break + default: + ref = ''+url.pathname + } + contact_list += ''+ref+'' + } + 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() diff --git a/data/web/style.css b/data/web/style.css new file mode 100644 index 0000000..243c659 --- /dev/null +++ b/data/web/style.css @@ -0,0 +1,1502 @@ +/*! +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; +} diff --git a/description b/description new file mode 100644 index 0000000..ad7f9b4 --- /dev/null +++ b/description @@ -0,0 +1,2 @@ +description = "ESP8266 alarm clock" +owner = "rvb@rvb.name" diff --git a/esp-clock.ino b/esp-clock.ino new file mode 100644 index 0000000..a031d47 --- /dev/null +++ b/esp-clock.ino @@ -0,0 +1,50 @@ +#include "Clock.h" +#include + +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(); +} diff --git a/fonts.h b/fonts.h new file mode 100644 index 0000000..585b428 --- /dev/null +++ b/fonts.h @@ -0,0 +1,1828 @@ +#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 +}; diff --git a/hardware.cpp b/hardware.cpp new file mode 100644 index 0000000..0153632 --- /dev/null +++ b/hardware.cpp @@ -0,0 +1,204 @@ +#include "Clock.h" +#include +#include "time.h" +#include +#include +#include + +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); + } + } + } +} diff --git a/make_fs.sh b/make_fs.sh new file mode 100644 index 0000000..5ef8196 --- /dev/null +++ b/make_fs.sh @@ -0,0 +1,17 @@ +#!/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 diff --git a/net.cpp b/net.cpp new file mode 100644 index 0000000..ff18fcc --- /dev/null +++ b/net.cpp @@ -0,0 +1,55 @@ +#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 diff --git a/panel.cpp b/panel.cpp new file mode 100644 index 0000000..fc07261 --- /dev/null +++ b/panel.cpp @@ -0,0 +1,290 @@ +#include "Clock.h" +#include +#include +#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; + } +} + + diff --git a/time.cpp b/time.cpp new file mode 100644 index 0000000..4f36165 --- /dev/null +++ b/time.cpp @@ -0,0 +1,142 @@ +#include "Clock.h" +#include +#include + +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 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 timeHandler) { + for (int i=0; i timeHandler) { + for (int i=0; itm_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; + } else { /// late day ... night .... day till mindnight + return (hh>=night_from && hh +#include + +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()) { + IterateJsonObject(json.as(), rootName, lCfg); + } else if (json.is()) { + IterateJsonArray(json.as(), rootName, lCfg); + } else { + lCfg.setValue(rootName.c_str(), json.as().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(), "", 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 diff --git a/web.cpp b/web.cpp new file mode 100644 index 0000000..13e7916 --- /dev/null +++ b/web.cpp @@ -0,0 +1,424 @@ +#include "Clock.h" +#include +#include +#include +#include +#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() ) { + const char* name = kv.key().c_str(); + if (kv.value().is()) { + cfg.setValue(name, kv.value().as()); + } else if (kv.value().is()) { + cfg.setValue(name, kv.value().as()); + } else if (kv.value().is()) { + cfg.setValue(name, kv.value().as()); + } else if (kv.value().is()) { + cfg.setValue(name, kv.value().as()); + } else { + Serial.print(F("Неопознанный тип значения параметра ")); Serial.print(name); Serial.print(": "); Serial.println(kv.value().as().c_str()); + cfg.clear(); + setupConfig(); + request->send(500, "text/plain", "Unknown parameter type"); + } + } + // second - handle all changes + for( JsonPair kv : json.as() ) { + 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("Настройки сохранены")); + } +}