From 4a89b164bd318a7270f6ffe34de6c4ff871a6fa4 Mon Sep 17 00:00:00 2001 From: gmx168 <71901960+gmx168@users.noreply.github.com> Date: Sun, 29 Nov 2020 19:50:16 +0100 Subject: [PATCH 001/165] plik jezyka francuskiego fr.h dla Generic --- fr.h | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 fr.h diff --git a/fr.h b/fr.h new file mode 100644 index 00000000..d127405a --- /dev/null +++ b/fr.h @@ -0,0 +1,105 @@ +#ifndef _LANGUAGE_FR_S_H_ +#define _LANGUAGE_FR_S_H_ + +// translated by Fryga +#define S_SETTING_WIFI_SSID "Mettre à jour du WIFI" +#define S_WIFI_SSID "Nom du WIFI" +#define S_WIFI_PASS "Mot de passe" +#define S_HOST_NAME "Nom du module" +#define S_SETTING_SUPLA "Paramètres SUPLA" +#define S_SUPLA_SERVER "Adresse du serveur" +#define S_SUPLA_EMAIL "E-mail" +#define S_SETTING_ADMIN "Paramètres administrateur" +#define S_LOGIN "Connexion" +#define S_LOGIN_PASS "Mot de passe" +#define S_ROLLERSHUTTERS "Volet roulant" +#define S_SAVE "Enregistrer" +#define S_DEVICE_SETTINGS "Paramètres du module" +#define S_UPDATE "Mettre à jour" +#define S_RESTART "Réinitialisation" +#define S_RETURN "Retour" +#define S_TEMPLATE_BOARD "Modèle de la planches" +#define S_TYPE "Genre" +#define S_RELAYS "LES RELAIS" +#define S_BUTTONS "LES BOUTONS" +#define S_SENSORS_1WIRE "LES SENSORS du 1Wire" +#define S_SENSORS_I2C "LES SENSORS du i2c" +#define S_SENSORS_SPI "LES SENSORS du SPI" +#define S_LED_BUTTON_CFG "LED, BOUTON metre à jour" +#define S_QUANTITY "Quantité" +#define S_GPIO_SETTINGS_FOR_RELAYS "Paramètres du GPIO pour les relais" +#define S_RELAY "LE RELAIS" +#define S_RELAY_NR_SETTINGS "Paramètres de la relais No." +#define S_NO_RELAY_NR "Manquant numéro de relais" +#define S_STATE_CONTROL "Contrôle d'état" +#define S_REACTION_AFTER_RESET "Réaction après réinitialisation" +#define S_GPIO_SETTINGS_FOR_BUTTONS "Paramètres GPIO pour les boutons" +#define S_BUTTON "BOUTON" +#define S_BUTTON_NR_SETTINGS "Bouton mettre à jour" +#define S_NO_BUTTON_NR "Manquant numéro de bouton" +#define S_REACTION_TO "Réaction à" +#define S_GPIO_SETTINGS_FOR_LIMIT_SWITCH "Paramètres GPIO pour capteur de limite" +#define S_LIMIT_SWITCH "Capteur de limite" +#define S_GPIO_SETTINGS_FOR "GPIO mettre à jour pour" +#define S_FOUND "Trouvé" +#define S_NO_SENSORS_CONNECTED "Aucun capteur connecté" +#define S_SAVE_FOUND "Enregistrer trouvé" +#define S_TEMPERATURE "Température" +#define S_NAME "Nom" +#define S_ALTITUDE_ABOVE_SEA_LEVEL "Altitude" +#define S_GPIO_SETTINGS_FOR_CONFIG "Paramètres GPIO pour la à jour" +#define S_SOFTWARE_UPDATE "Mise à jour du logiciel" +#define S_DATA_SAVED "Données enregistrées" +#define S_RESTART_MODULE "Redémarrer le module" +#define S_DATA_ERASED_RESTART_DEVICE "Données effacées - redémarrer le module" +#define S_WRITE_ERROR_UNABLE_TO_READ_FILE_FS_PARTITION_MISSING "Erreur d'écriture - impossible de lire à partir du fichier FS. Partition manquante" +#define S_DATA_SAVED_RESTART_MODULE "Données enregistrées - redémarrer le module" +#define S_WRITE_ERROR_BAD_DATA "Erreur d'écriture - données incorrectes" + +//#### SuplaConfigESP.cpp #### +#define S_ALEREADY_INITIATED "Déjà lancé" +#define S_NOT_ASSIGNED_CB "CB non attribué" +#define S_INVALID_GUID_OR_DEVICE_REGISTRATION_INACTIVE "GUID non valide ou enregistrement de l'appareil INACTIF" +#define S_UNKNOWN_SEVER_ADDRESS "Adresse de serveur inconnue" +#define S_UNKNOWN_ID "ID inconnu" +#define S_INITIATED "Initié" +#define S_CHANNEL_LIMIT_EXCEEDED "Limite de canal dépassée" +#define S_DISCONNECTED "Déconnecté" +#define S_REGISRATION_IS_PENDING "L'enregistrement est en attente" +#define S_VARIABLE_ERROR "Erreur de variable" +#define S_PROTOCOL_VERSION_ERROR "Erreur de version de protocole" +#define S_BAD_CREDENTIALS "Informations d'identification incorrectes" +#define S_TEMPORARILY_UNAVAILABLE "Temporairement indisponible" +#define S_LOCATION_CONFLICT "Conflit d'emplacement" +#define S_CHANNEL_CONFLICT "Conflit de canal" +#define S_REGISTERED_AND_READY "Enregistré et prêt" +#define S_DEVICE_IS_DISCONNECTED "L'appareil est déconnecté" +#define S_LOCATION_IS_DISABLED "L'emplacement est désactivé" +#define S_DEVICE_LIMIT_EXCEEDED "Limite d'appareils dépassée" + +// #### SuplaCommonPROGMEM.h #### +#define S_OFF "ÉTEINDRE" +#define S_ON "ALLUMER" +#define S_LOW "BASSE" +#define S_HIGH "HAUT" +#define S_POSITION_MEMORY "MEMOIRE DE POSITION" +#define S_REACTION_ON_PRESS "ON PRESSE" +#define S_REACTION_ON_RELEASE "EN LIBÉRATION" +#define S_REACTION_ON_CHANGE "SUR LE CHANGEMENT" +#define S_CFG_10_PRESSES "10 FOIS SUR PRESSE" +#define S_5SEK_HOLD "5 SEC TENIR" + +// #### SuplaTemplateBoard.h #### +#define S_ABSENT "ABSENT" + +// #### SuplaWebPageSensor.cpp #### +#define S_IMPULSE_COUNTER "Compteur d'impulsions" +#define S_IMPULSE_COUNTER_DEBOUNCE_TIMEOUT "Délai anti-rebond" +#define S_IMPULSE_COUNTER_RAISING_EDGE "Bord montant" +#define S_IMPULSE_COUNTER_PULL_UP "Remonter" +#define S_IMPULSE_COUNTER_CHANGE_VALUE "Modifier la valeur" +#define S_IMPULSE_COUNTER_SETTINGS_NR "Paramètres Compteur d'impulsions No." +#define S_NO_IMPULSE_COUNTER_NR "Aucun Compteur d'impulsions No." + + +#endif // _LANGUAGE_FR_S_H_ \ No newline at end of file From 76adf54af545b49403907ce68228013478f82c28 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Mon, 30 Nov 2020 13:04:03 +0100 Subject: [PATCH 002/165] Update platformio.ini --- platformio.ini | 129 ++++++++++++++----------------------------------- 1 file changed, 36 insertions(+), 93 deletions(-) diff --git a/platformio.ini b/platformio.ini index 9ea7a14c..dbf7d6e8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,21 +22,6 @@ default_envs = [common] -platform = espressif8266 @ 2.6.0 -upload_speed = 256000 -monitor_speed = 74880 -upload_resetmethod = nodemcu -board_build.flash_mode = dout -board_build.ldscript = eagle.flash.1m64.ld -lib_deps = - milesburton/DallasTemperature@^3.9.1 - adafruit/DHT sensor library@^1.4.0 - paulstoffregen/OneWire@^2.3.5 - adafruit/Adafruit BME280 Library@^2.1.1 - datacute/DoubleResetDetector@^1.0.3 - closedcube/ClosedCube SHT31D@^1.5.1 - adafruit/Adafruit Si7021 Library@^1.3.0 - build_flags = -D BUILD_VERSION='"GUI 1.0.1"' -w -DATOMIC_FS_UPDATE @@ -56,63 +41,41 @@ build_flags = -D BUILD_VERSION='"GUI 1.0.1"' -D SUPLA_SI7021 -D SUPLA_MAX6675 -D SUPLA_HC_SR04 - -D SUPLA_IMPULSE_COUNTER + -D SUPLA_IMPULSE_COUNTER -extra_scripts = tools/copy_files.py - -[env:GUI_Generic_1M] -platform = ${common.platform} +[env] framework = arduino +platform = espressif8266 @ 2.6.0 +upload_speed = 256000 +monitor_speed = 74880 +upload_resetmethod = nodemcu +board_build.flash_mode = dout +board_build.ldscript = eagle.flash.1m64.ld +lib_deps = + milesburton/DallasTemperature@^3.9.1 + adafruit/DHT sensor library@^1.4.0 + paulstoffregen/OneWire@^2.3.5 + adafruit/Adafruit BME280 Library@^2.1.1 + datacute/DoubleResetDetector@^1.0.3 + closedcube/ClosedCube SHT31D@^1.5.1 + adafruit/Adafruit Si7021 Library@^1.3.0 +extra_scripts = tools/copy_files.py + +[env:GUI_Generic_1M] board = esp8285 -upload_resetmethod = ${common.upload_resetmethod} -board_build.flash_mode = ${common.board_build.flash_mode} -board_build.ldscript = ${common.board_build.ldscript} -upload_speed = ${common.upload_speed} -monitor_speed = ${common.monitor_speed} -lib_deps = ${common.lib_deps} -extra_scripts = ${common.extra_scripts} -;build_unflags = -D SUPLA_OTA build_flags = ${common.build_flags} [env:GUI_Generic_1M-en] -platform = ${common.platform} -framework = arduino board = esp8285 -upload_resetmethod = ${common.upload_resetmethod} -board_build.flash_mode = ${common.board_build.flash_mode} -board_build.ldscript = ${common.board_build.ldscript} -upload_speed = ${common.upload_speed} -monitor_speed = ${common.monitor_speed} -lib_deps = ${common.lib_deps} -extra_scripts = ${common.extra_scripts} -;build_unflags = -D SUPLA_OTA build_flags = ${common.build_flags} -D UI_LANGUAGE=en [env:GUI_Generic_4M] -platform = ${common.platform} -framework = arduino board = esp12e -upload_resetmethod = ${common.upload_resetmethod} -board_build.flash_mode = ${common.board_build.flash_mode} -board_build.ldscript = ${common.board_build.ldscript} -upload_speed = ${common.upload_speed} -monitor_speed = ${common.monitor_speed} -lib_deps = ${common.lib_deps} -extra_scripts = ${common.extra_scripts} build_flags = ${common.build_flags} [env:GUI_Generic_minimal] -platform = ${common.platform} board = esp8285 -framework = arduino -upload_resetmethod = ${common.upload_resetmethod} -board_build.flash_mode = ${common.board_build.flash_mode} -board_build.ldscript = ${common.board_build.ldscript} -upload_speed = ${common.upload_speed} -monitor_speed = ${common.monitor_speed} -lib_deps = ${common.lib_deps} -extra_scripts = ${common.extra_scripts} build_flags = ${common.build_flags} build_unflags = -D SUPLA_DS18B20 -D SUPLA_DHT11 @@ -126,16 +89,7 @@ build_unflags = -D SUPLA_DS18B20 -D SUPLA_IMPULSE_COUNTER [env:GUI_Generic_lite] -platform = ${common.platform} board = esp8285 -framework = arduino -upload_resetmethod = ${common.upload_resetmethod} -board_build.flash_mode = ${common.board_build.flash_mode} -board_build.ldscript = ${common.board_build.ldscript} -upload_speed = ${common.upload_speed} -monitor_speed = ${common.monitor_speed} -lib_deps = ${common.lib_deps} -extra_scripts = ${common.extra_scripts} build_flags = ${common.build_flags} build_unflags = -D SUPLA_DHT11 @@ -147,16 +101,7 @@ build_unflags = -D SUPLA_IMPULSE_COUNTER [env:GUI_Generic_sensors] -platform = ${common.platform} board = esp8285 -framework = arduino -upload_resetmethod = ${common.upload_resetmethod} -board_build.flash_mode = ${common.board_build.flash_mode} -board_build.ldscript = ${common.board_build.ldscript} -upload_speed = ${common.upload_speed} -monitor_speed = ${common.monitor_speed} -lib_deps = ${common.lib_deps} -extra_scripts = ${common.extra_scripts} build_flags = ${common.build_flags} build_unflags = -D SUPLA_RELAY -D SUPLA_BUTTON @@ -165,30 +110,28 @@ build_unflags = -D SUPLA_RELAY -D SUPLA_CONFIG [env:GUI_Generic_DEBUG] -platform = ${common.platform} -framework = arduino board = nodemcuv2 -upload_resetmethod = ${common.upload_resetmethod} -board_build.flash_mode = ${common.board_build.flash_mode} -board_build.ldscript = ${common.board_build.ldscript} -upload_speed = ${common.upload_speed} -monitor_speed = ${common.monitor_speed} -lib_deps = ${common.lib_deps} -extra_scripts = ${common.extra_scripts} build_flags = ${common.build_flags} - -D GUI_Generic -D DEBUG_MODE +build_unflags = -DUSE_CUSTOM [env:GUI_Generic_blank] -platform = ${common.platform} -framework = arduino board = nodemcuv2 -upload_resetmethod = ${common.upload_resetmethod} -board_build.flash_mode = ${common.board_build.flash_mode} -board_build.ldscript = ${common.board_build.ldscript} -upload_speed = ${common.upload_speed} -monitor_speed = ${common.monitor_speed} -lib_deps = ${common.lib_deps} -extra_scripts = ${common.extra_scripts} build_flags = ${common.build_flags} +build_unflags = -D SUPLA_OTA + -D SUPLA_RELAY + -D SUPLA_BUTTON + -D SUPLA_LIMIT_SWITCH + -D SUPLA_ROLLERSHUTTER + -D SUPLA_CONFIG + -D SUPLA_DS18B20 + -D SUPLA_DHT11 + -D SUPLA_DHT22 + -D SUPLA_SI7021_SONOFF + -D SUPLA_BME280 + -D SUPLA_SHT3x + -D SUPLA_SI7021 + -D SUPLA_MAX6675 + -D SUPLA_HC_SR04 + -D SUPLA_IMPULSE_COUNTER \ No newline at end of file From 3b47fb864220734bef256335c3b51d56e7fcbbde Mon Sep 17 00:00:00 2001 From: krycha88 Date: Tue, 1 Dec 2020 11:32:20 +0100 Subject: [PATCH 003/165] =?UTF-8?q?nowy=20spos=C3=B3b=20wyznaczania=20-=20?= =?UTF-8?q?const=20char=20*key=20->=20uint8=5Ft=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Markup.cpp | 12 ++-- src/Markup.h | 8 +-- src/SuplaConfigESP.cpp | 114 ++++++++++++++++-------------------- src/SuplaConfigManager.cpp | 62 +++++++++----------- src/SuplaConfigManager.h | 78 ++++++++++++------------ src/SuplaDeviceGUI.cpp | 7 +-- src/SuplaWebPageConfig.cpp | 35 +++++------ src/SuplaWebPageControl.cpp | 7 +-- src/SuplaWebPageRelay.cpp | 9 ++- src/SuplaWebPageSensor.cpp | 37 +++++------- src/SuplaWebServer.cpp | 20 +++---- 11 files changed, 181 insertions(+), 208 deletions(-) diff --git a/src/Markup.cpp b/src/Markup.cpp index cd91f446..a2901b00 100644 --- a/src/Markup.cpp +++ b/src/Markup.cpp @@ -15,7 +15,7 @@ void addFormHeaderEnd(String& html) { void addTextBox(String& html, const String& input_id, const String& name, - const String& value_key, + uint8_t value_key, const String& placeholder, int minlength, int maxlength, @@ -36,7 +36,7 @@ void addTextBox(String& html, } html += F("' value='"); - String value = String(ConfigManager->get(value_key.c_str())->getValue()); + String value = String(ConfigManager->get(value_key)->getValue()); if (value != placeholder) { html += value; } @@ -64,16 +64,16 @@ void addTextBox(String& html, } void addTextBox( - String& html, const String& input_id, const String& name, const String& value_key, int minlength, int maxlength, bool required, bool readonly) { + String& html, const String& input_id, const String& name, uint8_t value_key, int minlength, int maxlength, bool required, bool readonly) { return addTextBox(html, input_id, name, value_key, "", minlength, maxlength, required, readonly, false); } void addTextBoxPassword( - String& html, const String& input_id, const String& name, const String& value_key, int minlength, int maxlength, bool required) { + String& html, const String& input_id, const String& name, uint8_t value_key, int minlength, int maxlength, bool required) { return addTextBox(html, input_id, name, value_key, "", minlength, maxlength, required, false, true); } -void addNumberBox(String& html, const String& input_id, const String& name, const String& value_key, uint16_t max) { +void addNumberBox(String& html, const String& input_id, const String& name, uint8_t value_key, uint16_t max) { html += F(""); } diff --git a/src/Markup.h b/src/Markup.h index a29d4900..3613c9e5 100644 --- a/src/Markup.h +++ b/src/Markup.h @@ -8,7 +8,7 @@ void addFormHeaderEnd(String& html); void addTextBox(String& html, const String& input_id, const String& name, - const String& value_key, + uint8_t value_key, const String& placeholder, int minlength, int maxlength, @@ -18,15 +18,15 @@ void addTextBox(String& html, void addTextBox(String& html, const String& input_id, const String& name, - const String& value_key, + uint8_t value_key, int minlength, int maxlength, bool required, bool readonly = false); void addTextBoxPassword( - String& html, const String& input_id, const String& value_key, const String& name, int minlength, int maxlength, bool required); + String& html, const String& input_id, const String& name, uint8_t value_key, int minlength, int maxlength, bool required); -void addNumberBox(String& html, const String& input_id, const String& name, const String& value_key, uint16_t max); +void addNumberBox(String& html, const String& input_id, const String& name, uint8_t value_key, uint16_t max); void addListGPIOBox(String& html, const String& input_id, const String& name, uint8_t function, uint8_t nr = 0); diff --git a/src/SuplaConfigESP.cpp b/src/SuplaConfigESP.cpp index a85d26bb..16a0a381 100644 --- a/src/SuplaConfigESP.cpp +++ b/src/SuplaConfigESP.cpp @@ -237,17 +237,15 @@ void status_func(int status, const char *msg) { } int SuplaConfigESP::getGpio(int nr, int function) { - uint8_t gpio; - for (gpio = 0; gpio <= OFF_GPIO; gpio++) { - String key = GPIO; - key += gpio; + for (uint8_t gpio = 0; gpio <= OFF_GPIO; gpio++) { + uint8_t key = KEY_GPIO + gpio; if (function == FUNCTION_CFG_BUTTON) { if (checkBusyCfg(gpio)) { return gpio; } } - if (ConfigManager->get(key.c_str())->getElement(FUNCTION).toInt() == function) { - if (ConfigManager->get(key.c_str())->getElement(NR).toInt() == nr) { + if (ConfigManager->get(key)->getElement(FUNCTION).toInt() == function) { + if (ConfigManager->get(key)->getElement(NR).toInt() == nr) { return gpio; } } @@ -256,13 +254,11 @@ int SuplaConfigESP::getGpio(int nr, int function) { } int SuplaConfigESP::getLevel(int nr, int function) { - uint8_t gpio; - for (gpio = 0; gpio <= OFF_GPIO; gpio++) { - String key = GPIO; - key += gpio; - if (ConfigManager->get(key.c_str())->getElement(FUNCTION).toInt() == function) { - if (ConfigManager->get(key.c_str())->getElement(NR).toInt() == nr) { - uint8_t level = ConfigManager->get(key.c_str())->getElement(LEVEL).toInt(); + for (uint8_t gpio = 0; gpio <= OFF_GPIO; gpio++) { + uint8_t key = KEY_GPIO + gpio; + if (ConfigManager->get(key)->getElement(FUNCTION).toInt() == function) { + if (ConfigManager->get(key)->getElement(NR).toInt() == nr) { + uint8_t level = ConfigManager->get(key)->getElement(LEVEL).toInt(); return level; } } @@ -271,13 +267,11 @@ int SuplaConfigESP::getLevel(int nr, int function) { } int SuplaConfigESP::getMemory(int nr, int function) { - uint8_t gpio; - for (gpio = 0; gpio <= OFF_GPIO; gpio++) { - String key = GPIO; - key += gpio; - if (ConfigManager->get(key.c_str())->getElement(FUNCTION).toInt() == function) { - if (ConfigManager->get(key.c_str())->getElement(NR).toInt() == nr) { - uint8_t level = ConfigManager->get(key.c_str())->getElement(MEMORY).toInt(); + for (uint8_t gpio = 0; gpio <= OFF_GPIO; gpio++) { + uint8_t key = KEY_GPIO + gpio; + if (ConfigManager->get(key)->getElement(FUNCTION).toInt() == function) { + if (ConfigManager->get(key)->getElement(NR).toInt() == nr) { + uint8_t level = ConfigManager->get(key)->getElement(MEMORY).toInt(); return level; } } @@ -285,9 +279,8 @@ int SuplaConfigESP::getMemory(int nr, int function) { return OFF_GPIO; } bool SuplaConfigESP::checkBusyCfg(int gpio) { - String key = GPIO; - key += gpio; - if (ConfigManager->get(key.c_str())->getElement(FUNCTION_CFG_LED).toInt() == 1) { + uint8_t key = KEY_GPIO + gpio; + if (ConfigManager->get(key)->getElement(FUNCTION_CFG_LED).toInt() == 1) { return true; } return false; @@ -298,9 +291,9 @@ int SuplaConfigESP::checkBusyGpio(int gpio, int function) { return true; } else { - String key = GPIO; - key += gpio; - if (ConfigManager->get(key.c_str())->getElement(FUNCTION).toInt() == FUNCTION_BUTTON) { + uint8_t key = KEY_GPIO + gpio; + + if (ConfigManager->get(key)->getElement(FUNCTION).toInt() == FUNCTION_BUTTON) { if (function == FUNCTION_CFG_BUTTON) { return false; } @@ -310,8 +303,8 @@ int SuplaConfigESP::checkBusyGpio(int gpio, int function) { return true; } } - if (ConfigManager->get(key.c_str())->getElement(FUNCTION).toInt() != FUNCTION_OFF) { - if (ConfigManager->get(key.c_str())->getElement(FUNCTION).toInt() != function) { + if (ConfigManager->get(key)->getElement(FUNCTION).toInt() != FUNCTION_OFF) { + if (ConfigManager->get(key)->getElement(FUNCTION).toInt() != function) { return true; } } @@ -320,43 +313,42 @@ int SuplaConfigESP::checkBusyGpio(int gpio, int function) { } void SuplaConfigESP::setGpio(uint8_t gpio, uint8_t nr, uint8_t function, uint8_t level, uint8_t memory) { - String key = GPIO; - key += gpio; + uint8_t key = KEY_GPIO + gpio; + if (function == FUNCTION_CFG_BUTTON) { - ConfigManager->setElement(key.c_str(), CFG, 1); + ConfigManager->setElement(key, CFG, 1); return; } - ConfigManager->setElement(key.c_str(), NR, nr); - ConfigManager->setElement(key.c_str(), FUNCTION, function); - ConfigManager->setElement(key.c_str(), LEVEL, level); - ConfigManager->setElement(key.c_str(), MEMORY, memory); + ConfigManager->setElement(key, NR, nr); + ConfigManager->setElement(key, FUNCTION, function); + ConfigManager->setElement(key, LEVEL, level); + ConfigManager->setElement(key, MEMORY, memory); // ConfigManager->setElement(key.c_str(), MEMORY, memory); // ConfigManager->setElement(key.c_str(), CFG, cfg); } void SuplaConfigESP::clearGpio(uint8_t gpio, uint8_t function) { - String key = GPIO; - key += gpio; + uint8_t key = KEY_GPIO + gpio; + if (function == FUNCTION_CFG_BUTTON) { - ConfigManager->setElement(key.c_str(), CFG, 0); + ConfigManager->setElement(key, CFG, 0); return; } - ConfigManager->setElement(key.c_str(), NR, 0); - ConfigManager->setElement(key.c_str(), FUNCTION, FUNCTION_OFF); - ConfigManager->setElement(key.c_str(), LEVEL, 0); - ConfigManager->setElement(key.c_str(), MEMORY, 0); + ConfigManager->setElement(key, NR, 0); + ConfigManager->setElement(key, FUNCTION, FUNCTION_OFF); + ConfigManager->setElement(key, LEVEL, 0); + ConfigManager->setElement(key, MEMORY, 0); } uint8_t SuplaConfigESP::countFreeGpio(uint8_t exception) { uint8_t count = 0; for (uint8_t gpio = 0; gpio < OFF_GPIO; gpio++) { if (gpio != 6 && gpio != 7 && gpio != 8 && gpio != 11) { - String key = GPIO; - key += gpio; - if (ConfigManager->get(key.c_str())->getElement(FUNCTION).toInt() == FUNCTION_OFF || - ConfigManager->get(key.c_str())->getElement(FUNCTION).toInt() == exception) { + uint8_t key = KEY_GPIO + gpio; + if (ConfigManager->get(key)->getElement(FUNCTION).toInt() == FUNCTION_OFF || + ConfigManager->get(key)->getElement(FUNCTION).toInt() == exception) { count++; } } @@ -377,35 +369,31 @@ void SuplaConfigESP::factoryReset() { ConfigManager->set(KEY_HOST_NAME, DEFAULT_HOSTNAME); ConfigManager->set(KEY_LOGIN, DEFAULT_LOGIN); ConfigManager->set(KEY_LOGIN_PASS, DEFAULT_LOGIN_PASS); - ConfigManager->set(KEY_MAX_ROLLERSHUTTER, "0"); ConfigManager->set(KEY_MAX_RELAY, "1"); ConfigManager->set(KEY_MAX_BUTTON, "1"); ConfigManager->set(KEY_MAX_LIMIT_SWITCH, "0"); ConfigManager->set(KEY_MAX_DHT22, "1"); ConfigManager->set(KEY_MAX_DHT11, "1"); ConfigManager->set(KEY_MULTI_MAX_DS18B20, "1"); + ConfigManager->set(KEY_MAX_ROLLERSHUTTER, "0"); ConfigManager->set(KEY_ALTITUDE_BME280, "0"); - - int nr; - String key; - - for (nr = 0; nr <= 17; nr++) { - key = GPIO; - key += nr; - ConfigManager->set(key.c_str(), "0,0,0,0,0"); - } - + ConfigManager->set(KEY_IMPULSE_COUNTER_DEBOUNCE_TIMEOUT, "0"); + ConfigManager->set(KEY_MAX_IMPULSE_COUNTER, "0"); ConfigManager->set(KEY_ACTIVE_SENSOR, "0,0,0,0,0"); ConfigManager->set(KEY_BOARD, "0"); ConfigManager->set(KEY_CFG_MODE, "0"); + uint8_t nr, key; + for (nr = 0; nr <= 17; nr++) { + key = KEY_GPIO + nr; + ConfigManager->set(key, "0,0,0,0,0"); + } + for (nr = 0; nr <= MAX_DS18B20; nr++) { - key = KEY_DS; - key += nr; - ConfigManager->set(key.c_str(), ""); - key = KEY_DS_NAME; - key += nr; - ConfigManager->set(key.c_str(), ""); + key = KEY_DS + nr; + ConfigManager->set(key, ""); + key = KEY_DS_NAME + nr; + ConfigManager->set(key, ""); } ConfigManager->save(); diff --git a/src/SuplaConfigManager.cpp b/src/SuplaConfigManager.cpp index ec4d8823..cb78f92d 100644 --- a/src/SuplaConfigManager.cpp +++ b/src/SuplaConfigManager.cpp @@ -26,21 +26,22 @@ #define CONFIG_FILE_PATH "/dat" -ConfigOption::ConfigOption(const char *key, const char *value, int maxLength) { +ConfigOption::ConfigOption(uint8_t key, const char *value, int maxLength) { // size_t size = strlen(key) + 1; // _key = (char *)malloc(sizeof(char) * size); // memcpy(_key, key, size - 1); // _key[size - 1] = 0; - size_t size = strlen(key) + 1; + /*size_t size = strlen(key) + 1; _key = new char[size]; strncpy(_key, key, size); _key[size - 1] = '\0'; - +*/ + _key = key; _maxLength = maxLength + 1; setValue(value); } -const char *ConfigOption::getKey() { +uint8_t ConfigOption::getKey() { return _key; } @@ -151,37 +152,32 @@ SuplaConfigManager::SuplaConfigManager() { this->addKey(KEY_HOST_NAME, DEFAULT_HOSTNAME, MAX_HOSTNAME); this->addKey(KEY_SUPLA_SERVER, DEFAULT_SERVER, MAX_SUPLA_SERVER); this->addKey(KEY_SUPLA_EMAIL, DEFAULT_EMAIL, MAX_EMAIL); - this->addKey(KEY_MAX_ROLLERSHUTTER, "0", 2); this->addKey(KEY_MAX_RELAY, "0", 2); this->addKey(KEY_MAX_BUTTON, "0", 2); this->addKey(KEY_MAX_LIMIT_SWITCH, "0", 2); this->addKey(KEY_MAX_DHT22, "1", 2); this->addKey(KEY_MAX_DHT11, "1", 2); this->addKey(KEY_MULTI_MAX_DS18B20, "1", 2); + this->addKey(KEY_MAX_ROLLERSHUTTER, "0", 2); this->addKey(KEY_ALTITUDE_BME280, "0", 4); this->addKey(KEY_IMPULSE_COUNTER_DEBOUNCE_TIMEOUT, "10", 4); this->addKey(KEY_MAX_IMPULSE_COUNTER, "0", 2); + this->addKey(KEY_ACTIVE_SENSOR, "0,0,0,0,0", 14); + this->addKey(KEY_BOARD, "0", 2); + this->addKey(KEY_CFG_MODE, "0", 2); - int nr; - String key; + uint8_t nr, key; for (nr = 0; nr <= 17; nr++) { - key = GPIO; - key += nr; - this->addKey(key.c_str(), "0,0,0,0,0", 14); + key = KEY_GPIO + nr; + this->addKey(key, "0,0,0,0,0", 14); } - this->addKey(KEY_ACTIVE_SENSOR, "0,0,0,0,0", 14); - this->addKey(KEY_BOARD, "0", 2); - this->addKey(KEY_CFG_MODE, "0", 2); - for (nr = 0; nr <= MAX_DS18B20; nr++) { - key = KEY_DS; - key += nr; - this->addKey(key.c_str(), MAX_DS18B20_ADDRESS_HEX); - key = KEY_DS_NAME; - key += nr; - this->addKey(key.c_str(), MAX_DS18B20_NAME); + key = KEY_DS + nr; + this->addKey(key, MAX_DS18B20_ADDRESS_HEX); + key = KEY_DS_NAME + nr; + this->addKey(key, MAX_DS18B20_NAME); } this->load(); // switch (this->load()) { @@ -203,11 +199,11 @@ SuplaConfigManager::SuplaConfigManager() { // } } -uint8_t SuplaConfigManager::addKey(const char *key, int maxLength) { +uint8_t SuplaConfigManager::addKey(uint8_t key, int maxLength) { return addKey(key, "", maxLength); } -uint8_t SuplaConfigManager::addKey(const char *key, const char *value, int maxLength) { +uint8_t SuplaConfigManager::addKey(uint8_t key, const char *value, int maxLength) { if (_optionCount == CONFIG_MAX_OPTIONS) { return E_CONFIG_MAX; } @@ -217,7 +213,7 @@ uint8_t SuplaConfigManager::addKey(const char *key, const char *value, int maxLe return E_CONFIG_OK; } -uint8_t SuplaConfigManager::addKeyAndRead(const char *key, const char *value, int maxLength) { +uint8_t SuplaConfigManager::addKeyAndRead(uint8_t key, const char *value, int maxLength) { addKey(key, maxLength); if (this->loadItem(key) != E_CONFIG_OK) { this->set(key, value); @@ -225,9 +221,9 @@ uint8_t SuplaConfigManager::addKeyAndRead(const char *key, const char *value, in return E_CONFIG_OK; } -uint8_t SuplaConfigManager::deleteKey(const char *key) { +uint8_t SuplaConfigManager::deleteKey(uint8_t key) { for (int i = 0; i < _optionCount; i++) { - if (strcmp(_options[i]->getKey(), key) == 0) { + if (_options[i]->getKey() == key) { delete _options[_optionCount]; _optionCount -= 1; } @@ -281,7 +277,7 @@ uint8_t SuplaConfigManager::load() { } } -uint8_t SuplaConfigManager::loadItem(const char *key) { +uint8_t SuplaConfigManager::loadItem(uint8_t key) { if (SPIFFS.begin()) { if (SPIFFS.exists(CONFIG_FILE_PATH)) { File configFile = SPIFFS.open(CONFIG_FILE_PATH, "r"); @@ -299,7 +295,7 @@ uint8_t SuplaConfigManager::loadItem(const char *key) { configFile.read(content, length); for (i = 0; i < _optionCount; i++) { - if (strcmp(_options[i]->getKey(), key) == 0) { + if (_options[i]->getKey() == key) { _options[i]->setValue((const char *)(content + offset)); } offset += _options[i]->getLength(); @@ -369,18 +365,18 @@ bool SuplaConfigManager::isDeviceConfigured() { strcmp(this->get(KEY_LOGIN)->getValue(), "") == 0; } -ConfigOption *SuplaConfigManager::get(const char *key) { +ConfigOption *SuplaConfigManager::get(uint8_t key) { for (int i = 0; i < _optionCount; i++) { - if (strcmp(_options[i]->getKey(), key) == 0) { + if (_options[i]->getKey() == key) { return _options[i]; } } return NULL; } -bool SuplaConfigManager::set(const char *key, const char *value) { +bool SuplaConfigManager::set(uint8_t key, const char *value) { for (int i = 0; i < _optionCount; i++) { - if (strcmp(key, _options[i]->getKey()) == 0) { + if (key == _options[i]->getKey()) { _options[i]->setValue(value); return true; } @@ -388,9 +384,9 @@ bool SuplaConfigManager::set(const char *key, const char *value) { return false; } -bool SuplaConfigManager::setElement(const char *key, int index, int newvalue) { +bool SuplaConfigManager::setElement(uint8_t key, int index, int newvalue) { for (int i = 0; i < _optionCount; i++) { - if (strcmp(key, _options[i]->getKey()) == 0) { + if (key == _options[i]->getKey()) { String data = _options[i]->replaceElement(index, newvalue); _options[i]->setValue(data.c_str()); return true; diff --git a/src/SuplaConfigManager.h b/src/SuplaConfigManager.h index ce736f55..7d36b9a1 100644 --- a/src/SuplaConfigManager.h +++ b/src/SuplaConfigManager.h @@ -25,33 +25,35 @@ #define DEFAULT_SERVER "svrX.supla.org" #define DEFAULT_EMAIL "email@address.com" -#define KEY_SUPLA_GUID "GUID" -#define KEY_SUPLA_AUTHKEY "AUTHKEY" -#define KEY_WIFI_SSID "wifiSSID" -#define KEY_WIFI_PASS "wifiPass" -#define KEY_LOGIN "login" -#define KEY_LOGIN_PASS "loginPass" -#define KEY_HOST_NAME "hostName" -#define KEY_SUPLA_SERVER "suplaServer" -#define KEY_SUPLA_EMAIL "suplaEmail" -#define KEY_DS "ds" -#define KEY_DS_NAME "dsName" -#define KEY_MULTI_MAX_DS18B20 "multiMaxDs" -#define KEY_SUPLA_FUNCTION "function" -#define KEY_MAX_RELAY "maxRelay" -#define KEY_MAX_BUTTON "maxButton" -#define KEY_MAX_LIMIT_SWITCH "maxLimitSwitch" -#define KEY_MAX_DHT11 "maxDht11" -#define KEY_MAX_DHT22 "maxDht22" -#define KEY_MAX_ROLLERSHUTTER "maxRollerShutter" -#define KEY_ALTITUDE_BME280 "altbme280" -#define KEY_ACTIVE_SENSOR "sensor" -#define KEY_BOARD "board" -#define KEY_CFG_MODE "cfgmode" -#define KEY_IMPULSE_COUNTER_DEBOUNCE_TIMEOUT "icDebounceTimeout" -#define KEY_MAX_IMPULSE_COUNTER "maxicRaisingEdge" - -#define GPIO "GPIO" +enum _key { +KEY_SUPLA_GUID, +KEY_SUPLA_AUTHKEY, +KEY_WIFI_SSID, +KEY_WIFI_PASS, +KEY_LOGIN, +KEY_LOGIN_PASS, +KEY_HOST_NAME, +KEY_SUPLA_SERVER, +KEY_SUPLA_EMAIL, +KEY_MAX_RELAY, +KEY_MAX_BUTTON, +KEY_MAX_LIMIT_SWITCH, +KEY_MAX_DHT22, +KEY_MAX_DHT11, +KEY_MULTI_MAX_DS18B20, +KEY_MAX_ROLLERSHUTTER, +KEY_ALTITUDE_BME280, +KEY_IMPULSE_COUNTER_DEBOUNCE_TIMEOUT, +KEY_MAX_IMPULSE_COUNTER, +KEY_ACTIVE_SENSOR, +KEY_BOARD, +KEY_CFG_MODE, +KEY_GPIO = 40, +KEY_DS = 57, +KEY_DS_NAME +}; + +//#define GPIO "GPIO" #define SEPARATOR ',' enum _settings @@ -117,8 +119,8 @@ enum _e_onfig class ConfigOption { public: - ConfigOption(const char *key, const char *value, int maxLength); - const char *getKey(); + ConfigOption(uint8_t key, const char *value, int maxLength); + uint8_t getKey(); const char *getValue(); int getValueInt(); uint8_t *getValueBin(size_t size); @@ -131,7 +133,7 @@ class ConfigOption { String replaceElement(int index, int value); private: - char *_key; + uint8_t _key; char *_value; int _maxLength; }; @@ -139,18 +141,18 @@ class ConfigOption { class SuplaConfigManager { public: SuplaConfigManager(); - uint8_t addKey(const char *key, int maxLength); - uint8_t addKey(const char *key, const char *value, int maxLength); - uint8_t addKeyAndRead(const char *key, const char *value, int maxLength); - uint8_t deleteKey(const char *key); + uint8_t addKey(uint8_t key, int maxLength); + uint8_t addKey(uint8_t key, const char *value, int maxLength); + uint8_t addKeyAndRead(uint8_t key, const char *value, int maxLength); + uint8_t deleteKey(uint8_t key); uint8_t load(); - uint8_t loadItem(const char *key); + uint8_t loadItem(uint8_t key); uint8_t save(); void showAllValue(); - ConfigOption *get(const char *key); - bool set(const char *key, const char *value); - bool setElement(const char *key, int index, int newvalue); + ConfigOption *get(uint8_t key); + bool set(uint8_t key, const char *value); + bool setElement(uint8_t key, int index, int newvalue); bool isDeviceConfigured(); void setGUIDandAUTHKEY(); diff --git a/src/SuplaDeviceGUI.cpp b/src/SuplaDeviceGUI.cpp index b70dc6c4..28da64d2 100644 --- a/src/SuplaDeviceGUI.cpp +++ b/src/SuplaDeviceGUI.cpp @@ -89,10 +89,9 @@ void addRelayButton(int pinRelay, int pinButton, bool highIsOn) { void addDS18B20MultiThermometer(int pinNumber) { if (ConfigManager->get(KEY_MULTI_MAX_DS18B20)->getValueInt() > 1) { for (int i = 0; i < ConfigManager->get(KEY_MULTI_MAX_DS18B20)->getValueInt(); ++i) { - String ds_key = KEY_DS; - ds_key += i; - sensorDS.push_back(new DS18B20(pinNumber, ConfigManager->get(ds_key.c_str())->getValueBin(MAX_DS18B20_ADDRESS))); - supla_log(LOG_DEBUG, "Index %d - address %s", i, ConfigManager->get(ds_key.c_str())->getValue()); + uint8_t ds_key = KEY_DS + i; + sensorDS.push_back(new DS18B20(pinNumber, ConfigManager->get(ds_key)->getValueBin(MAX_DS18B20_ADDRESS))); + supla_log(LOG_DEBUG, "Index %d - address %s", i, ConfigManager->get(ds_key)->getValue()); } } else { diff --git a/src/SuplaWebPageConfig.cpp b/src/SuplaWebPageConfig.cpp index d36720f5..724b36f6 100644 --- a/src/SuplaWebPageConfig.cpp +++ b/src/SuplaWebPageConfig.cpp @@ -36,25 +36,24 @@ void SuplaWebPageConfig::handleConfigSave() { return WebServer->httpServer.requestAuthentication(); } - String key, input; + String input; + uint8_t key; input = INPUT_CFG_LED_GPIO; - key = GPIO; - key += WebServer->httpServer.arg(input).toInt(); + key = KEY_GPIO + WebServer->httpServer.arg(input).toInt(); + if (ConfigESP->getGpio(FUNCTION_CFG_LED) != WebServer->httpServer.arg(input).toInt() || WebServer->httpServer.arg(input).toInt() == OFF_GPIO) { ConfigESP->clearGpio(ConfigESP->getGpio(FUNCTION_CFG_LED)); } if (WebServer->httpServer.arg(input).toInt() != OFF_GPIO) { - key = GPIO; - key += WebServer->httpServer.arg(input).toInt(); - if (ConfigManager->get(key.c_str())->getElement(FUNCTION).toInt() == FUNCTION_OFF) { + key = KEY_GPIO + WebServer->httpServer.arg(input).toInt(); + if (ConfigManager->get(key)->getElement(FUNCTION).toInt() == FUNCTION_OFF) { ConfigESP->setGpio(WebServer->httpServer.arg(input).toInt(), 1, FUNCTION_CFG_LED, 1); } else if (ConfigESP->getGpio(FUNCTION_CFG_LED) == WebServer->httpServer.arg(input).toInt() && - ConfigManager->get(key.c_str())->getElement(FUNCTION).toInt() == FUNCTION_CFG_LED) { - key = GPIO; - key += ConfigESP->getGpio(FUNCTION_CFG_LED); + ConfigManager->get(key)->getElement(FUNCTION).toInt() == FUNCTION_CFG_LED) { + key = KEY_GPIO + ConfigESP->getGpio(FUNCTION_CFG_LED); input = INPUT_CFG_LED_LEVEL; - ConfigManager->setElement(key.c_str(), LEVEL, WebServer->httpServer.arg(input).toInt()); + ConfigManager->setElement(key, LEVEL, WebServer->httpServer.arg(input).toInt()); } else { WebServer->sendContent(supla_webpage_config(6)); @@ -63,22 +62,20 @@ void SuplaWebPageConfig::handleConfigSave() { } input = INPUT_CFG_BTN_GPIO; - key = GPIO; - key += WebServer->httpServer.arg(input).toInt(); + key = KEY_GPIO + WebServer->httpServer.arg(input).toInt(); if (ConfigESP->getGpio(FUNCTION_CFG_BUTTON) != WebServer->httpServer.arg(input).toInt() || WebServer->httpServer.arg(input).toInt() == OFF_GPIO) { ConfigESP->clearGpio(ConfigESP->getGpio(FUNCTION_CFG_BUTTON)); } if (WebServer->httpServer.arg(input).toInt() != OFF_GPIO) { - key = GPIO; - key += WebServer->httpServer.arg(input).toInt(); - if (ConfigManager->get(key.c_str())->getElement(FUNCTION).toInt() == FUNCTION_OFF || + key = KEY_GPIO + WebServer->httpServer.arg(input).toInt(); + if (ConfigManager->get(key)->getElement(FUNCTION).toInt() == FUNCTION_OFF || (ConfigESP->getGpio(FUNCTION_CFG_BUTTON) == WebServer->httpServer.arg(input).toInt() && - ConfigManager->get(key.c_str())->getElement(FUNCTION).toInt() == FUNCTION_CFG_BUTTON)) { + ConfigManager->get(key)->getElement(FUNCTION).toInt() == FUNCTION_CFG_BUTTON)) { ConfigESP->setGpio(WebServer->httpServer.arg(input).toInt(), FUNCTION_CFG_BUTTON); } else if (ConfigESP->checkBusyGpio(WebServer->httpServer.arg(input).toInt(), FUNCTION_BUTTON) == false) { - // ConfigManager->setElement(key.c_str(), CFG, 1); - ConfigESP->setGpio(key.toInt(), FUNCTION_CFG_BUTTON); + // ConfigManager->setElement(key, CFG, 1); + ConfigESP->setGpio(key, FUNCTION_CFG_BUTTON); } else { WebServer->sendContent(supla_webpage_config(6)); @@ -95,7 +92,7 @@ void SuplaWebPageConfig::handleConfigSave() { key = GPIO; key += ConfigESP->getGpio(i, FUNCTION_BUTTON); if (ConfigESP->getGpio(i, FUNCTION_BUTTON) != WebServer->httpServer.arg(input).toInt() || WebServer->httpServer.arg(input).toInt() == OFF_GPIO) - { if (ConfigManager->get(key.c_str())->getElement(CFG).toInt() == 1) { ConfigManager->setElement(key.c_str(), CFG, 0); + { if (ConfigManager->get(key)->getElement(CFG).toInt() == 1) { ConfigManager->setElement(key, CFG, 0); } } } diff --git a/src/SuplaWebPageControl.cpp b/src/SuplaWebPageControl.cpp index 725a8ddf..307a410c 100644 --- a/src/SuplaWebPageControl.cpp +++ b/src/SuplaWebPageControl.cpp @@ -145,7 +145,7 @@ void SuplaWebPageControl::handleButtonSaveSet() { return WebServer->httpServer.requestAuthentication(); } - String readUrl, nr_button, key, input; + String readUrl, nr_button, input; uint8_t place; String path = PATH_START; @@ -154,12 +154,11 @@ void SuplaWebPageControl::handleButtonSaveSet() { place = readUrl.indexOf(path); nr_button = readUrl.substring(place + path.length(), place + path.length() + 3); - key = GPIO; - key += ConfigESP->getGpio(nr_button.toInt(), FUNCTION_BUTTON); + uint8_t key = KEY_GPIO + ConfigESP->getGpio(nr_button.toInt(), FUNCTION_BUTTON); input = INPUT_BUTTON_LEVEL; input += nr_button; - ConfigManager->setElement(key.c_str(), LEVEL, WebServer->httpServer.arg(input).toInt()); + ConfigManager->setElement(key, LEVEL, WebServer->httpServer.arg(input).toInt()); switch (ConfigManager->save()) { case E_CONFIG_OK: diff --git a/src/SuplaWebPageRelay.cpp b/src/SuplaWebPageRelay.cpp index 4e3bf9a5..86e24207 100644 --- a/src/SuplaWebPageRelay.cpp +++ b/src/SuplaWebPageRelay.cpp @@ -119,7 +119,7 @@ void SuplaWebPageRelay::handleRelaySaveSet() { return WebServer->httpServer.requestAuthentication(); } - String readUrl, nr_relay, key, input; + String readUrl, nr_relay, input; uint8_t place; String path = PATH_START; @@ -128,16 +128,15 @@ void SuplaWebPageRelay::handleRelaySaveSet() { place = readUrl.indexOf(path); nr_relay = readUrl.substring(place + path.length(), place + path.length() + 3); - key = GPIO; - key += ConfigESP->getGpio(nr_relay.toInt(), FUNCTION_RELAY); + uint8_t key = KEY_GPIO + ConfigESP->getGpio(nr_relay.toInt(), FUNCTION_RELAY); input = INPUT_RELAY_MEMORY; input += nr_relay; - ConfigManager->setElement(key.c_str(), MEMORY, WebServer->httpServer.arg(input).toInt()); + ConfigManager->setElement(key, MEMORY, WebServer->httpServer.arg(input).toInt()); input = INPUT_RELAY_LEVEL; input += nr_relay; - ConfigManager->setElement(key.c_str(), LEVEL, WebServer->httpServer.arg(input).toInt()); + ConfigManager->setElement(key, LEVEL, WebServer->httpServer.arg(input).toInt()); switch (ConfigManager->save()) { case E_CONFIG_OK: diff --git a/src/SuplaWebPageSensor.cpp b/src/SuplaWebPageSensor.cpp index 7fe08b19..6cd19426 100644 --- a/src/SuplaWebPageSensor.cpp +++ b/src/SuplaWebPageSensor.cpp @@ -88,20 +88,18 @@ void SuplaWebPageSensor::handleDSSave() { return WebServer->httpServer.requestAuthentication(); } for (uint8_t i = 0; i < ConfigManager->get(KEY_MULTI_MAX_DS18B20)->getValueInt(); i++) { - String ds_key = KEY_DS; - String ds_name_key = KEY_DS_NAME; - ds_key += i; - ds_name_key += i; + uint8_t ds_key = KEY_DS + i; + uint8_t ds_name_key = KEY_DS_NAME + i; String ds = F("dschlid"); String ds_name = F("dsnameid"); ds += i; ds_name += i; - ConfigManager->set(ds_key.c_str(), WebServer->httpServer.arg(ds).c_str()); - ConfigManager->set(ds_name_key.c_str(), WebServer->httpServer.arg(ds_name).c_str()); + ConfigManager->set(ds_key, WebServer->httpServer.arg(ds).c_str()); + ConfigManager->set(ds_name_key, WebServer->httpServer.arg(ds_name).c_str()); - Supla::GUI::sensorDS[i]->setDeviceAddress(ConfigManager->get(ds_key.c_str())->getValueBin(MAX_DS18B20_ADDRESS)); + Supla::GUI::sensorDS[i]->setDeviceAddress(ConfigManager->get(ds_key)->getValueBin(MAX_DS18B20_ADDRESS)); } switch (ConfigManager->save()) { @@ -208,16 +206,14 @@ void SuplaWebPageSensor::showDS18B20(String &content, bool readonly) { content += S_TEMPERATURE; content += F(""); for (uint8_t i = 0; i < ConfigManager->get(KEY_MULTI_MAX_DS18B20)->getValueInt(); i++) { - String ds_key = KEY_DS; - String ds_name_key = KEY_DS_NAME; - ds_key += i; - ds_name_key += i; + uint8_t ds_key = KEY_DS + i; + uint8_t ds_name_key = KEY_DS_NAME + i; double temp = Supla::GUI::sensorDS[i]->getValue(); content += F("set(key.c_str(), WebServer->httpServer.arg(input).c_str()); + ConfigManager->set(key, WebServer->httpServer.arg(input).c_str()); } #endif @@ -727,7 +723,7 @@ void SuplaWebPageSensor::handleImpulseCounterSaveSet() { return WebServer->httpServer.requestAuthentication(); } - String readUrl, nr, key, input; + String readUrl, nr, input; uint8_t place; String path = PATH_START; @@ -736,16 +732,15 @@ void SuplaWebPageSensor::handleImpulseCounterSaveSet() { place = readUrl.indexOf(path); nr = readUrl.substring(place + path.length(), place + path.length() + 3); - key = GPIO; - key += ConfigESP->getGpio(nr.toInt(), FUNCTION_IMPULSE_COUNTER); + uint8_t key = KEY_GPIO + ConfigESP->getGpio(nr.toInt(), FUNCTION_IMPULSE_COUNTER); input = INPUT_IMPULSE_COUNTER_PULL_UP; input += nr; - ConfigManager->setElement(key.c_str(), MEMORY, WebServer->httpServer.arg(input).toInt()); + ConfigManager->setElement(key, MEMORY, WebServer->httpServer.arg(input).toInt()); input = INPUT_IMPULSE_COUNTER_RAISING_EDGE; input += nr; - ConfigManager->setElement(key.c_str(), LEVEL, WebServer->httpServer.arg(input).toInt()); + ConfigManager->setElement(key, LEVEL, WebServer->httpServer.arg(input).toInt()); ConfigManager->set(KEY_IMPULSE_COUNTER_DEBOUNCE_TIMEOUT, WebServer->httpServer.arg(INPUT_IMPULSE_COUNTER_DEBOUNCE_TIMEOUT).c_str()); Supla::GUI::impulseCounter[nr.toInt() - 1]->setCounter((unsigned long long)WebServer->httpServer.arg(INPUT_IMPULSE_COUNTER_CHANGE_VALUE).toInt()); diff --git a/src/SuplaWebServer.cpp b/src/SuplaWebServer.cpp index 7dd25519..c91df308 100644 --- a/src/SuplaWebServer.cpp +++ b/src/SuplaWebServer.cpp @@ -348,11 +348,10 @@ void SuplaWebServer::handleBoardSave() { ConfigManager->set(KEY_BOARD, httpServer.arg(input).c_str()); int nr; - String key; + uint8_t key; for (nr = 0; nr <= 17; nr++) { - key = GPIO; - key += nr; - ConfigManager->set(key.c_str(), "0,0,0,0,0"); + key = KEY_GPIO + nr; + ConfigManager->set(key, "0,0,0,0,0"); } chooseTemplateBoard(WebServer->httpServer.arg(input).toInt()); @@ -488,8 +487,8 @@ void SuplaWebServer::handleNotFound() { } bool SuplaWebServer::saveGPIO(const String& _input, uint8_t function, uint8_t nr, const String& input_max) { - uint8_t current_value; - String key, input; + uint8_t current_value, key; + String input; input = _input; if (nr != 0) { input += nr; @@ -498,19 +497,18 @@ bool SuplaWebServer::saveGPIO(const String& _input, uint8_t function, uint8_t nr nr = 1; } - key = GPIO; - key += WebServer->httpServer.arg(input).toInt(); + key = KEY_GPIO + WebServer->httpServer.arg(input).toInt();; if (ConfigESP->getGpio(nr, function) != WebServer->httpServer.arg(input).toInt() || WebServer->httpServer.arg(input).toInt() == OFF_GPIO) { ConfigESP->clearGpio(ConfigESP->getGpio(nr, function), function); } if (WebServer->httpServer.arg(input).toInt() != OFF_GPIO) { - if (ConfigManager->get(key.c_str())->getElement(FUNCTION).toInt() == FUNCTION_OFF) { + if (ConfigManager->get(key)->getElement(FUNCTION).toInt() == FUNCTION_OFF) { ConfigESP->setGpio(WebServer->httpServer.arg(input).toInt(), nr, function, 1); } else if (ConfigESP->getGpio(nr, function) == WebServer->httpServer.arg(input).toInt() && - ConfigManager->get(key.c_str())->getElement(FUNCTION).toInt() == function) { + ConfigManager->get(key)->getElement(FUNCTION).toInt() == function) { ConfigESP->setGpio(WebServer->httpServer.arg(input).toInt(), nr, function, ConfigESP->getLevel(nr, function)); } else { @@ -520,7 +518,7 @@ bool SuplaWebServer::saveGPIO(const String& _input, uint8_t function, uint8_t nr if (input_max != "\n") { current_value = WebServer->httpServer.arg(input_max).toInt(); - if (ConfigManager->get(key.c_str())->getElement(NR).toInt() > current_value) { + if (ConfigManager->get(key)->getElement(NR).toInt() > current_value) { ConfigESP->clearGpio(ConfigESP->getGpio(nr, function), function); } } From f3b1928b5c5d33b554f461be275bc6ea6a0dfcee Mon Sep 17 00:00:00 2001 From: krycha88 Date: Wed, 2 Dec 2020 17:44:31 +0100 Subject: [PATCH 004/165] =?UTF-8?q?zachowanie=20wstecznej=20kompatybilno?= =?UTF-8?q?=C5=9Bci=20kluczy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SuplaConfigManager.cpp | 12 ++++++----- src/SuplaConfigManager.h | 43 +++++++++++++++++++------------------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/SuplaConfigManager.cpp b/src/SuplaConfigManager.cpp index cb78f92d..7ba808aa 100644 --- a/src/SuplaConfigManager.cpp +++ b/src/SuplaConfigManager.cpp @@ -152,26 +152,27 @@ SuplaConfigManager::SuplaConfigManager() { this->addKey(KEY_HOST_NAME, DEFAULT_HOSTNAME, MAX_HOSTNAME); this->addKey(KEY_SUPLA_SERVER, DEFAULT_SERVER, MAX_SUPLA_SERVER); this->addKey(KEY_SUPLA_EMAIL, DEFAULT_EMAIL, MAX_EMAIL); + this->addKey(KEY_MAX_ROLLERSHUTTER, "0", 2); this->addKey(KEY_MAX_RELAY, "0", 2); this->addKey(KEY_MAX_BUTTON, "0", 2); this->addKey(KEY_MAX_LIMIT_SWITCH, "0", 2); this->addKey(KEY_MAX_DHT22, "1", 2); this->addKey(KEY_MAX_DHT11, "1", 2); this->addKey(KEY_MULTI_MAX_DS18B20, "1", 2); - this->addKey(KEY_MAX_ROLLERSHUTTER, "0", 2); this->addKey(KEY_ALTITUDE_BME280, "0", 4); this->addKey(KEY_IMPULSE_COUNTER_DEBOUNCE_TIMEOUT, "10", 4); this->addKey(KEY_MAX_IMPULSE_COUNTER, "0", 2); - this->addKey(KEY_ACTIVE_SENSOR, "0,0,0,0,0", 14); - this->addKey(KEY_BOARD, "0", 2); - this->addKey(KEY_CFG_MODE, "0", 2); uint8_t nr, key; - + for (nr = 0; nr <= 17; nr++) { key = KEY_GPIO + nr; this->addKey(key, "0,0,0,0,0", 14); } + + this->addKey(KEY_ACTIVE_SENSOR, "0,0,0,0,0", 14); + this->addKey(KEY_BOARD, "0", 2); + this->addKey(KEY_CFG_MODE, "0", 2); for (nr = 0; nr <= MAX_DS18B20; nr++) { key = KEY_DS + nr; @@ -179,6 +180,7 @@ SuplaConfigManager::SuplaConfigManager() { key = KEY_DS_NAME + nr; this->addKey(key, MAX_DS18B20_NAME); } + this->load(); // switch (this->load()) { // case E_CONFIG_OK: diff --git a/src/SuplaConfigManager.h b/src/SuplaConfigManager.h index 7d36b9a1..96574113 100644 --- a/src/SuplaConfigManager.h +++ b/src/SuplaConfigManager.h @@ -25,6 +25,25 @@ #define DEFAULT_SERVER "svrX.supla.org" #define DEFAULT_EMAIL "email@address.com" +#define MAX_GUID SUPLA_GUID_SIZE +#define MAX_AUTHKEY SUPLA_GUID_SIZE +#define MAX_SSID 32 +#define MAX_PASSWORD 64 +#define MIN_PASSWORD 8 +#define MAX_MLOGIN 32 +#define MAX_MPASSWORD 64 +#define MAX_HOSTNAME 32 +#define MAX_SUPLA_SERVER SUPLA_SERVER_NAME_MAXSIZE +#define MAX_EMAIL SUPLA_EMAIL_MAXSIZE +#define MAX_DS18B20_ADDRESS_HEX 16 +#define MAX_DS18B20_ADDRESS 8 +#define MAX_DS18B20_NAME 8 +#define MAX_TYPE_BUTTON 4 +#define MAX_MONOSTABLE_TRIGGER 1 +#define MAX_FUNCTION 1 +#define MAX_DS18B20 10 +#define MAX_GPIO 17 + enum _key { KEY_SUPLA_GUID, KEY_SUPLA_AUTHKEY, @@ -48,9 +67,9 @@ KEY_MAX_IMPULSE_COUNTER, KEY_ACTIVE_SENSOR, KEY_BOARD, KEY_CFG_MODE, -KEY_GPIO = 40, -KEY_DS = 57, -KEY_DS_NAME +KEY_GPIO, +KEY_DS = KEY_GPIO + MAX_GPIO + MAX_DS18B20, +KEY_DS_NAME = KEY_DS + MAX_DS18B20 }; //#define GPIO "GPIO" @@ -87,24 +106,6 @@ enum _function FUNCTION_IMPULSE_COUNTER }; -#define MAX_GUID SUPLA_GUID_SIZE -#define MAX_AUTHKEY SUPLA_GUID_SIZE -#define MAX_SSID 32 -#define MAX_PASSWORD 64 -#define MIN_PASSWORD 8 -#define MAX_MLOGIN 32 -#define MAX_MPASSWORD 64 -#define MAX_HOSTNAME 32 -#define MAX_SUPLA_SERVER SUPLA_SERVER_NAME_MAXSIZE -#define MAX_EMAIL SUPLA_EMAIL_MAXSIZE -#define MAX_DS18B20_ADDRESS_HEX 16 -#define MAX_DS18B20_ADDRESS 8 -#define MAX_DS18B20_NAME 8 -#define MAX_TYPE_BUTTON 4 -#define MAX_MONOSTABLE_TRIGGER 1 -#define MAX_FUNCTION 1 -#define MAX_DS18B20 10 - enum _e_onfig { E_CONFIG_OK, From 4cb6f67eebc96a01ec805ed41e72603465408986 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Fri, 4 Dec 2020 06:46:26 +0100 Subject: [PATCH 005/165] =?UTF-8?q?Mo=C5=BCliwo=C5=9B=C4=87=20okre=C5=9Ble?= =?UTF-8?q?nia=20=20ACTION=20dla=20przycisku?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- platformio.ini | 2 +- src/SuplaCommonPROGMEM.h | 3 +++ src/SuplaConfigESP.cpp | 16 ++++++++++++++++ src/SuplaConfigESP.h | 5 +++-- src/SuplaConfigManager.cpp | 6 +++--- src/SuplaConfigManager.h | 4 +++- src/SuplaDeviceGUI.cpp | 3 ++- src/SuplaWebPageControl.cpp | 28 ++++++++++++++++++++++++++++ src/SuplaWebPageControl.h | 1 + src/SuplaWebServer.cpp | 3 ++- src/language/de.h | 2 ++ src/language/en.h | 2 ++ src/language/es.h | 2 ++ src/language/fr.h | 2 ++ src/language/pl.h | 6 ++++-- 15 files changed, 74 insertions(+), 11 deletions(-) diff --git a/platformio.ini b/platformio.ini index dbf7d6e8..3846ea63 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = [common] -build_flags = -D BUILD_VERSION='"GUI 1.0.1"' +build_flags = -D BUILD_VERSION='"GUI 1.0.2"' -w -DATOMIC_FS_UPDATE -DBEARSSL_SSL_BASIC diff --git a/src/SuplaCommonPROGMEM.h b/src/SuplaCommonPROGMEM.h index 48902040..6210d672 100644 --- a/src/SuplaCommonPROGMEM.h +++ b/src/SuplaCommonPROGMEM.h @@ -96,6 +96,9 @@ const char REACTION_ON_RELEASE[] PROGMEM = S_REACTION_ON_RELEASE; const char REACTION_ON_CHANGE[] PROGMEM = S_REACTION_ON_CHANGE; const char* const TRIGGER_P[] PROGMEM = {REACTION_ON_PRESS, REACTION_ON_RELEASE, REACTION_ON_CHANGE}; +const char ACTION_TOGGLE[] PROGMEM = S_TOGGLE; +const char* const ACTION_P[] PROGMEM = {ON, OFF, ACTION_TOGGLE}; + const char CFG_10_PRESSES[] PROGMEM = S_CFG_10_PRESSES; const char CFG_5SEK_HOLD[] PROGMEM = S_5SEK_HOLD; const char* const CFG_MODE_P[] PROGMEM = {CFG_10_PRESSES, CFG_5SEK_HOLD}; diff --git a/src/SuplaConfigESP.cpp b/src/SuplaConfigESP.cpp index 16a0a381..67d9875a 100644 --- a/src/SuplaConfigESP.cpp +++ b/src/SuplaConfigESP.cpp @@ -278,6 +278,20 @@ int SuplaConfigESP::getMemory(int nr, int function) { } return OFF_GPIO; } + +int SuplaConfigESP::getAction(int nr, int function) { + for (uint8_t gpio = 0; gpio <= OFF_GPIO; gpio++) { + uint8_t key = KEY_GPIO + gpio; + if (ConfigManager->get(key)->getElement(FUNCTION).toInt() == function) { + if (ConfigManager->get(key)->getElement(NR).toInt() == nr) { + uint8_t action = ConfigManager->get(key)->getElement(ACTION).toInt(); + return action; + } + } + } + return OFF_GPIO; +} + bool SuplaConfigESP::checkBusyCfg(int gpio) { uint8_t key = KEY_GPIO + gpio; if (ConfigManager->get(key)->getElement(FUNCTION_CFG_LED).toInt() == 1) { @@ -324,6 +338,7 @@ void SuplaConfigESP::setGpio(uint8_t gpio, uint8_t nr, uint8_t function, uint8_t ConfigManager->setElement(key, FUNCTION, function); ConfigManager->setElement(key, LEVEL, level); ConfigManager->setElement(key, MEMORY, memory); + ConfigManager->setElement(key, ACTION, Supla::TOGGLE); // ConfigManager->setElement(key.c_str(), MEMORY, memory); // ConfigManager->setElement(key.c_str(), CFG, cfg); @@ -340,6 +355,7 @@ void SuplaConfigESP::clearGpio(uint8_t gpio, uint8_t function) { ConfigManager->setElement(key, FUNCTION, FUNCTION_OFF); ConfigManager->setElement(key, LEVEL, 0); ConfigManager->setElement(key, MEMORY, 0); + ConfigManager->setElement(key, ACTION, Supla::TOGGLE); } uint8_t SuplaConfigESP::countFreeGpio(uint8_t exception) { diff --git a/src/SuplaConfigESP.h b/src/SuplaConfigESP.h index 19218782..639d97dc 100644 --- a/src/SuplaConfigESP.h +++ b/src/SuplaConfigESP.h @@ -22,8 +22,7 @@ #include "GUI-Generic_Config.h" enum _configModeESP { NORMAL_MODE, CONFIG_MODE }; -enum _ConfigMode { CONFIG_MODE_10_ON_PRESSES, CONFIG_MODE_5SEK_HOLD }; - +enum _ConfigMode { CONFIG_MODE_10_ON_PRESSES, CONFIG_MODE_5SEK_HOLD, FACTORYRESET }; typedef struct { int status; @@ -78,6 +77,8 @@ class SuplaConfigESP : public Supla::Triggerable, public Supla::Element { int getMemory(int function) { return getMemory(1, function); } + + int getAction(int nr, int function); void factoryReset(); private: diff --git a/src/SuplaConfigManager.cpp b/src/SuplaConfigManager.cpp index 7ba808aa..0af9ff8f 100644 --- a/src/SuplaConfigManager.cpp +++ b/src/SuplaConfigManager.cpp @@ -101,9 +101,9 @@ String ConfigOption::getElement(int index) { String ConfigOption::replaceElement(int index, int newvalue) { String data = _value; - int lenght = 5; + int lenght = SETTINGSCOUNT; String table; - for (int i = 0; i < lenght; i++) { + for (int i = 0; i <= lenght; i++) { if (i == index) { table += newvalue; } @@ -167,7 +167,7 @@ SuplaConfigManager::SuplaConfigManager() { for (nr = 0; nr <= 17; nr++) { key = KEY_GPIO + nr; - this->addKey(key, "0,0,0,0,0", 14); + this->addKey(key, "0,0,0,0,0,0", 16); } this->addKey(KEY_ACTIVE_SENSOR, "0,0,0,0,0", 14); diff --git a/src/SuplaConfigManager.h b/src/SuplaConfigManager.h index 96574113..50db55cd 100644 --- a/src/SuplaConfigManager.h +++ b/src/SuplaConfigManager.h @@ -81,7 +81,9 @@ enum _settings FUNCTION, LEVEL, MEMORY, - CFG + CFG, + ACTION, + SETTINGSCOUNT }; enum _function diff --git a/src/SuplaDeviceGUI.cpp b/src/SuplaDeviceGUI.cpp index 28da64d2..5f85fbf2 100644 --- a/src/SuplaDeviceGUI.cpp +++ b/src/SuplaDeviceGUI.cpp @@ -78,7 +78,8 @@ void addRelayButton(int pinRelay, int pinButton, bool highIsOn) { button.push_back(new Supla::Control::Button(pinButton, true)); if (pinButton != OFF_GPIO) { - button[size]->addAction(Supla::TOGGLE, *relay[size], ConfigESP->getLevel(size + 1, FUNCTION_BUTTON)); + + button[size]->addAction(ConfigESP->getAction(size + 1, FUNCTION_BUTTON), *relay[size], ConfigESP->getLevel(size + 1, FUNCTION_BUTTON)); button[size]->setSwNoiseFilterDelay(50); } } diff --git a/src/SuplaWebPageControl.cpp b/src/SuplaWebPageControl.cpp index 307a410c..5e2bfaed 100644 --- a/src/SuplaWebPageControl.cpp +++ b/src/SuplaWebPageControl.cpp @@ -160,6 +160,10 @@ void SuplaWebPageControl::handleButtonSaveSet() { input += nr_button; ConfigManager->setElement(key, LEVEL, WebServer->httpServer.arg(input).toInt()); + input = INPUT_BUTTON_ACTION; + input += nr_button; + ConfigManager->setElement(key, ACTION, WebServer->httpServer.arg(input).toInt()); + switch (ConfigManager->save()) { case E_CONFIG_OK: // Serial.println(F("E_CONFIG_OK: Dane zapisane")); @@ -197,6 +201,8 @@ String SuplaWebPageControl::supla_webpage_button_set(int save) { page += F(" "); page += nr_button; page += F(""); + + page += F(""); + + page += F(""); diff --git a/src/SuplaWebPageControl.h b/src/SuplaWebPageControl.h index 88e17b98..99ef91f1 100644 --- a/src/SuplaWebPageControl.h +++ b/src/SuplaWebPageControl.h @@ -12,6 +12,7 @@ #define INPUT_BUTTON_SET "bts" #define INPUT_BUTTON_GPIO "btg" #define INPUT_BUTTON_LEVEL "icl" +#define INPUT_BUTTON_ACTION "bta" #define INPUT_LIMIT_SWITCH_GPIO "lsg" #define INPUT_MAX_BUTTON "mbt" #define INPUT_MAX_LIMIT_SWITCH "mls" diff --git a/src/SuplaWebServer.cpp b/src/SuplaWebServer.cpp index c91df308..ba35fa8a 100644 --- a/src/SuplaWebServer.cpp +++ b/src/SuplaWebServer.cpp @@ -497,7 +497,8 @@ bool SuplaWebServer::saveGPIO(const String& _input, uint8_t function, uint8_t nr nr = 1; } - key = KEY_GPIO + WebServer->httpServer.arg(input).toInt();; + key = KEY_GPIO + WebServer->httpServer.arg(input).toInt(); + ; if (ConfigESP->getGpio(nr, function) != WebServer->httpServer.arg(input).toInt() || WebServer->httpServer.arg(input).toInt() == OFF_GPIO) { ConfigESP->clearGpio(ConfigESP->getGpio(nr, function), function); diff --git a/src/language/de.h b/src/language/de.h index 68858331..7384c7d4 100644 --- a/src/language/de.h +++ b/src/language/de.h @@ -39,6 +39,7 @@ #define S_BUTTON_NR_SETTINGS "Parameter für Tasten Nr." #define S_NO_BUTTON_NR "Kein Tasten Nr." #define S_REACTION_TO "Reaktion auf" +#define S_ACTION "Action" #define S_GPIO_SETTINGS_FOR_LIMIT_SWITCH "GPIO-Einstellungen für Endschalter" #define S_LIMIT_SWITCH "ENDSCHALTER" #define S_GPIO_SETTINGS_FOR "GPIO-Parameter für" @@ -81,6 +82,7 @@ //#### SuplaCommonPROGMEM.h #### #define S_OFF "AUS" #define S_ON "EINGESCHALTET" +#define S_TOGGLE "TOGGLE" #define S_LOW "NIEDRIG" #define S_HIGH "HOCH" #define S_POSITION_MEMORY "LETZTEN STAND ZURÜCKSETZEN" diff --git a/src/language/en.h b/src/language/en.h index 08814beb..7cf2c9ad 100644 --- a/src/language/en.h +++ b/src/language/en.h @@ -39,6 +39,7 @@ #define S_BUTTON_NR_SETTINGS "Setting button nr." #define S_NO_BUTTON_NR "No button nr." #define S_REACTION_TO "Reaction to" +#define S_ACTION "Action" #define S_GPIO_SETTINGS_FOR_LIMIT_SWITCH "GPIO settings for limit switch" #define S_LIMIT_SWITCH "LIMIT SWITCH" #define S_GPIO_SETTINGS_FOR "GPIO settigs for" @@ -81,6 +82,7 @@ //#### SuplaCommonPROGMEM.h #### #define S_OFF "OFF" #define S_ON "ON" +#define S_TOGGLE "TOGGLE" #define S_LOW "LOW" #define S_HIGH "HIGH" #define S_POSITION_MEMORY "POSITION MEMORY" diff --git a/src/language/es.h b/src/language/es.h index d010616a..436af679 100644 --- a/src/language/es.h +++ b/src/language/es.h @@ -42,6 +42,7 @@ #define S_BUTTON_NR_SETTINGS "Parámetros del botón no." #define S_NO_BUTTON_NR "Falta del botón no." #define S_REACTION_TO "Reacción a" +#define S_ACTION "Action" #define S_GPIO_SETTINGS_FOR_LIMIT_SWITCH "Parámetros GPIO para el sensor de apertura" #define S_LIMIT_SWITCH "INTERRUPTOR DE LÍMITE" #define S_GPIO_SETTINGS_FOR "Parámetros GPIO para" @@ -84,6 +85,7 @@ //#### SuplaCommonPROGMEM.h #### #define S_OFF "APAGADO" #define S_ON "ENCENDIDO" +#define S_TOGGLE "TOGGLE" #define S_LOW "BAJO" #define S_HIGH "ALTO" #define S_POSITION_MEMORY "RECORDAR ESTADO" diff --git a/src/language/fr.h b/src/language/fr.h index 411644a8..f444bd0f 100644 --- a/src/language/fr.h +++ b/src/language/fr.h @@ -40,6 +40,7 @@ #define S_BUTTON_NR_SETTINGS "Bouton mettre à jour" #define S_NO_BUTTON_NR "Manquant numéro de bouton" #define S_REACTION_TO "Réaction à" +#define S_ACTION "Action" #define S_GPIO_SETTINGS_FOR_LIMIT_SWITCH "Paramètres GPIO pour capteur de limite" #define S_LIMIT_SWITCH "Capteur de limite" #define S_GPIO_SETTINGS_FOR "GPIO mettre à jour pour" @@ -82,6 +83,7 @@ // #### SuplaCommonPROGMEM.h #### #define S_OFF "ÉTEINDRE" #define S_ON "ALLUMER" +#define S_TOGGLE "TOGGLE" #define S_LOW "BASSE" #define S_HIGH "HAUT" #define S_POSITION_MEMORY "MEMOIRE DE POSITION" diff --git a/src/language/pl.h b/src/language/pl.h index 46ddc93a..ae898809 100644 --- a/src/language/pl.h +++ b/src/language/pl.h @@ -40,6 +40,7 @@ #define S_BUTTON_NR_SETTINGS "Ustawienia przycisku nr." #define S_NO_BUTTON_NR "Brak przycisku nr." #define S_REACTION_TO "Reakcja na" +#define S_ACTION "Akcja" #define S_GPIO_SETTINGS_FOR_LIMIT_SWITCH "Ustawienie GPIO dla cz. otwarcia" #define S_LIMIT_SWITCH "KRAŃCÓWKA" #define S_GPIO_SETTINGS_FOR "Ustawienie GPIO dla" @@ -80,8 +81,9 @@ #define S_DEVICE_LIMIT_EXCEEDED "Przekroczono limit urządzeń" //#### SuplaCommonPROGMEM.h #### -#define S_OFF "WYŁĄCZONY" -#define S_ON "ZAŁĄCZONY" +#define S_OFF "WYŁĄCZ" +#define S_ON "ZAŁĄCZ" +#define S_TOGGLE "PRZEŁĄCZ" #define S_LOW "ODWRÓCONE" #define S_HIGH "NORMALNE" #define S_POSITION_MEMORY "PAMIĘTAJ STAN" From ee0098812252b223d37731fd825d622bdf9730a0 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Tue, 8 Dec 2020 10:11:49 +0100 Subject: [PATCH 006/165] Pobieranie oraz wczytywanie kopi z pliku --- platformio.ini | 13 ++++-- src/GUI-Generic_Config.h | 4 +- src/Markup.cpp | 13 ++++++ src/Markup.h | 4 ++ src/SuplaCommonPROGMEM.h | 8 ++-- src/SuplaConfigManager.cpp | 2 - src/SuplaConfigManager.h | 2 + src/SuplaDeviceGUI.h | 6 +++ src/SuplaHTTPUpdateServer.cpp | 7 +++- src/SuplaWebPageDownload.cpp | 29 +++++++++++++ src/SuplaWebPageDownload.h | 10 +++++ src/SuplaWebPageTools.cpp | 29 +++++++++++++ src/SuplaWebPageTools.h | 10 +++++ src/SuplaWebPageUpload.cpp | 76 +++++++++++++++++++++++++++++++++++ src/SuplaWebPageUpload.h | 11 +++++ src/SuplaWebServer.cpp | 15 ++++--- 16 files changed, 221 insertions(+), 18 deletions(-) create mode 100644 src/SuplaWebPageDownload.cpp create mode 100644 src/SuplaWebPageDownload.h create mode 100644 src/SuplaWebPageTools.cpp create mode 100644 src/SuplaWebPageTools.h create mode 100644 src/SuplaWebPageUpload.cpp create mode 100644 src/SuplaWebPageUpload.h diff --git a/platformio.ini b/platformio.ini index 3846ea63..309ea8e0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = [common] -build_flags = -D BUILD_VERSION='"GUI 1.0.2"' +build_flags = -D BUILD_VERSION='"GUI 1.0.3"' -w -DATOMIC_FS_UPDATE -DBEARSSL_SSL_BASIC @@ -45,12 +45,11 @@ build_flags = -D BUILD_VERSION='"GUI 1.0.2"' [env] framework = arduino -platform = espressif8266 @ 2.6.0 +platform = espressif8266@2.6.0 upload_speed = 256000 monitor_speed = 74880 upload_resetmethod = nodemcu board_build.flash_mode = dout -board_build.ldscript = eagle.flash.1m64.ld lib_deps = milesburton/DallasTemperature@^3.9.1 adafruit/DHT sensor library@^1.4.0 @@ -63,19 +62,23 @@ extra_scripts = tools/copy_files.py [env:GUI_Generic_1M] board = esp8285 +board_build.ldscript = eagle.flash.1m64.ld build_flags = ${common.build_flags} [env:GUI_Generic_1M-en] board = esp8285 +board_build.ldscript = eagle.flash.1m64.ld build_flags = ${common.build_flags} -D UI_LANGUAGE=en [env:GUI_Generic_4M] board = esp12e +board_build.ldscript = eagle.flash.4m1m.ld build_flags = ${common.build_flags} [env:GUI_Generic_minimal] board = esp8285 +board_build.ldscript = eagle.flash.1m64.ld build_flags = ${common.build_flags} build_unflags = -D SUPLA_DS18B20 -D SUPLA_DHT11 @@ -90,6 +93,7 @@ build_unflags = -D SUPLA_DS18B20 [env:GUI_Generic_lite] board = esp8285 +board_build.ldscript = eagle.flash.1m64.ld build_flags = ${common.build_flags} build_unflags = -D SUPLA_DHT11 @@ -102,6 +106,7 @@ build_unflags = [env:GUI_Generic_sensors] board = esp8285 +board_build.ldscript = eagle.flash.1m64.ld build_flags = ${common.build_flags} build_unflags = -D SUPLA_RELAY -D SUPLA_BUTTON @@ -111,12 +116,14 @@ build_unflags = -D SUPLA_RELAY [env:GUI_Generic_DEBUG] board = nodemcuv2 +board_build.ldscript = eagle.flash.4m1m.ld build_flags = ${common.build_flags} -D DEBUG_MODE build_unflags = -DUSE_CUSTOM [env:GUI_Generic_blank] board = nodemcuv2 +board_build.ldscript = eagle.flash.4m1m.ld build_flags = ${common.build_flags} build_unflags = -D SUPLA_OTA -D SUPLA_RELAY diff --git a/src/GUI-Generic_Config.h b/src/GUI-Generic_Config.h index 6969251a..fb01e453 100644 --- a/src/GUI-Generic_Config.h +++ b/src/GUI-Generic_Config.h @@ -1,7 +1,7 @@ #ifndef GUI_Generic_Config_h #define GUI_Generic_Config_h -#define USE_CUSTOM +//#define USE_CUSTOM // User configuration #ifdef USE_CUSTOM @@ -15,7 +15,7 @@ #define SUPLA_OTA // Language en - english, pl - polish (default if not defined UI_LANGUAGE), es- spanish, fr - french, de - german, -#define UI_LANGUAGE de +//#define UI_LANGUAGE de #define SUPLA_RELAY #define SUPLA_BUTTON diff --git a/src/Markup.cpp b/src/Markup.cpp index a2901b00..9318a6a6 100644 --- a/src/Markup.cpp +++ b/src/Markup.cpp @@ -85,6 +85,15 @@ void addNumberBox(String& html, const String& input_id, const String& name, uint html += F("'>"); } +void addButton(String& html, const String& name, const String& url) { + html += F(""); + html += F("

"); +} + void addListGPIOBox(String& html, const String& input_id, const String& name, uint8_t function, uint8_t nr) { html += F("
"); html += F("

"); @@ -68,8 +76,7 @@ void addTextBox( return addTextBox(html, input_id, name, value_key, "", minlength, maxlength, required, readonly, false); } -void addTextBoxPassword( - String& html, const String& input_id, const String& name, uint8_t value_key, int minlength, int maxlength, bool required) { +void addTextBoxPassword(String& html, const String& input_id, const String& name, uint8_t value_key, int minlength, int maxlength, bool required) { return addTextBox(html, input_id, name, value_key, "", minlength, maxlength, required, false, true); } @@ -85,15 +92,6 @@ void addNumberBox(String& html, const String& input_id, const String& name, uint html += F("'>"); } -void addButton(String& html, const String& name, const String& url) { - html += F(""); - html += F("

"); -} - void addListGPIOBox(String& html, const String& input_id, const String& name, uint8_t function, uint8_t nr) { html += F(""); } +void addButton(String& html, const String& name, const String& url) { + html += F(""); + html += F("

"); +} + +void addButtonSubmit(String& html, const String& name) { + html += F(""); + html += F("

"); +} + String addListGPIOSelect(const char* input, uint8_t function, uint8_t nr) { String page = ""; page += F(""); - selected = ConfigESP->getLevel(FUNCTION_CFG_LED); - for (suported = 0; suported < 2; suported++) { - page += F("

"); - page += F("
"); - page += F(""); + addListBox(page, INPUT_CFG_MODE, S_CFG_MODE, CFG_MODE_P, 2, selected); + + addFormHeaderEnd(page); + addButtonSubmit(page, S_SAVE); + addFormEnd(page); + + addButton(page, S_RETURN, PATH_DEVICE_SETTINGS); + return page; } diff --git a/src/SuplaWebServer.cpp b/src/SuplaWebServer.cpp index 37b8095b..104a6a15 100644 --- a/src/SuplaWebServer.cpp +++ b/src/SuplaWebServer.cpp @@ -144,8 +144,8 @@ String SuplaWebServer::supla_webpage_start(int save) { String content = F(""); content += SuplaSaveResult(save); content += SuplaJavaScript(); - content += F("
"); + addForm(content, F("post")); addFormHeader(content, S_SETTING_WIFI_SSID); addTextBox(content, INPUT_WIFI_SSID, S_WIFI_SSID, KEY_WIFI_SSID, 0, MAX_SSID, true); addTextBoxPassword(content, INPUT_WIFI_PASS, S_WIFI_PASS, KEY_WIFI_PASS, MIN_PASSWORD, MAX_PASSWORD, true); @@ -165,20 +165,9 @@ String SuplaWebServer::supla_webpage_start(int save) { #ifdef SUPLA_ROLLERSHUTTER uint8_t maxrollershutter = ConfigManager->get(KEY_MAX_RELAY)->getValueInt(); if (maxrollershutter >= 2) { - content += F("
"); - content += F("

"); - content += S_ROLLERSHUTTERS; - content += F("

"); - content += F(""); - content += F("
"); + addFormHeader(content, S_ROLLERSHUTTERS); + addNumberBox(content, INPUT_ROLLERSHUTTER, S_QUANTITY, KEY_MAX_ROLLERSHUTTER, (maxrollershutter / 2)); + addFormHeaderEnd(content); } #endif @@ -186,20 +175,13 @@ String SuplaWebServer::supla_webpage_start(int save) { WebPageSensor->showDS18B20(content, true); #endif - content += F("
"); - content += F("
"); + addButtonSubmit(content, S_SAVE); + addFormEnd(content); addButton(content, S_DEVICE_SETTINGS, PATH_DEVICE_SETTINGS); - addButton(content, "Tools", PATH_TOOLS); + addButton(content, F("Tools"), PATH_TOOLS); + addButton(content, S_RESTART, PATH_REBOT); - content += F("
"); - content += F("
"); return content; } @@ -217,7 +199,7 @@ String SuplaWebServer::deviceSettings(int save) { content += WebServer->SuplaSaveResult(save); content += WebServer->SuplaJavaScript(PATH_DEVICE_SETTINGS); - + content += F("
"); From a12875e281b9fc6e55c3049ef76eb951510ddc54 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Sat, 19 Dec 2020 18:27:03 +0100 Subject: [PATCH 016/165] wsparcie dla OLEDa --- platformio.ini | 6 +- src/GUI-Generic.ino | 14 +- src/GUI-Generic_Config.h | 1 + src/Markup.cpp | 8 +- src/Markup.h | 2 +- src/SuplaConfigESP.cpp | 10 +- src/SuplaConfigESP.h | 1 + src/SuplaDeviceGUI.cpp | 7 +- src/SuplaDeviceGUI.h | 11 ++ src/SuplaOled.cpp | 290 +++++++++++++++++++++++++++++++++++++ src/SuplaOled.h | 67 +++++++++ src/SuplaWebPageSensor.cpp | 55 ++++--- src/SuplaWebPageSensor.h | 4 +- src/language/pl.h | 2 +- 14 files changed, 435 insertions(+), 43 deletions(-) create mode 100644 src/SuplaOled.cpp create mode 100644 src/SuplaOled.h diff --git a/platformio.ini b/platformio.ini index 5fb67a51..63ceb725 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = [common] -build_flags = -D BUILD_VERSION='"GUI 1.0.4"' +build_flags = -D BUILD_VERSION='"GUI 1.0.6"' -w -DATOMIC_FS_UPDATE -DBEARSSL_SSL_BASIC @@ -41,7 +41,8 @@ build_flags = -D BUILD_VERSION='"GUI 1.0.4"' -D SUPLA_SI7021 -D SUPLA_MAX6675 -D SUPLA_HC_SR04 - -D SUPLA_IMPULSE_COUNTER + -D SUPLA_IMPULSE_COUNTER + -D SUPLA_OLED [env] framework = arduino @@ -58,6 +59,7 @@ lib_deps = datacute/DoubleResetDetector@^1.0.3 closedcube/ClosedCube SHT31D@^1.5.1 adafruit/Adafruit Si7021 Library@^1.3.0 + thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays @ ^4.1.0 extra_scripts = tools/copy_files.py [env:GUI_Generic_1M] diff --git a/src/GUI-Generic.ino b/src/GUI-Generic.ino index 7466b190..dacd6309 100644 --- a/src/GUI-Generic.ino +++ b/src/GUI-Generic.ino @@ -149,18 +149,18 @@ void setup() { defined(SUPLA_BH1750) || defined(SUPLA_MAX44009) if (ConfigESP->getGpio(FUNCTION_SDA) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_SCL) != OFF_GPIO) { Wire.begin(ConfigESP->getGpio(FUNCTION_SDA), ConfigESP->getGpio(FUNCTION_SCL)); - #ifdef SUPLA_BME280 + switch (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_BME280).toInt()) { case BME280_ADDRESS_0X76: - new Supla::Sensor::BME280(0x76, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt()); + Supla::GUI::sensorBme280.push_back(new Supla::Sensor::BME280(0x76, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt())); break; case BME280_ADDRESS_0X77: - new Supla::Sensor::BME280(0x77, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt()); + Supla::GUI::sensorBme280.push_back(new Supla::Sensor::BME280(0x77, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt())); break; case BME280_ADDRESS_0X76_AND_0X77: - new Supla::Sensor::BME280(0x76, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt()); - new Supla::Sensor::BME280(0x77, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt()); + Supla::GUI::sensorBme280.push_back(new Supla::Sensor::BME280(0x76, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt())); + Supla::GUI::sensorBme280.push_back(new Supla::Sensor::BME280(0x77, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt())); break; } #endif @@ -207,6 +207,10 @@ void setup() { #endif +#ifdef SUPLA_OLED + new SuplaOled(); +#endif + Supla::GUI::begin(); } diff --git a/src/GUI-Generic_Config.h b/src/GUI-Generic_Config.h index fb01e453..537d5328 100644 --- a/src/GUI-Generic_Config.h +++ b/src/GUI-Generic_Config.h @@ -33,6 +33,7 @@ #define SUPLA_BME280 #define SUPLA_SHT3x #define SUPLA_SI7021 +#define SUPLA_OLED // #define SUPLA_HTU21D // 0x40 NOT SUPPORTED // #define SUPLA_SHT71 // 0x44 AND 0x45 NOT SUPPORTED // #define SUPLA_BH1750 // 0x23 AND 0x5C NOT SUPPORTED diff --git a/src/Markup.cpp b/src/Markup.cpp index 51689b05..18e63b89 100644 --- a/src/Markup.cpp +++ b/src/Markup.cpp @@ -11,9 +11,11 @@ void addFormEnd(String& html) { void addFormHeader(String& html, const String& name) { html += F("
"); - html += F("

"); - html += name; - html += F("

"); + if (name != "\n") { + html += F("

"); + html += name; + html += F("

"); + } } void addFormHeaderEnd(String& html) { diff --git a/src/Markup.h b/src/Markup.h index 9cfe2771..f794c7b8 100644 --- a/src/Markup.h +++ b/src/Markup.h @@ -6,7 +6,7 @@ void addForm(String& html, const String& method, const String& action = "/"); void addFormEnd(String& html); -void addFormHeader(String& html, const String& name); +void addFormHeader(String& html, const String& name = "\n"); void addFormHeaderEnd(String& html); void addTextBox(String& html, diff --git a/src/SuplaConfigESP.cpp b/src/SuplaConfigESP.cpp index 36e09b12..08bfc445 100644 --- a/src/SuplaConfigESP.cpp +++ b/src/SuplaConfigESP.cpp @@ -88,7 +88,7 @@ void SuplaConfigESP::runAction(int event, int action) { } if (configModeESP == CONFIG_MODE) { - if (event == Supla::ON_CLICK_1) { + if (event == Supla::ON_CLICK_1) { rebootESP(); } } @@ -113,11 +113,13 @@ void SuplaConfigESP::configModeInit() { void SuplaConfigESP::iterateAlways() { if (configModeESP == CONFIG_MODE) { - String CONFIG_WIFI_NAME = "SUPLA-ESP8266-" + getMacAddress(false); - WiFi.softAP(CONFIG_WIFI_NAME, ""); + WiFi.softAP(getConfigNameAP(), ""); } } +String SuplaConfigESP::getConfigNameAP() { + return "SUPLA-ESP8266-" + getMacAddress(false); +} const char *SuplaConfigESP::getLastStatusSupla() { return supla_status.msg; } @@ -414,6 +416,6 @@ void SuplaConfigESP::factoryReset(bool forceReset) { ConfigManager->save(); - // rebootESP(); + // rebootESP(); } } diff --git a/src/SuplaConfigESP.h b/src/SuplaConfigESP.h index 969a3783..1ed1e307 100644 --- a/src/SuplaConfigESP.h +++ b/src/SuplaConfigESP.h @@ -80,6 +80,7 @@ class SuplaConfigESP : public Supla::Triggerable, public Supla::Element { int getAction(int nr, int function); void factoryReset(bool forceReset = false); + String getConfigNameAP(); private: void configModeInit(); diff --git a/src/SuplaDeviceGUI.cpp b/src/SuplaDeviceGUI.cpp index 5f85fbf2..6a25ba62 100644 --- a/src/SuplaDeviceGUI.cpp +++ b/src/SuplaDeviceGUI.cpp @@ -76,9 +76,8 @@ void addRelayButton(int pinRelay, int pinButton, bool highIsOn) { relay[size]->keepTurnOnDuration(); eeprom.setStateSavePeriod(TIME_SAVE_PERIOD_SEK * 1000); - button.push_back(new Supla::Control::Button(pinButton, true)); if (pinButton != OFF_GPIO) { - + button.push_back(new Supla::Control::Button(pinButton, true)); button[size]->addAction(ConfigESP->getAction(size + 1, FUNCTION_BUTTON), *relay[size], ConfigESP->getLevel(size + 1, FUNCTION_BUTTON)); button[size]->setSwNoiseFilterDelay(50); } @@ -163,7 +162,9 @@ std::vector button; #ifdef SUPLA_DS18B20 std::vector sensorDS; #endif - +#ifdef SUPLA_BME280 +std::vector sensorBme280; +#endif } // namespace GUI } // namespace Supla diff --git a/src/SuplaDeviceGUI.h b/src/SuplaDeviceGUI.h index 7c1142e8..c2c250a7 100644 --- a/src/SuplaDeviceGUI.h +++ b/src/SuplaDeviceGUI.h @@ -44,6 +44,13 @@ #include +#ifdef SUPLA_BME280 +#include +#include "SuplaWebPageSensor.h" +#endif + +#include "SuplaOled.h" + namespace Supla { namespace GUI { @@ -78,8 +85,12 @@ extern std::vector RollerShutterButtonClose; #ifdef SUPLA_IMPULSE_COUNTER extern std::vector impulseCounter; void addImpulseCounter(int pin, bool lowToHigh, bool inputPullup, unsigned int debounceDelay); +#endif +#ifdef SUPLA_BME280 +extern std::vector sensorBme280; #endif + }; // namespace GUI }; // namespace Supla diff --git a/src/SuplaOled.cpp b/src/SuplaOled.cpp new file mode 100644 index 00000000..e4359fcc --- /dev/null +++ b/src/SuplaOled.cpp @@ -0,0 +1,290 @@ +#include "SuplaOled.h" +#include "SuplaDeviceGUI.h" + +#ifdef SUPLA_OLED +uint8_t* framesCountSensor; + +String getTempString(double temperature) { + if (temperature == -275) { + return F("error"); + } + else { + return String(temperature, 1); + } +} + +String getHumidityString(double humidity) { + if (humidity == -1) { + return F("error"); + } + else { + return String(humidity, 1); + } +} + +String getPressureString(double pressure) { + if (pressure == -1) { + return F("error"); + } + else { + return String(floor(pressure), 0); + } +} + +uint8_t getFramesCountSensor(OLEDDisplayUiState* state) { + if (state->frameState) { + return framesCountSensor[state->currentFrame]; + } + return 0; +} + +int32_t readRssi(void) { + int32_t rssi = WiFi.RSSI(); + if (WiFi.status() != WL_CONNECTED) + return -1; + if (rssi <= -100) + return 0; + if (rssi >= -50) + return 100; + return (2 * (rssi + 100)); +} + +void displaySignal(OLEDDisplay* display) { + int x = display->getWidth() - 17; + int y = 0; + int value = readRssi(); + // clear area only + display->setColor(BLACK); + display->fillRect(x, y, x + 46, 16); + display->setColor(WHITE); + if (value == -1) { + display->setFont(ArialMT_Plain_10); + display->drawString(x + 1, y, "x"); + } + else { + if (value > 0) + display->fillRect(x, y + 6, 3, 4); + else + display->drawRect(x, y + 6, 3, 4); + + if (value >= 25) + display->fillRect(x + 4, y + 4, 3, 6); + else + display->drawRect(x + 4, y + 4, 3, 6); + + if (value >= 50) + display->fillRect(x + 8, y + 2, 3, 8); + else + display->drawRect(x + 8, y + 2, 3, 8); + + if (value >= 75) + display->fillRect(x + 12, y, 3, 10); + else + display->drawRect(x + 12, y, 3, 10); + } +} + +void displayRelayState(OLEDDisplay* display) { + int y = 0; + int x = 0; + + display->setFont(ArialMT_Plain_10); + display->setTextAlignment(TEXT_ALIGN_LEFT); + for (int i = 0; i < Supla::GUI::relay.size(); i++) { + if (Supla::GUI::relay[i]->isOn()) { + display->setColor(WHITE); + display->fillRect(x, y + 1, 10, 10); + display->setColor(BLACK); + display->drawString(x + 2, y, String(i + 1)); + } + else { + display->setColor(WHITE); + display->drawString(x + 2, y, String(i + 1)); + } + x += 15; + } + display->setColor(WHITE); + display->drawHorizontalLine(0, 14, display->getWidth()); +} + +void msOverlay(OLEDDisplay* display, OLEDDisplayUiState* state) { + displaySignal(display); + if (Supla::GUI::relay.size()) { + displayRelayState(display); + } +} + +void displaySuplaStatus(OLEDDisplay* display) { + int x = 0; + int y = display->getHeight() / 3; + display->clear(); + + displaySignal(display); + + display->setFont(ArialMT_Plain_10); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setColor(WHITE); + display->drawStringMaxWidth(x, y, display->getWidth(), ConfigESP->supla_status.msg); + display->display(); +} + +void displayConfigMode(OLEDDisplay* display) { + display->clear(); + display->setFont(ArialMT_Plain_10); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setColor(WHITE); + display->drawString(0, 15, F("Tryb konfiguracyjny")); + display->drawString(0, 28, F("AP name:")); + display->drawString(0, 41, ConfigESP->getConfigNameAP()); + display->drawString(0, 54, F("IP: 192.168.4.1")); + display->display(); +} + +void displayBlank(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + // display->drawXbm(10, 17, supla_logo_width, supla_logo_height, supla_logo_bits); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(ArialMT_Plain_16); + display->drawString(10, display->getHeight() / 2, F("SUPLA")); +} + +void displayTemp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double temp, const String& name = "\n") { + int drawHeightIcon = display->getHeight() / 2 - 10; + int drawStringIcon = display->getHeight() / 2 - 5; + + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawXbm(x + 0, y + drawHeightIcon, temp_width, temp_height, temp_bits); + display->setFont(ArialMT_Plain_10); + if (name != NULL) { + display->drawString(x + temp_width + 20, y + display->getHeight() / 2 - 15, name); + } + display->setFont(ArialMT_Plain_24); + display->drawString(x + temp_width + 10, y + drawStringIcon, getTempString(temp)); + display->setFont(ArialMT_Plain_16); + display->drawString(x + temp_width + 10 + (getTempString(temp).length() * 12), y + drawStringIcon, "ºC"); +} + +void displaHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double humidity) { + int drawHeightIcon = display->getHeight() / 2 - 10; + int drawStringIcon = display->getHeight() / 2 - 5; + + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawXbm(x + 0, y + drawHeightIcon, humidity_width, humidity_height, humidity_bits); + display->setFont(ArialMT_Plain_24); + display->drawString(x + humidity_width + 20, y + drawStringIcon, getHumidityString(humidity)); + display->setFont(ArialMT_Plain_16); + display->drawString(x + humidity_width + 20 + (getHumidityString(humidity).length() * 12), y + drawStringIcon, "%"); +} + +void displayPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double pressure) { + int drawHeightIcon = display->getHeight() / 2 - 10; + int drawStringIcon = display->getHeight() / 2 - 5; + + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawXbm(x + 0, y + drawHeightIcon, pressure_width, pressure_height, pressure_bits); + display->setFont(ArialMT_Plain_24); + display->drawString(x + pressure_width + 15, y + drawStringIcon, getPressureString(pressure)); + display->setFont(ArialMT_Plain_10); + display->drawString(x + pressure_width + 15 + (getPressureString(pressure).length() * 14), y + drawStringIcon, "hPa"); +} + +void displayDs18b20(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + displayTemp(display, state, x, y, Supla::GUI::sensorDS[getFramesCountSensor(state)]->getValue(), + String(ConfigManager->get(KEY_DS_NAME + getFramesCountSensor(state))->getValue())); +} + +void displayBme280Temp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + displayTemp(display, state, x, y, Supla::GUI::sensorBme280[getFramesCountSensor(state)]->getTemp()); +} + +void displayBme280Humidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + displaHumidity(display, state, x, y, Supla::GUI::sensorBme280[getFramesCountSensor(state)]->getHumi()); +} + +void displayBme280Pressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + displayPressure(display, state, x, y, Supla::GUI::sensorBme280[getFramesCountSensor(state)]->getPressure()); +} + +SuplaOled::SuplaOled() { + if (ConfigESP->getGpio(FUNCTION_SDA) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_SCL) != OFF_GPIO) { + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt()) { + display = new SH1106Wire(0x3c, ConfigESP->getGpio(FUNCTION_SDA), ConfigESP->getGpio(FUNCTION_SCL)); + } + else { + display = new SSD1306Wire(0x3c, ConfigESP->getGpio(FUNCTION_SDA), ConfigESP->getGpio(FUNCTION_SCL)); + } + + ui = new OLEDDisplayUi(display); + + overlays[0] = {msOverlay}; + + int maxFrame = Supla::GUI::sensorDS.size() + (Supla::GUI::sensorBme280.size() * 3); + if (maxFrame == 0) + maxFrame = 1; + + frames = (FrameCallback*)malloc(sizeof(FrameCallback) * maxFrame); + framesCountSensor = (uint8_t*)malloc(sizeof(uint8_t) * maxFrame); + + for (int i = 0; i < Supla::GUI::sensorDS.size(); i++) { + frames[frameCount] = {displayDs18b20}; + framesCountSensor[frameCount] = i; + frameCount += 1; + } + + for (int i = 0; i < Supla::GUI::sensorBme280.size(); i++) { + frames[frameCount] = {displayBme280Temp}; + framesCountSensor[frameCount] = i; + frameCount += 1; + frames[frameCount] = {displayBme280Humidity}; + framesCountSensor[frameCount] = i; + frameCount += 1; + frames[frameCount] = {displayBme280Pressure}; + framesCountSensor[frameCount] = i; + frameCount += 1; + } + + if (frameCount == 0) { + frames[frameCount] = {displayBlank}; + frameCount += 1; + } + + if (frameCount == 1) { + ui->disableAllIndicators(); + ui->disableAutoTransition(); + } + else { + ui->setIndicatorPosition(BOTTOM); + ui->setIndicatorDirection(LEFT_RIGHT); + ui->setFrameAnimation(SLIDE_LEFT); + } + + ui->setTargetFPS(60); + ui->setFrames(frames, frameCount); + ui->setOverlays(overlays, overlaysCount); + ui->init(); + display->flipScreenVertically(); + } +} + +void SuplaOled::iterateAlways() { + if (ConfigESP->getGpio(FUNCTION_SDA) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_SCL) != OFF_GPIO) { + + if (ConfigESP->supla_status.status != STATUS_REGISTERED_AND_READY) { + displaySuplaStatus(display); + return; + } + + if (ConfigESP->configModeESP == NORMAL_MODE) { + int remainingTimeBudget = ui->update(); + + if (remainingTimeBudget > 0) + delay(remainingTimeBudget); + } + else { + displayConfigMode(display); + } + } +} +#endif \ No newline at end of file diff --git a/src/SuplaOled.h b/src/SuplaOled.h new file mode 100644 index 00000000..283e76b0 --- /dev/null +++ b/src/SuplaOled.h @@ -0,0 +1,67 @@ +#ifndef SuplaOled_H +#define SuplaOled_H + +#ifdef SUPLA_OLED + +#include "GUI-Generic_Config.h" +#include +#include +#include // Only needed for Arduino 1.6.5 and earlier +#include "SSD1306Wire.h" //OLED 0,96" +#include "SH1106Wire.h" //OLED 1.3" +#include "OLEDDisplayUi.h" + +const char SSD1306[] PROGMEM = "SSD1306 - 0,96''"; +const char SH1106[] PROGMEM = "SH1106 - 1,3''"; +const char* const OLED_P[] PROGMEM = {SSD1306, SH1106}; + +class SuplaOled : public Supla::Element { + public: + SuplaOled(); + + private: + void iterateAlways(); + OLEDDisplay *display; + OLEDDisplayUi *ui; + + FrameCallback *frames; + int frameCount = 0; + OverlayCallback overlays[1]; + int overlaysCount = 1; + + int count = 0; +}; + +// https://www.online-utility.org/image/convert/to/XBM +#define temp_width 32 +#define temp_height 32 +const uint8_t temp_bits[] PROGMEM = {0x00, 0x3C, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0x81, 0xF8, 0x03, 0x00, 0x99, 0x00, + 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, 0xF8, 0x03, 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, + 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, 0xF8, 0x03, 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, + 0x99, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, + 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x80, 0x99, 0x01, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x3C, 0x06, + 0x00, 0x20, 0x7E, 0x04, 0x00, 0x20, 0x7E, 0x04, 0x00, 0x20, 0x7E, 0x04, 0x00, 0x60, 0x3C, 0x06, 0x00, 0xC0, 0x18, + 0x03, 0x00, 0x80, 0x81, 0x01, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00}; + +#define humidity_width 32 +#define humidity_height 32 +const uint8_t humidity_bits[] PROGMEM = { + 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x30, 0x0C, 0x00, 0x00, 0x38, 0x1C, 0x00, 0x00, 0x18, + 0x18, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0x40, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x80, 0x01, 0x80, 0x00, + 0x80, 0x01, 0x80, 0x01, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0x60, 0x30, 0x08, 0x06, 0x30, 0x48, 0x0C, 0x0C, 0x30, 0x48, + 0x06, 0x0C, 0x30, 0x30, 0x03, 0x0C, 0x30, 0x80, 0x01, 0x0C, 0x30, 0xC0, 0x00, 0x0C, 0x30, 0x60, 0x06, 0x0C, 0x30, 0x30, 0x09, 0x0C, + 0x30, 0x10, 0x09, 0x0C, 0x30, 0x00, 0x06, 0x0C, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x00, 0x00, 0x03, 0x80, 0x01, + 0x80, 0x01, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x0E, 0x70, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF0, 0x0F, 0x00}; + +#define pressure_width 32 +#define pressure_height 32 +const uint8_t pressure_bits[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0x0F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x80, 0x81, 0x81, 0x01, 0xC0, 0x80, 0x01, 0x03, + 0x60, 0x80, 0x01, 0x06, 0x30, 0x00, 0xC0, 0x0C, 0x30, 0x03, 0xE0, 0x18, 0x18, 0x07, 0x40, 0x10, 0x0C, 0x82, 0x01, 0x30, 0x0C, 0x80, + 0x01, 0x30, 0x0C, 0x80, 0x01, 0x30, 0x0C, 0x80, 0x01, 0x30, 0x0C, 0x80, 0x01, 0x30, 0x0C, 0x81, 0x81, 0x30, 0x8C, 0x83, 0xC1, 0x31, + 0x8C, 0x81, 0x81, 0x31, 0x08, 0xC0, 0x03, 0x10, 0x08, 0xE0, 0x07, 0x10, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x80, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +#endif +#endif // SuplaOled_H \ No newline at end of file diff --git a/src/SuplaWebPageSensor.cpp b/src/SuplaWebPageSensor.cpp index 6cd19426..f0751b0f 100644 --- a/src/SuplaWebPageSensor.cpp +++ b/src/SuplaWebPageSensor.cpp @@ -5,6 +5,7 @@ #include "SuplaCommonPROGMEM.h" #include "GUIGenericCommon.h" #include "Markup.h" +#include "SuplaOled.h" SuplaWebPageSensor *WebPageSensor = new SuplaWebPageSensor(); @@ -447,6 +448,14 @@ void SuplaWebPageSensor::handlei2cSave() { } #endif +#ifdef SUPLA_OLED + key = KEY_ACTIVE_SENSOR; + input = INPUT_OLED; + if (strcmp(WebServer->httpServer.arg(input).c_str(), "") != 0) { + ConfigManager->setElement(KEY_ACTIVE_SENSOR, SENSOR_OLED, WebServer->httpServer.arg(input).toInt()); + } +#endif + switch (ConfigManager->save()) { case E_CONFIG_OK: WebServer->sendContent(supla_webpage_i2c(1)); @@ -463,52 +472,52 @@ String SuplaWebPageSensor::supla_webpage_i2c(int save) { String page, key; page += WebServer->SuplaSaveResult(save); page += WebServer->SuplaJavaScript(PATH_I2C); - page += F(""); -#if defined(SUPLA_BME280) || defined(SUPLA_SI7021) || defined(SUPLA_SHT3x) + addForm(page, F("post"), PATH_SAVE_I2C); +#if defined(SUPLA_BME280) || defined(SUPLA_SI7021) || defined(SUPLA_SHT3x) || defined(SUPLA_OLED) addFormHeader(page, String(S_GPIO_SETTINGS_FOR) + " i2c"); addListGPIOBox(page, INPUT_SDA_GPIO, "SDA", FUNCTION_SDA); addListGPIOBox(page, INPUT_SCL_GPIO, "SCL", FUNCTION_SCL); + addFormHeaderEnd(page); if (ConfigESP->getGpio(FUNCTION_SDA) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_SCL) != OFF_GPIO) { #ifdef SUPLA_BME280 selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_BME280).toInt(); + addFormHeader(page); addListBox(page, INPUT_BME280, "BME280 adres", BME280_P, 4, selected); addNumberBox(page, INPUT_ALTITUDE_BME280, S_ALTITUDE_ABOVE_SEA_LEVEL, KEY_ALTITUDE_BME280, 1500); + addFormHeaderEnd(page); #endif #ifdef SUPLA_SHT3x selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_SHT3x).toInt(); + addFormHeader(page); addListBox(page, INPUT_SHT3x, "SHT3x", SHT3x_P, 4, selected); + addFormHeaderEnd(page); #endif #ifdef SUPLA_SI7021 selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_SI7021).toInt(); - addListBox(page, INPUT_SI7021, "Si7021", STATE_P, 2, selected); + addFormHeader(page); + addListBox(page, INPUT_SI7021, "SI7021", STATE_P, 2, selected); + addFormHeaderEnd(page); +#endif + +#ifdef SUPLA_OLED + selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt(); + addFormHeader(page); + addListBox(page, INPUT_OLED, "OLED", OLED_P, 2, selected); + addFormHeaderEnd(page); #endif } - addFormHeaderEnd(page); #endif - page += F(""); - page += F("
"); - page += F("
"); - page += F("
"); - page += F("
"); - page += F("
"); + addButtonSubmit(page, S_SAVE); + addFormEnd(page); + + addButton(page, S_RETURN, PATH_DEVICE_SETTINGS); + addButton(page, S_RESTART, PATH_REBOT); + return page; } #endif diff --git a/src/SuplaWebPageSensor.h b/src/SuplaWebPageSensor.h index 76c6e7c2..f9cbdd5e 100644 --- a/src/SuplaWebPageSensor.h +++ b/src/SuplaWebPageSensor.h @@ -26,6 +26,7 @@ #define INPUT_ALTITUDE_BME280 "abme280" #define INPUT_SHT3x "sht30" #define INPUT_SI7021 "si7021" +#define INPUT_OLED "oled" #define INPUT_SI7021_SONOFF "si7021sonoff" #define INPUT_TRIG_GPIO "trig" #define INPUT_ECHO_GPIO "echo" @@ -49,7 +50,8 @@ enum _sensor SENSOR_BME280, SENSOR_SHT3x, SENSOR_SI7021, - SENSOR_MAX6675 + SENSOR_MAX6675, + SENSOR_OLED }; #endif diff --git a/src/language/pl.h b/src/language/pl.h index ae898809..043a25ef 100644 --- a/src/language/pl.h +++ b/src/language/pl.h @@ -23,7 +23,7 @@ #define S_RELAYS "PRZEKAŹNIKI" #define S_BUTTONS "PRZYCISKI" #define S_SENSORS_1WIRE "SENSORY 1Wire" -#define S_SENSORS_I2C "SENSORY i2c" +#define S_SENSORS_I2C "i2c" #define S_SENSORS_SPI "SENSORY SPI" #define S_SENSORS_OTHER "SENSORY INNE" #define S_LED_BUTTON_CFG "LED, BUTTON CONFIG" From b060e7680a1ecd292ed61b4347fdcd26f4d52ad2 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Sat, 19 Dec 2020 21:29:02 +0100 Subject: [PATCH 017/165] =?UTF-8?q?aktualizacja=20czujnik=C3=B3w=20SHT3x,?= =?UTF-8?q?=20Si7021,=20Si7021=20for=20Sonoff,=20MAX6675=20K?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/supla/sensor/MAX6675_K.cpp | 74 ++++++++ lib/SuplaDevice/src/supla/sensor/MAX6675_K.h | 92 +++------- lib/SuplaDevice/src/supla/sensor/SHT3x.h | 65 ++++--- lib/SuplaDevice/src/supla/sensor/Si7021.h | 15 +- .../src/supla/sensor/Si7021_sonoff.cpp | 118 ++++++++++++ .../src/supla/sensor/Si7021_sonoff.h | 168 ++++-------------- 6 files changed, 298 insertions(+), 234 deletions(-) create mode 100644 lib/SuplaDevice/src/supla/sensor/MAX6675_K.cpp create mode 100644 lib/SuplaDevice/src/supla/sensor/Si7021_sonoff.cpp diff --git a/lib/SuplaDevice/src/supla/sensor/MAX6675_K.cpp b/lib/SuplaDevice/src/supla/sensor/MAX6675_K.cpp new file mode 100644 index 00000000..4f610197 --- /dev/null +++ b/lib/SuplaDevice/src/supla/sensor/MAX6675_K.cpp @@ -0,0 +1,74 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include "MAX6675_K.h" + +namespace Supla { +namespace Sensor { +MAX6675_K::MAX6675_K(uint8_t pin_CLK, uint8_t pin_CS, uint8_t pin_DO) + : pin_CLK(pin_CLK), pin_CS(pin_CS), pin_DO(pin_DO) { +} + +double MAX6675_K::getValue() { + uint16_t value; + + digitalWrite(pin_CS, LOW); + delay(1); + + value = spiRead(); + value <<= 8; + value |= spiRead(); + + digitalWrite(pin_CS, HIGH); + + if (value & 0x4) { // this means there is no probe connected to Max6675 + Serial.print(F("no probe connected to Max6675")); + return TEMPERATURE_NOT_AVAILABLE; + } + value >>= 3; + + return value * 0.25; +} + +void MAX6675_K::onInit() { + digitalWrite(pin_CS, HIGH); + + pinMode(pin_CS, OUTPUT); + pinMode(pin_CLK, OUTPUT); + pinMode(pin_DO, INPUT); + + channel.setNewValue(getValue()); +} + +byte MAX6675_K::spiRead() { + int i; + byte d = 0; + + for (i = 7; i >= 0; i--) { + digitalWrite(pin_CLK, LOW); + delay(1); + if (digitalRead(pin_DO)) { + d |= (1 << i); + } + + digitalWrite(pin_CLK, HIGH); + delay(1); + } + return d; +} + +}; // namespace Sensor +}; // namespace Supla \ No newline at end of file diff --git a/lib/SuplaDevice/src/supla/sensor/MAX6675_K.h b/lib/SuplaDevice/src/supla/sensor/MAX6675_K.h index e0f52438..868a9bff 100644 --- a/lib/SuplaDevice/src/supla/sensor/MAX6675_K.h +++ b/lib/SuplaDevice/src/supla/sensor/MAX6675_K.h @@ -1,80 +1,42 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + #ifndef _max6675_k_h #define _max6675_k_h -#if defined(ESP8266) -#include -#endif -#ifdef avr -#include -#endif - -#include -#include "supla/channel.h" -#include "supla/sensor/thermometer.h" +#include +#include namespace Supla { namespace Sensor { class MAX6675_K : public Thermometer { public: - MAX6675_K(uint8_t pin_CLK, uint8_t pin_CS, uint8_t pin_DO) { - _pin_CLK = pin_CLK; - _pin_CS = pin_CS; - _pin_DO = pin_DO; - } - - double getValue() { - uint16_t value; - - digitalWrite(_pin_CS, LOW); - delay(1); - - value = spi_read(); - value <<= 8; - value |= spi_read(); - - digitalWrite(_pin_CS, HIGH); - - if (value & 0x4) { - return -275; - } - value >>= 3; - - return value * 0.25; - } + MAX6675_K(uint8_t pin_CLK, uint8_t pin_CS, uint8_t pin_DO); + double getValue(); - void onInit() { - pinMode(_pin_CS, OUTPUT); - pinMode(_pin_CLK, OUTPUT); - pinMode(_pin_DO, INPUT); - - digitalWrite(_pin_CS, HIGH); - - channel.setNewValue(getValue()); - } - - byte spi_read(void) { - int i; - byte d = 0; - - for (i = 7; i >= 0; i--) { - digitalWrite(_pin_CLK, LOW); - delay(1); - if (digitalRead(_pin_DO)) { - d |= (1 << i); - } - - digitalWrite(_pin_CLK, HIGH); - delay(1); - } - - return d; - } + private: + void onInit(); + byte spiRead(); protected: - int8_t _pin_CLK; - int8_t _pin_CS; - int8_t _pin_DO; + int8_t pin_CLK; + int8_t pin_CS; + int8_t pin_DO; }; + }; // namespace Sensor }; // namespace Supla diff --git a/lib/SuplaDevice/src/supla/sensor/SHT3x.h b/lib/SuplaDevice/src/supla/sensor/SHT3x.h index 9f25c919..3406ac14 100644 --- a/lib/SuplaDevice/src/supla/sensor/SHT3x.h +++ b/lib/SuplaDevice/src/supla/sensor/SHT3x.h @@ -17,58 +17,73 @@ #ifndef _sht3x_h #define _sht3x_h -// Dependency: Risele SHT3x library - use library manager to install it +// Dependency: ClosedCube SHT3x library - use library manager to install it // https://github.com/closedcube/ClosedCube_SHT31D_Arduino -#include "ClosedCube_SHT31D.h" +#include #include "therm_hygro_meter.h" + namespace Supla { namespace Sensor { class SHT3x : public ThermHygroMeter { public: - SHT3x(int8_t address = 0x44) : address(address) { + SHT3x(int8_t address = 0x44) + : temperature(TEMPERATURE_NOT_AVAILABLE), + humidity(HUMIDITY_NOT_AVAILABLE), + address(address), + retryCount(0) { } double getTemp() { - float value = TEMPERATURE_NOT_AVAILABLE; + return temperature; + } - SHT31D result = sht.readTempAndHumidity( - SHT3XD_REPEATABILITY_LOW, SHT3XD_MODE_CLOCK_STRETCH, 50); + double getHumi() { + return humidity; + } - if (result.error == SHT3XD_NO_ERROR) { - value = result.t; - } else { - Serial.print("SHT [ERROR] Code #"); - Serial.println(result.error); + private: + void iterateAlways() { + if (millis() - lastReadTime > 10000) { + lastReadTime = millis(); + readValuesFromDevice(); + channel.setNewValue(getTemp(), getHumi()); } - return value; } - double getHumi() { - float value = HUMIDITY_NOT_AVAILABLE; + void onInit() { + sht.begin(address); + readValuesFromDevice(); + channel.setNewValue(getTemp(), getHumi()); + } + void readValuesFromDevice() { SHT31D result = sht.readTempAndHumidity( SHT3XD_REPEATABILITY_LOW, SHT3XD_MODE_CLOCK_STRETCH, 50); - if (result.error == SHT3XD_NO_ERROR) { - value = result.rh; - } else { - Serial.print("SHT [ERROR] Code #"); + if (result.error != SHT3XD_NO_ERROR) { + Serial.print(F("SHT [ERROR] Code #")); Serial.println(result.error); + retryCount++; + if (retryCount > 3) { + retryCount = 0; + temperature = TEMPERATURE_NOT_AVAILABLE; + humidity = HUMIDITY_NOT_AVAILABLE; + } + } else { + retryCount = 0; + temperature = result.t; + humidity = result.rh; } - return value; - } - - void onInit() { - sht.begin(address); - - channel.setNewValue(getTemp(), getHumi()); } protected: int8_t address; + double temperature; + double humidity; + int8_t retryCount; ::ClosedCube_SHT31D sht; // I2C }; diff --git a/lib/SuplaDevice/src/supla/sensor/Si7021.h b/lib/SuplaDevice/src/supla/sensor/Si7021.h index 0b6b70f6..055e3f6f 100644 --- a/lib/SuplaDevice/src/supla/sensor/Si7021.h +++ b/lib/SuplaDevice/src/supla/sensor/Si7021.h @@ -20,7 +20,7 @@ // Dependency: Adafruid Si7021 library - use library manager to install it // https://github.com/adafruit/Adafruit_Si7021 -#include "Adafruit_Si7021.h" +#include #include "therm_hygro_meter.h" namespace Supla { @@ -53,26 +53,25 @@ class Si7021 : public ThermHygroMeter { } void onInit() { - sensor = Adafruit_Si7021(); sensor.begin(); - Serial.print("Found model "); + Serial.print(F("Found model ")); switch (sensor.getModel()) { case SI_Engineering_Samples: - Serial.print("SI engineering samples"); + Serial.print(F("SI engineering samples")); break; case SI_7013: - Serial.print("Si7013"); + Serial.print(F("Si7013")); break; case SI_7020: - Serial.print("Si7020"); + Serial.print(F("Si7020")); break; case SI_7021: - Serial.print("Si7021"); + Serial.print(F("Si7021")); break; case SI_UNKNOWN: default: - Serial.print("Unknown"); + Serial.print(F("Unknown")); } channel.setNewValue(getTemp(), getHumi()); diff --git a/lib/SuplaDevice/src/supla/sensor/Si7021_sonoff.cpp b/lib/SuplaDevice/src/supla/sensor/Si7021_sonoff.cpp new file mode 100644 index 00000000..c3391e25 --- /dev/null +++ b/lib/SuplaDevice/src/supla/sensor/Si7021_sonoff.cpp @@ -0,0 +1,118 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include "Si7021_sonoff.h" + +namespace Supla { +namespace Sensor { +Si7021Sonoff::Si7021Sonoff(int pin) + : temperature(TEMPERATURE_NOT_AVAILABLE), + humidity(HUMIDITY_NOT_AVAILABLE), + pin(pin), + retryCount(0) { +} + +double Si7021Sonoff::getTemp() { + return temperature; +} + +double Si7021Sonoff::getHumi() { + return humidity; +} + +void Si7021Sonoff::iterateAlways() { + if (millis() - lastReadTime > 10000) { + lastReadTime = millis(); + read(); + channel.setNewValue(getTemp(), getHumi()); + } +} + +void Si7021Sonoff::onInit() { + pinMode(pin, INPUT); + + delay(100); + read(); + channel.setNewValue(getTemp(), getHumi()); +} + +double Si7021Sonoff::readTemp(uint8_t* data) { + double temp = (((data[2] & 0x7F) << 8) | data[3]) * 0.1; + if (data[2] & 0x80) { + temp *= -1; + } + return temp; +} + +double Si7021Sonoff::readHumi(uint8_t* data) { + double humi = ((data[0] << 8) | data[1]) * 0.1; + return humi; +} + +bool Si7021Sonoff::read() { + uint8_t data[5] = {0}; + + yield(); + + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); + delayMicroseconds(500); + digitalWrite(pin, HIGH); + delayMicroseconds(20); + pinMode(pin, INPUT); + + uint32_t i = 0; + if (waitState(0) && waitState(1) && waitState(0)) { + for (i = 0; i < 40; i++) { + if (!waitState(1)) { + break; + } + delayMicroseconds(35); + if (digitalRead(pin) == HIGH) { + data[i / 8] |= (1 << (7 - i % 8)); + } + if (!waitState(0)) { + break; + } + } + } + + uint8_t checksum = (data[0] + data[1] + data[2] + data[3]) & 0xFF; + if (i < 40 || data[4] != checksum) { + retryCount++; + if (retryCount > 3) { + retryCount = 0; + temperature = TEMPERATURE_NOT_AVAILABLE; + humidity = HUMIDITY_NOT_AVAILABLE; + } + } else { + retryCount = 0; + temperature = readTemp(data); + humidity = readHumi(data); + } +} + +bool Si7021Sonoff::waitState(bool state) { + unsigned long timeout = micros(); + while (micros() - timeout < 100) { + if (digitalRead(pin) == state) return true; + delayMicroseconds(1); + } + return false; +} + +}; // namespace Sensor +}; // namespace Supla \ No newline at end of file diff --git a/lib/SuplaDevice/src/supla/sensor/Si7021_sonoff.h b/lib/SuplaDevice/src/supla/sensor/Si7021_sonoff.h index b07ec66d..bac9667e 100644 --- a/lib/SuplaDevice/src/supla/sensor/Si7021_sonoff.h +++ b/lib/SuplaDevice/src/supla/sensor/Si7021_sonoff.h @@ -1,154 +1,50 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + #ifndef _si7021_sonoff_h #define _si7021_sonoff_h +#include #include + namespace Supla { namespace Sensor { class Si7021Sonoff : public ThermHygroMeter { public: - Si7021Sonoff(int pin) { - _pin = pin; - pinMode(_pin, INPUT); - delay(100); - retryCountTemp = 0; - retryCountHumi = 0; - lastValidTemp = TEMPERATURE_NOT_AVAILABLE; - lastValidHumi = HUMIDITY_NOT_AVAILABLE; - } - - double getTemp() { - double value = TEMPERATURE_NOT_AVAILABLE; - ReadTemp(); - value = _temp; - if (isnan(value)) { - value = TEMPERATURE_NOT_AVAILABLE; - } - - if (value == TEMPERATURE_NOT_AVAILABLE) { - retryCountTemp++; - if (retryCountTemp > 3) { - retryCountTemp = 0; - } - else { - value = lastValidTemp; - } - } - else { - retryCountTemp = 0; - } - lastValidTemp = value; - - return value; - } - - double getHumi() { - double value = HUMIDITY_NOT_AVAILABLE; - ReadTemp(); - value = _humidity; - if (isnan(value)) { - value = HUMIDITY_NOT_AVAILABLE; - } - - if (value == HUMIDITY_NOT_AVAILABLE) { - retryCountHumi++; - if (retryCountHumi > 3) { - retryCountHumi = 0; - } - else { - value = lastValidHumi; - } - } - else { - retryCountHumi = 0; - } - lastValidHumi = value; - - return value; - } - - void iterateAlways() { - if (lastReadTime + 10000 < millis()) { - lastReadTime = millis(); - channel.setNewValue(getTemp(), getHumi()); - } - } - - void onInit() { - channel.setNewValue(getTemp(), getHumi()); - } + Si7021Sonoff(int pin); + double getTemp(); + double getHumi(); private: - bool ReadTemp() { - _temp = NAN; - _humidity = NAN; - - uint8_t d[5]; - d[0] = d[1] = d[2] = d[3] = d[4] = 0; - - pinMode(_pin, OUTPUT); - digitalWrite(_pin, LOW); - delayMicroseconds(500); - digitalWrite(_pin, HIGH); - delayMicroseconds(20); - pinMode(_pin, INPUT); - - uint32_t i = 0; - if (WaitState(0) and WaitState(1) and WaitState(0)) { - for (i = 0; i < 40; i++) { - if (!WaitState(1)) { - break; - } - delayMicroseconds(35); - if (digitalRead(_pin) == HIGH) { - d[i / 8] |= (1 << (7 - i % 8)); - } - if (!WaitState(0)) { - break; - } - } - } - - if (i < 40) { - return false; - } - - uint8_t checksum = (d[0] + d[1] + d[2] + d[3]) & 0xFF; - if (d[4] == checksum) { - _temp = (((d[2] & 0x7F) << 8) | d[3]) * 0.1; - _humidity = ((d[0] << 8) | d[1]) * 0.1; - if (d[2] & 0x80) { - _temp *= -1; - } - } - - if (isnan(_temp) || isnan(_humidity)) { - Serial.println(F("Invalid NAN reading")); - return false; - } - return true; - } - - bool WaitState(bool state) { - unsigned long timeout = micros(); - while (micros() - timeout < 100) { - if (digitalRead(_pin) == state) - return true; - delayMicroseconds(1); - } - return false; - } + void iterateAlways(); + void onInit(); + double readTemp(uint8_t* data); + double readHumi(uint8_t* data); + bool read(); + bool waitState(bool state); protected: - int8_t _pin; - double _temp = NAN, _humidity = NAN; - double lastValidTemp; - double lastValidHumi; - int8_t retryCountTemp; - int8_t retryCountHumi; + int8_t pin; + double temperature; + double humidity; + int8_t retryCount; }; }; // namespace Sensor }; // namespace Supla -#endif +#endif \ No newline at end of file From 7037e3654fa0bfd2f5cdcfba03dd27258367732b Mon Sep 17 00:00:00 2001 From: krycha88 Date: Mon, 21 Dec 2020 18:11:01 +0100 Subject: [PATCH 018/165] =?UTF-8?q?wsparcie=20kolejnych=20czujnik=C3=B3w?= =?UTF-8?q?=20dla=20OLED?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- platformio.ini | 2 +- src/GUI-Generic.ino | 43 +++-------------------- src/SuplaConfigESP.h | 1 + src/SuplaDeviceGUI.cpp | 13 +++++++ src/SuplaDeviceGUI.h | 58 ++++++++++++++++++++++++------- src/SuplaOled.cpp | 72 +++++++++++++++++++++++++++++++++++++-- src/SuplaOled.h | 20 +++++++++-- src/SuplaWebPageRelay.cpp | 1 - 8 files changed, 153 insertions(+), 57 deletions(-) diff --git a/platformio.ini b/platformio.ini index 63ceb725..dee6b5fe 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = [common] -build_flags = -D BUILD_VERSION='"GUI 1.0.6"' +build_flags = -D BUILD_VERSION='"GUI 1.0.7"' -w -DATOMIC_FS_UPDATE -DBEARSSL_SSL_BASIC diff --git a/src/GUI-Generic.ino b/src/GUI-Generic.ino index dacd6309..6498e0ba 100644 --- a/src/GUI-Generic.ino +++ b/src/GUI-Generic.ino @@ -13,40 +13,7 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#include "GUI-Generic_Config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef SUPLA_BME280 -#include -#include "SuplaWebPageSensor.h" -#endif -#ifdef SUPLA_SHT3x -#include -#endif -#ifdef SUPLA_SI7021 -#include -#endif -#ifdef SUPLA_SI7021_SONOFF -#include -#endif -#ifdef SUPLA_MAX6675 -#include -#endif -#ifdef SUPLA_IMPULSE_COUNTER -#include -#endif #include "SuplaDeviceGUI.h" -#include "SuplaWebServer.h" #define DRD_TIMEOUT 5 // Number of seconds after reset during which a subseqent reset will be considered a double reset. #define DRD_ADDRESS 0 // RTC Memory Address for the DoubleResetDetector to use @@ -61,7 +28,6 @@ void setup() { } uint8_t nr, gpio; - String key; #if defined(SUPLA_RELAY) || defined(SUPLA_ROLLERSHUTTER) uint8_t rollershutters = ConfigManager->get(KEY_MAX_ROLLERSHUTTER)->getValueInt(); @@ -122,7 +88,7 @@ void setup() { #ifdef SUPLA_DHT22 if (ConfigESP->getGpio(FUNCTION_DHT22) != OFF_GPIO && ConfigManager->get(KEY_MAX_DHT22)->getValueInt() > 0) { for (nr = 1; nr <= ConfigManager->get(KEY_MAX_DHT22)->getValueInt(); nr++) { - new Supla::Sensor::DHT(ConfigESP->getGpio(nr, FUNCTION_DHT22), DHT22); + Supla::GUI::sensorDHT22.push_back(new Supla::Sensor::DHT(ConfigESP->getGpio(nr, FUNCTION_DHT22), DHT22)); } } #endif @@ -135,7 +101,7 @@ void setup() { #ifdef SUPLA_SI7021_SONOFF if (ConfigESP->getGpio(FUNCTION_SI7021_SONOFF) != OFF_GPIO) { - new Supla::Sensor::Si7021Sonoff(ConfigESP->getGpio(FUNCTION_SI7021_SONOFF)); + Supla::GUI::sensorSi7021Sonoff.push_back(new Supla::Sensor::Si7021Sonoff(ConfigESP->getGpio(FUNCTION_SI7021_SONOFF))); } #endif @@ -190,7 +156,7 @@ void setup() { #ifdef SUPLA_MAX6675 if (ConfigESP->getGpio(FUNCTION_CLK) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_CS) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_D0) != OFF_GPIO) { - new Supla::Sensor::MAX6675_K(ConfigESP->getGpio(FUNCTION_CLK), ConfigESP->getGpio(FUNCTION_CS), ConfigESP->getGpio(FUNCTION_D0)); + Supla::GUI::sensorMAX6675_K.push_back(new Supla::Sensor::MAX6675_K(ConfigESP->getGpio(FUNCTION_CLK), ConfigESP->getGpio(FUNCTION_CS), ConfigESP->getGpio(FUNCTION_D0))); } #endif @@ -208,7 +174,8 @@ void setup() { #endif #ifdef SUPLA_OLED - new SuplaOled(); + oled = new SuplaOled(); + oled->addButtonOled(ConfigESP->getGpio(FUNCTION_CFG_BUTTON)); #endif Supla::GUI::begin(); diff --git a/src/SuplaConfigESP.h b/src/SuplaConfigESP.h index 1ed1e307..b96df291 100644 --- a/src/SuplaConfigESP.h +++ b/src/SuplaConfigESP.h @@ -17,6 +17,7 @@ #ifndef SuplaConfigESP_h #define SuplaConfigESP_h +#include "Arduino.h" #include #include #include "GUI-Generic_Config.h" diff --git a/src/SuplaDeviceGUI.cpp b/src/SuplaDeviceGUI.cpp index 6a25ba62..64c418be 100644 --- a/src/SuplaDeviceGUI.cpp +++ b/src/SuplaDeviceGUI.cpp @@ -162,10 +162,23 @@ std::vector button; #ifdef SUPLA_DS18B20 std::vector sensorDS; #endif + #ifdef SUPLA_BME280 std::vector sensorBme280; #endif +#ifdef SUPLA_SI7021_SONOFF +std::vector sensorSi7021Sonoff; +#endif + +#ifdef SUPLA_DHT22 +std::vector sensorDHT22; +#endif + +#ifdef SUPLA_MAX6675 +std::vector sensorMAX6675_K; +#endif + } // namespace GUI } // namespace Supla diff --git a/src/SuplaDeviceGUI.h b/src/SuplaDeviceGUI.h index c2c250a7..ed5bb1f5 100644 --- a/src/SuplaDeviceGUI.h +++ b/src/SuplaDeviceGUI.h @@ -19,17 +19,10 @@ #include "GUI-Generic_Config.h" -#include - -#include -#include -#include -#include "SuplaSensorDS18B20.h" -#include +#include +#include -#ifdef DEBUG_MODE -#include -#endif +#include #include "SuplaConfigESP.h" #include "SuplaConfigManager.h" @@ -41,15 +34,44 @@ #include "SuplaWebPageTools.h" #include "GUIGenericCommon.h" #include "Markup.h" +#include "SuplaOled.h" #include +#include +#include +#include + +#include "SuplaSensorDS18B20.h" +#include +#include +#include #ifdef SUPLA_BME280 #include #include "SuplaWebPageSensor.h" #endif - -#include "SuplaOled.h" +#ifdef SUPLA_SI7021_SONOFF +#include +#endif +#ifdef SUPLA_BME280 +#include +#include "SuplaWebPageSensor.h" +#endif +#ifdef SUPLA_SHT3x +#include +#endif +#ifdef SUPLA_SI7021 +#include +#endif +#ifdef SUPLA_MAX6675 +#include +#endif +#ifdef SUPLA_IMPULSE_COUNTER +#include +#endif +#ifdef DEBUG_MODE +#include +#endif namespace Supla { namespace GUI { @@ -91,6 +113,18 @@ void addImpulseCounter(int pin, bool lowToHigh, bool inputPullup, unsigned int d extern std::vector sensorBme280; #endif +#ifdef SUPLA_SI7021_SONOFF +extern std::vector sensorSi7021Sonoff; +#endif + +#ifdef SUPLA_DHT22 +extern std::vector sensorDHT22; +#endif + +#ifdef SUPLA_MAX6675 +extern std::vector sensorMAX6675_K; +#endif + }; // namespace GUI }; // namespace Supla diff --git a/src/SuplaOled.cpp b/src/SuplaOled.cpp index e4359fcc..e4bba5bd 100644 --- a/src/SuplaOled.cpp +++ b/src/SuplaOled.cpp @@ -207,6 +207,26 @@ void displayBme280Pressure(OLEDDisplay* display, OLEDDisplayUiState* state, int1 displayPressure(display, state, x, y, Supla::GUI::sensorBme280[getFramesCountSensor(state)]->getPressure()); } +void displaySi7021SonoffTemp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + displayTemp(display, state, x, y, Supla::GUI::sensorSi7021Sonoff[getFramesCountSensor(state)]->getTemp()); +} + +void displaySi7021SonoffHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + displaHumidity(display, state, x, y, Supla::GUI::sensorSi7021Sonoff[getFramesCountSensor(state)]->getHumi()); +} + +void displayDHT22Temp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + displayTemp(display, state, x, y, Supla::GUI::sensorDHT22[getFramesCountSensor(state)]->getTemp()); +} + +void displayDHT22Humidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + displaHumidity(display, state, x, y, Supla::GUI::sensorDHT22[getFramesCountSensor(state)]->getHumi()); +} + +void displayMAX6675Temp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + displayTemp(display, state, x, y, Supla::GUI::sensorMAX6675_K[getFramesCountSensor(state)]->getValue()); +} + SuplaOled::SuplaOled() { if (ConfigESP->getGpio(FUNCTION_SDA) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_SCL) != OFF_GPIO) { if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt()) { @@ -220,7 +240,9 @@ SuplaOled::SuplaOled() { overlays[0] = {msOverlay}; - int maxFrame = Supla::GUI::sensorDS.size() + (Supla::GUI::sensorBme280.size() * 3); + int maxFrame = Supla::GUI::sensorDS.size() + (Supla::GUI::sensorBme280.size() * 3) + Supla::GUI::sensorSi7021Sonoff.size() + + (Supla::GUI::sensorDHT22.size() * 2) + Supla::GUI::sensorMAX6675_K.size(); + if (maxFrame == 0) maxFrame = 1; @@ -245,6 +267,30 @@ SuplaOled::SuplaOled() { frameCount += 1; } + for (int i = 0; i < Supla::GUI::sensorSi7021Sonoff.size(); i++) { + frames[frameCount] = {displaySi7021SonoffTemp}; + framesCountSensor[frameCount] = i; + frameCount += 1; + frames[frameCount] = {displaySi7021SonoffHumidity}; + framesCountSensor[frameCount] = i; + frameCount += 1; + } + + for (int i = 0; i < Supla::GUI::sensorDHT22.size(); i++) { + frames[frameCount] = {displayDHT22Temp}; + framesCountSensor[frameCount] = i; + frameCount += 1; + frames[frameCount] = {displayDHT22Humidity}; + framesCountSensor[frameCount] = i; + frameCount += 1; + } + + for (int i = 0; i < Supla::GUI::sensorMAX6675_K.size(); i++) { + frames[frameCount] = {displayMAX6675Temp}; + framesCountSensor[frameCount] = i; + frameCount += 1; + } + if (frameCount == 0) { frames[frameCount] = {displayBlank}; frameCount += 1; @@ -270,12 +316,17 @@ SuplaOled::SuplaOled() { void SuplaOled::iterateAlways() { if (ConfigESP->getGpio(FUNCTION_SDA) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_SCL) != OFF_GPIO) { - if (ConfigESP->supla_status.status != STATUS_REGISTERED_AND_READY) { displaySuplaStatus(display); return; } + if (millis() - timeLastChangeOled > 30000 && oledON) { + display->setBrightness(50); + oledON = false; + // display.displayOff(); + } + if (ConfigESP->configModeESP == NORMAL_MODE) { int remainingTimeBudget = ui->update(); @@ -287,4 +338,21 @@ void SuplaOled::iterateAlways() { } } } + +void SuplaOled::addButtonOled(int pin) { + if (pin != OFF_GPIO) { + Supla::Control::Button* button = new Supla::Control::Button(pin, true, true); + button->addAction(TURN_ON_OLED, oled, Supla::ON_PRESS); + } +} + +void SuplaOled::runAction(int event, int action) { + if (action == TURN_ON_OLED) { + display->setBrightness(255); + timeLastChangeOled = millis(); + oledON = true; + } +} + +SuplaOled* oled; #endif \ No newline at end of file diff --git a/src/SuplaOled.h b/src/SuplaOled.h index 283e76b0..9457c9ba 100644 --- a/src/SuplaOled.h +++ b/src/SuplaOled.h @@ -5,22 +5,31 @@ #include "GUI-Generic_Config.h" #include +#include #include #include // Only needed for Arduino 1.6.5 and earlier #include "SSD1306Wire.h" //OLED 0,96" #include "SH1106Wire.h" //OLED 1.3" #include "OLEDDisplayUi.h" +enum customActions +{ + TURN_ON_OLED +}; + const char SSD1306[] PROGMEM = "SSD1306 - 0,96''"; const char SH1106[] PROGMEM = "SH1106 - 1,3''"; -const char* const OLED_P[] PROGMEM = {SSD1306, SH1106}; +const char *const OLED_P[] PROGMEM = {SSD1306, SH1106}; -class SuplaOled : public Supla::Element { +class SuplaOled : public Supla::Triggerable, public Supla::Element { public: SuplaOled(); + void addButtonOled(int pin); private: void iterateAlways(); + void runAction(int event, int action); + OLEDDisplay *display; OLEDDisplayUi *ui; @@ -30,8 +39,13 @@ class SuplaOled : public Supla::Element { int overlaysCount = 1; int count = 0; + + unsigned long timeLastChangeOled = millis(); + bool oledON = true; }; +extern SuplaOled *oled; + // https://www.online-utility.org/image/convert/to/XBM #define temp_width 32 #define temp_height 32 @@ -62,6 +76,6 @@ const uint8_t pressure_bits[] PROGMEM = { 0x01, 0x30, 0x0C, 0x80, 0x01, 0x30, 0x0C, 0x80, 0x01, 0x30, 0x0C, 0x80, 0x01, 0x30, 0x0C, 0x81, 0x81, 0x30, 0x8C, 0x83, 0xC1, 0x31, 0x8C, 0x81, 0x81, 0x31, 0x08, 0xC0, 0x03, 0x10, 0x08, 0xE0, 0x07, 0x10, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - + #endif #endif // SuplaOled_H \ No newline at end of file diff --git a/src/SuplaWebPageRelay.cpp b/src/SuplaWebPageRelay.cpp index 86e24207..75c9dd43 100644 --- a/src/SuplaWebPageRelay.cpp +++ b/src/SuplaWebPageRelay.cpp @@ -76,7 +76,6 @@ void SuplaWebPageRelay::handleRelaySave() { } String SuplaWebPageRelay::supla_webpage_relay(int save) { - String key; uint8_t selected, suported, nr; String pagerelay = ""; From 44e132b0070926a0580156ad957047de9746c09a Mon Sep 17 00:00:00 2001 From: krycha88 Date: Tue, 22 Dec 2020 12:53:32 +0100 Subject: [PATCH 019/165] fixed OLED --- platformio.ini | 3 ++- src/GUI-Generic.ino | 11 ++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/platformio.ini b/platformio.ini index dee6b5fe..725e3cd1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = [common] -build_flags = -D BUILD_VERSION='"GUI 1.0.7"' +build_flags = -D BUILD_VERSION='"GUI 1.0.8"' -w -DATOMIC_FS_UPDATE -DBEARSSL_SSL_BASIC @@ -60,6 +60,7 @@ lib_deps = closedcube/ClosedCube SHT31D@^1.5.1 adafruit/Adafruit Si7021 Library@^1.3.0 thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays @ ^4.1.0 + xoseperez/HLW8012 @ ^1.1.1 extra_scripts = tools/copy_files.py [env:GUI_Generic_1M] diff --git a/src/GUI-Generic.ino b/src/GUI-Generic.ino index 6498e0ba..e477a76e 100644 --- a/src/GUI-Generic.ino +++ b/src/GUI-Generic.ino @@ -150,6 +150,12 @@ void setup() { if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_SI7021).toInt()) { new Supla::Sensor::Si7021(); } +#endif +#ifdef SUPLA_OLED + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt()) { + oled = new SuplaOled(); + oled->addButtonOled(ConfigESP->getGpio(FUNCTION_CFG_BUTTON)); + } #endif } #endif @@ -173,11 +179,6 @@ void setup() { #endif -#ifdef SUPLA_OLED - oled = new SuplaOled(); - oled->addButtonOled(ConfigESP->getGpio(FUNCTION_CFG_BUTTON)); -#endif - Supla::GUI::begin(); } From c66a93571e5b6469abbc3996810588203e7c1d81 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Thu, 24 Dec 2020 14:22:11 +0100 Subject: [PATCH 020/165] =?UTF-8?q?dodanie=20mo=C5=BCliwo=C5=9Bci=20wy?= =?UTF-8?q?=C5=82=C4=85czenia=20OLEDa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SuplaCommonPROGMEM.h | 3 +++ src/SuplaOled.h | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/SuplaCommonPROGMEM.h b/src/SuplaCommonPROGMEM.h index 01aabf4e..e9ee41d8 100644 --- a/src/SuplaCommonPROGMEM.h +++ b/src/SuplaCommonPROGMEM.h @@ -54,6 +54,9 @@ const char HTTP_COPYRIGHT[] PROGMEM = "https://forum.supla.org/\n"; +const char HTTP_RBT[] PROGMEM = + "
"; + const char GPIO0[] PROGMEM = "GPIO0-D3"; const char GPIO1[] PROGMEM = "GPIO1-TX"; const char GPIO2[] PROGMEM = "GPIO2-D4"; diff --git a/src/SuplaOled.h b/src/SuplaOled.h index 9457c9ba..1a864cc9 100644 --- a/src/SuplaOled.h +++ b/src/SuplaOled.h @@ -17,10 +17,6 @@ enum customActions TURN_ON_OLED }; -const char SSD1306[] PROGMEM = "SSD1306 - 0,96''"; -const char SH1106[] PROGMEM = "SH1106 - 1,3''"; -const char *const OLED_P[] PROGMEM = {SSD1306, SH1106}; - class SuplaOled : public Supla::Triggerable, public Supla::Element { public: SuplaOled(); From a31a2e4694882408e34d7ddb4aa54902de6a9f11 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Thu, 24 Dec 2020 14:23:11 +0100 Subject: [PATCH 021/165] Update SuplaCommonPROGMEM.h --- src/SuplaCommonPROGMEM.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/SuplaCommonPROGMEM.h b/src/SuplaCommonPROGMEM.h index e9ee41d8..0e0b6990 100644 --- a/src/SuplaCommonPROGMEM.h +++ b/src/SuplaCommonPROGMEM.h @@ -108,6 +108,12 @@ const char CFG_10_PRESSES[] PROGMEM = S_CFG_10_PRESSES; const char CFG_5SEK_HOLD[] PROGMEM = S_5SEK_HOLD; const char* const CFG_MODE_P[] PROGMEM = {CFG_10_PRESSES, CFG_5SEK_HOLD}; +#ifdef SUPLA_OLED +const char SSD1306[] PROGMEM = "SSD1306 - 0,96''"; +const char SH1106[] PROGMEM = "SH1106 - 1,3''"; +const char *const OLED_P[] PROGMEM = {OFF, SSD1306, SH1106}; +#endif + String StateString(uint8_t adr); String LevelString(uint8_t nr); String MemoryString(uint8_t nr); From f1b7294e6b4af4ff7ceb3028fc559259d7f4c672 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Thu, 24 Dec 2020 14:24:31 +0100 Subject: [PATCH 022/165] =?UTF-8?q?Dodanie=20Restartu=20urz=C4=85dzenia=20?= =?UTF-8?q?na=20ka=C5=BCdej=20stronie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Markup.cpp | 45 +++++++++++++++++++++ src/Markup.h | 17 ++++---- src/SuplaDeviceGUI.h | 1 + src/SuplaHTTPUpdateServer.cpp | 4 +- src/SuplaWebPageConfig.cpp | 6 +-- src/SuplaWebPageControl.cpp | 16 +++----- src/SuplaWebPageRelay.cpp | 12 +++--- src/SuplaWebPageSensor.cpp | 73 ++++++++++------------------------- src/SuplaWebPageUpload.cpp | 6 +-- src/SuplaWebServer.cpp | 59 ++-------------------------- src/SuplaWebServer.h | 2 - 11 files changed, 97 insertions(+), 144 deletions(-) diff --git a/src/Markup.cpp b/src/Markup.cpp index 18e63b89..b1cba4b8 100644 --- a/src/Markup.cpp +++ b/src/Markup.cpp @@ -210,4 +210,49 @@ String addListGPIOSelect(const char* input, uint8_t function, uint8_t nr) { String getURL(const String& url) { return String(F(PATH_START)) + url; +} + +const String SuplaJavaScript(const String& java_return) { + String java_script = + F("\n"); +#ifdef SUPLA_OTA + java_script += + F("\n"); +#endif + return java_script; +} + +const String SuplaSaveResult(int save) { + if (save == 0) + return F(""); + String saveresult = ""; + saveresult += F("
"); + if (save == 1) { + saveresult += S_DATA_SAVED; + } + else if (save == 2) { + saveresult += S_RESTART_MODULE; + } + else if (save == 3) { + saveresult += S_DATA_ERASED_RESTART_DEVICE; + } + else if (save == 4) { + saveresult += S_WRITE_ERROR_UNABLE_TO_READ_FILE_FS_PARTITION_MISSING; + } + else if (save == 5) { + saveresult += S_DATA_SAVED_RESTART_MODULE; + } + else if (save == 6) { + saveresult += S_WRITE_ERROR_BAD_DATA; + } + else if (save == 7) { + saveresult += F("data saved"); + } + saveresult += F("
"); + return saveresult; } \ No newline at end of file diff --git a/src/Markup.h b/src/Markup.h index f794c7b8..1a348def 100644 --- a/src/Markup.h +++ b/src/Markup.h @@ -19,16 +19,9 @@ void addTextBox(String& html, bool required, bool readonly = false, bool password = false); -void addTextBox(String& html, - const String& input_id, - const String& name, - uint8_t value_key, - int minlength, - int maxlength, - bool required, - bool readonly = false); -void addTextBoxPassword( - String& html, const String& input_id, const String& name, uint8_t value_key, int minlength, int maxlength, bool required); +void addTextBox( + String& html, const String& input_id, const String& name, uint8_t value_key, int minlength, int maxlength, bool required, bool readonly = false); +void addTextBoxPassword(String& html, const String& input_id, const String& name, uint8_t value_key, int minlength, int maxlength, bool required); void addNumberBox(String& html, const String& input_id, const String& name, uint8_t value_key, uint16_t max); @@ -45,4 +38,8 @@ void addButtonSubmit(String& html, const String& name); String addListGPIOSelect(const char* input, uint8_t function, uint8_t nr = 0); String getURL(const String& url); + +const String SuplaJavaScript(const String& java_return = PATH_START); + +const String SuplaSaveResult(int save); #endif // Markup_h diff --git a/src/SuplaDeviceGUI.h b/src/SuplaDeviceGUI.h index ed5bb1f5..7619e957 100644 --- a/src/SuplaDeviceGUI.h +++ b/src/SuplaDeviceGUI.h @@ -33,6 +33,7 @@ #include "SuplaWebPageUpload.h" #include "SuplaWebPageTools.h" #include "GUIGenericCommon.h" +#include "SuplaCommonPROGMEM.h" #include "Markup.h" #include "SuplaOled.h" diff --git a/src/SuplaHTTPUpdateServer.cpp b/src/SuplaHTTPUpdateServer.cpp index 3c93fb1a..7a68aaf8 100644 --- a/src/SuplaHTTPUpdateServer.cpp +++ b/src/SuplaHTTPUpdateServer.cpp @@ -149,7 +149,7 @@ void ESP8266HTTPUpdateServer::handleFirmwareUp() { String ESP8266HTTPUpdateServer::suplaWebPageUpddate() { String content = ""; - content += WebServer->SuplaJavaScript(); + content += SuplaJavaScript(); content += F("
"); content += F("

"); content += S_SOFTWARE_UPDATE; @@ -165,7 +165,7 @@ String ESP8266HTTPUpdateServer::suplaWebPageUpddate() { content += PATH_TOOLS; content += F("'>

"); + content += F("

"); return content; } diff --git a/src/SuplaWebPageConfig.cpp b/src/SuplaWebPageConfig.cpp index 14265f7a..1a28163b 100644 --- a/src/SuplaWebPageConfig.cpp +++ b/src/SuplaWebPageConfig.cpp @@ -68,8 +68,8 @@ String SuplaWebPageConfig::supla_webpage_config(int save) { uint8_t selected, suported; String page = ""; - page += WebServer->SuplaSaveResult(save); - page += WebServer->SuplaJavaScript(PATH_CONFIG); + page += SuplaSaveResult(save); + page += SuplaJavaScript(PATH_CONFIG); addForm(page, F("post"), PATH_SAVE_CONFIG); addFormHeader(page, S_GPIO_SETTINGS_FOR_CONFIG); @@ -81,7 +81,7 @@ String SuplaWebPageConfig::supla_webpage_config(int save) { selected = ConfigManager->get(KEY_CFG_MODE)->getValueInt(); addListBox(page, INPUT_CFG_MODE, S_CFG_MODE, CFG_MODE_P, 2, selected); - + addFormHeaderEnd(page); addButtonSubmit(page, S_SAVE); addFormEnd(page); diff --git a/src/SuplaWebPageControl.cpp b/src/SuplaWebPageControl.cpp index 5e2bfaed..9033f567 100644 --- a/src/SuplaWebPageControl.cpp +++ b/src/SuplaWebPageControl.cpp @@ -1,7 +1,6 @@ #include "SuplaWebPageControl.h" #include "SuplaDeviceGUI.h" #include "SuplaWebServer.h" -#include "SuplaCommonPROGMEM.h" #include "GUIGenericCommon.h" #include "Markup.h" @@ -92,8 +91,8 @@ String SuplaWebPageControl::supla_webpage_control(int save) { uint8_t nr, suported, selected; String pagebutton, key; - pagebutton += WebServer->SuplaSaveResult(save); - pagebutton += WebServer->SuplaJavaScript(PATH_CONTROL); + pagebutton += SuplaSaveResult(save); + pagebutton += SuplaJavaScript(PATH_CONTROL); pagebutton += F("
"); @@ -125,7 +124,7 @@ String SuplaWebPageControl::supla_webpage_control(int save) { pagebutton += PATH_DEVICE_SETTINGS; pagebutton += F("'>"); + pagebutton += F("

"); return pagebutton; } @@ -189,8 +188,8 @@ String SuplaWebPageControl::supla_webpage_button_set(int save) { nr_button = readUrl.substring(place + path.length(), place + path.length() + 3); String page = ""; - page += WebServer->SuplaSaveResult(save); - page += WebServer->SuplaJavaScript(PATH_CONTROL); + page += SuplaSaveResult(save); + page += SuplaJavaScript(PATH_CONTROL); uint8_t buttons = ConfigManager->get(KEY_MAX_BUTTON)->getValueInt(); if (nr_button.toInt() <= buttons && ConfigESP->getGpio(nr_button.toInt(), FUNCTION_BUTTON) != OFF_GPIO) { page += F(""); - page += F(" + + + + + First char code: + + + Width: + + + Height: + + + + + + + + + + + + + + +
+ +
+
+

+ +
+
+ +
+
+
+
+ + + diff --git a/lib/esp8266-oled-ssd1306-master/resources/glyphEditor.png b/lib/esp8266-oled-ssd1306-master/resources/glyphEditor.png new file mode 100644 index 0000000000000000000000000000000000000000..800efc56f4bd50ba347eab57f3205ec8d0c0dd31 GIT binary patch literal 68751 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 M zFex8C1FB2SFed3sXJV=bggW5d3zB$kd9vzP!S6TUg}zDBa87s)sIVr2LOKXhQV5hG zfzQ+E0L)VE-V5175{xSlGuG^ZDf)Wpugt)?!0#`*Yb==ww0>daTQ;QpNP(A%&qr%H z>EF|QP7UGNyHB^fyPL0W&)$%e?$d_Y>PZn(Sf*k-KJx?DuHpX*gkR89FTcP_^1_l( zpx!+`x@Q3zHSg;OB-bemCQ9)kP-MWC=H{5&y-s{;V`PX(R4}9V0)^@t=peR?4K?NO zgqBp4IA+XFgY>DH{>W_IZD42CNq#N&>3rx1E;MIOH})*{^7jP5(*#7@R~Cm**m`8T z+dTj!zw9&W^$avagNIFNuE4t1Te0|%{t$8W>~quf9J+@Dpsk~_7BX>Tjw`nm zaGg;80CElCIFzlFB-biT2l@93IoS;R^PN)XVD&Okzk}A}?mm}kuzIvqR47Ei49H|N z%AnfN{@5q;!vsYdkNl1x5rx_^@-^hYkX%52(>_!Ep9wvqFhy*n?rgEApj5Ubb*|r` zFBS*b;1HWI804VO0}1~}LD#v3#eH_UcV#siQ^HDmE59QuSw$1Csz$goa+sZ3!7YGg z2I{JnEq@fH&j|`8N`@$Me1bcQiG9S^GU)j$l!+0%9J3@g$ZF;^@{p7c0F)iZ&qpLp!r*X?=j7mci>g(5Vlu zEili^S+CZq08DSuSm$naWc7d7-D7;eCV)rtQ%9z=D9TERk@?B?Dgn^-|4;BYv*Pjp zE%@u|18CD}k+ES6`)_ zMZw_dTdkZp=DR!XjQJ;DqbMo#`q}G!F&zEg)3GaseFR9=2$l{a>J2k14nn7DV&ovsKb_;#ErO)LVc)$zgdFJ(XmTCsuUleZ zaS1in`$lM#g5{sDk@*YzS52=)k`S}vdw+SeHeKgO=K^-g_l0q8FSfmpWOZXp@+`4I z?!4;KkV8ed+as^8^oetP>j##?nV{|A7J^u$gEAer^em_s`r@-e($;GYhJlflVtw4&UzPaUwpNY*uxI!okrB0Cjh_D zE*vl$=+0tpI?pO@>&5sLhz+^*&BTB`#3jfI-**ghydVefbBUPWsP^(*s_FXS_HXG- zoVO&EqB2t=uT403eHON_L(4*U&78IJZf#u!>oJ0t^*^RWqmojF1G>m2o|RP%DEIt8 zn0kdYXY=s!_mmT}I}i#sENHuIPGfNLso;qY{-cn>n~EcCEh!#8*akqd z8F2dBYfYf5V-uX5*C0OoEU;=FBP@C}RLpferQk}#4 z?G6XC=r@3%UsxDi)3mALS^|jE&@D`*my6#yxqdr7t(hLsIJ9_q`5SK4bm6K}cZKc2 zW_v+?K#h#lj`lRWF1W#SO*QO&Fpk~fj|^j&F5_Z}z@HLcU43{spiE!wMEBBSi6CRO z_U~$zY_<;859`6(w)q}TZgJk1Ztep}JM-s$HBTqoktubkiM64Rwj;@+Z!oUN)gN~FXeIf(SJ!KKy(WE zj)xRv+h@>_GNeTHrBbekK6`MU{Dg($i(3U3Lz&UOTHwhG+R3jhOY>ojdLg)Nikr?idA8YajD3S`oJeeCw}PPrxy_`jB! z&NgldO}|+B)l0JU`{YoD4Ap#lAjZC~?je0FpjT2@`9iR**@VHtQyeJ0VQGyQ$#qz)K^$*o>oe@jtgAfqx8C2yAUqi z7%9E*m4zc0s6;!8umay1;JqI3T2GVUW#=lfU(HHjdLc^5dRjUXZElo@Ag83z2eS1vlZ{%sQMYvmFREqWNu-8}Ir3x>(a*6fMl2Tnv!)4IaMhsW3fy z6-FRct&-m@jjj{E@(1MX;4$uqqf#%$C=G*#!KWMD- zsx8qW3XXZsxDbCyS#`DOo9?CPlY7a?T)tk(JA=)zLD{PnOsF|(A6$6+r7;m&ii#%< zdt8_xm^A5ZWk1A_jGBe}Zpcg-0YI~xZTHZu+c8Y>gVX)buyNy|d!N{L35Fq7aToWL zJ#9o-H&wb>s1oiY(~kA_V5dPkyaZHOZA=%$xxbd?<0mL`NE4!qh*%5Zlsh~B(Zdf4 zIbf8fj)+tMeuR(fjmd1=4_J#DgPIy$Z@fjTqU~pt-U3%s_3ll;V9xhgXFSPGF)${; z9Zt^L9rs|ApRJx)=LN3dp8y!vXeVo`izuB7j0wndfIt6*81Wh%F>32%>^q7KdD1f} zH0jYY?!8oDQ}01OG@Yz<6)X=*jb{DNjO!q9^ZX9Zhog(50 z{I6y2HsWrsmM-n10R$sQqiA%^=X)gM<{ghF?+IGDiwb~b-A*C>-wM{ZC6@pyCv?UL zYr3Ev^QBC;|EF3_`h{R!J>Gm}2aF$2_StE^*z2$pl=!D|J>(c1Owx=G-nfTrIXyh; zz6UL5xy@GB2$aLm0%YCAo9yb)k~a8;@FmnRFVnw!3efqx(#WZHrv=o za{_j)x)H;Ci?@AkLbFf~qxh*P3<2BV`1z1}nd(xT5 zFHN5LMb2RFw)WrAX_oW{U6_~7uCoicw#ugw@q^@mn);gdVSJ$iGwH9p`PVmT#*&DB zV?j&)Q4c~bwKt}!pjR74U7L5=En%uarVaS~Z(V&B59LikH@g93WAeLP%(3VZ={wWe z&v6qL-6K*GRk`Tf`q9M;@L3BXY8+zx-hki+F)|S3WJOOl#^ zQz4%_wO4({0(VJQcaGWArdwXDcMB^yc^A2nW{%y8?5eOHGG9zv_8e4_a(OxZa83UL z;xAHI{3g-v3_AD1{VZTsvKcLw?|d83?M|KUhuZ3+d%q*(&PyU$NcF-!$RLRjHZ}=F z|2=i6X7^4e_bN;7c|s2@D6hqhp=C|+1ED$>-}fJj>oUINBW@ty7e!Pl28!>C8f_H8 zOr*aOG(0m@p%}msVYtwWanGFT%ZEWQqvR0%`d6R25YDl1Xy+;e(*mn17Y%0?A(}iO z>Log5uZq)~^W5ez2JK(FvpE|}*l99|RP4&>2tA_)13J@(kWcZS~91_ID9aY=-efFpCylem>rUS)OxAmg9f1t8G~zbw=V}ku{%N*BSIu5P z-gPPG^ID5}{4C;=41otzxk6_wUC{)Yc2U$@R=`uf`^ z$yW2fcvpah?TOMaav%O==$lwabQ*c7({Tl^;MK5+`%To`pii6I9k+eOs+l{0h_<;5 z@#^~Jwe8DXeS1>@yItMzd$7Dw3Fw@%I?=LPai{aC^V->}HHwOT>%aW+EM1>>?U%wb zb;KDOMxp_kt>y`zw!!`XHDKuAqqYh8ke7Jqp*V6+bjRU+zDe`&;3fN=&ZTcv_RO$$ z!1uk8FyfW$0ZVxGRx2Plk`Zip|tC+q#R%4CZ9GPKYJ`O~iFT$;5SUmleZ z-Q`yTJ?f3zXp{MS=^6BY5V`%imq=4keRYRUV0TU%!EUh%c7~la}(F{-@E-JM277 zW56Mz%qZrXU|-n1Vis0g2?W-KXh2|{%6kSxlJ6|;+UJ}r=8J!$%d3mlbktnM2>l9k zQx`q;huc&5#13JfcnT!c(&PjO2J(wMO|z8A^C%)AwEE-M(7EsZTDoaV;0v$iOmC$! zly97ZSUv7Q$;aCzMdWk=h_Rl>PoG`aSZk~3#Sb<%o&Auy)V0C0&BB4=#?I9ZfmopF z&laHvFem>qaZ)Zl_Rb*fiHDCQTgQbiYKCZxjhM0Ug-VMta3NIye=DJ5N!?Qg9K&3O zm$(T4S@IA&gFx$zcDql55)QOaAJC=Bp8R8P*f(^O$9;*uRRO95X51AjusnPU%%`nB zyj^WA*Z*s}ya>)$9Xo8-CBrM~op)A@32zP%9?3t-db^C{jU@uK<_$J4kPV&Of=~4g zhVGG$+M-Kob-E!psT8uJfL9ha>jh@z{9WsV*sxLJg617V2>YWC8lwrc$Jz_+$xVwI zU0QqypkptxDVS%-Ed=Zm$W+x5@j_+dRC(%a^EfOJJ`m_u{y3-e|O_BTUM>azaM5p_N{phGCMqBM;mx1GNAss!- z>j6!J*!*AePZT@b#9Ll9Y{}O!%rq^b4u(K++agwz~^% z%=>8bC+FN_x8VWX`)u}cS=c2wWZ3zl^t7Y%bioR+og+m5q9bg+AzG?=>vI0S*Q!Nd z>YfUFqtmRHuJ~zOD0|H4*T*(tFMGG|#RV#~_2N$-x5%R8po-bMfQO@lwIREQy;?F*5(d8cNUYUKO#6M<4QDGpl`GJ9_E1IvIRAiW;Y^gD43X8VkA~Sd+yJ?1{ zpm}0*BvG9}o&XHS*_5Pi;%PsEtxE9}^*p~fds&CEPv$fammS3zbg8}Ii4<`{Ly-Zw zkg>}{ag_yt#b_K0YA{N{tpmu>W;?Djm(XA>CgsSc*;{?x({xuF|Jm= zwiWJk%~RDP+(}c0)Q)+}xb~4Y#vDid5GfuRuJSf!X0@kP!#2#52es4zKn_{z@JBvw zzi;3&SJ&J`BEZJe%y@YbnAxThhY+fdW>Td4{M6blVnxPtcW*7i?2Qp~U=?%M)7N_5 z$n<*jth{0W2?`f`_y?z!5oIj&LXQopA?nuoeZ5gJ@y_2DZQqru34H!HU$v2y?eo zx@gL^{urJ|Ni6o<94Prt#%7=ni?Ln16Cfcupdc_x;(tk3{SsFad%)K>GbYkAc9Dg#Xi|t87y~;@1Y#_L{D!M#! zg=|DaJyFk+;8)2T+m3U}U-@LnSWg-Rq?8fgtqO(|8h{9&V6|K5B{RrXTc(FiAc}=q zF6iz%#77+*859B>pi$=9WkT)IwyeDc7fu8R>)Bx(>+?JaZ7vV@KxZ!4vgcK2>UR^H zZX*puzff0*0W!53&Iah%J%IU}y4}_}Ab`++rR*YeyUNy=mCMd4vf^~#3+S8j zmE#`EnS02F(c_GHhyb)EfA+tv$9(NyT$n#Xyulj%AMuG~;OSIO_pduZ;KW}1mB^&P z%_qXcJv;*;02r*qMRxQ1-#3OJQ`ABI`{uX1aR8Ut>U;v+FM#LwIT=;l{T(P`%>jVI z%`gLE$@AHlyw@&*_+eJ&rH4wXg&ywwK!F+1y9Fe`{xu<}Er$b@R_paib}YmVG|ir> zAdBi0m7Y~%t}!ANxfGZ}th@w^!xtz$CbWkg;ti~0%FAT}KbQTQUa%g?QvN9=-r+Y( z+FRNlcH?Z@xrdLcy?B+{1qv)oyg|U3W04-q$XHOkk0uJ`l(el&b-|G0kP{vHnxv(Cufx%^w)+tRE50uwaj51qhYgJh>IB zXmWc+#jOD%!03(QE`)O`eI?U~Xf3N^HNpLp!Yyeskc*7B>h`71OWsRJ?F!44w!jP4 zwxiy;rAHNhy945P@B5O!5VsKAISjP@;loWE#mqK^^z?Mn_0x<;fIcnFgqbs&>!utu zl7+l%9tFrfR8gv+#QsxaqHv|>AF6_s&&2iBnT>VMlV}_fF0fEi@Ax%P?ZlDwm}3?v zl7u{;58Lx#PCB;tCN zc*tQmti;qNx&&ww^U`^%BPFd!QbJJOCX}SNX;opxT? za-8Nq-765hoFAzxem(xk(wvS!f*=6%G-;j#A(GvWlgEKAx`S&u!!`&^?_(c>j^{t#`nelLACE`D&yl(s!aO(|4?Ln*7W9k6Z@O6 z-gyu{pMoCO+!l54>+bR^JqWdX1(pNikd^$7u<%0h@nz~T!3E)+HGf-{5b~U$5mo_x z*ZV*lAZx8go<8innPWtnqiWI0to9fRD=nW{cXPgI^LXL1cX2Y~VT`!&_g}pbIMP47 zI8L%?=7gyDO?=^9$$KvVV0`Aep^UtlL95XXnLz%f!|6$9rOy98hGN(hpt`O0P&R+g zTQ#_RfuLb-GPkUw#ch|LO_8rZ5w;mZa$n{y5Rlc+30bA&sT$fGr z2sTeB@w+1yvTyXY)z2;$*H+D#Su$wbVnIU8wf5yt+quUb3CwP$e9ZHs?AjY934^?G z$8%2=&L`V>tr3T~pFJ+2h~bg7Ht6cb@Y-DnWBOkr9HYR8KQ^1#8DG6J6Gzh?QT>G< zwB1S4w+WFB3+-x_X7$@4lwtqu-0JWCx1C!MDy!IDvu_uer(r#fVw~sv^xD4Yp`tuY$HAbn0Iz3IQs6ygvX47)SBGNB5L^AzG z0-6|`ucwO@_ghmJ-$w{nFZ=6Jb#NSmnIP@GQ$qX3&9+hmRBZL$u3Eo%n?BD5FUa5o z_p>*Z2E&v5Ht??5Wkuf7TLS|MJTA95-?S2u{S6`|cVgY7uUa!pa9z;YMc}vlx3MegE3&Z*6j+1`TdB&%KO!5unwW7y zuLH(_#y*+Nudo8d34dWXFjG9$6{R5m1EA8w7(SwjeJI~J46F) z^1+BmyAlw1v|~t!N@pO2)EqPe8q^WeZ;&!-Z4By`R|;AZuUZ}i$ekc*=0FvXFQ=u! zB8>UnjP$>e*0{;j6v>Vj|LP;|$IzO*w_dz4H-n7IXUA1%f)D7JsR|^A3h-57o;BYPxn9_omL-Coimo<2=MIsv+rh& z7{p&u%9xHsHuKgUzvwTsdW1Xt8iLfiW6JPyvaz<47@J+Y?Hhc=XtUpai5@k<9pgO@h|lBckUI~ZO}Az6yn+IC)0Q`TfyEWziF}8 zj#%_I5RviJ8!hXgdBom|ZynwQ*y3R?y(SVgX3*IM@W{i4 z^4F%Ul-zktEUyTH<(0g9$W(ip$Mq`J>PgjrA^I-#M;f!a1qElOE<<-$tf6OlEPaI2 z&I=a<-Zi<#1xslQU0-qZY_J)i)kO5urMl>aHd~5@@$w=Gzy&o~YP) znYU1k)yrv82a=%uHbixu$s_=_sbf5rCnu9`V?$l&ifzV6QopwUlvAT5)LvnoDNZ(w zr9DKY<{_+{r8?LYnqMM2Kg;l{=c8sV)B=VSnXDewa9r=4b-d7Zio=;6LkOhoz{cz0 z=$E5t2kiGCU-fd>-)oq=YM4T2Hv!pOs^7+~GcoWTrJpGk3qCM$m($D)+rYj@SNQ^b zSIhhgFP+T4!u4!7k&*hcNdiYLrjkV$AT;bG7ZV>4|Z9JTq=xE^888fjc_Y)T0 zD(W9*T5(!&veJq&|Kb||$*c=wJ8g0JtBD$5f`b`P{EjTs$uv}s))aY|%31uMu4Ba< zxMpr&8XCUiX^V7aC|&QcbN7(l{;)1fC`B>Sf?KoK!`qi62w$k*Jr1$%AiBDF1N-*| z4sNLi4q4IakQBQJ%Tz(rEqyvCW^t?V>)Yg`G*?L6pg7*rw&>>t#@5PbM&m=`)}X%t z{S6+V99^<*x(|QuU!5>_g7JI`PQT3LDeaW}jm9s%^$VzrNE+AAlohL<%7V!W)3w-C zT4;3@H$NKF)=h=Kzjbzx>2LQQTG`L&I`v;(QzwR_Y_^(G$hmBIQ`JAhz-WX_fXidn zzR*7D>Y#1|-iA|G;@-{nd4i%E@UsYc`TOb7R7*07wjvKymAb?^-5mHn7pu;^LCaYIJycNj#c5!HqIZ2~` zK10cuy<;fubi`P4Cjm`1=C-z)#AhagH1JKBN*Ba=tsEq3RVg1=L-7pK^_3@fo=O3B zqg)7h5EiSCsU?D##g^GQN_nR{+f;tu#!1-kn<9Xsl${O7t&J2pGC^hR#%@bDHQzm! zgPJcJ6E}9%YqDuP{eO^`x)*QB}vJtKrFOGS_>3 z+ZYsJEaY9Y#aV%HCj8|b9-W^Ld zeqHrLBgCu}=t&*La&K~&ZYYQ0hLHs*hRKPDJQjDNX1o-w6hRFFaI~&5n}>kldalN6 zD=Dl5#=?|>=I~eMoj|lRh+(HAB0+sKOOJW-1@)Pv_vsI#q*gy7A`59zw%I8TBD=98 zA=U?kNnef_DvQ3D=P10a{xIq1Ziag;s)=+iXR0^>Ua-94WTNw;m2p01JIy@NBM{M9@(hu{z?Rf0RT@=7wrPZ#Y6nJu#IrjHYTAgAlsH za_-hgCJUcyiG%JD)}IEzBL!=m*lfJPEKLfJF+?>d3mh*M)u=NmeYFmxZD-=1ukW8F zxbKySA$G+Cw%~|UV~^dL8keK>p&aYWeL47oZq+yN)x{{dagKB>>YczZ<}$Zf5U^fY zgQ{*V@`VAq9(yyF^VV+2m$@I3)Sq65?qE#i=w5zII{V1tLFe}3sw=*tRWA>aR6U`4 zpL;3K+XVNf5Ok+!)^!KGen~(ruv$F?g^Nvd{6OqLPnB&2Ry5BhjiHCtWY!ssmn?Gi zuz?+pI@+4EI7FM^`6LS>*nFN%@UmycW7Qk#dK%MI@Dn_J4LxIioYH$)Jj0{d z2|e6Fu$*@Yo^3$SjIY}zT})EADKQ`DFw;7t_LUKW+JV=wq33Gj-_E<;PJ7{N#z{Jf zS%xWRyL0t$$wI#8W?R*WW5FbZ<>M4x`SiQ^B*03&hsWCDY{y^r)I_ zWuV})SND>;srKBjmprP|8j3%$h`94pQo@NZMVe$~gjuY*&YB6oQ?bDmzm zp?mpR_q-g6Q2fP4{nv1gL|z%+guEX)UDAawCBfR0CQAq5ipyF0%o4~udHXA`%B#M<97BBw~#M-gj!DR8^}ou;7UX3`d2CvCt`G7Mm*bVF z0`l_dH>9i{b^W*L$p5+$`8*#;Pa^OG{QUvWeA~skqjXGnLD>m;L&41 z^2J5^buj}Z=Lo=sw~B$D>4{vsZM-rLVL=ZRJ|K9B@Ly$d8*#k!opjxTx(~u*u@R$jK(JrpXj|K|ZO8e?%pJT21Vvlvw z<>WSEmu0iRc%Xn7F{}6sExEVe^?XdDjiK~|l(jp{fvc+K<1G`xWZkV~d9nw)s|e#A z4}DGbor?x&Qj2@&O|KyH(el^c*LZ^7G&2vrP$I~Ads6e8sYKjr)1dc`aIO)-)29(M zS-#seMPb8Ik`oWwn}ZBXF>@cuJl4{?EpVe52RHt->ZQ-UQOw!G_6I>)jqg8Vk-4(p zqP4-o7`-`9*y4QL^7TIA4no|ozmI$6!WrN8G;W5EtqQ$|rPn&qI3p*Av9pLEbnR}| z$OaSHXMb>QFArnqc*tXh+M3%tO88e$*b7d$?(ESpY)j_xW}Wi4ZQpi!+mv%8WM;MA zSF_5c#o3c+TjN`Aa^Kds0E(DF|9UyfxpgG8z+x1?=Wc%XNtgBU8gV{s_;g49{VmAo zpQE2MCEYdQlYDNx>+GeTVy}h4btqb8zh<&YzZ5+`LKwNO#F5)ks^HrC=1tCBxd8S^ z1&>=ls-+AmAYBWmNxLJFPLer#(%SD-ZbKD3A|EgH6@79 zSzxaxBF|R@Vzl_tUs=KqZz@XiL$?Y)Jb4VRs^V&xSAh7NZNv35wshF&bU_iT?mhRneF}RCwuk+#EQbfj4 zI;d7Jt_Zm9cUKF1=5_rY)8KbF3x;igYn9z5`qR+ItQre6-&{N@?3~y{qSK5GlJ=~? zTB9|Uk?pO6=Z08&#{!kw)=|R{lVF%!ldtIX!vGGCJ@c4##H@;U{g-+~bp2t#GOunq zmPEi3?6)Af8T4}5{>CiTMmmdi;7#0!YDxNZc((gtFuIALN_LW!yR8%EG8*JN!4p}} zryYa$n0PL`paUzzDI(EMC-VYH1b4yJXTFVtol{Gr%>Jj6r@t_Em^pBeW7r3qPjVTS zlb64Qf%UPOzM1#TKNL~k-LLT^A(^PfUZYma(*Mt1en&v%xCes;CGXxR|rv>lV}<=9echDuw)U?;k; zkM&1v(2JA$iK7d_nxk@$*(AgT#yXEX9CmiFBEJPa`v`qcJOytS;UhtAbqZ%68L^5t zq$~#g6OXE2dcxH84{=Hhjt3kTY*R@bOpYsbn%Jke*j!l}^RmTOcVJbH@lvLL;2TNj z{UaWi329>Qjb|>Ft*v7O8EJ=tAcavo;pV*LfpAYf%I2Nbi_g5<jPO%4+F~$y8xE3oxQgV>0uMRh(k?i zpq#s8+~~#G%8?@!MpAzkv@>eG&|8~Ve}D_`=A15`Zne7!>!3gcS-Y+9S}%A*_x6%b z)5oDFiI<-jXIHRvFTKvk9qSIrJy+zJ5q3~eby=yUxCtM0J@Kp8x=5NSW~<)?`pP!w z%B7t9f@5w7Ka`wW$L5y|^ClYdnBT2^yIu5)q4qeVXL8C7mf93@LgxNG;=EYU36Yb8 z5F2ibR9Iga7CeWk&LF%66(wJxB_TEkLO_YaY>l>>D9k(A6S_ZhviwZY>ar&Z@he+F zGE4CCSQX=GP?iMOhB$I{O4Q1e?9(_Kn$^WDO>gc%FXR#hM<9<50^|<^AOfe+4dod>eg9G6Mf^G11wqvEOFm-T(6(2t9H zcUl=Bx))HmFnzkf4=X74Ymu!(#-g{8$d5JoNYlfkqou{S$VF2*^z2~`9X^Q)+N>Dd z=AjY7@Eku0++#4ho*XVm(FT@h?`P6^UI8rgaOR?!12P+3e-D1xmM93#M~{7z#5iQm zA#(?Qtk<-zi&#sVn1Zj#LFbdM*TbrH&z4x=hvkUHvxEBJkjwHNShI0CVtU8@f^iN3 zx@B5!ah85>yE53;yPpw>brltA+S+7-iK}}zdWoDdLKe|?=$$5!7|>%~F{$ccEo4Bo z{(LA2ah3!fOMQe~cNgkCZ86DKZXV=+9yx*>gxBop6 z!tP9D^o{0~E~>e&PK_GKiwKqQdDk`Omw&{QSCq+`)W4PeQOu*GWrDYm&jK=Qrn#BsSE?@OxOO zQKP=-fB$IHqNs?su=ePoAC7}80e1}%hC#b6r5!?;7t7l^T2JgQjqkO`Pq}_NNMiR> zQNNF&zrw?xJuc*LYu;lXxZ5e@A0NLX5fnlzzzUn`o)t$&;H6$I4hw~pSnO|Hy$m!+ z9z?EW`YlyKEVWvs`GXB{_vygAeeI1X-T(s|v(jSVu+5WI2jd5f)|QJ`($E@MWD7RX zdt;k4bhd~fE}UEoho|e~86$k(5X4l0HG&98TU#o3WdkR}0xWDFxB5+7z-^0dYi^f4<&G@$Om!%4{qLc8#oV!A%ER{#GarX?Nr%aIA zo(Ws3x#{rjbjQzog>bT#PzUx^o$n8IdsqShmauSJQ#U?3q|iROJ2v#gbvGI=r->^CgP9~@#J>py7?6P3l3kJ`g0uW|Y1RMm!&xVTI4 zxX!9CB-^1QYa~HwvWDj{dh60p{%PI(Wk2%t6yQV!M=)U}x3LAi^*nT1Qge*%Se!9&%-N-@_VXQ_PaRsY7)`V`*?GUqSMW$WiQZ9iDW$l1I0o4XaH0p#9BW zV)^mCe8yqX^wj;KpYj>Y)BO04(IL(zQ?!4>QZxI(;!-`?vhm6bf6 z$%2xApvW0%j!YwvqUdS zAG}|yGaI86kGGl~Gc8@nOx250kMH={USwZzwsU3KGFtgF>uG0g*P;&D<-0ZU&ncpw z+wlv|o<3GXr`m0@hBqO29DJ*Lj3KSn56tWD%H4>BsUvdbHiixHS|}ipM&;V=G_cy4 z!Kbtlv96q^Dc^%GEy`g(Ztf{*oATp4+h?#Y>3G*p#*87>gGEAa!G_&1O6)vh(_fp5 zJ0tn9ME*AxrEr<23*Ik5#^&ea%iGP-*R1VT#!IS2qA9dAeAy^=z4bcc(qH46z56pu zVQ9hUeqJ<$lXBXmhNCZ4hMRL2@8%y@M3Se@^RvNh-J9X91MVX~5h)81g3X?*lTAg_ z)gNGVkccjI<^tqlDWL;COk&h?TH7}rLH~JqlRK3y%#^=4xwqO@c6?0RMp4M0x>sO5 zqe@+3=>rotq#-Opr&-*TxIG@vIHM{*P?a*gH!e7&vYiWxZ5L4d;()&90F!xD ztgLiq*SgA|(D;{AN%xerR+C_H2&WiJHviAP+`H7>;^r=`8i_|INed!v63ZQO$ma=Y z?GL>tyz*xOI_9Uy*j86!dNOfeGw4`R@%8<#VgZ>P((=q~9p3ahiwD8;stb?RBjg_j zGDW1U#Z=kd>*Wc0s!7bjr^F+{W}RxBek9$^Z;Iwm-22Ix4u6|^FY1D*%6fY$ux996 z1?eB=-u^@BOZC^+M8``w$AeDp8GA0cQn$NapPIoj-}At49Op#+I&gM<_qsy+7PZ4&fJ)zg~q8{XzjoBysPM5*~A?l%EsQ~Y3XkP}P2FJH@*tyjT2eJ55u+t8crW`o<8!N`1w zk1el9+6F^C!OZNE;Kb>#Q}1=^{)HS0DA9yIG$M5GaIN$A(l6UuDx z1ELM;8|Ql1D_fZ5=u!?frQ*wzb9-)ZK`Xbpq#@7PRn^~7n~TXyVsO09vXOdTenn1& z`RuIYc>T5XC?68US$}Ak;cbYmi}A0)7sy!09Q8Lk0z{C&)?(?sLN*>Z2TweJHL@s5 zb(9R`w7IDdCH$NgccF)64|2LHkNru?qCIqfD7s^;l6D;wPg$*<`iZ3fzF>jk2UeF;!%*JQz9G0=T=|V~5p%yN<#*zE)g8gC9sUfBC z)9GI}9-&>gD4S<-e7xJX*7?q0MGv3X2rlvonFgGGp=gaJuZs?q8f6t$D)kEvnsjBE zJJp_yS37jAW@$a!a>{Ai)4XuAIn~&!Ebbw8&1O4o;bKNER4rVMr{8@qU|{1p6;%7D zRf4jeEN>+C{b14cd{Iaz@#J&c^K|9n8#(u^oePx2)(>>*H7n5|91LXXHQn_a@eJ%P zEU%+i#Ve$i^aeJ>Z)3=$M<{eFbjuuVxPLa^DY_8+YP#;mS?|- zuiviYgavB8a=lKid$~S6TMFLPPil;v@5`pBR9ZaSA&OiSYpkf?4|1?p_~v(B4f@Dg zcJz#%6uVWqokY2b56euRu;O2v-+f@!)c1*B+qtwxmAoeshTyfQdQpeClF*v<$hV9O zMo{8)SYkwOOnZDbcedBAq_@MBOuuZ^d$5X8sCv&&Z}EbCnz<*IrXo9fSe+BO1-0~Q zJPJh~;RcRjc{8#x;qp5pw&p+3`dP3jJ3MlPB=g<4jny)GH}jalCN50`_BV-x zOb_Q($}R!(l5&TdT7o~(c@FP89l*?S0A&pU!D>M)fL~~9nH!T}Q z5#5QySPZ&{*os-fK91yQ9@k~kyFSUUvWp^Ps#OPh{p41=Mbtb!JB0lNuT7GD!MX5< z%V-oCh@0R4n3(pM7W-Tqj#N17TgziVFj~)ju)1^XA;&vy-!lHgA`k7EG*nW|a7z20 zP}*eY>Bgq(i(?RQsdc+Y2qx-%#JP{cW?b)Is8b40Ch|BYKCK^R%yq^8HjYoyOKgKi zn9xKj@laRFKrBPjIR2(dTq5!FvbXy0m3vnwo00H%Ri+W?@?TY#ESNxs_tyveetp8= zPyEI@ou^hCo41Io@}bHj=0T;Nl#G+%(>voB)w0B%p*ztLB#{drt3)OaYh->;b{ieO zY?t;x>s^&gI71U%UUE>K3@3n4eCwAds`nqwYOam7n_94(s*!a*kj9CXWS9DVM@C3i z{mveoo%OfHUNa_e-fLM8Qba1ifiG!^aHQhZ>4 zu%otV5_PT4HxLkw4hmN}D`$&s;OI^EK}`jukGB8s}{r zo8f&K1K0HE<;{ro9*3RUKjPaJM^2wOwl>MX>jhe8KIhx-e0?E36RSac(K7esAc6hn zj6IUcb4jf^D#Caf(yviRwH1K}VH(@}PV^t6ob@_mV;P=3$P#e<4}*kMN*G5j75@Cz zRo$ef#G~EoOo2R77GCOShWuWG!Uh>0rd+(6hRZn!9F^)rxNGg$0EX4-$U}=>jpqRX ztR|CW^{L+VLCfX~JCkKa=|tZE)jhN*i^)53c1AO);~HgHpG-P~PYllSr7M8%RFozA zar?zk;@eGj5+Tgy>UWDnr6timx0`#lj@I9Ta}c$qw3F#%aS6K#UNyIAP}4AYj%%z7 zgJKL59&fzV2HXfIL&9NT@%y+-Zm%2{qp3K~SX7X(5u#`JIGmqx8IyTtbARiHV)(x zzw$lk?1Y#qkH%6DBvigho^NbdRQjaB8Mq3w3}YC zdv8hr`5J!{R*u{{gTJX@N?Qna4${{O3%|H7C5FllAIE-7r;@>w)WoFB*rwy6q{$wC zOuI6+&ywA-&A`Y!CU;NS5+LQ2NiW^|m%T4{%=^T0_)1<%@r5N*Yr=}~>~OBoTT0cqd2VdAeY0rQW*^~IO6!xbK&$R78YN|JxNj24pM8Z7L6?DYa1j{N- z9Va?OC{(LX3@{iYzd{NpI-pbfd2Cc25?(->v~1)w%D>zytSLRZKYf-bws$VR?-(2KWnAAasOyVvJzPK6fT5j-&$+zsX4>MM zPqCy^2EVqf;AgjbT51Ssz8K^^stRA{EI_xpFo&^693K*BLEiqE{!`5vHJsP!DRZ?e zAQ2_E=0zsDxtST27V1R{ma_3@8)bn_`nuPb(9b40!Hc-_PvJDFBl*uTHBy98~=fmH3ZEZy#4#+JE!uB^@pcqoz3o zwvQ7m$KTsqA!R7YjSeU3D%A^1TY=>jRT)n3^>3EzhcVh{|LqHOScK+IyatDdI4UT%O#ygk+!rFT;i3swW__ z9y9%!(1!nq@z0!jT_F(UKe{+;FNH8nD@bzDkwp|HF&E_2BxaG&+Z<`8UtaT6x*I7~ z4tbR67+hGpI5yH$!279R$9Cf-zh6&QD%$w+&jhJk7*U%}ZqHGH1`|&*YcG|M5d6im z9J`DP%iWsSu56Ptc8P+gA4}4td9_kLAuWAXx%rc_3tnCk*T5CDxm%k8xH|#Z8lc$soVJdX~?f}MH;o-ArvGksM^%OvQ5A@#6F5B!xI{{{nZ+%|2(wo^WCN4 zzyM+8wYDUUB53jtGsj+*^Vhm|ti*DC3~g`qVf$%{877f_vTM`d3vl^%{49rOm#x{S`Fb1G2&R>-(bxNU zXYkmU?2($EZF7XB2Ey!Hh$-xBY@ZWJPSD-CS?{Nhq!)T5-Z`Tn_2DN#sYz(iZg=E| zqw9TR+?IHE#>q&N54kO-C|R1^@jRJHtD|O2G?v zhbLc?G7bS>19MUULs;aUuZ!wr!N$8t{0(H&NI6|69BELys>1?&ifpXico$ceC26lF z=QT|4$s71SnfaIq$=A&0VxbJ@+eH%a!$-4NF0en}%QL zJy}G5^+$QgSB4bZH3G=uVNVti&nP}NVJN07+V#_Q3>@i0Tj%~DOO|)?fR0@`v2zhc zKE_GYrMHFvf;~4szb9tCtnmP6tNT5)*BL&G(I3XT#dX5z$QdyctvuzNTuzGG(^Yg5 zm92j4bBis=0jPdH0eqPiL zzL;uuBpWL{e|8#AXvAEg1UJO>LOG8k;5?A06#UgWfCo^%;%+xGjAN!V`{0$`F}@+H zJdrPYS>Pdm$3tc~thS`C=GJlNJK-^mKMVmWf|Pynk7G6rUFG*4xrU@)MELw}S3kMh z1S~+b|BDxJkzY*WI*J>}c5r|?I0$+Y6_oLm+|LeG?Y;*J)4(s_K+Mm^%%@8_mWILT z#FY~DSpIxlJT#Z$Bzz~I%0OQc6#cp^8V1B!DjIPCM z|2lMtqqe7EM1uC_vr>3wJFkODTu*#w+PSgs*ee!>FkY)x)#+}BQ26Hsqm^&pP*l0g z5Xh=-$p0yeIyx<`vnch(HO!xgq7M-A*O4-#12T2ziA~DGZE5`*Q}NH#3ENWw|7>6~ zpHIp5X;sGKKc-E6GcGi$tV^(@dc_!z(`^tg==6W=&sQclc^Xv3No_&&7)7T4Wb&aa zpCxQDiMH5i7P%$nU0z^KpAHMGZeB;#2z2cJ4Hhta`DDhs(qTgjfPh5PJJ-|N7~86< zpZJJZMngPH>?RXhUjQm6XLvXyg2(y z_B$~~(w$(q;T|s(nJitfJPc*o7tO?-Layx{?EJo#PJ@ z{+gnBASmvrPh@M6ci>?z%^4b+V|(gOH|}Mv0A@RD^R=Dq#m4tzO*P|yp5!+E{JFsn z8>7T*ZL7vn#qI|Mp-PX}J5}a>oXG0Si@yCsPWBmN3^WV9&*A?$7k>(wfRMv4^3U=^$_NobM2oJWE z(E>{AQzRE5P_iyfSDwIgT)Y&e3s3_)=a6=7YikUikipN5v}6`ds?J4|I2}Oh-=Y*| zP~#-wwIcJiknMQakvBAzZu{MMT{c_L{QN8>VM?rVRfp$oOniwQ(}X&u57%J90eThV z1}0^h4IlK@Ra|Ab_aDKBhp)n?;HHi*SKy;nU93tDq%_+_Sd1w5!O!pyqAbL4_^R-w z<8Cc;{t*nf@a~O4zRRCn@ZS?*FJ94sq~qZzt^W46A7%mZfi&?iNa?G2^-2s_B#^1b z^RI7VzqRJO+0e_-onO_yy℞9m-8fv#DxWbiNUN1>5} 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 V=l%V&;(J^#ke61GDt>7i_#ZiQOU?iQ literal 0 HcmV?d00001 diff --git a/lib/esp8266-oled-ssd1306-master/resources/xbmPreview.png b/lib/esp8266-oled-ssd1306-master/resources/xbmPreview.png new file mode 100644 index 0000000000000000000000000000000000000000..70ea3a53a843f220f18d2ef7cc0698516a990ba6 GIT binary patch literal 41692 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 literal 0 HcmV?d00001 diff --git a/lib/esp8266-oled-ssd1306-master/src/OLEDDisplay.cpp b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplay.cpp new file mode 100644 index 00000000..bf4a5ee0 --- /dev/null +++ b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplay.cpp @@ -0,0 +1,1043 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn + * Copyright (c) 2018 by Fabrice Weinberg + * Copyright (c) 2019 by Helmut Tschemernjak - www.radioshuttle.de + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * ThingPulse invests considerable time and money to develop these open source libraries. + * Please support us by buying our products (and not the clones) from + * https://thingpulse.com + * + */ + + /* + * TODO Helmut + * - test/finish dislplay.printf() on mbed-os + * - Finish _putc with drawLogBuffer when running display + */ + +#include "OLEDDisplay.h" + +OLEDDisplay::OLEDDisplay() { + + displayWidth = 128; + displayHeight = 64; + displayBufferSize = displayWidth * displayHeight / 8; + color = WHITE; + geometry = GEOMETRY_128_64; + textAlignment = TEXT_ALIGN_LEFT; + fontData = ArialMT_Plain_10; + fontTableLookupFunction = DefaultFontTableLookup; + buffer = NULL; +#ifdef OLEDDISPLAY_DOUBLE_BUFFER + buffer_back = NULL; +#endif +} + +OLEDDisplay::~OLEDDisplay() { + end(); +} + +bool OLEDDisplay::allocateBuffer() { + + logBufferSize = 0; + logBufferFilled = 0; + logBufferLine = 0; + logBufferMaxLines = 0; + logBuffer = NULL; + + if (!this->connect()) { + DEBUG_OLEDDISPLAY("[OLEDDISPLAY][init] Can't establish connection to display\n"); + return false; + } + + if(this->buffer==NULL) { + this->buffer = (uint8_t*) malloc((sizeof(uint8_t) * displayBufferSize) + getBufferOffset()); + this->buffer += getBufferOffset(); + + if(!this->buffer) { + DEBUG_OLEDDISPLAY("[OLEDDISPLAY][init] Not enough memory to create display\n"); + return false; + } + } + + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + if(this->buffer_back==NULL) { + this->buffer_back = (uint8_t*) malloc((sizeof(uint8_t) * displayBufferSize) + getBufferOffset()); + this->buffer_back += getBufferOffset(); + + if(!this->buffer_back) { + DEBUG_OLEDDISPLAY("[OLEDDISPLAY][init] Not enough memory to create back buffer\n"); + free(this->buffer - getBufferOffset()); + return false; + } + } + #endif + + return true; +} + +bool OLEDDisplay::init() { + + if(!allocateBuffer()) { + return false; + } + + sendInitCommands(); + resetDisplay(); + + return true; +} + +void OLEDDisplay::end() { + if (this->buffer) { free(this->buffer - getBufferOffset()); this->buffer = NULL; } + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + if (this->buffer_back) { free(this->buffer_back - getBufferOffset()); this->buffer_back = NULL; } + #endif + if (this->logBuffer != NULL) { free(this->logBuffer); this->logBuffer = NULL; } +} + +void OLEDDisplay::resetDisplay(void) { + clear(); + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + memset(buffer_back, 1, displayBufferSize); + #endif + display(); +} + +void OLEDDisplay::setColor(OLEDDISPLAY_COLOR color) { + this->color = color; +} + +OLEDDISPLAY_COLOR OLEDDisplay::getColor() { + return this->color; +} + +void OLEDDisplay::setPixel(int16_t x, int16_t y) { + if (x >= 0 && x < this->width() && y >= 0 && y < this->height()) { + switch (color) { + case WHITE: buffer[x + (y / 8) * this->width()] |= (1 << (y & 7)); break; + case BLACK: buffer[x + (y / 8) * this->width()] &= ~(1 << (y & 7)); break; + case INVERSE: buffer[x + (y / 8) * this->width()] ^= (1 << (y & 7)); break; + } + } +} + +void OLEDDisplay::setPixelColor(int16_t x, int16_t y, OLEDDISPLAY_COLOR color) { + if (x >= 0 && x < this->width() && y >= 0 && y < this->height()) { + switch (color) { + case WHITE: buffer[x + (y / 8) * this->width()] |= (1 << (y & 7)); break; + case BLACK: buffer[x + (y / 8) * this->width()] &= ~(1 << (y & 7)); break; + case INVERSE: buffer[x + (y / 8) * this->width()] ^= (1 << (y & 7)); break; + } + } +} + +void OLEDDisplay::clearPixel(int16_t x, int16_t y) { + if (x >= 0 && x < this->width() && y >= 0 && y < this->height()) { + switch (color) { + case BLACK: buffer[x + (y >> 3) * this->width()] |= (1 << (y & 7)); break; + case WHITE: buffer[x + (y >> 3) * this->width()] &= ~(1 << (y & 7)); break; + case INVERSE: buffer[x + (y >> 3) * this->width()] ^= (1 << (y & 7)); break; + } + } +} + + +// Bresenham's algorithm - thx wikipedia and Adafruit_GFX +void OLEDDisplay::drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1) { + int16_t steep = abs(y1 - y0) > abs(x1 - x0); + if (steep) { + _swap_int16_t(x0, y0); + _swap_int16_t(x1, y1); + } + + if (x0 > x1) { + _swap_int16_t(x0, x1); + _swap_int16_t(y0, y1); + } + + int16_t dx, dy; + dx = x1 - x0; + dy = abs(y1 - y0); + + int16_t err = dx / 2; + int16_t ystep; + + if (y0 < y1) { + ystep = 1; + } else { + ystep = -1; + } + + for (; x0<=x1; x0++) { + if (steep) { + setPixel(y0, x0); + } else { + setPixel(x0, y0); + } + err -= dy; + if (err < 0) { + y0 += ystep; + err += dx; + } + } +} + +void OLEDDisplay::drawRect(int16_t x, int16_t y, int16_t width, int16_t height) { + drawHorizontalLine(x, y, width); + drawVerticalLine(x, y, height); + drawVerticalLine(x + width - 1, y, height); + drawHorizontalLine(x, y + height - 1, width); +} + +void OLEDDisplay::fillRect(int16_t xMove, int16_t yMove, int16_t width, int16_t height) { + for (int16_t x = xMove; x < xMove + width; x++) { + drawVerticalLine(x, yMove, height); + } +} + +void OLEDDisplay::drawCircle(int16_t x0, int16_t y0, int16_t radius) { + int16_t x = 0, y = radius; + int16_t dp = 1 - radius; + do { + if (dp < 0) + dp = dp + (x++) * 2 + 3; + else + dp = dp + (x++) * 2 - (y--) * 2 + 5; + + setPixel(x0 + x, y0 + y); //For the 8 octants + setPixel(x0 - x, y0 + y); + setPixel(x0 + x, y0 - y); + setPixel(x0 - x, y0 - y); + setPixel(x0 + y, y0 + x); + setPixel(x0 - y, y0 + x); + setPixel(x0 + y, y0 - x); + setPixel(x0 - y, y0 - x); + + } while (x < y); + + setPixel(x0 + radius, y0); + setPixel(x0, y0 + radius); + setPixel(x0 - radius, y0); + setPixel(x0, y0 - radius); +} + +void OLEDDisplay::drawCircleQuads(int16_t x0, int16_t y0, int16_t radius, uint8_t quads) { + int16_t x = 0, y = radius; + int16_t dp = 1 - radius; + while (x < y) { + if (dp < 0) + dp = dp + (x++) * 2 + 3; + else + dp = dp + (x++) * 2 - (y--) * 2 + 5; + if (quads & 0x1) { + setPixel(x0 + x, y0 - y); + setPixel(x0 + y, y0 - x); + } + if (quads & 0x2) { + setPixel(x0 - y, y0 - x); + setPixel(x0 - x, y0 - y); + } + if (quads & 0x4) { + setPixel(x0 - y, y0 + x); + setPixel(x0 - x, y0 + y); + } + if (quads & 0x8) { + setPixel(x0 + x, y0 + y); + setPixel(x0 + y, y0 + x); + } + } + if (quads & 0x1 && quads & 0x8) { + setPixel(x0 + radius, y0); + } + if (quads & 0x4 && quads & 0x8) { + setPixel(x0, y0 + radius); + } + if (quads & 0x2 && quads & 0x4) { + setPixel(x0 - radius, y0); + } + if (quads & 0x1 && quads & 0x2) { + setPixel(x0, y0 - radius); + } +} + + +void OLEDDisplay::fillCircle(int16_t x0, int16_t y0, int16_t radius) { + int16_t x = 0, y = radius; + int16_t dp = 1 - radius; + do { + if (dp < 0) + dp = dp + (x++) * 2 + 3; + else + dp = dp + (x++) * 2 - (y--) * 2 + 5; + + drawHorizontalLine(x0 - x, y0 - y, 2*x); + drawHorizontalLine(x0 - x, y0 + y, 2*x); + drawHorizontalLine(x0 - y, y0 - x, 2*y); + drawHorizontalLine(x0 - y, y0 + x, 2*y); + + + } while (x < y); + drawHorizontalLine(x0 - radius, y0, 2 * radius); + +} + +void OLEDDisplay::drawHorizontalLine(int16_t x, int16_t y, int16_t length) { + if (y < 0 || y >= this->height()) { return; } + + if (x < 0) { + length += x; + x = 0; + } + + if ( (x + length) > this->width()) { + length = (this->width() - x); + } + + if (length <= 0) { return; } + + uint8_t * bufferPtr = buffer; + bufferPtr += (y >> 3) * this->width(); + bufferPtr += x; + + uint8_t drawBit = 1 << (y & 7); + + switch (color) { + case WHITE: while (length--) { + *bufferPtr++ |= drawBit; + }; break; + case BLACK: drawBit = ~drawBit; while (length--) { + *bufferPtr++ &= drawBit; + }; break; + case INVERSE: while (length--) { + *bufferPtr++ ^= drawBit; + }; break; + } +} + +void OLEDDisplay::drawVerticalLine(int16_t x, int16_t y, int16_t length) { + if (x < 0 || x >= this->width()) return; + + if (y < 0) { + length += y; + y = 0; + } + + if ( (y + length) > this->height()) { + length = (this->height() - y); + } + + if (length <= 0) return; + + + uint8_t yOffset = y & 7; + uint8_t drawBit; + uint8_t *bufferPtr = buffer; + + bufferPtr += (y >> 3) * this->width(); + bufferPtr += x; + + if (yOffset) { + yOffset = 8 - yOffset; + drawBit = ~(0xFF >> (yOffset)); + + if (length < yOffset) { + drawBit &= (0xFF >> (yOffset - length)); + } + + switch (color) { + case WHITE: *bufferPtr |= drawBit; break; + case BLACK: *bufferPtr &= ~drawBit; break; + case INVERSE: *bufferPtr ^= drawBit; break; + } + + if (length < yOffset) return; + + length -= yOffset; + bufferPtr += this->width(); + } + + if (length >= 8) { + switch (color) { + case WHITE: + case BLACK: + drawBit = (color == WHITE) ? 0xFF : 0x00; + do { + *bufferPtr = drawBit; + bufferPtr += this->width(); + length -= 8; + } while (length >= 8); + break; + case INVERSE: + do { + *bufferPtr = ~(*bufferPtr); + bufferPtr += this->width(); + length -= 8; + } while (length >= 8); + break; + } + } + + if (length > 0) { + drawBit = (1 << (length & 7)) - 1; + switch (color) { + case WHITE: *bufferPtr |= drawBit; break; + case BLACK: *bufferPtr &= ~drawBit; break; + case INVERSE: *bufferPtr ^= drawBit; break; + } + } +} + +void OLEDDisplay::drawProgressBar(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t progress) { + uint16_t radius = height / 2; + uint16_t xRadius = x + radius; + uint16_t yRadius = y + radius; + uint16_t doubleRadius = 2 * radius; + uint16_t innerRadius = radius - 2; + + setColor(WHITE); + drawCircleQuads(xRadius, yRadius, radius, 0b00000110); + drawHorizontalLine(xRadius, y, width - doubleRadius + 1); + drawHorizontalLine(xRadius, y + height, width - doubleRadius + 1); + drawCircleQuads(x + width - radius, yRadius, radius, 0b00001001); + + uint16_t maxProgressWidth = (width - doubleRadius + 1) * progress / 100; + + fillCircle(xRadius, yRadius, innerRadius); + fillRect(xRadius + 1, y + 2, maxProgressWidth, height - 3); + fillCircle(xRadius + maxProgressWidth, yRadius, innerRadius); +} + +void OLEDDisplay::drawFastImage(int16_t xMove, int16_t yMove, int16_t width, int16_t height, const uint8_t *image) { + drawInternal(xMove, yMove, width, height, image, 0, 0); +} + +void OLEDDisplay::drawXbm(int16_t xMove, int16_t yMove, int16_t width, int16_t height, const uint8_t *xbm) { + int16_t widthInXbm = (width + 7) / 8; + uint8_t data = 0; + + for(int16_t y = 0; y < height; y++) { + for(int16_t x = 0; x < width; x++ ) { + if (x & 7) { + data >>= 1; // Move a bit + } else { // Read new data every 8 bit + data = pgm_read_byte(xbm + (x / 8) + y * widthInXbm); + } + // if there is a bit draw it + if (data & 0x01) { + setPixel(xMove + x, yMove + y); + } + } + } +} + +void OLEDDisplay::drawIco16x16(int16_t xMove, int16_t yMove, const char *ico, bool inverse) { + uint16_t data; + + for(int16_t y = 0; y < 16; y++) { + data = pgm_read_byte(ico + (y << 1)) + (pgm_read_byte(ico + (y << 1) + 1) << 8); + for(int16_t x = 0; x < 16; x++ ) { + if ((data & 0x01) ^ inverse) { + setPixelColor(xMove + x, yMove + y, WHITE); + } else { + setPixelColor(xMove + x, yMove + y, BLACK); + } + data >>= 1; // Move a bit + } + } +} + +void OLEDDisplay::drawStringInternal(int16_t xMove, int16_t yMove, char* text, uint16_t textLength, uint16_t textWidth) { + uint8_t textHeight = pgm_read_byte(fontData + HEIGHT_POS); + uint8_t firstChar = pgm_read_byte(fontData + FIRST_CHAR_POS); + uint16_t sizeOfJumpTable = pgm_read_byte(fontData + CHAR_NUM_POS) * JUMPTABLE_BYTES; + + uint16_t cursorX = 0; + uint16_t cursorY = 0; + + switch (textAlignment) { + case TEXT_ALIGN_CENTER_BOTH: + yMove -= textHeight >> 1; + // Fallthrough + case TEXT_ALIGN_CENTER: + xMove -= textWidth >> 1; // divide by 2 + break; + case TEXT_ALIGN_RIGHT: + xMove -= textWidth; + break; + case TEXT_ALIGN_LEFT: + break; + } + + // Don't draw anything if it is not on the screen. + if (xMove + textWidth < 0 || xMove > this->width() ) {return;} + if (yMove + textHeight < 0 || yMove > this->width() ) {return;} + + for (uint16_t j = 0; j < textLength; j++) { + int16_t xPos = xMove + cursorX; + int16_t yPos = yMove + cursorY; + + uint8_t code = text[j]; + if (code >= firstChar) { + uint8_t charCode = code - firstChar; + + // 4 Bytes per char code + uint8_t msbJumpToChar = pgm_read_byte( fontData + JUMPTABLE_START + charCode * JUMPTABLE_BYTES ); // MSB \ JumpAddress + uint8_t lsbJumpToChar = pgm_read_byte( fontData + JUMPTABLE_START + charCode * JUMPTABLE_BYTES + JUMPTABLE_LSB); // LSB / + uint8_t charByteSize = pgm_read_byte( fontData + JUMPTABLE_START + charCode * JUMPTABLE_BYTES + JUMPTABLE_SIZE); // Size + uint8_t currentCharWidth = pgm_read_byte( fontData + JUMPTABLE_START + charCode * JUMPTABLE_BYTES + JUMPTABLE_WIDTH); // Width + + // Test if the char is drawable + if (!(msbJumpToChar == 255 && lsbJumpToChar == 255)) { + // Get the position of the char data + uint16_t charDataPosition = JUMPTABLE_START + sizeOfJumpTable + ((msbJumpToChar << 8) + lsbJumpToChar); + drawInternal(xPos, yPos, currentCharWidth, textHeight, fontData, charDataPosition, charByteSize); + } + + cursorX += currentCharWidth; + } + } +} + + +void OLEDDisplay::drawString(int16_t xMove, int16_t yMove, String strUser) { + uint16_t lineHeight = pgm_read_byte(fontData + HEIGHT_POS); + + // char* text must be freed! + char* text = utf8ascii(strUser); + + uint16_t yOffset = 0; + // If the string should be centered vertically too + // we need to now how heigh the string is. + if (textAlignment == TEXT_ALIGN_CENTER_BOTH) { + uint16_t lb = 0; + // Find number of linebreaks in text + for (uint16_t i=0;text[i] != 0; i++) { + lb += (text[i] == 10); + } + // Calculate center + yOffset = (lb * lineHeight) / 2; + } + + uint16_t line = 0; + char* textPart = strtok(text,"\n"); + while (textPart != NULL) { + uint16_t length = strlen(textPart); + drawStringInternal(xMove, yMove - yOffset + (line++) * lineHeight, textPart, length, getStringWidth(textPart, length)); + textPart = strtok(NULL, "\n"); + } + free(text); +} + +void OLEDDisplay::drawStringf( int16_t x, int16_t y, char* buffer, String format, ... ) +{ + va_list myargs; + va_start(myargs, format); + vsprintf(buffer, format.c_str(), myargs); + va_end(myargs); + drawString( x, y, buffer ); +} + +void OLEDDisplay::drawStringMaxWidth(int16_t xMove, int16_t yMove, uint16_t maxLineWidth, String strUser) { + uint16_t firstChar = pgm_read_byte(fontData + FIRST_CHAR_POS); + uint16_t lineHeight = pgm_read_byte(fontData + HEIGHT_POS); + + char* text = utf8ascii(strUser); + + uint16_t length = strlen(text); + uint16_t lastDrawnPos = 0; + uint16_t lineNumber = 0; + uint16_t strWidth = 0; + + uint16_t preferredBreakpoint = 0; + uint16_t widthAtBreakpoint = 0; + + for (uint16_t i = 0; i < length; i++) { + strWidth += pgm_read_byte(fontData + JUMPTABLE_START + (text[i] - firstChar) * JUMPTABLE_BYTES + JUMPTABLE_WIDTH); + + // Always try to break on a space or dash + if (text[i] == ' ' || text[i]== '-') { + preferredBreakpoint = i; + widthAtBreakpoint = strWidth; + } + + if (strWidth >= maxLineWidth) { + if (preferredBreakpoint == 0) { + preferredBreakpoint = i; + widthAtBreakpoint = strWidth; + } + drawStringInternal(xMove, yMove + (lineNumber++) * lineHeight , &text[lastDrawnPos], preferredBreakpoint - lastDrawnPos, widthAtBreakpoint); + lastDrawnPos = preferredBreakpoint + 1; + // It is possible that we did not draw all letters to i so we need + // to account for the width of the chars from `i - preferredBreakpoint` + // by calculating the width we did not draw yet. + strWidth = strWidth - widthAtBreakpoint; + preferredBreakpoint = 0; + } + } + + // Draw last part if needed + if (lastDrawnPos < length) { + drawStringInternal(xMove, yMove + lineNumber * lineHeight , &text[lastDrawnPos], length - lastDrawnPos, getStringWidth(&text[lastDrawnPos], length - lastDrawnPos)); + } + + free(text); +} + +uint16_t OLEDDisplay::getStringWidth(const char* text, uint16_t length) { + uint16_t firstChar = pgm_read_byte(fontData + FIRST_CHAR_POS); + + uint16_t stringWidth = 0; + uint16_t maxWidth = 0; + + while (length--) { + stringWidth += pgm_read_byte(fontData + JUMPTABLE_START + (text[length] - firstChar) * JUMPTABLE_BYTES + JUMPTABLE_WIDTH); + if (text[length] == 10) { + maxWidth = max(maxWidth, stringWidth); + stringWidth = 0; + } + } + + return max(maxWidth, stringWidth); +} + +uint16_t OLEDDisplay::getStringWidth(String strUser) { + char* text = utf8ascii(strUser); + uint16_t length = strlen(text); + uint16_t width = getStringWidth(text, length); + free(text); + return width; +} + +void OLEDDisplay::setTextAlignment(OLEDDISPLAY_TEXT_ALIGNMENT textAlignment) { + this->textAlignment = textAlignment; +} + +void OLEDDisplay::setFont(const uint8_t *fontData) { + this->fontData = fontData; +} + +void OLEDDisplay::displayOn(void) { + sendCommand(DISPLAYON); +} + +void OLEDDisplay::displayOff(void) { + sendCommand(DISPLAYOFF); +} + +void OLEDDisplay::invertDisplay(void) { + sendCommand(INVERTDISPLAY); +} + +void OLEDDisplay::normalDisplay(void) { + sendCommand(NORMALDISPLAY); +} + +void OLEDDisplay::setContrast(uint8_t contrast, uint8_t precharge, uint8_t comdetect) { + sendCommand(SETPRECHARGE); //0xD9 + sendCommand(precharge); //0xF1 default, to lower the contrast, put 1-1F + sendCommand(SETCONTRAST); + sendCommand(contrast); // 0-255 + sendCommand(SETVCOMDETECT); //0xDB, (additionally needed to lower the contrast) + sendCommand(comdetect); //0x40 default, to lower the contrast, put 0 + sendCommand(DISPLAYALLON_RESUME); + sendCommand(NORMALDISPLAY); + sendCommand(DISPLAYON); +} + +void OLEDDisplay::setBrightness(uint8_t brightness) { + uint8_t contrast = brightness; + if (brightness < 128) { + // Magic values to get a smooth/ step-free transition + contrast = brightness * 1.171; + } else { + contrast = brightness * 1.171 - 43; + } + + uint8_t precharge = 241; + if (brightness == 0) { + precharge = 0; + } + uint8_t comdetect = brightness / 8; + + setContrast(contrast, precharge, comdetect); +} + +void OLEDDisplay::resetOrientation() { + sendCommand(SEGREMAP); + sendCommand(COMSCANINC); //Reset screen rotation or mirroring +} + +void OLEDDisplay::flipScreenVertically() { + sendCommand(SEGREMAP | 0x01); + sendCommand(COMSCANDEC); //Rotate screen 180 Deg +} + +void OLEDDisplay::mirrorScreen() { + sendCommand(SEGREMAP); + sendCommand(COMSCANDEC); //Mirror screen +} + +void OLEDDisplay::clear(void) { + memset(buffer, 0, displayBufferSize); +} + +void OLEDDisplay::drawLogBuffer(uint16_t xMove, uint16_t yMove) { + uint16_t lineHeight = pgm_read_byte(fontData + HEIGHT_POS); + // Always align left + setTextAlignment(TEXT_ALIGN_LEFT); + + // State values + uint16_t length = 0; + uint16_t line = 0; + uint16_t lastPos = 0; + + for (uint16_t i=0;ilogBufferFilled;i++){ + // Everytime we have a \n print + if (this->logBuffer[i] == 10) { + length++; + // Draw string on line `line` from lastPos to length + // Passing 0 as the lenght because we are in TEXT_ALIGN_LEFT + drawStringInternal(xMove, yMove + (line++) * lineHeight, &this->logBuffer[lastPos], length, 0); + // Remember last pos + lastPos = i; + // Reset length + length = 0; + } else { + // Count chars until next linebreak + length++; + } + } + // Draw the remaining string + if (length > 0) { + drawStringInternal(xMove, yMove + line * lineHeight, &this->logBuffer[lastPos], length, 0); + } +} + +uint16_t OLEDDisplay::getWidth(void) { + return displayWidth; +} + +uint16_t OLEDDisplay::getHeight(void) { + return displayHeight; +} + +bool OLEDDisplay::setLogBuffer(uint16_t lines, uint16_t chars){ + if (logBuffer != NULL) free(logBuffer); + uint16_t size = lines * chars; + if (size > 0) { + this->logBufferLine = 0; // Lines printed + this->logBufferFilled = 0; // Nothing stored yet + this->logBufferMaxLines = lines; // Lines max printable + this->logBufferSize = size; // Total number of characters the buffer can hold + this->logBuffer = (char *) malloc(size * sizeof(uint8_t)); + if(!this->logBuffer) { + DEBUG_OLEDDISPLAY("[OLEDDISPLAY][setLogBuffer] Not enough memory to create log buffer\n"); + return false; + } + } + return true; +} + +size_t OLEDDisplay::write(uint8_t c) { + if (this->logBufferSize > 0) { + // Don't waste space on \r\n line endings, dropping \r + if (c == 13) return 1; + + // convert UTF-8 character to font table index + c = (this->fontTableLookupFunction)(c); + // drop unknown character + if (c == 0) return 1; + + bool maxLineNotReached = this->logBufferLine < this->logBufferMaxLines; + bool bufferNotFull = this->logBufferFilled < this->logBufferSize; + + // Can we write to the buffer? + if (bufferNotFull && maxLineNotReached) { + this->logBuffer[logBufferFilled] = c; + this->logBufferFilled++; + // Keep track of lines written + if (c == 10) this->logBufferLine++; + } else { + // Max line number is reached + if (!maxLineNotReached) this->logBufferLine--; + + // Find the end of the first line + uint16_t firstLineEnd = 0; + for (uint16_t i=0;ilogBufferFilled;i++) { + if (this->logBuffer[i] == 10){ + // Include last char too + firstLineEnd = i + 1; + break; + } + } + // If there was a line ending + if (firstLineEnd > 0) { + // Calculate the new logBufferFilled value + this->logBufferFilled = logBufferFilled - firstLineEnd; + // Now we move the lines infront of the buffer + memcpy(this->logBuffer, &this->logBuffer[firstLineEnd], logBufferFilled); + } else { + // Let's reuse the buffer if it was full + if (!bufferNotFull) { + this->logBufferFilled = 0; + }// else { + // Nothing to do here + //} + } + write(c); + } + } + // We are always writing all uint8_t to the buffer + return 1; +} + +size_t OLEDDisplay::write(const char* str) { + if (str == NULL) return 0; + size_t length = strlen(str); + for (size_t i = 0; i < length; i++) { + write(str[i]); + } + return length; +} + +#ifdef __MBED__ +int OLEDDisplay::_putc(int c) { + + if (!fontData) + return 1; + if (!logBufferSize) { + uint8_t textHeight = pgm_read_byte(fontData + HEIGHT_POS); + uint16_t lines = this->displayHeight / textHeight; + uint16_t chars = 2 * (this->displayWidth / textHeight); + + if (this->displayHeight % textHeight) + lines++; + if (this->displayWidth % textHeight) + chars++; + setLogBuffer(lines, chars); + } + + return this->write((uint8_t)c); +} +#endif + +// Private functions +void OLEDDisplay::setGeometry(OLEDDISPLAY_GEOMETRY g, uint16_t width, uint16_t height) { + this->geometry = g; + + switch (g) { + case GEOMETRY_128_64: + this->displayWidth = 128; + this->displayHeight = 64; + break; + case GEOMETRY_128_32: + this->displayWidth = 128; + this->displayHeight = 32; + break; + case GEOMETRY_64_48: + this->displayWidth = 64; + this->displayHeight = 48; + break; + case GEOMETRY_64_32: + this->displayWidth = 64; + this->displayHeight = 32; + break; + case GEOMETRY_RAWMODE: + this->displayWidth = width > 0 ? width : 128; + this->displayHeight = height > 0 ? height : 64; + break; + } + this->displayBufferSize = displayWidth * displayHeight / 8; +} + +void OLEDDisplay::sendInitCommands(void) { + if (geometry == GEOMETRY_RAWMODE) + return; + sendCommand(DISPLAYOFF); + sendCommand(SETDISPLAYCLOCKDIV); + sendCommand(0xF0); // Increase speed of the display max ~96Hz + sendCommand(SETMULTIPLEX); + sendCommand(this->height() - 1); + sendCommand(SETDISPLAYOFFSET); + sendCommand(0x00); + if(geometry == GEOMETRY_64_32) + sendCommand(0x00); + else + sendCommand(SETSTARTLINE); + sendCommand(CHARGEPUMP); + sendCommand(0x14); + sendCommand(MEMORYMODE); + sendCommand(0x00); + sendCommand(SEGREMAP); + sendCommand(COMSCANINC); + sendCommand(SETCOMPINS); + + if (geometry == GEOMETRY_128_64 || geometry == GEOMETRY_64_48 || geometry == GEOMETRY_64_32) { + sendCommand(0x12); + } else if (geometry == GEOMETRY_128_32) { + sendCommand(0x02); + } + + sendCommand(SETCONTRAST); + + if (geometry == GEOMETRY_128_64 || geometry == GEOMETRY_64_48 || geometry == GEOMETRY_64_32) { + sendCommand(0xCF); + } else if (geometry == GEOMETRY_128_32) { + sendCommand(0x8F); + } + + sendCommand(SETPRECHARGE); + sendCommand(0xF1); + sendCommand(SETVCOMDETECT); //0xDB, (additionally needed to lower the contrast) + sendCommand(0x40); //0x40 default, to lower the contrast, put 0 + sendCommand(DISPLAYALLON_RESUME); + sendCommand(NORMALDISPLAY); + sendCommand(0x2e); // stop scroll + sendCommand(DISPLAYON); +} + +void inline OLEDDisplay::drawInternal(int16_t xMove, int16_t yMove, int16_t width, int16_t height, const uint8_t *data, uint16_t offset, uint16_t bytesInData) { + if (width < 0 || height < 0) return; + if (yMove + height < 0 || yMove > this->height()) return; + if (xMove + width < 0 || xMove > this->width()) return; + + uint8_t rasterHeight = 1 + ((height - 1) >> 3); // fast ceil(height / 8.0) + int8_t yOffset = yMove & 7; + + bytesInData = bytesInData == 0 ? width * rasterHeight : bytesInData; + + int16_t initYMove = yMove; + int8_t initYOffset = yOffset; + + + for (uint16_t i = 0; i < bytesInData; i++) { + + // Reset if next horizontal drawing phase is started. + if ( i % rasterHeight == 0) { + yMove = initYMove; + yOffset = initYOffset; + } + + uint8_t currentByte = pgm_read_byte(data + offset + i); + + int16_t xPos = xMove + (i / rasterHeight); + int16_t yPos = ((yMove >> 3) + (i % rasterHeight)) * this->width(); + +// int16_t yScreenPos = yMove + yOffset; + int16_t dataPos = xPos + yPos; + + if (dataPos >= 0 && dataPos < displayBufferSize && + xPos >= 0 && xPos < this->width() ) { + + if (yOffset >= 0) { + switch (this->color) { + case WHITE: buffer[dataPos] |= currentByte << yOffset; break; + case BLACK: buffer[dataPos] &= ~(currentByte << yOffset); break; + case INVERSE: buffer[dataPos] ^= currentByte << yOffset; break; + } + + if (dataPos < (displayBufferSize - this->width())) { + switch (this->color) { + case WHITE: buffer[dataPos + this->width()] |= currentByte >> (8 - yOffset); break; + case BLACK: buffer[dataPos + this->width()] &= ~(currentByte >> (8 - yOffset)); break; + case INVERSE: buffer[dataPos + this->width()] ^= currentByte >> (8 - yOffset); break; + } + } + } else { + // Make new offset position + yOffset = -yOffset; + + switch (this->color) { + case WHITE: buffer[dataPos] |= currentByte >> yOffset; break; + case BLACK: buffer[dataPos] &= ~(currentByte >> yOffset); break; + case INVERSE: buffer[dataPos] ^= currentByte >> yOffset; break; + } + + // Prepare for next iteration by moving one block up + yMove -= 8; + + // and setting the new yOffset + yOffset = 8 - yOffset; + } +#ifndef __MBED__ + yield(); +#endif + } + } +} + +// You need to free the char! +char* OLEDDisplay::utf8ascii(String str) { + uint16_t k = 0; + uint16_t length = str.length() + 1; + + // Copy the string into a char array + char* s = (char*) malloc(length * sizeof(char)); + if(!s) { + DEBUG_OLEDDISPLAY("[OLEDDISPLAY][utf8ascii] Can't allocate another char array. Drop support for UTF-8.\n"); + return (char*) str.c_str(); + } + str.toCharArray(s, length); + + length--; + + for (uint16_t i=0; i < length; i++) { + char c = (this->fontTableLookupFunction)(s[i]); + if (c!=0) { + s[k++]=c; + } + } + + s[k]=0; + + // This will leak 's' be sure to free it in the calling function. + return s; +} + +void OLEDDisplay::setFontTableLookupFunction(FontTableLookupFunction function) { + this->fontTableLookupFunction = function; +} + + +char DefaultFontTableLookup(const uint8_t ch) { + // UTF-8 to font table index converter + // Code form http://playground.arduino.cc/Main/Utf8ascii + static uint8_t LASTCHAR; + + if (ch < 128) { // Standard ASCII-set 0..0x7F handling + LASTCHAR = 0; + return ch; + } + + uint8_t last = LASTCHAR; // get last char + LASTCHAR = ch; + + switch (last) { // conversion depnding on first UTF8-character + case 0xC2: return (uint8_t) ch; + case 0xC3: return (uint8_t) (ch | 0xC0); + case 0x82: if (ch == 0xAC) return (uint8_t) 0x80; // special case Euro-symbol + } + + return (uint8_t) 0; // otherwise: return zero, if character has to be ignored +} diff --git a/lib/esp8266-oled-ssd1306-master/src/OLEDDisplay.h b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplay.h new file mode 100644 index 00000000..8500b761 --- /dev/null +++ b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplay.h @@ -0,0 +1,383 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn + * Copyright (c) 2018 by Fabrice Weinberg + * Copyright (c) 2019 by Helmut Tschemernjak - www.radioshuttle.de + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * ThingPulse invests considerable time and money to develop these open source libraries. + * Please support us by buying our products (and not the clones) from + * https://thingpulse.com + * + */ + +#ifndef OLEDDISPLAY_h +#define OLEDDISPLAY_h + +#ifdef ARDUINO +#include +#elif __MBED__ +#define pgm_read_byte(addr) (*(const unsigned char *)(addr)) + +#include +#define delay(x) wait_ms(x) +#define yield() void() + +/* + * This is a little Arduino String emulation to keep the OLEDDisplay + * library code in common between Arduino and mbed-os + */ +class String { +public: + String(const char *s) { _str = s; }; + int length() { return strlen(_str); }; + const char *c_str() { return _str; }; + void toCharArray(char *buf, unsigned int bufsize, unsigned int index = 0) const { + memcpy(buf, _str + index, std::min(bufsize, strlen(_str))); + }; +private: + const char *_str; +}; + +#else +#error "Unkown operating system" +#endif + +#include "OLEDDisplayFonts.h" + +//#define DEBUG_OLEDDISPLAY(...) Serial.printf( __VA_ARGS__ ) +//#define DEBUG_OLEDDISPLAY(...) dprintf("%s", __VA_ARGS__ ) + +#ifndef DEBUG_OLEDDISPLAY +#define DEBUG_OLEDDISPLAY(...) +#endif + +// Use DOUBLE BUFFERING by default +#ifndef OLEDDISPLAY_REDUCE_MEMORY +#define OLEDDISPLAY_DOUBLE_BUFFER +#endif + +// Header Values +#define JUMPTABLE_BYTES 4 + +#define JUMPTABLE_LSB 1 +#define JUMPTABLE_SIZE 2 +#define JUMPTABLE_WIDTH 3 +#define JUMPTABLE_START 4 + +#define WIDTH_POS 0 +#define HEIGHT_POS 1 +#define FIRST_CHAR_POS 2 +#define CHAR_NUM_POS 3 + + +// Display commands +#define CHARGEPUMP 0x8D +#define COLUMNADDR 0x21 +#define COMSCANDEC 0xC8 +#define COMSCANINC 0xC0 +#define DISPLAYALLON 0xA5 +#define DISPLAYALLON_RESUME 0xA4 +#define DISPLAYOFF 0xAE +#define DISPLAYON 0xAF +#define EXTERNALVCC 0x1 +#define INVERTDISPLAY 0xA7 +#define MEMORYMODE 0x20 +#define NORMALDISPLAY 0xA6 +#define PAGEADDR 0x22 +#define SEGREMAP 0xA0 +#define SETCOMPINS 0xDA +#define SETCONTRAST 0x81 +#define SETDISPLAYCLOCKDIV 0xD5 +#define SETDISPLAYOFFSET 0xD3 +#define SETHIGHCOLUMN 0x10 +#define SETLOWCOLUMN 0x00 +#define SETMULTIPLEX 0xA8 +#define SETPRECHARGE 0xD9 +#define SETSEGMENTREMAP 0xA1 +#define SETSTARTLINE 0x40 +#define SETVCOMDETECT 0xDB +#define SWITCHCAPVCC 0x2 + +#ifndef _swap_int16_t +#define _swap_int16_t(a, b) { int16_t t = a; a = b; b = t; } +#endif + +enum OLEDDISPLAY_COLOR { + BLACK = 0, + WHITE = 1, + INVERSE = 2 +}; + +enum OLEDDISPLAY_TEXT_ALIGNMENT { + TEXT_ALIGN_LEFT = 0, + TEXT_ALIGN_RIGHT = 1, + TEXT_ALIGN_CENTER = 2, + TEXT_ALIGN_CENTER_BOTH = 3 +}; + + +enum OLEDDISPLAY_GEOMETRY { + GEOMETRY_128_64 = 0, + GEOMETRY_128_32 = 1, + GEOMETRY_64_48 = 2, + GEOMETRY_64_32 = 3, + GEOMETRY_RAWMODE = 4 +}; + +enum HW_I2C { + I2C_ONE, + I2C_TWO +}; + +typedef char (*FontTableLookupFunction)(const uint8_t ch); +char DefaultFontTableLookup(const uint8_t ch); + + +#ifdef ARDUINO +class OLEDDisplay : public Print { +#elif __MBED__ +class OLEDDisplay : public Stream { +#else +#error "Unkown operating system" +#endif + + public: + OLEDDisplay(); + virtual ~OLEDDisplay(); + + uint16_t width(void) const { return displayWidth; }; + uint16_t height(void) const { return displayHeight; }; + + // Use this to resume after a deep sleep without resetting the display (what init() would do). + // Returns true if connection to the display was established and the buffer allocated, false otherwise. + bool allocateBuffer(); + + // Allocates the buffer and initializes the driver & display. Resets the display! + // Returns false if buffer allocation failed, true otherwise. + bool init(); + + // Free the memory used by the display + void end(); + + // Cycle through the initialization + void resetDisplay(void); + + /* Drawing functions */ + // Sets the color of all pixel operations + void setColor(OLEDDISPLAY_COLOR color); + + // Returns the current color. + OLEDDISPLAY_COLOR getColor(); + + // Draw a pixel at given position + void setPixel(int16_t x, int16_t y); + + // Draw a pixel at given position and color + void setPixelColor(int16_t x, int16_t y, OLEDDISPLAY_COLOR color); + + // Clear a pixel at given position FIXME: INVERSE is untested with this function + void clearPixel(int16_t x, int16_t y); + + // Draw a line from position 0 to position 1 + void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1); + + // Draw the border of a rectangle at the given location + void drawRect(int16_t x, int16_t y, int16_t width, int16_t height); + + // Fill the rectangle + void fillRect(int16_t x, int16_t y, int16_t width, int16_t height); + + // Draw the border of a circle + void drawCircle(int16_t x, int16_t y, int16_t radius); + + // Draw all Quadrants specified in the quads bit mask + void drawCircleQuads(int16_t x0, int16_t y0, int16_t radius, uint8_t quads); + + // Fill circle + void fillCircle(int16_t x, int16_t y, int16_t radius); + + // Draw a line horizontally + void drawHorizontalLine(int16_t x, int16_t y, int16_t length); + + // Draw a line vertically + void drawVerticalLine(int16_t x, int16_t y, int16_t length); + + // Draws a rounded progress bar with the outer dimensions given by width and height. Progress is + // a unsigned byte value between 0 and 100 + void drawProgressBar(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t progress); + + // Draw a bitmap in the internal image format + void drawFastImage(int16_t x, int16_t y, int16_t width, int16_t height, const uint8_t *image); + + // Draw a XBM + void drawXbm(int16_t x, int16_t y, int16_t width, int16_t height, const uint8_t *xbm); + + // Draw icon 16x16 xbm format + void drawIco16x16(int16_t x, int16_t y, const char *ico, bool inverse = false); + + /* Text functions */ + + // Draws a string at the given location + void drawString(int16_t x, int16_t y, String text); + + // Draws a formatted string (like printf) at the given location + void drawStringf(int16_t x, int16_t y, char* buffer, String format, ... ); + + // Draws a String with a maximum width at the given location. + // If the given String is wider than the specified width + // The text will be wrapped to the next line at a space or dash + void drawStringMaxWidth(int16_t x, int16_t y, uint16_t maxLineWidth, String text); + + // Returns the width of the const char* with the current + // font settings + uint16_t getStringWidth(const char* text, uint16_t length); + + // Convencience method for the const char version + uint16_t getStringWidth(String text); + + // Specifies relative to which anchor point + // the text is rendered. Available constants: + // TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER_BOTH + void setTextAlignment(OLEDDISPLAY_TEXT_ALIGNMENT textAlignment); + + // Sets the current font. Available default fonts + // ArialMT_Plain_10, ArialMT_Plain_16, ArialMT_Plain_24 + void setFont(const uint8_t *fontData); + + // Set the function that will convert utf-8 to font table index + void setFontTableLookupFunction(FontTableLookupFunction function); + + /* Display functions */ + + // Turn the display on + void displayOn(void); + + // Turn the display offs + void displayOff(void); + + // Inverted display mode + void invertDisplay(void); + + // Normal display mode + void normalDisplay(void); + + // Set display contrast + // really low brightness & contrast: contrast = 10, precharge = 5, comdetect = 0 + // normal brightness & contrast: contrast = 100 + void setContrast(uint8_t contrast, uint8_t precharge = 241, uint8_t comdetect = 64); + + // Convenience method to access + void setBrightness(uint8_t); + + // Reset display rotation or mirroring + void resetOrientation(); + + // Turn the display upside down + void flipScreenVertically(); + + // Mirror the display (to be used in a mirror or as a projector) + void mirrorScreen(); + + // Write the buffer to the display memory + virtual void display(void) = 0; + + // Clear the local pixel buffer + void clear(void); + + // Log buffer implementation + + // This will define the lines and characters you can + // print to the screen. When you exeed the buffer size (lines * chars) + // the output may be truncated due to the size constraint. + bool setLogBuffer(uint16_t lines, uint16_t chars); + + // Draw the log buffer at position (x, y) + void drawLogBuffer(uint16_t x, uint16_t y); + + // Get screen geometry + uint16_t getWidth(void); + uint16_t getHeight(void); + + // Implement needed function to be compatible with Print class + size_t write(uint8_t c); + size_t write(const char* s); + + // Implement needed function to be compatible with Stream class +#ifdef __MBED__ + int _putc(int c); + int _getc() { return -1; }; +#endif + + + uint8_t *buffer; + + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + uint8_t *buffer_back; + #endif + + protected: + + OLEDDISPLAY_GEOMETRY geometry; + + uint16_t displayWidth; + uint16_t displayHeight; + uint16_t displayBufferSize; + + // Set the correct height, width and buffer for the geometry + void setGeometry(OLEDDISPLAY_GEOMETRY g, uint16_t width = 0, uint16_t height = 0); + + OLEDDISPLAY_TEXT_ALIGNMENT textAlignment; + OLEDDISPLAY_COLOR color; + + const uint8_t *fontData; + + // State values for logBuffer + uint16_t logBufferSize; + uint16_t logBufferFilled; + uint16_t logBufferLine; + uint16_t logBufferMaxLines; + char *logBuffer; + + + // the header size of the buffer used, e.g. for the SPI command header + virtual int getBufferOffset(void) = 0; + + // Send a command to the display (low level function) + virtual void sendCommand(uint8_t com) {(void)com;}; + + // Connect to the display + virtual bool connect() { return false; }; + + // Send all the init commands + void sendInitCommands(); + + // converts utf8 characters to extended ascii + char* utf8ascii(String s); + + void inline drawInternal(int16_t xMove, int16_t yMove, int16_t width, int16_t height, const uint8_t *data, uint16_t offset, uint16_t bytesInData) __attribute__((always_inline)); + + void drawStringInternal(int16_t xMove, int16_t yMove, char* text, uint16_t textLength, uint16_t textWidth); + + FontTableLookupFunction fontTableLookupFunction; +}; + +#endif diff --git a/lib/esp8266-oled-ssd1306-master/src/OLEDDisplayFonts.h b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplayFonts.h new file mode 100644 index 00000000..abc61ba1 --- /dev/null +++ b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplayFonts.h @@ -0,0 +1,1278 @@ +#ifndef OLEDDISPLAYFONTS_h +#define OLEDDISPLAYFONTS_h + +#ifdef __MBED__ +#define PROGMEM +#endif + +const uint8_t ArialMT_Plain_10[] PROGMEM = { + 0x0A, // Width: 10 + 0x0D, // Height: 13 + 0x20, // First Char: 32 + 0xE0, // Numbers of Chars: 224 + + // Jump Table: + 0xFF, 0xFF, 0x00, 0x03, // 32:65535 + 0x00, 0x00, 0x04, 0x03, // 33:0 + 0x00, 0x04, 0x05, 0x04, // 34:4 + 0x00, 0x09, 0x09, 0x06, // 35:9 + 0x00, 0x12, 0x0A, 0x06, // 36:18 + 0x00, 0x1C, 0x10, 0x09, // 37:28 + 0x00, 0x2C, 0x0E, 0x07, // 38:44 + 0x00, 0x3A, 0x01, 0x02, // 39:58 + 0x00, 0x3B, 0x06, 0x03, // 40:59 + 0x00, 0x41, 0x06, 0x03, // 41:65 + 0x00, 0x47, 0x05, 0x04, // 42:71 + 0x00, 0x4C, 0x09, 0x06, // 43:76 + 0x00, 0x55, 0x04, 0x03, // 44:85 + 0x00, 0x59, 0x03, 0x03, // 45:89 + 0x00, 0x5C, 0x04, 0x03, // 46:92 + 0x00, 0x60, 0x05, 0x03, // 47:96 + 0x00, 0x65, 0x0A, 0x06, // 48:101 + 0x00, 0x6F, 0x08, 0x06, // 49:111 + 0x00, 0x77, 0x0A, 0x06, // 50:119 + 0x00, 0x81, 0x0A, 0x06, // 51:129 + 0x00, 0x8B, 0x0B, 0x06, // 52:139 + 0x00, 0x96, 0x0A, 0x06, // 53:150 + 0x00, 0xA0, 0x0A, 0x06, // 54:160 + 0x00, 0xAA, 0x09, 0x06, // 55:170 + 0x00, 0xB3, 0x0A, 0x06, // 56:179 + 0x00, 0xBD, 0x0A, 0x06, // 57:189 + 0x00, 0xC7, 0x04, 0x03, // 58:199 + 0x00, 0xCB, 0x04, 0x03, // 59:203 + 0x00, 0xCF, 0x0A, 0x06, // 60:207 + 0x00, 0xD9, 0x09, 0x06, // 61:217 + 0x00, 0xE2, 0x09, 0x06, // 62:226 + 0x00, 0xEB, 0x0B, 0x06, // 63:235 + 0x00, 0xF6, 0x14, 0x0A, // 64:246 + 0x01, 0x0A, 0x0E, 0x07, // 65:266 + 0x01, 0x18, 0x0C, 0x07, // 66:280 + 0x01, 0x24, 0x0C, 0x07, // 67:292 + 0x01, 0x30, 0x0B, 0x07, // 68:304 + 0x01, 0x3B, 0x0C, 0x07, // 69:315 + 0x01, 0x47, 0x09, 0x06, // 70:327 + 0x01, 0x50, 0x0D, 0x08, // 71:336 + 0x01, 0x5D, 0x0C, 0x07, // 72:349 + 0x01, 0x69, 0x04, 0x03, // 73:361 + 0x01, 0x6D, 0x08, 0x05, // 74:365 + 0x01, 0x75, 0x0E, 0x07, // 75:373 + 0x01, 0x83, 0x0C, 0x06, // 76:387 + 0x01, 0x8F, 0x10, 0x08, // 77:399 + 0x01, 0x9F, 0x0C, 0x07, // 78:415 + 0x01, 0xAB, 0x0E, 0x08, // 79:427 + 0x01, 0xB9, 0x0B, 0x07, // 80:441 + 0x01, 0xC4, 0x0E, 0x08, // 81:452 + 0x01, 0xD2, 0x0C, 0x07, // 82:466 + 0x01, 0xDE, 0x0C, 0x07, // 83:478 + 0x01, 0xEA, 0x0B, 0x06, // 84:490 + 0x01, 0xF5, 0x0C, 0x07, // 85:501 + 0x02, 0x01, 0x0D, 0x07, // 86:513 + 0x02, 0x0E, 0x11, 0x09, // 87:526 + 0x02, 0x1F, 0x0E, 0x07, // 88:543 + 0x02, 0x2D, 0x0D, 0x07, // 89:557 + 0x02, 0x3A, 0x0C, 0x06, // 90:570 + 0x02, 0x46, 0x06, 0x03, // 91:582 + 0x02, 0x4C, 0x06, 0x03, // 92:588 + 0x02, 0x52, 0x04, 0x03, // 93:594 + 0x02, 0x56, 0x09, 0x05, // 94:598 + 0x02, 0x5F, 0x0C, 0x06, // 95:607 + 0x02, 0x6B, 0x03, 0x03, // 96:619 + 0x02, 0x6E, 0x0A, 0x06, // 97:622 + 0x02, 0x78, 0x0A, 0x06, // 98:632 + 0x02, 0x82, 0x0A, 0x05, // 99:642 + 0x02, 0x8C, 0x0A, 0x06, // 100:652 + 0x02, 0x96, 0x0A, 0x06, // 101:662 + 0x02, 0xA0, 0x05, 0x03, // 102:672 + 0x02, 0xA5, 0x0A, 0x06, // 103:677 + 0x02, 0xAF, 0x0A, 0x06, // 104:687 + 0x02, 0xB9, 0x04, 0x02, // 105:697 + 0x02, 0xBD, 0x04, 0x02, // 106:701 + 0x02, 0xC1, 0x08, 0x05, // 107:705 + 0x02, 0xC9, 0x04, 0x02, // 108:713 + 0x02, 0xCD, 0x10, 0x08, // 109:717 + 0x02, 0xDD, 0x0A, 0x06, // 110:733 + 0x02, 0xE7, 0x0A, 0x06, // 111:743 + 0x02, 0xF1, 0x0A, 0x06, // 112:753 + 0x02, 0xFB, 0x0A, 0x06, // 113:763 + 0x03, 0x05, 0x05, 0x03, // 114:773 + 0x03, 0x0A, 0x08, 0x05, // 115:778 + 0x03, 0x12, 0x06, 0x03, // 116:786 + 0x03, 0x18, 0x0A, 0x06, // 117:792 + 0x03, 0x22, 0x09, 0x05, // 118:802 + 0x03, 0x2B, 0x0E, 0x07, // 119:811 + 0x03, 0x39, 0x0A, 0x05, // 120:825 + 0x03, 0x43, 0x09, 0x05, // 121:835 + 0x03, 0x4C, 0x0A, 0x05, // 122:844 + 0x03, 0x56, 0x06, 0x03, // 123:854 + 0x03, 0x5C, 0x04, 0x03, // 124:860 + 0x03, 0x60, 0x05, 0x03, // 125:864 + 0x03, 0x65, 0x09, 0x06, // 126:869 + 0xFF, 0xFF, 0x00, 0x00, // 127:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 128:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 129:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 130:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 131:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 132:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 133:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 134:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 135:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 136:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 137:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 138:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 139:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 140:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 141:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 142:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 143:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 144:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 145:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 146:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 147:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 148:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 149:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 150:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 151:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 152:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 153:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 154:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 155:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 156:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 157:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 158:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 159:65535 + 0xFF, 0xFF, 0x00, 0x03, // 160:65535 + 0x03, 0x6E, 0x04, 0x03, // 161:878 + 0x03, 0x72, 0x0A, 0x06, // 162:882 + 0x03, 0x7C, 0x0C, 0x06, // 163:892 + 0x03, 0x88, 0x0A, 0x06, // 164:904 + 0x03, 0x92, 0x0A, 0x06, // 165:914 + 0x03, 0x9C, 0x04, 0x03, // 166:924 + 0x03, 0xA0, 0x0A, 0x06, // 167:928 + 0x03, 0xAA, 0x05, 0x03, // 168:938 + 0x03, 0xAF, 0x0D, 0x07, // 169:943 + 0x03, 0xBC, 0x07, 0x04, // 170:956 + 0x03, 0xC3, 0x0A, 0x06, // 171:963 + 0x03, 0xCD, 0x09, 0x06, // 172:973 + 0x03, 0xD6, 0x03, 0x03, // 173:982 + 0x03, 0xD9, 0x0D, 0x07, // 174:985 + 0x03, 0xE6, 0x0B, 0x06, // 175:998 + 0x03, 0xF1, 0x07, 0x04, // 176:1009 + 0x03, 0xF8, 0x0A, 0x05, // 177:1016 + 0x04, 0x02, 0x05, 0x03, // 178:1026 + 0x04, 0x07, 0x05, 0x03, // 179:1031 + 0x04, 0x0C, 0x05, 0x03, // 180:1036 + 0x04, 0x11, 0x0A, 0x06, // 181:1041 + 0x04, 0x1B, 0x09, 0x05, // 182:1051 + 0x04, 0x24, 0x03, 0x03, // 183:1060 + 0x04, 0x27, 0x06, 0x03, // 184:1063 + 0x04, 0x2D, 0x05, 0x03, // 185:1069 + 0x04, 0x32, 0x07, 0x04, // 186:1074 + 0x04, 0x39, 0x0A, 0x06, // 187:1081 + 0x04, 0x43, 0x10, 0x08, // 188:1091 + 0x04, 0x53, 0x10, 0x08, // 189:1107 + 0x04, 0x63, 0x10, 0x08, // 190:1123 + 0x04, 0x73, 0x0A, 0x06, // 191:1139 + 0x04, 0x7D, 0x0E, 0x07, // 192:1149 + 0x04, 0x8B, 0x0E, 0x07, // 193:1163 + 0x04, 0x99, 0x0E, 0x07, // 194:1177 + 0x04, 0xA7, 0x0E, 0x07, // 195:1191 + 0x04, 0xB5, 0x0E, 0x07, // 196:1205 + 0x04, 0xC3, 0x0E, 0x07, // 197:1219 + 0x04, 0xD1, 0x12, 0x0A, // 198:1233 + 0x04, 0xE3, 0x0C, 0x07, // 199:1251 + 0x04, 0xEF, 0x0C, 0x07, // 200:1263 + 0x04, 0xFB, 0x0C, 0x07, // 201:1275 + 0x05, 0x07, 0x0C, 0x07, // 202:1287 + 0x05, 0x13, 0x0C, 0x07, // 203:1299 + 0x05, 0x1F, 0x05, 0x03, // 204:1311 + 0x05, 0x24, 0x04, 0x03, // 205:1316 + 0x05, 0x28, 0x04, 0x03, // 206:1320 + 0x05, 0x2C, 0x05, 0x03, // 207:1324 + 0x05, 0x31, 0x0B, 0x07, // 208:1329 + 0x05, 0x3C, 0x0C, 0x07, // 209:1340 + 0x05, 0x48, 0x0E, 0x08, // 210:1352 + 0x05, 0x56, 0x0E, 0x08, // 211:1366 + 0x05, 0x64, 0x0E, 0x08, // 212:1380 + 0x05, 0x72, 0x0E, 0x08, // 213:1394 + 0x05, 0x80, 0x0E, 0x08, // 214:1408 + 0x05, 0x8E, 0x0A, 0x06, // 215:1422 + 0x05, 0x98, 0x0D, 0x08, // 216:1432 + 0x05, 0xA5, 0x0C, 0x07, // 217:1445 + 0x05, 0xB1, 0x0C, 0x07, // 218:1457 + 0x05, 0xBD, 0x0C, 0x07, // 219:1469 + 0x05, 0xC9, 0x0C, 0x07, // 220:1481 + 0x05, 0xD5, 0x0D, 0x07, // 221:1493 + 0x05, 0xE2, 0x0B, 0x07, // 222:1506 + 0x05, 0xED, 0x0C, 0x06, // 223:1517 + 0x05, 0xF9, 0x0A, 0x06, // 224:1529 + 0x06, 0x03, 0x0A, 0x06, // 225:1539 + 0x06, 0x0D, 0x0A, 0x06, // 226:1549 + 0x06, 0x17, 0x0A, 0x06, // 227:1559 + 0x06, 0x21, 0x0A, 0x06, // 228:1569 + 0x06, 0x2B, 0x0A, 0x06, // 229:1579 + 0x06, 0x35, 0x10, 0x09, // 230:1589 + 0x06, 0x45, 0x0A, 0x05, // 231:1605 + 0x06, 0x4F, 0x0A, 0x06, // 232:1615 + 0x06, 0x59, 0x0A, 0x06, // 233:1625 + 0x06, 0x63, 0x0A, 0x06, // 234:1635 + 0x06, 0x6D, 0x0A, 0x06, // 235:1645 + 0x06, 0x77, 0x05, 0x03, // 236:1655 + 0x06, 0x7C, 0x04, 0x03, // 237:1660 + 0x06, 0x80, 0x05, 0x03, // 238:1664 + 0x06, 0x85, 0x05, 0x03, // 239:1669 + 0x06, 0x8A, 0x0A, 0x06, // 240:1674 + 0x06, 0x94, 0x0A, 0x06, // 241:1684 + 0x06, 0x9E, 0x0A, 0x06, // 242:1694 + 0x06, 0xA8, 0x0A, 0x06, // 243:1704 + 0x06, 0xB2, 0x0A, 0x06, // 244:1714 + 0x06, 0xBC, 0x0A, 0x06, // 245:1724 + 0x06, 0xC6, 0x0A, 0x06, // 246:1734 + 0x06, 0xD0, 0x09, 0x05, // 247:1744 + 0x06, 0xD9, 0x0A, 0x06, // 248:1753 + 0x06, 0xE3, 0x0A, 0x06, // 249:1763 + 0x06, 0xED, 0x0A, 0x06, // 250:1773 + 0x06, 0xF7, 0x0A, 0x06, // 251:1783 + 0x07, 0x01, 0x0A, 0x06, // 252:1793 + 0x07, 0x0B, 0x09, 0x05, // 253:1803 + 0x07, 0x14, 0x0A, 0x06, // 254:1812 + 0x07, 0x1E, 0x09, 0x05, // 255:1822 + + // Font Data: + 0x00,0x00,0xF8,0x02, // 33 + 0x38,0x00,0x00,0x00,0x38, // 34 + 0xA0,0x03,0xE0,0x00,0xB8,0x03,0xE0,0x00,0xB8, // 35 + 0x30,0x01,0x28,0x02,0xF8,0x07,0x48,0x02,0x90,0x01, // 36 + 0x00,0x00,0x30,0x00,0x48,0x00,0x30,0x03,0xC0,0x00,0xB0,0x01,0x48,0x02,0x80,0x01, // 37 + 0x80,0x01,0x50,0x02,0x68,0x02,0xA8,0x02,0x18,0x01,0x80,0x03,0x80,0x02, // 38 + 0x38, // 39 + 0xE0,0x03,0x10,0x04,0x08,0x08, // 40 + 0x08,0x08,0x10,0x04,0xE0,0x03, // 41 + 0x28,0x00,0x18,0x00,0x28, // 42 + 0x40,0x00,0x40,0x00,0xF0,0x01,0x40,0x00,0x40, // 43 + 0x00,0x00,0x00,0x06, // 44 + 0x80,0x00,0x80, // 45 + 0x00,0x00,0x00,0x02, // 46 + 0x00,0x03,0xE0,0x00,0x18, // 47 + 0xF0,0x01,0x08,0x02,0x08,0x02,0x08,0x02,0xF0,0x01, // 48 + 0x00,0x00,0x20,0x00,0x10,0x00,0xF8,0x03, // 49 + 0x10,0x02,0x08,0x03,0x88,0x02,0x48,0x02,0x30,0x02, // 50 + 0x10,0x01,0x08,0x02,0x48,0x02,0x48,0x02,0xB0,0x01, // 51 + 0xC0,0x00,0xA0,0x00,0x90,0x00,0x88,0x00,0xF8,0x03,0x80, // 52 + 0x60,0x01,0x38,0x02,0x28,0x02,0x28,0x02,0xC8,0x01, // 53 + 0xF0,0x01,0x28,0x02,0x28,0x02,0x28,0x02,0xD0,0x01, // 54 + 0x08,0x00,0x08,0x03,0xC8,0x00,0x38,0x00,0x08, // 55 + 0xB0,0x01,0x48,0x02,0x48,0x02,0x48,0x02,0xB0,0x01, // 56 + 0x70,0x01,0x88,0x02,0x88,0x02,0x88,0x02,0xF0,0x01, // 57 + 0x00,0x00,0x20,0x02, // 58 + 0x00,0x00,0x20,0x06, // 59 + 0x00,0x00,0x40,0x00,0xA0,0x00,0xA0,0x00,0x10,0x01, // 60 + 0xA0,0x00,0xA0,0x00,0xA0,0x00,0xA0,0x00,0xA0, // 61 + 0x00,0x00,0x10,0x01,0xA0,0x00,0xA0,0x00,0x40, // 62 + 0x10,0x00,0x08,0x00,0x08,0x00,0xC8,0x02,0x48,0x00,0x30, // 63 + 0x00,0x00,0xC0,0x03,0x30,0x04,0xD0,0x09,0x28,0x0A,0x28,0x0A,0xC8,0x0B,0x68,0x0A,0x10,0x05,0xE0,0x04, // 64 + 0x00,0x02,0xC0,0x01,0xB0,0x00,0x88,0x00,0xB0,0x00,0xC0,0x01,0x00,0x02, // 65 + 0x00,0x00,0xF8,0x03,0x48,0x02,0x48,0x02,0x48,0x02,0xF0,0x01, // 66 + 0x00,0x00,0xF0,0x01,0x08,0x02,0x08,0x02,0x08,0x02,0x10,0x01, // 67 + 0x00,0x00,0xF8,0x03,0x08,0x02,0x08,0x02,0x10,0x01,0xE0, // 68 + 0x00,0x00,0xF8,0x03,0x48,0x02,0x48,0x02,0x48,0x02,0x48,0x02, // 69 + 0x00,0x00,0xF8,0x03,0x48,0x00,0x48,0x00,0x08, // 70 + 0x00,0x00,0xE0,0x00,0x10,0x01,0x08,0x02,0x48,0x02,0x50,0x01,0xC0, // 71 + 0x00,0x00,0xF8,0x03,0x40,0x00,0x40,0x00,0x40,0x00,0xF8,0x03, // 72 + 0x00,0x00,0xF8,0x03, // 73 + 0x00,0x03,0x00,0x02,0x00,0x02,0xF8,0x01, // 74 + 0x00,0x00,0xF8,0x03,0x80,0x00,0x60,0x00,0x90,0x00,0x08,0x01,0x00,0x02, // 75 + 0x00,0x00,0xF8,0x03,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02, // 76 + 0x00,0x00,0xF8,0x03,0x30,0x00,0xC0,0x01,0x00,0x02,0xC0,0x01,0x30,0x00,0xF8,0x03, // 77 + 0x00,0x00,0xF8,0x03,0x30,0x00,0x40,0x00,0x80,0x01,0xF8,0x03, // 78 + 0x00,0x00,0xF0,0x01,0x08,0x02,0x08,0x02,0x08,0x02,0x08,0x02,0xF0,0x01, // 79 + 0x00,0x00,0xF8,0x03,0x48,0x00,0x48,0x00,0x48,0x00,0x30, // 80 + 0x00,0x00,0xF0,0x01,0x08,0x02,0x08,0x02,0x08,0x03,0x08,0x03,0xF0,0x02, // 81 + 0x00,0x00,0xF8,0x03,0x48,0x00,0x48,0x00,0xC8,0x00,0x30,0x03, // 82 + 0x00,0x00,0x30,0x01,0x48,0x02,0x48,0x02,0x48,0x02,0x90,0x01, // 83 + 0x00,0x00,0x08,0x00,0x08,0x00,0xF8,0x03,0x08,0x00,0x08, // 84 + 0x00,0x00,0xF8,0x01,0x00,0x02,0x00,0x02,0x00,0x02,0xF8,0x01, // 85 + 0x08,0x00,0x70,0x00,0x80,0x01,0x00,0x02,0x80,0x01,0x70,0x00,0x08, // 86 + 0x18,0x00,0xE0,0x01,0x00,0x02,0xF0,0x01,0x08,0x00,0xF0,0x01,0x00,0x02,0xE0,0x01,0x18, // 87 + 0x00,0x02,0x08,0x01,0x90,0x00,0x60,0x00,0x90,0x00,0x08,0x01,0x00,0x02, // 88 + 0x08,0x00,0x10,0x00,0x20,0x00,0xC0,0x03,0x20,0x00,0x10,0x00,0x08, // 89 + 0x08,0x03,0x88,0x02,0xC8,0x02,0x68,0x02,0x38,0x02,0x18,0x02, // 90 + 0x00,0x00,0xF8,0x0F,0x08,0x08, // 91 + 0x18,0x00,0xE0,0x00,0x00,0x03, // 92 + 0x08,0x08,0xF8,0x0F, // 93 + 0x40,0x00,0x30,0x00,0x08,0x00,0x30,0x00,0x40, // 94 + 0x00,0x08,0x00,0x08,0x00,0x08,0x00,0x08,0x00,0x08,0x00,0x08, // 95 + 0x08,0x00,0x10, // 96 + 0x00,0x00,0x00,0x03,0xA0,0x02,0xA0,0x02,0xE0,0x03, // 97 + 0x00,0x00,0xF8,0x03,0x20,0x02,0x20,0x02,0xC0,0x01, // 98 + 0x00,0x00,0xC0,0x01,0x20,0x02,0x20,0x02,0x40,0x01, // 99 + 0x00,0x00,0xC0,0x01,0x20,0x02,0x20,0x02,0xF8,0x03, // 100 + 0x00,0x00,0xC0,0x01,0xA0,0x02,0xA0,0x02,0xC0,0x02, // 101 + 0x20,0x00,0xF0,0x03,0x28, // 102 + 0x00,0x00,0xC0,0x05,0x20,0x0A,0x20,0x0A,0xE0,0x07, // 103 + 0x00,0x00,0xF8,0x03,0x20,0x00,0x20,0x00,0xC0,0x03, // 104 + 0x00,0x00,0xE8,0x03, // 105 + 0x00,0x08,0xE8,0x07, // 106 + 0xF8,0x03,0x80,0x00,0xC0,0x01,0x20,0x02, // 107 + 0x00,0x00,0xF8,0x03, // 108 + 0x00,0x00,0xE0,0x03,0x20,0x00,0x20,0x00,0xE0,0x03,0x20,0x00,0x20,0x00,0xC0,0x03, // 109 + 0x00,0x00,0xE0,0x03,0x20,0x00,0x20,0x00,0xC0,0x03, // 110 + 0x00,0x00,0xC0,0x01,0x20,0x02,0x20,0x02,0xC0,0x01, // 111 + 0x00,0x00,0xE0,0x0F,0x20,0x02,0x20,0x02,0xC0,0x01, // 112 + 0x00,0x00,0xC0,0x01,0x20,0x02,0x20,0x02,0xE0,0x0F, // 113 + 0x00,0x00,0xE0,0x03,0x20, // 114 + 0x40,0x02,0xA0,0x02,0xA0,0x02,0x20,0x01, // 115 + 0x20,0x00,0xF8,0x03,0x20,0x02, // 116 + 0x00,0x00,0xE0,0x01,0x00,0x02,0x00,0x02,0xE0,0x03, // 117 + 0x20,0x00,0xC0,0x01,0x00,0x02,0xC0,0x01,0x20, // 118 + 0xE0,0x01,0x00,0x02,0xC0,0x01,0x20,0x00,0xC0,0x01,0x00,0x02,0xE0,0x01, // 119 + 0x20,0x02,0x40,0x01,0x80,0x00,0x40,0x01,0x20,0x02, // 120 + 0x20,0x00,0xC0,0x09,0x00,0x06,0xC0,0x01,0x20, // 121 + 0x20,0x02,0x20,0x03,0xA0,0x02,0x60,0x02,0x20,0x02, // 122 + 0x80,0x00,0x78,0x0F,0x08,0x08, // 123 + 0x00,0x00,0xF8,0x0F, // 124 + 0x08,0x08,0x78,0x0F,0x80, // 125 + 0xC0,0x00,0x40,0x00,0xC0,0x00,0x80,0x00,0xC0, // 126 + 0x00,0x00,0xA0,0x0F, // 161 + 0x00,0x00,0xC0,0x01,0xA0,0x0F,0x78,0x02,0x40,0x01, // 162 + 0x40,0x02,0x70,0x03,0xC8,0x02,0x48,0x02,0x08,0x02,0x10,0x02, // 163 + 0x00,0x00,0xE0,0x01,0x20,0x01,0x20,0x01,0xE0,0x01, // 164 + 0x48,0x01,0x70,0x01,0xC0,0x03,0x70,0x01,0x48,0x01, // 165 + 0x00,0x00,0x38,0x0F, // 166 + 0xD0,0x04,0x28,0x09,0x48,0x09,0x48,0x0A,0x90,0x05, // 167 + 0x08,0x00,0x00,0x00,0x08, // 168 + 0xE0,0x00,0x10,0x01,0x48,0x02,0xA8,0x02,0xA8,0x02,0x10,0x01,0xE0, // 169 + 0x68,0x00,0x68,0x00,0x68,0x00,0x78, // 170 + 0x00,0x00,0x80,0x01,0x40,0x02,0x80,0x01,0x40,0x02, // 171 + 0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0xE0, // 172 + 0x80,0x00,0x80, // 173 + 0xE0,0x00,0x10,0x01,0xE8,0x02,0x68,0x02,0xC8,0x02,0x10,0x01,0xE0, // 174 + 0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02, // 175 + 0x00,0x00,0x38,0x00,0x28,0x00,0x38, // 176 + 0x40,0x02,0x40,0x02,0xF0,0x03,0x40,0x02,0x40,0x02, // 177 + 0x48,0x00,0x68,0x00,0x58, // 178 + 0x48,0x00,0x58,0x00,0x68, // 179 + 0x00,0x00,0x10,0x00,0x08, // 180 + 0x00,0x00,0xE0,0x0F,0x00,0x02,0x00,0x02,0xE0,0x03, // 181 + 0x70,0x00,0xF8,0x0F,0x08,0x00,0xF8,0x0F,0x08, // 182 + 0x00,0x00,0x40, // 183 + 0x00,0x00,0x00,0x14,0x00,0x18, // 184 + 0x00,0x00,0x10,0x00,0x78, // 185 + 0x30,0x00,0x48,0x00,0x48,0x00,0x30, // 186 + 0x00,0x00,0x40,0x02,0x80,0x01,0x40,0x02,0x80,0x01, // 187 + 0x00,0x00,0x10,0x02,0x78,0x01,0xC0,0x00,0x20,0x01,0x90,0x01,0xC8,0x03,0x00,0x01, // 188 + 0x00,0x00,0x10,0x02,0x78,0x01,0x80,0x00,0x60,0x00,0x50,0x02,0x48,0x03,0xC0,0x02, // 189 + 0x48,0x00,0x58,0x00,0x68,0x03,0x80,0x00,0x60,0x01,0x90,0x01,0xC8,0x03,0x00,0x01, // 190 + 0x00,0x00,0x00,0x06,0x00,0x09,0xA0,0x09,0x00,0x04, // 191 + 0x00,0x02,0xC0,0x01,0xB0,0x00,0x89,0x00,0xB2,0x00,0xC0,0x01,0x00,0x02, // 192 + 0x00,0x02,0xC0,0x01,0xB0,0x00,0x8A,0x00,0xB1,0x00,0xC0,0x01,0x00,0x02, // 193 + 0x00,0x02,0xC0,0x01,0xB2,0x00,0x89,0x00,0xB2,0x00,0xC0,0x01,0x00,0x02, // 194 + 0x00,0x02,0xC2,0x01,0xB1,0x00,0x8A,0x00,0xB1,0x00,0xC0,0x01,0x00,0x02, // 195 + 0x00,0x02,0xC0,0x01,0xB2,0x00,0x88,0x00,0xB2,0x00,0xC0,0x01,0x00,0x02, // 196 + 0x00,0x02,0xC0,0x01,0xBE,0x00,0x8A,0x00,0xBE,0x00,0xC0,0x01,0x00,0x02, // 197 + 0x00,0x03,0xC0,0x00,0xE0,0x00,0x98,0x00,0x88,0x00,0xF8,0x03,0x48,0x02,0x48,0x02,0x48,0x02, // 198 + 0x00,0x00,0xF0,0x01,0x08,0x02,0x08,0x16,0x08,0x1A,0x10,0x01, // 199 + 0x00,0x00,0xF8,0x03,0x49,0x02,0x4A,0x02,0x48,0x02,0x48,0x02, // 200 + 0x00,0x00,0xF8,0x03,0x48,0x02,0x4A,0x02,0x49,0x02,0x48,0x02, // 201 + 0x00,0x00,0xFA,0x03,0x49,0x02,0x4A,0x02,0x48,0x02,0x48,0x02, // 202 + 0x00,0x00,0xF8,0x03,0x4A,0x02,0x48,0x02,0x4A,0x02,0x48,0x02, // 203 + 0x00,0x00,0xF9,0x03,0x02, // 204 + 0x02,0x00,0xF9,0x03, // 205 + 0x01,0x00,0xFA,0x03, // 206 + 0x02,0x00,0xF8,0x03,0x02, // 207 + 0x40,0x00,0xF8,0x03,0x48,0x02,0x48,0x02,0x10,0x01,0xE0, // 208 + 0x00,0x00,0xFA,0x03,0x31,0x00,0x42,0x00,0x81,0x01,0xF8,0x03, // 209 + 0x00,0x00,0xF0,0x01,0x08,0x02,0x09,0x02,0x0A,0x02,0x08,0x02,0xF0,0x01, // 210 + 0x00,0x00,0xF0,0x01,0x08,0x02,0x0A,0x02,0x09,0x02,0x08,0x02,0xF0,0x01, // 211 + 0x00,0x00,0xF0,0x01,0x08,0x02,0x0A,0x02,0x09,0x02,0x0A,0x02,0xF0,0x01, // 212 + 0x00,0x00,0xF0,0x01,0x0A,0x02,0x09,0x02,0x0A,0x02,0x09,0x02,0xF0,0x01, // 213 + 0x00,0x00,0xF0,0x01,0x0A,0x02,0x08,0x02,0x0A,0x02,0x08,0x02,0xF0,0x01, // 214 + 0x10,0x01,0xA0,0x00,0xE0,0x00,0xA0,0x00,0x10,0x01, // 215 + 0x00,0x00,0xF0,0x02,0x08,0x03,0xC8,0x02,0x28,0x02,0x18,0x03,0xE8, // 216 + 0x00,0x00,0xF8,0x01,0x01,0x02,0x02,0x02,0x00,0x02,0xF8,0x01, // 217 + 0x00,0x00,0xF8,0x01,0x02,0x02,0x01,0x02,0x00,0x02,0xF8,0x01, // 218 + 0x00,0x00,0xF8,0x01,0x02,0x02,0x01,0x02,0x02,0x02,0xF8,0x01, // 219 + 0x00,0x00,0xF8,0x01,0x02,0x02,0x00,0x02,0x02,0x02,0xF8,0x01, // 220 + 0x08,0x00,0x10,0x00,0x20,0x00,0xC2,0x03,0x21,0x00,0x10,0x00,0x08, // 221 + 0x00,0x00,0xF8,0x03,0x10,0x01,0x10,0x01,0x10,0x01,0xE0, // 222 + 0x00,0x00,0xF0,0x03,0x08,0x01,0x48,0x02,0xB0,0x02,0x80,0x01, // 223 + 0x00,0x00,0x00,0x03,0xA4,0x02,0xA8,0x02,0xE0,0x03, // 224 + 0x00,0x00,0x00,0x03,0xA8,0x02,0xA4,0x02,0xE0,0x03, // 225 + 0x00,0x00,0x00,0x03,0xA8,0x02,0xA4,0x02,0xE8,0x03, // 226 + 0x00,0x00,0x08,0x03,0xA4,0x02,0xA8,0x02,0xE4,0x03, // 227 + 0x00,0x00,0x00,0x03,0xA8,0x02,0xA0,0x02,0xE8,0x03, // 228 + 0x00,0x00,0x00,0x03,0xAE,0x02,0xAA,0x02,0xEE,0x03, // 229 + 0x00,0x00,0x40,0x03,0xA0,0x02,0xA0,0x02,0xC0,0x01,0xA0,0x02,0xA0,0x02,0xC0,0x02, // 230 + 0x00,0x00,0xC0,0x01,0x20,0x16,0x20,0x1A,0x40,0x01, // 231 + 0x00,0x00,0xC0,0x01,0xA4,0x02,0xA8,0x02,0xC0,0x02, // 232 + 0x00,0x00,0xC0,0x01,0xA8,0x02,0xA4,0x02,0xC0,0x02, // 233 + 0x00,0x00,0xC0,0x01,0xA8,0x02,0xA4,0x02,0xC8,0x02, // 234 + 0x00,0x00,0xC0,0x01,0xA8,0x02,0xA0,0x02,0xC8,0x02, // 235 + 0x00,0x00,0xE4,0x03,0x08, // 236 + 0x08,0x00,0xE4,0x03, // 237 + 0x08,0x00,0xE4,0x03,0x08, // 238 + 0x08,0x00,0xE0,0x03,0x08, // 239 + 0x00,0x00,0xC0,0x01,0x28,0x02,0x38,0x02,0xE0,0x01, // 240 + 0x00,0x00,0xE8,0x03,0x24,0x00,0x28,0x00,0xC4,0x03, // 241 + 0x00,0x00,0xC0,0x01,0x24,0x02,0x28,0x02,0xC0,0x01, // 242 + 0x00,0x00,0xC0,0x01,0x28,0x02,0x24,0x02,0xC0,0x01, // 243 + 0x00,0x00,0xC0,0x01,0x28,0x02,0x24,0x02,0xC8,0x01, // 244 + 0x00,0x00,0xC8,0x01,0x24,0x02,0x28,0x02,0xC4,0x01, // 245 + 0x00,0x00,0xC0,0x01,0x28,0x02,0x20,0x02,0xC8,0x01, // 246 + 0x40,0x00,0x40,0x00,0x50,0x01,0x40,0x00,0x40, // 247 + 0x00,0x00,0xC0,0x02,0xA0,0x03,0x60,0x02,0xA0,0x01, // 248 + 0x00,0x00,0xE0,0x01,0x04,0x02,0x08,0x02,0xE0,0x03, // 249 + 0x00,0x00,0xE0,0x01,0x08,0x02,0x04,0x02,0xE0,0x03, // 250 + 0x00,0x00,0xE8,0x01,0x04,0x02,0x08,0x02,0xE0,0x03, // 251 + 0x00,0x00,0xE0,0x01,0x08,0x02,0x00,0x02,0xE8,0x03, // 252 + 0x20,0x00,0xC0,0x09,0x08,0x06,0xC4,0x01,0x20, // 253 + 0x00,0x00,0xF8,0x0F,0x20,0x02,0x20,0x02,0xC0,0x01, // 254 + 0x20,0x00,0xC8,0x09,0x00,0x06,0xC8,0x01,0x20 // 255 +}; + +const uint8_t ArialMT_Plain_16[] PROGMEM = { + 0x10, // Width: 16 + 0x13, // Height: 19 + 0x20, // First Char: 32 + 0xE0, // Numbers of Chars: 224 + + // Jump Table: + 0xFF, 0xFF, 0x00, 0x04, // 32:65535 + 0x00, 0x00, 0x08, 0x04, // 33:0 + 0x00, 0x08, 0x0D, 0x06, // 34:8 + 0x00, 0x15, 0x1A, 0x09, // 35:21 + 0x00, 0x2F, 0x17, 0x09, // 36:47 + 0x00, 0x46, 0x26, 0x0E, // 37:70 + 0x00, 0x6C, 0x1D, 0x0B, // 38:108 + 0x00, 0x89, 0x04, 0x03, // 39:137 + 0x00, 0x8D, 0x0C, 0x05, // 40:141 + 0x00, 0x99, 0x0B, 0x05, // 41:153 + 0x00, 0xA4, 0x0D, 0x06, // 42:164 + 0x00, 0xB1, 0x17, 0x09, // 43:177 + 0x00, 0xC8, 0x09, 0x04, // 44:200 + 0x00, 0xD1, 0x0B, 0x05, // 45:209 + 0x00, 0xDC, 0x08, 0x04, // 46:220 + 0x00, 0xE4, 0x0A, 0x04, // 47:228 + 0x00, 0xEE, 0x17, 0x09, // 48:238 + 0x01, 0x05, 0x11, 0x09, // 49:261 + 0x01, 0x16, 0x17, 0x09, // 50:278 + 0x01, 0x2D, 0x17, 0x09, // 51:301 + 0x01, 0x44, 0x17, 0x09, // 52:324 + 0x01, 0x5B, 0x17, 0x09, // 53:347 + 0x01, 0x72, 0x17, 0x09, // 54:370 + 0x01, 0x89, 0x16, 0x09, // 55:393 + 0x01, 0x9F, 0x17, 0x09, // 56:415 + 0x01, 0xB6, 0x17, 0x09, // 57:438 + 0x01, 0xCD, 0x05, 0x04, // 58:461 + 0x01, 0xD2, 0x06, 0x04, // 59:466 + 0x01, 0xD8, 0x17, 0x09, // 60:472 + 0x01, 0xEF, 0x17, 0x09, // 61:495 + 0x02, 0x06, 0x17, 0x09, // 62:518 + 0x02, 0x1D, 0x16, 0x09, // 63:541 + 0x02, 0x33, 0x2F, 0x10, // 64:563 + 0x02, 0x62, 0x1D, 0x0B, // 65:610 + 0x02, 0x7F, 0x1D, 0x0B, // 66:639 + 0x02, 0x9C, 0x20, 0x0C, // 67:668 + 0x02, 0xBC, 0x20, 0x0C, // 68:700 + 0x02, 0xDC, 0x1D, 0x0B, // 69:732 + 0x02, 0xF9, 0x19, 0x0A, // 70:761 + 0x03, 0x12, 0x20, 0x0C, // 71:786 + 0x03, 0x32, 0x1D, 0x0C, // 72:818 + 0x03, 0x4F, 0x05, 0x04, // 73:847 + 0x03, 0x54, 0x14, 0x08, // 74:852 + 0x03, 0x68, 0x1D, 0x0B, // 75:872 + 0x03, 0x85, 0x17, 0x09, // 76:901 + 0x03, 0x9C, 0x23, 0x0D, // 77:924 + 0x03, 0xBF, 0x1D, 0x0C, // 78:959 + 0x03, 0xDC, 0x20, 0x0C, // 79:988 + 0x03, 0xFC, 0x1C, 0x0B, // 80:1020 + 0x04, 0x18, 0x20, 0x0C, // 81:1048 + 0x04, 0x38, 0x1D, 0x0C, // 82:1080 + 0x04, 0x55, 0x1D, 0x0B, // 83:1109 + 0x04, 0x72, 0x19, 0x0A, // 84:1138 + 0x04, 0x8B, 0x1D, 0x0C, // 85:1163 + 0x04, 0xA8, 0x1C, 0x0B, // 86:1192 + 0x04, 0xC4, 0x2B, 0x0F, // 87:1220 + 0x04, 0xEF, 0x20, 0x0B, // 88:1263 + 0x05, 0x0F, 0x19, 0x0B, // 89:1295 + 0x05, 0x28, 0x1A, 0x0A, // 90:1320 + 0x05, 0x42, 0x0C, 0x04, // 91:1346 + 0x05, 0x4E, 0x0B, 0x04, // 92:1358 + 0x05, 0x59, 0x09, 0x04, // 93:1369 + 0x05, 0x62, 0x14, 0x08, // 94:1378 + 0x05, 0x76, 0x1B, 0x09, // 95:1398 + 0x05, 0x91, 0x07, 0x05, // 96:1425 + 0x05, 0x98, 0x17, 0x09, // 97:1432 + 0x05, 0xAF, 0x17, 0x09, // 98:1455 + 0x05, 0xC6, 0x14, 0x08, // 99:1478 + 0x05, 0xDA, 0x17, 0x09, // 100:1498 + 0x05, 0xF1, 0x17, 0x09, // 101:1521 + 0x06, 0x08, 0x0A, 0x04, // 102:1544 + 0x06, 0x12, 0x17, 0x09, // 103:1554 + 0x06, 0x29, 0x14, 0x09, // 104:1577 + 0x06, 0x3D, 0x05, 0x04, // 105:1597 + 0x06, 0x42, 0x06, 0x04, // 106:1602 + 0x06, 0x48, 0x17, 0x08, // 107:1608 + 0x06, 0x5F, 0x05, 0x04, // 108:1631 + 0x06, 0x64, 0x23, 0x0D, // 109:1636 + 0x06, 0x87, 0x14, 0x09, // 110:1671 + 0x06, 0x9B, 0x17, 0x09, // 111:1691 + 0x06, 0xB2, 0x17, 0x09, // 112:1714 + 0x06, 0xC9, 0x18, 0x09, // 113:1737 + 0x06, 0xE1, 0x0D, 0x05, // 114:1761 + 0x06, 0xEE, 0x14, 0x08, // 115:1774 + 0x07, 0x02, 0x0B, 0x04, // 116:1794 + 0x07, 0x0D, 0x14, 0x09, // 117:1805 + 0x07, 0x21, 0x13, 0x08, // 118:1825 + 0x07, 0x34, 0x1F, 0x0C, // 119:1844 + 0x07, 0x53, 0x14, 0x08, // 120:1875 + 0x07, 0x67, 0x13, 0x08, // 121:1895 + 0x07, 0x7A, 0x14, 0x08, // 122:1914 + 0x07, 0x8E, 0x0F, 0x05, // 123:1934 + 0x07, 0x9D, 0x06, 0x04, // 124:1949 + 0x07, 0xA3, 0x0E, 0x05, // 125:1955 + 0x07, 0xB1, 0x17, 0x09, // 126:1969 + 0xFF, 0xFF, 0x00, 0x00, // 127:65535 + 0xFF, 0xFF, 0x00, 0x10, // 128:65535 + 0xFF, 0xFF, 0x00, 0x10, // 129:65535 + 0xFF, 0xFF, 0x00, 0x10, // 130:65535 + 0xFF, 0xFF, 0x00, 0x10, // 131:65535 + 0xFF, 0xFF, 0x00, 0x10, // 132:65535 + 0xFF, 0xFF, 0x00, 0x10, // 133:65535 + 0xFF, 0xFF, 0x00, 0x10, // 134:65535 + 0xFF, 0xFF, 0x00, 0x10, // 135:65535 + 0xFF, 0xFF, 0x00, 0x10, // 136:65535 + 0xFF, 0xFF, 0x00, 0x10, // 137:65535 + 0xFF, 0xFF, 0x00, 0x10, // 138:65535 + 0xFF, 0xFF, 0x00, 0x10, // 139:65535 + 0xFF, 0xFF, 0x00, 0x10, // 140:65535 + 0xFF, 0xFF, 0x00, 0x10, // 141:65535 + 0xFF, 0xFF, 0x00, 0x10, // 142:65535 + 0xFF, 0xFF, 0x00, 0x10, // 143:65535 + 0xFF, 0xFF, 0x00, 0x10, // 144:65535 + 0xFF, 0xFF, 0x00, 0x10, // 145:65535 + 0xFF, 0xFF, 0x00, 0x10, // 146:65535 + 0xFF, 0xFF, 0x00, 0x10, // 147:65535 + 0xFF, 0xFF, 0x00, 0x10, // 148:65535 + 0xFF, 0xFF, 0x00, 0x10, // 149:65535 + 0xFF, 0xFF, 0x00, 0x10, // 150:65535 + 0xFF, 0xFF, 0x00, 0x10, // 151:65535 + 0xFF, 0xFF, 0x00, 0x10, // 152:65535 + 0xFF, 0xFF, 0x00, 0x10, // 153:65535 + 0xFF, 0xFF, 0x00, 0x10, // 154:65535 + 0xFF, 0xFF, 0x00, 0x10, // 155:65535 + 0xFF, 0xFF, 0x00, 0x10, // 156:65535 + 0xFF, 0xFF, 0x00, 0x10, // 157:65535 + 0xFF, 0xFF, 0x00, 0x10, // 158:65535 + 0xFF, 0xFF, 0x00, 0x10, // 159:65535 + 0xFF, 0xFF, 0x00, 0x04, // 160:65535 + 0x07, 0xC8, 0x09, 0x05, // 161:1992 + 0x07, 0xD1, 0x17, 0x09, // 162:2001 + 0x07, 0xE8, 0x17, 0x09, // 163:2024 + 0x07, 0xFF, 0x14, 0x09, // 164:2047 + 0x08, 0x13, 0x1A, 0x09, // 165:2067 + 0x08, 0x2D, 0x06, 0x04, // 166:2093 + 0x08, 0x33, 0x17, 0x09, // 167:2099 + 0x08, 0x4A, 0x07, 0x05, // 168:2122 + 0x08, 0x51, 0x23, 0x0C, // 169:2129 + 0x08, 0x74, 0x0E, 0x06, // 170:2164 + 0x08, 0x82, 0x14, 0x09, // 171:2178 + 0x08, 0x96, 0x17, 0x09, // 172:2198 + 0x08, 0xAD, 0x0B, 0x05, // 173:2221 + 0x08, 0xB8, 0x23, 0x0C, // 174:2232 + 0x08, 0xDB, 0x19, 0x09, // 175:2267 + 0x08, 0xF4, 0x0D, 0x06, // 176:2292 + 0x09, 0x01, 0x17, 0x09, // 177:2305 + 0x09, 0x18, 0x0E, 0x05, // 178:2328 + 0x09, 0x26, 0x0D, 0x05, // 179:2342 + 0x09, 0x33, 0x0A, 0x05, // 180:2355 + 0x09, 0x3D, 0x17, 0x09, // 181:2365 + 0x09, 0x54, 0x19, 0x09, // 182:2388 + 0x09, 0x6D, 0x08, 0x05, // 183:2413 + 0x09, 0x75, 0x0C, 0x05, // 184:2421 + 0x09, 0x81, 0x0B, 0x05, // 185:2433 + 0x09, 0x8C, 0x0D, 0x06, // 186:2444 + 0x09, 0x99, 0x17, 0x09, // 187:2457 + 0x09, 0xB0, 0x26, 0x0D, // 188:2480 + 0x09, 0xD6, 0x26, 0x0D, // 189:2518 + 0x09, 0xFC, 0x26, 0x0D, // 190:2556 + 0x0A, 0x22, 0x1A, 0x0A, // 191:2594 + 0x0A, 0x3C, 0x1D, 0x0B, // 192:2620 + 0x0A, 0x59, 0x1D, 0x0B, // 193:2649 + 0x0A, 0x76, 0x1D, 0x0B, // 194:2678 + 0x0A, 0x93, 0x1D, 0x0B, // 195:2707 + 0x0A, 0xB0, 0x1D, 0x0B, // 196:2736 + 0x0A, 0xCD, 0x1D, 0x0B, // 197:2765 + 0x0A, 0xEA, 0x2C, 0x10, // 198:2794 + 0x0B, 0x16, 0x20, 0x0C, // 199:2838 + 0x0B, 0x36, 0x1D, 0x0B, // 200:2870 + 0x0B, 0x53, 0x1D, 0x0B, // 201:2899 + 0x0B, 0x70, 0x1D, 0x0B, // 202:2928 + 0x0B, 0x8D, 0x1D, 0x0B, // 203:2957 + 0x0B, 0xAA, 0x05, 0x04, // 204:2986 + 0x0B, 0xAF, 0x07, 0x04, // 205:2991 + 0x0B, 0xB6, 0x0A, 0x04, // 206:2998 + 0x0B, 0xC0, 0x07, 0x04, // 207:3008 + 0x0B, 0xC7, 0x20, 0x0C, // 208:3015 + 0x0B, 0xE7, 0x1D, 0x0C, // 209:3047 + 0x0C, 0x04, 0x20, 0x0C, // 210:3076 + 0x0C, 0x24, 0x20, 0x0C, // 211:3108 + 0x0C, 0x44, 0x20, 0x0C, // 212:3140 + 0x0C, 0x64, 0x20, 0x0C, // 213:3172 + 0x0C, 0x84, 0x20, 0x0C, // 214:3204 + 0x0C, 0xA4, 0x17, 0x09, // 215:3236 + 0x0C, 0xBB, 0x20, 0x0C, // 216:3259 + 0x0C, 0xDB, 0x1D, 0x0C, // 217:3291 + 0x0C, 0xF8, 0x1D, 0x0C, // 218:3320 + 0x0D, 0x15, 0x1D, 0x0C, // 219:3349 + 0x0D, 0x32, 0x1D, 0x0C, // 220:3378 + 0x0D, 0x4F, 0x19, 0x0B, // 221:3407 + 0x0D, 0x68, 0x1D, 0x0B, // 222:3432 + 0x0D, 0x85, 0x17, 0x0A, // 223:3461 + 0x0D, 0x9C, 0x17, 0x09, // 224:3484 + 0x0D, 0xB3, 0x17, 0x09, // 225:3507 + 0x0D, 0xCA, 0x17, 0x09, // 226:3530 + 0x0D, 0xE1, 0x17, 0x09, // 227:3553 + 0x0D, 0xF8, 0x17, 0x09, // 228:3576 + 0x0E, 0x0F, 0x17, 0x09, // 229:3599 + 0x0E, 0x26, 0x29, 0x0E, // 230:3622 + 0x0E, 0x4F, 0x14, 0x08, // 231:3663 + 0x0E, 0x63, 0x17, 0x09, // 232:3683 + 0x0E, 0x7A, 0x17, 0x09, // 233:3706 + 0x0E, 0x91, 0x17, 0x09, // 234:3729 + 0x0E, 0xA8, 0x17, 0x09, // 235:3752 + 0x0E, 0xBF, 0x05, 0x04, // 236:3775 + 0x0E, 0xC4, 0x07, 0x04, // 237:3780 + 0x0E, 0xCB, 0x0A, 0x04, // 238:3787 + 0x0E, 0xD5, 0x07, 0x04, // 239:3797 + 0x0E, 0xDC, 0x17, 0x09, // 240:3804 + 0x0E, 0xF3, 0x14, 0x09, // 241:3827 + 0x0F, 0x07, 0x17, 0x09, // 242:3847 + 0x0F, 0x1E, 0x17, 0x09, // 243:3870 + 0x0F, 0x35, 0x17, 0x09, // 244:3893 + 0x0F, 0x4C, 0x17, 0x09, // 245:3916 + 0x0F, 0x63, 0x17, 0x09, // 246:3939 + 0x0F, 0x7A, 0x17, 0x09, // 247:3962 + 0x0F, 0x91, 0x17, 0x0A, // 248:3985 + 0x0F, 0xA8, 0x14, 0x09, // 249:4008 + 0x0F, 0xBC, 0x14, 0x09, // 250:4028 + 0x0F, 0xD0, 0x14, 0x09, // 251:4048 + 0x0F, 0xE4, 0x14, 0x09, // 252:4068 + 0x0F, 0xF8, 0x13, 0x08, // 253:4088 + 0x10, 0x0B, 0x17, 0x09, // 254:4107 + 0x10, 0x22, 0x13, 0x08, // 255:4130 + + // Font Data: + 0x00,0x00,0x00,0x00,0x00,0x00,0xF8,0x5F, // 33 + 0x00,0x00,0x00,0x78,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x78, // 34 + 0x80,0x08,0x00,0x80,0x78,0x00,0xC0,0x0F,0x00,0xB8,0x08,0x00,0x80,0x08,0x00,0x80,0x78,0x00,0xC0,0x0F,0x00,0xB8,0x08,0x00,0x80,0x08, // 35 + 0x00,0x00,0x00,0xE0,0x10,0x00,0x10,0x21,0x00,0x08,0x41,0x00,0xFC,0xFF,0x00,0x08,0x42,0x00,0x10,0x22,0x00,0x20,0x1C, // 36 + 0x00,0x00,0x00,0xF0,0x00,0x00,0x08,0x01,0x00,0x08,0x01,0x00,0x08,0x61,0x00,0xF0,0x18,0x00,0x00,0x06,0x00,0xC0,0x01,0x00,0x30,0x3C,0x00,0x08,0x42,0x00,0x00,0x42,0x00,0x00,0x42,0x00,0x00,0x3C, // 37 + 0x00,0x00,0x00,0x00,0x1C,0x00,0x70,0x22,0x00,0x88,0x41,0x00,0x08,0x43,0x00,0x88,0x44,0x00,0x70,0x28,0x00,0x00,0x10,0x00,0x00,0x28,0x00,0x00,0x44, // 38 + 0x00,0x00,0x00,0x78, // 39 + 0x00,0x00,0x00,0x80,0x3F,0x00,0x70,0xC0,0x01,0x08,0x00,0x02, // 40 + 0x00,0x00,0x00,0x08,0x00,0x02,0x70,0xC0,0x01,0x80,0x3F, // 41 + 0x10,0x00,0x00,0xD0,0x00,0x00,0x38,0x00,0x00,0xD0,0x00,0x00,0x10, // 42 + 0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0xC0,0x1F,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02, // 43 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x01, // 44 + 0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08, // 45 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40, // 46 + 0x00,0x60,0x00,0x00,0x1E,0x00,0xE0,0x01,0x00,0x18, // 47 + 0x00,0x00,0x00,0xE0,0x1F,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0xE0,0x1F, // 48 + 0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0x00,0x10,0x00,0x00,0xF8,0x7F, // 49 + 0x00,0x00,0x00,0x20,0x40,0x00,0x10,0x60,0x00,0x08,0x50,0x00,0x08,0x48,0x00,0x08,0x44,0x00,0x10,0x43,0x00,0xE0,0x40, // 50 + 0x00,0x00,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x88,0x41,0x00,0xF0,0x22,0x00,0x00,0x1C, // 51 + 0x00,0x0C,0x00,0x00,0x0A,0x00,0x00,0x09,0x00,0xC0,0x08,0x00,0x20,0x08,0x00,0x10,0x08,0x00,0xF8,0x7F,0x00,0x00,0x08, // 52 + 0x00,0x00,0x00,0xC0,0x11,0x00,0xB8,0x20,0x00,0x88,0x40,0x00,0x88,0x40,0x00,0x88,0x40,0x00,0x08,0x21,0x00,0x08,0x1E, // 53 + 0x00,0x00,0x00,0xE0,0x1F,0x00,0x10,0x21,0x00,0x88,0x40,0x00,0x88,0x40,0x00,0x88,0x40,0x00,0x10,0x21,0x00,0x20,0x1E, // 54 + 0x00,0x00,0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08,0x78,0x00,0x08,0x07,0x00,0xC8,0x00,0x00,0x28,0x00,0x00,0x18, // 55 + 0x00,0x00,0x00,0x60,0x1C,0x00,0x90,0x22,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x90,0x22,0x00,0x60,0x1C, // 56 + 0x00,0x00,0x00,0xE0,0x11,0x00,0x10,0x22,0x00,0x08,0x44,0x00,0x08,0x44,0x00,0x08,0x44,0x00,0x10,0x22,0x00,0xE0,0x1F, // 57 + 0x00,0x00,0x00,0x40,0x40, // 58 + 0x00,0x00,0x00,0x40,0xC0,0x01, // 59 + 0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x05,0x00,0x00,0x05,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x40,0x10, // 60 + 0x00,0x00,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x80,0x08, // 61 + 0x00,0x00,0x00,0x40,0x10,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x00,0x05,0x00,0x00,0x05,0x00,0x00,0x02, // 62 + 0x00,0x00,0x00,0x60,0x00,0x00,0x10,0x00,0x00,0x08,0x00,0x00,0x08,0x5C,0x00,0x08,0x02,0x00,0x10,0x01,0x00,0xE0, // 63 + 0x00,0x00,0x00,0x00,0x3F,0x00,0xC0,0x40,0x00,0x20,0x80,0x00,0x10,0x1E,0x01,0x10,0x21,0x01,0x88,0x40,0x02,0x48,0x40,0x02,0x48,0x40,0x02,0x48,0x20,0x02,0x88,0x7C,0x02,0xC8,0x43,0x02,0x10,0x40,0x02,0x10,0x20,0x01,0x60,0x10,0x01,0x80,0x8F, // 64 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x07,0x00,0x70,0x04,0x00,0x08,0x04,0x00,0x70,0x04,0x00,0x80,0x07,0x00,0x00,0x1C,0x00,0x00,0x60, // 65 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x90,0x22,0x00,0x60,0x1C, // 66 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0x20,0x10, // 67 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 68 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x40, // 69 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08, // 70 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x42,0x00,0x08,0x42,0x00,0x10,0x22,0x00,0x20,0x12,0x00,0x00,0x0E, // 71 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0xF8,0x7F, // 72 + 0x00,0x00,0x00,0xF8,0x7F, // 73 + 0x00,0x00,0x00,0x00,0x38,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0xF8,0x3F, // 74 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x00,0x04,0x00,0x00,0x02,0x00,0x00,0x01,0x00,0x80,0x03,0x00,0x40,0x04,0x00,0x20,0x18,0x00,0x10,0x20,0x00,0x08,0x40, // 75 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40, // 76 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x30,0x00,0x00,0xC0,0x00,0x00,0x00,0x03,0x00,0x00,0x1C,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x00,0x03,0x00,0xC0,0x00,0x00,0x30,0x00,0x00,0xF8,0x7F, // 77 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x10,0x00,0x00,0x60,0x00,0x00,0x80,0x00,0x00,0x00,0x03,0x00,0x00,0x04,0x00,0x00,0x18,0x00,0x00,0x20,0x00,0xF8,0x7F, // 78 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 79 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x10,0x01,0x00,0xE0, // 80 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x50,0x00,0x08,0x50,0x00,0x10,0x20,0x00,0x20,0x70,0x00,0xC0,0x4F, // 81 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x06,0x00,0x08,0x1A,0x00,0x10,0x21,0x00,0xE0,0x40, // 82 + 0x00,0x00,0x00,0x60,0x10,0x00,0x90,0x20,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x42,0x00,0x08,0x42,0x00,0x10,0x22,0x00,0x20,0x1C, // 83 + 0x08,0x00,0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08,0x00,0x00,0xF8,0x7F,0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08, // 84 + 0x00,0x00,0x00,0xF8,0x1F,0x00,0x00,0x20,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0xF8,0x1F, // 85 + 0x00,0x00,0x00,0x18,0x00,0x00,0xE0,0x00,0x00,0x00,0x07,0x00,0x00,0x18,0x00,0x00,0x60,0x00,0x00,0x18,0x00,0x00,0x07,0x00,0xE0,0x00,0x00,0x18, // 86 + 0x18,0x00,0x00,0xE0,0x01,0x00,0x00,0x1E,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x03,0x00,0x70,0x00,0x00,0x08,0x00,0x00,0x70,0x00,0x00,0x80,0x03,0x00,0x00,0x1C,0x00,0x00,0x60,0x00,0x00,0x1E,0x00,0xE0,0x01,0x00,0x18, // 87 + 0x00,0x40,0x00,0x08,0x20,0x00,0x10,0x10,0x00,0x60,0x0C,0x00,0x80,0x02,0x00,0x00,0x01,0x00,0x80,0x02,0x00,0x60,0x0C,0x00,0x10,0x10,0x00,0x08,0x20,0x00,0x00,0x40, // 88 + 0x08,0x00,0x00,0x30,0x00,0x00,0x40,0x00,0x00,0x80,0x01,0x00,0x00,0x7E,0x00,0x80,0x01,0x00,0x40,0x00,0x00,0x30,0x00,0x00,0x08, // 89 + 0x00,0x40,0x00,0x08,0x60,0x00,0x08,0x58,0x00,0x08,0x44,0x00,0x08,0x43,0x00,0x88,0x40,0x00,0x68,0x40,0x00,0x18,0x40,0x00,0x08,0x40, // 90 + 0x00,0x00,0x00,0xF8,0xFF,0x03,0x08,0x00,0x02,0x08,0x00,0x02, // 91 + 0x18,0x00,0x00,0xE0,0x01,0x00,0x00,0x1E,0x00,0x00,0x60, // 92 + 0x08,0x00,0x02,0x08,0x00,0x02,0xF8,0xFF,0x03, // 93 + 0x00,0x01,0x00,0xC0,0x00,0x00,0x30,0x00,0x00,0x08,0x00,0x00,0x30,0x00,0x00,0xC0,0x00,0x00,0x00,0x01, // 94 + 0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02, // 95 + 0x00,0x00,0x00,0x08,0x00,0x00,0x10, // 96 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x40,0x42,0x00,0x40,0x22,0x00,0x80,0x7F, // 97 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x80,0x20,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x80,0x20,0x00,0x00,0x1F, // 98 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x80,0x20, // 99 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x80,0x20,0x00,0xF8,0x7F, // 100 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x24,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x80,0x24,0x00,0x00,0x17, // 101 + 0x40,0x00,0x00,0xF0,0x7F,0x00,0x48,0x00,0x00,0x48, // 102 + 0x00,0x00,0x00,0x00,0x1F,0x01,0x80,0x20,0x02,0x40,0x40,0x02,0x40,0x40,0x02,0x40,0x40,0x02,0x80,0x20,0x01,0xC0,0xFF, // 103 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x80,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x80,0x7F, // 104 + 0x00,0x00,0x00,0xC8,0x7F, // 105 + 0x00,0x00,0x02,0xC8,0xFF,0x01, // 106 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x00,0x08,0x00,0x00,0x04,0x00,0x00,0x06,0x00,0x00,0x19,0x00,0x80,0x20,0x00,0x40,0x40, // 107 + 0x00,0x00,0x00,0xF8,0x7F, // 108 + 0x00,0x00,0x00,0xC0,0x7F,0x00,0x80,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x80,0x7F,0x00,0x80,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x80,0x7F, // 109 + 0x00,0x00,0x00,0xC0,0x7F,0x00,0x80,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x80,0x7F, // 110 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x80,0x20,0x00,0x00,0x1F, // 111 + 0x00,0x00,0x00,0xC0,0xFF,0x03,0x80,0x20,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x80,0x20,0x00,0x00,0x1F, // 112 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x80,0x20,0x00,0xC0,0xFF,0x03, // 113 + 0x00,0x00,0x00,0xC0,0x7F,0x00,0x80,0x00,0x00,0x40,0x00,0x00,0x40, // 114 + 0x00,0x00,0x00,0x80,0x23,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x80,0x38, // 115 + 0x40,0x00,0x00,0xF0,0x7F,0x00,0x40,0x40,0x00,0x40,0x40, // 116 + 0x00,0x00,0x00,0xC0,0x3F,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0xC0,0x7F, // 117 + 0xC0,0x00,0x00,0x00,0x03,0x00,0x00,0x1C,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x00,0x03,0x00,0xC0, // 118 + 0xC0,0x00,0x00,0x00,0x1F,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x00,0x03,0x00,0xC0,0x00,0x00,0x00,0x03,0x00,0x00,0x1C,0x00,0x00,0x60,0x00,0x00,0x1F,0x00,0xC0, // 119 + 0x40,0x40,0x00,0x80,0x20,0x00,0x00,0x1B,0x00,0x00,0x04,0x00,0x00,0x1B,0x00,0x80,0x20,0x00,0x40,0x40, // 120 + 0xC0,0x01,0x00,0x00,0x06,0x02,0x00,0x38,0x02,0x00,0xE0,0x01,0x00,0x38,0x00,0x00,0x07,0x00,0xC0, // 121 + 0x40,0x40,0x00,0x40,0x60,0x00,0x40,0x58,0x00,0x40,0x44,0x00,0x40,0x43,0x00,0xC0,0x40,0x00,0x40,0x40, // 122 + 0x00,0x04,0x00,0x00,0x04,0x00,0xF0,0xFB,0x01,0x08,0x00,0x02,0x08,0x00,0x02, // 123 + 0x00,0x00,0x00,0xF8,0xFF,0x03, // 124 + 0x08,0x00,0x02,0x08,0x00,0x02,0xF0,0xFB,0x01,0x00,0x04,0x00,0x00,0x04, // 125 + 0x00,0x02,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x01, // 126 + 0x00,0x00,0x00,0x00,0x00,0x00,0x40,0xFF,0x03, // 161 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x03,0x40,0xF0,0x00,0x40,0x4E,0x00,0xC0,0x41,0x00,0xB8,0x20,0x00,0x00,0x11, // 162 + 0x00,0x41,0x00,0xE0,0x31,0x00,0x10,0x2F,0x00,0x08,0x21,0x00,0x08,0x21,0x00,0x08,0x40,0x00,0x10,0x40,0x00,0x20,0x20, // 163 + 0x00,0x00,0x00,0x40,0x0B,0x00,0x80,0x04,0x00,0x40,0x08,0x00,0x40,0x08,0x00,0x80,0x04,0x00,0x40,0x0B, // 164 + 0x08,0x0A,0x00,0x10,0x0A,0x00,0x60,0x0A,0x00,0x80,0x0B,0x00,0x00,0x7E,0x00,0x80,0x0B,0x00,0x60,0x0A,0x00,0x10,0x0A,0x00,0x08,0x0A, // 165 + 0x00,0x00,0x00,0xF8,0xF1,0x03, // 166 + 0x00,0x86,0x00,0x70,0x09,0x01,0xC8,0x10,0x02,0x88,0x10,0x02,0x08,0x21,0x02,0x08,0x61,0x02,0x30,0xD2,0x01,0x00,0x0C, // 167 + 0x08,0x00,0x00,0x00,0x00,0x00,0x08, // 168 + 0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0xC8,0x47,0x00,0x28,0x48,0x00,0x28,0x48,0x00,0x28,0x48,0x00,0x28,0x48,0x00,0x48,0x44,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 169 + 0xD0,0x00,0x00,0x48,0x01,0x00,0x28,0x01,0x00,0x28,0x01,0x00,0xF0,0x01, // 170 + 0x00,0x00,0x00,0x00,0x04,0x00,0x00,0x1B,0x00,0x80,0x20,0x00,0x00,0x04,0x00,0x00,0x1B,0x00,0x80,0x20, // 171 + 0x00,0x00,0x00,0x80,0x00,0x00,0x80,0x00,0x00,0x80,0x00,0x00,0x80,0x00,0x00,0x80,0x00,0x00,0x80,0x00,0x00,0x80,0x0F, // 172 + 0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08, // 173 + 0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0xE8,0x4F,0x00,0x28,0x41,0x00,0x28,0x41,0x00,0x28,0x43,0x00,0x28,0x45,0x00,0xC8,0x48,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 174 + 0x04,0x00,0x00,0x04,0x00,0x00,0x04,0x00,0x00,0x04,0x00,0x00,0x04,0x00,0x00,0x04,0x00,0x00,0x04,0x00,0x00,0x04,0x00,0x00,0x04, // 175 + 0x00,0x00,0x00,0x30,0x00,0x00,0x48,0x00,0x00,0x48,0x00,0x00,0x30, // 176 + 0x00,0x00,0x00,0x00,0x41,0x00,0x00,0x41,0x00,0x00,0x41,0x00,0xE0,0x4F,0x00,0x00,0x41,0x00,0x00,0x41,0x00,0x00,0x41, // 177 + 0x10,0x01,0x00,0x88,0x01,0x00,0x48,0x01,0x00,0x48,0x01,0x00,0x30,0x01, // 178 + 0x90,0x00,0x00,0x08,0x01,0x00,0x08,0x01,0x00,0x28,0x01,0x00,0xD8, // 179 + 0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x00,0x00,0x08, // 180 + 0x00,0x00,0x00,0xC0,0xFF,0x03,0x00,0x20,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0xC0,0x7F, // 181 + 0xF0,0x00,0x00,0xF8,0x00,0x00,0xF8,0x01,0x00,0xF8,0x01,0x00,0xF8,0xFF,0x03,0x08,0x00,0x00,0x08,0x00,0x00,0xF8,0xFF,0x03,0x08, // 182 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02, // 183 + 0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x80,0x02,0x00,0x00,0x03, // 184 + 0x00,0x00,0x00,0x10,0x00,0x00,0x08,0x00,0x00,0xF8,0x01, // 185 + 0xF0,0x00,0x00,0x08,0x01,0x00,0x08,0x01,0x00,0x08,0x01,0x00,0xF0, // 186 + 0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x20,0x00,0x00,0x1B,0x00,0x00,0x04,0x00,0x80,0x20,0x00,0x00,0x1B,0x00,0x00,0x04, // 187 + 0x00,0x00,0x00,0x10,0x00,0x00,0x08,0x40,0x00,0xF8,0x21,0x00,0x00,0x10,0x00,0x00,0x0C,0x00,0x00,0x02,0x00,0x80,0x01,0x00,0x40,0x30,0x00,0x30,0x28,0x00,0x08,0x24,0x00,0x00,0x7E,0x00,0x00,0x20, // 188 + 0x00,0x00,0x00,0x10,0x00,0x00,0x08,0x40,0x00,0xF8,0x31,0x00,0x00,0x08,0x00,0x00,0x04,0x00,0x00,0x03,0x00,0x80,0x00,0x00,0x60,0x44,0x00,0x10,0x62,0x00,0x08,0x52,0x00,0x00,0x52,0x00,0x00,0x4C, // 189 + 0x90,0x00,0x00,0x08,0x01,0x00,0x08,0x41,0x00,0x28,0x21,0x00,0xD8,0x18,0x00,0x00,0x04,0x00,0x00,0x03,0x00,0x80,0x00,0x00,0x40,0x30,0x00,0x30,0x28,0x00,0x08,0x24,0x00,0x00,0x7E,0x00,0x00,0x20, // 190 + 0x00,0x00,0x00,0x00,0xE0,0x00,0x00,0x10,0x01,0x00,0x08,0x02,0x40,0x07,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x01,0x00,0xC0, // 191 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x07,0x00,0x71,0x04,0x00,0x0A,0x04,0x00,0x70,0x04,0x00,0x80,0x07,0x00,0x00,0x1C,0x00,0x00,0x60, // 192 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x07,0x00,0x70,0x04,0x00,0x0A,0x04,0x00,0x71,0x04,0x00,0x80,0x07,0x00,0x00,0x1C,0x00,0x00,0x60, // 193 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x07,0x00,0x72,0x04,0x00,0x09,0x04,0x00,0x71,0x04,0x00,0x82,0x07,0x00,0x00,0x1C,0x00,0x00,0x60, // 194 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x07,0x00,0x72,0x04,0x00,0x09,0x04,0x00,0x72,0x04,0x00,0x81,0x07,0x00,0x00,0x1C,0x00,0x00,0x60, // 195 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x07,0x00,0x72,0x04,0x00,0x08,0x04,0x00,0x72,0x04,0x00,0x80,0x07,0x00,0x00,0x1C,0x00,0x00,0x60, // 196 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x07,0x00,0x7E,0x04,0x00,0x0A,0x04,0x00,0x7E,0x04,0x00,0x80,0x07,0x00,0x00,0x1C,0x00,0x00,0x60, // 197 + 0x00,0x60,0x00,0x00,0x18,0x00,0x00,0x06,0x00,0x80,0x05,0x00,0x60,0x04,0x00,0x18,0x04,0x00,0x08,0x04,0x00,0x08,0x04,0x00,0xF8,0x7F,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41, // 198 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x08,0x40,0x02,0x08,0xC0,0x02,0x08,0x40,0x03,0x08,0x40,0x00,0x10,0x20,0x00,0x20,0x10, // 199 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x09,0x41,0x00,0x0A,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x40, // 200 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x0A,0x41,0x00,0x09,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x40, // 201 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x0A,0x41,0x00,0x09,0x41,0x00,0x09,0x41,0x00,0x0A,0x41,0x00,0x08,0x41,0x00,0x08,0x40, // 202 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x0A,0x41,0x00,0x08,0x41,0x00,0x0A,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x40, // 203 + 0x01,0x00,0x00,0xFA,0x7F, // 204 + 0x00,0x00,0x00,0xFA,0x7F,0x00,0x01, // 205 + 0x02,0x00,0x00,0xF9,0x7F,0x00,0x01,0x00,0x00,0x02, // 206 + 0x02,0x00,0x00,0xF8,0x7F,0x00,0x02, // 207 + 0x00,0x02,0x00,0xF8,0x7F,0x00,0x08,0x42,0x00,0x08,0x42,0x00,0x08,0x42,0x00,0x08,0x42,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 208 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x10,0x00,0x00,0x60,0x00,0x00,0x82,0x00,0x00,0x01,0x03,0x00,0x02,0x04,0x00,0x01,0x18,0x00,0x00,0x20,0x00,0xF8,0x7F, // 209 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x09,0x40,0x00,0x0A,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 210 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x0A,0x40,0x00,0x09,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 211 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x0A,0x40,0x00,0x09,0x40,0x00,0x09,0x40,0x00,0x0A,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 212 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x0A,0x40,0x00,0x09,0x40,0x00,0x0A,0x40,0x00,0x09,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 213 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x0A,0x40,0x00,0x08,0x40,0x00,0x0A,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 214 + 0x00,0x00,0x00,0x40,0x10,0x00,0x80,0x08,0x00,0x00,0x05,0x00,0x00,0x07,0x00,0x00,0x05,0x00,0x80,0x08,0x00,0x40,0x10, // 215 + 0x00,0x00,0x00,0xC0,0x4F,0x00,0x20,0x30,0x00,0x10,0x30,0x00,0x08,0x4C,0x00,0x08,0x42,0x00,0x08,0x41,0x00,0xC8,0x40,0x00,0x30,0x20,0x00,0x30,0x10,0x00,0xC8,0x0F, // 216 + 0x00,0x00,0x00,0xF8,0x1F,0x00,0x00,0x20,0x00,0x00,0x40,0x00,0x01,0x40,0x00,0x02,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0xF8,0x1F, // 217 + 0x00,0x00,0x00,0xF8,0x1F,0x00,0x00,0x20,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x02,0x40,0x00,0x01,0x40,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0xF8,0x1F, // 218 + 0x00,0x00,0x00,0xF8,0x1F,0x00,0x00,0x20,0x00,0x00,0x40,0x00,0x02,0x40,0x00,0x01,0x40,0x00,0x01,0x40,0x00,0x02,0x40,0x00,0x00,0x20,0x00,0xF8,0x1F, // 219 + 0x00,0x00,0x00,0xF8,0x1F,0x00,0x00,0x20,0x00,0x00,0x40,0x00,0x02,0x40,0x00,0x00,0x40,0x00,0x02,0x40,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0xF8,0x1F, // 220 + 0x08,0x00,0x00,0x30,0x00,0x00,0x40,0x00,0x00,0x80,0x01,0x00,0x02,0x7E,0x00,0x81,0x01,0x00,0x40,0x00,0x00,0x30,0x00,0x00,0x08, // 221 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x20,0x10,0x00,0x20,0x10,0x00,0x20,0x10,0x00,0x20,0x10,0x00,0x20,0x10,0x00,0x20,0x10,0x00,0x40,0x08,0x00,0x80,0x07, // 222 + 0x00,0x00,0x00,0xE0,0x7F,0x00,0x10,0x00,0x00,0x08,0x20,0x00,0x88,0x43,0x00,0x70,0x42,0x00,0x00,0x44,0x00,0x00,0x38, // 223 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x40,0x44,0x00,0x48,0x44,0x00,0x50,0x42,0x00,0x40,0x22,0x00,0x80,0x7F, // 224 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x40,0x44,0x00,0x50,0x44,0x00,0x48,0x42,0x00,0x40,0x22,0x00,0x80,0x7F, // 225 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x50,0x44,0x00,0x48,0x44,0x00,0x48,0x42,0x00,0x50,0x22,0x00,0x80,0x7F, // 226 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x50,0x44,0x00,0x48,0x44,0x00,0x50,0x42,0x00,0x48,0x22,0x00,0x80,0x7F, // 227 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x50,0x44,0x00,0x40,0x44,0x00,0x50,0x42,0x00,0x40,0x22,0x00,0x80,0x7F, // 228 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x5C,0x44,0x00,0x54,0x44,0x00,0x5C,0x42,0x00,0x40,0x22,0x00,0x80,0x7F, // 229 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x40,0x42,0x00,0x40,0x22,0x00,0x80,0x3F,0x00,0x80,0x24,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x80,0x24,0x00,0x00,0x17, // 230 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x40,0x40,0x02,0x40,0xC0,0x02,0x40,0x40,0x03,0x80,0x20, // 231 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x24,0x00,0x48,0x44,0x00,0x50,0x44,0x00,0x40,0x44,0x00,0x80,0x24,0x00,0x00,0x17, // 232 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x24,0x00,0x40,0x44,0x00,0x50,0x44,0x00,0x48,0x44,0x00,0x80,0x24,0x00,0x00,0x17, // 233 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x24,0x00,0x50,0x44,0x00,0x48,0x44,0x00,0x48,0x44,0x00,0x90,0x24,0x00,0x00,0x17, // 234 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x24,0x00,0x50,0x44,0x00,0x40,0x44,0x00,0x50,0x44,0x00,0x80,0x24,0x00,0x00,0x17, // 235 + 0x08,0x00,0x00,0xD0,0x7F, // 236 + 0x00,0x00,0x00,0xD0,0x7F,0x00,0x08, // 237 + 0x10,0x00,0x00,0xC8,0x7F,0x00,0x08,0x00,0x00,0x10, // 238 + 0x10,0x00,0x00,0xC0,0x7F,0x00,0x10, // 239 + 0x00,0x00,0x00,0x00,0x1F,0x00,0xA0,0x20,0x00,0x68,0x40,0x00,0x58,0x40,0x00,0x70,0x40,0x00,0xE8,0x20,0x00,0x00,0x1F, // 240 + 0x00,0x00,0x00,0xC0,0x7F,0x00,0x90,0x00,0x00,0x48,0x00,0x00,0x50,0x00,0x00,0x48,0x00,0x00,0x80,0x7F, // 241 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x48,0x40,0x00,0x50,0x40,0x00,0x40,0x40,0x00,0x80,0x20,0x00,0x00,0x1F, // 242 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x40,0x40,0x00,0x50,0x40,0x00,0x48,0x40,0x00,0x80,0x20,0x00,0x00,0x1F, // 243 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x50,0x40,0x00,0x48,0x40,0x00,0x48,0x40,0x00,0x90,0x20,0x00,0x00,0x1F, // 244 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x50,0x40,0x00,0x48,0x40,0x00,0x50,0x40,0x00,0x88,0x20,0x00,0x00,0x1F, // 245 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x50,0x40,0x00,0x40,0x40,0x00,0x50,0x40,0x00,0x80,0x20,0x00,0x00,0x1F, // 246 + 0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x80,0x0A,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02, // 247 + 0x00,0x00,0x00,0x00,0x5F,0x00,0x80,0x30,0x00,0x40,0x48,0x00,0x40,0x44,0x00,0x40,0x42,0x00,0x80,0x21,0x00,0x40,0x1F, // 248 + 0x00,0x00,0x00,0xC0,0x3F,0x00,0x00,0x40,0x00,0x08,0x40,0x00,0x10,0x40,0x00,0x00,0x20,0x00,0xC0,0x7F, // 249 + 0x00,0x00,0x00,0xC0,0x3F,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x10,0x40,0x00,0x08,0x20,0x00,0xC0,0x7F, // 250 + 0x00,0x00,0x00,0xC0,0x3F,0x00,0x10,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0xC0,0x7F, // 251 + 0x00,0x00,0x00,0xD0,0x3F,0x00,0x00,0x40,0x00,0x10,0x40,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0xC0,0x7F, // 252 + 0xC0,0x01,0x00,0x00,0x06,0x02,0x00,0x38,0x02,0x10,0xE0,0x01,0x08,0x38,0x00,0x00,0x07,0x00,0xC0, // 253 + 0x00,0x00,0x00,0xF8,0xFF,0x03,0x80,0x20,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x80,0x20,0x00,0x00,0x1F, // 254 + 0xC0,0x01,0x00,0x00,0x06,0x02,0x10,0x38,0x02,0x00,0xE0,0x01,0x10,0x38,0x00,0x00,0x07,0x00,0xC0 // 255 +}; +const uint8_t ArialMT_Plain_24[] PROGMEM = { + 0x18, // Width: 24 + 0x1C, // Height: 28 + 0x20, // First Char: 32 + 0xE0, // Numbers of Chars: 224 + + // Jump Table: + 0xFF, 0xFF, 0x00, 0x07, // 32:65535 + 0x00, 0x00, 0x13, 0x07, // 33:0 + 0x00, 0x13, 0x1A, 0x09, // 34:19 + 0x00, 0x2D, 0x33, 0x0D, // 35:45 + 0x00, 0x60, 0x2F, 0x0D, // 36:96 + 0x00, 0x8F, 0x4F, 0x15, // 37:143 + 0x00, 0xDE, 0x3B, 0x10, // 38:222 + 0x01, 0x19, 0x0A, 0x05, // 39:281 + 0x01, 0x23, 0x1C, 0x08, // 40:291 + 0x01, 0x3F, 0x1B, 0x08, // 41:319 + 0x01, 0x5A, 0x21, 0x09, // 42:346 + 0x01, 0x7B, 0x32, 0x0E, // 43:379 + 0x01, 0xAD, 0x10, 0x07, // 44:429 + 0x01, 0xBD, 0x1B, 0x08, // 45:445 + 0x01, 0xD8, 0x0F, 0x07, // 46:472 + 0x01, 0xE7, 0x19, 0x07, // 47:487 + 0x02, 0x00, 0x2F, 0x0D, // 48:512 + 0x02, 0x2F, 0x23, 0x0D, // 49:559 + 0x02, 0x52, 0x2F, 0x0D, // 50:594 + 0x02, 0x81, 0x2F, 0x0D, // 51:641 + 0x02, 0xB0, 0x2F, 0x0D, // 52:688 + 0x02, 0xDF, 0x2F, 0x0D, // 53:735 + 0x03, 0x0E, 0x2F, 0x0D, // 54:782 + 0x03, 0x3D, 0x2D, 0x0D, // 55:829 + 0x03, 0x6A, 0x2F, 0x0D, // 56:874 + 0x03, 0x99, 0x2F, 0x0D, // 57:921 + 0x03, 0xC8, 0x0F, 0x07, // 58:968 + 0x03, 0xD7, 0x10, 0x07, // 59:983 + 0x03, 0xE7, 0x2F, 0x0E, // 60:999 + 0x04, 0x16, 0x2F, 0x0E, // 61:1046 + 0x04, 0x45, 0x2E, 0x0E, // 62:1093 + 0x04, 0x73, 0x2E, 0x0D, // 63:1139 + 0x04, 0xA1, 0x5B, 0x18, // 64:1185 + 0x04, 0xFC, 0x3B, 0x10, // 65:1276 + 0x05, 0x37, 0x3B, 0x10, // 66:1335 + 0x05, 0x72, 0x3F, 0x11, // 67:1394 + 0x05, 0xB1, 0x3F, 0x11, // 68:1457 + 0x05, 0xF0, 0x3B, 0x10, // 69:1520 + 0x06, 0x2B, 0x35, 0x0F, // 70:1579 + 0x06, 0x60, 0x43, 0x13, // 71:1632 + 0x06, 0xA3, 0x3B, 0x11, // 72:1699 + 0x06, 0xDE, 0x0F, 0x07, // 73:1758 + 0x06, 0xED, 0x27, 0x0C, // 74:1773 + 0x07, 0x14, 0x3F, 0x10, // 75:1812 + 0x07, 0x53, 0x2F, 0x0D, // 76:1875 + 0x07, 0x82, 0x43, 0x14, // 77:1922 + 0x07, 0xC5, 0x3B, 0x11, // 78:1989 + 0x08, 0x00, 0x47, 0x13, // 79:2048 + 0x08, 0x47, 0x3A, 0x10, // 80:2119 + 0x08, 0x81, 0x47, 0x13, // 81:2177 + 0x08, 0xC8, 0x3F, 0x11, // 82:2248 + 0x09, 0x07, 0x3B, 0x10, // 83:2311 + 0x09, 0x42, 0x35, 0x0F, // 84:2370 + 0x09, 0x77, 0x3B, 0x11, // 85:2423 + 0x09, 0xB2, 0x39, 0x10, // 86:2482 + 0x09, 0xEB, 0x59, 0x17, // 87:2539 + 0x0A, 0x44, 0x3B, 0x10, // 88:2628 + 0x0A, 0x7F, 0x3D, 0x10, // 89:2687 + 0x0A, 0xBC, 0x37, 0x0F, // 90:2748 + 0x0A, 0xF3, 0x14, 0x07, // 91:2803 + 0x0B, 0x07, 0x1B, 0x07, // 92:2823 + 0x0B, 0x22, 0x18, 0x07, // 93:2850 + 0x0B, 0x3A, 0x2A, 0x0B, // 94:2874 + 0x0B, 0x64, 0x34, 0x0D, // 95:2916 + 0x0B, 0x98, 0x11, 0x08, // 96:2968 + 0x0B, 0xA9, 0x2F, 0x0D, // 97:2985 + 0x0B, 0xD8, 0x33, 0x0D, // 98:3032 + 0x0C, 0x0B, 0x2B, 0x0C, // 99:3083 + 0x0C, 0x36, 0x2F, 0x0D, // 100:3126 + 0x0C, 0x65, 0x2F, 0x0D, // 101:3173 + 0x0C, 0x94, 0x1A, 0x07, // 102:3220 + 0x0C, 0xAE, 0x2F, 0x0D, // 103:3246 + 0x0C, 0xDD, 0x2F, 0x0D, // 104:3293 + 0x0D, 0x0C, 0x0F, 0x05, // 105:3340 + 0x0D, 0x1B, 0x10, 0x05, // 106:3355 + 0x0D, 0x2B, 0x2F, 0x0C, // 107:3371 + 0x0D, 0x5A, 0x0F, 0x05, // 108:3418 + 0x0D, 0x69, 0x47, 0x14, // 109:3433 + 0x0D, 0xB0, 0x2F, 0x0D, // 110:3504 + 0x0D, 0xDF, 0x2F, 0x0D, // 111:3551 + 0x0E, 0x0E, 0x33, 0x0D, // 112:3598 + 0x0E, 0x41, 0x30, 0x0D, // 113:3649 + 0x0E, 0x71, 0x1E, 0x08, // 114:3697 + 0x0E, 0x8F, 0x2B, 0x0C, // 115:3727 + 0x0E, 0xBA, 0x1B, 0x07, // 116:3770 + 0x0E, 0xD5, 0x2F, 0x0D, // 117:3797 + 0x0F, 0x04, 0x2A, 0x0C, // 118:3844 + 0x0F, 0x2E, 0x42, 0x11, // 119:3886 + 0x0F, 0x70, 0x2B, 0x0C, // 120:3952 + 0x0F, 0x9B, 0x2A, 0x0C, // 121:3995 + 0x0F, 0xC5, 0x2B, 0x0C, // 122:4037 + 0x0F, 0xF0, 0x1C, 0x08, // 123:4080 + 0x10, 0x0C, 0x10, 0x06, // 124:4108 + 0x10, 0x1C, 0x1B, 0x08, // 125:4124 + 0x10, 0x37, 0x32, 0x0E, // 126:4151 + 0xFF, 0xFF, 0x00, 0x00, // 127:65535 + 0xFF, 0xFF, 0x00, 0x18, // 128:65535 + 0xFF, 0xFF, 0x00, 0x18, // 129:65535 + 0xFF, 0xFF, 0x00, 0x18, // 130:65535 + 0xFF, 0xFF, 0x00, 0x18, // 131:65535 + 0xFF, 0xFF, 0x00, 0x18, // 132:65535 + 0xFF, 0xFF, 0x00, 0x18, // 133:65535 + 0xFF, 0xFF, 0x00, 0x18, // 134:65535 + 0xFF, 0xFF, 0x00, 0x18, // 135:65535 + 0xFF, 0xFF, 0x00, 0x18, // 136:65535 + 0xFF, 0xFF, 0x00, 0x18, // 137:65535 + 0xFF, 0xFF, 0x00, 0x18, // 138:65535 + 0xFF, 0xFF, 0x00, 0x18, // 139:65535 + 0xFF, 0xFF, 0x00, 0x18, // 140:65535 + 0xFF, 0xFF, 0x00, 0x18, // 141:65535 + 0xFF, 0xFF, 0x00, 0x18, // 142:65535 + 0xFF, 0xFF, 0x00, 0x18, // 143:65535 + 0xFF, 0xFF, 0x00, 0x18, // 144:65535 + 0xFF, 0xFF, 0x00, 0x18, // 145:65535 + 0xFF, 0xFF, 0x00, 0x18, // 146:65535 + 0xFF, 0xFF, 0x00, 0x18, // 147:65535 + 0xFF, 0xFF, 0x00, 0x18, // 148:65535 + 0xFF, 0xFF, 0x00, 0x18, // 149:65535 + 0xFF, 0xFF, 0x00, 0x18, // 150:65535 + 0xFF, 0xFF, 0x00, 0x18, // 151:65535 + 0xFF, 0xFF, 0x00, 0x18, // 152:65535 + 0xFF, 0xFF, 0x00, 0x18, // 153:65535 + 0xFF, 0xFF, 0x00, 0x18, // 154:65535 + 0xFF, 0xFF, 0x00, 0x18, // 155:65535 + 0xFF, 0xFF, 0x00, 0x18, // 156:65535 + 0xFF, 0xFF, 0x00, 0x18, // 157:65535 + 0xFF, 0xFF, 0x00, 0x18, // 158:65535 + 0xFF, 0xFF, 0x00, 0x18, // 159:65535 + 0xFF, 0xFF, 0x00, 0x07, // 160:65535 + 0x10, 0x69, 0x14, 0x08, // 161:4201 + 0x10, 0x7D, 0x2B, 0x0D, // 162:4221 + 0x10, 0xA8, 0x2F, 0x0D, // 163:4264 + 0x10, 0xD7, 0x33, 0x0D, // 164:4311 + 0x11, 0x0A, 0x31, 0x0D, // 165:4362 + 0x11, 0x3B, 0x10, 0x06, // 166:4411 + 0x11, 0x4B, 0x2F, 0x0D, // 167:4427 + 0x11, 0x7A, 0x19, 0x08, // 168:4474 + 0x11, 0x93, 0x46, 0x12, // 169:4499 + 0x11, 0xD9, 0x1A, 0x09, // 170:4569 + 0x11, 0xF3, 0x27, 0x0D, // 171:4595 + 0x12, 0x1A, 0x2F, 0x0E, // 172:4634 + 0x12, 0x49, 0x1B, 0x08, // 173:4681 + 0x12, 0x64, 0x46, 0x12, // 174:4708 + 0x12, 0xAA, 0x31, 0x0D, // 175:4778 + 0x12, 0xDB, 0x1E, 0x0A, // 176:4827 + 0x12, 0xF9, 0x33, 0x0D, // 177:4857 + 0x13, 0x2C, 0x1A, 0x08, // 178:4908 + 0x13, 0x46, 0x1A, 0x08, // 179:4934 + 0x13, 0x60, 0x19, 0x08, // 180:4960 + 0x13, 0x79, 0x2F, 0x0E, // 181:4985 + 0x13, 0xA8, 0x31, 0x0D, // 182:5032 + 0x13, 0xD9, 0x12, 0x08, // 183:5081 + 0x13, 0xEB, 0x18, 0x08, // 184:5099 + 0x14, 0x03, 0x16, 0x08, // 185:5123 + 0x14, 0x19, 0x1E, 0x09, // 186:5145 + 0x14, 0x37, 0x2E, 0x0D, // 187:5175 + 0x14, 0x65, 0x4F, 0x14, // 188:5221 + 0x14, 0xB4, 0x4B, 0x14, // 189:5300 + 0x14, 0xFF, 0x4B, 0x14, // 190:5375 + 0x15, 0x4A, 0x33, 0x0F, // 191:5450 + 0x15, 0x7D, 0x3B, 0x10, // 192:5501 + 0x15, 0xB8, 0x3B, 0x10, // 193:5560 + 0x15, 0xF3, 0x3B, 0x10, // 194:5619 + 0x16, 0x2E, 0x3B, 0x10, // 195:5678 + 0x16, 0x69, 0x3B, 0x10, // 196:5737 + 0x16, 0xA4, 0x3B, 0x10, // 197:5796 + 0x16, 0xDF, 0x5B, 0x18, // 198:5855 + 0x17, 0x3A, 0x3F, 0x11, // 199:5946 + 0x17, 0x79, 0x3B, 0x10, // 200:6009 + 0x17, 0xB4, 0x3B, 0x10, // 201:6068 + 0x17, 0xEF, 0x3B, 0x10, // 202:6127 + 0x18, 0x2A, 0x3B, 0x10, // 203:6186 + 0x18, 0x65, 0x11, 0x07, // 204:6245 + 0x18, 0x76, 0x11, 0x07, // 205:6262 + 0x18, 0x87, 0x15, 0x07, // 206:6279 + 0x18, 0x9C, 0x15, 0x07, // 207:6300 + 0x18, 0xB1, 0x3F, 0x11, // 208:6321 + 0x18, 0xF0, 0x3B, 0x11, // 209:6384 + 0x19, 0x2B, 0x47, 0x13, // 210:6443 + 0x19, 0x72, 0x47, 0x13, // 211:6514 + 0x19, 0xB9, 0x47, 0x13, // 212:6585 + 0x1A, 0x00, 0x47, 0x13, // 213:6656 + 0x1A, 0x47, 0x47, 0x13, // 214:6727 + 0x1A, 0x8E, 0x2B, 0x0E, // 215:6798 + 0x1A, 0xB9, 0x47, 0x13, // 216:6841 + 0x1B, 0x00, 0x3B, 0x11, // 217:6912 + 0x1B, 0x3B, 0x3B, 0x11, // 218:6971 + 0x1B, 0x76, 0x3B, 0x11, // 219:7030 + 0x1B, 0xB1, 0x3B, 0x11, // 220:7089 + 0x1B, 0xEC, 0x3D, 0x10, // 221:7148 + 0x1C, 0x29, 0x3A, 0x10, // 222:7209 + 0x1C, 0x63, 0x37, 0x0F, // 223:7267 + 0x1C, 0x9A, 0x2F, 0x0D, // 224:7322 + 0x1C, 0xC9, 0x2F, 0x0D, // 225:7369 + 0x1C, 0xF8, 0x2F, 0x0D, // 226:7416 + 0x1D, 0x27, 0x2F, 0x0D, // 227:7463 + 0x1D, 0x56, 0x2F, 0x0D, // 228:7510 + 0x1D, 0x85, 0x2F, 0x0D, // 229:7557 + 0x1D, 0xB4, 0x53, 0x15, // 230:7604 + 0x1E, 0x07, 0x2B, 0x0C, // 231:7687 + 0x1E, 0x32, 0x2F, 0x0D, // 232:7730 + 0x1E, 0x61, 0x2F, 0x0D, // 233:7777 + 0x1E, 0x90, 0x2F, 0x0D, // 234:7824 + 0x1E, 0xBF, 0x2F, 0x0D, // 235:7871 + 0x1E, 0xEE, 0x11, 0x07, // 236:7918 + 0x1E, 0xFF, 0x11, 0x07, // 237:7935 + 0x1F, 0x10, 0x15, 0x07, // 238:7952 + 0x1F, 0x25, 0x15, 0x07, // 239:7973 + 0x1F, 0x3A, 0x2F, 0x0D, // 240:7994 + 0x1F, 0x69, 0x2F, 0x0D, // 241:8041 + 0x1F, 0x98, 0x2F, 0x0D, // 242:8088 + 0x1F, 0xC7, 0x2F, 0x0D, // 243:8135 + 0x1F, 0xF6, 0x2F, 0x0D, // 244:8182 + 0x20, 0x25, 0x2F, 0x0D, // 245:8229 + 0x20, 0x54, 0x2F, 0x0D, // 246:8276 + 0x20, 0x83, 0x32, 0x0D, // 247:8323 + 0x20, 0xB5, 0x33, 0x0F, // 248:8373 + 0x20, 0xE8, 0x2F, 0x0D, // 249:8424 + 0x21, 0x17, 0x2F, 0x0D, // 250:8471 + 0x21, 0x46, 0x2F, 0x0D, // 251:8518 + 0x21, 0x75, 0x2F, 0x0D, // 252:8565 + 0x21, 0xA4, 0x2A, 0x0C, // 253:8612 + 0x21, 0xCE, 0x2F, 0x0D, // 254:8654 + 0x21, 0xFD, 0x2A, 0x0C, // 255:8701 + + // Font Data: + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x33,0x00,0xE0,0xFF,0x33, // 33 + 0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xE0,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xE0,0x07, // 34 + 0x00,0x0C,0x03,0x00,0x00,0x0C,0x33,0x00,0x00,0x0C,0x3F,0x00,0x00,0xFC,0x0F,0x00,0x80,0xFF,0x03,0x00,0xE0,0x0F,0x03,0x00,0x60,0x0C,0x33,0x00,0x00,0x0C,0x3F,0x00,0x00,0xFC,0x0F,0x00,0x80,0xFF,0x03,0x00,0xE0,0x0F,0x03,0x00,0x60,0x0C,0x03,0x00,0x00,0x0C,0x03, // 35 + 0x00,0x00,0x00,0x00,0x80,0x07,0x06,0x00,0xC0,0x0F,0x1E,0x00,0xC0,0x18,0x1C,0x00,0x60,0x18,0x38,0x00,0x60,0x30,0x30,0x00,0xF0,0xFF,0xFF,0x00,0x60,0x30,0x30,0x00,0x60,0x60,0x38,0x00,0xC0,0x60,0x18,0x00,0xC0,0xC1,0x1F,0x00,0x00,0x81,0x07, // 36 + 0x00,0x00,0x00,0x00,0x80,0x0F,0x00,0x00,0xC0,0x1F,0x00,0x00,0x60,0x30,0x00,0x00,0x20,0x20,0x00,0x00,0x20,0x20,0x20,0x00,0x60,0x30,0x38,0x00,0xC0,0x1F,0x1E,0x00,0x80,0x8F,0x0F,0x00,0x00,0xC0,0x03,0x00,0x00,0xF0,0x00,0x00,0x00,0x3C,0x00,0x00,0x00,0x8F,0x0F,0x00,0xC0,0xC3,0x1F,0x00,0xE0,0x60,0x30,0x00,0x20,0x20,0x20,0x00,0x00,0x20,0x20,0x00,0x00,0x60,0x30,0x00,0x00,0xC0,0x1F,0x00,0x00,0x80,0x0F, // 37 + 0x00,0x00,0x00,0x00,0x00,0x80,0x07,0x00,0x00,0xC0,0x0F,0x00,0x80,0xE3,0x1C,0x00,0xC0,0x77,0x38,0x00,0xE0,0x3C,0x30,0x00,0x60,0x38,0x30,0x00,0x60,0x78,0x30,0x00,0xE0,0xEC,0x38,0x00,0xC0,0x8F,0x1B,0x00,0x80,0x03,0x1F,0x00,0x00,0x00,0x0F,0x00,0x00,0xC0,0x1F,0x00,0x00,0xC0,0x38,0x00,0x00,0x00,0x10, // 38 + 0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xE0,0x07, // 39 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x0F,0x00,0x00,0xFE,0x7F,0x00,0x80,0x0F,0xF0,0x01,0xC0,0x01,0x80,0x03,0x60,0x00,0x00,0x06,0x20,0x00,0x00,0x04, // 40 + 0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x04,0x60,0x00,0x00,0x06,0xC0,0x01,0x80,0x03,0x80,0x0F,0xF0,0x01,0x00,0xFE,0x7F,0x00,0x00,0xF0,0x0F, // 41 + 0x00,0x00,0x00,0x00,0x80,0x00,0x00,0x00,0x80,0x04,0x00,0x00,0x80,0x0F,0x00,0x00,0xE0,0x03,0x00,0x00,0xE0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x80,0x04,0x00,0x00,0x80, // 42 + 0x00,0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0xFF,0x0F,0x00,0x00,0xFF,0x0F,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60, // 43 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x03,0x00,0x00,0xF0,0x01, // 44 + 0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01, // 45 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30, // 46 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0xE0,0x0F,0x00,0x00,0xFC,0x01,0x00,0x80,0x3F,0x00,0x00,0xE0,0x03,0x00,0x00,0x60, // 47 + 0x00,0x00,0x00,0x00,0x00,0xFE,0x03,0x00,0x80,0xFF,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xE0,0x00,0x38,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0xE0,0x00,0x38,0x00,0xC0,0x01,0x1C,0x00,0x80,0xFF,0x0F,0x00,0x00,0xFE,0x03, // 48 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x07,0x00,0x00,0x00,0x03,0x00,0x00,0x80,0x01,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 49 + 0x00,0x00,0x00,0x00,0x00,0x03,0x30,0x00,0xC0,0x03,0x38,0x00,0xC0,0x00,0x3C,0x00,0x60,0x00,0x36,0x00,0x60,0x00,0x33,0x00,0x60,0x80,0x31,0x00,0x60,0xC0,0x30,0x00,0x60,0x60,0x30,0x00,0xC0,0x30,0x30,0x00,0xC0,0x1F,0x30,0x00,0x00,0x0F,0x30, // 50 + 0x00,0x00,0x00,0x00,0x00,0x01,0x06,0x00,0xC0,0x01,0x0E,0x00,0xC0,0x00,0x1C,0x00,0x60,0x00,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0xC0,0x38,0x30,0x00,0xC0,0x6F,0x18,0x00,0x80,0xC7,0x0F,0x00,0x00,0x80,0x07, // 51 + 0x00,0x00,0x00,0x00,0x00,0x80,0x03,0x00,0x00,0xC0,0x03,0x00,0x00,0xF0,0x03,0x00,0x00,0x3C,0x03,0x00,0x00,0x0E,0x03,0x00,0x80,0x07,0x03,0x00,0xC0,0x01,0x03,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x03, // 52 + 0x00,0x00,0x00,0x00,0x00,0x30,0x06,0x00,0x80,0x3F,0x0E,0x00,0xE0,0x1F,0x18,0x00,0x60,0x08,0x30,0x00,0x60,0x0C,0x30,0x00,0x60,0x0C,0x30,0x00,0x60,0x0C,0x30,0x00,0x60,0x0C,0x30,0x00,0x60,0x18,0x1C,0x00,0x60,0xF0,0x0F,0x00,0x00,0xE0,0x03, // 53 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x03,0x00,0x80,0xFF,0x0F,0x00,0xC0,0x63,0x1C,0x00,0xC0,0x30,0x38,0x00,0x60,0x18,0x30,0x00,0x60,0x18,0x30,0x00,0x60,0x18,0x30,0x00,0x60,0x18,0x30,0x00,0xE0,0x30,0x18,0x00,0xC0,0xF1,0x0F,0x00,0x80,0xC1,0x07, // 54 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x3C,0x00,0x60,0x80,0x3F,0x00,0x60,0xE0,0x03,0x00,0x60,0x78,0x00,0x00,0x60,0x0E,0x00,0x00,0x60,0x03,0x00,0x00,0xE0,0x01,0x00,0x00,0x60, // 55 + 0x00,0x00,0x00,0x00,0x00,0x80,0x07,0x00,0x80,0xC7,0x1F,0x00,0xC0,0x6F,0x18,0x00,0xE0,0x38,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0xE0,0x38,0x30,0x00,0xC0,0x6F,0x18,0x00,0x80,0xC7,0x1F,0x00,0x00,0x80,0x07, // 56 + 0x00,0x00,0x00,0x00,0x00,0x1F,0x0C,0x00,0x80,0x7F,0x1C,0x00,0xC0,0x61,0x38,0x00,0x60,0xC0,0x30,0x00,0x60,0xC0,0x30,0x00,0x60,0xC0,0x30,0x00,0x60,0xC0,0x30,0x00,0x60,0x60,0x18,0x00,0xC0,0x31,0x1E,0x00,0x80,0xFF,0x0F,0x00,0x00,0xFE,0x01, // 57 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30, // 58 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x30,0x03,0x00,0x06,0xF0,0x01, // 59 + 0x00,0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0x50,0x00,0x00,0x00,0xD8,0x00,0x00,0x00,0xD8,0x00,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x04,0x01,0x00,0x00,0x06,0x03,0x00,0x00,0x06,0x03,0x00,0x00,0x03,0x06, // 60 + 0x00,0x00,0x00,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01, // 61 + 0x00,0x00,0x00,0x00,0x00,0x03,0x06,0x00,0x00,0x06,0x03,0x00,0x00,0x06,0x03,0x00,0x00,0x04,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0xD8,0x00,0x00,0x00,0xD8,0x00,0x00,0x00,0x50,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0x20, // 62 + 0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x80,0x03,0x00,0x00,0xC0,0x01,0x00,0x00,0xE0,0x00,0x00,0x00,0x60,0x80,0x33,0x00,0x60,0xC0,0x33,0x00,0x60,0xE0,0x00,0x00,0x60,0x30,0x00,0x00,0xC0,0x38,0x00,0x00,0xC0,0x1F,0x00,0x00,0x00,0x07, // 63 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x0F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x1E,0xF0,0x00,0x00,0x07,0xC0,0x01,0x80,0xC3,0x87,0x01,0xC0,0xF1,0x9F,0x03,0xC0,0x38,0x18,0x03,0xC0,0x0C,0x30,0x03,0x60,0x0E,0x30,0x06,0x60,0x06,0x30,0x06,0x60,0x06,0x18,0x06,0x60,0x06,0x0C,0x06,0x60,0x0C,0x1E,0x06,0x60,0xF8,0x3F,0x06,0xE0,0xFE,0x31,0x06,0xC0,0x0E,0x30,0x06,0xC0,0x01,0x18,0x03,0x80,0x03,0x1C,0x03,0x00,0x07,0x8F,0x01,0x00,0xFE,0x87,0x01,0x00,0xF8,0xC1,0x00,0x00,0x00,0x40, // 64 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0x80,0x0F,0x00,0x00,0xF0,0x03,0x00,0x00,0xFE,0x01,0x00,0x80,0x8F,0x01,0x00,0xE0,0x83,0x01,0x00,0x60,0x80,0x01,0x00,0xE0,0x83,0x01,0x00,0x80,0x8F,0x01,0x00,0x00,0xFE,0x01,0x00,0x00,0xF0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 65 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0xC0,0x78,0x30,0x00,0xC0,0xFF,0x18,0x00,0x80,0xC7,0x1F,0x00,0x00,0x80,0x07, // 66 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x03,0x0F,0x00,0x00,0x02,0x03, // 67 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0xE0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x03,0x0E,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 68 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x00,0x30, // 69 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60, // 70 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0xE0,0x00,0x18,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x60,0x30,0x00,0x60,0x60,0x30,0x00,0xE0,0x60,0x38,0x00,0xC0,0x60,0x18,0x00,0xC0,0x61,0x18,0x00,0x80,0xE3,0x0F,0x00,0x00,0xE2,0x0F, // 71 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 72 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 73 + 0x00,0x00,0x00,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x1E,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x38,0x00,0xE0,0xFF,0x1F,0x00,0xE0,0xFF,0x0F, // 74 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0xE0,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x7C,0x00,0x00,0x00,0xFE,0x00,0x00,0x00,0xE7,0x01,0x00,0x80,0x83,0x07,0x00,0xC0,0x01,0x0F,0x00,0xE0,0x00,0x1E,0x00,0x60,0x00,0x38,0x00,0x20,0x00,0x30,0x00,0x00,0x00,0x20, // 75 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30, // 76 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0x01,0x00,0x00,0xC0,0x0F,0x00,0x00,0x00,0xFE,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0x00,0x3F,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x3F,0x00,0x00,0xE0,0x07,0x00,0x00,0xFE,0x00,0x00,0xC0,0x0F,0x00,0x00,0xE0,0x01,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 77 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0xC0,0x01,0x00,0x00,0x80,0x03,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x3C,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0xE0,0x01,0x00,0x00,0x80,0x03,0x00,0x00,0x00,0x0F,0x00,0x00,0x00,0x1C,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 78 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0xE0,0x00,0x38,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0xE0,0x00,0x38,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x07,0x0F,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 79 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x60,0x00,0x00,0x60,0x60,0x00,0x00,0x60,0x60,0x00,0x00,0x60,0x60,0x00,0x00,0x60,0x60,0x00,0x00,0x60,0x60,0x00,0x00,0x60,0x60,0x00,0x00,0x60,0x60,0x00,0x00,0xC0,0x30,0x00,0x00,0xC0,0x3F,0x00,0x00,0x00,0x0F, // 80 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x0C,0x00,0xC0,0x00,0x18,0x00,0xE0,0x00,0x18,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x36,0x00,0x60,0x00,0x36,0x00,0xE0,0x00,0x3C,0x00,0xC0,0x00,0x1C,0x00,0xC0,0x01,0x1C,0x00,0x80,0x07,0x3F,0x00,0x00,0xFF,0x77,0x00,0x00,0xFC,0x61, // 81 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x70,0x00,0x00,0x60,0xF0,0x00,0x00,0x60,0xF0,0x03,0x00,0x60,0xB0,0x07,0x00,0xE0,0x18,0x1F,0x00,0xC0,0x1F,0x3C,0x00,0x80,0x0F,0x30,0x00,0x00,0x00,0x20, // 82 + 0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x07,0x0F,0x00,0xC0,0x1F,0x1C,0x00,0xC0,0x18,0x18,0x00,0x60,0x38,0x38,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x70,0x30,0x00,0xC0,0x60,0x18,0x00,0xC0,0xE1,0x18,0x00,0x80,0xC3,0x0F,0x00,0x00,0x83,0x07, // 83 + 0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60, // 84 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x03,0x00,0xE0,0xFF,0x0F,0x00,0x00,0x00,0x1C,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x1C,0x00,0xE0,0xFF,0x0F,0x00,0xE0,0xFF,0x03, // 85 + 0x20,0x00,0x00,0x00,0xE0,0x01,0x00,0x00,0xC0,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0xF8,0x01,0x00,0x00,0xC0,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x3E,0x00,0x00,0xC0,0x0F,0x00,0x00,0xF8,0x01,0x00,0x00,0x3E,0x00,0x00,0xC0,0x0F,0x00,0x00,0xE0,0x01,0x00,0x00,0x20, // 86 + 0x60,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0x80,0xFF,0x00,0x00,0x00,0xF8,0x0F,0x00,0x00,0x80,0x3F,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x3F,0x00,0x00,0xE0,0x0F,0x00,0x00,0xFC,0x01,0x00,0x80,0x1F,0x00,0x00,0xE0,0x03,0x00,0x00,0x60,0x00,0x00,0x00,0xE0,0x03,0x00,0x00,0x80,0x1F,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xE0,0x0F,0x00,0x00,0x00,0x3F,0x00,0x00,0x00,0x30,0x00,0x00,0x80,0x3F,0x00,0x00,0xF8,0x0F,0x00,0x80,0xFF,0x00,0x00,0xE0,0x07,0x00,0x00,0x60, // 87 + 0x00,0x00,0x20,0x00,0x20,0x00,0x30,0x00,0x60,0x00,0x3C,0x00,0xE0,0x01,0x1E,0x00,0xC0,0x83,0x07,0x00,0x00,0xCF,0x03,0x00,0x00,0xFE,0x01,0x00,0x00,0x38,0x00,0x00,0x00,0xFE,0x01,0x00,0x00,0xCF,0x03,0x00,0xC0,0x03,0x07,0x00,0xE0,0x01,0x1E,0x00,0x60,0x00,0x3C,0x00,0x20,0x00,0x30,0x00,0x00,0x00,0x20, // 88 + 0x20,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0xC0,0x01,0x00,0x00,0x80,0x03,0x00,0x00,0x00,0x07,0x00,0x00,0x00,0x1E,0x00,0x00,0x00,0x3C,0x00,0x00,0x00,0xF0,0x3F,0x00,0x00,0xF0,0x3F,0x00,0x00,0x3C,0x00,0x00,0x00,0x1E,0x00,0x00,0x00,0x07,0x00,0x00,0xC0,0x03,0x00,0x00,0xE0,0x01,0x00,0x00,0x60,0x00,0x00,0x00,0x20, // 89 + 0x00,0x00,0x30,0x00,0x60,0x00,0x38,0x00,0x60,0x00,0x3C,0x00,0x60,0x00,0x37,0x00,0x60,0x80,0x33,0x00,0x60,0xC0,0x31,0x00,0x60,0xE0,0x30,0x00,0x60,0x38,0x30,0x00,0x60,0x1C,0x30,0x00,0x60,0x0E,0x30,0x00,0x60,0x07,0x30,0x00,0xE0,0x01,0x30,0x00,0xE0,0x00,0x30,0x00,0x60,0x00,0x30, // 90 + 0x00,0x00,0x00,0x00,0xE0,0xFF,0xFF,0x07,0xE0,0xFF,0xFF,0x07,0x60,0x00,0x00,0x06,0x60,0x00,0x00,0x06, // 91 + 0x60,0x00,0x00,0x00,0xE0,0x03,0x00,0x00,0x80,0x3F,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xE0,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 92 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x06,0x60,0x00,0x00,0x06,0xE0,0xFF,0xFF,0x07,0xE0,0xFF,0xFF,0x07, // 93 + 0x00,0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x1F,0x00,0x00,0xC0,0x07,0x00,0x00,0xE0,0x00,0x00,0x00,0xE0,0x00,0x00,0x00,0xC0,0x07,0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x20, // 94 + 0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06, // 95 + 0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0xE0,0x00,0x00,0x00,0x80, // 96 + 0x00,0x00,0x00,0x00,0x00,0x18,0x0E,0x00,0x00,0x1C,0x1F,0x00,0x00,0x8C,0x39,0x00,0x00,0x86,0x31,0x00,0x00,0x86,0x31,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x18,0x00,0x00,0xCE,0x0C,0x00,0x00,0xFC,0x1F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x00,0x20, // 97 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0x18,0x0C,0x00,0x00,0x0C,0x18,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x0E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xE0,0x03, // 98 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x0E,0x38,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x0E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0x18,0x0C, // 99 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x03,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x0E,0x38,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x0C,0x18,0x00,0x00,0x18,0x0C,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 100 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0xDC,0x1C,0x00,0x00,0xCE,0x38,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xCE,0x38,0x00,0x00,0xDC,0x18,0x00,0x00,0xF8,0x0C,0x00,0x00,0xF0,0x04, // 101 + 0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0xC0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x06,0x00,0x00,0x60,0x06,0x00,0x00,0x60,0x06, // 102 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x83,0x01,0x00,0xF8,0x8F,0x03,0x00,0x1C,0x1C,0x07,0x00,0x0E,0x38,0x06,0x00,0x06,0x30,0x06,0x00,0x06,0x30,0x06,0x00,0x06,0x30,0x06,0x00,0x0C,0x18,0x07,0x00,0x18,0x8C,0x03,0x00,0xFE,0xFF,0x01,0x00,0xFE,0xFF, // 103 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0x18,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0xFC,0x3F,0x00,0x00,0xF8,0x3F, // 104 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x60,0xFE,0x3F,0x00,0x60,0xFE,0x3F, // 105 + 0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x60,0xFE,0xFF,0x07,0x60,0xFE,0xFF,0x03, // 106 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0xC0,0x00,0x00,0x00,0xE0,0x00,0x00,0x00,0xF0,0x01,0x00,0x00,0x98,0x07,0x00,0x00,0x0C,0x0E,0x00,0x00,0x06,0x3C,0x00,0x00,0x02,0x30,0x00,0x00,0x00,0x20, // 107 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 108 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F,0x00,0x00,0x0C,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0xFC,0x3F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x0C,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0xFC,0x3F,0x00,0x00,0xF8,0x3F, // 109 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F,0x00,0x00,0x18,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0xFC,0x3F,0x00,0x00,0xF8,0x3F, // 110 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x0E,0x38,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x0E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xF0,0x07, // 111 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0xFF,0x07,0x00,0xFE,0xFF,0x07,0x00,0x18,0x0C,0x00,0x00,0x0C,0x18,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x0E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xE0,0x03, // 112 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x03,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x0E,0x38,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x0C,0x18,0x00,0x00,0x18,0x0C,0x00,0x00,0xFE,0xFF,0x07,0x00,0xFE,0xFF,0x07, // 113 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F,0x00,0x00,0x0C,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06, // 114 + 0x00,0x00,0x00,0x00,0x00,0x38,0x0C,0x00,0x00,0x7C,0x1C,0x00,0x00,0xEE,0x38,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x31,0x00,0x00,0xC6,0x31,0x00,0x00,0x8E,0x39,0x00,0x00,0x9C,0x1F,0x00,0x00,0x18,0x0F, // 115 + 0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0xC0,0xFF,0x1F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30, // 116 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x0F,0x00,0x00,0xFE,0x1F,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x18,0x00,0x00,0x00,0x0C,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F, // 117 + 0x00,0x06,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0xC0,0x07,0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x1F,0x00,0x00,0xC0,0x07,0x00,0x00,0xF8,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x06, // 118 + 0x00,0x0E,0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0xF0,0x03,0x00,0x00,0x80,0x1F,0x00,0x00,0x00,0x38,0x00,0x00,0x80,0x1F,0x00,0x00,0xE0,0x03,0x00,0x00,0x7C,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x7C,0x00,0x00,0x00,0xE0,0x03,0x00,0x00,0x80,0x1F,0x00,0x00,0x00,0x38,0x00,0x00,0x80,0x1F,0x00,0x00,0xF0,0x03,0x00,0x00,0x7E,0x00,0x00,0x00,0x0E, // 119 + 0x00,0x02,0x20,0x00,0x00,0x06,0x30,0x00,0x00,0x1E,0x3C,0x00,0x00,0x38,0x0E,0x00,0x00,0xF0,0x07,0x00,0x00,0xC0,0x01,0x00,0x00,0xE0,0x07,0x00,0x00,0x38,0x0E,0x00,0x00,0x1C,0x3C,0x00,0x00,0x0E,0x30,0x00,0x00,0x02,0x20, // 120 + 0x00,0x00,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x7E,0x00,0x06,0x00,0xF0,0x01,0x06,0x00,0x80,0x0F,0x07,0x00,0x00,0xFE,0x03,0x00,0x00,0xFC,0x00,0x00,0xC0,0x1F,0x00,0x00,0xF8,0x03,0x00,0x00,0x3E,0x00,0x00,0x00,0x06, // 121 + 0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x06,0x3C,0x00,0x00,0x06,0x3E,0x00,0x00,0x06,0x37,0x00,0x00,0xC6,0x33,0x00,0x00,0xE6,0x30,0x00,0x00,0x76,0x30,0x00,0x00,0x3E,0x30,0x00,0x00,0x1E,0x30,0x00,0x00,0x06,0x30, // 122 + 0x00,0x00,0x00,0x00,0x00,0x80,0x01,0x00,0x00,0xC0,0x03,0x00,0xC0,0x7F,0xFE,0x03,0xE0,0x3F,0xFC,0x07,0x60,0x00,0x00,0x06,0x60,0x00,0x00,0x06, // 123 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0xFF,0x0F,0xE0,0xFF,0xFF,0x0F, // 124 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x06,0x60,0x00,0x00,0x06,0xE0,0x3F,0xFC,0x07,0xC0,0x7F,0xFF,0x03,0x00,0xC0,0x03,0x00,0x00,0x80,0x01, // 125 + 0x00,0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0xE0,0x00,0x00,0x00,0x60, // 126 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE6,0xFF,0x07,0x00,0xE6,0xFF,0x07, // 161 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x9C,0x07,0x00,0x0E,0x78,0x00,0x00,0x06,0x3F,0x00,0x00,0xF6,0x30,0x00,0x00,0x0E,0x30,0x00,0xE0,0x0D,0x1C,0x00,0x00,0x1C,0x0E,0x00,0x00,0x10,0x06, // 162 + 0x00,0x60,0x10,0x00,0x00,0x60,0x38,0x00,0x00,0x7F,0x1C,0x00,0xC0,0xFF,0x1F,0x00,0xE0,0xE0,0x19,0x00,0x60,0x60,0x18,0x00,0x60,0x60,0x18,0x00,0x60,0x60,0x30,0x00,0xE0,0x00,0x30,0x00,0xC0,0x01,0x30,0x00,0x80,0x01,0x38,0x00,0x00,0x00,0x10, // 163 + 0x00,0x00,0x00,0x00,0x00,0x02,0x04,0x00,0x00,0xF7,0x0E,0x00,0x00,0xFE,0x07,0x00,0x00,0x0C,0x03,0x00,0x00,0x06,0x06,0x00,0x00,0x06,0x06,0x00,0x00,0x06,0x06,0x00,0x00,0x06,0x06,0x00,0x00,0x0C,0x03,0x00,0x00,0xFE,0x07,0x00,0x00,0xF7,0x0E,0x00,0x00,0x02,0x04, // 164 + 0xE0,0x60,0x06,0x00,0xC0,0x61,0x06,0x00,0x80,0x67,0x06,0x00,0x00,0x7E,0x06,0x00,0x00,0x7C,0x06,0x00,0x00,0xF0,0x3F,0x00,0x00,0xF0,0x3F,0x00,0x00,0x7C,0x06,0x00,0x00,0x7E,0x06,0x00,0x80,0x67,0x06,0x00,0xC0,0x61,0x06,0x00,0xE0,0x60,0x06,0x00,0x20, // 165 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0x7F,0xF8,0x0F,0xE0,0x7F,0xF8,0x0F, // 166 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x00,0x00,0x80,0xF3,0xC1,0x00,0xC0,0x1F,0xC3,0x03,0xE0,0x0C,0x07,0x03,0x60,0x1C,0x06,0x06,0x60,0x18,0x0C,0x06,0x60,0x30,0x1C,0x06,0xE0,0x70,0x38,0x07,0xC0,0xE1,0xF4,0x03,0x80,0xC1,0xE7,0x01,0x00,0x80,0x03, // 167 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60, // 168 + 0x00,0xF8,0x00,0x00,0x00,0xFE,0x03,0x00,0x00,0x07,0x07,0x00,0x80,0x01,0x0C,0x00,0xC0,0x79,0x1C,0x00,0xC0,0xFE,0x19,0x00,0x60,0x86,0x31,0x00,0x60,0x03,0x33,0x00,0x60,0x03,0x33,0x00,0x60,0x03,0x33,0x00,0x60,0x03,0x33,0x00,0x60,0x87,0x33,0x00,0xC0,0x86,0x19,0x00,0xC0,0x85,0x1C,0x00,0x80,0x01,0x0C,0x00,0x00,0x07,0x07,0x00,0x00,0xFE,0x03,0x00,0x00,0xF8, // 169 + 0x00,0x00,0x00,0x00,0xC0,0x1C,0x00,0x00,0xE0,0x3E,0x00,0x00,0x60,0x32,0x00,0x00,0x60,0x32,0x00,0x00,0xE0,0x3F,0x00,0x00,0xC0,0x3F, // 170 + 0x00,0x00,0x00,0x00,0x00,0x80,0x00,0x00,0x00,0xE0,0x03,0x00,0x00,0x78,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x84,0x10,0x00,0x00,0xE0,0x03,0x00,0x00,0x78,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x04,0x10, // 171 + 0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFC,0x01, // 172 + 0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01, // 173 + 0x00,0xF8,0x00,0x00,0x00,0xFE,0x03,0x00,0x00,0x07,0x07,0x00,0x80,0x01,0x0C,0x00,0xC0,0x01,0x1C,0x00,0xC0,0xFE,0x1B,0x00,0x60,0xFE,0x33,0x00,0x60,0x66,0x30,0x00,0x60,0x66,0x30,0x00,0x60,0xE6,0x30,0x00,0x60,0xFE,0x31,0x00,0x60,0x3C,0x33,0x00,0xC0,0x00,0x1A,0x00,0xC0,0x01,0x1C,0x00,0x80,0x01,0x0C,0x00,0x00,0x07,0x07,0x00,0x00,0xFE,0x03,0x00,0x00,0xF8, // 174 + 0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C, // 175 + 0x00,0x00,0x00,0x00,0x80,0x03,0x00,0x00,0x40,0x04,0x00,0x00,0x20,0x08,0x00,0x00,0x20,0x08,0x00,0x00,0x20,0x08,0x00,0x00,0x40,0x04,0x00,0x00,0x80,0x03, // 176 + 0x00,0x00,0x00,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0xFF,0x3F,0x00,0x00,0xFF,0x3F,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30, // 177 + 0x40,0x20,0x00,0x00,0x60,0x30,0x00,0x00,0x20,0x38,0x00,0x00,0x20,0x2C,0x00,0x00,0x20,0x26,0x00,0x00,0xE0,0x23,0x00,0x00,0xC0,0x21, // 178 + 0x40,0x10,0x00,0x00,0x60,0x30,0x00,0x00,0x20,0x20,0x00,0x00,0x20,0x22,0x00,0x00,0x20,0x22,0x00,0x00,0xE0,0x3D,0x00,0x00,0xC0,0x1D, // 179 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x00,0x00,0x00,0xE0,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x20, // 180 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0xFF,0x07,0x00,0xFE,0xFF,0x07,0x00,0x00,0x1C,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x1C,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F, // 181 + 0x00,0x0F,0x00,0x00,0xC0,0x3F,0x00,0x00,0xC0,0x3F,0x00,0x00,0xE0,0x7F,0x00,0x00,0xE0,0x7F,0x00,0x00,0xE0,0xFF,0xFF,0x07,0xE0,0xFF,0xFF,0x07,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0xE0,0xFF,0xFF,0x07,0xE0,0xFF,0xFF,0x07,0x60,0x00,0x00,0x00,0x60, // 182 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60, // 183 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x02,0x00,0x00,0xC0,0x02,0x00,0x00,0x80,0x03,0x00,0x00,0x00,0x01, // 184 + 0x00,0x00,0x00,0x00,0x80,0x01,0x00,0x00,0xC0,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0xE0,0x3F,0x00,0x00,0xE0,0x3F, // 185 + 0x00,0x00,0x00,0x00,0x80,0x0F,0x00,0x00,0xC0,0x1F,0x00,0x00,0xE0,0x38,0x00,0x00,0x60,0x30,0x00,0x00,0xE0,0x38,0x00,0x00,0xC0,0x1F,0x00,0x00,0x80,0x0F, // 186 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x10,0x00,0x00,0x1C,0x1C,0x00,0x00,0x78,0x0F,0x00,0x00,0xE0,0x03,0x00,0x00,0x84,0x10,0x00,0x00,0x1C,0x1C,0x00,0x00,0x78,0x0F,0x00,0x00,0xE0,0x03,0x00,0x00,0x80, // 187 + 0x00,0x00,0x00,0x00,0x80,0x01,0x00,0x00,0xC0,0x00,0x00,0x00,0xC0,0x00,0x20,0x00,0xE0,0x3F,0x38,0x00,0xE0,0x3F,0x1C,0x00,0x00,0x00,0x0E,0x00,0x00,0x80,0x03,0x00,0x00,0xC0,0x01,0x00,0x00,0xE0,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x1C,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x07,0x0C,0x00,0xC0,0x01,0x0E,0x00,0xE0,0x80,0x0B,0x00,0x60,0xC0,0x08,0x00,0x00,0xE0,0x3F,0x00,0x00,0xE0,0x3F,0x00,0x00,0x00,0x08, // 188 + 0x00,0x00,0x00,0x00,0x80,0x01,0x00,0x00,0xC0,0x00,0x00,0x00,0xC0,0x00,0x20,0x00,0xE0,0x3F,0x30,0x00,0xE0,0x3F,0x1C,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x07,0x00,0x00,0xC0,0x01,0x00,0x00,0xE0,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0x1C,0x00,0x00,0x00,0x4E,0x20,0x00,0x00,0x67,0x30,0x00,0xC0,0x21,0x38,0x00,0xE0,0x20,0x2C,0x00,0x60,0x20,0x26,0x00,0x00,0xE0,0x27,0x00,0x00,0xC0,0x21, // 189 + 0x40,0x10,0x00,0x00,0x60,0x30,0x00,0x00,0x20,0x20,0x00,0x00,0x20,0x22,0x20,0x00,0x20,0x22,0x30,0x00,0xE0,0x3D,0x38,0x00,0xC0,0x1D,0x0E,0x00,0x00,0x00,0x07,0x00,0x00,0x80,0x03,0x00,0x00,0xE0,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x0E,0x0C,0x00,0x00,0x07,0x0E,0x00,0x80,0x83,0x0B,0x00,0xE0,0xC0,0x08,0x00,0x60,0xE0,0x3F,0x00,0x20,0xE0,0x3F,0x00,0x00,0x00,0x08, // 190 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xF0,0x00,0x00,0x00,0xF8,0x03,0x00,0x00,0x1E,0x03,0x00,0x00,0x07,0x07,0x00,0xE6,0x03,0x06,0x00,0xE6,0x01,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x07,0x00,0x00,0x80,0x03,0x00,0x00,0xC0,0x01,0x00,0x00,0xC0, // 191 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0x80,0x0F,0x00,0x00,0xF0,0x03,0x00,0x00,0xFE,0x01,0x00,0x82,0x8F,0x01,0x00,0xE6,0x83,0x01,0x00,0x6E,0x80,0x01,0x00,0xE8,0x83,0x01,0x00,0x80,0x8F,0x01,0x00,0x00,0xFE,0x01,0x00,0x00,0xF0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 192 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0x80,0x0F,0x00,0x00,0xF0,0x03,0x00,0x00,0xFE,0x01,0x00,0x80,0x8F,0x01,0x00,0xE8,0x83,0x01,0x00,0x6E,0x80,0x01,0x00,0xE6,0x83,0x01,0x00,0x82,0x8F,0x01,0x00,0x00,0xFE,0x01,0x00,0x00,0xF0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 193 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0x80,0x0F,0x00,0x00,0xF0,0x03,0x00,0x00,0xFE,0x01,0x00,0x88,0x8F,0x01,0x00,0xEC,0x83,0x01,0x00,0x66,0x80,0x01,0x00,0xE6,0x83,0x01,0x00,0x8C,0x8F,0x01,0x00,0x08,0xFE,0x01,0x00,0x00,0xF0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 194 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0x80,0x0F,0x00,0x00,0xF0,0x03,0x00,0x0C,0xFE,0x01,0x00,0x8E,0x8F,0x01,0x00,0xE6,0x83,0x01,0x00,0x66,0x80,0x01,0x00,0xEC,0x83,0x01,0x00,0x8C,0x8F,0x01,0x00,0x0E,0xFE,0x01,0x00,0x06,0xF0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 195 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0x80,0x0F,0x00,0x00,0xF0,0x03,0x00,0x00,0xFE,0x01,0x00,0x8C,0x8F,0x01,0x00,0xEC,0x83,0x01,0x00,0x60,0x80,0x01,0x00,0xE0,0x83,0x01,0x00,0x8C,0x8F,0x01,0x00,0x0C,0xFE,0x01,0x00,0x00,0xF0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 196 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0x80,0x0F,0x00,0x00,0xF0,0x03,0x00,0x00,0xFE,0x01,0x00,0x9C,0x8F,0x01,0x00,0xE2,0x83,0x01,0x00,0x62,0x80,0x01,0x00,0xE2,0x83,0x01,0x00,0x9C,0x8F,0x01,0x00,0x00,0xFE,0x01,0x00,0x00,0xF0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 197 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3C,0x00,0x00,0x00,0x0F,0x00,0x00,0xC0,0x03,0x00,0x00,0xF0,0x01,0x00,0x00,0xBC,0x01,0x00,0x00,0x8F,0x01,0x00,0xC0,0x83,0x01,0x00,0xE0,0x80,0x01,0x00,0x60,0x80,0x01,0x00,0x60,0x80,0x01,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x00,0x30, // 198 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0x60,0x00,0x30,0x02,0x60,0x00,0x30,0x02,0x60,0x00,0xF0,0x02,0x60,0x00,0xB0,0x03,0x60,0x00,0x30,0x01,0x60,0x00,0x30,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x03,0x0F,0x00,0x00,0x02,0x03, // 199 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x62,0x30,0x30,0x00,0x66,0x30,0x30,0x00,0x6E,0x30,0x30,0x00,0x68,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x00,0x30, // 200 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x68,0x30,0x30,0x00,0x6E,0x30,0x30,0x00,0x66,0x30,0x30,0x00,0x62,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x00,0x30, // 201 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x68,0x30,0x30,0x00,0x6C,0x30,0x30,0x00,0x66,0x30,0x30,0x00,0x66,0x30,0x30,0x00,0x6C,0x30,0x30,0x00,0x68,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x00,0x30, // 202 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x6C,0x30,0x30,0x00,0x6C,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x6C,0x30,0x30,0x00,0x6C,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x00,0x30, // 203 + 0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0xE6,0xFF,0x3F,0x00,0xEE,0xFF,0x3F,0x00,0x08, // 204 + 0x00,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0xEE,0xFF,0x3F,0x00,0xE6,0xFF,0x3F,0x00,0x02, // 205 + 0x08,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0xE6,0xFF,0x3F,0x00,0xE6,0xFF,0x3F,0x00,0x0C,0x00,0x00,0x00,0x08, // 206 + 0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x0C,0x00,0x00,0x00,0x0C, // 207 + 0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0xE0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x03,0x0E,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 208 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0xC0,0x01,0x00,0x00,0x8C,0x03,0x00,0x00,0x0E,0x0E,0x00,0x00,0x06,0x3C,0x00,0x00,0x06,0x70,0x00,0x00,0x0C,0xE0,0x01,0x00,0x0C,0x80,0x03,0x00,0x0E,0x00,0x0F,0x00,0x06,0x00,0x1C,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 209 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0xE0,0x00,0x38,0x00,0x62,0x00,0x30,0x00,0x66,0x00,0x30,0x00,0x6E,0x00,0x30,0x00,0x68,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0xE0,0x00,0x38,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x07,0x0F,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 210 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0xE0,0x00,0x38,0x00,0x60,0x00,0x30,0x00,0x68,0x00,0x30,0x00,0x6E,0x00,0x30,0x00,0x66,0x00,0x30,0x00,0x62,0x00,0x30,0x00,0xE0,0x00,0x38,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x07,0x0F,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 211 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0xE0,0x00,0x38,0x00,0x68,0x00,0x30,0x00,0x6C,0x00,0x30,0x00,0x66,0x00,0x30,0x00,0x66,0x00,0x30,0x00,0x6C,0x00,0x30,0x00,0xE8,0x00,0x38,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x07,0x0F,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 212 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xCC,0x00,0x18,0x00,0xEE,0x00,0x38,0x00,0x66,0x00,0x30,0x00,0x66,0x00,0x30,0x00,0x6C,0x00,0x30,0x00,0x6C,0x00,0x30,0x00,0x6E,0x00,0x30,0x00,0xE6,0x00,0x38,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x07,0x0F,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 213 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0xE0,0x00,0x38,0x00,0x6C,0x00,0x30,0x00,0x6C,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x6C,0x00,0x30,0x00,0xEC,0x00,0x38,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x07,0x0F,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 214 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x03,0x00,0x00,0x8E,0x03,0x00,0x00,0xDC,0x01,0x00,0x00,0xF8,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0xDC,0x01,0x00,0x00,0x8E,0x03,0x00,0x00,0x06,0x03, // 215 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x21,0x00,0x00,0xFF,0x77,0x00,0x80,0x07,0x3F,0x00,0xC0,0x01,0x1E,0x00,0xC0,0x00,0x1F,0x00,0xE0,0x80,0x3B,0x00,0x60,0xC0,0x31,0x00,0x60,0xE0,0x30,0x00,0x60,0x70,0x30,0x00,0x60,0x38,0x30,0x00,0x60,0x1C,0x30,0x00,0xE0,0x0E,0x38,0x00,0xC0,0x07,0x18,0x00,0xC0,0x03,0x1C,0x00,0xE0,0x07,0x0F,0x00,0x70,0xFF,0x07,0x00,0x20,0xFC,0x01, // 216 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x03,0x00,0xE0,0xFF,0x0F,0x00,0x00,0x00,0x1C,0x00,0x00,0x00,0x38,0x00,0x02,0x00,0x30,0x00,0x06,0x00,0x30,0x00,0x0E,0x00,0x30,0x00,0x08,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x1C,0x00,0xE0,0xFF,0x0F,0x00,0xE0,0xFF,0x03, // 217 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x03,0x00,0xE0,0xFF,0x0F,0x00,0x00,0x00,0x1C,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x30,0x00,0x08,0x00,0x30,0x00,0x0E,0x00,0x30,0x00,0x06,0x00,0x30,0x00,0x02,0x00,0x30,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x1C,0x00,0xE0,0xFF,0x0F,0x00,0xE0,0xFF,0x03, // 218 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x03,0x00,0xE0,0xFF,0x0F,0x00,0x00,0x00,0x1C,0x00,0x00,0x00,0x38,0x00,0x08,0x00,0x30,0x00,0x0C,0x00,0x30,0x00,0x06,0x00,0x30,0x00,0x06,0x00,0x30,0x00,0x0C,0x00,0x30,0x00,0x08,0x00,0x38,0x00,0x00,0x00,0x1C,0x00,0xE0,0xFF,0x0F,0x00,0xE0,0xFF,0x03, // 219 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x03,0x00,0xE0,0xFF,0x0F,0x00,0x00,0x00,0x1C,0x00,0x00,0x00,0x38,0x00,0x0C,0x00,0x30,0x00,0x0C,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x0C,0x00,0x30,0x00,0x0C,0x00,0x38,0x00,0x00,0x00,0x1C,0x00,0xE0,0xFF,0x0F,0x00,0xE0,0xFF,0x03, // 220 + 0x20,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0xC0,0x01,0x00,0x00,0x80,0x03,0x00,0x00,0x00,0x07,0x00,0x00,0x00,0x1E,0x00,0x00,0x00,0x3C,0x00,0x00,0x08,0xF0,0x3F,0x00,0x0E,0xF0,0x3F,0x00,0x06,0x3C,0x00,0x00,0x02,0x1E,0x00,0x00,0x00,0x07,0x00,0x00,0xC0,0x03,0x00,0x00,0xE0,0x01,0x00,0x00,0x60,0x00,0x00,0x00,0x20, // 221 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0x03,0x06,0x00,0x00,0x03,0x06,0x00,0x00,0x03,0x06,0x00,0x00,0x03,0x06,0x00,0x00,0x03,0x06,0x00,0x00,0x03,0x06,0x00,0x00,0x03,0x06,0x00,0x00,0x03,0x07,0x00,0x00,0x86,0x03,0x00,0x00,0xFE,0x01,0x00,0x00,0xF8, // 222 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0xFF,0x3F,0x00,0xC0,0xFF,0x3F,0x00,0xC0,0x00,0x00,0x00,0x60,0x00,0x08,0x00,0x60,0x00,0x1C,0x00,0x60,0x00,0x38,0x00,0xE0,0x78,0x30,0x00,0xC0,0x7F,0x30,0x00,0x80,0xC7,0x30,0x00,0x00,0x80,0x39,0x00,0x00,0x80,0x1F,0x00,0x00,0x00,0x0F, // 223 + 0x00,0x00,0x00,0x00,0x00,0x18,0x0E,0x00,0x00,0x1C,0x1F,0x00,0x00,0x8C,0x39,0x00,0x20,0x86,0x31,0x00,0x60,0x86,0x31,0x00,0xE0,0xC6,0x30,0x00,0x80,0xC6,0x18,0x00,0x00,0xCE,0x0C,0x00,0x00,0xFC,0x1F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x00,0x20, // 224 + 0x00,0x00,0x00,0x00,0x00,0x18,0x0E,0x00,0x00,0x1C,0x1F,0x00,0x00,0x8C,0x39,0x00,0x00,0x86,0x31,0x00,0x80,0x86,0x31,0x00,0xE0,0xC6,0x30,0x00,0x60,0xC6,0x18,0x00,0x20,0xCE,0x0C,0x00,0x00,0xFC,0x1F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x00,0x20, // 225 + 0x00,0x00,0x00,0x00,0x00,0x18,0x0E,0x00,0x00,0x1C,0x1F,0x00,0x80,0x8C,0x39,0x00,0xC0,0x86,0x31,0x00,0x60,0x86,0x31,0x00,0x60,0xC6,0x30,0x00,0xC0,0xC6,0x18,0x00,0x80,0xCE,0x0C,0x00,0x00,0xFC,0x1F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x00,0x20, // 226 + 0x00,0x00,0x00,0x00,0x00,0x18,0x0E,0x00,0xC0,0x1C,0x1F,0x00,0xE0,0x8C,0x39,0x00,0x60,0x86,0x31,0x00,0x60,0x86,0x31,0x00,0xC0,0xC6,0x30,0x00,0xC0,0xC6,0x18,0x00,0xE0,0xCE,0x0C,0x00,0x60,0xFC,0x1F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x00,0x20, // 227 + 0x00,0x00,0x00,0x00,0x00,0x18,0x0E,0x00,0x00,0x1C,0x1F,0x00,0xC0,0x8C,0x39,0x00,0xC0,0x86,0x31,0x00,0x00,0x86,0x31,0x00,0x00,0xC6,0x30,0x00,0xC0,0xC6,0x18,0x00,0xC0,0xCE,0x0C,0x00,0x00,0xFC,0x1F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x00,0x20, // 228 + 0x00,0x00,0x00,0x00,0x00,0x18,0x0E,0x00,0x00,0x1C,0x1F,0x00,0x00,0x8C,0x39,0x00,0x70,0x86,0x31,0x00,0x88,0x86,0x31,0x00,0x88,0xC6,0x30,0x00,0x88,0xC6,0x18,0x00,0x70,0xCE,0x0C,0x00,0x00,0xFC,0x1F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x00,0x20, // 229 + 0x00,0x00,0x00,0x00,0x00,0x10,0x0F,0x00,0x00,0x9C,0x1F,0x00,0x00,0xCC,0x39,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0x66,0x18,0x00,0x00,0x6E,0x1C,0x00,0x00,0xFC,0x0F,0x00,0x00,0xFC,0x1F,0x00,0x00,0xCC,0x1C,0x00,0x00,0xCE,0x38,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xCC,0x18,0x00,0x00,0xF8,0x0C,0x00,0x00,0xE0,0x04, // 230 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x0E,0x38,0x02,0x00,0x06,0x30,0x02,0x00,0x06,0xF0,0x02,0x00,0x06,0xB0,0x03,0x00,0x0E,0x38,0x01,0x00,0x1C,0x1C,0x00,0x00,0x18,0x0C, // 231 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0xDC,0x1C,0x00,0x20,0xCE,0x38,0x00,0x60,0xC6,0x30,0x00,0xE0,0xC6,0x30,0x00,0x80,0xC6,0x30,0x00,0x00,0xCE,0x38,0x00,0x00,0xDC,0x18,0x00,0x00,0xF8,0x0C,0x00,0x00,0xF0,0x04, // 232 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0xDC,0x1C,0x00,0x00,0xCE,0x38,0x00,0x80,0xC6,0x30,0x00,0xE0,0xC6,0x30,0x00,0x60,0xC6,0x30,0x00,0x20,0xCE,0x38,0x00,0x00,0xDC,0x18,0x00,0x00,0xF8,0x0C,0x00,0x00,0xF0,0x04, // 233 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0xDC,0x1C,0x00,0x80,0xCE,0x38,0x00,0xC0,0xC6,0x30,0x00,0x60,0xC6,0x30,0x00,0x60,0xC6,0x30,0x00,0xC0,0xCE,0x38,0x00,0x80,0xDC,0x18,0x00,0x00,0xF8,0x0C,0x00,0x00,0xF0,0x04, // 234 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0xDC,0x1C,0x00,0xC0,0xCE,0x38,0x00,0xC0,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0xC0,0xCE,0x38,0x00,0xC0,0xDC,0x18,0x00,0x00,0xF8,0x0C,0x00,0x00,0xF0,0x04, // 235 + 0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x00,0x60,0xFE,0x3F,0x00,0xE0,0xFE,0x3F,0x00,0x80, // 236 + 0x00,0x00,0x00,0x00,0x80,0x00,0x00,0x00,0xE0,0xFE,0x3F,0x00,0x60,0xFE,0x3F,0x00,0x20, // 237 + 0x80,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0x60,0xFE,0x3F,0x00,0x60,0xFE,0x3F,0x00,0xC0,0x00,0x00,0x00,0x80, // 238 + 0xC0,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F,0x00,0xC0,0x00,0x00,0x00,0xC0, // 239 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1D,0x1C,0x00,0xA0,0x0F,0x38,0x00,0xA0,0x06,0x30,0x00,0xE0,0x06,0x30,0x00,0xC0,0x06,0x30,0x00,0xC0,0x0F,0x38,0x00,0x20,0x1F,0x1C,0x00,0x00,0xFC,0x0F,0x00,0x00,0xE0,0x07, // 240 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x3F,0x00,0xC0,0xFE,0x3F,0x00,0xE0,0x18,0x00,0x00,0x60,0x0C,0x00,0x00,0x60,0x06,0x00,0x00,0xC0,0x06,0x00,0x00,0xC0,0x06,0x00,0x00,0xE0,0x0E,0x00,0x00,0x60,0xFC,0x3F,0x00,0x00,0xF8,0x3F, // 241 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x20,0x0E,0x38,0x00,0x60,0x06,0x30,0x00,0xE0,0x06,0x30,0x00,0x80,0x06,0x30,0x00,0x00,0x0E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xF0,0x07, // 242 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x0E,0x38,0x00,0x80,0x06,0x30,0x00,0xE0,0x06,0x30,0x00,0x60,0x06,0x30,0x00,0x20,0x0E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xF0,0x07, // 243 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x80,0x0E,0x38,0x00,0xC0,0x06,0x30,0x00,0x60,0x06,0x30,0x00,0x60,0x06,0x30,0x00,0xC0,0x0E,0x38,0x00,0x80,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xF0,0x07, // 244 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0xC0,0x1C,0x1C,0x00,0xE0,0x0E,0x38,0x00,0x60,0x06,0x30,0x00,0x60,0x06,0x30,0x00,0xC0,0x06,0x30,0x00,0xC0,0x0E,0x38,0x00,0xE0,0x1C,0x1C,0x00,0x60,0xF8,0x0F,0x00,0x00,0xF0,0x07, // 245 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0xC0,0x0E,0x38,0x00,0xC0,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0xC0,0x0E,0x38,0x00,0xC0,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xF0,0x07, // 246 + 0x00,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0xB6,0x01,0x00,0x00,0xB6,0x01,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30, // 247 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xF0,0x67,0x00,0x00,0xF8,0x7F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x0E,0x3F,0x00,0x00,0x86,0x33,0x00,0x00,0xE6,0x31,0x00,0x00,0x76,0x30,0x00,0x00,0x3E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0xFF,0x0F,0x00,0x00,0xF3,0x07, // 248 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x0F,0x00,0x00,0xFE,0x1F,0x00,0x20,0x00,0x38,0x00,0x60,0x00,0x30,0x00,0xE0,0x00,0x30,0x00,0x80,0x00,0x30,0x00,0x00,0x00,0x18,0x00,0x00,0x00,0x0C,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F, // 249 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x0F,0x00,0x00,0xFE,0x1F,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x30,0x00,0x80,0x00,0x30,0x00,0xE0,0x00,0x30,0x00,0x60,0x00,0x18,0x00,0x20,0x00,0x0C,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F, // 250 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x0F,0x00,0x00,0xFE,0x1F,0x00,0x80,0x00,0x38,0x00,0xC0,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0xC0,0x00,0x18,0x00,0x80,0x00,0x0C,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F, // 251 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x0F,0x00,0x00,0xFE,0x1F,0x00,0xC0,0x00,0x38,0x00,0xC0,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0xC0,0x00,0x18,0x00,0xC0,0x00,0x0C,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F, // 252 + 0x00,0x00,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x7E,0x00,0x06,0x00,0xF0,0x01,0x06,0x00,0x80,0x0F,0x07,0x80,0x00,0xFE,0x03,0xE0,0x00,0xFC,0x00,0x60,0xC0,0x1F,0x00,0x20,0xF8,0x03,0x00,0x00,0x3E,0x00,0x00,0x00,0x06, // 253 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0xFF,0x07,0xE0,0xFF,0xFF,0x07,0x00,0x1C,0x18,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x0E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xF0,0x03, // 254 + 0x00,0x00,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x7E,0x00,0x06,0xC0,0xF0,0x01,0x06,0xC0,0x80,0x0F,0x07,0x00,0x00,0xFE,0x03,0x00,0x00,0xFC,0x00,0xC0,0xC0,0x1F,0x00,0xC0,0xF8,0x03,0x00,0x00,0x3E,0x00,0x00,0x00,0x06 // 255 +}; +#endif diff --git a/lib/esp8266-oled-ssd1306-master/src/OLEDDisplayUi.cpp b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplayUi.cpp new file mode 100644 index 00000000..6fcfafbf --- /dev/null +++ b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplayUi.cpp @@ -0,0 +1,468 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn + * Copyright (c) 2018 by Fabrice Weinberg + * Copyright (c) 2019 by Helmut Tschemernjak - www.radioshuttle.de + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * ThingPulse invests considerable time and money to develop these open source libraries. + * Please support us by buying our products (and not the clones) from + * https://thingpulse.com + * + */ + +#include "OLEDDisplayUi.h" + +void LoadingDrawDefault(OLEDDisplay *display, LoadingStage* stage, uint8_t progress) { + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(ArialMT_Plain_10); + display->drawString(64, 18, stage->process); + display->drawProgressBar(4, 32, 120, 8, progress); +}; + + +OLEDDisplayUi::OLEDDisplayUi(OLEDDisplay *display) { + this->display = display; + + indicatorPosition = BOTTOM; + indicatorDirection = LEFT_RIGHT; + activeSymbol = ANIMATION_activeSymbol; + inactiveSymbol = ANIMATION_inactiveSymbol; + frameAnimationDirection = SLIDE_RIGHT; + lastTransitionDirection = 1; + frameCount = 0; + nextFrameNumber = -1; + overlayCount = 0; + indicatorDrawState = 1; + loadingDrawFunction = LoadingDrawDefault; + updateInterval = 33; + state.lastUpdate = 0; + state.ticksSinceLastStateSwitch = 0; + state.frameState = FIXED; + state.currentFrame = 0; + state.frameTransitionDirection = 1; + state.isIndicatorDrawen = true; + state.manuelControll = false; + state.userData = NULL; + shouldDrawIndicators = true; + autoTransition = true; + setTimePerFrame(5000); + setTimePerTransition(500); +} + +void OLEDDisplayUi::init() { + this->display->init(); +} + +void OLEDDisplayUi::setTargetFPS(uint8_t fps){ + this->updateInterval = ((float) 1.0 / (float) fps) * 1000; + + this->ticksPerFrame = timePerFrame / updateInterval; + this->ticksPerTransition = timePerTransition / updateInterval; +} + +// -/------ Automatic controll ------\- + +void OLEDDisplayUi::enableAutoTransition(){ + this->autoTransition = true; +} +void OLEDDisplayUi::disableAutoTransition(){ + this->autoTransition = false; +} +void OLEDDisplayUi::setAutoTransitionForwards(){ + this->state.frameTransitionDirection = 1; + this->lastTransitionDirection = 1; +} +void OLEDDisplayUi::setAutoTransitionBackwards(){ + this->state.frameTransitionDirection = -1; + this->lastTransitionDirection = -1; +} +void OLEDDisplayUi::setTimePerFrame(uint16_t time){ + this->timePerFrame = time; + this->ticksPerFrame = timePerFrame / updateInterval; +} +void OLEDDisplayUi::setTimePerTransition(uint16_t time){ + this->timePerTransition = time; + this->ticksPerTransition = timePerTransition / updateInterval; +} + +// -/------ Customize indicator position and style -------\- +void OLEDDisplayUi::enableIndicator(){ + this->state.isIndicatorDrawen = true; +} + +void OLEDDisplayUi::disableIndicator(){ + this->state.isIndicatorDrawen = false; +} + +void OLEDDisplayUi::enableAllIndicators(){ + this->shouldDrawIndicators = true; +} + +void OLEDDisplayUi::disableAllIndicators(){ + this->shouldDrawIndicators = false; +} + +void OLEDDisplayUi::setIndicatorPosition(IndicatorPosition pos) { + this->indicatorPosition = pos; +} +void OLEDDisplayUi::setIndicatorDirection(IndicatorDirection dir) { + this->indicatorDirection = dir; +} +void OLEDDisplayUi::setActiveSymbol(const uint8_t* symbol) { + this->activeSymbol = symbol; +} +void OLEDDisplayUi::setInactiveSymbol(const uint8_t* symbol) { + this->inactiveSymbol = symbol; +} + + +// -/----- Frame settings -----\- +void OLEDDisplayUi::setFrameAnimation(AnimationDirection dir) { + this->frameAnimationDirection = dir; +} +void OLEDDisplayUi::setFrames(FrameCallback* frameFunctions, uint8_t frameCount) { + this->frameFunctions = frameFunctions; + this->frameCount = frameCount; + this->resetState(); +} + +// -/----- Overlays ------\- +void OLEDDisplayUi::setOverlays(OverlayCallback* overlayFunctions, uint8_t overlayCount){ + this->overlayFunctions = overlayFunctions; + this->overlayCount = overlayCount; +} + +// -/----- Loading Process -----\- + +void OLEDDisplayUi::setLoadingDrawFunction(LoadingDrawFunction loadingDrawFunction) { + this->loadingDrawFunction = loadingDrawFunction; +} + +void OLEDDisplayUi::runLoadingProcess(LoadingStage* stages, uint8_t stagesCount) { + uint8_t progress = 0; + uint8_t increment = 100 / stagesCount; + + for (uint8_t i = 0; i < stagesCount; i++) { + display->clear(); + this->loadingDrawFunction(this->display, &stages[i], progress); + display->display(); + + stages[i].callback(); + + progress += increment; + yield(); + } + + display->clear(); + this->loadingDrawFunction(this->display, &stages[stagesCount-1], progress); + display->display(); + + delay(150); +} + +// -/----- Manuel control -----\- +void OLEDDisplayUi::nextFrame() { + if (this->state.frameState != IN_TRANSITION) { + this->state.manuelControll = true; + this->state.frameState = IN_TRANSITION; + this->state.ticksSinceLastStateSwitch = 0; + this->lastTransitionDirection = this->state.frameTransitionDirection; + this->state.frameTransitionDirection = 1; + } +} +void OLEDDisplayUi::previousFrame() { + if (this->state.frameState != IN_TRANSITION) { + this->state.manuelControll = true; + this->state.frameState = IN_TRANSITION; + this->state.ticksSinceLastStateSwitch = 0; + this->lastTransitionDirection = this->state.frameTransitionDirection; + this->state.frameTransitionDirection = -1; + } +} + +void OLEDDisplayUi::switchToFrame(uint8_t frame) { + if (frame >= this->frameCount) return; + this->state.ticksSinceLastStateSwitch = 0; + if (frame == this->state.currentFrame) return; + this->state.frameState = FIXED; + this->state.currentFrame = frame; + this->state.isIndicatorDrawen = true; +} + +void OLEDDisplayUi::transitionToFrame(uint8_t frame) { + if (frame >= this->frameCount) return; + this->state.ticksSinceLastStateSwitch = 0; + if (frame == this->state.currentFrame) return; + this->nextFrameNumber = frame; + this->lastTransitionDirection = this->state.frameTransitionDirection; + this->state.manuelControll = true; + this->state.frameState = IN_TRANSITION; + this->state.frameTransitionDirection = frame < this->state.currentFrame ? -1 : 1; +} + + +// -/----- State information -----\- +OLEDDisplayUiState* OLEDDisplayUi::getUiState(){ + return &this->state; +} + +int16_t OLEDDisplayUi::update(){ +#ifdef ARDUINO + unsigned long frameStart = millis(); +#elif __MBED__ + Timer t; + t.start(); + unsigned long frameStart = t.read_ms(); +#else +#error "Unkown operating system" +#endif + int32_t timeBudget = this->updateInterval - (frameStart - this->state.lastUpdate); + if ( timeBudget <= 0) { + // Implement frame skipping to ensure time budget is keept + if (this->autoTransition && this->state.lastUpdate != 0) this->state.ticksSinceLastStateSwitch += ceil((double)-timeBudget / (double)this->updateInterval); + + this->state.lastUpdate = frameStart; + this->tick(); + } +#ifdef ARDUINO + return this->updateInterval - (millis() - frameStart); +#elif __MBED__ + return this->updateInterval - (t.read_ms() - frameStart); +#else +#error "Unkown operating system" +#endif +} + + +void OLEDDisplayUi::tick() { + this->state.ticksSinceLastStateSwitch++; + + switch (this->state.frameState) { + case IN_TRANSITION: + if (this->state.ticksSinceLastStateSwitch >= this->ticksPerTransition){ + this->state.frameState = FIXED; + this->state.currentFrame = getNextFrameNumber(); + this->state.ticksSinceLastStateSwitch = 0; + this->nextFrameNumber = -1; + } + break; + case FIXED: + // Revert manuelControll + if (this->state.manuelControll) { + this->state.frameTransitionDirection = this->lastTransitionDirection; + this->state.manuelControll = false; + } + if (this->state.ticksSinceLastStateSwitch >= this->ticksPerFrame){ + if (this->autoTransition){ + this->state.frameState = IN_TRANSITION; + } + this->state.ticksSinceLastStateSwitch = 0; + } + break; + } + + this->display->clear(); + this->drawFrame(); + if (shouldDrawIndicators) { + this->drawIndicator(); + } + this->drawOverlays(); + this->display->display(); +} + +void OLEDDisplayUi::resetState() { + this->state.lastUpdate = 0; + this->state.ticksSinceLastStateSwitch = 0; + this->state.frameState = FIXED; + this->state.currentFrame = 0; + this->state.isIndicatorDrawen = true; +} + +void OLEDDisplayUi::drawFrame(){ + switch (this->state.frameState){ + case IN_TRANSITION: { + float progress = (float) this->state.ticksSinceLastStateSwitch / (float) this->ticksPerTransition; + int16_t x = 0, y = 0, x1 = 0, y1 = 0; + switch(this->frameAnimationDirection){ + case SLIDE_LEFT: + x = -this->display->width() * progress; + y = 0; + x1 = x + this->display->width(); + y1 = 0; + break; + case SLIDE_RIGHT: + x = this->display->width() * progress; + y = 0; + x1 = x - this->display->width(); + y1 = 0; + break; + case SLIDE_UP: + x = 0; + y = -this->display->height() * progress; + x1 = 0; + y1 = y + this->display->height(); + break; + case SLIDE_DOWN: + default: + x = 0; + y = this->display->height() * progress; + x1 = 0; + y1 = y - this->display->height(); + break; + } + + // Invert animation if direction is reversed. + int8_t dir = this->state.frameTransitionDirection >= 0 ? 1 : -1; + x *= dir; y *= dir; x1 *= dir; y1 *= dir; + + bool drawenCurrentFrame; + + + // Prope each frameFunction for the indicator Drawen state + this->enableIndicator(); + (this->frameFunctions[this->state.currentFrame])(this->display, &this->state, x, y); + drawenCurrentFrame = this->state.isIndicatorDrawen; + + this->enableIndicator(); + (this->frameFunctions[this->getNextFrameNumber()])(this->display, &this->state, x1, y1); + + // Build up the indicatorDrawState + if (drawenCurrentFrame && !this->state.isIndicatorDrawen) { + // Drawen now but not next + this->indicatorDrawState = 2; + } else if (!drawenCurrentFrame && this->state.isIndicatorDrawen) { + // Not drawen now but next + this->indicatorDrawState = 1; + } else if (!drawenCurrentFrame && !this->state.isIndicatorDrawen) { + // Not drawen in both frames + this->indicatorDrawState = 3; + } + + // If the indicator isn't draw in the current frame + // reflect it in state.isIndicatorDrawen + if (!drawenCurrentFrame) this->state.isIndicatorDrawen = false; + + break; + } + case FIXED: + // Always assume that the indicator is drawn! + // And set indicatorDrawState to "not known yet" + this->indicatorDrawState = 0; + this->enableIndicator(); + (this->frameFunctions[this->state.currentFrame])(this->display, &this->state, 0, 0); + break; + } +} + +void OLEDDisplayUi::drawIndicator() { + + // Only draw if the indicator is invisible + // for both frames or + // the indiactor is shown and we are IN_TRANSITION + if (this->indicatorDrawState == 3 || (!this->state.isIndicatorDrawen && this->state.frameState != IN_TRANSITION)) { + return; + } + + uint8_t posOfHighlightFrame = 0; + float indicatorFadeProgress = 0; + + // if the indicator needs to be slided in we want to + // highlight the next frame in the transition + uint8_t frameToHighlight = this->indicatorDrawState == 1 ? this->getNextFrameNumber() : this->state.currentFrame; + + // Calculate the frame that needs to be highlighted + // based on the Direction the indiactor is drawn + switch (this->indicatorDirection){ + case LEFT_RIGHT: + posOfHighlightFrame = frameToHighlight; + break; + case RIGHT_LEFT: + default: + posOfHighlightFrame = this->frameCount - frameToHighlight; + break; + } + + switch (this->indicatorDrawState) { + case 1: // Indicator was not drawn in this frame but will be in next + // Slide IN + indicatorFadeProgress = 1 - ((float) this->state.ticksSinceLastStateSwitch / (float) this->ticksPerTransition); + break; + case 2: // Indicator was drawn in this frame but not in next + // Slide OUT + indicatorFadeProgress = ((float) this->state.ticksSinceLastStateSwitch / (float) this->ticksPerTransition); + break; + } + + //Space between indicators - reduce for small screen sizes + uint16_t indicatorSpacing = 12; + if (this->display->getHeight() < 64 && (this->indicatorPosition == RIGHT || this->indicatorPosition == LEFT)) { + indicatorSpacing = 6; + } + + uint16_t frameStartPos = (indicatorSpacing * frameCount / 2); + const uint8_t *image; + + uint16_t x = 0,y = 0; + + + for (uint8_t i = 0; i < this->frameCount; i++) { + + switch (this->indicatorPosition){ + case TOP: + y = 0 - (8 * indicatorFadeProgress); + x = (this->display->width() / 2) - frameStartPos + 12 * i; + break; + case BOTTOM: + y = (this->display->height() - 8) + (8 * indicatorFadeProgress); + x = (this->display->width() / 2) - frameStartPos + 12 * i; + break; + case RIGHT: + x = (this->display->width() - 8) + (8 * indicatorFadeProgress); + y = (this->display->height() / 2) - frameStartPos + 2 + 12 * i; + break; + case LEFT: + default: + x = 0 - (8 * indicatorFadeProgress); + y = (this->display->height() / 2) - frameStartPos + 2 + indicatorSpacing * i; + break; + } + + if (posOfHighlightFrame == i) { + image = this->activeSymbol; + } else { + image = this->inactiveSymbol; + } + + this->display->drawFastImage(x, y, 8, 8, image); + } +} + +void OLEDDisplayUi::drawOverlays() { + for (uint8_t i=0;ioverlayCount;i++){ + (this->overlayFunctions[i])(this->display, &this->state); + } +} + +uint8_t OLEDDisplayUi::getNextFrameNumber(){ + if (this->nextFrameNumber != -1) return this->nextFrameNumber; + return (this->state.currentFrame + this->frameCount + this->state.frameTransitionDirection) % this->frameCount; +} diff --git a/lib/esp8266-oled-ssd1306-master/src/OLEDDisplayUi.h b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplayUi.h new file mode 100644 index 00000000..9aa3f320 --- /dev/null +++ b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplayUi.h @@ -0,0 +1,315 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn + * Copyright (c) 2018 by Fabrice Weinberg + * Copyright (c) 2019 by Helmut Tschemernjak - www.radioshuttle.de + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * ThingPulse invests considerable time and money to develop these open source libraries. + * Please support us by buying our products (and not the clones) from + * https://thingpulse.com + * + */ + +#ifndef OLEDDISPLAYUI_h +#define OLEDDISPLAYUI_h + +#ifdef ARDUINO +#include +#elif __MBED__ +#include +#else +#error "Unkown operating system" +#endif + +#include "OLEDDisplay.h" + +//#define DEBUG_OLEDDISPLAYUI(...) Serial.printf( __VA_ARGS__ ) + +#ifndef DEBUG_OLEDDISPLAYUI +#define DEBUG_OLEDDISPLAYUI(...) +#endif + +enum AnimationDirection { + SLIDE_UP, + SLIDE_DOWN, + SLIDE_LEFT, + SLIDE_RIGHT +}; + +enum IndicatorPosition { + TOP, + RIGHT, + BOTTOM, + LEFT +}; + +enum IndicatorDirection { + LEFT_RIGHT, + RIGHT_LEFT +}; + +enum FrameState { + IN_TRANSITION, + FIXED +}; + + +const uint8_t ANIMATION_activeSymbol[] PROGMEM = { + 0x00, 0x18, 0x3c, 0x7e, 0x7e, 0x3c, 0x18, 0x00 +}; + +const uint8_t ANIMATION_inactiveSymbol[] PROGMEM = { + 0x00, 0x0, 0x0, 0x18, 0x18, 0x0, 0x0, 0x00 +}; + + +// Structure of the UiState +struct OLEDDisplayUiState { + uint64_t lastUpdate; + uint16_t ticksSinceLastStateSwitch; + + FrameState frameState; + uint8_t currentFrame; + + bool isIndicatorDrawen; + + // Normal = 1, Inverse = -1; + int8_t frameTransitionDirection; + + bool manuelControll; + + // Custom data that can be used by the user + void* userData; +}; + +struct LoadingStage { + const char* process; + void (*callback)(); +}; + +typedef void (*FrameCallback)(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y); +typedef void (*OverlayCallback)(OLEDDisplay *display, OLEDDisplayUiState* state); +typedef void (*LoadingDrawFunction)(OLEDDisplay *display, LoadingStage* stage, uint8_t progress); + +class OLEDDisplayUi { + private: + OLEDDisplay *display; + + // Symbols for the Indicator + IndicatorPosition indicatorPosition; + IndicatorDirection indicatorDirection; + + const uint8_t* activeSymbol; + const uint8_t* inactiveSymbol; + + bool shouldDrawIndicators; + + // Values for the Frames + AnimationDirection frameAnimationDirection; + + int8_t lastTransitionDirection; + + uint16_t ticksPerFrame; // ~ 5000ms at 30 FPS + uint16_t ticksPerTransition; // ~ 500ms at 30 FPS + + bool autoTransition; + + FrameCallback* frameFunctions; + uint8_t frameCount; + + // Internally used to transition to a specific frame + int8_t nextFrameNumber; + + // Values for Overlays + OverlayCallback* overlayFunctions; + uint8_t overlayCount; + + // Will the Indicator be drawen + // 3 Not drawn in both frames + // 2 Drawn this frame but not next + // 1 Not drown this frame but next + // 0 Not known yet + uint8_t indicatorDrawState; + + // Loading screen + LoadingDrawFunction loadingDrawFunction; + + // UI State + OLEDDisplayUiState state; + + // Bookeeping for update + uint16_t updateInterval = 33; + + uint16_t timePerFrame; + uint16_t timePerTransition; + + uint8_t getNextFrameNumber(); + void drawIndicator(); + void drawFrame(); + void drawOverlays(); + void tick(); + void resetState(); + + public: + + OLEDDisplayUi(OLEDDisplay *display); + + /** + * Initialise the display + */ + void init(); + + /** + * Configure the internal used target FPS + */ + void setTargetFPS(uint8_t fps); + + // Automatic Controll + /** + * Enable automatic transition to next frame after the some time can be configured with `setTimePerFrame` and `setTimePerTransition`. + */ + void enableAutoTransition(); + + /** + * Disable automatic transition to next frame. + */ + void disableAutoTransition(); + + /** + * Set the direction if the automatic transitioning + */ + void setAutoTransitionForwards(); + void setAutoTransitionBackwards(); + + /** + * Set the approx. time a frame is displayed + */ + void setTimePerFrame(uint16_t time); + + /** + * Set the approx. time a transition will take + */ + void setTimePerTransition(uint16_t time); + + // Customize indicator position and style + + /** + * Draw the indicator. + * This is the defaut state for all frames if + * the indicator was hidden on the previous frame + * it will be slided in. + */ + void enableIndicator(); + + /** + * Don't draw the indicator. + * This will slide out the indicator + * when transitioning to the next frame. + */ + void disableIndicator(); + + /** + * Enable drawing of indicators + */ + void enableAllIndicators(); + + /** + * Disable draw of indicators. + */ + void disableAllIndicators(); + + /** + * Set the position of the indicator bar. + */ + void setIndicatorPosition(IndicatorPosition pos); + + /** + * Set the direction of the indicator bar. Defining the order of frames ASCENDING / DESCENDING + */ + void setIndicatorDirection(IndicatorDirection dir); + + /** + * Set the symbol to indicate an active frame in the indicator bar. + */ + void setActiveSymbol(const uint8_t* symbol); + + /** + * Set the symbol to indicate an inactive frame in the indicator bar. + */ + void setInactiveSymbol(const uint8_t* symbol); + + + // Frame settings + + /** + * Configure what animation is used to transition from one frame to another + */ + void setFrameAnimation(AnimationDirection dir); + + /** + * Add frame drawing functions + */ + void setFrames(FrameCallback* frameFunctions, uint8_t frameCount); + + // Overlay + + /** + * Add overlays drawing functions that are draw independent of the Frames + */ + void setOverlays(OverlayCallback* overlayFunctions, uint8_t overlayCount); + + + // Loading animation + /** + * Set the function that will draw each step + * in the loading animation + */ + void setLoadingDrawFunction(LoadingDrawFunction loadingFunction); + + + /** + * Run the loading process + */ + void runLoadingProcess(LoadingStage* stages, uint8_t stagesCount); + + + // Manual Control + void nextFrame(); + void previousFrame(); + + /** + * Switch without transition to frame `frame`. + */ + void switchToFrame(uint8_t frame); + + /** + * Transition to frame `frame`, when the `frame` number is bigger than the current + * frame the forward animation will be used, otherwise the backwards animation is used. + */ + void transitionToFrame(uint8_t frame); + + // State Info + OLEDDisplayUiState* getUiState(); + + int16_t update(); +}; +#endif diff --git a/lib/esp8266-oled-ssd1306-master/src/SH1106.h b/lib/esp8266-oled-ssd1306-master/src/SH1106.h new file mode 100644 index 00000000..47188d1f --- /dev/null +++ b/lib/esp8266-oled-ssd1306-master/src/SH1106.h @@ -0,0 +1,39 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn + * Copyright (c) 2018 by Fabrice Weinberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * ThingPulse invests considerable time and money to develop these open source libraries. + * Please support us by buying our products (and not the clones) from + * https://thingpulse.com + * + */ + +#ifndef SH1106_h +#define SH1106_h +#include "SH1106Wire.h" + +// For make SH1106 an alias for SH1106Wire +typedef SH1106Wire SH1106; + + +#endif diff --git a/lib/esp8266-oled-ssd1306-master/src/SH1106Brzo.h b/lib/esp8266-oled-ssd1306-master/src/SH1106Brzo.h new file mode 100644 index 00000000..be9c0c74 --- /dev/null +++ b/lib/esp8266-oled-ssd1306-master/src/SH1106Brzo.h @@ -0,0 +1,141 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn + * Copyright (c) 2018 by Fabrice Weinberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * ThingPulse invests considerable time and money to develop these open source libraries. + * Please support us by buying our products (and not the clones) from + * https://thingpulse.com + * + */ + +#ifndef SH1106Brzo_h +#define SH1106Brzo_h + +#include "OLEDDisplay.h" +#include + +#if F_CPU == 160000000L + #define BRZO_I2C_SPEED 1000 +#else + #define BRZO_I2C_SPEED 800 +#endif + +class SH1106Brzo : public OLEDDisplay { + private: + uint8_t _address; + uint8_t _sda; + uint8_t _scl; + + public: + SH1106Brzo(uint8_t _address, uint8_t _sda, uint8_t _scl, OLEDDISPLAY_GEOMETRY g = GEOMETRY_128_64) { + setGeometry(g); + + this->_address = _address; + this->_sda = _sda; + this->_scl = _scl; + } + + bool connect(){ + brzo_i2c_setup(_sda, _scl, 0); + return true; + } + + void display(void) { + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + uint8_t minBoundY = UINT8_MAX; + uint8_t maxBoundY = 0; + + uint8_t minBoundX = UINT8_MAX; + uint8_t maxBoundX = 0; + uint8_t x, y; + + // Calculate the Y bounding box of changes + // and copy buffer[pos] to buffer_back[pos]; + for (y = 0; y < (displayHeight / 8); y++) { + for (x = 0; x < displayWidth; x++) { + uint16_t pos = x + y * displayWidth; + if (buffer[pos] != buffer_back[pos]) { + minBoundY = _min(minBoundY, y); + maxBoundY = _max(maxBoundY, y); + minBoundX = _min(minBoundX, x); + maxBoundX = _max(maxBoundX, x); + } + buffer_back[pos] = buffer[pos]; + } + yield(); + } + + // If the minBoundY wasn't updated + // we can savely assume that buffer_back[pos] == buffer[pos] + // holdes true for all values of pos + if (minBoundY == UINT8_MAX) return; + + byte k = 0; + uint8_t sendBuffer[17]; + sendBuffer[0] = 0x40; + + // Calculate the colum offset + uint8_t minBoundXp2H = (minBoundX + 2) & 0x0F; + uint8_t minBoundXp2L = 0x10 | ((minBoundX + 2) >> 4 ); + + brzo_i2c_start_transaction(this->_address, BRZO_I2C_SPEED); + + for (y = minBoundY; y <= maxBoundY; y++) { + sendCommand(0xB0 + y); + sendCommand(minBoundXp2H); + sendCommand(minBoundXp2L); + for (x = minBoundX; x <= maxBoundX; x++) { + k++; + sendBuffer[k] = buffer[x + y * displayWidth]; + if (k == 16) { + brzo_i2c_write(sendBuffer, 17, true); + k = 0; + } + } + if (k != 0) { + brzo_i2c_write(sendBuffer, k + 1, true); + k = 0; + } + yield(); + } + if (k != 0) { + brzo_i2c_write(sendBuffer, k + 1, true); + } + brzo_i2c_end_transaction(); + #else + #endif + } + + private: + int getBufferOffset(void) { + return 0; + } + inline void sendCommand(uint8_t com) __attribute__((always_inline)){ + uint8_t command[2] = {0x80 /* command mode */, com}; + brzo_i2c_start_transaction(_address, BRZO_I2C_SPEED); + brzo_i2c_write(command, 2, true); + brzo_i2c_end_transaction(); + } +}; + +#endif diff --git a/lib/esp8266-oled-ssd1306-master/src/SH1106Spi.h b/lib/esp8266-oled-ssd1306-master/src/SH1106Spi.h new file mode 100644 index 00000000..23693bc4 --- /dev/null +++ b/lib/esp8266-oled-ssd1306-master/src/SH1106Spi.h @@ -0,0 +1,135 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn + * Copyright (c) 2018 by Fabrice Weinberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * ThingPulse invests considerable time and money to develop these open source libraries. + * Please support us by buying our products (and not the clones) from + * https://thingpulse.com + * + */ + +#ifndef SH1106Spi_h +#define SH1106Spi_h + +#include "OLEDDisplay.h" +#include + +class SH1106Spi : public OLEDDisplay { + private: + uint8_t _rst; + uint8_t _dc; + + public: + SH1106Spi(uint8_t _rst, uint8_t _dc, uint8_t _cs, OLEDDISPLAY_GEOMETRY g = GEOMETRY_128_64) { + setGeometry(g); + + this->_rst = _rst; + this->_dc = _dc; + } + + bool connect(){ + pinMode(_dc, OUTPUT); + pinMode(_rst, OUTPUT); + + SPI.begin (); + SPI.setClockDivider (SPI_CLOCK_DIV2); + + // Pulse Reset low for 10ms + digitalWrite(_rst, HIGH); + delay(1); + digitalWrite(_rst, LOW); + delay(10); + digitalWrite(_rst, HIGH); + return true; + } + + void display(void) { + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + uint8_t minBoundY = UINT8_MAX; + uint8_t maxBoundY = 0; + + uint8_t minBoundX = UINT8_MAX; + uint8_t maxBoundX = 0; + + uint8_t x, y; + + // Calculate the Y bounding box of changes + // and copy buffer[pos] to buffer_back[pos]; + for (y = 0; y < (displayHeight / 8); y++) { + for (x = 0; x < displayWidth; x++) { + uint16_t pos = x + y * displayWidth; + if (buffer[pos] != buffer_back[pos]) { + minBoundY = _min(minBoundY, y); + maxBoundY = _max(maxBoundY, y); + minBoundX = _min(minBoundX, x); + maxBoundX = _max(maxBoundX, x); + } + buffer_back[pos] = buffer[pos]; + } + yield(); + } + + // If the minBoundY wasn't updated + // we can savely assume that buffer_back[pos] == buffer[pos] + // holdes true for all values of pos + if (minBoundY == UINT8_MAX) return; + + // Calculate the colum offset + uint8_t minBoundXp2H = (minBoundX + 2) & 0x0F; + uint8_t minBoundXp2L = 0x10 | ((minBoundX + 2) >> 4 ); + + for (y = minBoundY; y <= maxBoundY; y++) { + sendCommand(0xB0 + y); + sendCommand(minBoundXp2H); + sendCommand(minBoundXp2L); + digitalWrite(_dc, HIGH); // data mode + for (x = minBoundX; x <= maxBoundX; x++) { + SPI.transfer(buffer[x + y * displayWidth]); + } + yield(); + } + #else + for (uint8_t y=0; y + +#define SH1106_SET_PUMP_VOLTAGE 0X30 +#define SH1106_SET_PUMP_MODE 0XAD +#define SH1106_PUMP_ON 0X8B +#define SH1106_PUMP_OFF 0X8A +//-------------------------------------- + +class SH1106Wire : public OLEDDisplay { + private: + uint8_t _address; + uint8_t _sda; + uint8_t _scl; + bool _doI2cAutoInit = false; + TwoWire* _wire = NULL; + int _frequency; + + public: + /** + * Create and initialize the Display using Wire library + * + * Beware for retro-compatibility default values are provided for all parameters see below. + * Please note that if you don't wan't SD1306Wire to initialize and change frequency speed ot need to + * ensure -1 value are specified for all 3 parameters. This can be usefull to control TwoWire with multiple + * device on the same bus. + * + * @param _address I2C Display address + * @param _sda I2C SDA pin number, default to -1 to skip Wire begin call + * @param _scl I2C SCL pin number, default to -1 (only SDA = -1 is considered to skip Wire begin call) + * @param g display geometry dafault to generic GEOMETRY_128_64, see OLEDDISPLAY_GEOMETRY definition for other options + * @param _i2cBus on ESP32 with 2 I2C HW buses, I2C_ONE for 1st Bus, I2C_TWO fot 2nd bus, default I2C_ONE + * @param _frequency for Frequency by default Let's use ~700khz if ESP8266 is in 160Mhz mode, this will be limited to ~400khz if the ESP8266 in 80Mhz mode + */ + SH1106Wire(uint8_t _address, uint8_t _sda, uint8_t _scl, OLEDDISPLAY_GEOMETRY g = GEOMETRY_128_64, HW_I2C _i2cBus = I2C_ONE, int _frequency = 700000) { + setGeometry(g); + + this->_address = _address; + this->_sda = _sda; + this->_scl = _scl; +#if !defined(ARDUINO_ARCH_ESP32) + this->_wire = &Wire; +#else + this->_wire = (_i2cBus==I2C_ONE) ? &Wire : &Wire1; +#endif + this->_frequency = _frequency; + } + + bool connect() { +#if !defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH8266) + _wire->begin(); +#else + // On ESP32 arduino, -1 means 'don't change pins', someone else has called begin for us. + if(this->_sda != -1) + _wire->begin(this->_sda, this->_scl); +#endif + // Let's use ~700khz if ESP8266 is in 160Mhz mode + // this will be limited to ~400khz if the ESP8266 in 80Mhz mode. + if(this->_frequency != -1) + _wire->setClock(this->_frequency); + return true; + } + + void display(void) { + initI2cIfNeccesary(); + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + uint8_t minBoundY = UINT8_MAX; + uint8_t maxBoundY = 0; + + uint8_t minBoundX = UINT8_MAX; + uint8_t maxBoundX = 0; + + uint8_t x, y; + + // Calculate the Y bounding box of changes + // and copy buffer[pos] to buffer_back[pos]; + for (y = 0; y < (displayHeight / 8); y++) { + for (x = 0; x < displayWidth; x++) { + uint16_t pos = x + y * displayWidth; + if (buffer[pos] != buffer_back[pos]) { + minBoundY = _min(minBoundY, y); + maxBoundY = _max(maxBoundY, y); + minBoundX = _min(minBoundX, x); + maxBoundX = _max(maxBoundX, x); + } + buffer_back[pos] = buffer[pos]; + } + yield(); + } + + // If the minBoundY wasn't updated + // we can savely assume that buffer_back[pos] == buffer[pos] + // holdes true for all values of pos + if (minBoundY == UINT8_MAX) return; + + // Calculate the colum offset + uint8_t minBoundXp2H = (minBoundX + 2) & 0x0F; + uint8_t minBoundXp2L = 0x10 | ((minBoundX + 2) >> 4 ); + + byte k = 0; + for (y = minBoundY; y <= maxBoundY; y++) { + sendCommand(0xB0 + y); + sendCommand(minBoundXp2H); + sendCommand(minBoundXp2L); + for (x = minBoundX; x <= maxBoundX; x++) { + if (k == 0) { + _wire->beginTransmission(_address); + _wire->write(0x40); + } + _wire->write(buffer[x + y * displayWidth]); + k++; + if (k == 16) { + _wire->endTransmission(); + k = 0; + } + } + if (k != 0) { + _wire->endTransmission(); + k = 0; + } + yield(); + } + + if (k != 0) { + _wire->endTransmission(); + } + #else + uint8_t * p = &buffer[0]; + for (uint8_t y=0; y<8; y++) { + sendCommand(0xB0+y); + sendCommand(0x02); + sendCommand(0x10); + for( uint8_t x=0; x<8; x++) { + _wire->beginTransmission(_address); + _wire->write(0x40); + for (uint8_t k = 0; k < 16; k++) { + _wire->write(*p++); + } + _wire->endTransmission(); + } + } + #endif + } + + void setI2cAutoInit(bool doI2cAutoInit) { + _doI2cAutoInit = doI2cAutoInit; + } + + private: + int getBufferOffset(void) { + return 0; + } + inline void sendCommand(uint8_t command) __attribute__((always_inline)){ + _wire->beginTransmission(_address); + _wire->write(0x80); + _wire->write(command); + _wire->endTransmission(); + } + + void initI2cIfNeccesary() { + if (_doI2cAutoInit) { +#ifdef ARDUINO_ARCH_AVR + _wire->begin(); +#else + _wire->begin(this->_sda, this->_scl); +#endif + } + } + +}; + +#endif diff --git a/lib/esp8266-oled-ssd1306-master/src/SSD1306.h b/lib/esp8266-oled-ssd1306-master/src/SSD1306.h new file mode 100644 index 00000000..f6bd554c --- /dev/null +++ b/lib/esp8266-oled-ssd1306-master/src/SSD1306.h @@ -0,0 +1,39 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn + * Copyright (c) 2018 by Fabrice Weinberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * ThingPulse invests considerable time and money to develop these open source libraries. + * Please support us by buying our products (and not the clones) from + * https://thingpulse.com + * + */ + +#ifndef SSD1306_h +#define SSD1306_h +#include "SSD1306Wire.h" + +// For legacy support make SSD1306 an alias for SSD1306 +typedef SSD1306Wire SSD1306; + + +#endif diff --git a/lib/esp8266-oled-ssd1306-master/src/SSD1306Brzo.h b/lib/esp8266-oled-ssd1306-master/src/SSD1306Brzo.h new file mode 100644 index 00000000..fbcffcda --- /dev/null +++ b/lib/esp8266-oled-ssd1306-master/src/SSD1306Brzo.h @@ -0,0 +1,167 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn + * Copyright (c) 2018 by Fabrice Weinberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * ThingPulse invests considerable time and money to develop these open source libraries. + * Please support us by buying our products (and not the clones) from + * https://thingpulse.com + * + */ + +#ifndef SSD1306Brzo_h +#define SSD1306Brzo_h + +#include "OLEDDisplay.h" +#include + +#if F_CPU == 160000000L + #define BRZO_I2C_SPEED 1000 +#else + #define BRZO_I2C_SPEED 800 +#endif + +class SSD1306Brzo : public OLEDDisplay { + private: + uint8_t _address; + uint8_t _sda; + uint8_t _scl; + + public: + SSD1306Brzo(uint8_t _address, uint8_t _sda, uint8_t _scl, OLEDDISPLAY_GEOMETRY g = GEOMETRY_128_64) { + setGeometry(g); + + this->_address = _address; + this->_sda = _sda; + this->_scl = _scl; + } + + bool connect(){ + brzo_i2c_setup(_sda, _scl, 0); + return true; + } + + void display(void) { + const int x_offset = (128 - this->width()) / 2; + + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + uint8_t minBoundY = UINT8_MAX; + uint8_t maxBoundY = 0; + + uint8_t minBoundX = UINT8_MAX; + uint8_t maxBoundX = 0; + + uint8_t x, y; + + // Calculate the Y bounding box of changes + // and copy buffer[pos] to buffer_back[pos]; + for (y = 0; y < (this->height() / 8); y++) { + for (x = 0; x < this->width(); x++) { + uint16_t pos = x + y * this->width(); + if (buffer[pos] != buffer_back[pos]) { + minBoundY = _min(minBoundY, y); + maxBoundY = _max(maxBoundY, y); + minBoundX = _min(minBoundX, x); + maxBoundX = _max(maxBoundX, x); + } + buffer_back[pos] = buffer[pos]; + } + yield(); + } + + // If the minBoundY wasn't updated + // we can savely assume that buffer_back[pos] == buffer[pos] + // holdes true for all values of pos + if (minBoundY == UINT8_MAX) return; + + sendCommand(COLUMNADDR); + sendCommand(x_offset + minBoundX); + sendCommand(x_offset + maxBoundX); + + sendCommand(PAGEADDR); + sendCommand(minBoundY); + sendCommand(maxBoundY); + + byte k = 0; + + int buflen = ( this->width() / 8 ) + 1; + + uint8_t sendBuffer[buflen]; + sendBuffer[0] = 0x40; + brzo_i2c_start_transaction(this->_address, BRZO_I2C_SPEED); + for (y = minBoundY; y <= maxBoundY; y++) { + for (x = minBoundX; x <= maxBoundX; x++) { + k++; + sendBuffer[k] = buffer[x + y * this->width()]; + if (k == (buflen-1)) { + brzo_i2c_write(sendBuffer, buflen, true); + k = 0; + } + } + yield(); + } + brzo_i2c_write(sendBuffer, k + 1, true); + brzo_i2c_end_transaction(); + #else + // No double buffering + sendCommand(COLUMNADDR); + + sendCommand(x_offset); + sendCommand(x_offset + (this->width() - 1)); + + sendCommand(PAGEADDR); + sendCommand(0x0); + sendCommand((this->height() / 8) - 1); + + int buflen = ( this->width() / 8 ) + 1; + + uint8_t sendBuffer[buflen]; + sendBuffer[0] = 0x40; + + brzo_i2c_start_transaction(this->_address, BRZO_I2C_SPEED); + + for (uint16_t i=0; i + +#ifndef UINT8_MAX + #define UINT8_MAX 0xff +#endif + +class SSD1306I2C : public OLEDDisplay { +public: + SSD1306I2C(uint8_t _address, PinName _sda, PinName _scl, OLEDDISPLAY_GEOMETRY g = GEOMETRY_128_64) { + setGeometry(g); + + this->_address = _address << 1; // convert from 7 to 8 bit for mbed. + this->_sda = _sda; + this->_scl = _scl; + _i2c = new I2C(_sda, _scl); + } + + bool connect() { + // mbed supports 100k and 400k some device maybe 1000k +#ifdef TARGET_STM32L4 + _i2c->frequency(1000000); +#else + _i2c->frequency(400000); +#endif + return true; + } + + void display(void) { + const int x_offset = (128 - this->width()) / 2; +#ifdef OLEDDISPLAY_DOUBLE_BUFFER + uint8_t minBoundY = UINT8_MAX; + uint8_t maxBoundY = 0; + + uint8_t minBoundX = UINT8_MAX; + uint8_t maxBoundX = 0; + uint8_t x, y; + + // Calculate the Y bounding box of changes + // and copy buffer[pos] to buffer_back[pos]; + for (y = 0; y < (this->height() / 8); y++) { + for (x = 0; x < this->width(); x++) { + uint16_t pos = x + y * this->width(); + if (buffer[pos] != buffer_back[pos]) { + minBoundY = std::min(minBoundY, y); + maxBoundY = std::max(maxBoundY, y); + minBoundX = std::min(minBoundX, x); + maxBoundX = std::max(maxBoundX, x); + } + buffer_back[pos] = buffer[pos]; + } + yield(); + } + + // If the minBoundY wasn't updated + // we can savely assume that buffer_back[pos] == buffer[pos] + // holdes true for all values of pos + + if (minBoundY == UINT8_MAX) return; + + sendCommand(COLUMNADDR); + sendCommand(x_offset + minBoundX); // column start address (0 = reset) + sendCommand(x_offset + maxBoundX); // column end address (127 = reset) + + sendCommand(PAGEADDR); + sendCommand(minBoundY); // page start address + sendCommand(maxBoundY); // page end address + + for (y = minBoundY; y <= maxBoundY; y++) { + uint8_t *start = &buffer[(minBoundX + y * this->width())-1]; + uint8_t save = *start; + + *start = 0x40; // control + _i2c->write(_address, (char *)start, (maxBoundX-minBoundX) + 1 + 1); + *start = save; + } +#else + + sendCommand(COLUMNADDR); + sendCommand(x_offset); // column start address (0 = reset) + sendCommand(x_offset + (this->width() - 1));// column end address (127 = reset) + + sendCommand(PAGEADDR); + sendCommand(0x0); // page start address (0 = reset) + + if (geometry == GEOMETRY_128_64) { + sendCommand(0x7); + } else if (geometry == GEOMETRY_128_32) { + sendCommand(0x3); + } + + buffer[-1] = 0x40; // control + _i2c->write(_address, (char *)&buffer[-1], displayBufferSize + 1); +#endif + } + +private: + int getBufferOffset(void) { + return 0; + } + + inline void sendCommand(uint8_t command) __attribute__((always_inline)) { + char _data[2]; + _data[0] = 0x80; // control + _data[1] = command; + _i2c->write(_address, _data, sizeof(_data)); + } + + uint8_t _address; + PinName _sda; + PinName _scl; + I2C *_i2c; +}; + +#endif + +#endif diff --git a/lib/esp8266-oled-ssd1306-master/src/SSD1306Spi.h b/lib/esp8266-oled-ssd1306-master/src/SSD1306Spi.h new file mode 100644 index 00000000..08cb4a80 --- /dev/null +++ b/lib/esp8266-oled-ssd1306-master/src/SSD1306Spi.h @@ -0,0 +1,163 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn + * Copyright (c) 2018 by Fabrice Weinberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * ThingPulse invests considerable time and money to develop these open source libraries. + * Please support us by buying our products (and not the clones) from + * https://thingpulse.com + * + */ + +#ifndef SSD1306Spi_h +#define SSD1306Spi_h + +#include "OLEDDisplay.h" +#include + +#if F_CPU == 160000000L + #define BRZO_I2C_SPEED 1000 +#else + #define BRZO_I2C_SPEED 800 +#endif + +class SSD1306Spi : public OLEDDisplay { + private: + uint8_t _rst; + uint8_t _dc; + uint8_t _cs; + + public: + SSD1306Spi(uint8_t _rst, uint8_t _dc, uint8_t _cs, OLEDDISPLAY_GEOMETRY g = GEOMETRY_128_64) { + setGeometry(g); + + this->_rst = _rst; + this->_dc = _dc; + this->_cs = _cs; + } + + bool connect(){ + pinMode(_dc, OUTPUT); + pinMode(_cs, OUTPUT); + pinMode(_rst, OUTPUT); + + SPI.begin (); + SPI.setClockDivider (SPI_CLOCK_DIV2); + + // Pulse Reset low for 10ms + digitalWrite(_rst, HIGH); + delay(1); + digitalWrite(_rst, LOW); + delay(10); + digitalWrite(_rst, HIGH); + return true; + } + + void display(void) { + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + uint8_t minBoundY = UINT8_MAX; + uint8_t maxBoundY = 0; + + uint8_t minBoundX = UINT8_MAX; + uint8_t maxBoundX = 0; + + uint8_t x, y; + + // Calculate the Y bounding box of changes + // and copy buffer[pos] to buffer_back[pos]; + for (y = 0; y < (displayHeight / 8); y++) { + for (x = 0; x < displayWidth; x++) { + uint16_t pos = x + y * displayWidth; + if (buffer[pos] != buffer_back[pos]) { + minBoundY = _min(minBoundY, y); + maxBoundY = _max(maxBoundY, y); + minBoundX = _min(minBoundX, x); + maxBoundX = _max(maxBoundX, x); + } + buffer_back[pos] = buffer[pos]; + } + yield(); + } + + // If the minBoundY wasn't updated + // we can savely assume that buffer_back[pos] == buffer[pos] + // holdes true for all values of pos + if (minBoundY == UINT8_MAX) return; + + sendCommand(COLUMNADDR); + sendCommand(minBoundX); + sendCommand(maxBoundX); + + sendCommand(PAGEADDR); + sendCommand(minBoundY); + sendCommand(maxBoundY); + + digitalWrite(_cs, HIGH); + digitalWrite(_dc, HIGH); // data mode + digitalWrite(_cs, LOW); + for (y = minBoundY; y <= maxBoundY; y++) { + for (x = minBoundX; x <= maxBoundX; x++) { + SPI.transfer(buffer[x + y * displayWidth]); + } + yield(); + } + digitalWrite(_cs, HIGH); + #else + // No double buffering + sendCommand(COLUMNADDR); + sendCommand(0x0); + sendCommand(0x7F); + + sendCommand(PAGEADDR); + sendCommand(0x0); + + if (geometry == GEOMETRY_128_64 || geometry == GEOMETRY_64_48 || geometry == GEOMETRY_64_32 ) { + sendCommand(0x7); + } else if (geometry == GEOMETRY_128_32) { + sendCommand(0x3); + } + + digitalWrite(_cs, HIGH); + digitalWrite(_dc, HIGH); // data mode + digitalWrite(_cs, LOW); + for (uint16_t i=0; i +#include + +#if defined(ARDUINO_ARCH_AVR) || defined(ARDUINO_ARCH_STM32) +#define _min min +#define _max max +#endif +//-------------------------------------- + +class SSD1306Wire : public OLEDDisplay { + private: + uint8_t _address; + int _sda; + int _scl; + bool _doI2cAutoInit = false; + TwoWire* _wire = NULL; + int _frequency; + + public: + + /** + * Create and initialize the Display using Wire library + * + * Beware for retro-compatibility default values are provided for all parameters see below. + * Please note that if you don't wan't SD1306Wire to initialize and change frequency speed ot need to + * ensure -1 value are specified for all 3 parameters. This can be usefull to control TwoWire with multiple + * device on the same bus. + * + * @param _address I2C Display address + * @param _sda I2C SDA pin number, default to -1 to skip Wire begin call + * @param _scl I2C SCL pin number, default to -1 (only SDA = -1 is considered to skip Wire begin call) + * @param g display geometry dafault to generic GEOMETRY_128_64, see OLEDDISPLAY_GEOMETRY definition for other options + * @param _i2cBus on ESP32 with 2 I2C HW buses, I2C_ONE for 1st Bus, I2C_TWO fot 2nd bus, default I2C_ONE + * @param _frequency for Frequency by default Let's use ~700khz if ESP8266 is in 160Mhz mode, this will be limited to ~400khz if the ESP8266 in 80Mhz mode + */ + SSD1306Wire(uint8_t _address, int _sda = -1, int _scl = -1, OLEDDISPLAY_GEOMETRY g = GEOMETRY_128_64, HW_I2C _i2cBus = I2C_ONE, int _frequency = 700000) { + setGeometry(g); + + this->_address = _address; + this->_sda = _sda; + this->_scl = _scl; +#if !defined(ARDUINO_ARCH_ESP32) + this->_wire = &Wire; +#else + this->_wire = (_i2cBus==I2C_ONE) ? &Wire : &Wire1; +#endif + this->_frequency = _frequency; + } + + bool connect() { +#if !defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH8266) + _wire->begin(); +#else + // On ESP32 arduino, -1 means 'don't change pins', someone else has called begin for us. + if(this->_sda != -1) + _wire->begin(this->_sda, this->_scl); +#endif + // Let's use ~700khz if ESP8266 is in 160Mhz mode + // this will be limited to ~400khz if the ESP8266 in 80Mhz mode. + if(this->_frequency != -1) + _wire->setClock(this->_frequency); + return true; + } + + void display(void) { + initI2cIfNeccesary(); + const int x_offset = (128 - this->width()) / 2; + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + uint8_t minBoundY = UINT8_MAX; + uint8_t maxBoundY = 0; + + uint8_t minBoundX = UINT8_MAX; + uint8_t maxBoundX = 0; + uint8_t x, y; + + // Calculate the Y bounding box of changes + // and copy buffer[pos] to buffer_back[pos]; + for (y = 0; y < (this->height() / 8); y++) { + for (x = 0; x < this->width(); x++) { + uint16_t pos = x + y * this->width(); + if (buffer[pos] != buffer_back[pos]) { + minBoundY = std::min(minBoundY, y); + maxBoundY = std::max(maxBoundY, y); + minBoundX = std::min(minBoundX, x); + maxBoundX = std::max(maxBoundX, x); + } + buffer_back[pos] = buffer[pos]; + } + yield(); + } + + // If the minBoundY wasn't updated + // we can savely assume that buffer_back[pos] == buffer[pos] + // holdes true for all values of pos + + if (minBoundY == UINT8_MAX) return; + + sendCommand(COLUMNADDR); + sendCommand(x_offset + minBoundX); + sendCommand(x_offset + maxBoundX); + + sendCommand(PAGEADDR); + sendCommand(minBoundY); + sendCommand(maxBoundY); + + byte k = 0; + for (y = minBoundY; y <= maxBoundY; y++) { + for (x = minBoundX; x <= maxBoundX; x++) { + if (k == 0) { + _wire->beginTransmission(_address); + _wire->write(0x40); + } + + _wire->write(buffer[x + y * this->width()]); + k++; + if (k == 16) { + _wire->endTransmission(); + k = 0; + } + } + yield(); + } + + if (k != 0) { + _wire->endTransmission(); + } + #else + + sendCommand(COLUMNADDR); + sendCommand(x_offset); + sendCommand(x_offset + (this->width() - 1)); + + sendCommand(PAGEADDR); + sendCommand(0x0); + + for (uint16_t i=0; i < displayBufferSize; i++) { + _wire->beginTransmission(this->_address); + _wire->write(0x40); + for (uint8_t x = 0; x < 16; x++) { + _wire->write(buffer[i]); + i++; + } + i--; + _wire->endTransmission(); + } + #endif + } + + void setI2cAutoInit(bool doI2cAutoInit) { + _doI2cAutoInit = doI2cAutoInit; + } + + private: + int getBufferOffset(void) { + return 0; + } + inline void sendCommand(uint8_t command) __attribute__((always_inline)){ + initI2cIfNeccesary(); + _wire->beginTransmission(_address); + _wire->write(0x80); + _wire->write(command); + _wire->endTransmission(); + } + + void initI2cIfNeccesary() { + if (_doI2cAutoInit) { +#if !defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH8266) + _wire->begin(); +#else + _wire->begin(this->_sda, this->_scl); +#endif + } + } + +}; + +#endif diff --git a/src/SuplaCommonPROGMEM.h b/src/SuplaCommonPROGMEM.h index 0e0b6990..29eea492 100644 --- a/src/SuplaCommonPROGMEM.h +++ b/src/SuplaCommonPROGMEM.h @@ -111,7 +111,8 @@ const char* const CFG_MODE_P[] PROGMEM = {CFG_10_PRESSES, CFG_5SEK_HOLD}; #ifdef SUPLA_OLED const char SSD1306[] PROGMEM = "SSD1306 - 0,96''"; const char SH1106[] PROGMEM = "SH1106 - 1,3''"; -const char *const OLED_P[] PROGMEM = {OFF, SSD1306, SH1106}; +const char SSD1306_WEMOS_SHIELD[] PROGMEM = "SSD1306 - 0,66'' WEMOS OLED shield"; +const char *const OLED_P[] PROGMEM = {OFF, SSD1306, SH1106, SSD1306_WEMOS_SHIELD}; #endif String StateString(uint8_t adr); diff --git a/src/SuplaOled.cpp b/src/SuplaOled.cpp index 0dfa7c1d..70b9b328 100644 --- a/src/SuplaOled.cpp +++ b/src/SuplaOled.cpp @@ -103,8 +103,10 @@ void displayRelayState(OLEDDisplay* display) { } x += 15; } - display->setColor(WHITE); - display->drawHorizontalLine(0, 14, display->getWidth()); + if (!GEOMETRY_64_48) { + display->setColor(WHITE); + display->drawHorizontalLine(0, 14, display->getWidth()); + } } void msOverlay(OLEDDisplay* display, OLEDDisplayUiState* state) { @@ -149,45 +151,78 @@ void displayBlank(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, in void displayTemp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double temp, const String& name) { int drawHeightIcon = display->getHeight() / 2 - 10; - int drawStringIcon = display->getHeight() / 2 - 5; + int drawStringIcon = display->getHeight() / 2 - 6; + + int temp_width = TEMP_WIDTH; + int temp_height = TEMP_HEIGHT; + + if (GEOMETRY_64_48) { + temp_width = 0; + temp_height = 0; + } + else { + temp_width = temp_width + 10; + } display->setColor(WHITE); display->setTextAlignment(TEXT_ALIGN_LEFT); display->drawXbm(x + 0, y + drawHeightIcon, temp_width, temp_height, temp_bits); display->setFont(ArialMT_Plain_10); if (name != NULL) { - display->drawString(x + temp_width + 20, y + display->getHeight() / 2 - 15, name); + display->drawString(x + temp_width, y + display->getHeight() / 2 - 15, name); } display->setFont(ArialMT_Plain_24); - display->drawString(x + temp_width + 10, y + drawStringIcon, getTempString(temp)); + display->drawString(x + temp_width, y + drawStringIcon, getTempString(temp)); display->setFont(ArialMT_Plain_16); - display->drawString(x + temp_width + 10 + (getTempString(temp).length() * 12), y + drawStringIcon, "ºC"); + display->drawString(x + temp_width + (getTempString(temp).length() * 12), y + drawStringIcon, "ºC"); } void displaHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double humidity) { int drawHeightIcon = display->getHeight() / 2 - 10; - int drawStringIcon = display->getHeight() / 2 - 5; + int drawStringIcon = display->getHeight() / 2 - 6; + + int humidity_width = HUMIDITY_WIDTH; + int humidity_height = HUMIDITY_HEIGHT; + + if (GEOMETRY_64_48) { + humidity_width = 0; + humidity_height = 0; + } + else { + humidity_width = humidity_width + 10; + } display->setColor(WHITE); display->setTextAlignment(TEXT_ALIGN_LEFT); display->drawXbm(x + 0, y + drawHeightIcon, humidity_width, humidity_height, humidity_bits); display->setFont(ArialMT_Plain_24); - display->drawString(x + humidity_width + 20, y + drawStringIcon, getHumidityString(humidity)); + display->drawString(x + humidity_width, y + drawStringIcon, getHumidityString(humidity)); display->setFont(ArialMT_Plain_16); - display->drawString(x + humidity_width + 20 + (getHumidityString(humidity).length() * 12), y + drawStringIcon, "%"); + display->drawString(x + humidity_width + (getHumidityString(humidity).length() * 12), y + drawStringIcon, "%"); } void displayPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double pressure) { int drawHeightIcon = display->getHeight() / 2 - 10; - int drawStringIcon = display->getHeight() / 2 - 5; + int drawStringIcon = display->getHeight() / 2 - 6; + + int pressure_width = PRESSURE_WIDTH; + int pressure_height = PRESSURE_HEIGHT; + + if (GEOMETRY_64_48) { + pressure_width = 0; + pressure_height = 0; + } + else { + pressure_width = pressure_width + 15; + } display->setColor(WHITE); display->setTextAlignment(TEXT_ALIGN_LEFT); display->drawXbm(x + 0, y + drawHeightIcon, pressure_width, pressure_height, pressure_bits); display->setFont(ArialMT_Plain_24); - display->drawString(x + pressure_width + 15, y + drawStringIcon, getPressureString(pressure)); + display->drawString(x + pressure_width, y + drawStringIcon, getPressureString(pressure)); display->setFont(ArialMT_Plain_10); - display->drawString(x + pressure_width + 15 + (getPressureString(pressure).length() * 14), y + drawStringIcon, "hPa"); + display->drawString(x + pressure_width + (getPressureString(pressure).length() * 14), y + drawStringIcon, "hPa"); } void displayDs18b20(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { @@ -229,11 +264,16 @@ void displayMAX6675Temp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t SuplaOled::SuplaOled() { if (ConfigESP->getGpio(FUNCTION_SDA) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_SCL) != OFF_GPIO) { - if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt()) { - display = new SH1106Wire(0x3c, ConfigESP->getGpio(FUNCTION_SDA), ConfigESP->getGpio(FUNCTION_SCL)); - } - else { - display = new SSD1306Wire(0x3c, ConfigESP->getGpio(FUNCTION_SDA), ConfigESP->getGpio(FUNCTION_SCL)); + switch (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt()) { + case OLED_SSD1306_0_96: + display = new SSD1306Wire(0x3c, ConfigESP->getGpio(FUNCTION_SDA), ConfigESP->getGpio(FUNCTION_SCL)); + break; + case OLED_SH1106_1_3: + display = new SH1106Wire(0x3c, ConfigESP->getGpio(FUNCTION_SDA), ConfigESP->getGpio(FUNCTION_SCL)); + break; + case OLED_SSD1306_0_66: + display = new SSD1306Wire(0x3c, ConfigESP->getGpio(FUNCTION_SDA), ConfigESP->getGpio(FUNCTION_SCL), GEOMETRY_64_48); + break; } ui = new OLEDDisplayUi(display); diff --git a/src/SuplaOled.h b/src/SuplaOled.h index 3e6c6a96..dc3b6c4d 100644 --- a/src/SuplaOled.h +++ b/src/SuplaOled.h @@ -8,7 +8,7 @@ #include #include #include // Only needed for Arduino 1.6.5 and earlier -#include //OLED 0,96" +#include //OLED 0,96" or 0.66" WEMOS OLED shield #include //OLED 1.3" #include @@ -17,6 +17,13 @@ enum customActions TURN_ON_OLED }; +enum _OLED +{ + OLED_SSD1306_0_96 = 1, + OLED_SH1106_1_3, + OLED_SSD1306_0_66 +}; + String getTempString(double temperature); String getHumidityString(double humidity); String getPressureString(double pressure); @@ -50,10 +57,10 @@ class SuplaOled : public Supla::Triggerable, public Supla::Element { void iterateAlways(); void runAction(int event, int action); - OLEDDisplay *display; - OLEDDisplayUi *ui; + OLEDDisplay* display; + OLEDDisplayUi* ui; - FrameCallback *frames; + FrameCallback* frames; int frameCount = 0; OverlayCallback overlays[1]; int overlaysCount = 1; @@ -65,8 +72,8 @@ class SuplaOled : public Supla::Triggerable, public Supla::Element { }; // https://www.online-utility.org/image/convert/to/XBM -#define temp_width 32 -#define temp_height 32 +#define TEMP_WIDTH 32 +#define TEMP_HEIGHT 32 const uint8_t temp_bits[] PROGMEM = {0x00, 0x3C, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0x81, 0xF8, 0x03, 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, 0xF8, 0x03, 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, 0xF8, 0x03, 0x00, 0x99, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, @@ -75,8 +82,8 @@ const uint8_t temp_bits[] PROGMEM = {0x00, 0x3C, 0x00, 0x00, 0x00, 0x66, 0x00, 0 0x00, 0x20, 0x7E, 0x04, 0x00, 0x20, 0x7E, 0x04, 0x00, 0x20, 0x7E, 0x04, 0x00, 0x60, 0x3C, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x80, 0x81, 0x01, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00}; -#define humidity_width 32 -#define humidity_height 32 +#define HUMIDITY_WIDTH 32 +#define HUMIDITY_HEIGHT 32 const uint8_t humidity_bits[] PROGMEM = { 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, 0x30, 0x0C, 0x00, 0x00, 0x38, 0x1C, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x0C, 0x30, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x06, 0x40, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x80, 0x01, 0x80, 0x00, @@ -85,8 +92,8 @@ const uint8_t humidity_bits[] PROGMEM = { 0x30, 0x10, 0x09, 0x0C, 0x30, 0x00, 0x06, 0x0C, 0x20, 0x00, 0x00, 0x04, 0x60, 0x00, 0x00, 0x06, 0xC0, 0x00, 0x00, 0x03, 0x80, 0x01, 0x80, 0x01, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x0E, 0x70, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0xF0, 0x0F, 0x00}; -#define pressure_width 32 -#define pressure_height 32 +#define PRESSURE_WIDTH 32 +#define PRESSURE_HEIGHT 32 const uint8_t pressure_bits[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0x00, 0xFC, 0x3F, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x80, 0x81, 0x81, 0x01, 0xC0, 0x80, 0x01, 0x03, From 7f9a935418b904a93cbe32c87a326ec1cfe064b2 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Mon, 28 Dec 2020 16:59:13 +0100 Subject: [PATCH 026/165] wsparcie dla HLW8012 --- lib/SuplaDevice/src/supla/supla_lib_config.h | 2 +- platformio.ini | 7 +- src/GUI-Generic.ino | 9 +- src/GUI-Generic_Config.h | 1 + src/Markup.cpp | 35 ++++++ src/Markup.h | 4 + src/SuplaConfigESP.cpp | 7 ++ src/SuplaConfigManager.h | 58 +++++----- src/SuplaDeviceGUI.cpp | 18 +++- src/SuplaDeviceGUI.h | 8 ++ src/SuplaTemplateBoard.cpp | 18 ++++ src/SuplaTemplateBoard.h | 5 +- src/SuplaWebPageSensor.cpp | 107 ++++++++++++++++++- src/SuplaWebPageSensor.h | 16 +++ src/SuplaWebPageUpload.cpp | 3 +- 15 files changed, 257 insertions(+), 41 deletions(-) diff --git a/lib/SuplaDevice/src/supla/supla_lib_config.h b/lib/SuplaDevice/src/supla/supla_lib_config.h index 4e8071bb..d6df3384 100644 --- a/lib/SuplaDevice/src/supla/supla_lib_config.h +++ b/lib/SuplaDevice/src/supla/supla_lib_config.h @@ -7,6 +7,6 @@ #ifndef supla_lib_config_h_ #define supla_lib_config_h_ -#define SUPLA_COMM_DEBUG +//#define SUPLA_COMM_DEBUG #endif diff --git a/platformio.ini b/platformio.ini index 51e6c23e..2faedc17 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = [common] -build_flags = -D BUILD_VERSION='"GUI 1.0.9"' +build_flags = -D BUILD_VERSION='"GUI 1.1.4"' -w -DATOMIC_FS_UPDATE -DBEARSSL_SSL_BASIC @@ -42,7 +42,8 @@ build_flags = -D BUILD_VERSION='"GUI 1.0.9"' -D SUPLA_MAX6675 -D SUPLA_HC_SR04 -D SUPLA_IMPULSE_COUNTER - -D SUPLA_OLED + -D SUPLA_OLED + -D SUPLA_HLW8012 [env] framework = arduino @@ -59,7 +60,7 @@ lib_deps = datacute/DoubleResetDetector@^1.0.3 closedcube/ClosedCube SHT31D@^1.5.1 adafruit/Adafruit Si7021 Library@^1.3.0 - thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays @ ^4.1.0 + ;thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays @ ^4.1.0 xoseperez/HLW8012 @ ^1.1.1 extra_scripts = tools/copy_files.py diff --git a/src/GUI-Generic.ino b/src/GUI-Generic.ino index 96e8018e..70fd892b 100644 --- a/src/GUI-Generic.ino +++ b/src/GUI-Generic.ino @@ -162,7 +162,8 @@ void setup() { #ifdef SUPLA_MAX6675 if (ConfigESP->getGpio(FUNCTION_CLK) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_CS) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_D0) != OFF_GPIO) { - Supla::GUI::sensorMAX6675_K.push_back(new Supla::Sensor::MAX6675_K(ConfigESP->getGpio(FUNCTION_CLK), ConfigESP->getGpio(FUNCTION_CS), ConfigESP->getGpio(FUNCTION_D0))); + Supla::GUI::sensorMAX6675_K.push_back( + new Supla::Sensor::MAX6675_K(ConfigESP->getGpio(FUNCTION_CLK), ConfigESP->getGpio(FUNCTION_CS), ConfigESP->getGpio(FUNCTION_D0))); } #endif @@ -179,6 +180,12 @@ void setup() { #endif +#ifdef SUPLA_HLW8012 + if (ConfigESP->getGpio(FUNCTION_CF) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_CF1) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_SEL) != OFF_GPIO) { + Supla::GUI::addHLW8012(ConfigESP->getGpio(FUNCTION_CF), ConfigESP->getGpio(FUNCTION_CF1), ConfigESP->getGpio(FUNCTION_SEL)); + } +#endif + Supla::GUI::begin(); } diff --git a/src/GUI-Generic_Config.h b/src/GUI-Generic_Config.h index 537d5328..f81948fb 100644 --- a/src/GUI-Generic_Config.h +++ b/src/GUI-Generic_Config.h @@ -45,6 +45,7 @@ // ##### Other ##### #define SUPLA_HC_SR04 #define SUPLA_IMPULSE_COUNTER +#define SUPLA_HLW8012 #endif #endif // GUI-Generic_Config_h diff --git a/src/Markup.cpp b/src/Markup.cpp index b1cba4b8..afdc7cf2 100644 --- a/src/Markup.cpp +++ b/src/Markup.cpp @@ -94,6 +94,41 @@ void addNumberBox(String& html, const String& input_id, const String& name, uint html += F("'>"); } +void addNumberBox(String& html, const String& input_id, const String& name, const String& placeholder, bool required, const String& value) { + html += F(""); +} + +void addLinkBox(String& html, const String& name, const String& url) { + html += F(""); + html += F(""); + html += F(""); +} + void addListGPIOBox(String& html, const String& input_id, const String& name, uint8_t function, uint8_t nr) { html += F("

"); + addFormHeaderEnd(page); + + addForm(page, F("post"), PATH_SAVE_HLW8012_CALIBRATE); + addFormHeader(page, "Ustawienia kalibracji"); + addNumberBox(page, INPUT_CALIB_POWER, "Moc żarówki [W]", "25", true); + addNumberBox(page, INPUT_CALIB_VOLTAGE, "Napięcie [V]", "230", true); + addFormHeaderEnd(page); + + addButtonSubmit(page, "Kalibracja"); + addFormEnd(page); + + addButton(page, S_RETURN, PATH_OTHER); + return page; +} #endif \ No newline at end of file diff --git a/src/SuplaWebPageSensor.h b/src/SuplaWebPageSensor.h index f9cbdd5e..8113cffa 100644 --- a/src/SuplaWebPageSensor.h +++ b/src/SuplaWebPageSensor.h @@ -127,6 +127,22 @@ class SuplaWebPageSensor { #if defined(SUPLA_HC_SR04) || defined(SUPLA_IMPULSE_COUNTER) String supla_webpage_other(int save); #endif + +#if defined(SUPLA_HLW8012) +#define INPUT_CF "cf" +#define INPUT_CF1 "cf1" +#define INPUT_SEL "sel" + +#define PATH_HLW8012_CALIBRATE "calibrate" +#define PATH_SAVE_HLW8012_CALIBRATE "savecalibrate" + +#define INPUT_CALIB_POWER "power" +#define INPUT_CALIB_VOLTAGE "voltage" + + void handleHLW8012Calibrate(); + void handleHLW8012CalibrateSave(); + String suplaWebpageHLW8012Calibrate(uint8_t save); +#endif }; extern SuplaWebPageSensor* WebPageSensor; diff --git a/src/SuplaWebPageUpload.cpp b/src/SuplaWebPageUpload.cpp index 4b5d24b1..c364e500 100644 --- a/src/SuplaWebPageUpload.cpp +++ b/src/SuplaWebPageUpload.cpp @@ -38,7 +38,7 @@ void handleUpload(int save) { content += getURL(PATH_TOOLS); content += F("'>"); + content += F("

"); WebServer->sendContent(content); // WebServer->httpServer.send(200, PSTR("text/html"), FPSTR(uploadIndex)); @@ -65,6 +65,7 @@ void handleFileUpload() { dataFile.close(); // WebServer->httpServer.sendHeader("Location", "/upload"); // WebServer->httpServer.send(303); + ConfigManager->load(); handleUpload(1); } else { From ef7964f13a281f01435427ba9a056fa289931f6d Mon Sep 17 00:00:00 2001 From: krycha88 Date: Mon, 28 Dec 2020 21:22:03 +0100 Subject: [PATCH 027/165] #define supla_lib_config_h_ --- lib/SuplaDevice/src/supla/supla_lib_config.h | 2 +- src/GUI-Generic_Config.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/SuplaDevice/src/supla/supla_lib_config.h b/lib/SuplaDevice/src/supla/supla_lib_config.h index d6df3384..4e8071bb 100644 --- a/lib/SuplaDevice/src/supla/supla_lib_config.h +++ b/lib/SuplaDevice/src/supla/supla_lib_config.h @@ -7,6 +7,6 @@ #ifndef supla_lib_config_h_ #define supla_lib_config_h_ -//#define SUPLA_COMM_DEBUG +#define SUPLA_COMM_DEBUG #endif diff --git a/src/GUI-Generic_Config.h b/src/GUI-Generic_Config.h index f81948fb..70aa3273 100644 --- a/src/GUI-Generic_Config.h +++ b/src/GUI-Generic_Config.h @@ -1,6 +1,8 @@ #ifndef GUI_Generic_Config_h #define GUI_Generic_Config_h +#define supla_lib_config_h_ // silences unnecessary debug messages "should be disabled by default" + //#define USE_CUSTOM // User configuration From c80c44c8a57ad0a76dbb967a01141d0cb47dad21 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Mon, 28 Dec 2020 21:22:37 +0100 Subject: [PATCH 028/165] =?UTF-8?q?zmiana=20czasu=20zapisu=20dla=20licznik?= =?UTF-8?q?=C3=B3w=20energi=20na=2010min?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- platformio.ini | 2 +- src/SuplaDeviceGUI.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 2faedc17..1e6fd4bc 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = [common] -build_flags = -D BUILD_VERSION='"GUI 1.1.4"' +build_flags = -D BUILD_VERSION='"GUI 1.1.5"' -w -DATOMIC_FS_UPDATE -DBEARSSL_SSL_BASIC diff --git a/src/SuplaDeviceGUI.cpp b/src/SuplaDeviceGUI.cpp index d16717b8..6e2d5471 100644 --- a/src/SuplaDeviceGUI.cpp +++ b/src/SuplaDeviceGUI.cpp @@ -19,7 +19,7 @@ #if defined(SUPLA_RELAY) || defined(SUPLA_ROLLERSHUTTER) || defined(SUPLA_IMPULSE_COUNTER) || defined(SUPLA_HLW8012) #define TIME_SAVE_PERIOD_SEK 30 // the time is given in seconds -#define TIME_SAVE_PERIOD_IMPULSE_COUNTER_SEK 300 // 5min +#define TIME_SAVE_PERIOD_IMPULSE_COUNTER_SEK 600 // 10min #define STORAGE_OFFSET 0 #include Supla::Eeprom eeprom(STORAGE_OFFSET); From 3529abad0b9dfe42ca3e9c4c98af8ff591941e5f Mon Sep 17 00:00:00 2001 From: krycha88 Date: Tue, 29 Dec 2020 06:47:41 +0100 Subject: [PATCH 029/165] =?UTF-8?q?poprawa=20wybierania=20BOARD=5FGOSUND?= =?UTF-8?q?=5FSP111=20z=20szablonu=20p=C5=82ytek?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- platformio.ini | 2 +- src/SuplaTemplateBoard.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 1e6fd4bc..36bfbc41 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = [common] -build_flags = -D BUILD_VERSION='"GUI 1.1.5"' +build_flags = -D BUILD_VERSION='"GUI 1.1.6"' -w -DATOMIC_FS_UPDATE -DBEARSSL_SSL_BASIC diff --git a/src/SuplaTemplateBoard.cpp b/src/SuplaTemplateBoard.cpp index fe887f6e..474322b8 100644 --- a/src/SuplaTemplateBoard.cpp +++ b/src/SuplaTemplateBoard.cpp @@ -42,6 +42,7 @@ void addHLW8012(int8_t pinCF, int8_t pinCF1, int8_t pinSEL) { ConfigESP->setGpio(pinCF, FUNCTION_CF); ConfigESP->setGpio(pinCF1, FUNCTION_CF1); ConfigESP->setGpio(pinSEL, FUNCTION_SEL); + Supla::GUI::addHLW8012(ConfigESP->getGpio(FUNCTION_CF), ConfigESP->getGpio(FUNCTION_CF1), ConfigESP->getGpio(FUNCTION_SEL)); } void chooseTemplateBoard(uint8_t board) { From a895adb44a1ee39370f39229c48925ecf5436bd4 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Tue, 12 Jan 2021 06:48:15 +0100 Subject: [PATCH 030/165] =?UTF-8?q?poprawa=20wy=C5=9Bwietlania=20ikonek=20?= =?UTF-8?q?na=20OLED?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SuplaOled.cpp | 62 ++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/src/SuplaOled.cpp b/src/SuplaOled.cpp index 70b9b328..5f792999 100644 --- a/src/SuplaOled.cpp +++ b/src/SuplaOled.cpp @@ -27,7 +27,7 @@ String getPressureString(double pressure) { return F("error"); } else { - return String(floor(pressure), 0); + return String(pressure, 0); } } @@ -103,10 +103,8 @@ void displayRelayState(OLEDDisplay* display) { } x += 15; } - if (!GEOMETRY_64_48) { - display->setColor(WHITE); - display->drawHorizontalLine(0, 14, display->getWidth()); - } + display->setColor(WHITE); + display->drawHorizontalLine(0, 14, display->getWidth()); } void msOverlay(OLEDDisplay* display, OLEDDisplayUiState* state) { @@ -150,27 +148,29 @@ void displayBlank(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, in } void displayTemp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double temp, const String& name) { + uint8_t temp_width, temp_height; + int drawHeightIcon = display->getHeight() / 2 - 10; int drawStringIcon = display->getHeight() / 2 - 6; - int temp_width = TEMP_WIDTH; - int temp_height = TEMP_HEIGHT; + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); - if (GEOMETRY_64_48) { + if (display->getWidth() <= 64 || display->getHeight() <= 48) { temp_width = 0; temp_height = 0; } else { - temp_width = temp_width + 10; - } + display->drawXbm(x + 0, y + drawHeightIcon, TEMP_WIDTH, TEMP_HEIGHT, temp_bits); + display->setFont(ArialMT_Plain_10); + if (name != NULL) { + display->drawString(x + TEMP_WIDTH + 20, y + display->getHeight() / 2 - 15, name); + } - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawXbm(x + 0, y + drawHeightIcon, temp_width, temp_height, temp_bits); - display->setFont(ArialMT_Plain_10); - if (name != NULL) { - display->drawString(x + temp_width, y + display->getHeight() / 2 - 15, name); + temp_width = TEMP_WIDTH + 10; + temp_height = TEMP_HEIGHT; } + display->setFont(ArialMT_Plain_24); display->drawString(x + temp_width, y + drawStringIcon, getTempString(temp)); display->setFont(ArialMT_Plain_16); @@ -178,23 +178,24 @@ void displayTemp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int } void displaHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double humidity) { + uint8_t humidity_width, humidity_height; + int drawHeightIcon = display->getHeight() / 2 - 10; int drawStringIcon = display->getHeight() / 2 - 6; - int humidity_width = HUMIDITY_WIDTH; - int humidity_height = HUMIDITY_HEIGHT; + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); - if (GEOMETRY_64_48) { + if (display->getWidth() <= 64 || display->getHeight() <= 48) { humidity_width = 0; humidity_height = 0; } else { - humidity_width = humidity_width + 10; + display->drawXbm(x + 0, y + drawHeightIcon, HUMIDITY_WIDTH, HUMIDITY_HEIGHT, humidity_bits); + humidity_width = HUMIDITY_WIDTH + 20; + humidity_height = HUMIDITY_HEIGHT; } - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawXbm(x + 0, y + drawHeightIcon, humidity_width, humidity_height, humidity_bits); display->setFont(ArialMT_Plain_24); display->drawString(x + humidity_width, y + drawStringIcon, getHumidityString(humidity)); display->setFont(ArialMT_Plain_16); @@ -202,23 +203,24 @@ void displaHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, } void displayPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double pressure) { + uint8_t pressure_width, pressure_height; + int drawHeightIcon = display->getHeight() / 2 - 10; int drawStringIcon = display->getHeight() / 2 - 6; - int pressure_width = PRESSURE_WIDTH; - int pressure_height = PRESSURE_HEIGHT; + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); - if (GEOMETRY_64_48) { + if (display->getWidth() <= 64 || display->getHeight() <= 48) { pressure_width = 0; pressure_height = 0; } else { - pressure_width = pressure_width + 15; + display->drawXbm(x + 0, y + drawHeightIcon, PRESSURE_WIDTH, PRESSURE_HEIGHT, pressure_bits); + pressure_width = PRESSURE_WIDTH + 15; + pressure_height = PRESSURE_HEIGHT; } - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawXbm(x + 0, y + drawHeightIcon, pressure_width, pressure_height, pressure_bits); display->setFont(ArialMT_Plain_24); display->drawString(x + pressure_width, y + drawStringIcon, getPressureString(pressure)); display->setFont(ArialMT_Plain_10); From da2d73acd3d0fba846aa94d9254a5cea43ca9b3a Mon Sep 17 00:00:00 2001 From: krycha88 Date: Tue, 12 Jan 2021 06:50:06 +0100 Subject: [PATCH 031/165] aktualizacja SuplaDevice --- .../SequenceButton/SequenceButton.ino | 92 +++++++ lib/SuplaDevice/extras/test/CMakeLists.txt | 39 +++ .../extras/test/UptimeTests/uptime_tests.cpp | 60 ++++ lib/SuplaDevice/src/CMakeLists.txt | 5 + .../src/supla/control/MCP23017/S_MCP23017.cpp | 205 ++++++++++++++ .../src/supla/control/MCP23017/S_MCP23017.h | 52 ++++ .../Ejemplo_Mcp23017_io.ino | 189 +++++++++++++ .../src/supla/control/MCP23017/readme.txt | 15 + .../supla/control/MCP23017/supla_mcp23017.h | 78 ++++++ .../src/supla/control/bistable_relay.cpp | 7 +- .../supla/control/bistable_roller_shutter.cpp | 11 +- lib/SuplaDevice/src/supla/control/button.cpp | 107 ++----- lib/SuplaDevice/src/supla/control/button.h | 40 +-- lib/SuplaDevice/src/supla/control/relay.cpp | 50 ++-- .../src/supla/control/roller_shutter.cpp | 20 +- .../src/supla/control/sequence_button.cpp | 137 +++++++++ .../src/supla/control/sequence_button.h | 58 ++++ .../src/supla/control/simple_button.cpp | 108 ++++++++ .../src/supla/control/simple_button.h | 71 +++++ .../src/supla/control/virtual_relay.h | 4 + lib/SuplaDevice/src/supla/events.h | 5 +- lib/SuplaDevice/src/supla/io.cpp | 25 ++ lib/SuplaDevice/src/supla/io.h | 5 + .../src/supla/network/esp32_wifi.h | 115 +------- lib/SuplaDevice/src/supla/network/esp_wifi.h | 90 ++++-- lib/SuplaDevice/src/supla/pv/afore.cpp | 15 +- lib/SuplaDevice/src/supla/pv/afore.h | 1 + lib/SuplaDevice/src/supla/pv/fronius.cpp | 15 +- lib/SuplaDevice/src/supla/pv/fronius.h | 1 + lib/SuplaDevice/src/supla/sensor/binary.cpp | 45 +++ lib/SuplaDevice/src/supla/sensor/binary.h | 31 +-- .../src/supla/sensor/electricity_meter.cpp | 260 ++++++++++++++++++ .../src/supla/sensor/electricity_meter.h | 203 ++------------ .../src/supla/sensor/impulse_counter.cpp | 7 +- lib/SuplaDevice/src/supla/storage/storage.cpp | 14 + lib/SuplaDevice/src/supla/storage/storage.h | 2 + lib/SuplaDevice/src/supla/timer.cpp | 23 +- 37 files changed, 1682 insertions(+), 523 deletions(-) create mode 100644 lib/SuplaDevice/examples/SequenceButton/SequenceButton.ino create mode 100644 lib/SuplaDevice/extras/test/CMakeLists.txt create mode 100644 lib/SuplaDevice/extras/test/UptimeTests/uptime_tests.cpp create mode 100644 lib/SuplaDevice/src/CMakeLists.txt create mode 100644 lib/SuplaDevice/src/supla/control/MCP23017/S_MCP23017.cpp create mode 100644 lib/SuplaDevice/src/supla/control/MCP23017/S_MCP23017.h create mode 100644 lib/SuplaDevice/src/supla/control/MCP23017/examples/Ejemplo_Mcp23017_io/Ejemplo_Mcp23017_io.ino create mode 100644 lib/SuplaDevice/src/supla/control/MCP23017/readme.txt create mode 100644 lib/SuplaDevice/src/supla/control/MCP23017/supla_mcp23017.h create mode 100644 lib/SuplaDevice/src/supla/control/sequence_button.cpp create mode 100644 lib/SuplaDevice/src/supla/control/sequence_button.h create mode 100644 lib/SuplaDevice/src/supla/control/simple_button.cpp create mode 100644 lib/SuplaDevice/src/supla/control/simple_button.h create mode 100644 lib/SuplaDevice/src/supla/sensor/binary.cpp create mode 100644 lib/SuplaDevice/src/supla/sensor/electricity_meter.cpp diff --git a/lib/SuplaDevice/examples/SequenceButton/SequenceButton.ino b/lib/SuplaDevice/examples/SequenceButton/SequenceButton.ino new file mode 100644 index 00000000..8647974a --- /dev/null +++ b/lib/SuplaDevice/examples/SequenceButton/SequenceButton.ino @@ -0,0 +1,92 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include +#include +#include +#include + +// Choose proper network interface for your card: +// Arduino Mega with EthernetShield W5100: +//#include +// Ethernet MAC address +//uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; +//Supla::EthernetShield ethernet(mac); +// +// Arduino Mega with ENC28J60: +// #include +// Supla::ENC28J60 ethernet(mac); +// +// ESP8266 based board: +#include +Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); +// +// ESP32 based board: +// #include +// Supla::ESP32Wifi wifi("your_wifi_ssid", "your_wifi_password"); + + +void setup() { + + Serial.begin(115200); + + // Replace the falowing GUID with value that you can retrieve from https://www.supla.org/arduino/get-guid + char GUID[SUPLA_GUID_SIZE] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; + + // Replace the following AUTHKEY with value that you can retrieve from: https://www.supla.org/arduino/get-authkey + char AUTHKEY[SUPLA_AUTHKEY_SIZE] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; + + /* + * Having your device already registered at cloud.supla.org, + * you want to change CHANNEL sequence or remove any of them, + * then you must also remove the device itself from cloud.supla.org. + * Otherwise you will get "Channel conflict!" error. + */ + + auto secretRelay = new Supla::Control::Relay(30, false); // Low level trigger relay on pin 30 + auto alarmRelay = new Supla::Control::Relay(31, false); // Low level trigger relay on pin 31 + auto seqButton = new Supla::Control::SequenceButton(D9, true, true); // Button on pin 28 with internal pullUp + // and LOW is considered as "pressed" state + + // Sequence of lenghts [ms] of button being presset, released, pressed, released, etc. + // Aplication will print on Serial recorded sequence, so use it to record your rhythm and put it here + uint16_t sequence[30] = {90, 590, 90, 140, 90, 290, 90, 230, 90, 140, 90}; + + seqButton->setSequence(sequence); + seqButton->setMargin(0.5); // we allow +- 50% deviation of state length compared to matching sequence + + // Button will trigger secretRelay when correct rhythm will be detected or alarmRelay otherwise + seqButton->addAction(Supla::TURN_ON, secretRelay, Supla::ON_SEQUENCE_MATCH); + seqButton->addAction(Supla::TURN_ON, alarmRelay, Supla::ON_SEQUENCE_DOESNT_MATCH); + + /* + * SuplaDevice Initialization. + * Server address is available at https://cloud.supla.org + * If you do not have an account, you can create it at https://cloud.supla.org/account/create + * SUPLA and SUPLA CLOUD are free of charge + * + */ + + SuplaDevice.begin(GUID, // Global Unique Identifier + "svr1.supla.org", // SUPLA server address + "email@address", // Email address used to login to Supla Cloud + AUTHKEY); // Authorization key + +} + +void loop() { + SuplaDevice.iterate(); +} diff --git a/lib/SuplaDevice/extras/test/CMakeLists.txt b/lib/SuplaDevice/extras/test/CMakeLists.txt new file mode 100644 index 00000000..12e94f97 --- /dev/null +++ b/lib/SuplaDevice/extras/test/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.11) + +project(supladevice) + +enable_testing() + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + +include_directories(../../src) + +add_subdirectory(../../src/ build) + +include(FetchContent) + +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG release-1.8.0 + ) + +FetchContent_GetProperties(googletest) +if(NOT googletest_POPULATED) + FetchContent_Populate(googletest) + add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR}) +endif() + +file(GLOB SRCS UptimeTests/*.cpp) + +add_executable(supladevicetests ${SRCS}) + +target_link_libraries(supladevicetests + gtest + gtest_main + supladevicelib + ) + +add_test(NAME supladevicetests + COMMAND supladevicetests) diff --git a/lib/SuplaDevice/extras/test/UptimeTests/uptime_tests.cpp b/lib/SuplaDevice/extras/test/UptimeTests/uptime_tests.cpp new file mode 100644 index 00000000..0280d0e1 --- /dev/null +++ b/lib/SuplaDevice/extras/test/UptimeTests/uptime_tests.cpp @@ -0,0 +1,60 @@ +#include +#include + +TEST(UptimeTests, LastResetCauseSetAndGet) { + Supla::Uptime uptime; + EXPECT_EQ(uptime.getUptime(), 0); + EXPECT_EQ(uptime.getConnectionUptime(), 0); + EXPECT_EQ(uptime.getLastResetCause(), SUPLA_LASTCONNECTIONRESETCAUSE_UNKNOWN); + + // setter should not work unless first resetConnectionUptime is called + uptime.setConnectionLostCause(SUPLA_LASTCONNECTIONRESETCAUSE_WIFI_CONNECTION_LOST); + EXPECT_EQ(uptime.getUptime(), 0); + EXPECT_EQ(uptime.getConnectionUptime(), 0); + EXPECT_EQ(uptime.getLastResetCause(), SUPLA_LASTCONNECTIONRESETCAUSE_UNKNOWN); + + uptime.resetConnectionUptime(); + uptime.setConnectionLostCause(SUPLA_LASTCONNECTIONRESETCAUSE_WIFI_CONNECTION_LOST); + EXPECT_EQ(uptime.getUptime(), 0); + EXPECT_EQ(uptime.getConnectionUptime(), 0); + EXPECT_EQ(uptime.getLastResetCause(), SUPLA_LASTCONNECTIONRESETCAUSE_WIFI_CONNECTION_LOST); +} + +TEST(UptimeTests, IterateShouldIncreaseUptimeCounters) { + Supla::Uptime uptime; + unsigned long millis = 0; + + EXPECT_EQ(uptime.getUptime(), 0); + EXPECT_EQ(uptime.getConnectionUptime(), 0); + EXPECT_EQ(uptime.getLastResetCause(), SUPLA_LASTCONNECTIONRESETCAUSE_UNKNOWN); + + millis += 999; + uptime.iterate(millis); + EXPECT_EQ(uptime.getUptime(), 0); + EXPECT_EQ(uptime.getConnectionUptime(), 0); + EXPECT_EQ(uptime.getLastResetCause(), SUPLA_LASTCONNECTIONRESETCAUSE_UNKNOWN); + + millis += 1500; + uptime.iterate(millis); + EXPECT_EQ(uptime.getUptime(), 2); + EXPECT_EQ(uptime.getConnectionUptime(), 2); + EXPECT_EQ(uptime.getLastResetCause(), SUPLA_LASTCONNECTIONRESETCAUSE_UNKNOWN); + + millis += 20000; + uptime.iterate(millis); + EXPECT_EQ(uptime.getUptime(), 22); + EXPECT_EQ(uptime.getConnectionUptime(), 22); + EXPECT_EQ(uptime.getLastResetCause(), SUPLA_LASTCONNECTIONRESETCAUSE_UNKNOWN); + + uptime.resetConnectionUptime(); + EXPECT_EQ(uptime.getUptime(), 22); + EXPECT_EQ(uptime.getConnectionUptime(), 0); + EXPECT_EQ(uptime.getLastResetCause(), SUPLA_LASTCONNECTIONRESETCAUSE_UNKNOWN); + + millis += 2500; + uptime.iterate(millis); + EXPECT_EQ(uptime.getUptime(), 24); + EXPECT_EQ(uptime.getConnectionUptime(), 2); + EXPECT_EQ(uptime.getLastResetCause(), SUPLA_LASTCONNECTIONRESETCAUSE_UNKNOWN); +} + diff --git a/lib/SuplaDevice/src/CMakeLists.txt b/lib/SuplaDevice/src/CMakeLists.txt new file mode 100644 index 00000000..2643ea2b --- /dev/null +++ b/lib/SuplaDevice/src/CMakeLists.txt @@ -0,0 +1,5 @@ +set(SRCS + supla/uptime.cpp +) + +add_library(supladevicelib SHARED ${SRCS}) diff --git a/lib/SuplaDevice/src/supla/control/MCP23017/S_MCP23017.cpp b/lib/SuplaDevice/src/supla/control/MCP23017/S_MCP23017.cpp new file mode 100644 index 00000000..af9a21c3 --- /dev/null +++ b/lib/SuplaDevice/src/supla/control/MCP23017/S_MCP23017.cpp @@ -0,0 +1,205 @@ +#include +#include +#include "S_MCP23017.h" + +static const uint8_t MCP23017_BASEADDRESS = 0x20; + +static const uint8_t MCP23017_IODIRA = 0x00; +static const uint8_t MCP23017_IODIRB = 0x01; +static const uint8_t MCP23017_IPOLA = 0x02; +static const uint8_t MCP23017_IPOLB = 0x03; +static const uint8_t MCP23017_GPINTENA = 0x04; +static const uint8_t MCP23017_GPINTENB = 0x05; +static const uint8_t MCP23017_DEFVALA = 0x06; +static const uint8_t MCP23017_DEFVALB = 0x07; +static const uint8_t MCP23017_INTCONA = 0x08; +static const uint8_t MCP23017_INTCONB = 0x09; +static const uint8_t MCP23017_IOCONA = 0x0A; +static const uint8_t MCP23017_IOCONB = 0x0B; +static const uint8_t MCP23017_GPPUA = 0x0C; +static const uint8_t MCP23017_GPPUB = 0x0D; +static const uint8_t MCP23017_INTFA = 0x0E; +static const uint8_t MCP23017_INTFB = 0x0F; +static const uint8_t MCP23017_INTCAPA = 0x10; +static const uint8_t MCP23017_INTCAPB = 0x11; +static const uint8_t MCP23017_GPIOA = 0x12; +static const uint8_t MCP23017_GPIOB = 0x13; +static const uint8_t MCP23017_OLATA = 0x14; +static const uint8_t MCP23017_OLATB = 0x15; + +#ifdef ESP8266 +void MCP23017::init(uint8_t sda, uint8_t scl, bool fast) { + Wire.begin(sda, scl); + if (fast) + Wire.setClock(400000); +} +#else +void MCP23017::init(bool fast) { + Wire.begin(); + if (fast) + Wire.setClock(400000); +} +#endif + +bool MCP23017::begin(uint8_t address) { + _address = MCP23017_BASEADDRESS | (address & 0x07); + return (writeReg16(MCP23017_IOCONA, 0x4242) && writeReg16(MCP23017_IODIRA, 0xFFFF)); // INT MIRROR & INT POL HIGH, ALL INPUTS +} + +void MCP23017::pinMode(uint8_t pin, uint8_t mode) { + if (pin < 16) { + if (mode == OUTPUT) { + updateReg(pin < 8 ? MCP23017_IODIRA : MCP23017_IODIRB, ~(uint8_t)(1 << (pin % 8)), 0x00); + } else if ((mode == INPUT) || (mode == INPUT_PULLUP)) { + updateReg(pin < 8 ? MCP23017_IODIRA : MCP23017_IODIRB, 0xFF, 1 << (pin % 8)); + if (mode == INPUT_PULLUP) { + updateReg(pin < 8 ? MCP23017_GPPUA : MCP23017_GPPUB, 0xFF, 1 << (pin % 8)); + updateReg(pin < 8 ? MCP23017_IPOLA : MCP23017_IPOLB, 0xFF, 1 << (pin % 8)); + } else { + updateReg(pin < 8 ? MCP23017_GPPUA : MCP23017_GPPUB, ~(uint8_t)(1 << (pin % 8)), 0x00); + updateReg(pin < 8 ? MCP23017_IPOLA : MCP23017_IPOLB, ~(uint8_t)(1 << (pin % 8)), 0x00); + } + } + } +} + +void MCP23017::setPullup(uint8_t pin, bool pullup, bool inverse) { + if (pin < 16) { + if (pullup) + updateReg(pin < 8 ? MCP23017_GPPUA : MCP23017_GPPUB, 0xFF, 1 << (pin % 8)); + else + updateReg(pin < 8 ? MCP23017_GPPUA : MCP23017_GPPUB, ~(uint8_t)(1 << (pin % 8)), 0x00); + if (inverse) + updateReg(pin < 8 ? MCP23017_IPOLA : MCP23017_IPOLB, 0xFF, 1 << (pin % 8)); + else + updateReg(pin < 8 ? MCP23017_IPOLA : MCP23017_IPOLB, ~(uint8_t)(1 << (pin % 8)), 0x00); + } +} + +bool MCP23017::digitalRead(uint8_t pin) { + unsigned long now = millis(); + if (now > get_ba){ + ba = readReg16(MCP23017_GPIOA); // --------- reads "readReg16" only once every 20 ms ---- + get_ba = now + 20; + } + if (pin < 16) { + return ((ba >> (pin % 16)) & 0x01); + } +} + +void MCP23017::digitalWrite(uint8_t pin, bool value) { + if (pin < 16) { + if (value) + updateReg(pin < 8 ? MCP23017_GPIOA : MCP23017_GPIOB, 0xFF, 1 << (pin % 8)); + else + updateReg(pin < 8 ? MCP23017_GPIOA : MCP23017_GPIOB, ~(uint8_t)(1 << (pin % 8)), 0x00); + } +} + +uint16_t MCP23017::digitalReads() { + return readReg16(MCP23017_GPIOA); +} + +void MCP23017::digitalWrites(uint16_t values) { + writeReg16(MCP23017_GPIOA, values); +} + +void MCP23017::attachInterrupt(uint8_t pin, callback_t callback) { + _callback = callback; + ::pinMode(pin, INPUT); + ::attachInterrupt(digitalPinToInterrupt(pin), std::bind(&MCP23017::_interrupt, this), RISING); +} + +void MCP23017::detachInterrupt(uint8_t pin) { + ::detachInterrupt(digitalPinToInterrupt(pin)); + _callback = NULL; +} + +void MCP23017::setupInterrupt(uint8_t pin, bool enable) { + if (pin < 16) { + if (enable) + updateReg(pin < 8 ? MCP23017_GPINTENA : MCP23017_GPINTENB, 0xFF, 1 << (pin % 8)); + else + updateReg(pin < 8 ? MCP23017_GPINTENA : MCP23017_GPINTENB, ~(uint8_t)(1 << (pin % 8)), 0x00); + } +} + +void MCP23017::setupInterrupts(uint16_t pins, bool enable) { + if (enable) + updateReg16(MCP23017_GPINTENA, 0xFFFF, pins); + else + updateReg16(MCP23017_GPINTENA, ~pins, 0x00); +} + +bool MCP23017::writeReg(uint8_t reg, uint8_t value) { + Wire.beginTransmission(_address); + Wire.write(reg); + Wire.write(value); + return (Wire.endTransmission() == 0); +} + +bool MCP23017::writeReg16(uint8_t reg, uint16_t value) { + Wire.beginTransmission(_address); + Wire.write(reg); + Wire.write(value & 0xFF); + Wire.write(value >> 8); + return (Wire.endTransmission() == 0); +} + +uint8_t MCP23017::readReg(uint8_t reg) { + Wire.beginTransmission(_address); + Wire.write(reg); + if (Wire.endTransmission() != 0) + return 0; // Error! + Wire.requestFrom(_address, (uint8_t)1); + return Wire.read(); +} + +uint16_t MCP23017::readReg16(uint8_t reg) { + Wire.beginTransmission(_address); + Wire.write(reg); + if (Wire.endTransmission() != 0) + return 0; // Error! + Wire.requestFrom(_address, (uint8_t)2); + uint8_t a = Wire.read(); + return ((Wire.read() << 8) | a); +} + +bool MCP23017::updateReg(uint8_t reg, uint8_t andMask, uint8_t orMask) { + Wire.beginTransmission(_address); + Wire.write(reg); + if (Wire.endTransmission() != 0) + return false; // Error! + Wire.requestFrom(_address, (uint8_t)1); + uint8_t a = (Wire.read() & andMask) | orMask; + Wire.beginTransmission(_address); + Wire.write(reg); + Wire.write(a); + return (Wire.endTransmission() == 0); +} + +bool MCP23017::updateReg16(uint8_t reg, uint16_t andMask, uint16_t orMask) { + Wire.beginTransmission(_address); + Wire.write(reg); + if (Wire.endTransmission() != 0) + return false; // Error! + Wire.requestFrom(_address, (uint8_t)2); + uint16_t ab = Wire.read(); + ab |= (Wire.read() << 8); + ab &= andMask; + ab |= orMask; + Wire.beginTransmission(_address); + Wire.write(reg); + Wire.write(ab & 0xFF); + Wire.write(ab >> 8); + return (Wire.endTransmission() == 0); +} + +void IRAM_ATTR MCP23017::_interrupt() { + uint16_t pins, values; + + pins = readReg16(MCP23017_INTFA); + values = readReg16(MCP23017_INTCAPA); + if (_callback) + _callback(pins, values); +} diff --git a/lib/SuplaDevice/src/supla/control/MCP23017/S_MCP23017.h b/lib/SuplaDevice/src/supla/control/MCP23017/S_MCP23017.h new file mode 100644 index 00000000..ef08791e --- /dev/null +++ b/lib/SuplaDevice/src/supla/control/MCP23017/S_MCP23017.h @@ -0,0 +1,52 @@ +#ifndef _S_MCP23017_H +#define _S_MCP23017_H + +#include +#include + +class MCP23017 { +public: + typedef std::function callback_t; + + MCP23017() : _callback(NULL) {} + +#ifdef ESP8266 + static void init(uint8_t sda, uint8_t scl, bool fast = true); + static void init(bool fast = true) { + init(SDA, SCL, fast); + } +#else + static void init(bool fast = true); +#endif + + bool begin(uint8_t address = 0); + + void pinMode(uint8_t pin, uint8_t mode); + void setPullup(uint8_t pin, bool pullup, bool inverse = false); + bool digitalRead(uint8_t pin); + void digitalWrite(uint8_t pin, bool value); + uint16_t digitalReads(); + void digitalWrites(uint16_t values); + + void attachInterrupt(uint8_t pin, callback_t callback); + void detachInterrupt(uint8_t pin); + void setupInterrupt(uint8_t pin, bool enable = true); + void setupInterrupts(uint16_t pins, bool enable = true); + +protected: + bool writeReg(uint8_t reg, uint8_t value); + bool writeReg16(uint8_t reg, uint16_t value); + uint8_t readReg(uint8_t reg); + uint16_t readReg16(uint8_t reg); + bool updateReg(uint8_t reg, uint8_t andMask, uint8_t orMask); + bool updateReg16(uint8_t reg, uint16_t andMask, uint16_t orMask); + + void _interrupt(); + + uint8_t _address; + uint16_t ba = 0; + unsigned long get_ba = 0; + callback_t _callback; +}; + +#endif diff --git a/lib/SuplaDevice/src/supla/control/MCP23017/examples/Ejemplo_Mcp23017_io/Ejemplo_Mcp23017_io.ino b/lib/SuplaDevice/src/supla/control/MCP23017/examples/Ejemplo_Mcp23017_io/Ejemplo_Mcp23017_io.ino new file mode 100644 index 00000000..dd4ee2be --- /dev/null +++ b/lib/SuplaDevice/src/supla/control/MCP23017/examples/Ejemplo_Mcp23017_io/Ejemplo_Mcp23017_io.ino @@ -0,0 +1,189 @@ +#define supla_lib_config_h_ // silences unnecessary debug messages "should be disabled by default" +#include +#include +#include +#include + +// +// ESP8266 based board: +#include +Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); +// + +void setup() { + + Serial.begin(115200); + + mcp1.init(4, 5); // init(uint8_t sda, uint8_t scl, bool fast) = Wire.begin + + if (! mcp1.begin(0))Serial.println("MCP23017 1 not found!"); // begin(uint8_t address) "Pin 100 - 115" + if (! mcp2.begin(1))Serial.println("MCP23017 2 not found!"); // begin(uint8_t address) "Pin 116 - 131" + if (! mcp3.begin(2))Serial.println("MCP23017 3 not found!"); // begin(uint8_t address) "Pin 132 - 147" + if (! mcp4.begin(3))Serial.println("MCP23017 4 not found!"); // begin(uint8_t address) "Pin 148 - 163" + + // Replace the falowing GUID with value that you can retrieve from https://www.supla.org/arduino/get-guid + char GUID[SUPLA_GUID_SIZE] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; + + // Replace the following AUTHKEY with value that you can retrieve from: https://www.supla.org/arduino/get-authkey + char AUTHKEY[SUPLA_AUTHKEY_SIZE] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; + + /* + * Having your device already registered at cloud.supla.org, + * you want to change CHANNEL sequence or remove any of them, + * then you must also remove the device itself from cloud.supla.org. + * Otherwise you will get "Channel conflict!" error. + */ + + auto relay_1 = new Supla::Control::Relay(100, true); + auto relay_2 = new Supla::Control::Relay(101, true); + auto relay_3 = new Supla::Control::Relay(102, true); + auto relay_4 = new Supla::Control::Relay(103, true); + auto relay_5 = new Supla::Control::Relay(104, true); + auto relay_6 = new Supla::Control::Relay(105, true); + auto relay_7 = new Supla::Control::Relay(106, true); + auto relay_8 = new Supla::Control::Relay(107, true); + auto relay_9 = new Supla::Control::Relay(108, true); + auto relay_10 = new Supla::Control::Relay(109, true); + auto relay_11 = new Supla::Control::Relay(110, true); + auto relay_12 = new Supla::Control::Relay(111, true); + auto relay_13 = new Supla::Control::Relay(112, true); + auto relay_14 = new Supla::Control::Relay(113, true); + auto relay_15 = new Supla::Control::Relay(114, true); + auto relay_16 = new Supla::Control::Relay(115, true); + + auto relay_17 = new Supla::Control::Relay(148, true); + auto relay_18 = new Supla::Control::Relay(149, true); + auto relay_19 = new Supla::Control::Relay(150, true); + auto relay_20 = new Supla::Control::Relay(151, true); + auto relay_21 = new Supla::Control::Relay(152, true); + auto relay_22 = new Supla::Control::Relay(153, true); + auto relay_23 = new Supla::Control::Relay(154, true); + auto relay_24 = new Supla::Control::Relay(155, true); + auto relay_25 = new Supla::Control::Relay(156, true); + auto relay_26 = new Supla::Control::Relay(157, true); + auto relay_27 = new Supla::Control::Relay(158, true); + auto relay_28 = new Supla::Control::Relay(159, true); + auto relay_29 = new Supla::Control::Relay(160, true); + auto relay_30 = new Supla::Control::Relay(161, true); + auto relay_31 = new Supla::Control::Relay(162, true); + auto relay_32 = new Supla::Control::Relay(163, true); + + auto button_1 = new Supla::Control::Button(116, true, true); + auto button_2 = new Supla::Control::Button(117, true, true); + auto button_3 = new Supla::Control::Button(118, true, true); + auto button_4 = new Supla::Control::Button(119, true, true); + auto button_5 = new Supla::Control::Button(120, true, true); + auto button_6 = new Supla::Control::Button(121, true, true); + auto button_7 = new Supla::Control::Button(122, true, true); + auto button_8 = new Supla::Control::Button(123, true, true); + auto button_9 = new Supla::Control::Button(124, true, true); + auto button_10 = new Supla::Control::Button(125, true, true); + auto button_11 = new Supla::Control::Button(126, true, true); + auto button_12 = new Supla::Control::Button(127, true, true); + auto button_13 = new Supla::Control::Button(128, true, true); + auto button_14 = new Supla::Control::Button(129, true, true); + auto button_15 = new Supla::Control::Button(130, true, true); + auto button_16 = new Supla::Control::Button(131, true, true); + + auto button_17 = new Supla::Control::Button(132, true, true); + auto button_18 = new Supla::Control::Button(133, true, true); + auto button_19 = new Supla::Control::Button(134, true, true); + auto button_20 = new Supla::Control::Button(135, true, true); + auto button_21 = new Supla::Control::Button(136, true, true); + auto button_22 = new Supla::Control::Button(137, true, true); + auto button_23 = new Supla::Control::Button(138, true, true); + auto button_24 = new Supla::Control::Button(139, true, true); + auto button_25 = new Supla::Control::Button(140, true, true); + auto button_26 = new Supla::Control::Button(141, true, true); + auto button_27 = new Supla::Control::Button(142, true, true); + auto button_28 = new Supla::Control::Button(143, true, true); + auto button_29 = new Supla::Control::Button(144, true, true); + auto button_30 = new Supla::Control::Button(145, true, true); + auto button_31 = new Supla::Control::Button(146, true, true); + auto button_32 = new Supla::Control::Button(147, true, true); + + button_1->addAction(Supla::TOGGLE, relay_1, Supla::ON_PRESS); + button_1->setSwNoiseFilterDelay(50); + button_2->addAction(Supla::TOGGLE, relay_2, Supla::ON_PRESS); + button_2->setSwNoiseFilterDelay(50); + button_3->addAction(Supla::TOGGLE, relay_3, Supla::ON_PRESS); + button_3->setSwNoiseFilterDelay(50); + button_4->addAction(Supla::TOGGLE, relay_4, Supla::ON_PRESS); + button_4->setSwNoiseFilterDelay(50); + button_5->addAction(Supla::TOGGLE, relay_5, Supla::ON_PRESS); + button_5->setSwNoiseFilterDelay(50); + button_6->addAction(Supla::TOGGLE, relay_6, Supla::ON_PRESS); + button_6->setSwNoiseFilterDelay(50); + button_7->addAction(Supla::TOGGLE, relay_7, Supla::ON_PRESS); + button_7->setSwNoiseFilterDelay(50); + button_8->addAction(Supla::TOGGLE, relay_8, Supla::ON_PRESS); + button_8->setSwNoiseFilterDelay(50); + button_9->addAction(Supla::TOGGLE, relay_9, Supla::ON_PRESS); + button_9->setSwNoiseFilterDelay(50); + button_10->addAction(Supla::TOGGLE, relay_10, Supla::ON_PRESS); + button_10->setSwNoiseFilterDelay(50); + button_11->addAction(Supla::TOGGLE, relay_11, Supla::ON_PRESS); + button_11->setSwNoiseFilterDelay(50); + button_12->addAction(Supla::TOGGLE, relay_12, Supla::ON_PRESS); + button_12->setSwNoiseFilterDelay(50); + button_13->addAction(Supla::TOGGLE, relay_13, Supla::ON_PRESS); + button_13->setSwNoiseFilterDelay(50); + button_14->addAction(Supla::TOGGLE, relay_14, Supla::ON_PRESS); + button_14->setSwNoiseFilterDelay(50); + button_15->addAction(Supla::TOGGLE, relay_15, Supla::ON_PRESS); + button_15->setSwNoiseFilterDelay(50); + button_16->addAction(Supla::TOGGLE, relay_16, Supla::ON_PRESS); + button_16->setSwNoiseFilterDelay(50); + + button_17->addAction(Supla::TOGGLE, relay_17, Supla::ON_PRESS); + button_17->setSwNoiseFilterDelay(50); + button_18->addAction(Supla::TOGGLE, relay_18, Supla::ON_PRESS); + button_18->setSwNoiseFilterDelay(50); + button_19->addAction(Supla::TOGGLE, relay_19, Supla::ON_PRESS); + button_19->setSwNoiseFilterDelay(50); + button_20->addAction(Supla::TOGGLE, relay_20, Supla::ON_PRESS); + button_20->setSwNoiseFilterDelay(50); + button_21->addAction(Supla::TOGGLE, relay_21, Supla::ON_PRESS); + button_21->setSwNoiseFilterDelay(50); + button_22->addAction(Supla::TOGGLE, relay_22, Supla::ON_PRESS); + button_22->setSwNoiseFilterDelay(50); + button_23->addAction(Supla::TOGGLE, relay_23, Supla::ON_PRESS); + button_23->setSwNoiseFilterDelay(50); + button_24->addAction(Supla::TOGGLE, relay_24, Supla::ON_PRESS); + button_24->setSwNoiseFilterDelay(50); + button_25->addAction(Supla::TOGGLE, relay_25, Supla::ON_PRESS); + button_25->setSwNoiseFilterDelay(50); + button_26->addAction(Supla::TOGGLE, relay_26, Supla::ON_PRESS); + button_26->setSwNoiseFilterDelay(50); + button_27->addAction(Supla::TOGGLE, relay_27, Supla::ON_PRESS); + button_27->setSwNoiseFilterDelay(50); + button_28->addAction(Supla::TOGGLE, relay_28, Supla::ON_PRESS); + button_28->setSwNoiseFilterDelay(50); + button_29->addAction(Supla::TOGGLE, relay_29, Supla::ON_PRESS); + button_29->setSwNoiseFilterDelay(50); + button_30->addAction(Supla::TOGGLE, relay_30, Supla::ON_PRESS); + button_30->setSwNoiseFilterDelay(50); + button_31->addAction(Supla::TOGGLE, relay_31, Supla::ON_PRESS); + button_31->setSwNoiseFilterDelay(50); + button_32->addAction(Supla::TOGGLE, relay_32, Supla::ON_PRESS); + button_32->setSwNoiseFilterDelay(50); + + /* + * SuplaDevice Initialization. + * Server address is available at https://cloud.supla.org + * If you do not have an account, you can create it at https://cloud.supla.org/account/create + * SUPLA and SUPLA CLOUD are free of charge + * + */ + + SuplaDevice.begin(GUID, // Global Unique Identifier + "svr1.supla.org", // SUPLA server address + "email@address", // Email address used to login to Supla Cloud + AUTHKEY); // Authorization key + +} + +void loop() { + SuplaDevice.iterate(); + delay(25); +} diff --git a/lib/SuplaDevice/src/supla/control/MCP23017/readme.txt b/lib/SuplaDevice/src/supla/control/MCP23017/readme.txt new file mode 100644 index 00000000..9cb235ee --- /dev/null +++ b/lib/SuplaDevice/src/supla/control/MCP23017/readme.txt @@ -0,0 +1,15 @@ +Support for up to 4 x "MCP23017" additional 64 Gpios. + + +BASIC USE: + +INSTANTIATE +#include + +SETUP + mcp1.init(uint8_t sda, uint8_t scl); // init(uint8_t sda, uint8_t scl) = Wire.begin(); + + mcp1.begin(uint8_t mcp23017_address); // Pin 100 - 115 + mcp2.begin(uint8_t mcp23017_address); // Pin 116 - 131 + mcp3.begin(uint8_t mcp23017_address); // Pin 132 - 147 + mcp4.begin(uint8_t mcp23017_address); // Pin 148 - 163 \ No newline at end of file diff --git a/lib/SuplaDevice/src/supla/control/MCP23017/supla_mcp23017.h b/lib/SuplaDevice/src/supla/control/MCP23017/supla_mcp23017.h new file mode 100644 index 00000000..8dfa9598 --- /dev/null +++ b/lib/SuplaDevice/src/supla/control/MCP23017/supla_mcp23017.h @@ -0,0 +1,78 @@ + +#ifndef S_mcp_23017_h +#define S_mcp_23017_h + +#include +#include +#include + + + MCP23017 mcp1; + MCP23017 mcp2; + MCP23017 mcp3; + MCP23017 mcp4; + + +class CustomControl : public Supla::Io { + public: + void customDigitalWrite(int channelNumber, uint8_t pin, uint8_t val) { + if (pin < 100) { + return ::digitalWrite(pin,val); + } + if ((pin > 99) && (pin < 116)){ + mcp1.digitalWrite(pin - 100, val); + return; + } + if ((pin > 115) && (pin < 132)){ + mcp2.digitalWrite(pin - 116, val); + } + if ((pin > 131) && (pin < 148)){ + mcp3.digitalWrite(pin - 132, val); + return; + } + if ((pin > 147) && (pin < 164)){ + mcp4.digitalWrite(pin - 148, val); + return; + } + } + int customDigitalRead(int channelNumber, uint8_t pin) { + if (pin < 100){ + return ::digitalRead(pin); + } + if ((pin > 99)&& (pin < 116)){ + return mcp1.digitalRead(pin - 100); + } + if ((pin > 115)&& (pin < 132)){ + return mcp2.digitalRead(pin - 116); + } + if ((pin > 131)&& (pin < 148)){ + return mcp3.digitalRead(pin - 132); + } + if ((pin > 147)&& (pin < 164)){ + return mcp4.digitalRead(pin - 148); + } + } + void customPinMode(int channelNumber, uint8_t pin, uint8_t mode) { + (void)(channelNumber); + if (pin < 100){ + return ::pinMode(pin, mode); + } + if ((pin > 99)&& (pin < 116)){ + mcp1.pinMode(pin - 100, mode); + } + if ((pin > 115)&& (pin < 132)){ + mcp2.pinMode(pin - 116, mode); + } + if ((pin > 131)&& (pin < 148)){ + mcp3.pinMode(pin - 132, mode); + } + if ((pin > 147)&& (pin < 164)){ + mcp4.pinMode(pin - 148, mode); + } + } + +}CustomControl; + + +#endif + diff --git a/lib/SuplaDevice/src/supla/control/bistable_relay.cpp b/lib/SuplaDevice/src/supla/control/bistable_relay.cpp index 3fabf297..d03f2aa4 100644 --- a/lib/SuplaDevice/src/supla/control/bistable_relay.cpp +++ b/lib/SuplaDevice/src/supla/control/bistable_relay.cpp @@ -40,7 +40,7 @@ BistableRelay::BistableRelay(int pin, void BistableRelay::onInit() { if (statusPin >= 0) { - pinMode(statusPin, statusPullUp ? INPUT_PULLUP : INPUT); + Supla::Io::pinMode(channel.getChannelNumber(), statusPin, statusPullUp ? INPUT_PULLUP : INPUT); channel.setNewValue(isOn()); } else { channel.setNewValue(false); @@ -56,7 +56,7 @@ void BistableRelay::onInit() { turnOff(); } - pinMode(pin, OUTPUT); + Supla::Io::pinMode(channel.getChannelNumber(), pin, OUTPUT); } void BistableRelay::iterateAlways() { @@ -138,6 +138,9 @@ void BistableRelay::internalToggle() { busy = true; disarmTimeMs = millis(); Supla::Io::digitalWrite(channel.getChannelNumber(), pin, pinOnValue()); + + // Schedule save in 5 s after state change + Supla::Storage::ScheduleSave(5000); } void BistableRelay::toggle(_supla_int_t duration) { diff --git a/lib/SuplaDevice/src/supla/control/bistable_roller_shutter.cpp b/lib/SuplaDevice/src/supla/control/bistable_roller_shutter.cpp index 5fc8fe1f..9401633f 100644 --- a/lib/SuplaDevice/src/supla/control/bistable_roller_shutter.cpp +++ b/lib/SuplaDevice/src/supla/control/bistable_roller_shutter.cpp @@ -15,6 +15,7 @@ */ #include "bistable_roller_shutter.h" +#include namespace Supla { namespace Control { @@ -33,28 +34,30 @@ void BistableRollerShutter::stopMovement() { } currentDirection = STOP_DIR; doNothingTime = millis(); + // Schedule save in 5 s after stop movement of roller shutter + Supla::Storage::ScheduleSave(5000); } void BistableRollerShutter::relayDownOn() { activeBiRelay = true; toggleTime = millis(); - digitalWrite(pinDown, highIsOn ? HIGH : LOW); + Supla::Io::digitalWrite(channel.getChannelNumber(), pinDown, highIsOn ? HIGH : LOW); } void BistableRollerShutter::relayUpOn() { activeBiRelay = true; toggleTime = millis(); - digitalWrite(pinUp, highIsOn ? HIGH : LOW); + Supla::Io::digitalWrite(channel.getChannelNumber(), pinUp, highIsOn ? HIGH : LOW); } void BistableRollerShutter::relayDownOff() { activeBiRelay = false; - digitalWrite(pinDown, highIsOn ? LOW : HIGH); + Supla::Io::digitalWrite(channel.getChannelNumber(), pinDown, highIsOn ? LOW : HIGH); } void BistableRollerShutter::relayUpOff() { activeBiRelay = false; - digitalWrite(pinUp, highIsOn ? LOW : HIGH); + Supla::Io::digitalWrite(channel.getChannelNumber(), pinUp, highIsOn ? LOW : HIGH); } void BistableRollerShutter::onTimer() { diff --git a/lib/SuplaDevice/src/supla/control/button.cpp b/lib/SuplaDevice/src/supla/control/button.cpp index 950b9f3e..8fcb294e 100644 --- a/lib/SuplaDevice/src/supla/control/button.cpp +++ b/lib/SuplaDevice/src/supla/control/button.cpp @@ -16,60 +16,13 @@ #include "button.h" -enum StateResults {PRESSED, RELEASED, TO_PRESSED, TO_RELEASED}; - -Supla::Control::ButtonState::ButtonState(int pin, bool pullUp, bool invertLogic) - : debounceTimeMs(0), - filterTimeMs(0), - debounceDelayMs(50), - swNoiseFilterDelayMs(20), - pin(pin), - newStatusCandidate(LOW), - prevState(LOW), - pullUp(pullUp), - invertLogic(invertLogic) { -} - -int Supla::Control::ButtonState::update() { - if (millis() - debounceTimeMs > debounceDelayMs) { - int currentState = digitalRead(pin); - if (currentState != prevState) { - // If status is changed, then make sure that it will be kept at - // least swNoiseFilterDelayMs ms to avoid noise - if (currentState != newStatusCandidate) { - newStatusCandidate = currentState; - filterTimeMs = millis(); - } else if (millis() - filterTimeMs > swNoiseFilterDelayMs) { - // If new status is kept at least swNoiseFilterDelayMs ms, then apply - // change of status - debounceTimeMs = millis(); - prevState = currentState; - if (currentState == valueOnPress()) { - return TO_PRESSED; - } else { - return TO_RELEASED; - } - } - } else { - // If current status is the same as prevState, then reset - // new status candidate - newStatusCandidate = prevState; - } - } - if (prevState == valueOnPress()) { - return PRESSED; - } else { - return RELEASED; - } -} Supla::Control::Button::Button(int pin, bool pullUp, bool invertLogic) - : state(pin, pullUp, invertLogic), + : SimpleButton(pin, pullUp, invertLogic), holdTimeMs(0), multiclickTimeMs(0), - clickCounter(0), lastStateChangeMs(0), - enableExtDetection(false), + clickCounter(0), holdSend(false), bistable(false) { } @@ -88,13 +41,24 @@ void Supla::Control::Button::onTimer() { runAction(ON_CHANGE); } + if (stateChanged) { + lastStateChangeMs = millis(); + if (stateResult == TO_PRESSED || bistable) { + clickCounter++; + } + } + if (!stateChanged) { if (!bistable && stateResult == PRESSED) { if (clickCounter <= 1 && holdTimeMs > 0 && timeDelta > holdTimeMs && !holdSend) { runAction(ON_HOLD); holdSend = true; } - } else if (bistable || stateResult == RELEASED) { + } else if (clickCounter > 0 && (bistable || stateResult == RELEASED)) { + if (multiclickTimeMs == 0) { + holdSend = false; + clickCounter = 0; + } if (multiclickTimeMs > 0 && timeDelta > multiclickTimeMs) { if (!holdSend) { switch (clickCounter) { @@ -129,49 +93,15 @@ void Supla::Control::Button::onTimer() { runAction(ON_CLICK_10); break; } - } + if (clickCounter >= 10) { + runAction(ON_CRAZY_CLICKER); + } + } holdSend = false; clickCounter = 0; } } } - - if (stateChanged) { - lastStateChangeMs = millis(); - if (stateResult == TO_PRESSED || bistable) { - clickCounter++; - } - - } -} - -void Supla::Control::Button::onInit() { - state.init(); -} - -void Supla::Control::ButtonState::init() { - pinMode(pin, pullUp ? INPUT_PULLUP : INPUT); - prevState = digitalRead(pin); - newStatusCandidate = prevState; -} - -int Supla::Control::ButtonState::valueOnPress() { - return invertLogic ? LOW : HIGH; -} - -void Supla::Control::Button::setSwNoiseFilterDelay(unsigned int newDelayMs) { - state.setSwNoiseFilterDelay(newDelayMs); -} -void Supla::Control::ButtonState::setSwNoiseFilterDelay(unsigned int newDelayMs) { - swNoiseFilterDelayMs = newDelayMs; -} - -void Supla::Control::Button::setDebounceDelay(unsigned int newDelayMs) { - state.setDebounceDelay(newDelayMs); -} - -void Supla::Control::ButtonState::setDebounceDelay(unsigned int newDelayMs) { - debounceDelayMs = newDelayMs; } void Supla::Control::Button::setHoldTime(unsigned int timeMs) { @@ -188,3 +118,4 @@ void Supla::Control::Button::setMulticlickTime(unsigned int timeMs, bool bistabl holdTimeMs = 0; } } + diff --git a/lib/SuplaDevice/src/supla/control/button.h b/lib/SuplaDevice/src/supla/control/button.h index 7cf146a3..a48328d5 100644 --- a/lib/SuplaDevice/src/supla/control/button.h +++ b/lib/SuplaDevice/src/supla/control/button.h @@ -18,58 +18,24 @@ #define _button_h #include - -#include "../element.h" -#include "../events.h" -#include "../local_action.h" +#include "simple_button.h" namespace Supla { namespace Control { -class ButtonState { - public: - ButtonState(int pin, bool pullUp, bool invertLogic); - int update(); - void init(); - - void setSwNoiseFilterDelay(unsigned int newDelayMs); - void setDebounceDelay(unsigned int newDelayMs); - void setHoldTime(unsigned int timeMs); - void setMulticlickTime(unsigned int timeMs); - - protected: - int valueOnPress(); - - unsigned long debounceTimeMs; - unsigned long filterTimeMs; - unsigned int debounceDelayMs; - unsigned int swNoiseFilterDelayMs; - int pin; - int8_t newStatusCandidate; - int8_t prevState; - bool pullUp; - bool invertLogic; -}; - -class Button : public Element, - public LocalAction { +class Button : public SimpleButton { public: Button(int pin, bool pullUp = false, bool invertLogic = false); void onTimer(); - void onInit(); - void setSwNoiseFilterDelay(unsigned int newDelayMs); - void setDebounceDelay(unsigned int newDelayMs); void setHoldTime(unsigned int timeMs); void setMulticlickTime(unsigned int timeMs, bool bistableButton = false); protected: - ButtonState state; unsigned int holdTimeMs; unsigned int multiclickTimeMs; - uint8_t clickCounter; unsigned long lastStateChangeMs; - bool enableExtDetection; + uint8_t clickCounter; bool holdSend; bool bistable; }; diff --git a/lib/SuplaDevice/src/supla/control/relay.cpp b/lib/SuplaDevice/src/supla/control/relay.cpp index 081d1fe7..d47fd9a2 100644 --- a/lib/SuplaDevice/src/supla/control/relay.cpp +++ b/lib/SuplaDevice/src/supla/control/relay.cpp @@ -57,7 +57,7 @@ void Relay::onInit() { turnOff(); } - pinMode(pin, OUTPUT); // pin mode is set after setting pin value in order to + Supla::Io::pinMode(channel.getChannelNumber(), pin, OUTPUT); // pin mode is set after setting pin value in order to // avoid problems with LOW trigger relays } @@ -94,6 +94,9 @@ void Relay::turnOn(_supla_int_t duration) { Supla::Io::digitalWrite(channel.getChannelNumber(), pin, pinOnValue()); channel.setNewValue(true); + + // Schedule save in 5 s after state change + Supla::Storage::ScheduleSave(5000); } void Relay::turnOff(_supla_int_t duration) { @@ -102,6 +105,9 @@ void Relay::turnOff(_supla_int_t duration) { Supla::Io::digitalWrite(channel.getChannelNumber(), pin, pinOffValue()); channel.setNewValue(false); + + // Schedule save in 5 s after state change + Supla::Storage::ScheduleSave(5000); } bool Relay::isOn() { @@ -140,39 +146,39 @@ Channel *Relay::getChannel() { } void Relay::onSaveState() { - if (keepTurnOnDurationMs) { - Supla::Storage::WriteState((unsigned char *)&storedTurnOnDurationMs, - sizeof(storedTurnOnDurationMs)); - } + Supla::Storage::WriteState((unsigned char *)&storedTurnOnDurationMs, + sizeof(storedTurnOnDurationMs)); + bool enabled = false; if (stateOnInit < 0) { - bool enabled = isOn(); - Supla::Storage::WriteState((unsigned char *)&enabled, sizeof(enabled)); - } + enabled = isOn(); + } + Supla::Storage::WriteState((unsigned char *)&enabled, sizeof(enabled)); } void Relay::onLoadState() { + Supla::Storage::ReadState((unsigned char *)&storedTurnOnDurationMs, + sizeof(storedTurnOnDurationMs)); if (keepTurnOnDurationMs) { - Supla::Storage::ReadState((unsigned char *)&storedTurnOnDurationMs, - sizeof(storedTurnOnDurationMs)); Serial.print(F("Relay[")); Serial.print(channel.getChannelNumber()); Serial.print(F("]: restored durationMs: ")); Serial.println(storedTurnOnDurationMs); + } else { + storedTurnOnDurationMs = 0; } + bool enabled = false; + Supla::Storage::ReadState((unsigned char *)&enabled, sizeof(enabled)); if (stateOnInit < 0) { - bool enabled = false; - if (Supla::Storage::ReadState((unsigned char *)&enabled, sizeof(enabled))) { - Serial.print(F("Relay[")); - Serial.print(channel.getChannelNumber()); - Serial.print(F("]: restored relay state: ")); - if (enabled) { - Serial.println(F("ON")); - stateOnInit = STATE_ON_INIT_RESTORED_ON; - } else { - Serial.println(F("OFF")); - stateOnInit = STATE_ON_INIT_RESTORED_OFF; - } + Serial.print(F("Relay[")); + Serial.print(channel.getChannelNumber()); + Serial.print(F("]: restored relay state: ")); + if (enabled) { + Serial.println(F("ON")); + stateOnInit = STATE_ON_INIT_RESTORED_ON; + } else { + Serial.println(F("OFF")); + stateOnInit = STATE_ON_INIT_RESTORED_OFF; } } } diff --git a/lib/SuplaDevice/src/supla/control/roller_shutter.cpp b/lib/SuplaDevice/src/supla/control/roller_shutter.cpp index 630fb105..12e91b88 100644 --- a/lib/SuplaDevice/src/supla/control/roller_shutter.cpp +++ b/lib/SuplaDevice/src/supla/control/roller_shutter.cpp @@ -15,7 +15,7 @@ */ #include "roller_shutter.h" -#include "supla/storage/storage.h" +#include namespace Supla { namespace Control { @@ -53,10 +53,10 @@ RollerShutter::RollerShutter(int pinUp, int pinDown, bool highIsOn) } void RollerShutter::onInit() { - pinMode(pinUp, OUTPUT); - pinMode(pinDown, OUTPUT); - digitalWrite(pinUp, highIsOn ? LOW : HIGH); - digitalWrite(pinDown, highIsOn ? LOW : HIGH); + Supla::Io::digitalWrite(channel.getChannelNumber(), pinUp, highIsOn ? LOW : HIGH); + Supla::Io::digitalWrite(channel.getChannelNumber(), pinDown, highIsOn ? LOW : HIGH); + Supla::Io::pinMode(channel.getChannelNumber(), pinUp, OUTPUT); + Supla::Io::pinMode(channel.getChannelNumber(), pinDown, OUTPUT); } /* @@ -268,22 +268,24 @@ void RollerShutter::stopMovement() { switchOffRelays(); currentDirection = STOP_DIR; doNothingTime = millis(); + // Schedule save in 5 s after stop movement of roller shutter + Supla::Storage::ScheduleSave(5000); } void RollerShutter::relayDownOn() { - digitalWrite(pinDown, highIsOn ? HIGH : LOW); + Supla::Io::digitalWrite(channel.getChannelNumber(), pinDown, highIsOn ? HIGH : LOW); } void RollerShutter::relayUpOn() { - digitalWrite(pinUp, highIsOn ? HIGH : LOW); + Supla::Io::digitalWrite(channel.getChannelNumber(), pinUp, highIsOn ? HIGH : LOW); } void RollerShutter::relayDownOff() { - digitalWrite(pinDown, highIsOn ? LOW : HIGH); + Supla::Io::digitalWrite(channel.getChannelNumber(), pinDown, highIsOn ? LOW : HIGH); } void RollerShutter::relayUpOff() { - digitalWrite(pinUp, highIsOn ? LOW : HIGH); + Supla::Io::digitalWrite(channel.getChannelNumber(), pinUp, highIsOn ? LOW : HIGH); } void RollerShutter::startClosing() { diff --git a/lib/SuplaDevice/src/supla/control/sequence_button.cpp b/lib/SuplaDevice/src/supla/control/sequence_button.cpp new file mode 100644 index 00000000..f86dd4c8 --- /dev/null +++ b/lib/SuplaDevice/src/supla/control/sequence_button.cpp @@ -0,0 +1,137 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include "sequence_button.h" +#include + +Supla::Control::SequenceButton::SequenceButton(int pin, bool pullUp, bool invertLogic) + : SimpleButton(pin, pullUp, invertLogic), + lastStateChangeMs(0), + longestSequenceTimeDeltaWithMargin(800), + clickCounter(0), + sequenceDetectecion(true), + currentSequence(), + matchSequence(), + margin(0.3) { +} + +void Supla::Control::SequenceButton::onTimer() { + unsigned int timeDelta = millis() - lastStateChangeMs; + bool stateChanged = false; + int stateResult = state.update(); + if (stateResult == TO_PRESSED) { + stateChanged = true; + runAction(ON_PRESS); + runAction(ON_CHANGE); + } else if (stateResult == TO_RELEASED) { + stateChanged = true; + runAction(ON_RELEASE); + runAction(ON_CHANGE); + } + + if (stateChanged) { + lastStateChangeMs = millis(); + if (clickCounter > 0 && clickCounter < SEQUENCE_MAX_SIZE + 1) { + currentSequence.data[clickCounter - 1] = timeDelta; + } + if (clickCounter == 0) { + memset(currentSequence.data, 0, sizeof(uint16_t [SEQUENCE_MAX_SIZE])); + } + clickCounter++; + } + + if (!stateChanged) { + if (clickCounter > 0 && stateResult == RELEASED) { + if (timeDelta > longestSequenceTimeDeltaWithMargin) { + Serial.print(F("Recorded sequence: ")); + if (clickCounter > 31) { + clickCounter = 31; + } + for (int i = 0; i < clickCounter - 1; i++) { + Serial.print(currentSequence.data[i]); + Serial.print(F(", ")); + } + Serial.println(); + + int matchSequenceSize = 0; + for (; matchSequenceSize < 30; matchSequenceSize++) { + if (matchSequence.data[matchSequenceSize] == 0) { + break; + } + } + if (matchSequenceSize != clickCounter - 1) { + Serial.println(F("Sequence size doesn't match")); + runAction(ON_SEQUENCE_DOESNT_MATCH); + } else { + bool match = true; + for (int i = 0; i < clickCounter - 1; i++) { + unsigned int marginValue = calculateMargin(matchSequence.data[i]); + if (!(matchSequence.data[i] - marginValue <= currentSequence.data[i] && matchSequence.data[i] + marginValue >= currentSequence.data[i])) { + match = false; + break; + } + } + if (match) { + Serial.println(F("Sequence match")); + runAction(ON_SEQUENCE_MATCH); + } else { + Serial.println(F("Sequence doesn't match")); + runAction(ON_SEQUENCE_DOESNT_MATCH); + } + + } + clickCounter = 0; + } + } + } + +} + +unsigned int Supla::Control::SequenceButton::calculateMargin(unsigned int value) { + unsigned int result = margin*value; + if (result < 20) { + result = 20; + } + return result; +} + +void Supla::Control::SequenceButton::setMargin(float newMargin) { + margin = newMargin; + if (margin < 0) { + margin = 0; + } else if (margin > 1) { + margin = 1; + } +} + +void Supla::Control::SequenceButton::setSequence(uint16_t *sequence) { + uint16_t maxValue = 0; + for (int i = 0; i < SEQUENCE_MAX_SIZE; i++) { + matchSequence.data[i] = sequence[i]; + if (sequence[i] > maxValue) { + maxValue = sequence[i]; + } + } + maxValue *= 1.5; + if (maxValue < 500) { + maxValue = 500; + } + longestSequenceTimeDeltaWithMargin = maxValue; +} + +void Supla::Control::SequenceButton::getLastRecordedSequence(uint16_t *sequence) { + memcpy(sequence, currentSequence.data, sizeof(uint16_t [SEQUENCE_MAX_SIZE])); +} diff --git a/lib/SuplaDevice/src/supla/control/sequence_button.h b/lib/SuplaDevice/src/supla/control/sequence_button.h new file mode 100644 index 00000000..d4e12b12 --- /dev/null +++ b/lib/SuplaDevice/src/supla/control/sequence_button.h @@ -0,0 +1,58 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#ifndef _sequence_button_h +#define _sequence_button_h + +#include "button.h" + +namespace Supla { +namespace Control { + +#define SEQUENCE_MAX_SIZE 30 + +struct ClickSequence { + uint16_t data[SEQUENCE_MAX_SIZE]; +}; + +class SequenceButton : public SimpleButton { + public: + SequenceButton(int pin, bool pullUp = false, bool invertLogic = false); + + void onTimer(); + + void setSequence(uint16_t *sequence); + void setMargin(float); + void getLastRecordedSequence(uint16_t *sequence); + + protected: + unsigned long lastStateChangeMs; + uint16_t longestSequenceTimeDeltaWithMargin; + uint8_t clickCounter; + bool sequenceDetectecion; + + ClickSequence currentSequence; + ClickSequence matchSequence; + + float margin; + unsigned int calculateMargin(unsigned int); + +}; + +}; // namespace Control +}; // namespace Supla + +#endif diff --git a/lib/SuplaDevice/src/supla/control/simple_button.cpp b/lib/SuplaDevice/src/supla/control/simple_button.cpp new file mode 100644 index 00000000..7714f675 --- /dev/null +++ b/lib/SuplaDevice/src/supla/control/simple_button.cpp @@ -0,0 +1,108 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include "button.h" +#include "../io.h" + +Supla::Control::ButtonState::ButtonState(int pin, bool pullUp, bool invertLogic) + : debounceTimeMs(0), + filterTimeMs(0), + debounceDelayMs(50), + swNoiseFilterDelayMs(20), + pin(pin), + newStatusCandidate(LOW), + prevState(LOW), + pullUp(pullUp), + invertLogic(invertLogic) { +} + +int Supla::Control::ButtonState::update() { + if (millis() - debounceTimeMs > debounceDelayMs) { + int currentState = Supla::Io::digitalRead(pin); + if (currentState != prevState) { + // If status is changed, then make sure that it will be kept at + // least swNoiseFilterDelayMs ms to avoid noise + if (currentState != newStatusCandidate) { + newStatusCandidate = currentState; + filterTimeMs = millis(); + } else if (millis() - filterTimeMs > swNoiseFilterDelayMs) { + // If new status is kept at least swNoiseFilterDelayMs ms, then apply + // change of status + debounceTimeMs = millis(); + prevState = currentState; + if (currentState == valueOnPress()) { + return TO_PRESSED; + } else { + return TO_RELEASED; + } + } + } else { + // If current status is the same as prevState, then reset + // new status candidate + newStatusCandidate = prevState; + } + } + if (prevState == valueOnPress()) { + return PRESSED; + } else { + return RELEASED; + } +} + +Supla::Control::SimpleButton::SimpleButton(int pin, bool pullUp, bool invertLogic) + : state(pin, pullUp, invertLogic) { +} + +void Supla::Control::SimpleButton::onTimer() { + int stateResult = state.update(); + if (stateResult == TO_PRESSED) { + runAction(ON_PRESS); + runAction(ON_CHANGE); + } else if (stateResult == TO_RELEASED) { + runAction(ON_RELEASE); + runAction(ON_CHANGE); + } +} + +void Supla::Control::SimpleButton::onInit() { + state.init(); +} + +void Supla::Control::ButtonState::init() { + Supla::Io::pinMode(pin, pullUp ? INPUT_PULLUP : INPUT); + prevState = Supla::Io::digitalRead(pin); + newStatusCandidate = prevState; +} + +int Supla::Control::ButtonState::valueOnPress() { + return invertLogic ? LOW : HIGH; +} + +void Supla::Control::SimpleButton::setSwNoiseFilterDelay(unsigned int newDelayMs) { + state.setSwNoiseFilterDelay(newDelayMs); +} +void Supla::Control::ButtonState::setSwNoiseFilterDelay(unsigned int newDelayMs) { + swNoiseFilterDelayMs = newDelayMs; +} + +void Supla::Control::SimpleButton::setDebounceDelay(unsigned int newDelayMs) { + state.setDebounceDelay(newDelayMs); +} + +void Supla::Control::ButtonState::setDebounceDelay(unsigned int newDelayMs) { + debounceDelayMs = newDelayMs; +} + diff --git a/lib/SuplaDevice/src/supla/control/simple_button.h b/lib/SuplaDevice/src/supla/control/simple_button.h new file mode 100644 index 00000000..8b66fb7e --- /dev/null +++ b/lib/SuplaDevice/src/supla/control/simple_button.h @@ -0,0 +1,71 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#ifndef _simple_button_h +#define _simple_button_h + +#include + +#include "../element.h" +#include "../events.h" +#include "../local_action.h" + +namespace Supla { +namespace Control { + +enum StateResults {PRESSED, RELEASED, TO_PRESSED, TO_RELEASED}; + +class ButtonState { + public: + ButtonState(int pin, bool pullUp, bool invertLogic); + int update(); + void init(); + + void setSwNoiseFilterDelay(unsigned int newDelayMs); + void setDebounceDelay(unsigned int newDelayMs); + + protected: + int valueOnPress(); + + unsigned long debounceTimeMs; + unsigned long filterTimeMs; + unsigned int debounceDelayMs; + unsigned int swNoiseFilterDelayMs; + int pin; + int8_t newStatusCandidate; + int8_t prevState; + bool pullUp; + bool invertLogic; +}; + +class SimpleButton : public Element, + public LocalAction { + public: + SimpleButton(int pin, bool pullUp = false, bool invertLogic = false); + + void onTimer(); + void onInit(); + void setSwNoiseFilterDelay(unsigned int newDelayMs); + void setDebounceDelay(unsigned int newDelayMs); + + protected: + ButtonState state; +}; + +}; // namespace Control +}; // namespace Supla + +#endif diff --git a/lib/SuplaDevice/src/supla/control/virtual_relay.h b/lib/SuplaDevice/src/supla/control/virtual_relay.h index 1a54c51f..15ed52a4 100644 --- a/lib/SuplaDevice/src/supla/control/virtual_relay.h +++ b/lib/SuplaDevice/src/supla/control/virtual_relay.h @@ -46,6 +46,8 @@ class VirtualRelay : public Relay { state = true; channel.setNewValue(state); + // Schedule save in 5 s after state change + Supla::Storage::ScheduleSave(5000); } virtual void turnOff(_supla_int_t duration = 0) { @@ -54,6 +56,8 @@ class VirtualRelay : public Relay { state = false; channel.setNewValue(state); + // Schedule save in 5 s after state change + Supla::Storage::ScheduleSave(5000); } virtual bool isOn() { diff --git a/lib/SuplaDevice/src/supla/events.h b/lib/SuplaDevice/src/supla/events.h index c72c122f..cadbb616 100644 --- a/lib/SuplaDevice/src/supla/events.h +++ b/lib/SuplaDevice/src/supla/events.h @@ -34,7 +34,10 @@ enum Event { ON_CLICK_7, ON_CLICK_8, ON_CLICK_9, - ON_CLICK_10 + ON_CLICK_10, + ON_CRAZY_CLICKER, // triggered on >= 10 clicks + ON_SEQUENCE_MATCH, // triggered by SequenceButton + ON_SEQUENCE_DOESNT_MATCH // triggered by SequenceButton }; }; diff --git a/lib/SuplaDevice/src/supla/io.cpp b/lib/SuplaDevice/src/supla/io.cpp index 59cccc81..84c8684d 100644 --- a/lib/SuplaDevice/src/supla/io.cpp +++ b/lib/SuplaDevice/src/supla/io.cpp @@ -19,6 +19,26 @@ #include namespace Supla { +void Io::pinMode(uint8_t pin, uint8_t mode) { + return pinMode(-1, pin, mode); +} + +int Io::digitalRead(uint8_t pin) { + return digitalRead(-1, pin); +} + +void Io::digitalWrite(uint8_t pin, uint8_t val) { + digitalWrite(-1, pin, val); +} + +void Io::pinMode(int channelNumber, uint8_t pin, uint8_t mode) { + if (ioInstance) { + ioInstance->customPinMode(channelNumber, pin, mode); + } else { + ::pinMode(pin, mode); + } +} + int Io::digitalRead(int channelNumber, uint8_t pin) { if (ioInstance) { return ioInstance->customDigitalRead(channelNumber, pin); @@ -56,4 +76,9 @@ void Io::customDigitalWrite(int channelNumber, uint8_t pin, uint8_t val) { ::digitalWrite(pin, val); } +void Io::customPinMode(int channelNumber, uint8_t pin, uint8_t mode) { + (void)(channelNumber); + ::pinMode(pin, mode); +} + }; // namespace Supla diff --git a/lib/SuplaDevice/src/supla/io.h b/lib/SuplaDevice/src/supla/io.h index 461f8b6a..722d6685 100644 --- a/lib/SuplaDevice/src/supla/io.h +++ b/lib/SuplaDevice/src/supla/io.h @@ -30,12 +30,17 @@ namespace Supla { // changed. class Io { public: + static void pinMode(uint8_t pin, uint8_t mode); + static int digitalRead(uint8_t pin); + static void digitalWrite(uint8_t pin, uint8_t val); + static void pinMode(int channelNumber, uint8_t pin, uint8_t mode); static int digitalRead(int channelNumber, uint8_t pin); static void digitalWrite(int channelNumber, uint8_t pin, uint8_t val); static Io *ioInstance; Io(); + virtual void customPinMode(int channelNumber, uint8_t pin, uint8_t mode); virtual int customDigitalRead(int channelNumber, uint8_t pin); virtual void customDigitalWrite(int channelNumber, uint8_t pin, uint8_t val); }; diff --git a/lib/SuplaDevice/src/supla/network/esp32_wifi.h b/lib/SuplaDevice/src/supla/network/esp32_wifi.h index fa3915d1..e45ee3cc 100644 --- a/lib/SuplaDevice/src/supla/network/esp32_wifi.h +++ b/lib/SuplaDevice/src/supla/network/esp32_wifi.h @@ -14,120 +14,15 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#ifndef esp_wifi_h__ -#define esp_wifi_h__ +// DEPRECATED: please use esp_wifi.h instead -#include -#include +#ifndef esp32_wifi_h__ +#define esp32_wifi_h__ -#include "../supla_lib_config.h" -#include "network.h" - -#define MAX_SSID_SIZE 32 -#define MAX_WIFI_PASSWORD_SIZE 64 - -// TODO: change logs to supla_log +#include "esp_wifi.h" namespace Supla { -class ESP32Wifi : public Supla::Network { - public: - ESP32Wifi(const char *wifiSsid, - const char *wifiPassword, - IPAddress *ip = NULL) - : Network(ip) { - strcpy(ssid, wifiSsid); - strcpy(password, wifiPassword); - } - - int read(void *buf, int count) { - _supla_int_t size = client.available(); - - if (size > 0) { - if (size > count) size = count; - long readSize = client.read((uint8_t *)buf, size); -#ifdef SUPLA_COMM_DEBUG - Serial.print(F("Received: [")); - for (int i = 0; i < readSize; i++) { - Serial.print(static_cast(buf)[i], HEX); - Serial.print(F(" ")); - } - Serial.println(F("]")); -#endif - - return readSize; - } - return -1; - } - - int write(void *buf, int count) { -#ifdef SUPLA_COMM_DEBUG - Serial.print(F("Sending: [")); - for (int i = 0; i < count; i++) { - Serial.print(static_cast(buf)[i], HEX); - Serial.print(F(" ")); - } - Serial.println(F("]")); -#endif - long sendSize = client.write((const uint8_t *)buf, count); - return sendSize; - } - - int connect(const char *server, int port = -1) { - int connectionPort = (port == -1 ? 2015 : port); - supla_log( - LOG_DEBUG, "Establishing connection with: %s (port: %d)", server, connectionPort); - return client.connect(server, connectionPort); - } - - bool connected() { - return client.connected(); - } - - bool isReady() { - return WiFi.status() == WL_CONNECTED; - } - - void disconnect() { - client.stop(); - } - - // TODO: add handling of custom local ip - void setup() { - WiFiEventId_t event_gotIP = WiFi.onEvent( - [](WiFiEvent_t event, WiFiEventInfo_t info) { - Serial.print(F("local IP: ")); - Serial.println(WiFi.localIP()); - Serial.print(F("subnetMask: ")); - Serial.println(WiFi.subnetMask()); - Serial.print(F("gatewayIP: ")); - Serial.println(WiFi.gatewayIP()); - long rssi = WiFi.RSSI(); - Serial.print(F("Signal Strength (RSSI): ")); - Serial.print(rssi); - Serial.println(F(" dBm")); - }, - WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP); - - WiFiEventId_t event_disconnected = WiFi.onEvent( - [](WiFiEvent_t event, WiFiEventInfo_t info) { - Serial.println(F("wifi Station disconnected")); - }, - WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED); - - Serial.print(F("WIFI: establishing connection with SSID: \"")); - Serial.print(ssid); - Serial.println(F("\"")); - WiFi.begin(ssid, password); - yield(); - } - - protected: - WiFiClient client; - - char ssid[MAX_SSID_SIZE]; - char password[MAX_WIFI_PASSWORD_SIZE]; +typedef ESPWifi ESP32Wifi; }; -}; // namespace Supla - #endif diff --git a/lib/SuplaDevice/src/supla/network/esp_wifi.h b/lib/SuplaDevice/src/supla/network/esp_wifi.h index b819e23f..adbeac69 100644 --- a/lib/SuplaDevice/src/supla/network/esp_wifi.h +++ b/lib/SuplaDevice/src/supla/network/esp_wifi.h @@ -18,7 +18,13 @@ #define esp_wifi_h__ #include + +#ifdef ARDUINO_ARCH_ESP8266 #include +#else +#include +#endif + #include #include "../supla_lib_config.h" @@ -27,7 +33,9 @@ #define MAX_SSID_SIZE 32 #define MAX_WIFI_PASSWORD_SIZE 64 +#ifdef ARDUINO_ARCH_ESP8266 WiFiEventHandler gotIpEventHandler, disconnectedEventHandler; +#endif // TODO: change logs to supla_log @@ -42,6 +50,10 @@ class ESPWifi : public Supla::Network { password[0] = '\0'; setSsid(wifiSsid); setPassword(wifiPassword); +#ifdef ARDUINO_ARCH_ESP32 + enableSSL( + false); // current ESP32 WiFiClientSecure does not suport "setInsecure" +#endif } int read(void *buf, int count) { @@ -85,10 +97,18 @@ class ESPWifi : public Supla::Network { client = new WiFiClientSecure(); if (fingerprint.length() > 0) { message += " with certificate matching"; +#ifdef ARDUINO_ARCH_ESP8266 ((WiFiClientSecure *)client)->setFingerprint(fingerprint.c_str()); +#else + message += " - NOT SUPPORTED ON ESP32 implmentation"; +#endif } else { message += " without certificate matching"; +#ifdef ARDUINO_ARCH_ESP8266 ((WiFiClientSecure *)client)->setInsecure(); +#else + message += " - NOT SUPPORTED ON ESP32 implmentation"; +#endif } } else { message = "unsecured connection"; @@ -141,30 +161,52 @@ class ESPWifi : public Supla::Network { void setup() { if (!wifiConfigured) { wifiConfigured = true; - gotIpEventHandler = - WiFi.onStationModeGotIP([](const WiFiEventStationModeGotIP &event) { - (void)(event); - Serial.print(F("local IP: ")); - Serial.println(WiFi.localIP()); - Serial.print(F("subnetMask: ")); - Serial.println(WiFi.subnetMask()); - Serial.print(F("gatewayIP: ")); - Serial.println(WiFi.gatewayIP()); - long rssi = WiFi.RSSI(); - Serial.print(F("Signal strength (RSSI): ")); - Serial.print(rssi); - Serial.println(F(" dBm")); - }); - disconnectedEventHandler = WiFi.onStationModeDisconnected( - [](const WiFiEventStationModeDisconnected &event) { - (void)(event); - Serial.println(F("WiFi station disconnected")); - }); - - Serial.print(F("WiFi: establishing connection with SSID: \"")); - Serial.print(ssid); - Serial.println(F("\"")); - WiFi.begin(ssid, password); +#ifdef ARDUINO_ARCH_ESP8266 + gotIpEventHandler = + WiFi.onStationModeGotIP([](const WiFiEventStationModeGotIP &event) { + (void)(event); + Serial.print(F("local IP: ")); + Serial.println(WiFi.localIP()); + Serial.print(F("subnetMask: ")); + Serial.println(WiFi.subnetMask()); + Serial.print(F("gatewayIP: ")); + Serial.println(WiFi.gatewayIP()); + long rssi = WiFi.RSSI(); + Serial.print(F("Signal strength (RSSI): ")); + Serial.print(rssi); + Serial.println(F(" dBm")); + }); + disconnectedEventHandler = WiFi.onStationModeDisconnected( + [](const WiFiEventStationModeDisconnected &event) { + (void)(event); + Serial.println(F("WiFi station disconnected")); + }); +#else + WiFiEventId_t event_gotIP = WiFi.onEvent( + [](WiFiEvent_t event, WiFiEventInfo_t info) { + Serial.print(F("local IP: ")); + Serial.println(WiFi.localIP()); + Serial.print(F("subnetMask: ")); + Serial.println(WiFi.subnetMask()); + Serial.print(F("gatewayIP: ")); + Serial.println(WiFi.gatewayIP()); + long rssi = WiFi.RSSI(); + Serial.print(F("Signal Strength (RSSI): ")); + Serial.print(rssi); + Serial.println(F(" dBm")); + }, + WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP); + + WiFiEventId_t event_disconnected = WiFi.onEvent( + [](WiFiEvent_t event, WiFiEventInfo_t info) { + Serial.println(F("wifi Station disconnected")); + }, + WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED); +#endif + Serial.print(F("WiFi: establishing connection with SSID: \"")); + Serial.print(ssid); + Serial.println(F("\"")); + WiFi.begin(ssid, password); } else { Serial.println(F("WiFi: resetting WiFi connection")); if (client) { diff --git a/lib/SuplaDevice/src/supla/pv/afore.cpp b/lib/SuplaDevice/src/supla/pv/afore.cpp index 7b6f5484..06c0933f 100644 --- a/lib/SuplaDevice/src/supla/pv/afore.cpp +++ b/lib/SuplaDevice/src/supla/pv/afore.cpp @@ -30,7 +30,9 @@ Afore::Afore(IPAddress ip, int port, const char *loginAndPass) vFound(false), varFound(false), dataIsReady(false), - dataFetchInProgress(false) { + dataFetchInProgress(false), + connectionTimeoutMs(0) { + refreshRateSec = 15; int len = strlen(loginAndPass); if (len > LOGIN_AND_PASSOWORD_MAX_LENGTH) { len = LOGIN_AND_PASSOWORD_MAX_LENGTH; @@ -40,6 +42,13 @@ Afore::Afore(IPAddress ip, int port, const char *loginAndPass) void Afore::iterateAlways() { if (dataFetchInProgress) { + if (millis() - connectionTimeoutMs > 30000) { + Serial.println(F("AFORE: connection timeout. Remote host is not responding")); + pvClient.stop(); + dataFetchInProgress = false; + dataIsReady = false; + return; + } if (!pvClient.connected()) { Serial.println(F("AFORE fetch completed")); dataFetchInProgress = false; @@ -105,12 +114,13 @@ void Afore::iterateAlways() { bool Afore::iterateConnected(void *srpc) { if (!dataFetchInProgress) { - if (lastReadTime == 0 || millis() - lastReadTime > 15000) { + if (lastReadTime == 0 || millis() - lastReadTime > refreshRateSec*1000) { lastReadTime = millis(); Serial.println(F("AFORE connecting")); if (pvClient.connect(ip, port)) { retryCounter = 0; dataFetchInProgress = true; + connectionTimeoutMs = lastReadTime; Serial.println(F("Succesful connect")); pvClient.print("GET /status.html HTTP/1.1\nAuthorization: Basic "); @@ -137,3 +147,4 @@ bool Afore::iterateConnected(void *srpc) { void Afore::readValuesFromDevice() { } + diff --git a/lib/SuplaDevice/src/supla/pv/afore.h b/lib/SuplaDevice/src/supla/pv/afore.h index 85a978e6..bb46c26a 100644 --- a/lib/SuplaDevice/src/supla/pv/afore.h +++ b/lib/SuplaDevice/src/supla/pv/afore.h @@ -55,6 +55,7 @@ class Afore : public Supla::Sensor::OnePhaseElectricityMeter { bool varFound; bool dataIsReady; bool dataFetchInProgress; + unsigned long connectionTimeoutMs; }; }; // namespace PV }; // namespace Supla diff --git a/lib/SuplaDevice/src/supla/pv/fronius.cpp b/lib/SuplaDevice/src/supla/pv/fronius.cpp index 3e2200b4..871f80a3 100644 --- a/lib/SuplaDevice/src/supla/pv/fronius.cpp +++ b/lib/SuplaDevice/src/supla/pv/fronius.cpp @@ -36,11 +36,20 @@ Fronius::Fronius(IPAddress ip, int port, int deviceId) deviceId(deviceId), startCharFound(false), dataIsReady(false), - dataFetchInProgress(false) { + dataFetchInProgress(false), + connectionTimeoutMs(0) { + refreshRateSec = 15; } void Fronius::iterateAlways() { if (dataFetchInProgress) { + if (millis() - connectionTimeoutMs > 30000) { + Serial.println(F("Fronius: connection timeout. Remote host is not responding")); + pvClient.stop(); + dataFetchInProgress = false; + dataIsReady = false; + return; + } if (!pvClient.connected()) { Serial.println(F("Fronius fetch completed")); dataFetchInProgress = false; @@ -147,13 +156,14 @@ void Fronius::iterateAlways() { bool Fronius::iterateConnected(void *srpc) { if (!dataFetchInProgress) { - if (lastReadTime == 0 || millis() - lastReadTime > 15000) { + if (lastReadTime == 0 || millis() - lastReadTime > refreshRateSec*1000) { lastReadTime = millis(); Serial.print(F("Fronius connecting ")); Serial.println(deviceId); if (pvClient.connect(ip, port)) { retryCounter = 0; dataFetchInProgress = true; + connectionTimeoutMs = lastReadTime; Serial.println(F("Succesful connect")); char buf[100]; @@ -189,3 +199,4 @@ bool Fronius::iterateConnected(void *srpc) { void Fronius::readValuesFromDevice() { } + diff --git a/lib/SuplaDevice/src/supla/pv/fronius.h b/lib/SuplaDevice/src/supla/pv/fronius.h index 1f761732..f32d4699 100644 --- a/lib/SuplaDevice/src/supla/pv/fronius.h +++ b/lib/SuplaDevice/src/supla/pv/fronius.h @@ -56,6 +56,7 @@ class Fronius : public Supla::Sensor::OnePhaseElectricityMeter { bool startCharFound; bool dataIsReady; bool dataFetchInProgress; + unsigned long connectionTimeoutMs; }; }; // namespace PV }; // namespace Supla diff --git a/lib/SuplaDevice/src/supla/sensor/binary.cpp b/lib/SuplaDevice/src/supla/sensor/binary.cpp new file mode 100644 index 00000000..b305457d --- /dev/null +++ b/lib/SuplaDevice/src/supla/sensor/binary.cpp @@ -0,0 +1,45 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include "binary.h" +#include "../io.h" + +Supla::Sensor::Binary::Binary(int pin, bool pullUp = false) + : pin(pin), pullUp(pullUp), lastReadTime(0) { + channel.setType(SUPLA_CHANNELTYPE_SENSORNO); +} + +bool Supla::Sensor::Binary::getValue() { + return Supla::Io::digitalRead(channel.getChannelNumber(), pin) == LOW ? false + : true; +} + +void Supla::Sensor::Binary::iterateAlways() { + if (lastReadTime + 100 < millis()) { + lastReadTime = millis(); + channel.setNewValue(getValue()); + } +} + +void Supla::Sensor::Binary::onInit() { + Supla::Io::pinMode( + channel.getChannelNumber(), pin, pullUp ? INPUT_PULLUP : INPUT); + channel.setNewValue(getValue()); +} + +Supla::Channel *Supla::Sensor::Binary::getChannel() { + return &channel; +} diff --git a/lib/SuplaDevice/src/supla/sensor/binary.h b/lib/SuplaDevice/src/supla/sensor/binary.h index 8bd7e52d..31db9fff 100644 --- a/lib/SuplaDevice/src/supla/sensor/binary.h +++ b/lib/SuplaDevice/src/supla/sensor/binary.h @@ -21,38 +21,19 @@ #include "../channel.h" #include "../element.h" -#include "../io.h" namespace Supla { namespace Sensor { class Binary : public Element { public: - Binary(int pin, bool pullUp = false) : pin(pin), pullUp(pullUp), lastReadTime(0) { - channel.setType(SUPLA_CHANNELTYPE_SENSORNO); - } - - bool getValue() { - return Supla::Io::digitalRead(channel.getChannelNumber(), pin) == LOW - ? false - : true; - } - - void iterateAlways() { - if (lastReadTime + 100 < millis()) { - lastReadTime = millis(); - channel.setNewValue(getValue()); - } - } - - void onInit() { - pinMode(pin, pullUp ? INPUT_PULLUP : INPUT); - channel.setNewValue(getValue()); - } + Binary(int pin, bool pullUp); + bool getValue(); + void iterateAlways(); + void onInit(); protected: - Channel *getChannel() { - return &channel; - } + Channel *getChannel(); + Channel channel; int pin; bool pullUp; diff --git a/lib/SuplaDevice/src/supla/sensor/electricity_meter.cpp b/lib/SuplaDevice/src/supla/sensor/electricity_meter.cpp new file mode 100644 index 00000000..13fbf36a --- /dev/null +++ b/lib/SuplaDevice/src/supla/sensor/electricity_meter.cpp @@ -0,0 +1,260 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include + +#include "electricity_meter.h" + +Supla::Sensor::ElectricityMeter::ElectricityMeter() + : valueChanged(false), lastReadTime(0), refreshRateSec(5) { + extChannel.setType(SUPLA_CHANNELTYPE_ELECTRICITY_METER); + extChannel.setDefault(SUPLA_CHANNELFNC_ELECTRICITY_METER); + memset(&emValue, 0, sizeof(emValue)); + emValue.period = 5; + for (int i = 0; i < MAX_PHASES; i++) { + rawCurrent[i] = 0; + } + currentMeasurementAvailable = false; +} + +void Supla::Sensor::ElectricityMeter::updateChannelValues() { + if (!valueChanged) { + return; + } + valueChanged = false; + + emValue.m_count = 1; + + // Update current messurement precision based on last updates + if (currentMeasurementAvailable) { + bool over65A = false; + for (int i = 0; i < MAX_PHASES; i++) { + if (rawCurrent[i] > 65000) { + over65A = true; + } + } + + for (int i = 0; i < MAX_PHASES; i++) { + if (over65A) { + emValue.m[0].current[i] = rawCurrent[i] / 10; + } else { + emValue.m[0].current[i] = rawCurrent[i]; + } + } + + if (over65A) { + emValue.measured_values ^= (!EM_VAR_CURRENT); + emValue.measured_values |= EM_VAR_CURRENT_OVER_65A; + } else { + emValue.measured_values ^= (!EM_VAR_CURRENT_OVER_65A); + emValue.measured_values |= EM_VAR_CURRENT; + } + } + + // Prepare extended channel value + srpc_evtool_v2_emextended2extended(&emValue, extChannel.getExtValue()); + extChannel.setNewValue(emValue); +} + +// energy in 0.00001 kWh +void Supla::Sensor::ElectricityMeter::setFwdActEnergy( + int phase, unsigned _supla_int64_t energy) { + if (phase >= 0 && phase < MAX_PHASES) { + if (emValue.total_forward_active_energy[phase] != energy) { + valueChanged = true; + } + emValue.total_forward_active_energy[phase] = energy; + emValue.measured_values |= EM_VAR_FORWARD_ACTIVE_ENERGY; + } +} + +// energy in 0.00001 kWh +void Supla::Sensor::ElectricityMeter::setRvrActEnergy( + int phase, unsigned _supla_int64_t energy) { + if (phase >= 0 && phase < MAX_PHASES) { + if (emValue.total_reverse_active_energy[phase] != energy) { + valueChanged = true; + } + emValue.total_reverse_active_energy[phase] = energy; + emValue.measured_values |= EM_VAR_REVERSE_ACTIVE_ENERGY; + } +} + +// energy in 0.00001 kWh +void Supla::Sensor::ElectricityMeter::setFwdReactEnergy( + int phase, unsigned _supla_int64_t energy) { + if (phase >= 0 && phase < MAX_PHASES) { + if (emValue.total_forward_reactive_energy[phase] != energy) { + valueChanged = true; + } + emValue.total_forward_reactive_energy[phase] = energy; + emValue.measured_values |= EM_VAR_FORWARD_REACTIVE_ENERGY; + } +} + +// energy in 0.00001 kWh +void Supla::Sensor::ElectricityMeter::setRvrReactEnergy( + int phase, unsigned _supla_int64_t energy) { + if (phase >= 0 && phase < MAX_PHASES) { + if (emValue.total_reverse_reactive_energy[phase] != energy) { + valueChanged = true; + } + emValue.total_reverse_reactive_energy[phase] = energy; + emValue.measured_values |= EM_VAR_REVERSE_REACTIVE_ENERGY; + } +} + +// voltage in 0.01 V +void Supla::Sensor::ElectricityMeter::setVoltage( + int phase, unsigned _supla_int16_t voltage) { + if (phase >= 0 && phase < MAX_PHASES) { + if (emValue.m[0].voltage[phase] != voltage) { + valueChanged = true; + } + emValue.m[0].voltage[phase] = voltage; + emValue.measured_values |= EM_VAR_VOLTAGE; + } +} + +// current in 0.001 A +void Supla::Sensor::ElectricityMeter::setCurrent( + int phase, unsigned _supla_int_t current) { + if (phase >= 0 && phase < MAX_PHASES) { + if (rawCurrent[phase] != current) { + valueChanged = true; + } + rawCurrent[phase] = current; + currentMeasurementAvailable = true; + } +} + +// Frequency in 0.01 Hz +void Supla::Sensor::ElectricityMeter::setFreq(unsigned _supla_int16_t freq) { + if (emValue.m[0].freq != freq) { + valueChanged = true; + } + emValue.m[0].freq = freq; + emValue.measured_values |= EM_VAR_FREQ; +} + +// power in 0.00001 kW +void Supla::Sensor::ElectricityMeter::setPowerActive(int phase, + _supla_int_t power) { + if (phase >= 0 && phase < MAX_PHASES) { + if (emValue.m[0].power_active[phase] != power) { + valueChanged = true; + } + emValue.m[0].power_active[phase] = power; + emValue.measured_values |= EM_VAR_POWER_ACTIVE; + } +} + +// power in 0.00001 kvar +void Supla::Sensor::ElectricityMeter::setPowerReactive(int phase, + _supla_int_t power) { + if (phase >= 0 && phase < MAX_PHASES) { + if (emValue.m[0].power_reactive[phase] != power) { + valueChanged = true; + } + emValue.m[0].power_reactive[phase] = power; + emValue.measured_values |= EM_VAR_POWER_REACTIVE; + } +} + +// power in 0.00001 kVA +void Supla::Sensor::ElectricityMeter::setPowerApparent(int phase, + _supla_int_t power) { + if (phase >= 0 && phase < MAX_PHASES) { + if (emValue.m[0].power_apparent[phase] != power) { + valueChanged = true; + } + emValue.m[0].power_apparent[phase] = power; + emValue.measured_values |= EM_VAR_POWER_APPARENT; + } +} + +// power in 0.001 +void Supla::Sensor::ElectricityMeter::setPowerFactor(int phase, + _supla_int_t powerFactor) { + if (phase >= 0 && phase < MAX_PHASES) { + if (emValue.m[0].power_factor[phase] != powerFactor) { + valueChanged = true; + } + emValue.m[0].power_factor[phase] = powerFactor; + emValue.measured_values |= EM_VAR_POWER_FACTOR; + } +} + +// phase angle in 0.1 degree +void Supla::Sensor::ElectricityMeter::setPhaseAngle(int phase, + _supla_int_t phaseAngle) { + if (phase >= 0 && phase < MAX_PHASES) { + if (emValue.m[0].phase_angle[phase] != phaseAngle) { + valueChanged = true; + } + emValue.m[0].phase_angle[phase] = phaseAngle; + emValue.measured_values |= EM_VAR_PHASE_ANGLE; + } +} + +void Supla::Sensor::ElectricityMeter::resetReadParameters() { + if (emValue.measured_values != 0) { + emValue.measured_values = 0; + memset(&emValue.m[0], 0, sizeof(TElectricityMeter_Measurement)); + valueChanged = true; + } +} + +// Please implement this class for reading value from elecricity meter device. +// It will be called every 5 s. Use set methods defined above in order to +// set values on channel. Don't use any other method to modify channel values. +void Supla::Sensor::ElectricityMeter::readValuesFromDevice() { +} + +// Put here initialization code for electricity meter device. +// It will be called within SuplaDevce.begin method. +// It should also read first data set, so at the end it should call those two +// methods: +// readValuesFromDevice(); +// updateChannelValues(); +void Supla::Sensor::ElectricityMeter::onInit() { +} + +void Supla::Sensor::ElectricityMeter::iterateAlways() { + if (millis() - lastReadTime > refreshRateSec*1000) { + lastReadTime = millis(); + readValuesFromDevice(); + updateChannelValues(); + } +} + +// Implement this method to reset stored energy value (i.e. to set energy +// counter back to 0 kWh +void Supla::Sensor::ElectricityMeter::resetStorage() { +} + +Supla::Channel *Supla::Sensor::ElectricityMeter::getChannel() { + return &extChannel; +} + +void Supla::Sensor::ElectricityMeter::setResreshRate(unsigned int sec) { + refreshRateSec = sec; + if (refreshRateSec == 0) { + refreshRateSec = 1; + } +} + + diff --git a/lib/SuplaDevice/src/supla/sensor/electricity_meter.h b/lib/SuplaDevice/src/supla/sensor/electricity_meter.h index 3180e7e3..713ea99c 100644 --- a/lib/SuplaDevice/src/supla/sensor/electricity_meter.h +++ b/lib/SuplaDevice/src/supla/sensor/electricity_meter.h @@ -17,7 +17,6 @@ #ifndef _electricity_meter_h #define _electricity_meter_h -#include #include #include "../channel_extended.h" @@ -30,199 +29,52 @@ namespace Supla { namespace Sensor { class ElectricityMeter : public Element { public: - ElectricityMeter() : valueChanged(false), lastReadTime(0) { - extChannel.setType(SUPLA_CHANNELTYPE_ELECTRICITY_METER); - extChannel.setDefault(SUPLA_CHANNELFNC_ELECTRICITY_METER); - memset(&emValue, 0, sizeof(emValue)); - emValue.period = 5; - for (int i = 0; i < MAX_PHASES; i++) { - rawCurrent[i] = 0; - } - currentMeasurementAvailable = false; - } + ElectricityMeter(); - virtual void updateChannelValues() { - if (!valueChanged) { - return; - } - valueChanged = false; - - emValue.m_count = 1; - - // Update current messurement precision based on last updates - if (currentMeasurementAvailable) { - bool over65A = false; - for (int i = 0; i < MAX_PHASES; i++) { - if (rawCurrent[i] > 65000) { - over65A = true; - } - } - - for (int i = 0; i < MAX_PHASES; i++) { - if (over65A) { - emValue.m[0].current[i] = rawCurrent[i] / 10; - } else { - emValue.m[0].current[i] = rawCurrent[i]; - } - } - - if (over65A) { - emValue.measured_values ^= (!EM_VAR_CURRENT); - emValue.measured_values |= EM_VAR_CURRENT_OVER_65A; - } else { - emValue.measured_values ^= (!EM_VAR_CURRENT_OVER_65A); - emValue.measured_values |= EM_VAR_CURRENT; - } - } - - // Prepare extended channel value - srpc_evtool_v2_emextended2extended(&emValue, extChannel.getExtValue()); - extChannel.setNewValue(emValue); - } + virtual void updateChannelValues(); // energy in 0.00001 kWh - void setFwdActEnergy(int phase, unsigned _supla_int64_t energy) { - if (phase >= 0 && phase < MAX_PHASES) { - if (emValue.total_forward_active_energy[phase] != energy) { - valueChanged = true; - } - emValue.total_forward_active_energy[phase] = energy; - emValue.measured_values |= EM_VAR_FORWARD_ACTIVE_ENERGY; - } - } + void setFwdActEnergy(int phase, unsigned _supla_int64_t energy); // energy in 0.00001 kWh - void setRvrActEnergy(int phase, unsigned _supla_int64_t energy) { - if (phase >= 0 && phase < MAX_PHASES) { - if (emValue.total_reverse_active_energy[phase] != energy) { - valueChanged = true; - } - emValue.total_reverse_active_energy[phase] = energy; - emValue.measured_values |= EM_VAR_REVERSE_ACTIVE_ENERGY; - } - } + void setRvrActEnergy(int phase, unsigned _supla_int64_t energy); // energy in 0.00001 kWh - void setFwdReactEnergy(int phase, unsigned _supla_int64_t energy) { - if (phase >= 0 && phase < MAX_PHASES) { - if (emValue.total_forward_reactive_energy[phase] != energy) { - valueChanged = true; - } - emValue.total_forward_reactive_energy[phase] = energy; - emValue.measured_values |= EM_VAR_FORWARD_REACTIVE_ENERGY; - } - } + void setFwdReactEnergy(int phase, unsigned _supla_int64_t energy); // energy in 0.00001 kWh - void setRvrReactEnergy(int phase, unsigned _supla_int64_t energy) { - if (phase >= 0 && phase < MAX_PHASES) { - if (emValue.total_reverse_reactive_energy[phase] != energy) { - valueChanged = true; - } - emValue.total_reverse_reactive_energy[phase] = energy; - emValue.measured_values |= EM_VAR_REVERSE_REACTIVE_ENERGY; - } - } + void setRvrReactEnergy(int phase, unsigned _supla_int64_t energy); // voltage in 0.01 V - void setVoltage(int phase, unsigned _supla_int16_t voltage) { - if (phase >= 0 && phase < MAX_PHASES) { - if (emValue.m[0].voltage[phase] != voltage) { - valueChanged = true; - } - emValue.m[0].voltage[phase] = voltage; - emValue.measured_values |= EM_VAR_VOLTAGE; - } - } + void setVoltage(int phase, unsigned _supla_int16_t voltage); // current in 0.001 A - void setCurrent(int phase, unsigned _supla_int_t current) { - if (phase >= 0 && phase < MAX_PHASES) { - if (rawCurrent[phase] != current) { - valueChanged = true; - } - rawCurrent[phase] = current; - currentMeasurementAvailable = true; - } - } + void setCurrent(int phase, unsigned _supla_int_t current); // Frequency in 0.01 Hz - void setFreq(unsigned _supla_int16_t freq) { - if (emValue.m[0].freq != freq) { - valueChanged = true; - } - emValue.m[0].freq = freq; - emValue.measured_values |= EM_VAR_FREQ; - } + void setFreq(unsigned _supla_int16_t freq); // power in 0.00001 kW - void setPowerActive(int phase, _supla_int_t power) { - if (phase >= 0 && phase < MAX_PHASES) { - if (emValue.m[0].power_active[phase] != power) { - valueChanged = true; - } - emValue.m[0].power_active[phase] = power; - emValue.measured_values |= EM_VAR_POWER_ACTIVE; - } - } + void setPowerActive(int phase, _supla_int_t power); // power in 0.00001 kvar - void setPowerReactive(int phase, _supla_int_t power) { - if (phase >= 0 && phase < MAX_PHASES) { - if (emValue.m[0].power_reactive[phase] != power) { - valueChanged = true; - } - emValue.m[0].power_reactive[phase] = power; - emValue.measured_values |= EM_VAR_POWER_REACTIVE; - } - } + void setPowerReactive(int phase, _supla_int_t power); // power in 0.00001 kVA - void setPowerApparent(int phase, _supla_int_t power) { - if (phase >= 0 && phase < MAX_PHASES) { - if (emValue.m[0].power_apparent[phase] != power) { - valueChanged = true; - } - emValue.m[0].power_apparent[phase] = power; - emValue.measured_values |= EM_VAR_POWER_APPARENT; - } - } + void setPowerApparent(int phase, _supla_int_t power); // power in 0.001 - void setPowerFactor(int phase, _supla_int_t powerFactor) { - if (phase >= 0 && phase < MAX_PHASES) { - if (emValue.m[0].power_factor[phase] != powerFactor) { - valueChanged = true; - } - emValue.m[0].power_factor[phase] = powerFactor; - emValue.measured_values |= EM_VAR_POWER_FACTOR; - } - } + void setPowerFactor(int phase, _supla_int_t powerFactor); // phase angle in 0.1 degree - void setPhaseAngle(int phase, _supla_int_t phaseAngle) { - if (phase >= 0 && phase < MAX_PHASES) { - if (emValue.m[0].phase_angle[phase] != phaseAngle) { - valueChanged = true; - } - emValue.m[0].phase_angle[phase] = phaseAngle; - emValue.measured_values |= EM_VAR_PHASE_ANGLE; - } - } + void setPhaseAngle(int phase, _supla_int_t phaseAngle); - void resetReadParameters() { - if (emValue.measured_values != 0) { - emValue.measured_values = 0; - memset(&emValue.m[0], 0, sizeof(TElectricityMeter_Measurement)); - valueChanged = true; - } - } + void resetReadParameters(); // Please implement this class for reading value from elecricity meter device. // It will be called every 5 s. Use set methods defined above in order to // set values on channel. Don't use any other method to modify channel values. - virtual void readValuesFromDevice() { - } + virtual void readValuesFromDevice(); // Put here initialization code for electricity meter device. // It will be called within SuplaDevce.begin method. @@ -230,35 +82,30 @@ class ElectricityMeter : public Element { // methods: // readValuesFromDevice(); // updateChannelValues(); - void onInit() { - } + void onInit(); - void iterateAlways() { - if (lastReadTime + 5000 < millis()) { - lastReadTime = millis(); - readValuesFromDevice(); - updateChannelValues(); - } - } + void iterateAlways(); // Implement this method to reset stored energy value (i.e. to set energy // counter back to 0 kWh - virtual void resetStorage() { - } + virtual void resetStorage(); + + void setResreshRate(unsigned int sec); protected: - Channel *getChannel() { - return &extChannel; - } + Channel *getChannel(); + TElectricityMeter_ExtendedValue_V2 emValue; ChannelExtended extChannel; unsigned _supla_int_t rawCurrent[MAX_PHASES]; bool valueChanged; bool currentMeasurementAvailable; unsigned long lastReadTime; + unsigned int refreshRateSec; }; }; // namespace Sensor }; // namespace Supla #endif + diff --git a/lib/SuplaDevice/src/supla/sensor/impulse_counter.cpp b/lib/SuplaDevice/src/supla/sensor/impulse_counter.cpp index d710eea3..4589a9fd 100644 --- a/lib/SuplaDevice/src/supla/sensor/impulse_counter.cpp +++ b/lib/SuplaDevice/src/supla/sensor/impulse_counter.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include "impulse_counter.h" @@ -52,9 +53,9 @@ ImpulseCounter::ImpulseCounter(int _impulsePin, void ImpulseCounter::onInit() { if (inputPullup) { - pinMode(impulsePin, INPUT_PULLUP); + Supla::Io::pinMode(channel.getChannelNumber(), impulsePin, INPUT_PULLUP); } else { - pinMode(impulsePin, INPUT); + Supla::Io::pinMode(channel.getChannelNumber(), impulsePin, INPUT); } } @@ -88,7 +89,7 @@ void ImpulseCounter::incCounter() { } void ImpulseCounter::onFastTimer() { - int currentState = digitalRead(impulsePin); + int currentState = Supla::Io::digitalRead(channel.getChannelNumber(), impulsePin); if (prevState == (detectLowToHigh == true ? LOW : HIGH)) { if (millis() - lastImpulseMillis > debounceDelay) { if (currentState == (detectLowToHigh == true ? HIGH : LOW)) { diff --git a/lib/SuplaDevice/src/supla/storage/storage.cpp b/lib/SuplaDevice/src/supla/storage/storage.cpp index 52dfaf8e..8e21d098 100644 --- a/lib/SuplaDevice/src/supla/storage/storage.cpp +++ b/lib/SuplaDevice/src/supla/storage/storage.cpp @@ -83,6 +83,12 @@ bool Storage::SaveStateAllowed(unsigned long ms) { return false; } +void Storage::ScheduleSave(unsigned long delayMs) { + if (Instance()) { + Instance()->scheduleSave(delayMs); + } +} + Storage::Storage(unsigned int storageStartingOffset) : storageStartingOffset(storageStartingOffset), deviceConfigOffset(0), @@ -319,3 +325,11 @@ bool Storage::saveStateAllowed(unsigned long ms) { return false; } +void Storage::scheduleSave(unsigned long delayMs) { + unsigned long currentMs = millis(); + unsigned long newTimestamp = currentMs - saveStatePeriod - 1 + delayMs; + + if (currentMs - lastWriteTimestamp < currentMs - newTimestamp) { + lastWriteTimestamp = newTimestamp; + } +} diff --git a/lib/SuplaDevice/src/supla/storage/storage.h b/lib/SuplaDevice/src/supla/storage/storage.h index 66284302..f25a4091 100644 --- a/lib/SuplaDevice/src/supla/storage/storage.h +++ b/lib/SuplaDevice/src/supla/storage/storage.h @@ -36,6 +36,7 @@ class Storage { static void PrepareState(bool dryRun = false); static bool FinalizeSaveState(); static bool SaveStateAllowed(unsigned long); + static void ScheduleSave(unsigned long delayMs); Storage(unsigned int storageStartingOffset = 0); @@ -51,6 +52,7 @@ class Storage { virtual void prepareState(bool performDryRun); virtual bool finalizeSaveState(); virtual bool saveStateAllowed(unsigned long); + virtual void scheduleSave(unsigned long delayMs); virtual void commit() = 0; diff --git a/lib/SuplaDevice/src/supla/timer.cpp b/lib/SuplaDevice/src/supla/timer.cpp index b77eb54a..a0a50ec7 100644 --- a/lib/SuplaDevice/src/supla/timer.cpp +++ b/lib/SuplaDevice/src/supla/timer.cpp @@ -20,7 +20,7 @@ #include "timer.h" #if defined(ARDUINO_ARCH_ESP32) -#include +#include #endif namespace { @@ -38,14 +38,14 @@ void esp_fastTimer_cb(void *timer_arg) { SuplaDevice.onFastTimer(); } #elif defined(ARDUINO_ARCH_ESP32) -hw_timer_t *supla_esp_timer = NULL; -hw_timer_t *supla_esp_fastTimer = NULL; +Ticker supla_esp_timer; +Ticker supla_esp_fastTimer; -void IRAM_ATTR esp_timer_cb() { +void esp_timer_cb() { SuplaDevice.onTimer(); } -void IRAM_ATTR esp_fastTimer_cb() { +void esp_fastTimer_cb() { SuplaDevice.onFastTimer(); } #else @@ -71,17 +71,8 @@ void initTimers() { os_timer_arm(&supla_esp_fastTimer, 1, 1); #elif defined(ARDUINO_ARCH_ESP32) - supla_esp_timer = timerBegin(0, 80, true); // timer 0, div 80 - timerAttachInterrupt(supla_esp_timer, &esp_timer_cb, true); // attach callback - timerAlarmWrite(supla_esp_timer, 10 * 1000, false); // set time in us - timerAlarmEnable(supla_esp_timer); // enable interrupt - - supla_esp_fastTimer = timerBegin(1, 80, true); - timerAttachInterrupt(supla_esp_fastTimer, - &esp_fastTimer_cb, - true); // attach callback - timerAlarmWrite(supla_esp_fastTimer, 1 * 1000, false); // set time in us - timerAlarmEnable(supla_esp_fastTimer); // enable interrupt + supla_esp_timer.attach_ms(10, esp_timer_cb); + supla_esp_fastTimer.attach_ms(1, esp_fastTimer_cb); #else // Timer 1 for interrupt frequency 100 Hz (10 ms) TCCR1A = 0; // set entire TCCR1A register to 0 From a72e11268a37707b44ac07c620a857f6b78a4125 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Thu, 14 Jan 2021 08:29:41 +0100 Subject: [PATCH 032/165] aktualizacja SuplaDevice --- lib/SuplaDevice/examples/Afore/Afore.ino | 33 +-- lib/SuplaDevice/examples/DHT/DHT.ino | 35 ++- .../DallasTemperature/DallasTemperature.ino | 35 ++- lib/SuplaDevice/examples/Fronius/Fronius.ino | 33 +-- .../HC_SR04_Distance_sensor.ino | 37 ++- .../ImpulseCounter/ImpulseCounter.ino | 36 ++- .../examples/Pzem_V_2/Pzem_V_2.ino | 28 +- .../examples/Pzem_V_3/Pzem_V_3.ino | 80 +++--- lib/SuplaDevice/examples/RGBW/RGBW.ino | 35 ++- .../examples/RollerShutter/RollerShutter.ino | 35 +-- .../SequenceButton/SequenceButton.ino | 35 ++- lib/SuplaDevice/extras/test/CMakeLists.txt | 20 +- .../test/ChannelTests/channel_tests.cpp | 270 ++++++++++++++++++ .../extras/test/IoTests/io_tests.cpp | 26 ++ .../extras/test/UptimeTests/uptime_tests.cpp | 16 ++ lib/SuplaDevice/extras/test/doubles/Arduino.h | 109 +++++++ .../extras/test/doubles/arduino_mock.cpp | 59 ++++ lib/SuplaDevice/extras/test/doubles/log.cpp | 22 ++ lib/SuplaDevice/extras/test/doubles/srpc.cpp | 29 ++ lib/SuplaDevice/src/CMakeLists.txt | 4 + lib/SuplaDevice/src/SuplaDevice.cpp | 1 - .../src/supla-common/IEEE754tools.h | 7 - lib/SuplaDevice/src/supla/channel.cpp | 119 +++++--- lib/SuplaDevice/src/supla/channel.h | 23 +- lib/SuplaDevice/src/supla/control/relay.h | 3 +- lib/SuplaDevice/src/supla/control/rgbw_base.h | 26 +- .../src/supla/control/roller_shutter.cpp | 29 +- .../src/supla/control/roller_shutter.h | 4 +- lib/SuplaDevice/src/supla/element.cpp | 4 + lib/SuplaDevice/src/supla/element.h | 3 +- lib/SuplaDevice/src/supla/sensor/binary.h | 2 +- lib/SuplaDevice/src/supla/sensor/distance.h | 5 +- .../src/supla/sensor/electricity_meter.h | 2 +- .../sensor/general_purpose_measurement_base.h | 3 +- .../src/supla/sensor/impulse_counter.h | 3 +- lib/SuplaDevice/src/supla/sensor/pressure.h | 4 +- lib/SuplaDevice/src/supla/sensor/rain.h | 4 +- .../src/supla/sensor/therm_hygro_meter.cpp | 37 +++ .../src/supla/sensor/therm_hygro_meter.h | 26 +- .../supla/sensor/therm_hygro_press_meter.cpp | 57 ++++ .../supla/sensor/therm_hygro_press_meter.h | 41 +-- .../src/supla/sensor/thermometer.cpp | 37 +++ .../src/supla/sensor/thermometer.h | 24 +- .../src/supla/sensor/virtual_binary.h | 2 +- lib/SuplaDevice/src/supla/sensor/weight.h | 4 +- lib/SuplaDevice/src/supla/sensor/wind.h | 4 +- lib/SuplaDevice/src/supla/tools.cpp | 13 + lib/SuplaDevice/src/supla/tools.h | 2 + 48 files changed, 1066 insertions(+), 400 deletions(-) create mode 100644 lib/SuplaDevice/extras/test/ChannelTests/channel_tests.cpp create mode 100644 lib/SuplaDevice/extras/test/IoTests/io_tests.cpp create mode 100644 lib/SuplaDevice/extras/test/doubles/Arduino.h create mode 100644 lib/SuplaDevice/extras/test/doubles/arduino_mock.cpp create mode 100644 lib/SuplaDevice/extras/test/doubles/log.cpp create mode 100644 lib/SuplaDevice/extras/test/doubles/srpc.cpp create mode 100644 lib/SuplaDevice/src/supla/sensor/therm_hygro_meter.cpp create mode 100644 lib/SuplaDevice/src/supla/sensor/therm_hygro_press_meter.cpp create mode 100644 lib/SuplaDevice/src/supla/sensor/thermometer.cpp diff --git a/lib/SuplaDevice/examples/Afore/Afore.ino b/lib/SuplaDevice/examples/Afore/Afore.ino index eaace6d9..859636c9 100644 --- a/lib/SuplaDevice/examples/Afore/Afore.ino +++ b/lib/SuplaDevice/examples/Afore/Afore.ino @@ -14,28 +14,25 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#include #include #include // Choose proper network interface for your card: -// Arduino Mega with EthernetShield W5100: -#include -// Ethernet MAC address -uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; -Supla::EthernetShield ethernet(mac); -// -// Arduino Mega with ENC28J60: -// #include -// Supla::ENC28J60 ethernet(mac); -// -// ESP8266 based board: -// #include -// Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); -// -// ESP32 based board: -// #include -// Supla::ESP32Wifi wifi("your_wifi_ssid", "your_wifi_password"); +#ifdef ARDUINO_ARCH_AVR + // Arduino Mega with EthernetShield W5100: + #include + // Ethernet MAC address + uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; + Supla::EthernetShield ethernet(mac); + + // Arduino Mega with ENC28J60: + // #include + // Supla::ENC28J60 ethernet(mac); +#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) + // ESP8266 and ESP32 based board: + #include + Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); +#endif void setup() { diff --git a/lib/SuplaDevice/examples/DHT/DHT.ino b/lib/SuplaDevice/examples/DHT/DHT.ino index b91c0fea..e0c85e2a 100644 --- a/lib/SuplaDevice/examples/DHT/DHT.ino +++ b/lib/SuplaDevice/examples/DHT/DHT.ino @@ -14,28 +14,25 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#include #include #include // Choose proper network interface for your card: -// Arduino Mega with EthernetShield W5100: -#include -// Ethernet MAC address -uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; -Supla::EthernetShield ethernet(mac); -// -// Arduino Mega with ENC28J60: -// #include -// Supla::ENC28J60 ethernet(mac); -// -// ESP8266 based board: -// #include -// Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); -// -// ESP32 based board: -// #include -// Supla::ESP32Wifi wifi("your_wifi_ssid", "your_wifi_password"); +#ifdef ARDUINO_ARCH_AVR + // Arduino Mega with EthernetShield W5100: + #include + // Ethernet MAC address + uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; + Supla::EthernetShield ethernet(mac); + + // Arduino Mega with ENC28J60: + // #include + // Supla::ENC28J60 ethernet(mac); +#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) + // ESP8266 and ESP32 based board: + #include + Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); +#endif /* * This example requires DHT sensor library installed. @@ -49,7 +46,7 @@ Supla::EthernetShield ethernet(mac); void setup() { - Serial.begin(9600); + Serial.begin(115200); // Replace the falowing GUID with value that you can retrieve from https://www.supla.org/arduino/get-guid char GUID[SUPLA_GUID_SIZE] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; diff --git a/lib/SuplaDevice/examples/DallasTemperature/DallasTemperature.ino b/lib/SuplaDevice/examples/DallasTemperature/DallasTemperature.ino index f10308df..81eb8fbc 100644 --- a/lib/SuplaDevice/examples/DallasTemperature/DallasTemperature.ino +++ b/lib/SuplaDevice/examples/DallasTemperature/DallasTemperature.ino @@ -14,7 +14,6 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#include #include /* @@ -26,27 +25,25 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // Choose proper network interface for your card: -// Arduino Mega with EthernetShield W5100: -#include -// Ethernet MAC address -uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; -Supla::EthernetShield ethernet(mac); -// -// Arduino Mega with ENC28J60: -// #include -// Supla::ENC28J60 ethernet(mac); -// -// ESP8266 based board: -// #include -// Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); -// -// ESP32 based board: -// #include -// Supla::ESP32Wifi wifi("your_wifi_ssid", "your_wifi_password"); +#ifdef ARDUINO_ARCH_AVR + // Arduino Mega with EthernetShield W5100: + #include + // Ethernet MAC address + uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; + Supla::EthernetShield ethernet(mac); + + // Arduino Mega with ENC28J60: + // #include + // Supla::ENC28J60 ethernet(mac); +#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) + // ESP8266 and ESP32 based board: + #include + Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); +#endif void setup() { - Serial.begin(9600); + Serial.begin(115200); // Replace the falowing GUID with value that you can retrieve from https://www.supla.org/arduino/get-guid char GUID[SUPLA_GUID_SIZE] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; diff --git a/lib/SuplaDevice/examples/Fronius/Fronius.ino b/lib/SuplaDevice/examples/Fronius/Fronius.ino index 98b3b7ad..27cc0e75 100644 --- a/lib/SuplaDevice/examples/Fronius/Fronius.ino +++ b/lib/SuplaDevice/examples/Fronius/Fronius.ino @@ -14,28 +14,25 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#include #include #include // Choose proper network interface for your card: -// Arduino Mega with EthernetShield W5100: -#include -// Ethernet MAC address -uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; -Supla::EthernetShield ethernet(mac); -// -// Arduino Mega with ENC28J60: -// #include -// Supla::ENC28J60 ethernet(mac); -// -// ESP8266 based board: -// #include -// Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); -// -// ESP32 based board: -// #include -// Supla::ESP32Wifi wifi("your_wifi_ssid", "your_wifi_password"); +#ifdef ARDUINO_ARCH_AVR + // Arduino Mega with EthernetShield W5100: + #include + // Ethernet MAC address + uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; + Supla::EthernetShield ethernet(mac); + + // Arduino Mega with ENC28J60: + // #include + // Supla::ENC28J60 ethernet(mac); +#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) + // ESP8266 and ESP32 based board: + #include + Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); +#endif void setup() { diff --git a/lib/SuplaDevice/examples/HC_SR04_Distance_sensor/HC_SR04_Distance_sensor.ino b/lib/SuplaDevice/examples/HC_SR04_Distance_sensor/HC_SR04_Distance_sensor.ino index 2de1ac58..0bb45c39 100644 --- a/lib/SuplaDevice/examples/HC_SR04_Distance_sensor/HC_SR04_Distance_sensor.ino +++ b/lib/SuplaDevice/examples/HC_SR04_Distance_sensor/HC_SR04_Distance_sensor.ino @@ -14,7 +14,6 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#include #include // Add include to HC_SR04 sensor @@ -22,27 +21,25 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // Choose proper network interface for your card: -// Arduino Mega with EthernetShield W5100: -#include -// Ethernet MAC address -uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; -Supla::EthernetShield ethernet(mac); -// -// Arduino Mega with ENC28J60: -// #include -// Supla::ENC28J60 ethernet(mac); -// -// ESP8266 based board: -// #include -// Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); -// -// ESP32 based board: -// #include -// Supla::ESP32Wifi wifi("your_wifi_ssid", "your_wifi_password"); +#ifdef ARDUINO_ARCH_AVR + // Arduino Mega with EthernetShield W5100: + #include + // Ethernet MAC address + uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; + Supla::EthernetShield ethernet(mac); + + // Arduino Mega with ENC28J60: + // #include + // Supla::ENC28J60 ethernet(mac); +#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) + // ESP8266 and ESP32 based board: + #include + Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); +#endif void setup() { - Serial.begin(9600); + Serial.begin(115200); // Replace the falowing GUID with value that you can retrieve from https://www.supla.org/arduino/get-guid char GUID[SUPLA_GUID_SIZE] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; @@ -63,7 +60,7 @@ void setup() { /* * SuplaDevice Initialization. - * Server address, LocationID and LocationPassword are available at https://cloud.supla.org + * Server address is available at https://cloud.supla.org * If you do not have an account, you can create it at https://cloud.supla.org/account/create * SUPLA and SUPLA CLOUD are free of charge * diff --git a/lib/SuplaDevice/examples/ImpulseCounter/ImpulseCounter.ino b/lib/SuplaDevice/examples/ImpulseCounter/ImpulseCounter.ino index 5035b970..fe626866 100644 --- a/lib/SuplaDevice/examples/ImpulseCounter/ImpulseCounter.ino +++ b/lib/SuplaDevice/examples/ImpulseCounter/ImpulseCounter.ino @@ -14,7 +14,6 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#include #include #include @@ -26,29 +25,26 @@ Supla::Eeprom eeprom(STORAGE_OFFSET); // #include // Supla::FramSpi fram(STORAGE_OFFSET); - // Choose proper network interface for your card: -// Arduino Mega with EthernetShield W5100: -#include -// Ethernet MAC address -uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; -Supla::EthernetShield ethernet(mac); -// -// Arduino Mega with ENC28J60: -// #include -// Supla::ENC28J60 ethernet(mac); -// -// ESP8266 based board: -// #include -// Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); -// -// ESP32 based board: -// #include -// Supla::ESP32Wifi wifi("your_wifi_ssid", "your_wifi_password"); +#ifdef ARDUINO_ARCH_AVR + // Arduino Mega with EthernetShield W5100: + #include + // Ethernet MAC address + uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; + Supla::EthernetShield ethernet(mac); + + // Arduino Mega with ENC28J60: + // #include + // Supla::ENC28J60 ethernet(mac); +#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) + // ESP8266 and ESP32 based board: + #include + Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); +#endif void setup() { - Serial.begin(9600); + Serial.begin(115200); // Replace the falowing GUID with value that you can retrieve from https://www.supla.org/arduino/get-guid char GUID[SUPLA_GUID_SIZE] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; diff --git a/lib/SuplaDevice/examples/Pzem_V_2/Pzem_V_2.ino b/lib/SuplaDevice/examples/Pzem_V_2/Pzem_V_2.ino index e03d4aa9..0b8c4918 100644 --- a/lib/SuplaDevice/examples/Pzem_V_2/Pzem_V_2.ino +++ b/lib/SuplaDevice/examples/Pzem_V_2/Pzem_V_2.ino @@ -13,24 +13,32 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ - // this example will work only on esp8266 and esp32 boards. On Arduino mega it will not fly. + //dependence: Arduino communication library for Peacefair PZEM-004T Energy monitor https://github.com/olehs/PZEM004T -#include #include #include -// ESP8266 based board: -#include -Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); -// -// ESP32 based board: -// #include -// Supla::ESP32Wifi wifi("your_wifi_ssid", "your_wifi_password"); +// Choose proper network interface for your card: +#ifdef ARDUINO_ARCH_AVR + // Arduino Mega with EthernetShield W5100: + #include + // Ethernet MAC address + uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; + Supla::EthernetShield ethernet(mac); + + // Arduino Mega with ENC28J60: + // #include + // Supla::ENC28J60 ethernet(mac); +#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) + // ESP8266 and ESP32 based board: + #include + Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); +#endif void setup() { - Serial.begin(9600); + Serial.begin(115200); // Replace the falowing GUID with value that you can retrieve from https://www.supla.org/arduino/get-guid char GUID[SUPLA_GUID_SIZE] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; diff --git a/lib/SuplaDevice/examples/Pzem_V_3/Pzem_V_3.ino b/lib/SuplaDevice/examples/Pzem_V_3/Pzem_V_3.ino index fd435453..85fe0bb6 100644 --- a/lib/SuplaDevice/examples/Pzem_V_3/Pzem_V_3.ino +++ b/lib/SuplaDevice/examples/Pzem_V_3/Pzem_V_3.ino @@ -13,56 +13,66 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ - // this example will work only on esp8266 and esp32 boards. On Arduino mega it will not fly. - //dependence: Arduino library for the Updated PZEM-004T v3.0 Power and Energy meter https://github.com/mandulaj/PZEM-004T-v30 -#include +// dependence: Arduino library for the Updated PZEM-004T v3.0 Power and Energy +// meter https://github.com/mandulaj/PZEM-004T-v30 + #include #include -// ESP8266 based board: +// Choose proper network interface for your card: +#ifdef ARDUINO_ARCH_AVR +// Arduino Mega with EthernetShield W5100: +#include +// Ethernet MAC address +uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; +Supla::EthernetShield ethernet(mac); + +// Arduino Mega with ENC28J60: +// #include +// Supla::ENC28J60 ethernet(mac); +#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) +// ESP8266 and ESP32 based board: #include Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); -// -// ESP32 based board: -// #include -// Supla::ESP32Wifi wifi("your_wifi_ssid", "your_wifi_password"); +#endif void setup() { + Serial.begin(115200); - Serial.begin(9600); - - // Replace the falowing GUID with value that you can retrieve from https://www.supla.org/arduino/get-guid - char GUID[SUPLA_GUID_SIZE] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; - - // Replace the following AUTHKEY with value that you can retrieve from: https://www.supla.org/arduino/get-authkey - char AUTHKEY[SUPLA_AUTHKEY_SIZE] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; + // Replace the falowing GUID with value that you can retrieve from + // https://www.supla.org/arduino/get-guid + char GUID[SUPLA_GUID_SIZE] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - /* - * Having your device already registered at cloud.supla.org, - * you want to change CHANNEL sequence or remove any of them, - * then you must also remove the device itself from cloud.supla.org. - * Otherwise you will get "Channel conflict!" error. - */ - - new Supla::Sensor::PZEMv3(5, 4); // (RX,TX) + // Replace the following AUTHKEY with value that you can retrieve from: + // https://www.supla.org/arduino/get-authkey + char AUTHKEY[SUPLA_AUTHKEY_SIZE] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + /* + * Having your device already registered at cloud.supla.org, + * you want to change CHANNEL sequence or remove any of them, + * then you must also remove the device itself from cloud.supla.org. + * Otherwise you will get "Channel conflict!" error. + */ - /* - * SuplaDevice Initialization. - * Server address, is available at https://cloud.supla.org - * If you do not have an account, you can create it at https://cloud.supla.org/account/create - * SUPLA and SUPLA CLOUD are free of charge - * - */ + new Supla::Sensor::PZEMv3(5, 4); // (RX,TX) - SuplaDevice.begin(GUID, // Global Unique Identifier - "svr1.supla.org", // SUPLA server address - "email@address", // Email address used to login to Supla Cloud - AUTHKEY); // Authorization key + /* + * SuplaDevice Initialization. + * Server address, is available at https://cloud.supla.org + * If you do not have an account, you can create it at + * https://cloud.supla.org/account/create SUPLA and SUPLA CLOUD are free of + * charge + * + */ + SuplaDevice.begin( + GUID, // Global Unique Identifier + "svr1.supla.org", // SUPLA server address + "email@address", // Email address used to login to Supla Cloud + AUTHKEY); // Authorization key } void loop() { - SuplaDevice.iterate(); + SuplaDevice.iterate(); } diff --git a/lib/SuplaDevice/examples/RGBW/RGBW.ino b/lib/SuplaDevice/examples/RGBW/RGBW.ino index 994fd701..fb446e08 100644 --- a/lib/SuplaDevice/examples/RGBW/RGBW.ino +++ b/lib/SuplaDevice/examples/RGBW/RGBW.ino @@ -14,28 +14,25 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#include #include #include // Choose proper network interface for your card: -// Arduino Mega with EthernetShield W5100: -#include -// Ethernet MAC address -uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; -Supla::EthernetShield ethernet(mac); -// -// Arduino Mega with ENC28J60: -// #include -// Supla::ENC28J60 ethernet(mac); -// -// ESP8266 based board: -// #include -// Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); -// -// ESP32 based board: -// #include -// Supla::ESP32Wifi wifi("your_wifi_ssid", "your_wifi_password"); +#ifdef ARDUINO_ARCH_AVR + // Arduino Mega with EthernetShield W5100: + #include + // Ethernet MAC address + uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; + Supla::EthernetShield ethernet(mac); + + // Arduino Mega with ENC28J60: + // #include + // Supla::ENC28J60 ethernet(mac); +#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) + // ESP8266 and ESP32 based board: + #include + Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); +#endif /* * Youtube: https://youtu.be/FE9tqzTjmA4 @@ -83,7 +80,7 @@ class RgbwLeds : public Supla::Control::RGBWBase { }; void setup() { - Serial.begin(9600); + Serial.begin(115200); // Replace the falowing GUID with value that you can retrieve from https://www.supla.org/arduino/get-guid char GUID[SUPLA_GUID_SIZE] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; diff --git a/lib/SuplaDevice/examples/RollerShutter/RollerShutter.ino b/lib/SuplaDevice/examples/RollerShutter/RollerShutter.ino index 9747b4e6..7cb0231d 100644 --- a/lib/SuplaDevice/examples/RollerShutter/RollerShutter.ino +++ b/lib/SuplaDevice/examples/RollerShutter/RollerShutter.ino @@ -14,7 +14,6 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#include #include #include #include @@ -27,30 +26,26 @@ Supla::Eeprom eeprom(STORAGE_OFFSET); // #include // Supla::FramSpi fram(STORAGE_OFFSET); - // Choose proper network interface for your card: -// Arduino Mega with EthernetShield W5100: -#include -// Ethernet MAC address -uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; -Supla::EthernetShield ethernet(mac); -// -// Arduino Mega with ENC28J60: -// #include -// Supla::ENC28J60 ethernet(mac); -// -// ESP8266 based board: -// #include -// Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); -// -// ESP32 based board: -// #include -// Supla::ESP32Wifi wifi("your_wifi_ssid", "your_wifi_password"); +#ifdef ARDUINO_ARCH_AVR + // Arduino Mega with EthernetShield W5100: + #include + // Ethernet MAC address + uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; + Supla::EthernetShield ethernet(mac); + // Arduino Mega with ENC28J60: + // #include + // Supla::ENC28J60 ethernet(mac); +#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) + // ESP8266 and ESP32 based board: + #include + Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); +#endif void setup() { - Serial.begin(9600); + Serial.begin(115200); // Replace the falowing GUID with value that you can retrieve from https://www.supla.org/arduino/get-guid char GUID[SUPLA_GUID_SIZE] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; diff --git a/lib/SuplaDevice/examples/SequenceButton/SequenceButton.ino b/lib/SuplaDevice/examples/SequenceButton/SequenceButton.ino index 8647974a..75f81134 100644 --- a/lib/SuplaDevice/examples/SequenceButton/SequenceButton.ino +++ b/lib/SuplaDevice/examples/SequenceButton/SequenceButton.ino @@ -14,29 +14,26 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#include #include #include #include // Choose proper network interface for your card: -// Arduino Mega with EthernetShield W5100: -//#include -// Ethernet MAC address -//uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; -//Supla::EthernetShield ethernet(mac); -// -// Arduino Mega with ENC28J60: -// #include -// Supla::ENC28J60 ethernet(mac); -// -// ESP8266 based board: -#include -Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); -// -// ESP32 based board: -// #include -// Supla::ESP32Wifi wifi("your_wifi_ssid", "your_wifi_password"); +#ifdef ARDUINO_ARCH_AVR + // Arduino Mega with EthernetShield W5100: + #include + // Ethernet MAC address + uint8_t mac[6] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; + Supla::EthernetShield ethernet(mac); + + // Arduino Mega with ENC28J60: + // #include + // Supla::ENC28J60 ethernet(mac); +#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) + // ESP8266 and ESP32 based board: + #include + Supla::ESPWifi wifi("your_wifi_ssid", "your_wifi_password"); +#endif void setup() { @@ -58,7 +55,7 @@ void setup() { auto secretRelay = new Supla::Control::Relay(30, false); // Low level trigger relay on pin 30 auto alarmRelay = new Supla::Control::Relay(31, false); // Low level trigger relay on pin 31 - auto seqButton = new Supla::Control::SequenceButton(D9, true, true); // Button on pin 28 with internal pullUp + auto seqButton = new Supla::Control::SequenceButton(28, true, true); // Button on pin 28 with internal pullUp // and LOW is considered as "pressed" state // Sequence of lenghts [ms] of button being presset, released, pressed, released, etc. diff --git a/lib/SuplaDevice/extras/test/CMakeLists.txt b/lib/SuplaDevice/extras/test/CMakeLists.txt index 12e94f97..a96d1608 100644 --- a/lib/SuplaDevice/extras/test/CMakeLists.txt +++ b/lib/SuplaDevice/extras/test/CMakeLists.txt @@ -6,17 +6,24 @@ enable_testing() set(CMAKE_CXX_STANDARD 14) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) +set(CMAKE_BUILD_TYPE Debug) include_directories(../../src) +include_directories(doubles) add_subdirectory(../../src/ build) +mark_as_advanced( +BUILD_GMOCK +BUILD_GTEST +) + include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git - GIT_TAG release-1.8.0 + GIT_TAG release-1.10.0 ) FetchContent_GetProperties(googletest) @@ -25,11 +32,18 @@ if(NOT googletest_POPULATED) add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR}) endif() -file(GLOB SRCS UptimeTests/*.cpp) +file(GLOB TEST_SRC + UptimeTests/*.cpp + ChannelTests/*cpp + IoTests/*.cpp + ) + +file(GLOB DOUBLE_SRC doubles/*.cpp) -add_executable(supladevicetests ${SRCS}) +add_executable(supladevicetests ${TEST_SRC} ${DOUBLE_SRC}) target_link_libraries(supladevicetests + gmock gtest gtest_main supladevicelib diff --git a/lib/SuplaDevice/extras/test/ChannelTests/channel_tests.cpp b/lib/SuplaDevice/extras/test/ChannelTests/channel_tests.cpp new file mode 100644 index 00000000..4fc872f4 --- /dev/null +++ b/lib/SuplaDevice/extras/test/ChannelTests/channel_tests.cpp @@ -0,0 +1,270 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include + +#include +#include + +TEST(ChannelTests, ChannelMethods) { + Supla::Channel first; + Supla::Channel second; + + EXPECT_EQ(first.getChannelNumber(), 0); + EXPECT_EQ(second.getChannelNumber(), 1); + + EXPECT_EQ(first.isExtended(), false); + EXPECT_EQ(first.isUpdateReady(), false); + EXPECT_EQ(first.getChannelType(), 0); + EXPECT_EQ(first.getExtValue(), nullptr); + + int number = first.getChannelNumber(); + char emptyArray[SUPLA_CHANNELVALUE_SIZE] = {}; + EXPECT_EQ(number, Supla::Channel::reg_dev.channels[number].Number); + EXPECT_EQ(Supla::Channel::reg_dev.channels[number].Type, 0); + EXPECT_EQ(Supla::Channel::reg_dev.channels[number].FuncList, 0); + EXPECT_EQ(Supla::Channel::reg_dev.channels[number].Default, 0); + EXPECT_EQ(Supla::Channel::reg_dev.channels[number].Flags, SUPLA_CHANNEL_FLAG_CHANNELSTATE); + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, emptyArray, SUPLA_CHANNELVALUE_SIZE)); + + first.setType(10); + EXPECT_EQ(first.getChannelType(), 10); + EXPECT_EQ(Supla::Channel::reg_dev.channels[number].Type, 10); + + first.setDefault(14); + EXPECT_EQ(Supla::Channel::reg_dev.channels[number].Default, 14); + + first.setFlag(2); + EXPECT_EQ(Supla::Channel::reg_dev.channels[number].Flags, SUPLA_CHANNEL_FLAG_CHANNELSTATE | 2); + + first.setFlag(4); + EXPECT_EQ(Supla::Channel::reg_dev.channels[number].Flags, SUPLA_CHANNEL_FLAG_CHANNELSTATE | 2 | 4); + + first.unsetFlag(2); + EXPECT_EQ(Supla::Channel::reg_dev.channels[number].Flags, SUPLA_CHANNEL_FLAG_CHANNELSTATE | 4); + + first.unsetFlag(SUPLA_CHANNEL_FLAG_CHANNELSTATE); + EXPECT_EQ(Supla::Channel::reg_dev.channels[number].Flags, 4); + + first.setFuncList(11); + EXPECT_EQ(Supla::Channel::reg_dev.channels[number].FuncList, 11); + +} + +TEST(ChannelTests, SetNewValue) { + Supla::Channel channel; + int number = channel.getChannelNumber(); + char emptyArray[SUPLA_CHANNELVALUE_SIZE] = {}; + + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, emptyArray, SUPLA_CHANNELVALUE_SIZE)); + EXPECT_FALSE(channel.isUpdateReady()); + + char array[SUPLA_CHANNELVALUE_SIZE] = {0, 1, 2, 3, 4, 5, 6, 7}; + channel.setNewValue(array); + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, array, SUPLA_CHANNELVALUE_SIZE)); + EXPECT_TRUE(channel.isUpdateReady()); + + channel.clearUpdateReady(); + EXPECT_FALSE(channel.isUpdateReady()); + + channel.setNewValue(array); + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, array, SUPLA_CHANNELVALUE_SIZE)); + EXPECT_FALSE(channel.isUpdateReady()); + + array[4] = 15; + channel.setNewValue(array); + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, array, SUPLA_CHANNELVALUE_SIZE)); + EXPECT_TRUE(channel.isUpdateReady()); + + ASSERT_EQ(sizeof(double), 8); + double temp = 3.1415; + channel.setNewValue(temp); + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, &temp, SUPLA_CHANNELVALUE_SIZE)); + EXPECT_TRUE(channel.isUpdateReady()); + channel.clearUpdateReady(); + + char arrayBool[SUPLA_CHANNELVALUE_SIZE] = {}; + arrayBool[0] = true; + channel.setNewValue(true); + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, arrayBool, SUPLA_CHANNELVALUE_SIZE)); + EXPECT_TRUE(channel.isUpdateReady()); + channel.clearUpdateReady(); + + channel.setNewValue(false); + arrayBool[0] = false; + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, arrayBool, SUPLA_CHANNELVALUE_SIZE)); + EXPECT_TRUE(channel.isUpdateReady()); + channel.clearUpdateReady(); + + int value = 1234; + ASSERT_EQ(sizeof(int), 4); + channel.setNewValue(value); + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, &value, sizeof(int))); + EXPECT_TRUE(channel.isUpdateReady()); + channel.clearUpdateReady(); + + _supla_int64_t value64 = 124346; + ASSERT_EQ(sizeof(value64), 8); + channel.setNewValue(value64); + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, &value64, sizeof(value64))); + EXPECT_TRUE(channel.isUpdateReady()); + channel.clearUpdateReady(); + + double humi = 95.2234123; + temp = 23.443322; + + int expectedTemp = temp * 1000; + int expectedHumi = humi * 1000; + + channel.setNewValue(temp, humi); + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, &expectedTemp, sizeof(expectedTemp))); + EXPECT_TRUE(0 == memcmp(&(Supla::Channel::reg_dev.channels[number].value[4]), &expectedHumi, sizeof(expectedHumi))); + EXPECT_TRUE(channel.isUpdateReady()); + channel.clearUpdateReady(); + + // RGBW channel setting + channel.setNewValue(1, 2, 3, 4, 5); + char rgbwArray[SUPLA_CHANNELVALUE_SIZE] = {5, 4, 3, 2, 1, 0, 0, 0}; + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, rgbwArray, SUPLA_CHANNELVALUE_SIZE)); + EXPECT_TRUE(channel.isUpdateReady()); + channel.clearUpdateReady(); + + TElectricityMeter_ExtendedValue_V2 emVal = {}; + TElectricityMeter_Value expectedValue = {}; + + emVal.m_count = 1; + emVal.measured_values |= EM_VAR_FORWARD_ACTIVE_ENERGY; + emVal.total_forward_active_energy[0] = 1000; + emVal.total_forward_active_energy[1] = 2000; + emVal.total_forward_active_energy[2] = 4000; + + expectedValue.total_forward_active_energy = (1000 + 2000 + 4000) / 1000; + + channel.setNewValue(emVal); + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, &expectedValue, sizeof(expectedValue))); + EXPECT_TRUE(channel.isUpdateReady()); + channel.clearUpdateReady(); + + emVal.measured_values |= EM_VAR_VOLTAGE; + emVal.m[0].voltage[0] = 10; + emVal.m[0].voltage[1] = 0; + emVal.m[0].voltage[2] = 0; + + expectedValue.flags = 0; + expectedValue.flags |= EM_VALUE_FLAG_PHASE1_ON; + + channel.setNewValue(emVal); + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, &expectedValue, sizeof(expectedValue))); + EXPECT_TRUE(channel.isUpdateReady()); + channel.clearUpdateReady(); + + emVal.m[0].voltage[0] = 0; + emVal.m[0].voltage[1] = 20; + emVal.m[0].voltage[2] = 0; + + expectedValue.flags = 0; + expectedValue.flags |= EM_VALUE_FLAG_PHASE2_ON; + + channel.setNewValue(emVal); + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, &expectedValue, sizeof(expectedValue))); + EXPECT_TRUE(channel.isUpdateReady()); + channel.clearUpdateReady(); + + + emVal.m[0].voltage[0] = 0; + emVal.m[0].voltage[1] = 0; + emVal.m[0].voltage[2] = 300; + + expectedValue.flags = 0; + expectedValue.flags |= EM_VALUE_FLAG_PHASE3_ON; + + channel.setNewValue(emVal); + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, &expectedValue, sizeof(expectedValue))); + EXPECT_TRUE(channel.isUpdateReady()); + channel.clearUpdateReady(); + + + emVal.m[0].voltage[0] = 10; + emVal.m[0].voltage[1] = 0; + emVal.m[0].voltage[2] = 540; + + expectedValue.flags = 0; + expectedValue.flags |= EM_VALUE_FLAG_PHASE1_ON | EM_VALUE_FLAG_PHASE3_ON; + + channel.setNewValue(emVal); + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, &expectedValue, sizeof(expectedValue))); + EXPECT_TRUE(channel.isUpdateReady()); + channel.clearUpdateReady(); + + emVal.m[0].voltage[0] = 10; + emVal.m[0].voltage[1] = 230; + emVal.m[0].voltage[2] = 540; + + expectedValue.flags = 0; + expectedValue.flags |= EM_VALUE_FLAG_PHASE1_ON | EM_VALUE_FLAG_PHASE3_ON | EM_VALUE_FLAG_PHASE2_ON; + + channel.setNewValue(emVal); + EXPECT_TRUE(0 == memcmp(Supla::Channel::reg_dev.channels[number].value, &expectedValue, sizeof(expectedValue))); + EXPECT_TRUE(channel.isUpdateReady()); + channel.clearUpdateReady(); +} + +TEST(ChannelTests, ExtendedChannelMethods) { + Supla::ChannelExtended extChannel; + + EXPECT_TRUE(extChannel.isExtended()); + EXPECT_NE(nullptr, extChannel.getExtValue() ); + +} + +TEST(ChannelTests, ChannelValueGetters) { + Supla::Channel channel; + + EXPECT_DOUBLE_EQ(channel.getValueDouble(), 0); + + double pi = 3.1415; + channel.setNewValue(pi); + EXPECT_DOUBLE_EQ(channel.getValueDouble(), pi); + + double e = 2.71828; + channel.setNewValue(pi, e); + EXPECT_NEAR(channel.getValueDoubleFirst(), pi, 0.001); + EXPECT_NEAR(channel.getValueDoubleSecond(), e, 0.001); + + int valueInt = 2021; + channel.setNewValue(valueInt); + EXPECT_EQ(channel.getValueInt32(), valueInt); + + _supla_int64_t valueInt64 = 202013012021000; + channel.setNewValue(valueInt64); + EXPECT_EQ(channel.getValueInt64(), valueInt64); + + channel.setNewValue(true); + EXPECT_TRUE(channel.getValueBool()); + + channel.setNewValue(false); + EXPECT_FALSE(channel.getValueBool()); + + uint8_t red = 10, green = 20, blue = 30, colorBright = 50, bright = 90; + channel.setNewValue(red, green, blue, colorBright, bright); + EXPECT_EQ(channel.getValueRed(), red); + EXPECT_EQ(channel.getValueGreen(), green); + EXPECT_EQ(channel.getValueBlue(), blue); + EXPECT_EQ(channel.getValueColorBrightness(), colorBright); + EXPECT_EQ(channel.getValueBrightness(), bright); + + +} diff --git a/lib/SuplaDevice/extras/test/IoTests/io_tests.cpp b/lib/SuplaDevice/extras/test/IoTests/io_tests.cpp new file mode 100644 index 00000000..b0cd081d --- /dev/null +++ b/lib/SuplaDevice/extras/test/IoTests/io_tests.cpp @@ -0,0 +1,26 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include + +#include + + +TEST(IoTests, DefaultBehavior) { + EXPECT_TRUE(true); + +} + diff --git a/lib/SuplaDevice/extras/test/UptimeTests/uptime_tests.cpp b/lib/SuplaDevice/extras/test/UptimeTests/uptime_tests.cpp index 0280d0e1..59241f9b 100644 --- a/lib/SuplaDevice/extras/test/UptimeTests/uptime_tests.cpp +++ b/lib/SuplaDevice/extras/test/UptimeTests/uptime_tests.cpp @@ -1,3 +1,19 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + #include #include diff --git a/lib/SuplaDevice/extras/test/doubles/Arduino.h b/lib/SuplaDevice/extras/test/doubles/Arduino.h new file mode 100644 index 00000000..66d39925 --- /dev/null +++ b/lib/SuplaDevice/extras/test/doubles/Arduino.h @@ -0,0 +1,109 @@ +#ifndef _test_double_arduino_h +#define _test_double_arduino_h + +#include + +#include + +typedef std::string String; + +#define LSBFIRST 0 + +void digitalWrite(uint8_t pin, uint8_t val); +int digitalRead(uint8_t pin); +void pinMode(uint8_t pin, uint8_t mode); + + +class SerialStub { + public: + SerialStub() { + } + + virtual ~SerialStub() { + } + + int printf(const char *format, ...) { + return 0; + } + int print(const String &) { + return 0; + } + + int print(const char[]) { + return 0; + } + + int print(char) { + return 0; + } + + int print(unsigned char) { + return 0; + } + + int print(int) { + return 0; + } + + int print(unsigned int) { + return 0; + } + + int print(long) { + return 0; + } + + int print(unsigned long) { + return 0; + } + + int print(double) { + return 0; + } + + + int println(const String &s) { + return 0; + } + + int println(const char[]) { + return 0; + } + + int println(char) { + return 0; + } + + int println(unsigned char) { + return 0; + } + + int println(int) { + return 0; + } + + int println(unsigned int) { + return 0; + } + + int println(long) { + return 0; + } + + int println(unsigned long) { + return 0; + } + + int println(double) { + return 0; + } + + int println(void) { + return 0; + } + +}; + +extern SerialStub Serial; + +#endif diff --git a/lib/SuplaDevice/extras/test/doubles/arduino_mock.cpp b/lib/SuplaDevice/extras/test/doubles/arduino_mock.cpp new file mode 100644 index 00000000..442b1c71 --- /dev/null +++ b/lib/SuplaDevice/extras/test/doubles/arduino_mock.cpp @@ -0,0 +1,59 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + + +#include "Arduino.h" +#include + +SerialStub Serial; + +class DigitalInterface { + public: + DigitalInterface() { + instance = this; + } + virtual ~DigitalInterface() { + instance = nullptr; + } + + virtual void digitalWrite(uint8_t, uint8_t) = 0; + virtual int digitalRead(uint8_t) = 0; + virtual void pinMode(uint8_t, uint8_t) = 0; + + static DigitalInterface *instance; +}; + +DigitalInterface *DigitalInterface::instance = nullptr; + +class DigitalInterfaceMock : public DigitalInterface { + MOCK_METHOD(void, digitalWrite, (uint8_t, uint8_t), (override)); +// MOCK_METHOD(int, digitalRead, (uint8_t), (override)); + // MOCK_METHOD(void, pinMode, (uint8_t, uint8_t), (override)); + +}; + +void digitalWrite(uint8_t pin, uint8_t val) { + DigitalInterface::instance->digitalWrite(pin, val); +} + +int digitalRead(uint8_t pin) { + return DigitalInterface::instance->digitalRead(pin); +} + +void pinMode(uint8_t pin, uint8_t mode) { + DigitalInterface::instance->pinMode(pin, mode); +} + diff --git a/lib/SuplaDevice/extras/test/doubles/log.cpp b/lib/SuplaDevice/extras/test/doubles/log.cpp new file mode 100644 index 00000000..e117cc36 --- /dev/null +++ b/lib/SuplaDevice/extras/test/doubles/log.cpp @@ -0,0 +1,22 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include + +void supla_log(int __pri, const char *__fmt, ...) { + return; +} + diff --git a/lib/SuplaDevice/extras/test/doubles/srpc.cpp b/lib/SuplaDevice/extras/test/doubles/srpc.cpp new file mode 100644 index 00000000..5ac9386a --- /dev/null +++ b/lib/SuplaDevice/extras/test/doubles/srpc.cpp @@ -0,0 +1,29 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include + +_supla_int_t SRPC_ICACHE_FLASH srpc_ds_async_channel_extendedvalue_changed( + void *_srpc, unsigned char channel_number, + TSuplaChannelExtendedValue *value) { + return 0; +} + +_supla_int_t SRPC_ICACHE_FLASH srpc_ds_async_channel_value_changed( + void *_srpc, unsigned char channel_number, char *value) { + return 0; +} + diff --git a/lib/SuplaDevice/src/CMakeLists.txt b/lib/SuplaDevice/src/CMakeLists.txt index 2643ea2b..a4a988b7 100644 --- a/lib/SuplaDevice/src/CMakeLists.txt +++ b/lib/SuplaDevice/src/CMakeLists.txt @@ -1,5 +1,9 @@ set(SRCS supla/uptime.cpp + supla/channel.cpp + supla/channel_extended.cpp + supla/io.cpp + supla/tools.cpp ) add_library(supladevicelib SHARED ${SRCS}) diff --git a/lib/SuplaDevice/src/SuplaDevice.cpp b/lib/SuplaDevice/src/SuplaDevice.cpp index 210a909e..de04f397 100644 --- a/lib/SuplaDevice/src/SuplaDevice.cpp +++ b/lib/SuplaDevice/src/SuplaDevice.cpp @@ -330,7 +330,6 @@ void SuplaDeviceClass::iterate(void) { if (!srpc_ds_async_registerdevice_e(srpc, &Supla::Channel::reg_dev)) { supla_log(LOG_DEBUG, "Fatal SRPC failure!"); } - Supla::Channel::clearAllUpdateReady(); } else if (registered == 1) { if (Supla::Network::Ping() == false) { diff --git a/lib/SuplaDevice/src/supla-common/IEEE754tools.h b/lib/SuplaDevice/src/supla-common/IEEE754tools.h index 5a42beec..c38cfbc1 100644 --- a/lib/SuplaDevice/src/supla-common/IEEE754tools.h +++ b/lib/SuplaDevice/src/supla-common/IEEE754tools.h @@ -13,13 +13,6 @@ #ifndef IEEE754tools_h #define IEEE754tools_h - -#if defined(ARDUINO) && ARDUINO >= 100 -#include "Arduino.h" -#else -#include "WProgram.h" -#endif - // IEEE754 float layout; struct IEEEfloat { diff --git a/lib/SuplaDevice/src/supla/channel.cpp b/lib/SuplaDevice/src/supla/channel.cpp index b6c9a658..c0c231b0 100644 --- a/lib/SuplaDevice/src/supla/channel.cpp +++ b/lib/SuplaDevice/src/supla/channel.cpp @@ -14,13 +14,14 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +#include + #include "supla/channel.h" #include "supla-common/log.h" #include "supla-common/srpc.h" #include "tools.h" namespace Supla { -Channel *Channel::firstPtr = nullptr; unsigned long Channel::lastCommunicationTimeMs = 0; TDS_SuplaRegisterDevice_E Channel::reg_dev; @@ -30,44 +31,20 @@ Channel::Channel() { channelNumber = -1; if (reg_dev.channel_count < SUPLA_CHANNELMAXCOUNT) { channelNumber = reg_dev.channel_count; + + memset(®_dev.channels[channelNumber], 0, sizeof(reg_dev.channels[channelNumber])); reg_dev.channels[channelNumber].Number = channelNumber; + reg_dev.channel_count++; } else { // TODO: add status CHANNEL_LIMIT_EXCEEDED } - if (firstPtr == nullptr) { - firstPtr = this; - } else { - last()->nextPtr = this; - } - nextPtr = nullptr; setFlag(SUPLA_CHANNEL_FLAG_CHANNELSTATE); } -Channel *Channel::begin() { - return firstPtr; -} - -Channel *Channel::last() { - Channel *ptr = firstPtr; - while (ptr && ptr->nextPtr) { - ptr = ptr->nextPtr; - } - return ptr; -} - -int Channel::size() { - int count = 0; - Channel *ptr = firstPtr; - if (ptr) { - count++; - } - while (ptr->nextPtr) { - count++; - ptr = ptr->nextPtr; - } - return count; +Channel::~Channel() { + reg_dev.channel_count--; } void Channel::setNewValue(double dbl) { @@ -84,8 +61,8 @@ void Channel::setNewValue(double dbl) { void Channel::setNewValue(double temp, double humi) { char newValue[SUPLA_CHANNELVALUE_SIZE]; - long t = temp * 1000.00; - long h = humi * 1000.00; + _supla_int_t t = temp * 1000.00; + _supla_int_t h = humi * 1000.00; memcpy(newValue, &t, 4); memcpy(&(newValue[4]), &h, 4); @@ -111,12 +88,12 @@ void Channel::setNewValue(_supla_int64_t value) { } } -void Channel::setNewValue(int value) { +void Channel::setNewValue(_supla_int_t value) { char newValue[SUPLA_CHANNELVALUE_SIZE]; memset(newValue, 0, SUPLA_CHANNELVALUE_SIZE); - memcpy(newValue, &value, sizeof(int)); + memcpy(newValue, &value, sizeof(value)); if (setNewValue(newValue)) { supla_log( LOG_DEBUG, "Channel(%d) value changed to %d", channelNumber, value); @@ -235,18 +212,6 @@ TSuplaChannelExtendedValue *Channel::getExtValue() { return nullptr; } -void Channel::clearAllUpdateReady() { - for (auto channel = begin(); channel != nullptr; channel = channel->next()) { - if (!channel->isExtended()) { - channel->clearUpdateReady(); - } - } -} - -Channel *Channel::next() { - return nextPtr; -} - void Channel::setUpdateReady() { valueChanged = true; }; @@ -283,4 +248,66 @@ _supla_int_t Channel::getChannelType() { return -1; } +double Channel::getValueDouble() { + double value; + if (sizeof(double) == 8) { + memcpy(&value, reg_dev.channels[channelNumber].value, 8); + } else if (sizeof(double) == 4) { + value = doublePacked2float((uint8_t *)(reg_dev.channels[channelNumber].value)); + } + + return value; +} + +double Channel::getValueDoubleFirst() { + _supla_int_t value; + memcpy(&value, reg_dev.channels[channelNumber].value, 4); + + return value / 1000.0; +} + +double Channel::getValueDoubleSecond() { + _supla_int_t value; + memcpy(&value, &(reg_dev.channels[channelNumber].value[4]), 4); + + return value / 1000.0; +} + +_supla_int_t Channel::getValueInt32() { + _supla_int_t value; + memcpy(&value, reg_dev.channels[channelNumber].value, sizeof(value)); + return value; +} + +_supla_int64_t Channel::getValueInt64() { + _supla_int64_t value; + memcpy(&value, reg_dev.channels[channelNumber].value, sizeof(value)); + return value; +} + +bool Channel::getValueBool() { + return reg_dev.channels[channelNumber].value[0]; +} + +uint8_t Channel::getValueRed() { + return reg_dev.channels[channelNumber].value[4]; +} + +uint8_t Channel::getValueGreen() { + return reg_dev.channels[channelNumber].value[3]; +} + +uint8_t Channel::getValueBlue() { + return reg_dev.channels[channelNumber].value[2]; +} + +uint8_t Channel::getValueColorBrightness() { + return reg_dev.channels[channelNumber].value[1]; +} + +uint8_t Channel::getValueBrightness() { + return reg_dev.channels[channelNumber].value[0]; +} + + }; // namespace Supla diff --git a/lib/SuplaDevice/src/supla/channel.h b/lib/SuplaDevice/src/supla/channel.h index c27b68a5..a91d347f 100644 --- a/lib/SuplaDevice/src/supla/channel.h +++ b/lib/SuplaDevice/src/supla/channel.h @@ -26,14 +26,11 @@ namespace Supla { class Channel { public: Channel(); - - static Channel *begin(); - static Channel *last(); - static int size(); + ~Channel(); void setNewValue(double dbl); void setNewValue(double temp, double humi); - void setNewValue(int value); + void setNewValue(_supla_int_t value); void setNewValue(bool value); void setNewValue(TElectricityMeter_ExtendedValue_V2 &emValue); void setNewValue(uint8_t red, @@ -44,6 +41,18 @@ class Channel { void setNewValue(_supla_int64_t value); bool setNewValue(char *newValue); + double getValueDouble(); + double getValueDoubleFirst(); + double getValueDoubleSecond(); + _supla_int_t getValueInt32(); + _supla_int64_t getValueInt64(); + bool getValueBool(); + uint8_t getValueRed(); + uint8_t getValueGreen(); + uint8_t getValueBlue(); + uint8_t getValueColorBrightness(); + uint8_t getValueBrightness(); + virtual bool isExtended(); bool isUpdateReady(); int getChannelNumber(); @@ -57,8 +66,6 @@ class Channel { void clearUpdateReady(); void sendUpdate(void *srpc); virtual TSuplaChannelExtendedValue *getExtValue(); - static void clearAllUpdateReady(); - Channel *next(); static unsigned long lastCommunicationTimeMs; static TDS_SuplaRegisterDevice_E reg_dev; @@ -69,8 +76,6 @@ class Channel { bool valueChanged; int channelNumber; - Channel *nextPtr; - static Channel *firstPtr; }; }; // namespace Supla diff --git a/lib/SuplaDevice/src/supla/control/relay.h b/lib/SuplaDevice/src/supla/control/relay.h index f7a8813f..ce9effb9 100644 --- a/lib/SuplaDevice/src/supla/control/relay.h +++ b/lib/SuplaDevice/src/supla/control/relay.h @@ -66,8 +66,9 @@ class Relay : public Element, public Triggerable { void iterateAlways(); int handleNewValueFromServer(TSD_SuplaChannelNewValue *newValue); - protected: Channel *getChannel(); + + protected: Channel channel; int pin; bool highIsOn; diff --git a/lib/SuplaDevice/src/supla/control/rgbw_base.h b/lib/SuplaDevice/src/supla/control/rgbw_base.h index 452a66cf..17f733d8 100644 --- a/lib/SuplaDevice/src/supla/control/rgbw_base.h +++ b/lib/SuplaDevice/src/supla/control/rgbw_base.h @@ -20,10 +20,10 @@ #include #include +#include "../actions.h" #include "../channel.h" #include "../element.h" #include "../triggerable.h" -#include "../actions.h" namespace Supla { namespace Control { @@ -50,15 +50,16 @@ class RGBWBase : public Element, public Triggerable { void setFadeEffectTime(int timeMs); void onTimer(); -void onInit() { + void onInit() { + // Send to Supla server new values + channel.setNewValue( + curRed, curGreen, curBlue, curColorBrightness, curBrightness); + } + + Channel *getChannel(); - // Send to Supla server new values - channel.setNewValue( - curRed, curGreen, curBlue, curColorBrightness, curBrightness); -} protected: uint8_t addWithLimit(int value, int addition, int limit = 255); - Channel *getChannel(); void iterateDimmerRGBW(int rgbStep, int wStep); Channel channel; @@ -74,13 +75,12 @@ void onInit() { bool dimIterationDirection; int iterationDelayCounter; int fadeEffect; - int hwRed; // 0 - 255 - int hwGreen; // 0 - 255 - int hwBlue; // 0 - 255 - int hwColorBrightness; // 0 - 100 - int hwBrightness; // 0 - 100 + int hwRed; // 0 - 255 + int hwGreen; // 0 - 255 + int hwBlue; // 0 - 255 + int hwColorBrightness; // 0 - 100 + int hwBrightness; // 0 - 100 unsigned long lastTick; - }; }; // namespace Control diff --git a/lib/SuplaDevice/src/supla/control/roller_shutter.cpp b/lib/SuplaDevice/src/supla/control/roller_shutter.cpp index 12e91b88..56102b77 100644 --- a/lib/SuplaDevice/src/supla/control/roller_shutter.cpp +++ b/lib/SuplaDevice/src/supla/control/roller_shutter.cpp @@ -14,9 +14,10 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#include "roller_shutter.h" #include +#include "roller_shutter.h" + namespace Supla { namespace Control { @@ -53,8 +54,10 @@ RollerShutter::RollerShutter(int pinUp, int pinDown, bool highIsOn) } void RollerShutter::onInit() { - Supla::Io::digitalWrite(channel.getChannelNumber(), pinUp, highIsOn ? LOW : HIGH); - Supla::Io::digitalWrite(channel.getChannelNumber(), pinDown, highIsOn ? LOW : HIGH); + Supla::Io::digitalWrite( + channel.getChannelNumber(), pinUp, highIsOn ? LOW : HIGH); + Supla::Io::digitalWrite( + channel.getChannelNumber(), pinDown, highIsOn ? LOW : HIGH); Supla::Io::pinMode(channel.getChannelNumber(), pinUp, OUTPUT); Supla::Io::pinMode(channel.getChannelNumber(), pinDown, OUTPUT); } @@ -273,19 +276,23 @@ void RollerShutter::stopMovement() { } void RollerShutter::relayDownOn() { - Supla::Io::digitalWrite(channel.getChannelNumber(), pinDown, highIsOn ? HIGH : LOW); + Supla::Io::digitalWrite( + channel.getChannelNumber(), pinDown, highIsOn ? HIGH : LOW); } void RollerShutter::relayUpOn() { - Supla::Io::digitalWrite(channel.getChannelNumber(), pinUp, highIsOn ? HIGH : LOW); + Supla::Io::digitalWrite( + channel.getChannelNumber(), pinUp, highIsOn ? HIGH : LOW); } void RollerShutter::relayDownOff() { - Supla::Io::digitalWrite(channel.getChannelNumber(), pinDown, highIsOn ? LOW : HIGH); + Supla::Io::digitalWrite( + channel.getChannelNumber(), pinDown, highIsOn ? LOW : HIGH); } void RollerShutter::relayUpOff() { - Supla::Io::digitalWrite(channel.getChannelNumber(), pinUp, highIsOn ? LOW : HIGH); + Supla::Io::digitalWrite( + channel.getChannelNumber(), pinUp, highIsOn ? LOW : HIGH); } void RollerShutter::startClosing() { @@ -455,10 +462,10 @@ void RollerShutter::onTimer() { } // if (newCurrentPosition != currentPosition) { // currentPosition = newCurrentPosition; - channel.setNewValue( - currentPosition); // value set on channel will be send to server - // during iterateConnected() execution - // } + channel.setNewValue(static_cast<_supla_int_t>( + currentPosition)); // value set on channel will be send to server + // during iterateConnected() execution + // } } Channel *RollerShutter::getChannel() { diff --git a/lib/SuplaDevice/src/supla/control/roller_shutter.h b/lib/SuplaDevice/src/supla/control/roller_shutter.h index 26e46f81..716a121e 100644 --- a/lib/SuplaDevice/src/supla/control/roller_shutter.h +++ b/lib/SuplaDevice/src/supla/control/roller_shutter.h @@ -59,6 +59,8 @@ class RollerShutter : public Element, public Triggerable { void onLoadState(); void onSaveState(); + Channel *getChannel(); + protected: virtual void stopMovement(); virtual void relayDownOn(); @@ -74,8 +76,6 @@ class RollerShutter : public Element, public Triggerable { bool lastDirectionWasClose(); bool inMove(); - Channel *getChannel(); - Channel channel; uint32_t closingTimeMs; diff --git a/lib/SuplaDevice/src/supla/element.cpp b/lib/SuplaDevice/src/supla/element.cpp index b7f1b22c..ac879eb5 100644 --- a/lib/SuplaDevice/src/supla/element.cpp +++ b/lib/SuplaDevice/src/supla/element.cpp @@ -95,6 +95,10 @@ Channel *Element::getChannel() { return nullptr; } +Channel *Element::getSecondaryChannel() { + return nullptr; +} + void Element::handleGetChannelState(TDSC_ChannelState &channelState) { (void)(channelState); return; diff --git a/lib/SuplaDevice/src/supla/element.h b/lib/SuplaDevice/src/supla/element.h index f2ee9fdc..82f76341 100644 --- a/lib/SuplaDevice/src/supla/element.h +++ b/lib/SuplaDevice/src/supla/element.h @@ -76,11 +76,12 @@ class Element { virtual int handleCalcfgFromServer(TSD_DeviceCalCfgRequest *request); int getChannelNumber(); + virtual Channel *getChannel(); + virtual Channel *getSecondaryChannel(); Element &disableChannelState(); protected: - virtual Channel *getChannel(); static Element *firstPtr; Element *nextPtr; }; diff --git a/lib/SuplaDevice/src/supla/sensor/binary.h b/lib/SuplaDevice/src/supla/sensor/binary.h index 31db9fff..4e5e41b9 100644 --- a/lib/SuplaDevice/src/supla/sensor/binary.h +++ b/lib/SuplaDevice/src/supla/sensor/binary.h @@ -30,9 +30,9 @@ class Binary : public Element { bool getValue(); void iterateAlways(); void onInit(); + Channel *getChannel(); protected: - Channel *getChannel(); Channel channel; int pin; diff --git a/lib/SuplaDevice/src/supla/sensor/distance.h b/lib/SuplaDevice/src/supla/sensor/distance.h index 953078a1..a24a8c74 100644 --- a/lib/SuplaDevice/src/supla/sensor/distance.h +++ b/lib/SuplaDevice/src/supla/sensor/distance.h @@ -20,7 +20,7 @@ #include "supla/channel.h" #include "supla/element.h" -#define DISTANCE_NOT_AVAILABLE -1 +#define DISTANCE_NOT_AVAILABLE -1.0 namespace Supla { namespace Sensor { @@ -43,10 +43,11 @@ class Distance : public Element { } } - protected: Channel *getChannel() { return &channel; } + + protected: Channel channel; unsigned long lastReadTime; }; diff --git a/lib/SuplaDevice/src/supla/sensor/electricity_meter.h b/lib/SuplaDevice/src/supla/sensor/electricity_meter.h index 713ea99c..e67bd517 100644 --- a/lib/SuplaDevice/src/supla/sensor/electricity_meter.h +++ b/lib/SuplaDevice/src/supla/sensor/electricity_meter.h @@ -92,9 +92,9 @@ class ElectricityMeter : public Element { void setResreshRate(unsigned int sec); - protected: Channel *getChannel(); + protected: TElectricityMeter_ExtendedValue_V2 emValue; ChannelExtended extChannel; unsigned _supla_int_t rawCurrent[MAX_PHASES]; diff --git a/lib/SuplaDevice/src/supla/sensor/general_purpose_measurement_base.h b/lib/SuplaDevice/src/supla/sensor/general_purpose_measurement_base.h index 6c494824..ec5e5661 100644 --- a/lib/SuplaDevice/src/supla/sensor/general_purpose_measurement_base.h +++ b/lib/SuplaDevice/src/supla/sensor/general_purpose_measurement_base.h @@ -37,10 +37,11 @@ class GeneralPurposeMeasurementBase : public Element { } } - protected: Channel *getChannel() { return &channel; } + + protected: Channel channel; unsigned long lastReadTime; }; diff --git a/lib/SuplaDevice/src/supla/sensor/impulse_counter.h b/lib/SuplaDevice/src/supla/sensor/impulse_counter.h index 6d6da313..eed631c3 100644 --- a/lib/SuplaDevice/src/supla/sensor/impulse_counter.h +++ b/lib/SuplaDevice/src/supla/sensor/impulse_counter.h @@ -46,6 +46,8 @@ class ImpulseCounter : public Element, public Triggerable { // Increment the counter by 1 void incCounter(); + Channel *getChannel(); + protected: int prevState; // Store previous state of pin (LOW/HIGH). It is used to track // changes on pin state. @@ -61,7 +63,6 @@ class ImpulseCounter : public Element, public Triggerable { unsigned _supla_int64_t counter; // Actual count of impulses - Channel *getChannel(); Channel channel; }; diff --git a/lib/SuplaDevice/src/supla/sensor/pressure.h b/lib/SuplaDevice/src/supla/sensor/pressure.h index d320fb33..cdb9a77d 100644 --- a/lib/SuplaDevice/src/supla/sensor/pressure.h +++ b/lib/SuplaDevice/src/supla/sensor/pressure.h @@ -43,11 +43,11 @@ class Pressure : public Element { } } - - protected: Channel *getChannel() { return &channel; } + + protected: Channel channel; unsigned long lastReadTime; }; diff --git a/lib/SuplaDevice/src/supla/sensor/rain.h b/lib/SuplaDevice/src/supla/sensor/rain.h index dd060d27..40300bc4 100644 --- a/lib/SuplaDevice/src/supla/sensor/rain.h +++ b/lib/SuplaDevice/src/supla/sensor/rain.h @@ -43,11 +43,11 @@ class Rain: public Element { } } - - protected: Channel *getChannel() { return &channel; } + + protected: Channel channel; unsigned long lastReadTime; }; diff --git a/lib/SuplaDevice/src/supla/sensor/therm_hygro_meter.cpp b/lib/SuplaDevice/src/supla/sensor/therm_hygro_meter.cpp new file mode 100644 index 00000000..b9604cfd --- /dev/null +++ b/lib/SuplaDevice/src/supla/sensor/therm_hygro_meter.cpp @@ -0,0 +1,37 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include "therm_hygro_meter.h" + +Supla::Sensor::ThermHygroMeter::ThermHygroMeter() { + channel.setType(SUPLA_CHANNELTYPE_HUMIDITYANDTEMPSENSOR); + channel.setDefault(SUPLA_CHANNELFNC_HUMIDITYANDTEMPERATURE); +} + +double Supla::Sensor::ThermHygroMeter::getTemp() { + return TEMPERATURE_NOT_AVAILABLE; +} + +double Supla::Sensor::ThermHygroMeter::getHumi() { + return HUMIDITY_NOT_AVAILABLE; +} + +void Supla::Sensor::ThermHygroMeter::iterateAlways() { + if (millis() - lastReadTime > 10000) { + lastReadTime = millis(); + channel.setNewValue(getTemp(), getHumi()); + } +} diff --git a/lib/SuplaDevice/src/supla/sensor/therm_hygro_meter.h b/lib/SuplaDevice/src/supla/sensor/therm_hygro_meter.h index 3e9f019a..ab79fea1 100644 --- a/lib/SuplaDevice/src/supla/sensor/therm_hygro_meter.h +++ b/lib/SuplaDevice/src/supla/sensor/therm_hygro_meter.h @@ -25,27 +25,11 @@ namespace Supla { namespace Sensor { class ThermHygroMeter : public Thermometer { public: - ThermHygroMeter() { - channel.setType(SUPLA_CHANNELTYPE_HUMIDITYANDTEMPSENSOR); - channel.setDefault(SUPLA_CHANNELFNC_HUMIDITYANDTEMPERATURE); - } - - virtual double getTemp() { - return TEMPERATURE_NOT_AVAILABLE; - } - - virtual double getHumi() { - return HUMIDITY_NOT_AVAILABLE; - } - - void iterateAlways() { - if (millis() - lastReadTime > 10000) { - lastReadTime = millis(); - channel.setNewValue(getTemp(), getHumi()); - } - } - - protected: + ThermHygroMeter(); + virtual double getTemp(); + virtual double getHumi(); + void iterateAlways(); + }; }; // namespace Sensor diff --git a/lib/SuplaDevice/src/supla/sensor/therm_hygro_press_meter.cpp b/lib/SuplaDevice/src/supla/sensor/therm_hygro_press_meter.cpp new file mode 100644 index 00000000..409fc14c --- /dev/null +++ b/lib/SuplaDevice/src/supla/sensor/therm_hygro_press_meter.cpp @@ -0,0 +1,57 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include "therm_hygro_press_meter.h" + +Supla::Sensor::ThermHygroPressMeter::ThermHygroPressMeter() { + pressureChannel.setType(SUPLA_CHANNELTYPE_PRESSURESENSOR); + pressureChannel.setDefault(SUPLA_CHANNELFNC_PRESSURESENSOR); +} + +double Supla::Sensor::ThermHygroPressMeter::getPressure() { + return PRESSURE_NOT_AVAILABLE; +} + +void Supla::Sensor::ThermHygroPressMeter::iterateAlways() { + if (millis() - lastReadTime > 10000) { + pressureChannel.setNewValue(getPressure()); + } + ThermHygroMeter::iterateAlways(); +} + +bool Supla::Sensor::ThermHygroPressMeter::iterateConnected(void *srpc) { + bool response = true; + if (pressureChannel.isUpdateReady() && + millis() - pressureChannel.lastCommunicationTimeMs > 100) { + pressureChannel.lastCommunicationTimeMs = millis(); + pressureChannel.sendUpdate(srpc); + response = false; + } + + if (!Element::iterateConnected(srpc)) { + response = false; + } + return response; +} + +Supla::Element &Supla::Sensor::ThermHygroPressMeter::disableChannelState() { + pressureChannel.unsetFlag(SUPLA_CHANNEL_FLAG_CHANNELSTATE); + return ThermHygroMeter::disableChannelState(); +} + +Supla::Channel *Supla::Sensor::ThermHygroPressMeter::getSecondaryChannel() { + return &pressureChannel; +} diff --git a/lib/SuplaDevice/src/supla/sensor/therm_hygro_press_meter.h b/lib/SuplaDevice/src/supla/sensor/therm_hygro_press_meter.h index 55441650..77d1156a 100644 --- a/lib/SuplaDevice/src/supla/sensor/therm_hygro_press_meter.h +++ b/lib/SuplaDevice/src/supla/sensor/therm_hygro_press_meter.h @@ -25,41 +25,12 @@ namespace Supla { namespace Sensor { class ThermHygroPressMeter : public ThermHygroMeter { public: - ThermHygroPressMeter() { - pressureChannel.setType(SUPLA_CHANNELTYPE_PRESSURESENSOR); - pressureChannel.setDefault(SUPLA_CHANNELFNC_PRESSURESENSOR); - } - - virtual double getPressure() { - return PRESSURE_NOT_AVAILABLE; - } - - void iterateAlways() { - if (millis() - lastReadTime > 10000) { - pressureChannel.setNewValue(getPressure()); - } - ThermHygroMeter::iterateAlways(); - } - - bool iterateConnected(void *srpc) { - bool response = true; - if (pressureChannel.isUpdateReady() && - millis() - pressureChannel.lastCommunicationTimeMs > 100) { - pressureChannel.lastCommunicationTimeMs = millis(); - pressureChannel.sendUpdate(srpc); - response = false; - } - - if (!Element::iterateConnected(srpc)) { - response = false; - } - return response; - } - - Element &disableChannelState() { - pressureChannel.unsetFlag(SUPLA_CHANNEL_FLAG_CHANNELSTATE); - return ThermHygroMeter::disableChannelState(); - } + ThermHygroPressMeter(); + virtual double getPressure(); + void iterateAlways(); + bool iterateConnected(void *srpc); + Element &disableChannelState(); + Channel *getSecondaryChannel(); protected: Channel pressureChannel; diff --git a/lib/SuplaDevice/src/supla/sensor/thermometer.cpp b/lib/SuplaDevice/src/supla/sensor/thermometer.cpp new file mode 100644 index 00000000..9525fb91 --- /dev/null +++ b/lib/SuplaDevice/src/supla/sensor/thermometer.cpp @@ -0,0 +1,37 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include "thermometer.h" + +Supla::Sensor::Thermometer::Thermometer() : lastReadTime(0) { + channel.setType(SUPLA_CHANNELTYPE_THERMOMETER); + channel.setDefault(SUPLA_CHANNELFNC_THERMOMETER); +} + +double Supla::Sensor::Thermometer::getValue() { + return TEMPERATURE_NOT_AVAILABLE; +} + +void Supla::Sensor::Thermometer::iterateAlways() { + if (lastReadTime + 10000 < millis()) { + lastReadTime = millis(); + channel.setNewValue(getValue()); + } +} + +Supla::Channel *Supla::Sensor::Thermometer::getChannel() { + return &channel; +} diff --git a/lib/SuplaDevice/src/supla/sensor/thermometer.h b/lib/SuplaDevice/src/supla/sensor/thermometer.h index 8dd11942..71f810fb 100644 --- a/lib/SuplaDevice/src/supla/sensor/thermometer.h +++ b/lib/SuplaDevice/src/supla/sensor/thermometer.h @@ -17,6 +17,7 @@ #ifndef _thermometer_h #define _thermometer_h +#include #include "supla/channel.h" #include "supla/element.h" @@ -26,26 +27,13 @@ namespace Supla { namespace Sensor { class Thermometer : public Element { public: - Thermometer() : lastReadTime(0) { - channel.setType(SUPLA_CHANNELTYPE_THERMOMETER); - channel.setDefault(SUPLA_CHANNELFNC_THERMOMETER); - } - - virtual double getValue() { - return TEMPERATURE_NOT_AVAILABLE; - } - - void iterateAlways() { - if (lastReadTime + 10000 < millis()) { - lastReadTime = millis(); - channel.setNewValue(getValue()); - } - } + Thermometer(); + virtual double getValue(); + void iterateAlways(); + + Channel *getChannel(); protected: - Channel *getChannel() { - return &channel; - } Channel channel; unsigned long lastReadTime; }; diff --git a/lib/SuplaDevice/src/supla/sensor/virtual_binary.h b/lib/SuplaDevice/src/supla/sensor/virtual_binary.h index 5c30c5f6..62d39b85 100644 --- a/lib/SuplaDevice/src/supla/sensor/virtual_binary.h +++ b/lib/SuplaDevice/src/supla/sensor/virtual_binary.h @@ -33,9 +33,9 @@ class VirtualBinary : public Element, public Triggerable { void iterateAlways(); void onInit(); void runAction(int event, int action); + Channel *getChannel(); protected: - Channel *getChannel(); Channel channel; bool state; unsigned long lastReadTime; diff --git a/lib/SuplaDevice/src/supla/sensor/weight.h b/lib/SuplaDevice/src/supla/sensor/weight.h index 8c318cbd..c5b8f8c4 100644 --- a/lib/SuplaDevice/src/supla/sensor/weight.h +++ b/lib/SuplaDevice/src/supla/sensor/weight.h @@ -43,11 +43,11 @@ class Weight : public Element { } } - - protected: Channel *getChannel() { return &channel; } + + protected: Channel channel; unsigned long lastReadTime; }; diff --git a/lib/SuplaDevice/src/supla/sensor/wind.h b/lib/SuplaDevice/src/supla/sensor/wind.h index 6869bc83..49b38afb 100644 --- a/lib/SuplaDevice/src/supla/sensor/wind.h +++ b/lib/SuplaDevice/src/supla/sensor/wind.h @@ -43,11 +43,11 @@ class Wind: public Element { } } - - protected: Channel *getChannel() { return &channel; } + + protected: Channel channel; unsigned long lastReadTime; }; diff --git a/lib/SuplaDevice/src/supla/tools.cpp b/lib/SuplaDevice/src/supla/tools.cpp index a4c9038c..f053ad41 100644 --- a/lib/SuplaDevice/src/supla/tools.cpp +++ b/lib/SuplaDevice/src/supla/tools.cpp @@ -41,3 +41,16 @@ void float2DoublePacked(float number, uint8_t *bar, int byteOrder) { } #endif } + +float doublePacked2float(uint8_t *bar) { + _FLOATCONV fl; + _DBLCONV dbl; + for (int i = 0; i < 8; i++) { + dbl.b[i] = bar[i]; + } + fl.p.s = dbl.p.s; + fl.p.m = dbl.p.m; + fl.p.e = dbl.p.e + 127 - 1023; // exponent adjust + + return fl.f; +} diff --git a/lib/SuplaDevice/src/supla/tools.h b/lib/SuplaDevice/src/supla/tools.h index 0a2c2a6d..ab2ac31f 100644 --- a/lib/SuplaDevice/src/supla/tools.h +++ b/lib/SuplaDevice/src/supla/tools.h @@ -19,9 +19,11 @@ #ifndef _tools_H_ #define _tools_H_ +#include #include "supla-common/IEEE754tools.h" void float2DoublePacked(float number, uint8_t *bar, int byteOrder = LSBFIRST); +float doublePacked2float(uint8_t *bar); #endif From 758093613b38a6e7aa6e9fabb7f0d0f76c9d3a51 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Thu, 14 Jan 2021 08:30:00 +0100 Subject: [PATCH 033/165] wsparcie dla MCP23017 --- platformio.ini | 16 +- src/GUI-Generic.ino | 33 +++-- src/GUI-Generic_Config.h | 1 + src/GUIGenericCommon.h | 2 +- src/Markup.cpp | 96 +++++++++--- src/Markup.h | 8 +- src/SuplaCommonPROGMEM.h | 36 ++++- src/SuplaConfigESP.cpp | 266 +++++++++++++++++++++++++++++++--- src/SuplaConfigESP.h | 32 +++- src/SuplaConfigManager.cpp | 33 ++--- src/SuplaConfigManager.h | 12 +- src/SuplaDeviceGUI.cpp | 2 +- src/SuplaDeviceGUI.h | 6 +- src/SuplaHTTPUpdateServer.cpp | 2 +- src/SuplaTemplateBoard.cpp | 4 + src/SuplaWebPageControl.cpp | 216 ++++++++++++--------------- src/SuplaWebPageControl.h | 31 ++-- src/SuplaWebPageRelay.cpp | 217 ++++++++++++--------------- src/SuplaWebPageRelay.h | 30 ++-- src/SuplaWebPageSensor.cpp | 88 ++++++----- src/SuplaWebPageSensor.h | 20 +-- src/SuplaWebServer.cpp | 126 ++++++++++++++-- src/SuplaWebServer.h | 33 ++++- 23 files changed, 883 insertions(+), 427 deletions(-) diff --git a/platformio.ini b/platformio.ini index 36bfbc41..9f97ff4b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = [common] -build_flags = -D BUILD_VERSION='"GUI 1.1.6"' +build_flags = -D BUILD_VERSION='"GUI 1.1.8"' -w -DATOMIC_FS_UPDATE -DBEARSSL_SSL_BASIC @@ -43,7 +43,8 @@ build_flags = -D BUILD_VERSION='"GUI 1.1.6"' -D SUPLA_HC_SR04 -D SUPLA_IMPULSE_COUNTER -D SUPLA_OLED - -D SUPLA_HLW8012 + -D SUPLA_HLW8012 + -D SUPLA_MCP23017 [env] framework = arduino @@ -52,6 +53,8 @@ upload_speed = 256000 monitor_speed = 74880 upload_resetmethod = nodemcu board_build.flash_mode = dout +; set frequency to 160MHz +board_build.f_cpu = 160000000L lib_deps = milesburton/DallasTemperature@^3.9.1 adafruit/DHT sensor library@^1.4.0 @@ -123,7 +126,7 @@ board = nodemcuv2 board_build.ldscript = eagle.flash.4m1m.ld build_flags = ${common.build_flags} -D DEBUG_MODE -build_unflags = -DUSE_CUSTOM +;build_unflags = -D SUPLA_IMPULSE_COUNTER [env:GUI_Generic_blank] board = nodemcuv2 @@ -145,4 +148,9 @@ build_unflags = -D SUPLA_OTA -D SUPLA_MAX6675 -D SUPLA_HC_SR04 -D SUPLA_IMPULSE_COUNTER - \ No newline at end of file + +[env:GUI_Generic_MCP23017] +board = esp8285 +board_build.ldscript = eagle.flash.1m64.ld +build_flags = ${common.build_flags} +build_unflags = -D SUPLA_IMPULSE_COUNTER \ No newline at end of file diff --git a/src/GUI-Generic.ino b/src/GUI-Generic.ino index 70fd892b..10f3ae50 100644 --- a/src/GUI-Generic.ino +++ b/src/GUI-Generic.ino @@ -15,6 +15,8 @@ */ #include "SuplaDeviceGUI.h" +#include + #define DRD_TIMEOUT 5 // Number of seconds after reset during which a subseqent reset will be considered a double reset. #define DRD_ADDRESS 0 // RTC Memory Address for the DoubleResetDetector to use DoubleResetDetector drd(DRD_TIMEOUT, DRD_ADDRESS); @@ -32,8 +34,8 @@ void setup() { #if defined(SUPLA_RELAY) || defined(SUPLA_ROLLERSHUTTER) uint8_t rollershutters = ConfigManager->get(KEY_MAX_ROLLERSHUTTER)->getValueInt(); - if (ConfigESP->getGpio(FUNCTION_RELAY) != OFF_GPIO && ConfigManager->get(KEY_MAX_RELAY)->getValueInt() > 0) { - for (nr = 1; nr <= ConfigManager->get(KEY_MAX_RELAY)->getValueInt(); nr++) { + for (nr = 1; nr <= ConfigManager->get(KEY_MAX_RELAY)->getValueInt(); nr++) { + if (ConfigESP->getGpio(nr, FUNCTION_RELAY) != OFF_GPIO) { #ifdef SUPLA_ROLLERSHUTTER if (rollershutters > 0) { #ifdef SUPLA_BUTTON @@ -65,8 +67,8 @@ void setup() { #endif #ifdef SUPLA_LIMIT_SWITCH - if (ConfigESP->getGpio(FUNCTION_LIMIT_SWITCH) != OFF_GPIO && ConfigManager->get(KEY_MAX_LIMIT_SWITCH)->getValueInt() > 0) { - for (nr = 1; nr <= ConfigManager->get(KEY_MAX_LIMIT_SWITCH)->getValueInt(); nr++) { + for (nr = 1; nr <= ConfigManager->get(KEY_MAX_LIMIT_SWITCH)->getValueInt(); nr++) { + if (ConfigESP->getGpio(nr, FUNCTION_LIMIT_SWITCH) != OFF_GPIO) { new Supla::Sensor::Binary(ConfigESP->getGpio(nr, FUNCTION_LIMIT_SWITCH), true); } } @@ -78,16 +80,16 @@ void setup() { #endif #ifdef SUPLA_DHT11 - if (ConfigESP->getGpio(FUNCTION_DHT11) != OFF_GPIO && ConfigManager->get(KEY_MAX_DHT11)->getValueInt() > 0) { - for (nr = 1; nr <= ConfigManager->get(KEY_MAX_DHT11)->getValueInt(); nr++) { + for (nr = 1; nr <= ConfigManager->get(KEY_MAX_DHT11)->getValueInt(); nr++) { + if (ConfigESP->getGpio(nr, FUNCTION_DHT11) != OFF_GPIO) { new Supla::Sensor::DHT(ConfigESP->getGpio(nr, FUNCTION_DHT11), DHT11); } } #endif #ifdef SUPLA_DHT22 - if (ConfigESP->getGpio(FUNCTION_DHT22) != OFF_GPIO && ConfigManager->get(KEY_MAX_DHT22)->getValueInt() > 0) { - for (nr = 1; nr <= ConfigManager->get(KEY_MAX_DHT22)->getValueInt(); nr++) { + for (nr = 1; nr <= ConfigManager->get(KEY_MAX_DHT22)->getValueInt(); nr++) { + if (ConfigESP->getGpio(nr, FUNCTION_DHT22) != OFF_GPIO) { Supla::GUI::sensorDHT22.push_back(new Supla::Sensor::DHT(ConfigESP->getGpio(nr, FUNCTION_DHT22), DHT22)); } } @@ -112,7 +114,7 @@ void setup() { #endif #if defined(SUPLA_BME280) || defined(SUPLA_SI7021) || defined(SUPLA_SHT3x) || defined(SUPLA_HTU21D) || defined(SUPLA_SHT71) || \ - defined(SUPLA_BH1750) || defined(SUPLA_MAX44009) + defined(SUPLA_BH1750) || defined(SUPLA_MAX44009) || defined(SUPLA_MCP23017) if (ConfigESP->getGpio(FUNCTION_SDA) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_SCL) != OFF_GPIO) { Wire.begin(ConfigESP->getGpio(FUNCTION_SDA), ConfigESP->getGpio(FUNCTION_SCL)); #ifdef SUPLA_BME280 @@ -157,6 +159,17 @@ void setup() { Supla::GUI::oled->addButtonOled(ConfigESP->getGpio(FUNCTION_CFG_BUTTON)); } #endif + + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt()) { + if (!mcp1.begin(0)) + Serial.println(F("MCP23017 1 not found!")); // begin(uint8_t address) "Pin 100 - 115" + if (!mcp2.begin(1)) + Serial.println(F("MCP23017 2 not found!")); // begin(uint8_t address) "Pin 116 - 131" + if (!mcp3.begin(2)) + Serial.println(F("MCP23017 3 not found!")); // begin(uint8_t address) "Pin 132 - 147" + if (!mcp4.begin(3)) + Serial.println(F("MCP23017 4 not found!")); // begin(uint8_t address) "Pin 148 - 163" + } } #endif @@ -170,7 +183,7 @@ void setup() { #ifdef SUPLA_IMPULSE_COUNTER if (ConfigManager->get(KEY_MAX_IMPULSE_COUNTER)->getValueInt() > 0) { for (nr = 1; nr <= ConfigManager->get(KEY_MAX_IMPULSE_COUNTER)->getValueInt(); nr++) { - if (ConfigESP->getGpio(FUNCTION_IMPULSE_COUNTER) != OFF_GPIO) { + if (ConfigESP->getGpio(nr, FUNCTION_IMPULSE_COUNTER) != OFF_GPIO) { Supla::GUI::addImpulseCounter(ConfigESP->getGpio(nr, FUNCTION_IMPULSE_COUNTER), ConfigESP->getLevel(nr, FUNCTION_IMPULSE_COUNTER), ConfigESP->getMemory(nr, FUNCTION_IMPULSE_COUNTER), ConfigManager->get(KEY_IMPULSE_COUNTER_DEBOUNCE_TIMEOUT)->getValueInt()); diff --git a/src/GUI-Generic_Config.h b/src/GUI-Generic_Config.h index 70aa3273..c3d7cb17 100644 --- a/src/GUI-Generic_Config.h +++ b/src/GUI-Generic_Config.h @@ -36,6 +36,7 @@ #define SUPLA_SHT3x #define SUPLA_SI7021 #define SUPLA_OLED +//#define SUPLA_MCP23017 // #define SUPLA_HTU21D // 0x40 NOT SUPPORTED // #define SUPLA_SHT71 // 0x44 AND 0x45 NOT SUPPORTED // #define SUPLA_BH1750 // 0x23 AND 0x5C NOT SUPPORTED diff --git a/src/GUIGenericCommon.h b/src/GUIGenericCommon.h index 62672230..3ed890bc 100644 --- a/src/GUIGenericCommon.h +++ b/src/GUIGenericCommon.h @@ -6,7 +6,7 @@ #else #define QUOTE(x) QUOTE_1(x) #define QUOTE_1(x) #x -#define INCLUDE_FILE(x) QUOTE(language/x.h) +#define INCLUDE_FILE(x) QUOTE(language / x.h) #include INCLUDE_FILE(UI_LANGUAGE) #endif diff --git a/src/Markup.cpp b/src/Markup.cpp index afdc7cf2..fc52249e 100644 --- a/src/Markup.cpp +++ b/src/Markup.cpp @@ -138,7 +138,38 @@ void addListGPIOBox(String& html, const String& input_id, const String& name, ui html += F(" "); html += name; html += F(""); - html += addListGPIOSelect(input_id.c_str(), function, nr); + addListGPIOSelect(html, input_id, function, nr); + html += F("
"); +} + +void addListMCP23017GPIOLinkBox(String& html, const String& input_id, const String& name, uint8_t function, const String& url, uint8_t nr) { + if (nr == 1) { + uint8_t address = ConfigESP->getAdressMCP23017(function); + addListBox(html, INPUT_ADRESS_MCP23017, F("MCP23017 Adres"), MCP23017_P, 3, address); + } + + html += F(""); + html += F(""); + addListMCP23017GPIO(html, input_id, function, nr); html += F(""); } @@ -171,15 +202,22 @@ void addListGPIOLinkBox(String& html, const String& input_id, const String& name html += F(""); } html += F(""); - html += addListGPIOSelect(input_id.c_str(), function, nr); + addListGPIOSelect(html, input_id, function, nr); html += F("
"); } -void addListBox(String& html, const String& input_id, const String& name, const char* const* array_P, uint8_t size, uint8_t selected) { +void addListBox(String& html, const String& input_id, const String& name, const char* const* array_P, uint8_t size, uint8_t selected, uint8_t nr) { html += F(""); + html += F("'>"); uint8_t selected = ConfigESP->getGpio(nr, function); for (uint8_t suported = 0; suported < sizeof(GPIO_P) / sizeof(GPIO_P[0]); suported++) { - if (ConfigESP->checkBusyGpio(suported, function) == false || selected == suported) { - page += F("
https://forum.supla.org/\n"; -const char HTTP_RBT[] PROGMEM = - ""; +const char HTTP_FAVICON[] PROGMEM = "\n"; + +const char HTTP_RBT[] PROGMEM = "
"; const char GPIO0[] PROGMEM = "GPIO0-D3"; const char GPIO1[] PROGMEM = "GPIO1-TX"; @@ -77,6 +79,26 @@ const char GPIONULL[] PROGMEM = ""; const char* const GPIO_P[] PROGMEM = {GPIO0, GPIO1, GPIO2, GPIO3, GPIO4, GPIO5, GPIONULL, GPIONULL, GPIONULL, GPIO9, GPIO10, GPIONULL, GPIO12, GPIO13, GPIO14, GPIO15, GPIO16, OFF}; +const char GPIO_A0[] PROGMEM = "A0"; +const char GPIO_A1[] PROGMEM = "A1"; +const char GPIO_A2[] PROGMEM = "A2"; +const char GPIO_A3[] PROGMEM = "A3"; +const char GPIO_A4[] PROGMEM = "A4"; +const char GPIO_A5[] PROGMEM = "A5"; +const char GPIO_A6[] PROGMEM = "A6"; +const char GPIO_A7[] PROGMEM = "A7"; +const char GPIO_B0[] PROGMEM = "B0"; +const char GPIO_B1[] PROGMEM = "B1"; +const char GPIO_B2[] PROGMEM = "B2"; +const char GPIO_B3[] PROGMEM = "B3"; +const char GPIO_B4[] PROGMEM = "B4"; +const char GPIO_B5[] PROGMEM = "B5"; +const char GPIO_B6[] PROGMEM = "B6"; +const char GPIO_B7[] PROGMEM = "B7"; + +const char* const GPIO_MCP23017_P[] PROGMEM = {GPIO_A0, GPIO_A1, GPIO_A2, GPIO_A3, GPIO_A4, GPIO_A5, GPIO_A6, GPIO_A7, GPIO_B0, + GPIO_B1, GPIO_B2, GPIO_B3, GPIO_B4, GPIO_B5, GPIO_B6, GPIO_B7, GPIONULL, OFF}; + const char ADR44[] PROGMEM = "0x44"; const char ADR45[] PROGMEM = "0x45"; const char ADR44_ADR45[] PROGMEM = "0x44 & 0x45"; @@ -87,6 +109,10 @@ const char ADR76_ADR77[] PROGMEM = "0x76 & 0x77"; const char* const BME280_P[] PROGMEM = {OFF, ADR76, ADR77, ADR76_ADR77}; const char* const SHT3x_P[] PROGMEM = {OFF, ADR44, ADR45, ADR44_ADR45}; +const char ADR20[] PROGMEM = "0x20"; +const char ADR21[] PROGMEM = "0x21"; +const char* const MCP23017_P[] PROGMEM = {ADR20, ADR21, OFF}; + const char* const STATE_P[] PROGMEM = {OFF, ON}; const char LOW_STATE_CONTROL[] PROGMEM = S_LOW; @@ -112,7 +138,7 @@ const char* const CFG_MODE_P[] PROGMEM = {CFG_10_PRESSES, CFG_5SEK_HOLD}; const char SSD1306[] PROGMEM = "SSD1306 - 0,96''"; const char SH1106[] PROGMEM = "SH1106 - 1,3''"; const char SSD1306_WEMOS_SHIELD[] PROGMEM = "SSD1306 - 0,66'' WEMOS OLED shield"; -const char *const OLED_P[] PROGMEM = {OFF, SSD1306, SH1106, SSD1306_WEMOS_SHIELD}; +const char* const OLED_P[] PROGMEM = {OFF, SSD1306, SH1106, SSD1306_WEMOS_SHIELD}; #endif String StateString(uint8_t adr); diff --git a/src/SuplaConfigESP.cpp b/src/SuplaConfigESP.cpp index 978c3738..fd04ca0c 100644 --- a/src/SuplaConfigESP.cpp +++ b/src/SuplaConfigESP.cpp @@ -20,6 +20,7 @@ #include "SuplaConfigManager.h" #include "SuplaDeviceGUI.h" #include "GUIGenericCommon.h" +#include "SuplaWebPageSensor.h" SuplaConfigESP::SuplaConfigESP() { configModeESP = NORMAL_MODE; @@ -95,11 +96,7 @@ void SuplaConfigESP::runAction(int event, int action) { } void SuplaConfigESP::rebootESP() { - delay(1000); - WiFi.forceSleepBegin(); - wdt_reset(); ESP.restart(); - while (1) wdt_reset(); } void SuplaConfigESP::configModeInit() { @@ -117,8 +114,9 @@ void SuplaConfigESP::iterateAlways() { } } -String SuplaConfigESP::getConfigNameAP() { - return "SUPLA-ESP8266-" + getMacAddress(false); +const String SuplaConfigESP::getConfigNameAP() { + String name = F("SUPLA-ESP8266-"); + return name += getMacAddress(false); } const char *SuplaConfigESP::getLastStatusSupla() { return supla_status.msg; @@ -251,6 +249,25 @@ int SuplaConfigESP::getGpio(int nr, int function) { return gpio; } } + // return OFF_GPIO; + //"Pin 100 - 115" + // Pin 116 - 131" + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt()) { + switch (getAdressMCP23017(function)) { + case 0: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_1).toInt() == function && + ConfigManager->get(key)->getElement(MCP23017_NR_1).toInt() == nr) { + return gpio + 100; + } + break; + case 1: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_2).toInt() == function && + ConfigManager->get(key)->getElement(MCP23017_NR_2).toInt() == nr) { + return gpio + 100 + 16; + } + break; + } + } } return OFF_GPIO; } @@ -260,10 +277,28 @@ int SuplaConfigESP::getLevel(int nr, int function) { uint8_t key = KEY_GPIO + gpio; if (ConfigManager->get(key)->getElement(FUNCTION).toInt() == function) { if (ConfigManager->get(key)->getElement(NR).toInt() == nr) { - uint8_t level = ConfigManager->get(key)->getElement(LEVEL).toInt(); - return level; + return ConfigManager->get(key)->getElement(LEVEL).toInt(); } } + + switch (getAdressMCP23017(function)) { + case 0: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_1).toInt() == function) { + if (ConfigManager->get(key)->getElement(MCP23017_NR_1).toInt() == nr) { + return ConfigManager->get(key)->getElement(LEVEL).toInt(); + ; + } + } + break; + case 1: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_2).toInt() == function) { + if (ConfigManager->get(key)->getElement(MCP23017_NR_2).toInt() == nr) { + return ConfigManager->get(key)->getElement(LEVEL).toInt(); + ; + } + } + break; + } } return OFF_GPIO; } @@ -273,10 +308,28 @@ int SuplaConfigESP::getMemory(int nr, int function) { uint8_t key = KEY_GPIO + gpio; if (ConfigManager->get(key)->getElement(FUNCTION).toInt() == function) { if (ConfigManager->get(key)->getElement(NR).toInt() == nr) { - uint8_t level = ConfigManager->get(key)->getElement(MEMORY).toInt(); - return level; + return ConfigManager->get(key)->getElement(MEMORY).toInt(); } } + + switch (getAdressMCP23017(function)) { + case 0: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_1).toInt() == function) { + if (ConfigManager->get(key)->getElement(MCP23017_NR_1).toInt() == nr) { + return ConfigManager->get(key)->getElement(MEMORY).toInt(); + ; + } + } + break; + case 1: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_2).toInt() == function) { + if (ConfigManager->get(key)->getElement(MCP23017_NR_2).toInt() == nr) { + return ConfigManager->get(key)->getElement(MEMORY).toInt(); + ; + } + } + break; + } } return OFF_GPIO; } @@ -286,10 +339,29 @@ int SuplaConfigESP::getAction(int nr, int function) { uint8_t key = KEY_GPIO + gpio; if (ConfigManager->get(key)->getElement(FUNCTION).toInt() == function) { if (ConfigManager->get(key)->getElement(NR).toInt() == nr) { - uint8_t action = ConfigManager->get(key)->getElement(ACTION).toInt(); - return action; + return ConfigManager->get(key)->getElement(ACTION).toInt(); + ; } } + + switch (getAdressMCP23017(function)) { + case 0: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_1).toInt() == function) { + if (ConfigManager->get(key)->getElement(MCP23017_NR_1).toInt() == nr) { + return ConfigManager->get(key)->getElement(ACTION).toInt(); + ; + } + } + break; + case 1: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_2).toInt() == function) { + if (ConfigManager->get(key)->getElement(MCP23017_NR_2).toInt() == nr) { + return ConfigManager->get(key)->getElement(ACTION).toInt(); + ; + } + } + break; + } } return OFF_GPIO; } @@ -304,7 +376,7 @@ bool SuplaConfigESP::checkBusyCfg(int gpio) { int SuplaConfigESP::checkBusyGpio(int gpio, int function) { if (gpio == 6 || gpio == 7 || gpio == 8 || gpio == 11) { - return true; + return false; } else { uint8_t key = KEY_GPIO + gpio; @@ -316,15 +388,15 @@ int SuplaConfigESP::checkBusyGpio(int gpio, int function) { } if (checkBusyCfg(gpio)) { if (function != FUNCTION_BUTTON) { - return true; + return false; } } if (ConfigManager->get(key)->getElement(FUNCTION).toInt() != FUNCTION_OFF) { if (ConfigManager->get(key)->getElement(FUNCTION).toInt() != function) { - return true; + return false; } } - return false; + return true; } } @@ -356,7 +428,7 @@ void SuplaConfigESP::clearGpio(uint8_t gpio, uint8_t function) { ConfigManager->setElement(key, NR, 0); ConfigManager->setElement(key, FUNCTION, FUNCTION_OFF); ConfigManager->setElement(key, LEVEL, 0); - ConfigManager->setElement(key, MEMORY, 0); + ConfigManager->setElement(key, MEMORY, 2); ConfigManager->setElement(key, ACTION, Supla::TOGGLE); } @@ -374,10 +446,148 @@ uint8_t SuplaConfigESP::countFreeGpio(uint8_t exception) { return count; } +bool SuplaConfigESP::checkBusyGpioMCP23017(uint8_t gpio, uint8_t function) { + if (gpio == OFF_GPIO) { + return true; + } + else if (gpio == 16) { + return false; + } + else { + uint8_t key = KEY_GPIO + gpio; + switch (getAdressMCP23017(function)) { + case 0: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_1).toInt() != FUNCTION_OFF) { + return false; + } + break; + case 1: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_2).toInt() != FUNCTION_OFF) { + return false; + } + break; + } + } + return true; +} + +uint8_t SuplaConfigESP::getGpioMCP23017(uint8_t nr, uint8_t function) { + for (uint8_t gpio = 0; gpio <= OFF_GPIO; gpio++) { + uint8_t key = KEY_GPIO + gpio; + + switch (getAdressMCP23017(function)) { + case 0: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_1).toInt() == function) { + if (ConfigManager->get(key)->getElement(MCP23017_NR_1).toInt() == nr) { + return gpio; + } + } + break; + case 1: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_2).toInt() == function) { + if (ConfigManager->get(key)->getElement(MCP23017_NR_2).toInt() == nr) { + return gpio; + } + } + break; + } + } + return OFF_GPIO; +} + +uint8_t SuplaConfigESP::getAdressMCP23017(uint8_t function) { + for (uint8_t gpio = 0; gpio <= OFF_GPIO; gpio++) { + uint8_t key = KEY_GPIO + gpio; + if (ConfigManager->get(key)->getElement(MCP23017_NR_1).toInt() != 0) { + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_1).toInt() == function) { + return 0; + } + } + if (ConfigManager->get(key)->getElement(MCP23017_NR_2).toInt() != 0) { + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_2).toInt() == function) { + return 1; + } + } + } + return 2; +} + +void SuplaConfigESP::setGpioMCP23017(uint8_t gpio, uint8_t adress, uint8_t nr, uint8_t function, uint8_t level, uint8_t memory) { + uint8_t key = KEY_GPIO + gpio; + + uint8_t _gpio = ConfigESP->getGpioMCP23017(nr, function); + ConfigESP->clearGpioMCP23017(_gpio, function); + + if (ConfigManager->get(key)->getElement(FUNCTION).toInt() == FUNCTION_OFF) { + ConfigManager->setElement(key, LEVEL, 0); + ConfigManager->setElement(key, MEMORY, 2); + ConfigManager->setElement(key, ACTION, Supla::TOGGLE); + } + else { + ConfigManager->setElement(key, LEVEL, level); + ConfigManager->setElement(key, MEMORY, memory); + ConfigManager->setElement(key, ACTION, Supla::TOGGLE); + } + + switch (adress) { + case 0: + ConfigManager->setElement(key, MCP23017_NR_1, nr); + ConfigManager->setElement(key, MCP23017_FUNCTION_1, function); + break; + case 1: + ConfigManager->setElement(key, MCP23017_NR_2, nr); + ConfigManager->setElement(key, MCP23017_FUNCTION_2, function); + break; + } +} + +void SuplaConfigESP::clearGpioMCP23017(uint8_t gpio, uint8_t function) { + uint8_t key = KEY_GPIO + gpio; + uint8_t adress = getAdressMCP23017(function); + + ConfigManager->setElement(key, getNrMCP23017(adress), 0); + ConfigManager->setElement(key, getFunctionMCP23017(adress), FUNCTION_OFF); +} + +void SuplaConfigESP::clearFunctionGpio(uint8_t function) { + for (uint8_t gpio = 0; gpio <= OFF_GPIO; gpio++) { + uint8_t key = KEY_GPIO + gpio; + + if (ConfigManager->get(key)->getElement(FUNCTION).toInt() == function) { + ConfigManager->setElement(key, NR, 0); + ConfigManager->setElement(key, FUNCTION, FUNCTION_OFF); + } + } +} + +uint8_t SuplaConfigESP::getFunctionMCP23017(uint8_t adress) { + switch (adress) { + case 0: + return MCP23017_FUNCTION_1; + break; + case 1: + return MCP23017_FUNCTION_2; + break; + } + return OFF_GPIO; +} + +uint8_t SuplaConfigESP::getNrMCP23017(uint8_t adress) { + switch (adress) { + case 0: + return MCP23017_NR_1; + break; + case 1: + return MCP23017_NR_2; + break; + } + return OFF_GPIO; +} + void SuplaConfigESP::factoryReset(bool forceReset) { delay(1000); - pinMode(0, INPUT); - if (!digitalRead(0) || forceReset) { + pinMode(0, INPUT_PULLUP); + if (digitalRead(0) != HIGH || forceReset) { Serial.println(F("FACTORY RESET!!!")); EEPROM.begin(1024); @@ -426,3 +636,21 @@ void SuplaConfigESP::factoryReset(bool forceReset) { // rebootESP(); } } + +uint32_t lowestRAM = 0; +uint32_t lowestFreeStack = 0; + +void checkRAM() { + uint32_t freeRAM = ESP.getFreeHeap(); + Serial.print(F("freeRAM: ")); + Serial.println(freeRAM); + if (freeRAM <= lowestRAM) { + lowestRAM = freeRAM; + } + uint32_t freeStack = ESP.getFreeContStack(); + Serial.print(F("freeStack: ")); + Serial.println(freeStack); + if (freeStack <= lowestFreeStack) { + lowestFreeStack = freeStack; + } +} diff --git a/src/SuplaConfigESP.h b/src/SuplaConfigESP.h index b96df291..7a0c9ae9 100644 --- a/src/SuplaConfigESP.h +++ b/src/SuplaConfigESP.h @@ -22,8 +22,20 @@ #include #include "GUI-Generic_Config.h" -enum _configModeESP { NORMAL_MODE, CONFIG_MODE }; -enum _ConfigMode { CONFIG_MODE_10_ON_PRESSES, CONFIG_MODE_5SEK_HOLD, FACTORYRESET }; +#include +#include + +enum _configModeESP +{ + NORMAL_MODE, + CONFIG_MODE +}; +enum _ConfigMode +{ + CONFIG_MODE_10_ON_PRESSES, + CONFIG_MODE_5SEK_HOLD, + FACTORYRESET +}; typedef struct { int status; @@ -81,7 +93,16 @@ class SuplaConfigESP : public Supla::Triggerable, public Supla::Element { int getAction(int nr, int function); void factoryReset(bool forceReset = false); - String getConfigNameAP(); + const String getConfigNameAP(); + + bool checkBusyGpioMCP23017(uint8_t gpio, uint8_t function); + uint8_t getGpioMCP23017(uint8_t nr, uint8_t function); + uint8_t getAdressMCP23017(uint8_t function); + void setGpioMCP23017(uint8_t gpio, uint8_t adress, uint8_t nr, uint8_t function, uint8_t level, uint8_t memory); + void clearGpioMCP23017(uint8_t gpio, uint8_t function); + void clearFunctionGpio(uint8_t function); + uint8_t getFunctionMCP23017(uint8_t adress); + uint8_t getNrMCP23017(uint8_t adress); private: void configModeInit(); @@ -97,4 +118,9 @@ class SuplaConfigESP : public Supla::Triggerable, public Supla::Element { void ledBlinking_func(void *timer_arg); void status_func(int status, const char *msg); + +uint32_t getFreeStackWatermark(); +unsigned long FreeMem(); +void checkRAM(); + #endif // SuplaConfigESP_h diff --git a/src/SuplaConfigManager.cpp b/src/SuplaConfigManager.cpp index 4854a66b..1b999582 100644 --- a/src/SuplaConfigManager.cpp +++ b/src/SuplaConfigManager.cpp @@ -34,8 +34,10 @@ ConfigOption::ConfigOption(uint8_t key, const char *value, int maxLength) { strncpy(_key, key, size); _key[size - 1] = '\0'; */ - _key = key; + _key = key; _maxLength = maxLength + 1; + + _value = new char[_maxLength]; setValue(value); } @@ -82,8 +84,9 @@ int ConfigOption::getLength() { return _maxLength; } -String ConfigOption::getElement(int index) { +const String ConfigOption::getElement(int index) { String data = _value; + data.reserve(_maxLength); int found = 0; int strIndex[] = {0, -1}; int maxIndex = data.length() - 1; @@ -97,8 +100,9 @@ String ConfigOption::getElement(int index) { return found > index ? data.substring(strIndex[0], strIndex[1]) : ""; } -String ConfigOption::replaceElement(int index, int newvalue) { +const String ConfigOption::replaceElement(int index, int newvalue) { String data = _value; + data.reserve(_maxLength); int lenght = SETTINGSCOUNT; String table; for (int i = 0; i <= lenght; i++) { @@ -115,16 +119,8 @@ String ConfigOption::replaceElement(int index, int newvalue) { } void ConfigOption::setValue(const char *value) { - // size_t size = _maxLength + 1; - //_value = (char *)malloc(sizeof(char) * (size)); - - // if (value != NULL) { - // memcpy(_value, value, size - 1); - // _value[size - 1] = 0; - //} if (value != NULL) { size_t size = getLength(); - _value = new char[size]; strncpy(_value, value, size); _value[size - 1] = '\0'; } @@ -162,13 +158,13 @@ SuplaConfigManager::SuplaConfigManager() { this->addKey(KEY_MAX_IMPULSE_COUNTER, "0", 2); uint8_t nr, key; - + for (nr = 0; nr <= 17; nr++) { key = KEY_GPIO + nr; - this->addKey(key, "0,0,0,0,0,0", 16); + this->addKey(key, "0,0,0,0,0,0", 28); } - - this->addKey(KEY_ACTIVE_SENSOR, "0,0,0,0,0", 14); + + this->addKey(KEY_ACTIVE_SENSOR, "0,0,0,0,0", 16); this->addKey(KEY_BOARD, "0", 2); this->addKey(KEY_CFG_MODE, "0", 2); @@ -334,7 +330,8 @@ uint8_t SuplaConfigManager::save() { if (configFile) { uint8_t *content = (uint8_t *)malloc(sizeof(uint8_t) * length); for (i = 0; i < _optionCount; i++) { - Serial.println("Save key " + String(_options[i]->getKey()) + ": " + String(_options[i]->getValue())); + Serial.printf_P(PSTR("Save key=%d"), _options[i]->getKey()); + Serial.printf_P(PSTR(" value=%s\n"), _options[i]->getValue()); memcpy(content + offset, _options[i]->getValue(), _options[i]->getLength()); offset += _options[i]->getLength(); } @@ -355,7 +352,9 @@ uint8_t SuplaConfigManager::save() { void SuplaConfigManager::showAllValue() { for (int i = 0; i < _optionCount; i++) { - Serial.println("Key: " + String(_options[i]->getKey()) + " Value: " + String(_options[i]->getValue())); + // Serial.println("Key: " + String(_options[i]->getKey()) + " Value: " + String(_options[i]->getValue())); + Serial.printf_P(PSTR("Key=%d"), _options[i]->getKey()); + Serial.printf_P(PSTR(" value=%s\n"), _options[i]->getValue()); } } diff --git a/src/SuplaConfigManager.h b/src/SuplaConfigManager.h index 73b6a35b..10eb2dde 100644 --- a/src/SuplaConfigManager.h +++ b/src/SuplaConfigManager.h @@ -86,6 +86,14 @@ enum _settings MEMORY, CFG, ACTION, + MCP23017_NR_1, + MCP23017_FUNCTION_1, + MCP23017_NR_2, + MCP23017_FUNCTION_2, + MCP23017_NR_3, + MCP23017_FUNCTION_3, + MCP23017_NR_4, + MCP23017_FUNCTION_4, SETTINGSCOUNT }; @@ -138,8 +146,8 @@ class ConfigOption { int getLength(); void setValue(const char *value); - String getElement(int index); - String replaceElement(int index, int value); + const String getElement(int index); + const String replaceElement(int index, int value); private: uint8_t _key; diff --git a/src/SuplaDeviceGUI.cpp b/src/SuplaDeviceGUI.cpp index 6e2d5471..3bc9c2ff 100644 --- a/src/SuplaDeviceGUI.cpp +++ b/src/SuplaDeviceGUI.cpp @@ -27,6 +27,7 @@ Supla::Eeprom eeprom(STORAGE_OFFSET); namespace Supla { namespace GUI { void begin() { + #ifdef DEBUG_MODE new Supla::Sensor::EspFreeHeap(); #endif @@ -50,7 +51,6 @@ void begin() { (char *)ConfigManager->get(KEY_SUPLA_AUTHKEY)->getValue()); // Authorization key ConfigManager->showAllValue(); - WebServer->begin(); } diff --git a/src/SuplaDeviceGUI.h b/src/SuplaDeviceGUI.h index b6dde7a5..59f0d387 100644 --- a/src/SuplaDeviceGUI.h +++ b/src/SuplaDeviceGUI.h @@ -28,6 +28,7 @@ #include "SuplaConfigManager.h" #include "SuplaWebServer.h" #include "SuplaWebPageRelay.h" +#include "SuplaWebPageSensor.h" #include "SuplaWebPageDownload.h" #include "SuplaWebPageUpload.h" @@ -47,16 +48,15 @@ #include #include #include + #ifdef SUPLA_BME280 #include -#include "SuplaWebPageSensor.h" #endif #ifdef SUPLA_SI7021_SONOFF #include #endif #ifdef SUPLA_BME280 #include -#include "SuplaWebPageSensor.h" #endif #ifdef SUPLA_SHT3x #include @@ -76,6 +76,8 @@ #include +#include + namespace Supla { namespace GUI { diff --git a/src/SuplaHTTPUpdateServer.cpp b/src/SuplaHTTPUpdateServer.cpp index 7a68aaf8..593fd9cc 100644 --- a/src/SuplaHTTPUpdateServer.cpp +++ b/src/SuplaHTTPUpdateServer.cpp @@ -133,7 +133,7 @@ void ESP8266HTTPUpdateServer::setup(ESP8266WebServer* server, const String& path else if (_authenticated && upload.status == UPLOAD_FILE_ABORTED) { Update.end(); if (_serial_output) - Serial.println("Update was aborted"); + Serial.println(F("Update was aborted")); } delay(0); }); diff --git a/src/SuplaTemplateBoard.cpp b/src/SuplaTemplateBoard.cpp index 474322b8..ebe6a4af 100644 --- a/src/SuplaTemplateBoard.cpp +++ b/src/SuplaTemplateBoard.cpp @@ -38,12 +38,14 @@ void addButtonCFG(uint8_t gpio) { ConfigESP->setGpio(gpio, FUNCTION_CFG_BUTTON); } +#ifdef SUPLA_HLW8012 void addHLW8012(int8_t pinCF, int8_t pinCF1, int8_t pinSEL) { ConfigESP->setGpio(pinCF, FUNCTION_CF); ConfigESP->setGpio(pinCF1, FUNCTION_CF1); ConfigESP->setGpio(pinSEL, FUNCTION_SEL); Supla::GUI::addHLW8012(ConfigESP->getGpio(FUNCTION_CF), ConfigESP->getGpio(FUNCTION_CF1), ConfigESP->getGpio(FUNCTION_SEL)); } +#endif void chooseTemplateBoard(uint8_t board) { ConfigManager->set(KEY_MAX_BUTTON, "0"); @@ -193,11 +195,13 @@ void chooseTemplateBoard(uint8_t board) { addButtonCFG(13); addButton(13, Supla::ON_RELEASE); addRelay(15); +#ifdef SUPLA_HLW8012 addHLW8012(5, 4, 12); Supla::GUI::counterHLW8012->setCurrentMultiplier(18388); Supla::GUI::counterHLW8012->setVoltageMultiplier(247704); Supla::GUI::counterHLW8012->setPowerMultiplier(2586583); Supla::GUI::counterHLW8012->saveState(); +#endif break; } } diff --git a/src/SuplaWebPageControl.cpp b/src/SuplaWebPageControl.cpp index 9033f567..28411956 100644 --- a/src/SuplaWebPageControl.cpp +++ b/src/SuplaWebPageControl.cpp @@ -35,7 +35,7 @@ void SuplaWebPageControl::handleControl() { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); } - WebServer->sendContent(supla_webpage_control(0)); + supla_webpage_control(0); } void SuplaWebPageControl::handleControlSave() { @@ -45,14 +45,22 @@ void SuplaWebPageControl::handleControlSave() { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); } - String key, input; - uint8_t nr, current_value, last_value; + + uint8_t nr, last_value; #ifdef SUPLA_BUTTON last_value = ConfigManager->get(KEY_MAX_BUTTON)->getValueInt(); for (nr = 1; nr <= last_value; nr++) { - if (!WebServer->saveGPIO(INPUT_BUTTON_GPIO, FUNCTION_BUTTON, nr, INPUT_MAX_BUTTON)) { - WebServer->sendContent(supla_webpage_control(6)); - return; + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt() != FUNCTION_OFF) { + if (!WebServer->saveGpioMCP23017(INPUT_BUTTON_GPIO, FUNCTION_BUTTON, nr, INPUT_MAX_BUTTON)) { + supla_webpage_control(6); + return; + } + } + else { + if (!WebServer->saveGPIO(INPUT_BUTTON_GPIO, FUNCTION_BUTTON, nr, INPUT_MAX_BUTTON)) { + supla_webpage_control(6); + return; + } } } @@ -65,7 +73,7 @@ void SuplaWebPageControl::handleControlSave() { last_value = ConfigManager->get(KEY_MAX_LIMIT_SWITCH)->getValueInt(); for (nr = 1; nr <= last_value; nr++) { if (!WebServer->saveGPIO(INPUT_LIMIT_SWITCH_GPIO, FUNCTION_LIMIT_SWITCH, nr, INPUT_MAX_LIMIT_SWITCH)) { - WebServer->sendContent(supla_webpage_control(6)); + supla_webpage_control(6); return; } } @@ -77,55 +85,58 @@ void SuplaWebPageControl::handleControlSave() { switch (ConfigManager->save()) { case E_CONFIG_OK: - // Serial.println(F("E_CONFIG_OK: Config save")); - WebServer->sendContent(supla_webpage_control(1)); + supla_webpage_control(1); break; case E_CONFIG_FILE_OPEN: - // Serial.println(F("E_CONFIG_FILE_OPEN: Couldn't open file")); - WebServer->sendContent(supla_webpage_control(2)); + supla_webpage_control(2); break; } } -String SuplaWebPageControl::supla_webpage_control(int save) { - uint8_t nr, suported, selected; - String pagebutton, key; +void SuplaWebPageControl::supla_webpage_control(int save) { + uint8_t nr, countFreeGpio; - pagebutton += SuplaSaveResult(save); - pagebutton += SuplaJavaScript(PATH_CONTROL); - pagebutton += F("
"); + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(PATH_CONTROL); + addForm(webContentBuffer, F("post"), PATH_SAVE_CONTROL); #if (defined(SUPLA_BUTTON) && defined(SUPLA_RELAY)) || (defined(SUPLA_BUTTON) && defined(SUPLA_ROLLERSHUTTER)) - addFormHeader(pagebutton, String(S_GPIO_SETTINGS_FOR_BUTTONS)); - addNumberBox(pagebutton, INPUT_MAX_BUTTON, S_QUANTITY, KEY_MAX_BUTTON, ConfigESP->countFreeGpio(FUNCTION_BUTTON)); + addFormHeader(webContentBuffer, String(S_GPIO_SETTINGS_FOR_BUTTONS)); + + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt() != FUNCTION_OFF) { + countFreeGpio = 16; + } + else { + countFreeGpio = ConfigESP->countFreeGpio(FUNCTION_BUTTON); + } + + addNumberBox(webContentBuffer, INPUT_MAX_BUTTON, S_QUANTITY, KEY_MAX_BUTTON, countFreeGpio); + for (nr = 1; nr <= ConfigManager->get(KEY_MAX_BUTTON)->getValueInt(); nr++) { - addListGPIOLinkBox(pagebutton, INPUT_BUTTON_GPIO, S_BUTTON, FUNCTION_BUTTON, PATH_BUTTON_SET, nr); + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt() != FUNCTION_OFF) { + addListMCP23017GPIOLinkBox(webContentBuffer, INPUT_BUTTON_GPIO, S_BUTTON, FUNCTION_BUTTON, PATH_BUTTON_SET, nr); + } + else { + addListGPIOLinkBox(webContentBuffer, INPUT_BUTTON_GPIO, S_BUTTON, FUNCTION_BUTTON, PATH_BUTTON_SET, nr); + } } - addFormHeaderEnd(pagebutton); + addFormHeaderEnd(webContentBuffer); #endif #ifdef SUPLA_LIMIT_SWITCH - addFormHeader(pagebutton, String(S_GPIO_SETTINGS_FOR_LIMIT_SWITCH)); - addNumberBox(pagebutton, INPUT_MAX_LIMIT_SWITCH, S_QUANTITY, KEY_MAX_LIMIT_SWITCH, ConfigESP->countFreeGpio(FUNCTION_LIMIT_SWITCH)); + addFormHeader(webContentBuffer, String(S_GPIO_SETTINGS_FOR_LIMIT_SWITCH)); + addNumberBox(webContentBuffer, INPUT_MAX_LIMIT_SWITCH, S_QUANTITY, KEY_MAX_LIMIT_SWITCH, ConfigESP->countFreeGpio(FUNCTION_LIMIT_SWITCH)); for (nr = 1; nr <= ConfigManager->get(KEY_MAX_LIMIT_SWITCH)->getValueInt(); nr++) { - addListGPIOBox(pagebutton, INPUT_LIMIT_SWITCH_GPIO, S_LIMIT_SWITCH, FUNCTION_LIMIT_SWITCH, nr); + addListGPIOBox(webContentBuffer, INPUT_LIMIT_SWITCH_GPIO, S_LIMIT_SWITCH, FUNCTION_LIMIT_SWITCH, nr); } - addFormHeaderEnd(pagebutton); + addFormHeaderEnd(webContentBuffer); #endif - pagebutton += F("
"); - pagebutton += F("
"); - pagebutton += F("

"); - return pagebutton; + addButtonSubmit(webContentBuffer, S_SAVE); + addFormEnd(webContentBuffer); + addButton(webContentBuffer, S_RETURN, PATH_DEVICE_SETTINGS); + + WebServer->sendContent(); } #if (defined(SUPLA_BUTTON) && defined(SUPLA_RELAY)) || (defined(SUPLA_BUTTON) && defined(SUPLA_ROLLERSHUTTER)) @@ -134,7 +145,7 @@ void SuplaWebPageControl::handleButtonSet() { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); } - WebServer->sendContent(supla_webpage_button_set(0)); + supla_webpage_button_set(0); } void SuplaWebPageControl::handleButtonSaveSet() { @@ -144,16 +155,28 @@ void SuplaWebPageControl::handleButtonSaveSet() { return WebServer->httpServer.requestAuthentication(); } - String readUrl, nr_button, input; - uint8_t place; + String readUrl, nr_button, input, path; + uint8_t place, key, gpio; + + input.reserve(10); + readUrl.reserve(11); + nr_button.reserve(2); + path.reserve(14); - String path = PATH_START; + path = PATH_START; path += PATH_SAVE_BUTTON_SET; readUrl = WebServer->httpServer.uri(); place = readUrl.indexOf(path); nr_button = readUrl.substring(place + path.length(), place + path.length() + 3); - uint8_t key = KEY_GPIO + ConfigESP->getGpio(nr_button.toInt(), FUNCTION_BUTTON); + + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt() != FUNCTION_OFF) { + gpio = ConfigESP->getGpioMCP23017(nr_button.toInt(), FUNCTION_BUTTON); + } + else { + gpio = ConfigESP->getGpio(nr_button.toInt(), FUNCTION_BUTTON); + } + key = KEY_GPIO + gpio; input = INPUT_BUTTON_LEVEL; input += nr_button; @@ -165,99 +188,46 @@ void SuplaWebPageControl::handleButtonSaveSet() { switch (ConfigManager->save()) { case E_CONFIG_OK: - // Serial.println(F("E_CONFIG_OK: Dane zapisane")); - WebServer->sendContent(supla_webpage_control(1)); + supla_webpage_control(1); break; - case E_CONFIG_FILE_OPEN: - // Serial.println(F("E_CONFIG_FILE_OPEN: Couldn't open file")); - WebServer->sendContent(supla_webpage_control(2)); + supla_webpage_control(2); break; } } -String SuplaWebPageControl::supla_webpage_button_set(int save) { - String readUrl, nr_button, key; - uint8_t place, selected, suported; +void SuplaWebPageControl::supla_webpage_button_set(int save) { + String path, readUrl, nr_button; + uint8_t place, selected; + + path.reserve(10); + readUrl.reserve(11); + nr_button.reserve(2); - String path = PATH_START; + path = PATH_START; path += PATH_BUTTON_SET; readUrl = WebServer->httpServer.uri(); place = readUrl.indexOf(path); nr_button = readUrl.substring(place + path.length(), place + path.length() + 3); - String page = ""; - page += SuplaSaveResult(save); - page += SuplaJavaScript(PATH_CONTROL); - uint8_t buttons = ConfigManager->get(KEY_MAX_BUTTON)->getValueInt(); - if (nr_button.toInt() <= buttons && ConfigESP->getGpio(nr_button.toInt(), FUNCTION_BUTTON) != OFF_GPIO) { - page += F("

"); - page += S_BUTTON_NR_SETTINGS; - page += F(" "); - page += nr_button; - page += F("

"); - - page += F(""); - - page += F(""); + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(PATH_CONTROL); - page += F("
"); - } - else { - page += F("

"); - page += S_NO_BUTTON_NR; - page += F("

"); - page += nr_button; - page += F(""); - } - page += F("
"); - page += F("

"); - - return page; + addForm(webContentBuffer, F("post"), PATH_SAVE_BUTTON_SET + nr_button); + addFormHeader(webContentBuffer, S_BUTTON_NR_SETTINGS + nr_button); + + selected = ConfigESP->getLevel(nr_button.toInt(), FUNCTION_BUTTON); + addListBox(webContentBuffer, INPUT_BUTTON_LEVEL + nr_button, S_REACTION_TO, TRIGGER_P, 3, selected); + + selected = ConfigESP->getAction(nr_button.toInt(), FUNCTION_BUTTON); + addListBox(webContentBuffer, INPUT_BUTTON_ACTION + nr_button, S_ACTION, ACTION_P, 3, selected); + + addFormHeaderEnd(webContentBuffer); + addButtonSubmit(webContentBuffer, S_SAVE); + addFormEnd(webContentBuffer); + addButton(webContentBuffer, S_RETURN, PATH_RELAY); + + WebServer->sendContent(); } #endif diff --git a/src/SuplaWebPageControl.h b/src/SuplaWebPageControl.h index 99ef91f1..c7043de2 100644 --- a/src/SuplaWebPageControl.h +++ b/src/SuplaWebPageControl.h @@ -4,18 +4,18 @@ #include "SuplaWebServer.h" #include "SuplaDeviceGUI.h" -#define PATH_CONTROL "control" -#define PATH_SAVE_CONTROL "savecontrol" -#define PATH_BUTTON_SET "setbutton" -#define PATH_SAVE_BUTTON_SET "savesetbutton" -#define INPUT_TRIGGER "trs" -#define INPUT_BUTTON_SET "bts" -#define INPUT_BUTTON_GPIO "btg" -#define INPUT_BUTTON_LEVEL "icl" -#define INPUT_BUTTON_ACTION "bta" -#define INPUT_LIMIT_SWITCH_GPIO "lsg" -#define INPUT_MAX_BUTTON "mbt" -#define INPUT_MAX_LIMIT_SWITCH "mls" +#define PATH_CONTROL "control" +#define PATH_SAVE_CONTROL "savecontrol" +#define PATH_BUTTON_SET "setbutton" +#define PATH_SAVE_BUTTON_SET "savesetbutton" +#define INPUT_TRIGGER "trs" +#define INPUT_BUTTON_SET "bts" +#define INPUT_BUTTON_GPIO "btg" +#define INPUT_BUTTON_LEVEL "icl" +#define INPUT_BUTTON_ACTION "bta" +#define INPUT_LIMIT_SWITCH_GPIO "lsg" +#define INPUT_MAX_BUTTON "mbt" +#define INPUT_MAX_LIMIT_SWITCH "mls" /*enum _trigger_button { TRIGGER_PRESS, @@ -32,15 +32,14 @@ class SuplaWebPageControl { #if (defined(SUPLA_BUTTON) && defined(SUPLA_RELAY)) || (defined(SUPLA_RSUPLA_BUTTONELAY) || defined(SUPLA_ROLLERSHUTTER)) void handleButtonSet(); void handleButtonSaveSet(); -#endif private: - String supla_webpage_control(int save); + void supla_webpage_control(int save); +#endif #ifdef SUPLA_BUTTON - String supla_webpage_button_set(int save); + void supla_webpage_button_set(int save); #endif - }; extern SuplaWebPageControl* WebPageControl; diff --git a/src/SuplaWebPageRelay.cpp b/src/SuplaWebPageRelay.cpp index a2e44450..9d3115d7 100644 --- a/src/SuplaWebPageRelay.cpp +++ b/src/SuplaWebPageRelay.cpp @@ -4,6 +4,7 @@ #include "SuplaCommonPROGMEM.h" #include "GUIGenericCommon.h" #include "Markup.h" +#include "SuplaWebPageSensor.h" #if defined(SUPLA_RELAY) || defined(SUPLA_ROLLERSHUTTER) SuplaWebPageRelay *WebPageRelay = new SuplaWebPageRelay(); @@ -13,23 +14,25 @@ SuplaWebPageRelay::SuplaWebPageRelay() { void SuplaWebPageRelay::createWebPageRelay() { String path; + path.reserve(11); + path += PATH_START; path += PATH_RELAY; - WebServer->httpServer.on(path, std::bind(&SuplaWebPageRelay::handleRelay, this)); + WebServer->httpServer.on(path, HTTP_GET, std::bind(&SuplaWebPageRelay::handleRelay, this)); path = PATH_START; path += PATH_SAVE_RELAY; - WebServer->httpServer.on(path, std::bind(&SuplaWebPageRelay::handleRelaySave, this)); + WebServer->httpServer.on(path, HTTP_POST, std::bind(&SuplaWebPageRelay::handleRelaySave, this)); - for (uint8_t i = 1; i <= MAX_GPIO; i++) { + for (uint8_t i = 1; i <= ConfigManager->get(KEY_MAX_RELAY)->getValueInt(); i++) { path = PATH_START; path += PATH_RELAY_SET; path += i; - WebServer->httpServer.on(path, std::bind(&SuplaWebPageRelay::handleRelaySet, this)); + WebServer->httpServer.on(path, HTTP_GET, std::bind(&SuplaWebPageRelay::handleRelaySet, this)); path = PATH_START; path += PATH_SAVE_RELAY_SET; path += i; - WebServer->httpServer.on(path, std::bind(&SuplaWebPageRelay::handleRelaySaveSet, this)); + WebServer->httpServer.on(path, HTTP_POST, std::bind(&SuplaWebPageRelay::handleRelaySaveSet, this)); } } @@ -38,12 +41,10 @@ void SuplaWebPageRelay::handleRelay() { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); } - WebServer->sendContent(supla_webpage_relay(0)); + supla_webpage_relay(0); } void SuplaWebPageRelay::handleRelaySave() { - // Serial.println(F("HTTP_POST - metoda handleRelaySave")); - if (ConfigESP->configModeESP == NORMAL_MODE) { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); @@ -53,9 +54,17 @@ void SuplaWebPageRelay::handleRelaySave() { last_value = ConfigManager->get(KEY_MAX_RELAY)->getValueInt(); for (nr = 1; nr <= last_value; nr++) { - if (!WebServer->saveGPIO(INPUT_RELAY_GPIO, FUNCTION_RELAY, nr, INPUT_MAX_RELAY)) { - WebServer->sendContent(supla_webpage_relay(6)); - return; + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt() != FUNCTION_OFF) { + if (!WebServer->saveGpioMCP23017(INPUT_RELAY_GPIO, FUNCTION_RELAY, nr, INPUT_MAX_RELAY)) { + supla_webpage_relay(6); + return; + } + } + else { + if (!WebServer->saveGPIO(INPUT_RELAY_GPIO, FUNCTION_RELAY, nr, INPUT_MAX_RELAY)) { + supla_webpage_relay(6); + return; + } } } @@ -65,42 +74,47 @@ void SuplaWebPageRelay::handleRelaySave() { switch (ConfigManager->save()) { case E_CONFIG_OK: - // Serial.println(F("E_CONFIG_OK: Config save")); - WebServer->sendContent(supla_webpage_relay(1)); + supla_webpage_relay(1); break; case E_CONFIG_FILE_OPEN: - // Serial.println(F("E_CONFIG_FILE_OPEN: Couldn't open file")); - WebServer->sendContent(supla_webpage_relay(2)); + supla_webpage_relay(2); break; } } -String SuplaWebPageRelay::supla_webpage_relay(int save) { - uint8_t selected, suported, nr; - - String pagerelay = ""; - pagerelay += SuplaSaveResult(save); - pagerelay += SuplaJavaScript(PATH_RELAY); - pagerelay += F("
"); - addFormHeader(pagerelay, String(S_GPIO_SETTINGS_FOR_RELAYS)); - addNumberBox(pagerelay, INPUT_MAX_RELAY, S_QUANTITY, KEY_MAX_RELAY, ConfigESP->countFreeGpio(FUNCTION_RELAY)); +void SuplaWebPageRelay::supla_webpage_relay(int save) { + uint8_t nr, countFreeGpio; + + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(PATH_RELAY); + + addForm(webContentBuffer, F("post"), PATH_SAVE_RELAY); + addFormHeader(webContentBuffer, S_GPIO_SETTINGS_FOR_RELAYS); + + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt() != FUNCTION_OFF) { + countFreeGpio = 16; + } + else { + countFreeGpio = ConfigESP->countFreeGpio(FUNCTION_RELAY); + } + + addNumberBox(webContentBuffer, INPUT_MAX_RELAY, S_QUANTITY, KEY_MAX_RELAY, countFreeGpio); + for (nr = 1; nr <= ConfigManager->get(KEY_MAX_RELAY)->getValueInt(); nr++) { - addListGPIOLinkBox(pagerelay, INPUT_RELAY_GPIO, S_RELAY, FUNCTION_RELAY, PATH_RELAY_SET, nr); + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt() != FUNCTION_OFF) { + addListMCP23017GPIOLinkBox(webContentBuffer, INPUT_RELAY_GPIO, S_RELAY, FUNCTION_RELAY, PATH_RELAY_SET, nr); + } + else { + addListGPIOLinkBox(webContentBuffer, INPUT_RELAY_GPIO, S_RELAY, FUNCTION_RELAY, PATH_RELAY_SET, nr); + } } - addFormHeaderEnd(pagerelay); - pagerelay += F("
"); - pagerelay += F("
"); - pagerelay += F("

"); - return pagerelay; + addFormHeaderEnd(webContentBuffer); + + addButtonSubmit(webContentBuffer, S_SAVE); + addFormEnd(webContentBuffer); + addButton(webContentBuffer, S_RETURN, PATH_DEVICE_SETTINGS); + + WebServer->sendContent(); } void SuplaWebPageRelay::handleRelaySet() { @@ -108,18 +122,21 @@ void SuplaWebPageRelay::handleRelaySet() { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); } - WebServer->sendContent(supla_webpage_relay_set(0)); + supla_webpage_relay_set(0); } void SuplaWebPageRelay::handleRelaySaveSet() { - // Serial.println(F("HTTP_POST - metoda handleRelaySaveSet")); if (ConfigESP->configModeESP == NORMAL_MODE) { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); } String readUrl, nr_relay, input; - uint8_t place; + uint8_t place, key, gpio; + + input.reserve(9); + readUrl.reserve(11); + nr_relay.reserve(2); String path = PATH_START; path += PATH_SAVE_RELAY_SET; @@ -127,7 +144,15 @@ void SuplaWebPageRelay::handleRelaySaveSet() { place = readUrl.indexOf(path); nr_relay = readUrl.substring(place + path.length(), place + path.length() + 3); - uint8_t key = KEY_GPIO + ConfigESP->getGpio(nr_relay.toInt(), FUNCTION_RELAY); + + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt() != FUNCTION_OFF) { + gpio = ConfigESP->getGpioMCP23017(nr_relay.toInt(), FUNCTION_RELAY); + } + else { + gpio = ConfigESP->getGpio(nr_relay.toInt(), FUNCTION_RELAY); + } + + key = KEY_GPIO + gpio; input = INPUT_RELAY_MEMORY; input += nr_relay; @@ -139,96 +164,46 @@ void SuplaWebPageRelay::handleRelaySaveSet() { switch (ConfigManager->save()) { case E_CONFIG_OK: - // Serial.println(F("E_CONFIG_OK: Dane zapisane")); - WebServer->sendContent(supla_webpage_relay(1)); + supla_webpage_relay(1); break; - case E_CONFIG_FILE_OPEN: - // Serial.println(F("E_CONFIG_FILE_OPEN: Couldn't open file")); - WebServer->sendContent(supla_webpage_relay(2)); + supla_webpage_relay(2); break; } } -String SuplaWebPageRelay::supla_webpage_relay_set(int save) { - String readUrl, nr_relay, key; - uint8_t place, selected, suported; +void SuplaWebPageRelay::supla_webpage_relay_set(int save) { + String path, readUrl, nr_relay; + uint8_t place, selected; - String path = PATH_START; + path.reserve(9); + readUrl.reserve(11); + nr_relay.reserve(2); + + path = PATH_START; path += PATH_RELAY_SET; readUrl = WebServer->httpServer.uri(); place = readUrl.indexOf(path); nr_relay = readUrl.substring(place + path.length(), place + path.length() + 3); - String page = ""; - page += SuplaSaveResult(save); - page += SuplaJavaScript(PATH_RELAY); - uint8_t relays = ConfigManager->get(KEY_MAX_RELAY)->getValueInt(); - if (nr_relay.toInt() <= relays && ConfigESP->getGpio(nr_relay.toInt(), FUNCTION_RELAY) != OFF_GPIO) { - page += F("

"); - page += S_RELAY_NR_SETTINGS; - page += F(" "); - page += nr_relay; - page += F("

"); - page += F(""); - page += F(""); - page += F("
"); - } - else { - page += F("

"); - page += S_NO_RELAY_NR; - page += F(" "); - page += nr_relay; - page += F("

"); - } - page += F("
"); - page += F("

"); - - return page; + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(PATH_RELAY); + + addForm(webContentBuffer, F("post"), PATH_SAVE_RELAY_SET + nr_relay); + addFormHeader(webContentBuffer, S_RELAY_NR_SETTINGS + nr_relay); + + selected = ConfigESP->getLevel(nr_relay.toInt(), FUNCTION_RELAY); + addListBox(webContentBuffer, INPUT_RELAY_LEVEL + nr_relay, S_STATE_CONTROL, LEVEL_P, 2, selected); + + selected = ConfigESP->getMemory(nr_relay.toInt(), FUNCTION_RELAY); + addListBox(webContentBuffer, INPUT_RELAY_MEMORY + nr_relay, S_REACTION_AFTER_RESET, MEMORY_P, 3, selected); + + addFormHeaderEnd(webContentBuffer); + addButtonSubmit(webContentBuffer, S_SAVE); + addFormEnd(webContentBuffer); + addButton(webContentBuffer, S_RETURN, PATH_RELAY); + + WebServer->sendContent(); } #endif diff --git a/src/SuplaWebPageRelay.h b/src/SuplaWebPageRelay.h index 88c71aa5..50b65fc1 100644 --- a/src/SuplaWebPageRelay.h +++ b/src/SuplaWebPageRelay.h @@ -4,23 +4,25 @@ #include "SuplaDeviceGUI.h" #include "SuplaWebServer.h" -#define PATH_RELAY "relay" -#define PATH_SAVE_RELAY "saverelay" -#define PATH_RELAY_SET "setrelay" -#define PATH_SAVE_RELAY_SET "savesetrelay" -#define INPUT_MAX_RELAY "mrl" -#define INPUT_RELAY_GPIO "rlg" -#define INPUT_RELAY_LEVEL "irl" -#define INPUT_RELAY_MEMORY "irm" -#define INPUT_RELAY_DURATION "ird" -#define INPUT_ROLLERSHUTTER "irsr" +#define PATH_RELAY "relay" +#define PATH_SAVE_RELAY "saverelay" +#define PATH_RELAY_SET "setrelay" +#define PATH_SAVE_RELAY_SET "savesetrelay" +#define INPUT_MAX_RELAY "mrl" +#define INPUT_RELAY_GPIO "rlg" +#define INPUT_ADRESS_MCP23017 "iam" +#define INPUT_RELAY_LEVEL "irl" +#define INPUT_RELAY_MEMORY "irm" +#define INPUT_RELAY_DURATION "ird" +#define INPUT_ROLLERSHUTTER "irsr" -enum _memory_relay { +enum _memory_relay +{ MEMORY_RELAY_OFF, MEMORY_RELAY_ON, MEMORY_RELAY_RESTORE }; - + #if defined(SUPLA_RELAY) || defined(SUPLA_ROLLERSHUTTER) class SuplaWebPageRelay { @@ -33,8 +35,8 @@ class SuplaWebPageRelay { void handleRelaySaveSet(); private: - String supla_webpage_relay_set(int save); - String supla_webpage_relay(int save); + void supla_webpage_relay_set(int save); + void supla_webpage_relay(int save); }; extern SuplaWebPageRelay* WebPageRelay; diff --git a/src/SuplaWebPageSensor.cpp b/src/SuplaWebPageSensor.cpp index 7e683f54..3234d52e 100644 --- a/src/SuplaWebPageSensor.cpp +++ b/src/SuplaWebPageSensor.cpp @@ -31,7 +31,7 @@ void SuplaWebPageSensor::createWebPageSensor() { #endif #endif -#if defined(SUPLA_BME280) || defined(SUPLA_SHT30) || defined(SUPLA_SI7021) +#if defined(SUPLA_BME280) || defined(SUPLA_SHT3x) || defined(SUPLA_SI7021) || defined(SUPLA_MCP23017) path = PATH_START; path += PATH_I2C; WebServer->httpServer.on(path, std::bind(&SuplaWebPageSensor::handlei2c, this)); @@ -80,9 +80,9 @@ void SuplaWebPageSensor::createWebPageSensor() { path += PATH_SAVE_HLW8012_CALIBRATE; WebServer->httpServer.on(path, std::bind(&SuplaWebPageSensor::handleHLW8012CalibrateSave, this)); #endif - #endif } + #ifdef SUPLA_DS18B20 void SuplaWebPageSensor::handleSearchDS() { if (ConfigESP->configModeESP == NORMAL_MODE) { @@ -273,8 +273,7 @@ void SuplaWebPageSensor::handle1WireSave() { return WebServer->httpServer.requestAuthentication(); } - String key, input; - uint8_t nr, current_value, last_value; + uint8_t nr, last_value; #ifdef SUPLA_DHT11 last_value = ConfigManager->get(KEY_MAX_DHT11)->getValueInt(); @@ -333,9 +332,8 @@ void SuplaWebPageSensor::handle1WireSave() { } String SuplaWebPageSensor::supla_webpage_1wire(int save) { - uint8_t nr, suported, selected; - uint16_t max; - String page, key; + uint8_t nr, max; + String page; page += SuplaSaveResult(save); page += SuplaJavaScript(PATH_1WIRE); @@ -392,7 +390,7 @@ String SuplaWebPageSensor::supla_webpage_1wire(int save) { } #endif -#if defined(SUPLA_BME280) || defined(SUPLA_SHT30) || defined(SUPLA_SI7021) +#if defined(SUPLA_BME280) || defined(SUPLA_SI7021) || defined(SUPLA_SHT3x) || defined(SUPLA_OLED) || defined(SUPLA_MCP23017) void SuplaWebPageSensor::handlei2c() { if (ConfigESP->configModeESP == NORMAL_MODE) { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) @@ -408,9 +406,8 @@ void SuplaWebPageSensor::handlei2cSave() { } String input; - uint8_t key, nr, current_value, last_value; + uint8_t key; -#if defined(SUPLA_BME280) || defined(SUPLA_SI7021) || defined(SUPLA_SHT3x) if (!WebServer->saveGPIO(INPUT_SDA_GPIO, FUNCTION_SDA)) { WebServer->sendContent(supla_webpage_i2c(6)); return; @@ -419,7 +416,6 @@ void SuplaWebPageSensor::handlei2cSave() { WebServer->sendContent(supla_webpage_i2c(6)); return; } -#endif #ifdef SUPLA_BME280 key = KEY_ACTIVE_SENSOR; @@ -459,6 +455,19 @@ void SuplaWebPageSensor::handlei2cSave() { } #endif +#ifdef SUPLA_MCP23017 + key = KEY_ACTIVE_SENSOR; + input = INPUT_MCP23017; + if (strcmp(WebServer->httpServer.arg(input).c_str(), "") != 0) { + ConfigManager->setElement(KEY_ACTIVE_SENSOR, SENSOR_MCP23017, WebServer->httpServer.arg(input).toInt()); + + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt()) { + ConfigESP->clearFunctionGpio(FUNCTION_RELAY); + ConfigESP->clearFunctionGpio(FUNCTION_BUTTON); + } + } +#endif + switch (ConfigManager->save()) { case E_CONFIG_OK: WebServer->sendContent(supla_webpage_i2c(1)); @@ -471,23 +480,22 @@ void SuplaWebPageSensor::handlei2cSave() { } String SuplaWebPageSensor::supla_webpage_i2c(int save) { - uint8_t nr, suported, selected, size; - String page, key; + uint8_t selected; + String page = ""; page += SuplaSaveResult(save); page += SuplaJavaScript(PATH_I2C); addForm(page, F("post"), PATH_SAVE_I2C); -#if defined(SUPLA_BME280) || defined(SUPLA_SI7021) || defined(SUPLA_SHT3x) || defined(SUPLA_OLED) - addFormHeader(page, String(S_GPIO_SETTINGS_FOR) + " i2c"); - addListGPIOBox(page, INPUT_SDA_GPIO, "SDA", FUNCTION_SDA); - addListGPIOBox(page, INPUT_SCL_GPIO, "SCL", FUNCTION_SCL); + addFormHeader(page, String(S_GPIO_SETTINGS_FOR) + F(" i2c")); + addListGPIOBox(page, INPUT_SDA_GPIO, F("SDA"), FUNCTION_SDA); + addListGPIOBox(page, INPUT_SCL_GPIO, F("SCL"), FUNCTION_SCL); addFormHeaderEnd(page); if (ConfigESP->getGpio(FUNCTION_SDA) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_SCL) != OFF_GPIO) { #ifdef SUPLA_BME280 selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_BME280).toInt(); addFormHeader(page); - addListBox(page, INPUT_BME280, "BME280 adres", BME280_P, 4, selected); + addListBox(page, INPUT_BME280, F("BME280 adres"), BME280_P, 4, selected); addNumberBox(page, INPUT_ALTITUDE_BME280, S_ALTITUDE_ABOVE_SEA_LEVEL, KEY_ALTITUDE_BME280, 1500); addFormHeaderEnd(page); #endif @@ -495,25 +503,31 @@ String SuplaWebPageSensor::supla_webpage_i2c(int save) { #ifdef SUPLA_SHT3x selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_SHT3x).toInt(); addFormHeader(page); - addListBox(page, INPUT_SHT3x, "SHT3x", SHT3x_P, 4, selected); + addListBox(page, INPUT_SHT3x, F("SHT3x"), SHT3x_P, 4, selected); addFormHeaderEnd(page); #endif #ifdef SUPLA_SI7021 selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_SI7021).toInt(); addFormHeader(page); - addListBox(page, INPUT_SI7021, "SI7021", STATE_P, 2, selected); + addListBox(page, INPUT_SI7021, F("SI7021"), STATE_P, 2, selected); addFormHeaderEnd(page); #endif #ifdef SUPLA_OLED selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt(); addFormHeader(page); - addListBox(page, INPUT_OLED, "OLED", OLED_P, 4, selected); + addListBox(page, INPUT_OLED, F("OLED"), OLED_P, 4, selected); addFormHeaderEnd(page); #endif - } + +#ifdef SUPLA_MCP23017 + selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt(); + addFormHeader(page); + addListBox(page, INPUT_MCP23017, F("MCP23017"), STATE_P, 2, selected); + addFormHeaderEnd(page); #endif + } addButtonSubmit(page, S_SAVE); addFormEnd(page); @@ -539,8 +553,8 @@ void SuplaWebPageSensor::handleSpiSave() { return WebServer->httpServer.requestAuthentication(); } - String key, input; - uint8_t nr, current_value, last_value; + String input; + uint8_t key; #if defined(SUPLA_MAX6675) if (!WebServer->saveGPIO(INPUT_CLK_GPIO, FUNCTION_CLK)) { @@ -578,7 +592,7 @@ void SuplaWebPageSensor::handleSpiSave() { String SuplaWebPageSensor::supla_webpage_spi(int save) { uint8_t nr, suported, selected; - String page, key; + String page; page += SuplaSaveResult(save); page += SuplaJavaScript(PATH_SPI); page += F("
"); } -void SuplaWebServer::sendContent(const String content) { +void SuplaWebServer::sendContent(const String& content) { // httpServer.send(200, "text/html", ""); - const int bufferSize = 1000; - String _buffer; - int bufferCounter = 0; + // const int bufferSize = 1000; + // String _buffer; + //_buffer.reserve(bufferSize); + // int bufferCounter = 0; + int fileSize = content.length(); #ifdef DEBUG_MODE - Serial.print("Content size: "); + Serial.print(F("Content size: ")); Serial.println(fileSize); + checkRAM(); #endif - httpServer.setContentLength(fileSize); + httpServer.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + httpServer.sendHeader("Pragma", "no-cache"); + httpServer.sendHeader("Expires", "-1"); + httpServer.setContentLength(CONTENT_LENGTH_UNKNOWN); httpServer.chunkedResponseModeStart(200, "text/html"); httpServer.sendContent_P(HTTP_META); + httpServer.sendContent_P(HTTP_FAVICON); httpServer.sendContent_P(HTTP_STYLE); httpServer.sendContent_P(HTTP_LOGO); @@ -317,7 +321,7 @@ void SuplaWebServer::sendContent(const String content) { httpServer.sendContent_P(HTTP_COPYRIGHT); // httpServer.send(200, "text/html", ""); - for (int i = 0; i < fileSize; i++) { + /*for (int i = 0; i < fileSize; i++) { _buffer += content[i]; bufferCounter++; @@ -333,9 +337,72 @@ void SuplaWebServer::sendContent(const String content) { yield(); bufferCounter = 0; _buffer = ""; + }*/ + + httpServer.sendContent(content); + httpServer.sendContent_P(HTTP_RBT); + httpServer.chunkedResponseFinalize(); +} + +String webContentBuffer; + +void SuplaWebServer::sendContent() { + // httpServer.send(200, "text/html", ""); + // const int bufferSize = 1000; + // String _buffer; + //_buffer.reserve(bufferSize); + // int bufferCounter = 0; + + httpServer.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + httpServer.sendHeader("Pragma", "no-cache"); + httpServer.sendHeader("Expires", "-1"); + httpServer.setContentLength(CONTENT_LENGTH_UNKNOWN); + httpServer.chunkedResponseModeStart(200, "text/html"); + + httpServer.sendContent_P(HTTP_META); + httpServer.sendContent_P(HTTP_FAVICON); + httpServer.sendContent_P(HTTP_STYLE); + httpServer.sendContent_P(HTTP_LOGO); + + String summary = FPSTR(HTTP_SUMMARY); + + summary.replace("{h}", ConfigManager->get(KEY_HOST_NAME)->getValue()); + summary.replace("{s}", ConfigESP->getLastStatusSupla()); + summary.replace("{v}", Supla::Channel::reg_dev.SoftVer); + summary.replace("{g}", ConfigManager->get(KEY_SUPLA_GUID)->getValueHex(SUPLA_GUID_SIZE)); + summary.replace("{m}", ConfigESP->getMacAddress(true)); + httpServer.sendContent(summary); + httpServer.sendContent_P(HTTP_COPYRIGHT); + + // httpServer.send(200, "text/html", ""); + /*for (int i = 0; i < fileSize; i++) { + _buffer += content[i]; + bufferCounter++; + + if (bufferCounter >= bufferSize) { + httpServer.sendContent(_buffer); + yield(); + bufferCounter = 0; + _buffer = ""; + } } + if (bufferCounter > 0) { + httpServer.sendContent(_buffer); + yield(); + bufferCounter = 0; + _buffer = ""; + }*/ + + httpServer.sendContent(webContentBuffer); httpServer.sendContent_P(HTTP_RBT); httpServer.chunkedResponseFinalize(); + +#ifdef DEBUG_MODE + Serial.printf_P(PSTR("Content size=%d\n"), webContentBuffer.length()); + Serial.printf_P(PSTR("Sent INDEX...Free mem=%d\n"), ESP.getFreeHeap()); + checkRAM(); +#endif + webContentBuffer = ""; } void SuplaWebServer::handleNotFound() { @@ -347,6 +414,7 @@ bool SuplaWebServer::saveGPIO(const String& _input, uint8_t function, uint8_t nr uint8_t current_value, key; String input; input = _input; + if (nr != 0) { input += nr; } @@ -354,6 +422,10 @@ bool SuplaWebServer::saveGPIO(const String& _input, uint8_t function, uint8_t nr nr = 1; } + if (strcmp(WebServer->httpServer.arg(input).c_str(), "") == 0) { + return true; + } + key = KEY_GPIO + WebServer->httpServer.arg(input).toInt(); if (ConfigESP->getGpio(nr, function) != WebServer->httpServer.arg(input).toInt() || WebServer->httpServer.arg(input).toInt() == OFF_GPIO) { @@ -383,4 +455,32 @@ bool SuplaWebServer::saveGPIO(const String& _input, uint8_t function, uint8_t nr } } return true; +} + +bool SuplaWebServer::saveGpioMCP23017(const String& input, uint8_t function, uint8_t nr, const String& input_max) { + uint8_t key, address, addressInput, gpio, gpioInput, functionElementInput; + String _input = input + nr; + + if (strcmp(WebServer->httpServer.arg(_input).c_str(), "") == 0) { + return true; + } + + addressInput = WebServer->httpServer.arg(INPUT_ADRESS_MCP23017).toInt(); + functionElementInput = ConfigManager->get(key)->getElement(ConfigESP->getFunctionMCP23017(addressInput)).toInt(); + gpioInput = WebServer->httpServer.arg(_input).toInt(); + + key = KEY_GPIO + gpioInput; + gpio = ConfigESP->getGpioMCP23017(nr, function); + address = ConfigESP->getAdressMCP23017(function); + + if (functionElementInput == FUNCTION_OFF) { + ConfigESP->setGpioMCP23017(gpioInput, addressInput, nr, function, 1, 0); + } + else if (gpio == gpioInput && functionElementInput == function) { + ConfigESP->setGpioMCP23017(gpioInput, addressInput, nr, function, ConfigESP->getLevel(nr, function), ConfigESP->getMemory(nr, function)); + } + else { + return false; + } + return true; } \ No newline at end of file diff --git a/src/SuplaWebServer.h b/src/SuplaWebServer.h index 51a4219e..333c7fc4 100644 --- a/src/SuplaWebServer.h +++ b/src/SuplaWebServer.h @@ -53,6 +53,25 @@ #define INPUT_ROLLERSHUTTER "irsr" #define INPUT_BOARD "board" + +extern String webContentBuffer; + +//https://www.esp8266.com/viewtopic.php?p=84249#p84249 +class MyWebServer : public ESP8266WebServer { + public: + virtual ~MyWebServer() { + if (this->_currentArgs) { + delete[] this->_currentArgs; + this->_currentArgs = nullptr; + } + + if (this->_postArgs) { + delete[] this->_postArgs; + this->_postArgs = nullptr; + } + } +}; + class SuplaWebServer : public Supla::Element { public: SuplaWebServer(); @@ -61,19 +80,20 @@ class SuplaWebServer : public Supla::Element { char www_username[MAX_MLOGIN]; char www_password[MAX_MPASSWORD]; - const String SuplaFavicon(); - const String SuplaIconEdit(); + const String& SuplaIconEdit(); String supla_webpage_start(int save); - void sendContent(const String content); + void sendContent(const String& content); + void sendContent(); + + MyWebServer httpServer; - ESP8266WebServer httpServer = {80}; - #ifdef SUPLA_OTA - ESP8266HTTPUpdateServer httpUpdater; + ESP8266HTTPUpdateServer httpUpdater; #endif bool saveGPIO(const String& input, uint8_t function, uint8_t nr = 0, const String& input_max = "\n"); + bool saveGpioMCP23017(const String& input, uint8_t function, uint8_t nr = 0, const String& input_max = "\n"); private: void iterateAlways(); @@ -92,5 +112,4 @@ class SuplaWebServer : public Supla::Element { void handleNotFound(); }; - #endif // SuplaWebServer_h From 5996f7308759a08de3cb3dcadfbf7e9abc6c7409 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Fri, 15 Jan 2021 13:26:32 +0100 Subject: [PATCH 034/165] =?UTF-8?q?nowy=20spos=C3=B3b=20pobierania=20warto?= =?UTF-8?q?=C5=9Bci=20dla=20OLEDa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/GUI-Generic.ino | 19 ++-- src/SuplaDeviceGUI.cpp | 20 ---- src/SuplaDeviceGUI.h | 20 ---- src/SuplaOled.cpp | 212 ++++++++++++++++++++++++----------------- src/SuplaOled.h | 33 ++++--- 5 files changed, 147 insertions(+), 157 deletions(-) diff --git a/src/GUI-Generic.ino b/src/GUI-Generic.ino index 10f3ae50..fbd59496 100644 --- a/src/GUI-Generic.ino +++ b/src/GUI-Generic.ino @@ -90,7 +90,7 @@ void setup() { #ifdef SUPLA_DHT22 for (nr = 1; nr <= ConfigManager->get(KEY_MAX_DHT22)->getValueInt(); nr++) { if (ConfigESP->getGpio(nr, FUNCTION_DHT22) != OFF_GPIO) { - Supla::GUI::sensorDHT22.push_back(new Supla::Sensor::DHT(ConfigESP->getGpio(nr, FUNCTION_DHT22), DHT22)); + new Supla::Sensor::DHT(ConfigESP->getGpio(nr, FUNCTION_DHT22), DHT22); } } #endif @@ -103,7 +103,7 @@ void setup() { #ifdef SUPLA_SI7021_SONOFF if (ConfigESP->getGpio(FUNCTION_SI7021_SONOFF) != OFF_GPIO) { - Supla::GUI::sensorSi7021Sonoff.push_back(new Supla::Sensor::Si7021Sonoff(ConfigESP->getGpio(FUNCTION_SI7021_SONOFF))); + new Supla::Sensor::Si7021Sonoff(ConfigESP->getGpio(FUNCTION_SI7021_SONOFF)); } #endif @@ -121,14 +121,14 @@ void setup() { switch (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_BME280).toInt()) { case BME280_ADDRESS_0X76: - Supla::GUI::sensorBme280.push_back(new Supla::Sensor::BME280(0x76, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt())); + new Supla::Sensor::BME280(0x76, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt()); break; case BME280_ADDRESS_0X77: - Supla::GUI::sensorBme280.push_back(new Supla::Sensor::BME280(0x77, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt())); + new Supla::Sensor::BME280(0x77, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt()); break; case BME280_ADDRESS_0X76_AND_0X77: - Supla::GUI::sensorBme280.push_back(new Supla::Sensor::BME280(0x76, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt())); - Supla::GUI::sensorBme280.push_back(new Supla::Sensor::BME280(0x77, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt())); + new Supla::Sensor::BME280(0x76, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt()); + new Supla::Sensor::BME280(0x77, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt()); break; } #endif @@ -155,8 +155,8 @@ void setup() { #endif #ifdef SUPLA_OLED if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt()) { - Supla::GUI::oled = new SuplaOled(); - Supla::GUI::oled->addButtonOled(ConfigESP->getGpio(FUNCTION_CFG_BUTTON)); + SuplaOled *oled = new SuplaOled(); + oled->addButtonOled(ConfigESP->getGpio(FUNCTION_CFG_BUTTON)); } #endif @@ -175,8 +175,7 @@ void setup() { #ifdef SUPLA_MAX6675 if (ConfigESP->getGpio(FUNCTION_CLK) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_CS) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_D0) != OFF_GPIO) { - Supla::GUI::sensorMAX6675_K.push_back( - new Supla::Sensor::MAX6675_K(ConfigESP->getGpio(FUNCTION_CLK), ConfigESP->getGpio(FUNCTION_CS), ConfigESP->getGpio(FUNCTION_D0))); + new Supla::Sensor::MAX6675_K(ConfigESP->getGpio(FUNCTION_CLK), ConfigESP->getGpio(FUNCTION_CS), ConfigESP->getGpio(FUNCTION_D0)); } #endif diff --git a/src/SuplaDeviceGUI.cpp b/src/SuplaDeviceGUI.cpp index 3bc9c2ff..9f1cf5c1 100644 --- a/src/SuplaDeviceGUI.cpp +++ b/src/SuplaDeviceGUI.cpp @@ -164,26 +164,6 @@ std::vector button; std::vector sensorDS; #endif -#ifdef SUPLA_BME280 -std::vector sensorBme280; -#endif - -#ifdef SUPLA_SI7021_SONOFF -std::vector sensorSi7021Sonoff; -#endif - -#ifdef SUPLA_DHT22 -std::vector sensorDHT22; -#endif - -#ifdef SUPLA_MAX6675 -std::vector sensorMAX6675_K; -#endif - -#ifdef SUPLA_OLED -SuplaOled *oled; -#endif - #ifdef SUPLA_HLW8012 Supla::Sensor::HJ101 *counterHLW8012; diff --git a/src/SuplaDeviceGUI.h b/src/SuplaDeviceGUI.h index 59f0d387..02399296 100644 --- a/src/SuplaDeviceGUI.h +++ b/src/SuplaDeviceGUI.h @@ -114,26 +114,6 @@ extern std::vector impulseCounter; void addImpulseCounter(int pin, bool lowToHigh, bool inputPullup, unsigned int debounceDelay); #endif -#ifdef SUPLA_BME280 -extern std::vector sensorBme280; -#endif - -#ifdef SUPLA_SI7021_SONOFF -extern std::vector sensorSi7021Sonoff; -#endif - -#ifdef SUPLA_DHT22 -extern std::vector sensorDHT22; -#endif - -#ifdef SUPLA_MAX6675 -extern std::vector sensorMAX6675_K; -#endif - -#ifdef SUPLA_OLED -extern SuplaOled *oled; -#endif - #ifdef SUPLA_HLW8012 extern Supla::Sensor::HJ101 *counterHLW8012; void addHLW8012(int8_t pinCF, int8_t pinCF1, int8_t pinSEL); diff --git a/src/SuplaOled.cpp b/src/SuplaOled.cpp index 5f792999..842d200e 100644 --- a/src/SuplaOled.cpp +++ b/src/SuplaOled.cpp @@ -2,7 +2,9 @@ #include "SuplaDeviceGUI.h" #ifdef SUPLA_OLED + uint8_t* framesCountSensor; +uint8_t* chanelSensor; String getTempString(double temperature) { if (temperature == -275) { @@ -49,7 +51,7 @@ int32_t readRssi(void) { return (2 * (rssi + 100)); } -void displaySignal(OLEDDisplay* display) { +void displayUiSignal(OLEDDisplay* display) { int x = display->getWidth() - 17; int y = 0; int value = readRssi(); @@ -84,7 +86,7 @@ void displaySignal(OLEDDisplay* display) { } } -void displayRelayState(OLEDDisplay* display) { +void displayUiRelayState(OLEDDisplay* display) { int y = 0; int x = 0; @@ -108,18 +110,18 @@ void displayRelayState(OLEDDisplay* display) { } void msOverlay(OLEDDisplay* display, OLEDDisplayUiState* state) { - displaySignal(display); + displayUiSignal(display); if (Supla::GUI::relay.size()) { - displayRelayState(display); + displayUiRelayState(display); } } -void displaySuplaStatus(OLEDDisplay* display) { +void displayUiSuplaStatus(OLEDDisplay* display) { int x = 0; int y = display->getHeight() / 3; display->clear(); - displaySignal(display); + displayUiSignal(display); display->setFont(ArialMT_Plain_10); display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -140,14 +142,14 @@ void displayConfigMode(OLEDDisplay* display) { display->display(); } -void displayBlank(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { +void displayUiBlank(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { // display->drawXbm(10, 17, supla_logo_width, supla_logo_height, supla_logo_bits); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(ArialMT_Plain_16); display->drawString(10, display->getHeight() / 2, F("SUPLA")); } -void displayTemp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double temp, const String& name) { +void displayUiTemperature(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double temp, const String& name) { uint8_t temp_width, temp_height; int drawHeightIcon = display->getHeight() / 2 - 10; @@ -177,7 +179,7 @@ void displayTemp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int display->drawString(x + temp_width + (getTempString(temp).length() * 12), y + drawStringIcon, "ºC"); } -void displaHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double humidity) { +void displaUiHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double humidity) { uint8_t humidity_width, humidity_height; int drawHeightIcon = display->getHeight() / 2 - 10; @@ -202,7 +204,7 @@ void displaHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, display->drawString(x + humidity_width + (getHumidityString(humidity).length() * 12), y + drawStringIcon, "%"); } -void displayPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double pressure) { +void displayUiPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double pressure) { uint8_t pressure_width, pressure_height; int drawHeightIcon = display->getHeight() / 2 - 10; @@ -227,41 +229,58 @@ void displayPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, display->drawString(x + pressure_width + (getPressureString(pressure).length() * 14), y + drawStringIcon, "hPa"); } -void displayDs18b20(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - displayTemp(display, state, x, y, Supla::GUI::sensorDS[getFramesCountSensor(state)]->getValue(), - String(ConfigManager->get(KEY_DS_NAME + getFramesCountSensor(state))->getValue())); -} - -void displayBme280Temp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - displayTemp(display, state, x, y, Supla::GUI::sensorBme280[getFramesCountSensor(state)]->getTemp()); -} - -void displayBme280Humidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - displaHumidity(display, state, x, y, Supla::GUI::sensorBme280[getFramesCountSensor(state)]->getHumi()); -} - -void displayBme280Pressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - displayPressure(display, state, x, y, Supla::GUI::sensorBme280[getFramesCountSensor(state)]->getPressure()); -} - -void displaySi7021SonoffTemp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - displayTemp(display, state, x, y, Supla::GUI::sensorSi7021Sonoff[getFramesCountSensor(state)]->getTemp()); -} - -void displaySi7021SonoffHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - displaHumidity(display, state, x, y, Supla::GUI::sensorSi7021Sonoff[getFramesCountSensor(state)]->getHumi()); +void displayTemperature(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + for (auto element = Supla::Element::begin(); element != nullptr; element = element->next()) { + if (element->getChannel()) { + auto channel = element->getChannel(); + if (channel->getChannelNumber() == chanelSensor[state->currentFrame]) { + double lastTemperature = channel->getValueDouble(); + + if (ConfigManager->get(KEY_MULTI_MAX_DS18B20)->getValueInt() >= state->currentFrame) { + displayUiTemperature(display, state, x, y, lastTemperature, ConfigManager->get(KEY_DS_NAME + state->currentFrame)->getValue()); + } + else { + displayUiTemperature(display, state, x, y, lastTemperature); + } + } + } + } } -void displayDHT22Temp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - displayTemp(display, state, x, y, Supla::GUI::sensorDHT22[getFramesCountSensor(state)]->getTemp()); +void displayDoubleTemperature(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + for (auto element = Supla::Element::begin(); element != nullptr; element = element->next()) { + if (element->getChannel()) { + auto channel = element->getChannel(); + if (channel->getChannelNumber() == chanelSensor[state->currentFrame]) { + double lastTemperature = channel->getValueDoubleFirst(); + displayUiTemperature(display, state, x, y, lastTemperature); + } + } + } } -void displayDHT22Humidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - displaHumidity(display, state, x, y, Supla::GUI::sensorDHT22[getFramesCountSensor(state)]->getHumi()); +void displayDoubleHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + for (auto element = Supla::Element::begin(); element != nullptr; element = element->next()) { + if (element->getChannel()) { + auto channel = element->getChannel(); + if (channel->getChannelNumber() == chanelSensor[state->currentFrame]) { + double lastHumidit = channel->getValueDoubleSecond(); + displaUiHumidity(display, state, x, y, lastHumidit); + } + } + } } -void displayMAX6675Temp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { - displayTemp(display, state, x, y, Supla::GUI::sensorMAX6675_K[getFramesCountSensor(state)]->getValue()); +void displayPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + for (auto element = Supla::Element::begin(); element != nullptr; element = element->next()) { + if (element->getSecondaryChannel()) { + auto channel = element->getSecondaryChannel(); + if (channel->getChannelNumber() == chanelSensor[state->currentFrame]) { + double lastPressure = channel->getValueDouble(); + displayUiPressure(display, state, x, y, lastPressure); + } + } + } } SuplaOled::SuplaOled() { @@ -281,60 +300,43 @@ SuplaOled::SuplaOled() { ui = new OLEDDisplayUi(display); overlays[0] = {msOverlay}; - - int maxFrame = Supla::GUI::sensorDS.size() + (Supla::GUI::sensorBme280.size() * 3) + Supla::GUI::sensorSi7021Sonoff.size() + - (Supla::GUI::sensorDHT22.size() * 2) + Supla::GUI::sensorMAX6675_K.size(); - - if (maxFrame == 0) - maxFrame = 1; + int maxFrame = getMaxFrame(); frames = (FrameCallback*)malloc(sizeof(FrameCallback) * maxFrame); - framesCountSensor = (uint8_t*)malloc(sizeof(uint8_t) * maxFrame); - - for (int i = 0; i < Supla::GUI::sensorDS.size(); i++) { - frames[frameCount] = {displayDs18b20}; - framesCountSensor[frameCount] = i; - frameCount += 1; - } - - for (int i = 0; i < Supla::GUI::sensorBme280.size(); i++) { - frames[frameCount] = {displayBme280Temp}; - framesCountSensor[frameCount] = i; - frameCount += 1; - frames[frameCount] = {displayBme280Humidity}; - framesCountSensor[frameCount] = i; - frameCount += 1; - frames[frameCount] = {displayBme280Pressure}; - framesCountSensor[frameCount] = i; - frameCount += 1; - } - - for (int i = 0; i < Supla::GUI::sensorSi7021Sonoff.size(); i++) { - frames[frameCount] = {displaySi7021SonoffTemp}; - framesCountSensor[frameCount] = i; - frameCount += 1; - frames[frameCount] = {displaySi7021SonoffHumidity}; - framesCountSensor[frameCount] = i; - frameCount += 1; - } - - for (int i = 0; i < Supla::GUI::sensorDHT22.size(); i++) { - frames[frameCount] = {displayDHT22Temp}; - framesCountSensor[frameCount] = i; - frameCount += 1; - frames[frameCount] = {displayDHT22Humidity}; - framesCountSensor[frameCount] = i; - frameCount += 1; - } - - for (int i = 0; i < Supla::GUI::sensorMAX6675_K.size(); i++) { - frames[frameCount] = {displayMAX6675Temp}; - framesCountSensor[frameCount] = i; - frameCount += 1; + chanelSensor = (uint8_t*)malloc(sizeof(uint8_t) * maxFrame); + + for (auto element = Supla::Element::begin(); element != nullptr; element = element->next()) { + if (element->getChannel()) { + auto channel = element->getChannel(); + Serial.println(channel->getChannelType()); + + if (channel->getChannelType() == SUPLA_CHANNELTYPE_THERMOMETER) { + frames[frameCount] = {displayTemperature}; + chanelSensor[frameCount] = channel->getChannelNumber(); + frameCount += 1; + } + + if (channel->getChannelType() == SUPLA_CHANNELTYPE_HUMIDITYANDTEMPSENSOR) { + frames[frameCount] = {displayDoubleTemperature}; + chanelSensor[frameCount] = channel->getChannelNumber(); + frameCount += 1; + frames[frameCount] = {displayDoubleHumidity}; + chanelSensor[frameCount] = channel->getChannelNumber(); + frameCount += 1; + } + } + if (element->getSecondaryChannel()) { + auto channel = element->getSecondaryChannel(); + if (channel->getChannelType() == SUPLA_CHANNELTYPE_PRESSURESENSOR) { + frames[frameCount] = {displayPressure}; + chanelSensor[frameCount] = channel->getChannelNumber(); + frameCount += 1; + } + } } if (frameCount == 0) { - frames[frameCount] = {displayBlank}; + frames[frameCount] = {displayUiBlank}; frameCount += 1; } @@ -359,7 +361,7 @@ SuplaOled::SuplaOled() { void SuplaOled::iterateAlways() { if (ConfigESP->getGpio(FUNCTION_SDA) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_SCL) != OFF_GPIO) { if (ConfigESP->supla_status.status != STATUS_REGISTERED_AND_READY) { - displaySuplaStatus(display); + displayUiSuplaStatus(display); return; } @@ -384,7 +386,7 @@ void SuplaOled::iterateAlways() { void SuplaOled::addButtonOled(int pin) { if (pin != OFF_GPIO) { Supla::Control::Button* button = new Supla::Control::Button(pin, true, true); - button->addAction(TURN_ON_OLED, Supla::GUI::oled, Supla::ON_PRESS); + button->addAction(TURN_ON_OLED, this, Supla::ON_PRESS); } } @@ -395,4 +397,34 @@ void SuplaOled::runAction(int event, int action) { oledON = true; } } + +int SuplaOled::getMaxFrame() { + int maxFrame = 0; + for (auto element = Supla::Element::begin(); element != nullptr; element = element->next()) { + if (element->getChannel()) { + auto channel = element->getChannel(); + Serial.println(channel->getChannelType()); + + if (channel->getChannelType() == SUPLA_CHANNELTYPE_THERMOMETER) { + maxFrame += 1; + } + + if (channel->getChannelType() == SUPLA_CHANNELTYPE_HUMIDITYANDTEMPSENSOR) { + maxFrame += 2; + } + } + + if (element->getSecondaryChannel()) { + auto channel = element->getSecondaryChannel(); + if (channel->getChannelType() == SUPLA_CHANNELTYPE_PRESSURESENSOR) { + maxFrame += 1; + } + } + } + + if (maxFrame == 0) + maxFrame = 1; + + return maxFrame; +} #endif \ No newline at end of file diff --git a/src/SuplaOled.h b/src/SuplaOled.h index dc3b6c4d..2f9987e6 100644 --- a/src/SuplaOled.h +++ b/src/SuplaOled.h @@ -29,24 +29,22 @@ String getHumidityString(double humidity); String getPressureString(double pressure); uint8_t getFramesCountSensor(OLEDDisplayUiState* state); int32_t readRssi(void); -void displaySignal(OLEDDisplay* display); -void displayRelayState(OLEDDisplay* display); + void msOverlay(OLEDDisplay* display, OLEDDisplayUiState* state); -void displaySuplaStatus(OLEDDisplay* display); -void displayConfigMode(OLEDDisplay* display); -void displayBlank(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void displayTemp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double temp, const String& name = "\n"); -void displaHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double humidity); -void displayPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double pressure); -void displayDs18b20(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void displayBme280Temp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void displayBme280Humidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void displayBme280Pressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void displaySi7021SonoffTemp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void displaySi7021SonoffHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void displayDHT22Temp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void displayDHT22Humidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); -void displayMAX6675Temp(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); + +void displayUiSignal(OLEDDisplay* display); +void displayUiRelayState(OLEDDisplay* display); +void displayUiSuplaStatus(OLEDDisplay* display); +void displayUiConfigMode(OLEDDisplay* display); +void displayUiBlank(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); +void displayUiTemperature(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double temp, const String& name = "\n"); +void displaUiHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double humidity); +void displayUiPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double pressure); + +void displayTemperature(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); +void displayDoubleTemperature(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); +void displayDoubleHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); +void displayPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); class SuplaOled : public Supla::Triggerable, public Supla::Element { public: @@ -56,6 +54,7 @@ class SuplaOled : public Supla::Triggerable, public Supla::Element { private: void iterateAlways(); void runAction(int event, int action); + int getMaxFrame(); OLEDDisplay* display; OLEDDisplayUi* ui; From 513f90c7f026aadc8820e918d29497871c81baea Mon Sep 17 00:00:00 2001 From: krycha88 Date: Sat, 16 Jan 2021 10:20:24 +0100 Subject: [PATCH 035/165] =?UTF-8?q?mo=C5=BCliwo=C5=9B=C4=87=20okre=C5=9Ble?= =?UTF-8?q?nia=20nazwy=20dla=20ka=C5=BCdego=20ekranu=20dla=20OLEDa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- platformio.ini | 6 +- src/GUI-Generic.ino | 17 +++--- src/GUIGenericCommon.cpp | 38 ++++++++++++ src/GUIGenericCommon.h | 5 ++ src/Markup.cpp | 22 ++++++- src/Markup.h | 16 ++++++ src/SuplaConfigESP.cpp | 4 +- src/SuplaConfigManager.cpp | 47 +++++++++------ src/SuplaConfigManager.h | 14 +++-- src/SuplaDeviceGUI.cpp | 6 +- src/SuplaOled.cpp | 115 +++++++++++++------------------------ src/SuplaOled.h | 10 +--- src/SuplaWebPageSensor.cpp | 56 ++++++++++++------ src/SuplaWebPageSensor.h | 3 + 14 files changed, 219 insertions(+), 140 deletions(-) create mode 100644 src/GUIGenericCommon.cpp diff --git a/platformio.ini b/platformio.ini index 9f97ff4b..ffe3d704 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = [common] -build_flags = -D BUILD_VERSION='"GUI 1.1.8"' +build_flags = -D BUILD_VERSION='"GUI 1.1.10"' -w -DATOMIC_FS_UPDATE -DBEARSSL_SSL_BASIC @@ -41,7 +41,7 @@ build_flags = -D BUILD_VERSION='"GUI 1.1.8"' -D SUPLA_SI7021 -D SUPLA_MAX6675 -D SUPLA_HC_SR04 - -D SUPLA_IMPULSE_COUNTER + ;-D SUPLA_IMPULSE_COUNTER -D SUPLA_OLED -D SUPLA_HLW8012 -D SUPLA_MCP23017 @@ -126,7 +126,7 @@ board = nodemcuv2 board_build.ldscript = eagle.flash.4m1m.ld build_flags = ${common.build_flags} -D DEBUG_MODE -;build_unflags = -D SUPLA_IMPULSE_COUNTER +build_unflags = -D SUPLA_IMPULSE_COUNTER [env:GUI_Generic_blank] board = nodemcuv2 diff --git a/src/GUI-Generic.ino b/src/GUI-Generic.ino index fbd59496..1d47261d 100644 --- a/src/GUI-Generic.ino +++ b/src/GUI-Generic.ino @@ -79,6 +79,12 @@ void setup() { ConfigManager->get(KEY_CFG_MODE)->getValueInt(), ConfigESP->getLevel(FUNCTION_CFG_LED)); #endif +#ifdef SUPLA_DS18B20 + if (ConfigESP->getGpio(FUNCTION_DS18B20) != OFF_GPIO) { + Supla::GUI::addDS18B20MultiThermometer(ConfigESP->getGpio(FUNCTION_DS18B20)); + } +#endif + #ifdef SUPLA_DHT11 for (nr = 1; nr <= ConfigManager->get(KEY_MAX_DHT11)->getValueInt(); nr++) { if (ConfigESP->getGpio(nr, FUNCTION_DHT11) != OFF_GPIO) { @@ -95,12 +101,6 @@ void setup() { } #endif -#ifdef SUPLA_DS18B20 - if (ConfigESP->getGpio(FUNCTION_DS18B20) != OFF_GPIO) { - Supla::GUI::addDS18B20MultiThermometer(ConfigESP->getGpio(FUNCTION_DS18B20)); - } -#endif - #ifdef SUPLA_SI7021_SONOFF if (ConfigESP->getGpio(FUNCTION_SI7021_SONOFF) != OFF_GPIO) { new Supla::Sensor::Si7021Sonoff(ConfigESP->getGpio(FUNCTION_SI7021_SONOFF)); @@ -114,11 +114,11 @@ void setup() { #endif #if defined(SUPLA_BME280) || defined(SUPLA_SI7021) || defined(SUPLA_SHT3x) || defined(SUPLA_HTU21D) || defined(SUPLA_SHT71) || \ - defined(SUPLA_BH1750) || defined(SUPLA_MAX44009) || defined(SUPLA_MCP23017) + defined(SUPLA_BH1750) || defined(SUPLA_MAX44009) || defined(SUPLA_OLED) || defined(SUPLA_MCP23017) if (ConfigESP->getGpio(FUNCTION_SDA) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_SCL) != OFF_GPIO) { Wire.begin(ConfigESP->getGpio(FUNCTION_SDA), ConfigESP->getGpio(FUNCTION_SCL)); -#ifdef SUPLA_BME280 +#ifdef SUPLA_BME280 switch (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_BME280).toInt()) { case BME280_ADDRESS_0X76: new Supla::Sensor::BME280(0x76, ConfigManager->get(KEY_ALTITUDE_BME280)->getValueInt()); @@ -153,6 +153,7 @@ void setup() { new Supla::Sensor::Si7021(); } #endif + #ifdef SUPLA_OLED if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt()) { SuplaOled *oled = new SuplaOled(); diff --git a/src/GUIGenericCommon.cpp b/src/GUIGenericCommon.cpp new file mode 100644 index 00000000..86e92967 --- /dev/null +++ b/src/GUIGenericCommon.cpp @@ -0,0 +1,38 @@ +#include "GUIGenericCommon.h" +#include "SuplaDeviceGUI.h" + +uint8_t *HexToBytes(String _value) { + int size = 16; + uint8_t *buffer = (uint8_t *)malloc(sizeof(uint8_t) * (size)); + + for (int i = 0; i < size; i++) { + sscanf(&_value[i * 2], "%2hhx", &buffer[i]); + } + return buffer; +} + +int getNumberChannels() { + int maxFrame = 0; + for (auto element = Supla::Element::begin(); element != nullptr; element = element->next()) { + if (element->getChannel()) { + auto channel = element->getChannel(); + + if (channel->getChannelType() == SUPLA_CHANNELTYPE_THERMOMETER) { + maxFrame += 1; + } + + if (channel->getChannelType() == SUPLA_CHANNELTYPE_HUMIDITYANDTEMPSENSOR) { + maxFrame += 2; + } + } + + if (element->getSecondaryChannel()) { + auto channel = element->getSecondaryChannel(); + if (channel->getChannelType() == SUPLA_CHANNELTYPE_PRESSURESENSOR) { + maxFrame += 1; + } + } + } + + return maxFrame; +} \ No newline at end of file diff --git a/src/GUIGenericCommon.h b/src/GUIGenericCommon.h index 3ed890bc..7fcd07c8 100644 --- a/src/GUIGenericCommon.h +++ b/src/GUIGenericCommon.h @@ -10,4 +10,9 @@ #include INCLUDE_FILE(UI_LANGUAGE) #endif +#include "SuplaDeviceGUI.h" + +uint8_t *HexToBytes(String _value); +int getNumberChannels(); + #endif // GUI_GENERIC_COMMON_H diff --git a/src/Markup.cpp b/src/Markup.cpp index fc52249e..0ba76b4a 100644 --- a/src/Markup.cpp +++ b/src/Markup.cpp @@ -25,7 +25,7 @@ void addFormHeaderEnd(String& html) { void addTextBox(String& html, const String& input_id, const String& name, - uint8_t value_key, + const String& value, const String& placeholder, int minlength, int maxlength, @@ -46,7 +46,6 @@ void addTextBox(String& html, } html += F("' value='"); - String value = String(ConfigManager->get(value_key)->getValue()); if (value != placeholder) { html += value; } @@ -73,11 +72,30 @@ void addTextBox(String& html, html += F(" "); } +void addTextBox(String& html, + const String& input_id, + const String& name, + uint8_t value_key, + const String& placeholder, + int minlength, + int maxlength, + bool required, + bool readonly, + bool password) { + String value = String(ConfigManager->get(value_key)->getValue()); + return addTextBox(html, input_id, name, value, "", minlength, maxlength, required, readonly, password); +} + void addTextBox( String& html, const String& input_id, const String& name, uint8_t value_key, int minlength, int maxlength, bool required, bool readonly) { return addTextBox(html, input_id, name, value_key, "", minlength, maxlength, required, readonly, false); } +void addTextBox( + String& html, const String& input_id, const String& name, const String& value, int minlength, int maxlength, bool required, bool readonly) { + return addTextBox(html, input_id, name, value, "", minlength, maxlength, required, readonly, false); +} + void addTextBoxPassword(String& html, const String& input_id, const String& name, uint8_t value_key, int minlength, int maxlength, bool required) { return addTextBox(html, input_id, name, value_key, "", minlength, maxlength, required, false, true); } diff --git a/src/Markup.h b/src/Markup.h index 582d59fa..b06c2556 100644 --- a/src/Markup.h +++ b/src/Markup.h @@ -9,6 +9,17 @@ void addFormEnd(String& html); void addFormHeader(String& html, const String& name = "\n"); void addFormHeaderEnd(String& html); +void addTextBox(String& html, + const String& input_id, + const String& name, + const String& value, + const String& placeholder, + int minlength, + int maxlength, + bool required, + bool readonly = false, + bool password = false); + void addTextBox(String& html, const String& input_id, const String& name, @@ -19,8 +30,13 @@ void addTextBox(String& html, bool required, bool readonly = false, bool password = false); + void addTextBox( String& html, const String& input_id, const String& name, uint8_t value_key, int minlength, int maxlength, bool required, bool readonly = false); + +void addTextBox( + String& html, const String& input_id, const String& name, const String& value, int minlength, int maxlength, bool required, bool readonly = false); + void addTextBoxPassword(String& html, const String& input_id, const String& name, uint8_t value_key, int minlength, int maxlength, bool required); void addNumberBox(String& html, const String& input_id, const String& name, uint8_t value_key, uint16_t max); diff --git a/src/SuplaConfigESP.cpp b/src/SuplaConfigESP.cpp index fd04ca0c..143de405 100644 --- a/src/SuplaConfigESP.cpp +++ b/src/SuplaConfigESP.cpp @@ -624,12 +624,12 @@ void SuplaConfigESP::factoryReset(bool forceReset) { ConfigManager->set(key, "0,0,0,0,0"); } - for (nr = 0; nr <= MAX_DS18B20; nr++) { + /* for (nr = 0; nr <= MAX_DS18B20; nr++) { key = KEY_DS + nr; ConfigManager->set(key, ""); key = KEY_DS_NAME + nr; ConfigManager->set(key, ""); - } + }*/ ConfigManager->save(); diff --git a/src/SuplaConfigManager.cpp b/src/SuplaConfigManager.cpp index 1b999582..77b3f46a 100644 --- a/src/SuplaConfigManager.cpp +++ b/src/SuplaConfigManager.cpp @@ -53,15 +53,6 @@ int ConfigOption::getValueInt() { return atoi(_value); } -uint8_t *ConfigOption::getValueBin(size_t size) { - uint8_t *buffer = (uint8_t *)malloc(sizeof(uint8_t) * (size)); - - for (int i = 0; i < size; i++) { - sscanf(&_value[i * 2], "%2hhx", &buffer[i]); - } - return buffer; -} - const char *ConfigOption::getValueHex(size_t size) { char *buffer = (char *)malloc(sizeof(char) * (size * 2)); int a, b; @@ -118,6 +109,22 @@ const String ConfigOption::replaceElement(int index, int newvalue) { return table; } +const String ConfigOption::replaceElement(int index, const char *newvalue) { + int lenght = SETTINGSCOUNT; + String table; + for (int i = 0; i <= lenght; i++) { + if (i == index) { + table += newvalue; + } + else { + table += this->getElement(i); + } + if (i < lenght - 1) + table += SEPARATOR; + } + return table; +} + void ConfigOption::setValue(const char *value) { if (value != NULL) { size_t size = getLength(); @@ -161,19 +168,14 @@ SuplaConfigManager::SuplaConfigManager() { for (nr = 0; nr <= 17; nr++) { key = KEY_GPIO + nr; - this->addKey(key, "0,0,0,0,0,0", 28); + this->addKey(key, "0,0,0,0,0,0", SETTINGSCOUNT * 2); } this->addKey(KEY_ACTIVE_SENSOR, "0,0,0,0,0", 16); this->addKey(KEY_BOARD, "0", 2); this->addKey(KEY_CFG_MODE, "0", 2); - - for (nr = 0; nr <= MAX_DS18B20; nr++) { - key = KEY_DS + nr; - this->addKey(key, MAX_DS18B20_ADDRESS_HEX); - key = KEY_DS_NAME + nr; - this->addKey(key, MAX_DS18B20_NAME); - } + this->addKey(KEY_ADDR_DS18B20, MAX_DS18B20_ADDRESS_HEX * MAX_DS18B20); + this->addKey(KEY_NAME_SENSOR, MAX_DS18B20_NAME * MAX_DS18B20); this->load(); // switch (this->load()) { @@ -394,6 +396,17 @@ bool SuplaConfigManager::setElement(uint8_t key, int index, int newvalue) { return false; } +bool SuplaConfigManager::setElement(uint8_t key, int index, const char *newvalue) { + for (int i = 0; i < _optionCount; i++) { + if (key == _options[i]->getKey()) { + String data = _options[i]->replaceElement(index, newvalue); + _options[i]->setValue(data.c_str()); + return true; + } + } + return false; +} + void SuplaConfigManager::setGUIDandAUTHKEY() { if (strcmp(this->get(KEY_SUPLA_GUID)->getValue(), "") != 0 || strcmp(this->get(KEY_SUPLA_AUTHKEY)->getValue(), "") != 0) { return; diff --git a/src/SuplaConfigManager.h b/src/SuplaConfigManager.h index 10eb2dde..db8f15b9 100644 --- a/src/SuplaConfigManager.h +++ b/src/SuplaConfigManager.h @@ -43,7 +43,8 @@ #define MAX_TYPE_BUTTON 4 #define MAX_MONOSTABLE_TRIGGER 1 #define MAX_FUNCTION 1 -#define MAX_DS18B20 10 + +#define MAX_DS18B20 20 #define MAX_GPIO 17 enum _key @@ -70,9 +71,12 @@ enum _key KEY_ACTIVE_SENSOR, KEY_BOARD, KEY_CFG_MODE, + KEY_ADDR_DS18B20, + KEY_NAME_SENSOR, + KEY_GPIO, - KEY_DS = KEY_GPIO + MAX_GPIO + MAX_DS18B20, - KEY_DS_NAME = KEY_DS + MAX_DS18B20 + //KEY_DS = KEY_GPIO + MAX_GPIO + MAX_DS18B20, + //KEY_DS_NAME = KEY_DS + MAX_DS18B20 }; //#define GPIO "GPIO" @@ -140,14 +144,15 @@ class ConfigOption { uint8_t getKey(); const char *getValue(); int getValueInt(); - uint8_t *getValueBin(size_t size); const char *getValueHex(size_t size); int getValueElement(int element); int getLength(); void setValue(const char *value); const String getElement(int index); + uint8_t getElement(int index, size_t size); const String replaceElement(int index, int value); + const String replaceElement(int index, const char *newvalue); private: uint8_t _key; @@ -170,6 +175,7 @@ class SuplaConfigManager { ConfigOption *get(uint8_t key); bool set(uint8_t key, const char *value); bool setElement(uint8_t key, int index, int newvalue); + bool setElement(uint8_t key, int index, const char *newvalue); bool isDeviceConfigured(); void setGUIDandAUTHKEY(); diff --git a/src/SuplaDeviceGUI.cpp b/src/SuplaDeviceGUI.cpp index 9f1cf5c1..bcfa68e7 100644 --- a/src/SuplaDeviceGUI.cpp +++ b/src/SuplaDeviceGUI.cpp @@ -27,7 +27,6 @@ Supla::Eeprom eeprom(STORAGE_OFFSET); namespace Supla { namespace GUI { void begin() { - #ifdef DEBUG_MODE new Supla::Sensor::EspFreeHeap(); #endif @@ -89,9 +88,8 @@ void addRelayButton(int pinRelay, int pinButton, bool highIsOn) { void addDS18B20MultiThermometer(int pinNumber) { if (ConfigManager->get(KEY_MULTI_MAX_DS18B20)->getValueInt() > 1) { for (int i = 0; i < ConfigManager->get(KEY_MULTI_MAX_DS18B20)->getValueInt(); ++i) { - uint8_t ds_key = KEY_DS + i; - sensorDS.push_back(new DS18B20(pinNumber, ConfigManager->get(ds_key)->getValueBin(MAX_DS18B20_ADDRESS))); - supla_log(LOG_DEBUG, "Index %d - address %s", i, ConfigManager->get(ds_key)->getValue()); + sensorDS.push_back(new DS18B20(pinNumber, HexToBytes(ConfigManager->get(KEY_ADDR_DS18B20)->getElement(i)))); + supla_log(LOG_DEBUG, "Index %d - address %s", i, ConfigManager->get(KEY_ADDR_DS18B20)->getElement(i).c_str()); } } else { diff --git a/src/SuplaOled.cpp b/src/SuplaOled.cpp index 842d200e..66340988 100644 --- a/src/SuplaOled.cpp +++ b/src/SuplaOled.cpp @@ -3,7 +3,6 @@ #ifdef SUPLA_OLED -uint8_t* framesCountSensor; uint8_t* chanelSensor; String getTempString(double temperature) { @@ -33,29 +32,22 @@ String getPressureString(double pressure) { } } -uint8_t getFramesCountSensor(OLEDDisplayUiState* state) { - if (state->frameState) { - return framesCountSensor[state->currentFrame]; - } - return 0; -} - -int32_t readRssi(void) { - int32_t rssi = WiFi.RSSI(); +int getQuality() { if (WiFi.status() != WL_CONNECTED) return -1; - if (rssi <= -100) + int dBm = WiFi.RSSI(); + if (dBm <= -100) return 0; - if (rssi >= -50) + if (dBm >= -50) return 100; - return (2 * (rssi + 100)); + return 2 * (dBm + 100); } void displayUiSignal(OLEDDisplay* display) { int x = display->getWidth() - 17; int y = 0; - int value = readRssi(); - // clear area only + int value = getQuality(); + display->setColor(BLACK); display->fillRect(x, y, x + 46, 16); display->setColor(WHITE); @@ -66,23 +58,15 @@ void displayUiSignal(OLEDDisplay* display) { else { if (value > 0) display->fillRect(x, y + 6, 3, 4); - else - display->drawRect(x, y + 6, 3, 4); if (value >= 25) display->fillRect(x + 4, y + 4, 3, 6); - else - display->drawRect(x + 4, y + 4, 3, 6); if (value >= 50) display->fillRect(x + 8, y + 2, 3, 8); - else - display->drawRect(x + 8, y + 2, 3, 8); if (value >= 75) display->fillRect(x + 12, y, 3, 10); - else - display->drawRect(x + 12, y, 3, 10); } } @@ -164,22 +148,23 @@ void displayUiTemperature(OLEDDisplay* display, OLEDDisplayUiState* state, int16 } else { display->drawXbm(x + 0, y + drawHeightIcon, TEMP_WIDTH, TEMP_HEIGHT, temp_bits); - display->setFont(ArialMT_Plain_10); - if (name != NULL) { - display->drawString(x + TEMP_WIDTH + 20, y + display->getHeight() / 2 - 15, name); - } temp_width = TEMP_WIDTH + 10; temp_height = TEMP_HEIGHT; } + if (name != NULL) { + display->setFont(ArialMT_Plain_10); + display->drawString(x + TEMP_WIDTH + 20, y + display->getHeight() / 2 - 15, name); + } + display->setFont(ArialMT_Plain_24); display->drawString(x + temp_width, y + drawStringIcon, getTempString(temp)); display->setFont(ArialMT_Plain_16); display->drawString(x + temp_width + (getTempString(temp).length() * 12), y + drawStringIcon, "ºC"); } -void displaUiHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double humidity) { +void displaUiHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double humidity, const String& name) { uint8_t humidity_width, humidity_height; int drawHeightIcon = display->getHeight() / 2 - 10; @@ -198,13 +183,18 @@ void displaUiHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x humidity_height = HUMIDITY_HEIGHT; } + if (name != NULL) { + display->setFont(ArialMT_Plain_10); + display->drawString(x + TEMP_WIDTH + 20, y + display->getHeight() / 2 - 15, name); + } + display->setFont(ArialMT_Plain_24); display->drawString(x + humidity_width, y + drawStringIcon, getHumidityString(humidity)); display->setFont(ArialMT_Plain_16); display->drawString(x + humidity_width + (getHumidityString(humidity).length() * 12), y + drawStringIcon, "%"); } -void displayUiPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double pressure) { +void displayUiPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double pressure, const String& name) { uint8_t pressure_width, pressure_height; int drawHeightIcon = display->getHeight() / 2 - 10; @@ -219,13 +209,18 @@ void displayUiPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t } else { display->drawXbm(x + 0, y + drawHeightIcon, PRESSURE_WIDTH, PRESSURE_HEIGHT, pressure_bits); - pressure_width = PRESSURE_WIDTH + 15; + pressure_width = PRESSURE_WIDTH + 10; pressure_height = PRESSURE_HEIGHT; } + if (name != NULL) { + display->setFont(ArialMT_Plain_10); + display->drawString(x + TEMP_WIDTH + 20, y + display->getHeight() / 2 - 15, name); + } + display->setFont(ArialMT_Plain_24); display->drawString(x + pressure_width, y + drawStringIcon, getPressureString(pressure)); - display->setFont(ArialMT_Plain_10); + display->setFont(ArialMT_Plain_16); display->drawString(x + pressure_width + (getPressureString(pressure).length() * 14), y + drawStringIcon, "hPa"); } @@ -235,13 +230,8 @@ void displayTemperature(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t auto channel = element->getChannel(); if (channel->getChannelNumber() == chanelSensor[state->currentFrame]) { double lastTemperature = channel->getValueDouble(); - - if (ConfigManager->get(KEY_MULTI_MAX_DS18B20)->getValueInt() >= state->currentFrame) { - displayUiTemperature(display, state, x, y, lastTemperature, ConfigManager->get(KEY_DS_NAME + state->currentFrame)->getValue()); - } - else { - displayUiTemperature(display, state, x, y, lastTemperature); - } + String name = ConfigManager->get(KEY_NAME_SENSOR)->getElement(state->currentFrame); + displayUiTemperature(display, state, x, y, lastTemperature, name); } } } @@ -253,7 +243,8 @@ void displayDoubleTemperature(OLEDDisplay* display, OLEDDisplayUiState* state, i auto channel = element->getChannel(); if (channel->getChannelNumber() == chanelSensor[state->currentFrame]) { double lastTemperature = channel->getValueDoubleFirst(); - displayUiTemperature(display, state, x, y, lastTemperature); + String name = ConfigManager->get(KEY_NAME_SENSOR)->getElement(state->currentFrame); + displayUiTemperature(display, state, x, y, lastTemperature, name); } } } @@ -265,7 +256,8 @@ void displayDoubleHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int1 auto channel = element->getChannel(); if (channel->getChannelNumber() == chanelSensor[state->currentFrame]) { double lastHumidit = channel->getValueDoubleSecond(); - displaUiHumidity(display, state, x, y, lastHumidit); + String name = ConfigManager->get(KEY_NAME_SENSOR)->getElement(state->currentFrame); + displaUiHumidity(display, state, x, y, lastHumidit, name); } } } @@ -277,7 +269,8 @@ void displayPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, auto channel = element->getSecondaryChannel(); if (channel->getChannelNumber() == chanelSensor[state->currentFrame]) { double lastPressure = channel->getValueDouble(); - displayUiPressure(display, state, x, y, lastPressure); + String name = ConfigManager->get(KEY_NAME_SENSOR)->getElement(state->currentFrame); + displayUiPressure(display, state, x, y, lastPressure, name); } } } @@ -300,15 +293,17 @@ SuplaOled::SuplaOled() { ui = new OLEDDisplayUi(display); overlays[0] = {msOverlay}; - int maxFrame = getMaxFrame(); + int maxFrame = getNumberChannels(); - frames = (FrameCallback*)malloc(sizeof(FrameCallback) * maxFrame); - chanelSensor = (uint8_t*)malloc(sizeof(uint8_t) * maxFrame); + if (maxFrame == 0) + maxFrame = 1; + + frames = new FrameCallback[maxFrame]; + chanelSensor = new uint8_t[maxFrame]; for (auto element = Supla::Element::begin(); element != nullptr; element = element->next()) { if (element->getChannel()) { auto channel = element->getChannel(); - Serial.println(channel->getChannelType()); if (channel->getChannelType() == SUPLA_CHANNELTYPE_THERMOMETER) { frames[frameCount] = {displayTemperature}; @@ -397,34 +392,4 @@ void SuplaOled::runAction(int event, int action) { oledON = true; } } - -int SuplaOled::getMaxFrame() { - int maxFrame = 0; - for (auto element = Supla::Element::begin(); element != nullptr; element = element->next()) { - if (element->getChannel()) { - auto channel = element->getChannel(); - Serial.println(channel->getChannelType()); - - if (channel->getChannelType() == SUPLA_CHANNELTYPE_THERMOMETER) { - maxFrame += 1; - } - - if (channel->getChannelType() == SUPLA_CHANNELTYPE_HUMIDITYANDTEMPSENSOR) { - maxFrame += 2; - } - } - - if (element->getSecondaryChannel()) { - auto channel = element->getSecondaryChannel(); - if (channel->getChannelType() == SUPLA_CHANNELTYPE_PRESSURESENSOR) { - maxFrame += 1; - } - } - } - - if (maxFrame == 0) - maxFrame = 1; - - return maxFrame; -} #endif \ No newline at end of file diff --git a/src/SuplaOled.h b/src/SuplaOled.h index 2f9987e6..4d7532e1 100644 --- a/src/SuplaOled.h +++ b/src/SuplaOled.h @@ -27,8 +27,7 @@ enum _OLED String getTempString(double temperature); String getHumidityString(double humidity); String getPressureString(double pressure); -uint8_t getFramesCountSensor(OLEDDisplayUiState* state); -int32_t readRssi(void); +int32_t getQuality(); void msOverlay(OLEDDisplay* display, OLEDDisplayUiState* state); @@ -38,8 +37,8 @@ void displayUiSuplaStatus(OLEDDisplay* display); void displayUiConfigMode(OLEDDisplay* display); void displayUiBlank(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); void displayUiTemperature(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double temp, const String& name = "\n"); -void displaUiHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double humidity); -void displayUiPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double pressure); +void displaUiHumidity(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double humidity, const String& name = "\n"); +void displayUiPressure(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y, double pressure, const String& name = "\n"); void displayTemperature(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); void displayDoubleTemperature(OLEDDisplay* display, OLEDDisplayUiState* state, int16_t x, int16_t y); @@ -54,7 +53,6 @@ class SuplaOled : public Supla::Triggerable, public Supla::Element { private: void iterateAlways(); void runAction(int event, int action); - int getMaxFrame(); OLEDDisplay* display; OLEDDisplayUi* ui; @@ -64,8 +62,6 @@ class SuplaOled : public Supla::Triggerable, public Supla::Element { OverlayCallback overlays[1]; int overlaysCount = 1; - int count = 0; - unsigned long timeLastChangeOled = millis(); bool oledON = true; }; diff --git a/src/SuplaWebPageSensor.cpp b/src/SuplaWebPageSensor.cpp index 3234d52e..43cbfcb6 100644 --- a/src/SuplaWebPageSensor.cpp +++ b/src/SuplaWebPageSensor.cpp @@ -98,18 +98,18 @@ void SuplaWebPageSensor::handleDSSave() { return WebServer->httpServer.requestAuthentication(); } for (uint8_t i = 0; i < ConfigManager->get(KEY_MULTI_MAX_DS18B20)->getValueInt(); i++) { - uint8_t ds_key = KEY_DS + i; - uint8_t ds_name_key = KEY_DS_NAME + i; + String dsAddr = INPUT_DS18B20_ADDR; + String dsName = INPUT_DS18B20_NAME; + dsAddr += i; + dsName += i; - String ds = F("dschlid"); - String ds_name = F("dsnameid"); - ds += i; - ds_name += i; + ConfigManager->setElement(KEY_ADDR_DS18B20, i, WebServer->httpServer.arg(dsAddr).c_str()); - ConfigManager->set(ds_key, WebServer->httpServer.arg(ds).c_str()); - ConfigManager->set(ds_name_key, WebServer->httpServer.arg(ds_name).c_str()); + if (strcmp(WebServer->httpServer.arg(dsName).c_str(), "") != 0) { + ConfigManager->setElement(KEY_NAME_SENSOR, i, WebServer->httpServer.arg(dsName).c_str()); + } - Supla::GUI::sensorDS[i]->setDeviceAddress(ConfigManager->get(ds_key)->getValueBin(MAX_DS18B20_ADDRESS)); + Supla::GUI::sensorDS[i]->setDeviceAddress(HexToBytes(ConfigManager->get(KEY_ADDR_DS18B20)->getElement(i))); } switch (ConfigManager->save()) { @@ -139,7 +139,7 @@ String SuplaWebPageSensor::supla_webpage_search(int save) { content += SuplaSaveResult(save); content += SuplaJavaScript(PATH_MULTI_DS); content += F("
"); - if (ConfigESP->getGpio(FUNCTION_DS18B20) < OFF_GPIO || !Supla::GUI::sensorDS.empty()) { + if (ConfigESP->getGpio(FUNCTION_DS18B20) < OFF_GPIO) { content += F(""); @@ -174,7 +174,8 @@ String SuplaWebPageSensor::supla_webpage_search(int save) { address[7]); supla_log(LOG_DEBUG, "Index %d - address %s", i, strAddr); - content += F(""); - content += F("setElement(KEY_ACTIVE_SENSOR, SENSOR_OLED, WebServer->httpServer.arg(input).toInt()); } + + for (uint8_t i = 0; i < getNumberChannels(); i++) { + input = INPUT_DS18B20_NAME; + input += i; + if (strcmp(WebServer->httpServer.arg(input).c_str(), "") != 0) { + ConfigManager->setElement(KEY_NAME_SENSOR, i, WebServer->httpServer.arg(input).c_str()); + } + } #endif #ifdef SUPLA_MCP23017 @@ -518,6 +526,18 @@ String SuplaWebPageSensor::supla_webpage_i2c(int save) { selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt(); addFormHeader(page); addListBox(page, INPUT_OLED, F("OLED"), OLED_P, 4, selected); + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt()) { + String name, sensorName, input; + + for (uint8_t i = 0; i < getNumberChannels(); i++) { + sensorName = String(ConfigManager->get(KEY_NAME_SENSOR)->getElement(i)); + input = INPUT_DS18B20_NAME; + input += i; + name = F("Ekran "); + name += i + 1; + addTextBox(page, input, name, sensorName, 0, MAX_DS18B20_NAME, false); + } + } addFormHeaderEnd(page); #endif diff --git a/src/SuplaWebPageSensor.h b/src/SuplaWebPageSensor.h index 362c6df7..152ccab2 100644 --- a/src/SuplaWebPageSensor.h +++ b/src/SuplaWebPageSensor.h @@ -81,6 +81,9 @@ class SuplaWebPageSensor { void createWebPageSensor(); #ifdef SUPLA_DS18B20 +#define INPUT_DS18B20_ADDR "dsaddr" +#define INPUT_DS18B20_NAME "dsname" + void handleSearchDS(); void handleDSSave(); void showDS18B20(String& content, bool readonly = false); From 79e22cba7e42f715a07d261ce04e40c98a95639c Mon Sep 17 00:00:00 2001 From: krycha88 Date: Sun, 17 Jan 2021 11:05:50 +0100 Subject: [PATCH 036/165] MCP23017 dla SUPLA_LIMIT_SWITCH --- platformio.ini | 2 - src/Markup.cpp | 25 ++++++-- src/Markup.h | 2 + src/SuplaCommonPROGMEM.cpp | 22 ------- src/SuplaCommonPROGMEM.h | 12 ++-- src/SuplaConfigESP.cpp | 113 ++++++++++++++++++++++++---------- src/SuplaConfigESP.h | 6 +- src/SuplaWebPageControl.cpp | 117 ++++++++++++++++++++++++++++-------- src/SuplaWebPageControl.h | 8 +++ src/SuplaWebServer.cpp | 39 +++++++----- src/SuplaWebServer.h | 4 +- 11 files changed, 239 insertions(+), 111 deletions(-) delete mode 100644 src/SuplaCommonPROGMEM.cpp diff --git a/platformio.ini b/platformio.ini index ffe3d704..bd7f22d3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -63,7 +63,6 @@ lib_deps = datacute/DoubleResetDetector@^1.0.3 closedcube/ClosedCube SHT31D@^1.5.1 adafruit/Adafruit Si7021 Library@^1.3.0 - ;thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays @ ^4.1.0 xoseperez/HLW8012 @ ^1.1.1 extra_scripts = tools/copy_files.py @@ -126,7 +125,6 @@ board = nodemcuv2 board_build.ldscript = eagle.flash.4m1m.ld build_flags = ${common.build_flags} -D DEBUG_MODE -build_unflags = -D SUPLA_IMPULSE_COUNTER [env:GUI_Generic_blank] board = nodemcuv2 diff --git a/src/Markup.cpp b/src/Markup.cpp index 0ba76b4a..703e2e5d 100644 --- a/src/Markup.cpp +++ b/src/Markup.cpp @@ -83,7 +83,7 @@ void addTextBox(String& html, bool readonly, bool password) { String value = String(ConfigManager->get(value_key)->getValue()); - return addTextBox(html, input_id, name, value, "", minlength, maxlength, required, readonly, password); + return addTextBox(html, input_id, name, value, placeholder, minlength, maxlength, required, readonly, password); } void addTextBox( @@ -160,10 +160,28 @@ void addListGPIOBox(String& html, const String& input_id, const String& name, ui html += F(""); } +void addListMCP23017GPIOBox(String& html, const String& input_id, const String& name, uint8_t function, uint8_t nr) { + if (nr == 1) { + uint8_t address = ConfigESP->getAdressMCP23017(nr, function); + addListBox(html, INPUT_ADRESS_MCP23017, F("MCP23017 Adres"), MCP23017_P, 4, address); + } + + html += F(""); + addListMCP23017GPIO(html, input_id, function, nr); + html += F(""); +} + void addListMCP23017GPIOLinkBox(String& html, const String& input_id, const String& name, uint8_t function, const String& url, uint8_t nr) { if (nr == 1) { - uint8_t address = ConfigESP->getAdressMCP23017(function); - addListBox(html, INPUT_ADRESS_MCP23017, F("MCP23017 Adres"), MCP23017_P, 3, address); + uint8_t address = ConfigESP->getAdressMCP23017(nr, function); + addListBox(html, INPUT_ADRESS_MCP23017, F("MCP23017 Adres"), MCP23017_P, 4, address); } html += F(""); @@ -305,7 +323,6 @@ void addListMCP23017GPIO(String& html, const String& input_id, uint8_t function, html += F("'>"); uint8_t selected = ConfigESP->getGpioMCP23017(nr, function); - uint8_t address = ConfigESP->getAdressMCP23017(function); for (uint8_t suported = 0; suported < sizeof(GPIO_MCP23017_P) / sizeof(GPIO_MCP23017_P[0]); suported++) { if (ConfigESP->checkBusyGpioMCP23017(suported, function) || selected == suported) { diff --git a/src/Markup.h b/src/Markup.h index b06c2556..f04c9200 100644 --- a/src/Markup.h +++ b/src/Markup.h @@ -47,6 +47,8 @@ void addLinkBox(String& html, const String& name, const String& url); void addListGPIOBox(String& html, const String& input_id, const String& name, uint8_t function, uint8_t nr = 0); +void addListMCP23017GPIOBox(String& html, const String& input_id, const String& name, uint8_t function, uint8_t nr); + void addListMCP23017GPIOLinkBox(String& html, const String& input_id, const String& name, uint8_t function, const String& url, uint8_t nr = 0); void addListGPIOLinkBox(String& html, const String& input_id, const String& name, uint8_t function, const String& url, uint8_t nr = 0); diff --git a/src/SuplaCommonPROGMEM.cpp b/src/SuplaCommonPROGMEM.cpp deleted file mode 100644 index 25415725..00000000 --- a/src/SuplaCommonPROGMEM.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "SuplaCommonPROGMEM.h" -#include "SuplaTemplateBoard.h" - -String StateString(uint8_t adr) { - return PGMT(STATE_P[adr]); -} - -String LevelString(uint8_t nr) { - return PGMT(LEVEL_P[nr]); -} - -String MemoryString(uint8_t nr) { - return PGMT(MEMORY_P[nr]); -} - -String TriggerString(uint8_t nr) { - return PGMT(TRIGGER_P[nr]); -} - -String BoardString(uint8_t board) { - return PGMT(BOARD_P[board]); -} diff --git a/src/SuplaCommonPROGMEM.h b/src/SuplaCommonPROGMEM.h index ffab8c05..6ce2578e 100644 --- a/src/SuplaCommonPROGMEM.h +++ b/src/SuplaCommonPROGMEM.h @@ -50,7 +50,7 @@ const char HTTP_LOGO[] PROGMEM = "102.1,188.6z " "M167.7,88.5c-1,0-2.1,0.1-3.1,0.3c-9,1.7-14.2,10.6-10.8,18.6c2.9,6.8,11.4,10.3,19,7.8c7.1-2.3,11.1-9.1,9.6-15.9C180.9,93,174.8,88.5,167.7,88.5z'/" ">"; -const char HTTP_SUMMARY[] PROGMEM = "

{h}

LAST STATE: {s}
Firmware: SuplaDevice {v}
GUID: {g}
MAC: {m}
\n"; +const char HTTP_SUMMARY[] PROGMEM = "

{h}

LAST STATE: {s}
Firmware: SuplaDevice {v}
GUID: {g}
MAC: {m}
Free Mem: {f}KB
\n"; const char HTTP_COPYRIGHT[] PROGMEM = "https://forum.supla.org/\n"; @@ -111,7 +111,9 @@ const char* const SHT3x_P[] PROGMEM = {OFF, ADR44, ADR45, ADR44_ADR45}; const char ADR20[] PROGMEM = "0x20"; const char ADR21[] PROGMEM = "0x21"; -const char* const MCP23017_P[] PROGMEM = {ADR20, ADR21, OFF}; +const char ADR22[] PROGMEM = "0x22"; +const char ADR23[] PROGMEM = "0x23"; +const char* const MCP23017_P[] PROGMEM = {ADR20, ADR21, ADR22, OFF}; const char* const STATE_P[] PROGMEM = {OFF, ON}; @@ -141,10 +143,4 @@ const char SSD1306_WEMOS_SHIELD[] PROGMEM = "SSD1306 - 0,66'' WEMOS OLED shield" const char* const OLED_P[] PROGMEM = {OFF, SSD1306, SH1106, SSD1306_WEMOS_SHIELD}; #endif -String StateString(uint8_t adr); -String LevelString(uint8_t nr); -String MemoryString(uint8_t nr); -String TriggerString(uint8_t nr); -String BoardString(uint8_t board); - #endif // SuplaCommonPROGMEM_h diff --git a/src/SuplaConfigESP.cpp b/src/SuplaConfigESP.cpp index 143de405..1ceba4e9 100644 --- a/src/SuplaConfigESP.cpp +++ b/src/SuplaConfigESP.cpp @@ -253,7 +253,7 @@ int SuplaConfigESP::getGpio(int nr, int function) { //"Pin 100 - 115" // Pin 116 - 131" if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt()) { - switch (getAdressMCP23017(function)) { + switch (getAdressMCP23017(nr, function)) { case 0: if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_1).toInt() == function && ConfigManager->get(key)->getElement(MCP23017_NR_1).toInt() == nr) { @@ -266,6 +266,12 @@ int SuplaConfigESP::getGpio(int nr, int function) { return gpio + 100 + 16; } break; + case 2: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_3).toInt() == function && + ConfigManager->get(key)->getElement(MCP23017_NR_3).toInt() == nr) { + return gpio + 100 + 16 + 16; + } + break; } } } @@ -281,7 +287,7 @@ int SuplaConfigESP::getLevel(int nr, int function) { } } - switch (getAdressMCP23017(function)) { + switch (getAdressMCP23017(nr, function)) { case 0: if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_1).toInt() == function) { if (ConfigManager->get(key)->getElement(MCP23017_NR_1).toInt() == nr) { @@ -298,6 +304,14 @@ int SuplaConfigESP::getLevel(int nr, int function) { } } break; + case 2: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_3).toInt() == function) { + if (ConfigManager->get(key)->getElement(MCP23017_NR_3).toInt() == nr) { + return ConfigManager->get(key)->getElement(LEVEL).toInt(); + ; + } + } + break; } } return OFF_GPIO; @@ -312,7 +326,7 @@ int SuplaConfigESP::getMemory(int nr, int function) { } } - switch (getAdressMCP23017(function)) { + switch (getAdressMCP23017(nr, function)) { case 0: if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_1).toInt() == function) { if (ConfigManager->get(key)->getElement(MCP23017_NR_1).toInt() == nr) { @@ -329,6 +343,14 @@ int SuplaConfigESP::getMemory(int nr, int function) { } } break; + case 2: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_3).toInt() == function) { + if (ConfigManager->get(key)->getElement(MCP23017_NR_3).toInt() == nr) { + return ConfigManager->get(key)->getElement(MEMORY).toInt(); + ; + } + } + break; } } return OFF_GPIO; @@ -344,7 +366,7 @@ int SuplaConfigESP::getAction(int nr, int function) { } } - switch (getAdressMCP23017(function)) { + switch (getAdressMCP23017(nr, function)) { case 0: if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_1).toInt() == function) { if (ConfigManager->get(key)->getElement(MCP23017_NR_1).toInt() == nr) { @@ -361,6 +383,14 @@ int SuplaConfigESP::getAction(int nr, int function) { } } break; + case 2: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_3).toInt() == function) { + if (ConfigManager->get(key)->getElement(MCP23017_NR_3).toInt() == nr) { + return ConfigManager->get(key)->getElement(ACTION).toInt(); + ; + } + } + break; } } return OFF_GPIO; @@ -455,17 +485,10 @@ bool SuplaConfigESP::checkBusyGpioMCP23017(uint8_t gpio, uint8_t function) { } else { uint8_t key = KEY_GPIO + gpio; - switch (getAdressMCP23017(function)) { - case 0: - if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_1).toInt() != FUNCTION_OFF) { - return false; - } - break; - case 1: - if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_2).toInt() != FUNCTION_OFF) { - return false; - } - break; + uint8_t address = ConfigESP->getAdressMCP23017(1, function); + + if (ConfigManager->get(key)->getElement(getFunctionMCP23017(address)).toInt() != FUNCTION_OFF) { + return false; } } return true; @@ -475,7 +498,7 @@ uint8_t SuplaConfigESP::getGpioMCP23017(uint8_t nr, uint8_t function) { for (uint8_t gpio = 0; gpio <= OFF_GPIO; gpio++) { uint8_t key = KEY_GPIO + gpio; - switch (getAdressMCP23017(function)) { + switch (getAdressMCP23017(nr, function)) { case 0: if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_1).toInt() == function) { if (ConfigManager->get(key)->getElement(MCP23017_NR_1).toInt() == nr) { @@ -490,33 +513,45 @@ uint8_t SuplaConfigESP::getGpioMCP23017(uint8_t nr, uint8_t function) { } } break; + case 2: + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_3).toInt() == function) { + if (ConfigManager->get(key)->getElement(MCP23017_NR_3).toInt() == nr) { + return gpio; + } + } + break; } } return OFF_GPIO; } -uint8_t SuplaConfigESP::getAdressMCP23017(uint8_t function) { +uint8_t SuplaConfigESP::getAdressMCP23017(uint8_t nr, uint8_t function) { for (uint8_t gpio = 0; gpio <= OFF_GPIO; gpio++) { uint8_t key = KEY_GPIO + gpio; - if (ConfigManager->get(key)->getElement(MCP23017_NR_1).toInt() != 0) { + if (ConfigManager->get(key)->getElement(MCP23017_NR_1).toInt() == nr) { if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_1).toInt() == function) { return 0; } } - if (ConfigManager->get(key)->getElement(MCP23017_NR_2).toInt() != 0) { + if (ConfigManager->get(key)->getElement(MCP23017_NR_2).toInt() == nr) { if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_2).toInt() == function) { return 1; } } + if (ConfigManager->get(key)->getElement(MCP23017_NR_3).toInt() == nr) { + if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_3).toInt() == function) { + return 2; + } + } } - return 2; + return OFF_MCP23017; } void SuplaConfigESP::setGpioMCP23017(uint8_t gpio, uint8_t adress, uint8_t nr, uint8_t function, uint8_t level, uint8_t memory) { uint8_t key = KEY_GPIO + gpio; uint8_t _gpio = ConfigESP->getGpioMCP23017(nr, function); - ConfigESP->clearGpioMCP23017(_gpio, function); + ConfigESP->clearGpioMCP23017(_gpio, nr, function); if (ConfigManager->get(key)->getElement(FUNCTION).toInt() == FUNCTION_OFF) { ConfigManager->setElement(key, LEVEL, 0); @@ -538,15 +573,21 @@ void SuplaConfigESP::setGpioMCP23017(uint8_t gpio, uint8_t adress, uint8_t nr, u ConfigManager->setElement(key, MCP23017_NR_2, nr); ConfigManager->setElement(key, MCP23017_FUNCTION_2, function); break; + case 2: + ConfigManager->setElement(key, MCP23017_NR_3, nr); + ConfigManager->setElement(key, MCP23017_FUNCTION_3, function); + break; } } -void SuplaConfigESP::clearGpioMCP23017(uint8_t gpio, uint8_t function) { +void SuplaConfigESP::clearGpioMCP23017(uint8_t gpio, uint8_t nr, uint8_t function) { uint8_t key = KEY_GPIO + gpio; - uint8_t adress = getAdressMCP23017(function); + uint8_t adress = getAdressMCP23017(nr, function); - ConfigManager->setElement(key, getNrMCP23017(adress), 0); - ConfigManager->setElement(key, getFunctionMCP23017(adress), FUNCTION_OFF); + if (adress != OFF_MCP23017) { + ConfigManager->setElement(key, getNrMCP23017(adress), 0); + ConfigManager->setElement(key, getFunctionMCP23017(adress), FUNCTION_OFF); + } } void SuplaConfigESP::clearFunctionGpio(uint8_t function) { @@ -568,8 +609,11 @@ uint8_t SuplaConfigESP::getFunctionMCP23017(uint8_t adress) { case 1: return MCP23017_FUNCTION_2; break; + case 2: + return MCP23017_FUNCTION_3; + break; } - return OFF_GPIO; + return FUNCTION_OFF; } uint8_t SuplaConfigESP::getNrMCP23017(uint8_t adress) { @@ -580,8 +624,11 @@ uint8_t SuplaConfigESP::getNrMCP23017(uint8_t adress) { case 1: return MCP23017_NR_2; break; + case 2: + return MCP23017_NR_3; + break; } - return OFF_GPIO; + return FUNCTION_OFF; } void SuplaConfigESP::factoryReset(bool forceReset) { @@ -624,12 +671,12 @@ void SuplaConfigESP::factoryReset(bool forceReset) { ConfigManager->set(key, "0,0,0,0,0"); } - /* for (nr = 0; nr <= MAX_DS18B20; nr++) { - key = KEY_DS + nr; - ConfigManager->set(key, ""); - key = KEY_DS_NAME + nr; - ConfigManager->set(key, ""); - }*/ + /* for (nr = 0; nr <= MAX_DS18B20; nr++) { + key = KEY_DS + nr; + ConfigManager->set(key, ""); + key = KEY_DS_NAME + nr; + ConfigManager->set(key, ""); + }*/ ConfigManager->save(); diff --git a/src/SuplaConfigESP.h b/src/SuplaConfigESP.h index 7a0c9ae9..9a526026 100644 --- a/src/SuplaConfigESP.h +++ b/src/SuplaConfigESP.h @@ -37,6 +37,8 @@ enum _ConfigMode FACTORYRESET }; +#define OFF_MCP23017 3 + typedef struct { int status; const char *msg; @@ -97,9 +99,9 @@ class SuplaConfigESP : public Supla::Triggerable, public Supla::Element { bool checkBusyGpioMCP23017(uint8_t gpio, uint8_t function); uint8_t getGpioMCP23017(uint8_t nr, uint8_t function); - uint8_t getAdressMCP23017(uint8_t function); + uint8_t getAdressMCP23017(uint8_t nr, uint8_t function); void setGpioMCP23017(uint8_t gpio, uint8_t adress, uint8_t nr, uint8_t function, uint8_t level, uint8_t memory); - void clearGpioMCP23017(uint8_t gpio, uint8_t function); + void clearGpioMCP23017(uint8_t gpio, uint8_t nr, uint8_t function); void clearFunctionGpio(uint8_t function); uint8_t getFunctionMCP23017(uint8_t adress); uint8_t getNrMCP23017(uint8_t adress); diff --git a/src/SuplaWebPageControl.cpp b/src/SuplaWebPageControl.cpp index 28411956..18131c29 100644 --- a/src/SuplaWebPageControl.cpp +++ b/src/SuplaWebPageControl.cpp @@ -28,6 +28,15 @@ void SuplaWebPageControl::createWebPageControl() { WebServer->httpServer.on(path, std::bind(&SuplaWebPageControl::handleButtonSaveSet, this)); } #endif + +#ifdef SUPLA_LIMIT_SWITCH + path = PATH_START; + path += PATH_SWITCH; + WebServer->httpServer.on(path, std::bind(&SuplaWebPageControl::handleLimitSwitch, this)); + path = PATH_START; + path += PATH_SAVE_SWITCH; + WebServer->httpServer.on(path, std::bind(&SuplaWebPageControl::handleLimitSwitchSave, this)); +#endif } void SuplaWebPageControl::handleControl() { @@ -69,20 +78,6 @@ void SuplaWebPageControl::handleControlSave() { } #endif -#ifdef SUPLA_LIMIT_SWITCH - last_value = ConfigManager->get(KEY_MAX_LIMIT_SWITCH)->getValueInt(); - for (nr = 1; nr <= last_value; nr++) { - if (!WebServer->saveGPIO(INPUT_LIMIT_SWITCH_GPIO, FUNCTION_LIMIT_SWITCH, nr, INPUT_MAX_LIMIT_SWITCH)) { - supla_webpage_control(6); - return; - } - } - - if (strcmp(WebServer->httpServer.arg(INPUT_MAX_LIMIT_SWITCH).c_str(), "") != 0) { - ConfigManager->set(KEY_MAX_LIMIT_SWITCH, WebServer->httpServer.arg(INPUT_MAX_LIMIT_SWITCH).c_str()); - } -#endif - switch (ConfigManager->save()) { case E_CONFIG_OK: supla_webpage_control(1); @@ -123,15 +118,6 @@ void SuplaWebPageControl::supla_webpage_control(int save) { addFormHeaderEnd(webContentBuffer); #endif -#ifdef SUPLA_LIMIT_SWITCH - addFormHeader(webContentBuffer, String(S_GPIO_SETTINGS_FOR_LIMIT_SWITCH)); - addNumberBox(webContentBuffer, INPUT_MAX_LIMIT_SWITCH, S_QUANTITY, KEY_MAX_LIMIT_SWITCH, ConfigESP->countFreeGpio(FUNCTION_LIMIT_SWITCH)); - for (nr = 1; nr <= ConfigManager->get(KEY_MAX_LIMIT_SWITCH)->getValueInt(); nr++) { - addListGPIOBox(webContentBuffer, INPUT_LIMIT_SWITCH_GPIO, S_LIMIT_SWITCH, FUNCTION_LIMIT_SWITCH, nr); - } - addFormHeaderEnd(webContentBuffer); -#endif - addButtonSubmit(webContentBuffer, S_SAVE); addFormEnd(webContentBuffer); addButton(webContentBuffer, S_RETURN, PATH_DEVICE_SETTINGS); @@ -226,7 +212,90 @@ void SuplaWebPageControl::supla_webpage_button_set(int save) { addFormHeaderEnd(webContentBuffer); addButtonSubmit(webContentBuffer, S_SAVE); addFormEnd(webContentBuffer); - addButton(webContentBuffer, S_RETURN, PATH_RELAY); + addButton(webContentBuffer, S_RETURN, PATH_CONTROL); + + WebServer->sendContent(); +} +#endif + +#ifdef SUPLA_LIMIT_SWITCH +void SuplaWebPageControl::handleLimitSwitch() { + if (ConfigESP->configModeESP == NORMAL_MODE) { + if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) + return WebServer->httpServer.requestAuthentication(); + } + suplaWebpageLimitSwitch(0); +} + +void SuplaWebPageControl::handleLimitSwitchSave() { + if (ConfigESP->configModeESP == NORMAL_MODE) { + if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) + return WebServer->httpServer.requestAuthentication(); + } + + uint8_t nr, last_value; + + last_value = ConfigManager->get(KEY_MAX_LIMIT_SWITCH)->getValueInt(); + for (nr = 1; nr <= last_value; nr++) { + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt() != FUNCTION_OFF) { + if (!WebServer->saveGpioMCP23017(INPUT_LIMIT_SWITCH_GPIO, FUNCTION_LIMIT_SWITCH, nr, INPUT_MAX_LIMIT_SWITCH)) { + suplaWebpageLimitSwitch(6); + return; + } + } + else { + if (!WebServer->saveGPIO(INPUT_LIMIT_SWITCH_GPIO, FUNCTION_LIMIT_SWITCH, nr, INPUT_MAX_LIMIT_SWITCH)) { + suplaWebpageLimitSwitch(6); + return; + } + } + } + + if (strcmp(WebServer->httpServer.arg(INPUT_MAX_LIMIT_SWITCH).c_str(), "") != 0) { + ConfigManager->set(KEY_MAX_LIMIT_SWITCH, WebServer->httpServer.arg(INPUT_MAX_LIMIT_SWITCH).c_str()); + } + + switch (ConfigManager->save()) { + case E_CONFIG_OK: + suplaWebpageLimitSwitch(1); + break; + case E_CONFIG_FILE_OPEN: + suplaWebpageLimitSwitch(2); + break; + } +} + +void SuplaWebPageControl::suplaWebpageLimitSwitch(int save) { + uint8_t nr, countFreeGpio; + + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(PATH_SWITCH); + addForm(webContentBuffer, F("post"), PATH_SAVE_SWITCH); + + addFormHeader(webContentBuffer, String(S_GPIO_SETTINGS_FOR_LIMIT_SWITCH)); + + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt() != FUNCTION_OFF) { + countFreeGpio = 16; + } + else { + countFreeGpio = ConfigESP->countFreeGpio(FUNCTION_LIMIT_SWITCH); + } + + addNumberBox(webContentBuffer, INPUT_MAX_LIMIT_SWITCH, S_QUANTITY, KEY_MAX_LIMIT_SWITCH, countFreeGpio); + + for (nr = 1; nr <= ConfigManager->get(KEY_MAX_LIMIT_SWITCH)->getValueInt(); nr++) { + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt() != FUNCTION_OFF) { + addListMCP23017GPIOBox(webContentBuffer, INPUT_LIMIT_SWITCH_GPIO, S_LIMIT_SWITCH, FUNCTION_LIMIT_SWITCH, nr); + } + else { + addListGPIOBox(webContentBuffer, INPUT_LIMIT_SWITCH_GPIO, S_LIMIT_SWITCH, FUNCTION_LIMIT_SWITCH, nr); + } + } + addFormHeaderEnd(webContentBuffer); + + addButtonSubmit(webContentBuffer, S_SAVE); + addFormEnd(webContentBuffer); + addButton(webContentBuffer, S_RETURN, PATH_DEVICE_SETTINGS); WebServer->sendContent(); } diff --git a/src/SuplaWebPageControl.h b/src/SuplaWebPageControl.h index c7043de2..c2443a5e 100644 --- a/src/SuplaWebPageControl.h +++ b/src/SuplaWebPageControl.h @@ -5,6 +5,8 @@ #include "SuplaDeviceGUI.h" #define PATH_CONTROL "control" +#define PATH_SWITCH "switch" +#define PATH_SAVE_SWITCH "saveswitch" #define PATH_SAVE_CONTROL "savecontrol" #define PATH_BUTTON_SET "setbutton" #define PATH_SAVE_BUTTON_SET "savesetbutton" @@ -29,6 +31,12 @@ class SuplaWebPageControl { void handleControl(); void handleControlSave(); +#ifdef SUPLA_LIMIT_SWITCH + void handleLimitSwitch(); + void handleLimitSwitchSave(); + void suplaWebpageLimitSwitch(int save); +#endif + #if (defined(SUPLA_BUTTON) && defined(SUPLA_RELAY)) || (defined(SUPLA_RSUPLA_BUTTONELAY) || defined(SUPLA_ROLLERSHUTTER)) void handleButtonSet(); void handleButtonSaveSet(); diff --git a/src/SuplaWebServer.cpp b/src/SuplaWebServer.cpp index 242d0402..38163f35 100644 --- a/src/SuplaWebServer.cpp +++ b/src/SuplaWebServer.cpp @@ -218,6 +218,10 @@ String SuplaWebServer::deviceSettings(int save) { addButton(content, S_BUTTONS, PATH_CONTROL); #endif +#ifdef SUPLA_LIMIT_SWITCH + addButton(content, F("KONTAKTRON"), PATH_SWITCH); +#endif + #if defined(SUPLA_DS18B20) || defined(SUPLA_DHT11) || defined(SUPLA_DHT22) || defined(SUPLA_SI7021_SONOFF) addButton(content, S_SENSORS_1WIRE, PATH_1WIRE); #endif @@ -317,6 +321,7 @@ void SuplaWebServer::sendContent(const String& content) { summary.replace("{v}", Supla::Channel::reg_dev.SoftVer); summary.replace("{g}", ConfigManager->get(KEY_SUPLA_GUID)->getValueHex(SUPLA_GUID_SIZE)); summary.replace("{m}", ConfigESP->getMacAddress(true)); + summary.replace("{f}", String(ESP.getFreeHeap() / 1024.0)); httpServer.sendContent(summary); httpServer.sendContent_P(HTTP_COPYRIGHT); @@ -371,6 +376,8 @@ void SuplaWebServer::sendContent() { summary.replace("{v}", Supla::Channel::reg_dev.SoftVer); summary.replace("{g}", ConfigManager->get(KEY_SUPLA_GUID)->getValueHex(SUPLA_GUID_SIZE)); summary.replace("{m}", ConfigESP->getMacAddress(true)); + summary.replace("{f}", String(ESP.getFreeHeap() / 1024.0)); + httpServer.sendContent(summary); httpServer.sendContent_P(HTTP_COPYRIGHT); @@ -457,30 +464,34 @@ bool SuplaWebServer::saveGPIO(const String& _input, uint8_t function, uint8_t nr return true; } -bool SuplaWebServer::saveGpioMCP23017(const String& input, uint8_t function, uint8_t nr, const String& input_max) { +bool SuplaWebServer::saveGpioMCP23017(const String& _input, uint8_t function, uint8_t nr, const String& input_max) { uint8_t key, address, addressInput, gpio, gpioInput, functionElementInput; - String _input = input + nr; + String input = _input + nr; - if (strcmp(WebServer->httpServer.arg(_input).c_str(), "") == 0) { + if (strcmp(WebServer->httpServer.arg(input).c_str(), "") == 0) { return true; } addressInput = WebServer->httpServer.arg(INPUT_ADRESS_MCP23017).toInt(); - functionElementInput = ConfigManager->get(key)->getElement(ConfigESP->getFunctionMCP23017(addressInput)).toInt(); - gpioInput = WebServer->httpServer.arg(_input).toInt(); + gpioInput = WebServer->httpServer.arg(input).toInt(); key = KEY_GPIO + gpioInput; + functionElementInput = ConfigManager->get(key)->getElement(ConfigESP->getFunctionMCP23017(addressInput)).toInt(); gpio = ConfigESP->getGpioMCP23017(nr, function); - address = ConfigESP->getAdressMCP23017(function); - - if (functionElementInput == FUNCTION_OFF) { - ConfigESP->setGpioMCP23017(gpioInput, addressInput, nr, function, 1, 0); - } - else if (gpio == gpioInput && functionElementInput == function) { - ConfigESP->setGpioMCP23017(gpioInput, addressInput, nr, function, ConfigESP->getLevel(nr, function), ConfigESP->getMemory(nr, function)); + if (addressInput == OFF_MCP23017) { + ConfigESP->clearGpioMCP23017(gpio, nr, function); } - else { - return false; + + if (gpioInput != OFF_GPIO) { + if (functionElementInput == FUNCTION_OFF) { + ConfigESP->setGpioMCP23017(gpioInput, addressInput, nr, function, 1, 0); + } + else if (gpio == gpioInput && functionElementInput == function) { + ConfigESP->setGpioMCP23017(gpioInput, addressInput, nr, function, ConfigESP->getLevel(nr, function), ConfigESP->getMemory(nr, function)); + } + else { + return false; + } } return true; } \ No newline at end of file diff --git a/src/SuplaWebServer.h b/src/SuplaWebServer.h index 333c7fc4..958b0e2e 100644 --- a/src/SuplaWebServer.h +++ b/src/SuplaWebServer.h @@ -92,8 +92,8 @@ class SuplaWebServer : public Supla::Element { ESP8266HTTPUpdateServer httpUpdater; #endif - bool saveGPIO(const String& input, uint8_t function, uint8_t nr = 0, const String& input_max = "\n"); - bool saveGpioMCP23017(const String& input, uint8_t function, uint8_t nr = 0, const String& input_max = "\n"); + bool saveGPIO(const String& _input, uint8_t function, uint8_t nr = 0, const String& input_max = "\n"); + bool saveGpioMCP23017(const String& _input, uint8_t function, uint8_t nr = 0, const String& input_max = "\n"); private: void iterateAlways(); From a97faf317fb3d552204a17c319b25ef800fbf80a Mon Sep 17 00:00:00 2001 From: krycha88 Date: Sun, 17 Jan 2021 12:48:55 +0100 Subject: [PATCH 037/165] =?UTF-8?q?nowy=20spos=C3=B3b=20wysy=C5=82ania=20s?= =?UTF-8?q?endContent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Markup.cpp | 6 +- src/SuplaCommonPROGMEM.h | 9 + src/SuplaHTTPUpdateServer.cpp | 47 ++- src/SuplaHTTPUpdateServer.h | 2 +- src/SuplaWebPageConfig.cpp | 40 +-- src/SuplaWebPageConfig.h | 2 +- src/SuplaWebPageSensor.cpp | 636 +++++++++++++++++----------------- src/SuplaWebPageSensor.h | 16 +- src/SuplaWebPageTools.cpp | 19 +- src/SuplaWebPageUpload.cpp | 31 +- src/SuplaWebServer.cpp | 195 ++++------- src/SuplaWebServer.h | 8 +- 12 files changed, 465 insertions(+), 546 deletions(-) diff --git a/src/Markup.cpp b/src/Markup.cpp index 703e2e5d..d58d9739 100644 --- a/src/Markup.cpp +++ b/src/Markup.cpp @@ -141,7 +141,7 @@ void addLinkBox(String& html, const String& name, const String& url) { html += url; html += F("'>"); html += name; - html += WebServer->SuplaIconEdit(); + html += PGMT(ICON_EDIT); html += F(""); html += F(""); html += F("
"); @@ -201,7 +201,7 @@ void addListMCP23017GPIOLinkBox(String& html, const String& input_id, const Stri } html += name; if (ConfigESP->getGpioMCP23017(nr, function) != OFF_GPIO) { - html += WebServer->SuplaIconEdit(); + html += PGMT(ICON_EDIT); html += F(""); } html += F(""); @@ -234,7 +234,7 @@ void addListGPIOLinkBox(String& html, const String& input_id, const String& name } html += name; if (ConfigESP->getGpio(_nr, function) != OFF_GPIO) { - html += WebServer->SuplaIconEdit(); + html += PGMT(ICON_EDIT); html += F(""); } html += F(""); diff --git a/src/SuplaCommonPROGMEM.h b/src/SuplaCommonPROGMEM.h index 6ce2578e..325f5c40 100644 --- a/src/SuplaCommonPROGMEM.h +++ b/src/SuplaCommonPROGMEM.h @@ -59,6 +59,15 @@ const char HTTP_FAVICON[] PROGMEM = ""; +const char ICON_EDIT[] PROGMEM = + ""; + const char GPIO0[] PROGMEM = "GPIO0-D3"; const char GPIO1[] PROGMEM = "GPIO1-TX"; const char GPIO2[] PROGMEM = "GPIO2-D4"; diff --git a/src/SuplaHTTPUpdateServer.cpp b/src/SuplaHTTPUpdateServer.cpp index 593fd9cc..501242e5 100644 --- a/src/SuplaHTTPUpdateServer.cpp +++ b/src/SuplaHTTPUpdateServer.cpp @@ -17,7 +17,7 @@ static const char serverIndex[] PROGMEM = - +
Flash Size: {f}kB
@@ -29,9 +29,9 @@ static const char serverIndex[] PROGMEM = )"; -static const char successResponse[] PROGMEM = "Update Success! Rebooting..."; +static const char successResponse[] PROGMEM = "Update Success! Rebooting..."; static const char twoStepResponse[] PROGMEM = - "WARNING only use 2-step OTA update. Use GUI-GenericUpdater.bin"; + "WARNING only use 2-step OTA update. Use GUI-GenericUpdater.bin"; ESP8266HTTPUpdateServer::ESP8266HTTPUpdateServer(bool serial_debug) { _serial_output = serial_debug; @@ -144,30 +144,27 @@ void ESP8266HTTPUpdateServer::handleFirmwareUp() { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); } - WebServer->sendContent(suplaWebPageUpddate()); + suplaWebPageUpddate(); } -String ESP8266HTTPUpdateServer::suplaWebPageUpddate() { - String content = ""; - content += SuplaJavaScript(); - content += F("
"); - content += F("

"); - content += S_SOFTWARE_UPDATE; - content += F("

"); - content += F("
"); - content += F("
"); - content += F(""); - content += F("
"); - content += F("
"); - content += F("

"); - - return content; +void ESP8266HTTPUpdateServer::suplaWebPageUpddate() { + webContentBuffer += SuplaJavaScript(); + webContentBuffer += F("
"); + webContentBuffer += F("

"); + webContentBuffer += S_SOFTWARE_UPDATE; + webContentBuffer += F("

"); + webContentBuffer += F("
"); + webContentBuffer += F("
"); + webContentBuffer += F(""); + webContentBuffer += F("
"); + webContentBuffer += F("
"); + webContentBuffer += F("

"); } void ESP8266HTTPUpdateServer::_setUpdaterError() { diff --git a/src/SuplaHTTPUpdateServer.h b/src/SuplaHTTPUpdateServer.h index 5d76d0ef..0466f93e 100644 --- a/src/SuplaHTTPUpdateServer.h +++ b/src/SuplaHTTPUpdateServer.h @@ -46,7 +46,7 @@ class ESP8266HTTPUpdateServer String _updaterError; void handleFirmwareUp(); - String suplaWebPageUpddate(); + void suplaWebPageUpddate(); }; #endif diff --git a/src/SuplaWebPageConfig.cpp b/src/SuplaWebPageConfig.cpp index 1a28163b..def78b14 100644 --- a/src/SuplaWebPageConfig.cpp +++ b/src/SuplaWebPageConfig.cpp @@ -25,7 +25,7 @@ void SuplaWebPageConfig::handleConfig() { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); } - WebServer->sendContent(supla_webpage_config(0)); + supla_webpage_config(0); } void SuplaWebPageConfig::handleConfigSave() { @@ -35,7 +35,7 @@ void SuplaWebPageConfig::handleConfigSave() { } if (!WebServer->saveGPIO(INPUT_CFG_LED_GPIO, FUNCTION_CFG_LED)) { - WebServer->sendContent(supla_webpage_config(6)); + supla_webpage_config(6); return; } @@ -44,7 +44,7 @@ void SuplaWebPageConfig::handleConfigSave() { ConfigManager->setElement(key, LEVEL, WebServer->httpServer.arg(input).toInt()); if (!WebServer->saveGPIO(INPUT_CFG_BTN_GPIO, FUNCTION_CFG_BUTTON)) { - WebServer->sendContent(supla_webpage_config(6)); + supla_webpage_config(6); return; } @@ -55,38 +55,36 @@ void SuplaWebPageConfig::handleConfigSave() { switch (ConfigManager->save()) { case E_CONFIG_OK: // Serial.println(F("E_CONFIG_OK: Config save")); - WebServer->sendContent(supla_webpage_config(1)); + supla_webpage_config(1); break; case E_CONFIG_FILE_OPEN: // Serial.println(F("E_CONFIG_FILE_OPEN: Couldn't open file")); - WebServer->sendContent(supla_webpage_config(2)); + supla_webpage_config(2); break; } } -String SuplaWebPageConfig::supla_webpage_config(int save) { +void SuplaWebPageConfig::supla_webpage_config(int save) { uint8_t selected, suported; - String page = ""; - page += SuplaSaveResult(save); - page += SuplaJavaScript(PATH_CONFIG); + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(PATH_CONFIG); - addForm(page, F("post"), PATH_SAVE_CONFIG); - addFormHeader(page, S_GPIO_SETTINGS_FOR_CONFIG); - addListGPIOBox(page, INPUT_CFG_LED_GPIO, F("LED"), FUNCTION_CFG_LED); + addForm(webContentBuffer, F("post"), PATH_SAVE_CONFIG); + addFormHeader(webContentBuffer, S_GPIO_SETTINGS_FOR_CONFIG); + addListGPIOBox(webContentBuffer, INPUT_CFG_LED_GPIO, F("LED"), FUNCTION_CFG_LED); selected = ConfigESP->getLevel(FUNCTION_CFG_LED); - addListBox(page, INPUT_CFG_LED_LEVEL, S_STATE_CONTROL, LEVEL_P, 2, selected); - addListGPIOBox(page, INPUT_CFG_BTN_GPIO, S_BUTTON, FUNCTION_CFG_BUTTON); + addListBox(webContentBuffer, INPUT_CFG_LED_LEVEL, S_STATE_CONTROL, LEVEL_P, 2, selected); + addListGPIOBox(webContentBuffer, INPUT_CFG_BTN_GPIO, S_BUTTON, FUNCTION_CFG_BUTTON); selected = ConfigManager->get(KEY_CFG_MODE)->getValueInt(); - addListBox(page, INPUT_CFG_MODE, S_CFG_MODE, CFG_MODE_P, 2, selected); + addListBox(webContentBuffer, INPUT_CFG_MODE, S_CFG_MODE, CFG_MODE_P, 2, selected); - addFormHeaderEnd(page); - addButtonSubmit(page, S_SAVE); - addFormEnd(page); + addFormHeaderEnd(webContentBuffer); + addButtonSubmit(webContentBuffer, S_SAVE); + addFormEnd(webContentBuffer); - addButton(page, S_RETURN, PATH_DEVICE_SETTINGS); - - return page; + addButton(webContentBuffer, S_RETURN, PATH_DEVICE_SETTINGS); + WebServer->sendContent(); } diff --git a/src/SuplaWebPageConfig.h b/src/SuplaWebPageConfig.h index 7733bb33..c2d55089 100644 --- a/src/SuplaWebPageConfig.h +++ b/src/SuplaWebPageConfig.h @@ -19,7 +19,7 @@ class SuplaWebPageConfig { void handleConfigSave(); private: - String supla_webpage_config(int save); + void supla_webpage_config(int save); }; extern SuplaWebPageConfig *WebPageConfig; diff --git a/src/SuplaWebPageSensor.cpp b/src/SuplaWebPageSensor.cpp index 43cbfcb6..112347b0 100644 --- a/src/SuplaWebPageSensor.cpp +++ b/src/SuplaWebPageSensor.cpp @@ -89,7 +89,7 @@ void SuplaWebPageSensor::handleSearchDS() { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); } - WebServer->sendContent(supla_webpage_search(0)); + supla_webpage_search(0); } void SuplaWebPageSensor::handleDSSave() { @@ -115,18 +115,17 @@ void SuplaWebPageSensor::handleDSSave() { switch (ConfigManager->save()) { case E_CONFIG_OK: // Serial.println(F("E_CONFIG_OK: Config save")); - WebServer->sendContent(supla_webpage_search(1)); + supla_webpage_search(1); // WebServer->rebootESP(); break; case E_CONFIG_FILE_OPEN: // Serial.println(F("E_CONFIG_FILE_OPEN: Couldn't open file")); - WebServer->sendContent(supla_webpage_search(2)); + supla_webpage_search(2); break; } } -String SuplaWebPageSensor::supla_webpage_search(int save) { - String content = ""; +void SuplaWebPageSensor::supla_webpage_search(int save) { uint8_t count = 0; uint8_t pin = ConfigESP->getGpio(FUNCTION_DS18B20); @@ -136,26 +135,26 @@ String SuplaWebPageSensor::supla_webpage_search(int save) { char strAddr[64]; uint8_t i; - content += SuplaSaveResult(save); - content += SuplaJavaScript(PATH_MULTI_DS); - content += F("
"); + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(PATH_MULTI_DS); + webContentBuffer += F("
"); if (ConfigESP->getGpio(FUNCTION_DS18B20) < OFF_GPIO) { - content += F("
"); - this->showDS18B20(content); - content += F("
"); - content += F("
"); - } - content += F("
"); - content += F("
"); - content += F("

"); - content += S_FOUND; - content += F(" DS18b20

"); + webContentBuffer += F(""); + this->showDS18B20(); + webContentBuffer += F(""); + webContentBuffer += F("
"); + } + webContentBuffer += F("
"); + webContentBuffer += F("
"); + webContentBuffer += F("

"); + webContentBuffer += S_FOUND; + webContentBuffer += F(" DS18b20

"); sensors.setOneWire(&ow); sensors.begin(); if (sensors.isParasitePowerMode()) { @@ -174,15 +173,15 @@ String SuplaWebPageSensor::supla_webpage_search(int save) { address[7]); supla_log(LOG_DEBUG, "Index %d - address %s", i, strAddr); - content += F(""); + webContentBuffer += F("' value='"); + webContentBuffer += String(strAddr); + webContentBuffer += F("' maxlength="); + webContentBuffer += MAX_DS18B20_ADDRESS_HEX; + webContentBuffer += F(" readonly>
"); - content += F("
"); - content += F(""); - content += F("

"); - content += F("

"); - - return content; + webContentBuffer += F(""); + } + webContentBuffer += F("
"); + webContentBuffer += F(""); + webContentBuffer += F(""); + webContentBuffer += F("

"); + webContentBuffer += F("

"); + WebServer->sendContent(); } -void SuplaWebPageSensor::showDS18B20(String &content, bool readonly) { +void SuplaWebPageSensor::showDS18B20(bool readonly) { if (ConfigESP->getGpio(FUNCTION_DS18B20) != OFF_GPIO) { - content += F("
"); - content += F("

"); - content += S_TEMPERATURE; - content += F("

"); + webContentBuffer += F("
"); + webContentBuffer += F("

"); + webContentBuffer += S_TEMPERATURE; + webContentBuffer += F("

"); for (uint8_t i = 0; i < ConfigManager->get(KEY_MULTI_MAX_DS18B20)->getValueInt(); i++) { double temp = Supla::GUI::sensorDS[i]->getValue(); - content += F(""); - content += F(""); + webContentBuffer += F(""); + webContentBuffer += F(" °C "); + webContentBuffer += F(""); delay(0); } - content += F("
"); + webContentBuffer += F("
"); } } #endif @@ -264,7 +262,7 @@ void SuplaWebPageSensor::handle1Wire() { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); } - WebServer->sendContent(supla_webpage_1wire(0)); + supla_webpage_1wire(0); } void SuplaWebPageSensor::handle1WireSave() { @@ -279,7 +277,7 @@ void SuplaWebPageSensor::handle1WireSave() { last_value = ConfigManager->get(KEY_MAX_DHT11)->getValueInt(); for (nr = 1; nr <= last_value; nr++) { if (!WebServer->saveGPIO(INPUT_DHT11_GPIO, FUNCTION_DHT11, nr, INPUT_MAX_DHT11)) { - WebServer->sendContent(supla_webpage_1wire(6)); + supla_webpage_1wire(6); return; } } @@ -293,7 +291,7 @@ void SuplaWebPageSensor::handle1WireSave() { last_value = ConfigManager->get(KEY_MAX_DHT22)->getValueInt(); for (nr = 1; nr <= last_value; nr++) { if (!WebServer->saveGPIO(INPUT_DHT22_GPIO, FUNCTION_DHT22, nr, INPUT_MAX_DHT22)) { - WebServer->sendContent(supla_webpage_1wire(6)); + supla_webpage_1wire(6); return; } } @@ -305,7 +303,7 @@ void SuplaWebPageSensor::handle1WireSave() { #ifdef SUPLA_DS18B20 if (!WebServer->saveGPIO(INPUT_MULTI_DS_GPIO, FUNCTION_DS18B20)) { - WebServer->sendContent(supla_webpage_1wire(6)); + supla_webpage_1wire(6); return; } if (strcmp(WebServer->httpServer.arg(INPUT_MAX_DS18B20).c_str(), "") > 0) { @@ -315,78 +313,78 @@ void SuplaWebPageSensor::handle1WireSave() { #ifdef SUPLA_SI7021_SONOFF if (!WebServer->saveGPIO(INPUT_SI7021_SONOFF, FUNCTION_SI7021_SONOFF)) { - WebServer->sendContent(supla_webpage_1wire(6)); + supla_webpage_1wire(6); return; } #endif switch (ConfigManager->save()) { case E_CONFIG_OK: - WebServer->sendContent(supla_webpage_1wire(1)); + supla_webpage_1wire(1); break; case E_CONFIG_FILE_OPEN: // Serial.println(F("E_CONFIG_FILE_OPEN: Couldn't open file")); - WebServer->sendContent(supla_webpage_1wire(2)); + supla_webpage_1wire(2); break; } } -String SuplaWebPageSensor::supla_webpage_1wire(int save) { +void SuplaWebPageSensor::supla_webpage_1wire(int save) { uint8_t nr, max; - String page; - page += SuplaSaveResult(save); - page += SuplaJavaScript(PATH_1WIRE); - page += F("
"); + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(PATH_1WIRE); + webContentBuffer += F(""); #ifdef SUPLA_DHT11 - addFormHeader(page, String(S_GPIO_SETTINGS_FOR) + " DHT11"); - addNumberBox(page, INPUT_MAX_DHT11, S_QUANTITY, KEY_MAX_DHT11, ConfigESP->countFreeGpio(FUNCTION_DHT11)); + addFormHeader(webContentBuffer, String(S_GPIO_SETTINGS_FOR) + " DHT11"); + addNumberBox(webContentBuffer, INPUT_MAX_DHT11, S_QUANTITY, KEY_MAX_DHT11, ConfigESP->countFreeGpio(FUNCTION_DHT11)); for (nr = 1; nr <= ConfigManager->get(KEY_MAX_DHT11)->getValueInt(); nr++) { - addListGPIOBox(page, INPUT_DHT11_GPIO, "DHT11", FUNCTION_DHT11, nr); + addListGPIOBox(webContentBuffer, INPUT_DHT11_GPIO, "DHT11", FUNCTION_DHT11, nr); } - addFormHeaderEnd(page); + addFormHeaderEnd(webContentBuffer); #endif #ifdef SUPLA_DHT22 - addFormHeader(page, String(S_GPIO_SETTINGS_FOR) + " DHT22"); - addNumberBox(page, INPUT_MAX_DHT22, S_QUANTITY, KEY_MAX_DHT22, ConfigESP->countFreeGpio(FUNCTION_DHT22)); + addFormHeader(webContentBuffer, String(S_GPIO_SETTINGS_FOR) + " DHT22"); + addNumberBox(webContentBuffer, INPUT_MAX_DHT22, S_QUANTITY, KEY_MAX_DHT22, ConfigESP->countFreeGpio(FUNCTION_DHT22)); for (nr = 1; nr <= ConfigManager->get(KEY_MAX_DHT22)->getValueInt(); nr++) { - addListGPIOBox(page, INPUT_DHT22_GPIO, "DHT22", FUNCTION_DHT22, nr); + addListGPIOBox(webContentBuffer, INPUT_DHT22_GPIO, "DHT22", FUNCTION_DHT22, nr); } - addFormHeaderEnd(page); + addFormHeaderEnd(webContentBuffer); #endif #ifdef SUPLA_SI7021_SONOFF - addFormHeader(page, String(S_GPIO_SETTINGS_FOR) + " Si7021 Sonoff"); - addListGPIOBox(page, INPUT_SI7021_SONOFF, "Si7021 Sonoff", FUNCTION_SI7021_SONOFF); - addFormHeaderEnd(page); + addFormHeader(webContentBuffer, String(S_GPIO_SETTINGS_FOR) + " Si7021 Sonoff"); + addListGPIOBox(webContentBuffer, INPUT_SI7021_SONOFF, "Si7021 Sonoff", FUNCTION_SI7021_SONOFF); + addFormHeaderEnd(webContentBuffer); #endif #ifdef SUPLA_DS18B20 - addFormHeader(page, String(S_GPIO_SETTINGS_FOR) + " Multi DS18B20"); - addNumberBox(page, INPUT_MAX_DS18B20, S_QUANTITY, KEY_MULTI_MAX_DS18B20, MAX_DS18B20); + addFormHeader(webContentBuffer, String(S_GPIO_SETTINGS_FOR) + " Multi DS18B20"); + addNumberBox(webContentBuffer, INPUT_MAX_DS18B20, S_QUANTITY, KEY_MULTI_MAX_DS18B20, MAX_DS18B20); if (ConfigManager->get(KEY_MULTI_MAX_DS18B20)->getValueInt() > 1) { - addListGPIOLinkBox(page, INPUT_MULTI_DS_GPIO, "MULTI DS18B20", FUNCTION_DS18B20, PATH_MULTI_DS); + addListGPIOLinkBox(webContentBuffer, INPUT_MULTI_DS_GPIO, "MULTI DS18B20", FUNCTION_DS18B20, PATH_MULTI_DS); } else { - addListGPIOBox(page, INPUT_MULTI_DS_GPIO, "MULTI DS18B20", FUNCTION_DS18B20); - } - addFormHeaderEnd(page); -#endif - - page += F("
"); - page += F("
"); - page += F("

"); - return page; + addListGPIOBox(webContentBuffer, INPUT_MULTI_DS_GPIO, "MULTI DS18B20", FUNCTION_DS18B20); + } + addFormHeaderEnd(webContentBuffer); +#endif + + webContentBuffer += F(""); + webContentBuffer += F("
"); + webContentBuffer += F("

"); + + WebServer->sendContent(); } #endif @@ -396,7 +394,7 @@ void SuplaWebPageSensor::handlei2c() { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); } - WebServer->sendContent(supla_webpage_i2c(0)); + supla_webpage_i2c(0); } void SuplaWebPageSensor::handlei2cSave() { @@ -409,11 +407,11 @@ void SuplaWebPageSensor::handlei2cSave() { uint8_t key; if (!WebServer->saveGPIO(INPUT_SDA_GPIO, FUNCTION_SDA)) { - WebServer->sendContent(supla_webpage_i2c(6)); + supla_webpage_i2c(6); return; } if (!WebServer->saveGPIO(INPUT_SCL_GPIO, FUNCTION_SCL)) { - WebServer->sendContent(supla_webpage_i2c(6)); + supla_webpage_i2c(6); return; } @@ -478,54 +476,54 @@ void SuplaWebPageSensor::handlei2cSave() { switch (ConfigManager->save()) { case E_CONFIG_OK: - WebServer->sendContent(supla_webpage_i2c(1)); + supla_webpage_i2c(1); break; case E_CONFIG_FILE_OPEN: // Serial.println(F("E_CONFIG_FILE_OPEN: Couldn't open file")); - WebServer->sendContent(supla_webpage_i2c(2)); + supla_webpage_i2c(2); break; } } -String SuplaWebPageSensor::supla_webpage_i2c(int save) { +void SuplaWebPageSensor::supla_webpage_i2c(int save) { uint8_t selected; - String page = ""; - page += SuplaSaveResult(save); - page += SuplaJavaScript(PATH_I2C); - addForm(page, F("post"), PATH_SAVE_I2C); - addFormHeader(page, String(S_GPIO_SETTINGS_FOR) + F(" i2c")); - addListGPIOBox(page, INPUT_SDA_GPIO, F("SDA"), FUNCTION_SDA); - addListGPIOBox(page, INPUT_SCL_GPIO, F("SCL"), FUNCTION_SCL); - addFormHeaderEnd(page); + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(PATH_I2C); + + addForm(webContentBuffer, F("post"), PATH_SAVE_I2C); + addFormHeader(webContentBuffer, String(S_GPIO_SETTINGS_FOR) + F(" i2c")); + addListGPIOBox(webContentBuffer, INPUT_SDA_GPIO, F("SDA"), FUNCTION_SDA); + addListGPIOBox(webContentBuffer, INPUT_SCL_GPIO, F("SCL"), FUNCTION_SCL); + addFormHeaderEnd(webContentBuffer); if (ConfigESP->getGpio(FUNCTION_SDA) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_SCL) != OFF_GPIO) { #ifdef SUPLA_BME280 selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_BME280).toInt(); - addFormHeader(page); - addListBox(page, INPUT_BME280, F("BME280 adres"), BME280_P, 4, selected); - addNumberBox(page, INPUT_ALTITUDE_BME280, S_ALTITUDE_ABOVE_SEA_LEVEL, KEY_ALTITUDE_BME280, 1500); - addFormHeaderEnd(page); + addFormHeader(webContentBuffer); + addListBox(webContentBuffer, INPUT_BME280, F("BME280 adres"), BME280_P, 4, selected); + addNumberBox(webContentBuffer, INPUT_ALTITUDE_BME280, S_ALTITUDE_ABOVE_SEA_LEVEL, KEY_ALTITUDE_BME280, 1500); + addFormHeaderEnd(webContentBuffer); #endif #ifdef SUPLA_SHT3x selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_SHT3x).toInt(); - addFormHeader(page); - addListBox(page, INPUT_SHT3x, F("SHT3x"), SHT3x_P, 4, selected); - addFormHeaderEnd(page); + addFormHeader(webContentBuffer); + addListBox(webContentBuffer, INPUT_SHT3x, F("SHT3x"), SHT3x_P, 4, selected); + addFormHeaderEnd(webContentBuffer); #endif #ifdef SUPLA_SI7021 selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_SI7021).toInt(); - addFormHeader(page); - addListBox(page, INPUT_SI7021, F("SI7021"), STATE_P, 2, selected); - addFormHeaderEnd(page); + addFormHeader(webContentBuffer); + addListBox(webContentBuffer, INPUT_SI7021, F("SI7021"), STATE_P, 2, selected); + addFormHeaderEnd(webContentBuffer); #endif #ifdef SUPLA_OLED selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt(); - addFormHeader(page); - addListBox(page, INPUT_OLED, F("OLED"), OLED_P, 4, selected); + addFormHeader(webContentBuffer); + addListBox(webContentBuffer, INPUT_OLED, F("OLED"), OLED_P, 4, selected); if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt()) { String name, sensorName, input; @@ -535,26 +533,25 @@ String SuplaWebPageSensor::supla_webpage_i2c(int save) { input += i; name = F("Ekran "); name += i + 1; - addTextBox(page, input, name, sensorName, 0, MAX_DS18B20_NAME, false); + addTextBox(webContentBuffer, input, name, sensorName, 0, MAX_DS18B20_NAME, false); } } - addFormHeaderEnd(page); + addFormHeaderEnd(webContentBuffer); #endif #ifdef SUPLA_MCP23017 selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MCP23017).toInt(); - addFormHeader(page); - addListBox(page, INPUT_MCP23017, F("MCP23017"), STATE_P, 2, selected); - addFormHeaderEnd(page); + addFormHeader(webContentBuffer); + addListBox(webContentBuffer, INPUT_MCP23017, F("MCP23017"), STATE_P, 2, selected); + addFormHeaderEnd(webContentBuffer); #endif } - addButtonSubmit(page, S_SAVE); - addFormEnd(page); - - addButton(page, S_RETURN, PATH_DEVICE_SETTINGS); + addButtonSubmit(webContentBuffer, S_SAVE); + addFormEnd(webContentBuffer); - return page; + addButton(webContentBuffer, S_RETURN, PATH_DEVICE_SETTINGS); + WebServer->sendContent(); } #endif @@ -564,7 +561,7 @@ void SuplaWebPageSensor::handleSpi() { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); } - WebServer->sendContent(supla_webpage_spi(0)); + supla_webpage_spi(0); } void SuplaWebPageSensor::handleSpiSave() { @@ -578,15 +575,15 @@ void SuplaWebPageSensor::handleSpiSave() { #if defined(SUPLA_MAX6675) if (!WebServer->saveGPIO(INPUT_CLK_GPIO, FUNCTION_CLK)) { - WebServer->sendContent(supla_webpage_spi(6)); + supla_webpage_spi(6); return; } if (!WebServer->saveGPIO(INPUT_CS_GPIO, FUNCTION_CS)) { - WebServer->sendContent(supla_webpage_spi(6)); + supla_webpage_spi(6); return; } if (!WebServer->saveGPIO(INPUT_D0_GPIO, FUNCTION_D0)) { - WebServer->sendContent(supla_webpage_spi(6)); + supla_webpage_spi(6); return; } #endif @@ -601,47 +598,47 @@ void SuplaWebPageSensor::handleSpiSave() { switch (ConfigManager->save()) { case E_CONFIG_OK: - WebServer->sendContent(supla_webpage_spi(1)); + supla_webpage_spi(1); break; case E_CONFIG_FILE_OPEN: // Serial.println(F("E_CONFIG_FILE_OPEN: Couldn't open file")); - WebServer->sendContent(supla_webpage_spi(2)); + supla_webpage_spi(2); break; } } -String SuplaWebPageSensor::supla_webpage_spi(int save) { +void SuplaWebPageSensor::supla_webpage_spi(int save) { uint8_t nr, suported, selected; - String page; - page += SuplaSaveResult(save); - page += SuplaJavaScript(PATH_SPI); - page += F("
"); + + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(PATH_SPI); + webContentBuffer += F(""); #if defined(SUPLA_MAX6675) - addFormHeader(page, String(S_GPIO_SETTINGS_FOR) + " SPI"); - addListGPIOBox(page, INPUT_CLK_GPIO, "CLK", FUNCTION_CLK); - addListGPIOBox(page, INPUT_CS_GPIO, "CS", FUNCTION_CS); - addListGPIOBox(page, INPUT_D0_GPIO, "D0", FUNCTION_D0); + addFormHeader(webContentBuffer, String(S_GPIO_SETTINGS_FOR) + " SPI"); + addListGPIOBox(webContentBuffer, INPUT_CLK_GPIO, "CLK", FUNCTION_CLK); + addListGPIOBox(webContentBuffer, INPUT_CS_GPIO, "CS", FUNCTION_CS); + addListGPIOBox(webContentBuffer, INPUT_D0_GPIO, "D0", FUNCTION_D0); if (ConfigESP->getGpio(FUNCTION_CLK) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_CS) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_D0) != OFF_GPIO) { selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_MAX6675).toInt(); - addListBox(page, INPUT_MAX6675, "MAX6675", STATE_P, 2, selected); - } - addFormHeaderEnd(page); -#endif - page += F("
"); - page += F("
"); - page += F("

"); - return page; + addListBox(webContentBuffer, INPUT_MAX6675, "MAX6675", STATE_P, 2, selected); + } + addFormHeaderEnd(webContentBuffer); +#endif + webContentBuffer += F(""); + webContentBuffer += F("
"); + webContentBuffer += F("

"); + WebServer->sendContent(); } #endif @@ -651,7 +648,7 @@ void SuplaWebPageSensor::handleOther() { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); } - WebServer->sendContent(supla_webpage_other(0)); + supla_webpage_other(0); } void SuplaWebPageSensor::handleOtherSave() { @@ -664,11 +661,11 @@ void SuplaWebPageSensor::handleOtherSave() { #ifdef SUPLA_HC_SR04 if (!WebServer->saveGPIO(INPUT_TRIG_GPIO, FUNCTION_TRIG)) { - WebServer->sendContent(supla_webpage_other(6)); + supla_webpage_other(6); return; } if (!WebServer->saveGPIO(INPUT_ECHO_GPIO, FUNCTION_ECHO)) { - WebServer->sendContent(supla_webpage_other(6)); + supla_webpage_other(6); return; } #endif @@ -679,7 +676,7 @@ void SuplaWebPageSensor::handleOtherSave() { last_value = ConfigManager->get(KEY_MAX_IMPULSE_COUNTER)->getValueInt(); for (nr = 1; nr <= last_value; nr++) { if (!WebServer->saveGPIO(INPUT_IMPULSE_COUNTER_GPIO, FUNCTION_IMPULSE_COUNTER, nr, INPUT_MAX_IMPULSE_COUNTER)) { - WebServer->sendContent(supla_webpage_other(6)); + supla_webpage_other(6); return; } } @@ -691,69 +688,68 @@ void SuplaWebPageSensor::handleOtherSave() { #ifdef SUPLA_HLW8012 if (!WebServer->saveGPIO(INPUT_CF, FUNCTION_CF)) { - WebServer->sendContent(supla_webpage_other(6)); + supla_webpage_other(6); return; } if (!WebServer->saveGPIO(INPUT_CF1, FUNCTION_CF1)) { - WebServer->sendContent(supla_webpage_other(6)); + supla_webpage_other(6); return; } if (!WebServer->saveGPIO(INPUT_SEL, FUNCTION_SEL)) { - WebServer->sendContent(supla_webpage_other(6)); + supla_webpage_other(6); return; } #endif switch (ConfigManager->save()) { case E_CONFIG_OK: - WebServer->sendContent(supla_webpage_other(1)); + supla_webpage_other(1); break; case E_CONFIG_FILE_OPEN: // Serial.println(F("E_CONFIG_FILE_OPEN: Couldn't open file")); - WebServer->sendContent(supla_webpage_other(2)); + supla_webpage_other(2); break; } } -String SuplaWebPageSensor::supla_webpage_other(int save) { +void SuplaWebPageSensor::supla_webpage_other(int save) { uint8_t nr, suported, selected; - String page = ""; - page += SuplaSaveResult(save); - page += SuplaJavaScript(PATH_OTHER); + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(PATH_OTHER); - addForm(page, F("post"), PATH_SAVE_OTHER); + addForm(webContentBuffer, F("post"), PATH_SAVE_OTHER); #ifdef SUPLA_HC_SR04 - addFormHeader(page, String(S_GPIO_SETTINGS_FOR) + F(" HC-SR04")); - addListGPIOBox(page, INPUT_TRIG_GPIO, F("TRIG"), FUNCTION_TRIG); - addListGPIOBox(page, INPUT_ECHO_GPIO, F("ECHO"), FUNCTION_ECHO); - addFormHeaderEnd(page); + addFormHeader(webContentBuffer, String(S_GPIO_SETTINGS_FOR) + F(" HC-SR04")); + addListGPIOBox(webContentBuffer, INPUT_TRIG_GPIO, F("TRIG"), FUNCTION_TRIG); + addListGPIOBox(webContentBuffer, INPUT_ECHO_GPIO, F("ECHO"), FUNCTION_ECHO); + addFormHeaderEnd(webContentBuffer); #endif #ifdef SUPLA_IMPULSE_COUNTER - addFormHeader(page, String(S_GPIO_SETTINGS_FOR) + " " + S_IMPULSE_COUNTER); - addNumberBox(page, INPUT_MAX_IMPULSE_COUNTER, S_QUANTITY, KEY_MAX_IMPULSE_COUNTER, ConfigESP->countFreeGpio(FUNCTION_IMPULSE_COUNTER)); + addFormHeader(webContentBuffer, String(S_GPIO_SETTINGS_FOR) + " " + S_IMPULSE_COUNTER); + addNumberBox(webContentBuffer, INPUT_MAX_IMPULSE_COUNTER, S_QUANTITY, KEY_MAX_IMPULSE_COUNTER, ConfigESP->countFreeGpio(FUNCTION_IMPULSE_COUNTER)); for (nr = 1; nr <= ConfigManager->get(KEY_MAX_IMPULSE_COUNTER)->getValueInt(); nr++) { - addListGPIOLinkBox(page, INPUT_IMPULSE_COUNTER_GPIO, F("IC GPIO"), FUNCTION_IMPULSE_COUNTER, PATH_IMPULSE_COUNTER_SET, nr); + addListGPIOLinkBox(webContentBuffer, INPUT_IMPULSE_COUNTER_GPIO, F("IC GPIO"), FUNCTION_IMPULSE_COUNTER, PATH_IMPULSE_COUNTER_SET, nr); } - addFormHeaderEnd(page); + addFormHeaderEnd(webContentBuffer); #endif #ifdef SUPLA_HLW8012 - addFormHeader(page, String(S_GPIO_SETTINGS_FOR) + F(" HLW8012")); - addListGPIOBox(page, INPUT_CF, F("CF"), FUNCTION_CF); - addListGPIOBox(page, INPUT_CF1, F("CF1"), FUNCTION_CF1); - addListGPIOBox(page, INPUT_SEL, F("SELi"), FUNCTION_SEL); + addFormHeader(webContentBuffer, String(S_GPIO_SETTINGS_FOR) + F(" HLW8012")); + addListGPIOBox(webContentBuffer, INPUT_CF, F("CF"), FUNCTION_CF); + addListGPIOBox(webContentBuffer, INPUT_CF1, F("CF1"), FUNCTION_CF1); + addListGPIOBox(webContentBuffer, INPUT_SEL, F("SELi"), FUNCTION_SEL); if (ConfigESP->getGpio(FUNCTION_CF) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_CF1) != OFF_GPIO && ConfigESP->getGpio(FUNCTION_SEL) != OFF_GPIO) { - addLinkBox(page, F("Kalibracja"), PATH_HLW8012_CALIBRATE); + addLinkBox(webContentBuffer, F("Kalibracja"), PATH_HLW8012_CALIBRATE); } - addFormHeaderEnd(page); + addFormHeaderEnd(webContentBuffer); #endif - addButtonSubmit(page, S_SAVE); - addFormEnd(page); - addButton(page, S_RETURN, PATH_DEVICE_SETTINGS); - return page; + addButtonSubmit(webContentBuffer, S_SAVE); + addFormEnd(webContentBuffer); + addButton(webContentBuffer, S_RETURN, PATH_DEVICE_SETTINGS); + WebServer->sendContent(); } #endif @@ -763,7 +759,7 @@ void SuplaWebPageSensor::handleImpulseCounterSet() { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); } - WebServer->sendContent(supla_impulse_counter_set(0)); + supla_impulse_counter_set(0); } void SuplaWebPageSensor::handleImpulseCounterSaveSet() { @@ -797,17 +793,17 @@ void SuplaWebPageSensor::handleImpulseCounterSaveSet() { switch (ConfigManager->save()) { case E_CONFIG_OK: // Serial.println(F("E_CONFIG_OK: Dane zapisane")); - WebServer->sendContent(supla_webpage_other(1)); + supla_webpage_other(1); break; case E_CONFIG_FILE_OPEN: // Serial.println(F("E_CONFIG_FILE_OPEN: Couldn't open file")); - WebServer->sendContent(supla_webpage_other(2)); + supla_webpage_other(2); break; } } -String SuplaWebPageSensor::supla_impulse_counter_set(int save) { +void SuplaWebPageSensor::supla_impulse_counter_set(int save) { String readUrl, nr; uint8_t place, selected, suported; @@ -818,87 +814,86 @@ String SuplaWebPageSensor::supla_impulse_counter_set(int save) { place = readUrl.indexOf(path); nr = readUrl.substring(place + path.length(), place + path.length() + 3); - String page = ""; - page += SuplaSaveResult(save); - page += SuplaJavaScript(PATH_OTHER); + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(PATH_OTHER); uint8_t relays = ConfigManager->get(KEY_MAX_IMPULSE_COUNTER)->getValueInt(); if (nr.toInt() <= relays && ConfigESP->getGpio(nr.toInt(), FUNCTION_IMPULSE_COUNTER) != OFF_GPIO) { - page += F("

"); - page += S_IMPULSE_COUNTER_SETTINGS_NR; - page += F(" "); - page += nr; - page += F("

"); - page += F(""); selected = ConfigESP->getMemory(nr.toInt(), FUNCTION_IMPULSE_COUNTER); for (suported = 0; suported < 2; suported++) { - page += F(""); - page += F(""); + webContentBuffer += F(""); - addNumberBox(page, INPUT_IMPULSE_COUNTER_DEBOUNCE_TIMEOUT, S_IMPULSE_COUNTER_DEBOUNCE_TIMEOUT, KEY_IMPULSE_COUNTER_DEBOUNCE_TIMEOUT, 99999999); - page += F(""); + webContentBuffer += count; + webContentBuffer += F("'>"); - page += F("
"); + webContentBuffer += F("
"); } else { - page += F("

"); - page += S_NO_IMPULSE_COUNTER_NR; - page += F(" "); - page += nr; - page += F("

"); - } - page += F("
"); - page += F("

"); - - return page; + webContentBuffer += F("

"); + webContentBuffer += S_NO_IMPULSE_COUNTER_NR; + webContentBuffer += F(" "); + webContentBuffer += nr; + webContentBuffer += F("

"); + } + webContentBuffer += F("
"); + webContentBuffer += F("

"); + WebServer->sendContent(); } #endif @@ -908,7 +903,7 @@ void SuplaWebPageSensor::handleHLW8012Calibrate() { if (!WebServer->httpServer.authenticate(WebServer->www_username, WebServer->www_password)) return WebServer->httpServer.requestAuthentication(); } - WebServer->sendContent(suplaWebpageHLW8012Calibrate(0)); + suplaWebpageHLW8012Calibrate(0); } void SuplaWebPageSensor::handleHLW8012CalibrateSave() { @@ -930,38 +925,37 @@ void SuplaWebPageSensor::handleHLW8012CalibrateSave() { if (calibPower && calibVoltage) { Supla::GUI::counterHLW8012->calibrate(calibPower, calibVoltage); - WebServer->sendContent(suplaWebpageHLW8012Calibrate(1)); + suplaWebpageHLW8012Calibrate(1); } else { - WebServer->sendContent(suplaWebpageHLW8012Calibrate(6)); + suplaWebpageHLW8012Calibrate(6); } } -String SuplaWebPageSensor::suplaWebpageHLW8012Calibrate(uint8_t save) { - String page; - page += SuplaSaveResult(save); - page += SuplaJavaScript(PATH_HLW8012_CALIBRATE); - - addFormHeader(page); - page += F("

Current Multi: "); - page += Supla::GUI::counterHLW8012->getCurrentMultiplier(); - page += F("
Voltage Multi: "); - page += Supla::GUI::counterHLW8012->getVoltageMultiplier(); - page += F("
Power Multi: "); - page += Supla::GUI::counterHLW8012->getPowerMultiplier(); - page += F("

"); - addFormHeaderEnd(page); - - addForm(page, F("post"), PATH_SAVE_HLW8012_CALIBRATE); - addFormHeader(page, "Ustawienia kalibracji"); - addNumberBox(page, INPUT_CALIB_POWER, "Moc żarówki [W]", "25", true); - addNumberBox(page, INPUT_CALIB_VOLTAGE, "Napięcie [V]", "230", true); - addFormHeaderEnd(page); - - addButtonSubmit(page, "Kalibracja"); - addFormEnd(page); - - addButton(page, S_RETURN, PATH_OTHER); - return page; +void SuplaWebPageSensor::suplaWebpageHLW8012Calibrate(uint8_t save) { + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(PATH_HLW8012_CALIBRATE); + + addFormHeader(webContentBuffer); + webContentBuffer += F("

Current Multi: "); + webContentBuffer += Supla::GUI::counterHLW8012->getCurrentMultiplier(); + webContentBuffer += F("
Voltage Multi: "); + webContentBuffer += Supla::GUI::counterHLW8012->getVoltageMultiplier(); + webContentBuffer += F("
Power Multi: "); + webContentBuffer += Supla::GUI::counterHLW8012->getPowerMultiplier(); + webContentBuffer += F("

"); + addFormHeaderEnd(webContentBuffer); + + addForm(webContentBuffer, F("post"), PATH_SAVE_HLW8012_CALIBRATE); + addFormHeader(webContentBuffer, "Ustawienia kalibracji"); + addNumberBox(webContentBuffer, INPUT_CALIB_POWER, "Moc żarówki [W]", "25", true); + addNumberBox(webContentBuffer, INPUT_CALIB_VOLTAGE, "Napięcie [V]", "230", true); + addFormHeaderEnd(webContentBuffer); + + addButtonSubmit(webContentBuffer, "Kalibracja"); + addFormEnd(webContentBuffer); + + addButton(webContentBuffer, S_RETURN, PATH_OTHER); + WebServer->sendContent(); } #endif \ No newline at end of file diff --git a/src/SuplaWebPageSensor.h b/src/SuplaWebPageSensor.h index 152ccab2..c0b98eb1 100644 --- a/src/SuplaWebPageSensor.h +++ b/src/SuplaWebPageSensor.h @@ -86,7 +86,7 @@ class SuplaWebPageSensor { void handleSearchDS(); void handleDSSave(); - void showDS18B20(String& content, bool readonly = false); + void showDS18B20(bool readonly = false); #endif #if defined(SUPLA_BME280) || defined(SUPLA_SI7021) || defined(SUPLA_SHT3x) || defined(SUPLA_OLED) || defined(SUPLA_MCP23017) @@ -94,7 +94,7 @@ class SuplaWebPageSensor { void handle1WireSave(); void handlei2c(); void handlei2cSave(); - String supla_webpage_i2c(int save); + void supla_webpage_i2c(int save); #endif #if defined(SUPLA_MAX6675) @@ -108,23 +108,23 @@ class SuplaWebPageSensor { void handleImpulseCounterSet(); void handleImpulseCounterSaveSet(); - String supla_impulse_counter_set(int save); + void supla_impulse_counter_set(int save); #endif private: #if defined(SUPLA_DS18B20) || defined(SUPLA_DHT11) || defined(SUPLA_DHT22) || defined(SUPLA_SI7021_SONOFF) - String supla_webpage_1wire(int save); + void supla_webpage_1wire(int save); #ifdef SUPLA_DS18B20 - String supla_webpage_search(int save); + void supla_webpage_search(int save); #endif #endif #if defined(SUPLA_MAX6675) - String supla_webpage_spi(int save); + void supla_webpage_spi(int save); #endif #if defined(SUPLA_HC_SR04) || defined(SUPLA_IMPULSE_COUNTER) - String supla_webpage_other(int save); + void supla_webpage_other(int save); #endif #if defined(SUPLA_HLW8012) @@ -140,7 +140,7 @@ class SuplaWebPageSensor { void handleHLW8012Calibrate(); void handleHLW8012CalibrateSave(); - String suplaWebpageHLW8012Calibrate(uint8_t save); + void suplaWebpageHLW8012Calibrate(uint8_t save); #endif }; diff --git a/src/SuplaWebPageTools.cpp b/src/SuplaWebPageTools.cpp index d8bb1ab2..4cd9173a 100644 --- a/src/SuplaWebPageTools.cpp +++ b/src/SuplaWebPageTools.cpp @@ -11,7 +11,7 @@ void createWebTools() { } WebServer->httpServer.sendHeader("Location", "/"); WebServer->httpServer.send(303); - WebServer->sendContent(WebServer->supla_webpage_start(0)); + WebServer->supla_webpage_start(0); ConfigESP->factoryReset(true); }); } @@ -22,20 +22,19 @@ void handleTools() { return WebServer->httpServer.requestAuthentication(); } - String content = ""; - addFormHeader(content, "Tools"); + addFormHeader(webContentBuffer, F("Tools")); //#ifdef SUPLA_BUTTON - addButton(content, "Save config", PATH_DOWNLOAD); + addButton(webContentBuffer, F("Save config"), PATH_DOWNLOAD); //#endif //#ifdef SUPLA_BUTTON - addButton(content, "Load config", PATH_UPLOAD); + addButton(webContentBuffer, F("Load config"), PATH_UPLOAD); //#endif #ifdef SUPLA_OTA - addButton(content, S_UPDATE, PATH_UPDATE_HENDLE); + addButton(webContentBuffer, S_UPDATE, PATH_UPDATE_HENDLE); #endif - addButton(content, "Factory reset", PATH_FACTORY_RESET); - addFormHeaderEnd(content); - addButton(content, S_RETURN, ""); + addButton(webContentBuffer, F("Factory reset"), PATH_FACTORY_RESET); + addFormHeaderEnd(webContentBuffer); + addButton(webContentBuffer, S_RETURN, ""); - WebServer->sendContent(content); + WebServer->sendContent(); } \ No newline at end of file diff --git a/src/SuplaWebPageUpload.cpp b/src/SuplaWebPageUpload.cpp index c364e500..c05e6f74 100644 --- a/src/SuplaWebPageUpload.cpp +++ b/src/SuplaWebPageUpload.cpp @@ -24,23 +24,22 @@ void handleUpload(int save) { return WebServer->httpServer.requestAuthentication(); } - String content = ""; - content += SuplaSaveResult(save); - content += SuplaJavaScript(); - content += F("
"); - content += F("

"); - content += "Wgraj konfiguracje"; - content += F("

"); - content += F("
"); - content += FPSTR(uploadIndex); - content += F("
"); - content += F("

"); + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(); + webContentBuffer += F("
"); + webContentBuffer += F("

"); + webContentBuffer += F("Wgraj konfiguracje"); + webContentBuffer += F("

"); + webContentBuffer += F("
"); + webContentBuffer += FPSTR(uploadIndex); + webContentBuffer += F("
"); + webContentBuffer += F("

"); - WebServer->sendContent(content); + WebServer->sendContent(); // WebServer->httpServer.send(200, PSTR("text/html"), FPSTR(uploadIndex)); } diff --git a/src/SuplaWebServer.cpp b/src/SuplaWebServer.cpp index 38163f35..72c18bc9 100644 --- a/src/SuplaWebServer.cpp +++ b/src/SuplaWebServer.cpp @@ -24,6 +24,8 @@ #include "SuplaTemplateBoard.h" #include "Markup.h" +String webContentBuffer; + SuplaWebServer::SuplaWebServer() { } @@ -81,7 +83,7 @@ void SuplaWebServer::handle() { if (!httpServer.authenticate(this->www_username, this->www_password)) return httpServer.requestAuthentication(); } - this->sendContent(supla_webpage_start(0)); + supla_webpage_start(0); } void SuplaWebServer::handleSave() { @@ -114,17 +116,17 @@ void SuplaWebServer::handleSave() { case E_CONFIG_OK: // Serial.println(F("E_CONFIG_OK: Dane zapisane")); if (ConfigESP->configModeESP == NORMAL_MODE) { - this->sendContent(supla_webpage_start(1)); + supla_webpage_start(1); ConfigESP->rebootESP(); } else { - this->sendContent(supla_webpage_start(7)); + supla_webpage_start(7); } break; case E_CONFIG_FILE_OPEN: // Serial.println(F("E_CONFIG_FILE_OPEN: Couldn't open file")); - this->sendContent(supla_webpage_start(4)); + supla_webpage_start(4); break; } } @@ -134,51 +136,50 @@ void SuplaWebServer::handleDeviceSettings() { if (!httpServer.authenticate(www_username, www_password)) return httpServer.requestAuthentication(); } - this->sendContent(deviceSettings(0)); + deviceSettings(0); } -String SuplaWebServer::supla_webpage_start(int save) { - String content = F(""); - content += SuplaSaveResult(save); - content += SuplaJavaScript(); +void SuplaWebServer::supla_webpage_start(int save) { + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(); - addForm(content, F("post")); - addFormHeader(content, S_SETTING_WIFI_SSID); - addTextBox(content, INPUT_WIFI_SSID, S_WIFI_SSID, KEY_WIFI_SSID, 0, MAX_SSID, true); - addTextBoxPassword(content, INPUT_WIFI_PASS, S_WIFI_PASS, KEY_WIFI_PASS, MIN_PASSWORD, MAX_PASSWORD, true); - addTextBox(content, INPUT_HOSTNAME, S_HOST_NAME, KEY_HOST_NAME, 0, MAX_HOSTNAME, true); - addFormHeaderEnd(content); + addForm(webContentBuffer, F("post")); + addFormHeader(webContentBuffer, S_SETTING_WIFI_SSID); + addTextBox(webContentBuffer, INPUT_WIFI_SSID, S_WIFI_SSID, KEY_WIFI_SSID, 0, MAX_SSID, true); + addTextBoxPassword(webContentBuffer, INPUT_WIFI_PASS, S_WIFI_PASS, KEY_WIFI_PASS, MIN_PASSWORD, MAX_PASSWORD, true); + addTextBox(webContentBuffer, INPUT_HOSTNAME, S_HOST_NAME, KEY_HOST_NAME, 0, MAX_HOSTNAME, true); + addFormHeaderEnd(webContentBuffer); - addFormHeader(content, S_SETTING_SUPLA); - addTextBox(content, INPUT_SERVER, S_SUPLA_SERVER, KEY_SUPLA_SERVER, DEFAULT_SERVER, 0, MAX_SUPLA_SERVER, true); - addTextBox(content, INPUT_EMAIL, S_SUPLA_EMAIL, KEY_SUPLA_EMAIL, DEFAULT_EMAIL, 0, MAX_EMAIL, true); - addFormHeaderEnd(content); + addFormHeader(webContentBuffer, S_SETTING_SUPLA); + addTextBox(webContentBuffer, INPUT_SERVER, S_SUPLA_SERVER, KEY_SUPLA_SERVER, DEFAULT_SERVER, 0, MAX_SUPLA_SERVER, true); + addTextBox(webContentBuffer, INPUT_EMAIL, S_SUPLA_EMAIL, KEY_SUPLA_EMAIL, DEFAULT_EMAIL, 0, MAX_EMAIL, true); + addFormHeaderEnd(webContentBuffer); - addFormHeader(content, S_SETTING_ADMIN); - addTextBox(content, INPUT_MODUL_LOGIN, S_LOGIN, KEY_LOGIN, 0, MAX_MLOGIN, true); - addTextBoxPassword(content, INPUT_MODUL_PASS, S_LOGIN_PASS, KEY_LOGIN_PASS, MIN_PASSWORD, MAX_MPASSWORD, true); - addFormHeaderEnd(content); + addFormHeader(webContentBuffer, S_SETTING_ADMIN); + addTextBox(webContentBuffer, INPUT_MODUL_LOGIN, S_LOGIN, KEY_LOGIN, 0, MAX_MLOGIN, true); + addTextBoxPassword(webContentBuffer, INPUT_MODUL_PASS, S_LOGIN_PASS, KEY_LOGIN_PASS, MIN_PASSWORD, MAX_MPASSWORD, true); + addFormHeaderEnd(webContentBuffer); #ifdef SUPLA_ROLLERSHUTTER uint8_t maxrollershutter = ConfigManager->get(KEY_MAX_RELAY)->getValueInt(); if (maxrollershutter >= 2) { - addFormHeader(content, S_ROLLERSHUTTERS); - addNumberBox(content, INPUT_ROLLERSHUTTER, S_QUANTITY, KEY_MAX_ROLLERSHUTTER, (maxrollershutter / 2)); - addFormHeaderEnd(content); + addFormHeader(webContentBuffer, S_ROLLERSHUTTERS); + addNumberBox(webContentBuffer, INPUT_ROLLERSHUTTER, S_QUANTITY, KEY_MAX_ROLLERSHUTTER, (maxrollershutter / 2)); + addFormHeaderEnd(webContentBuffer); } #endif #ifdef SUPLA_DS18B20 - WebPageSensor->showDS18B20(content, true); + WebPageSensor->showDS18B20(true); #endif - addButtonSubmit(content, S_SAVE); - addFormEnd(content); + addButtonSubmit(webContentBuffer, S_SAVE); + addFormEnd(webContentBuffer); - addButton(content, S_DEVICE_SETTINGS, PATH_DEVICE_SETTINGS); - addButton(content, F("Tools"), PATH_TOOLS); + addButton(webContentBuffer, S_DEVICE_SETTINGS, PATH_DEVICE_SETTINGS); + addButton(webContentBuffer, F("Tools"), PATH_TOOLS); - return content; + WebServer->sendContent(); } void SuplaWebServer::supla_webpage_reboot() { @@ -186,65 +187,63 @@ void SuplaWebServer::supla_webpage_reboot() { if (!httpServer.authenticate(www_username, www_password)) return httpServer.requestAuthentication(); } - this->sendContent(supla_webpage_start(2)); + supla_webpage_start(2); ConfigESP->rebootESP(); } -String SuplaWebServer::deviceSettings(int save) { - String content = ""; +void SuplaWebServer::deviceSettings(int save) { + webContentBuffer += SuplaSaveResult(save); + webContentBuffer += SuplaJavaScript(PATH_DEVICE_SETTINGS); - content += SuplaSaveResult(save); - content += SuplaJavaScript(PATH_DEVICE_SETTINGS); + webContentBuffer += F("
"); - content += F(""); - - addFormHeader(content, S_TEMPLATE_BOARD); + addFormHeader(webContentBuffer, S_TEMPLATE_BOARD); uint8_t selected = ConfigManager->get(KEY_BOARD)->getValueInt(); - addListBox(content, INPUT_BOARD, S_TYPE, BOARD_P, MAX_MODULE, selected); - addFormHeaderEnd(content); + addListBox(webContentBuffer, INPUT_BOARD, S_TYPE, BOARD_P, MAX_MODULE, selected); + addFormHeaderEnd(webContentBuffer); - content += F("


"); + webContentBuffer += F("

"); - addFormHeader(content, S_DEVICE_SETTINGS); + addFormHeader(webContentBuffer, S_DEVICE_SETTINGS); #if defined(SUPLA_RELAY) || defined(SUPLA_ROLLERSHUTTER) - addButton(content, S_RELAYS, PATH_RELAY); + addButton(webContentBuffer, S_RELAYS, PATH_RELAY); #endif #ifdef SUPLA_BUTTON - addButton(content, S_BUTTONS, PATH_CONTROL); + addButton(webContentBuffer, S_BUTTONS, PATH_CONTROL); #endif #ifdef SUPLA_LIMIT_SWITCH - addButton(content, F("KONTAKTRON"), PATH_SWITCH); + addButton(webContentBuffer, F("KONTAKTRON"), PATH_SWITCH); #endif #if defined(SUPLA_DS18B20) || defined(SUPLA_DHT11) || defined(SUPLA_DHT22) || defined(SUPLA_SI7021_SONOFF) - addButton(content, S_SENSORS_1WIRE, PATH_1WIRE); + addButton(webContentBuffer, S_SENSORS_1WIRE, PATH_1WIRE); #endif #if defined(SUPLA_BME280) || defined(SUPLA_SI7021) || defined(SUPLA_SHT3x) || defined(SUPLA_OLED) || defined(SUPLA_MCP23017) - addButton(content, S_SENSORS_I2C, PATH_I2C); + addButton(webContentBuffer, S_SENSORS_I2C, PATH_I2C); #endif #if defined(SUPLA_MAX6675) - addButton(content, S_SENSORS_SPI, PATH_SPI); + addButton(webContentBuffer, S_SENSORS_SPI, PATH_SPI); #endif #if defined(SUPLA_HC_SR04) || defined(SUPLA_IMPULSE_COUNTER) - addButton(content, S_SENSORS_OTHER, PATH_OTHER); + addButton(webContentBuffer, S_SENSORS_OTHER, PATH_OTHER); #endif #ifdef SUPLA_CONFIG - addButton(content, S_LED_BUTTON_CFG, PATH_CONFIG); + addButton(webContentBuffer, S_LED_BUTTON_CFG, PATH_CONFIG); #endif - addFormHeaderEnd(content); - addButton(content, S_RETURN, ""); + addFormHeaderEnd(webContentBuffer); + addButton(webContentBuffer, S_RETURN, ""); - return content; + WebServer->sendContent(); } void SuplaWebServer::handleBoardSave() { @@ -269,88 +268,14 @@ void SuplaWebServer::handleBoardSave() { switch (ConfigManager->save()) { case E_CONFIG_OK: - WebServer->sendContent(deviceSettings(1)); + deviceSettings(1); break; case E_CONFIG_FILE_OPEN: - WebServer->sendContent(deviceSettings(2)); + deviceSettings(2); break; } } -const String& SuplaWebServer::SuplaIconEdit() { - return F( - ""); -} - -void SuplaWebServer::sendContent(const String& content) { - // httpServer.send(200, "text/html", ""); - // const int bufferSize = 1000; - // String _buffer; - //_buffer.reserve(bufferSize); - // int bufferCounter = 0; - - int fileSize = content.length(); - -#ifdef DEBUG_MODE - Serial.print(F("Content size: ")); - Serial.println(fileSize); - checkRAM(); -#endif - - httpServer.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - httpServer.sendHeader("Pragma", "no-cache"); - httpServer.sendHeader("Expires", "-1"); - httpServer.setContentLength(CONTENT_LENGTH_UNKNOWN); - httpServer.chunkedResponseModeStart(200, "text/html"); - - httpServer.sendContent_P(HTTP_META); - httpServer.sendContent_P(HTTP_FAVICON); - httpServer.sendContent_P(HTTP_STYLE); - httpServer.sendContent_P(HTTP_LOGO); - - String summary = FPSTR(HTTP_SUMMARY); - - summary.replace("{h}", ConfigManager->get(KEY_HOST_NAME)->getValue()); - summary.replace("{s}", ConfigESP->getLastStatusSupla()); - summary.replace("{v}", Supla::Channel::reg_dev.SoftVer); - summary.replace("{g}", ConfigManager->get(KEY_SUPLA_GUID)->getValueHex(SUPLA_GUID_SIZE)); - summary.replace("{m}", ConfigESP->getMacAddress(true)); - summary.replace("{f}", String(ESP.getFreeHeap() / 1024.0)); - httpServer.sendContent(summary); - httpServer.sendContent_P(HTTP_COPYRIGHT); - - // httpServer.send(200, "text/html", ""); - /*for (int i = 0; i < fileSize; i++) { - _buffer += content[i]; - bufferCounter++; - - if (bufferCounter >= bufferSize) { - httpServer.sendContent(_buffer); - yield(); - bufferCounter = 0; - _buffer = ""; - } - } - if (bufferCounter > 0) { - httpServer.sendContent(_buffer); - yield(); - bufferCounter = 0; - _buffer = ""; - }*/ - - httpServer.sendContent(content); - httpServer.sendContent_P(HTTP_RBT); - httpServer.chunkedResponseFinalize(); -} - -String webContentBuffer; - void SuplaWebServer::sendContent() { // httpServer.send(200, "text/html", ""); // const int bufferSize = 1000; diff --git a/src/SuplaWebServer.h b/src/SuplaWebServer.h index 958b0e2e..9479bf2b 100644 --- a/src/SuplaWebServer.h +++ b/src/SuplaWebServer.h @@ -80,10 +80,9 @@ class SuplaWebServer : public Supla::Element { char www_username[MAX_MLOGIN]; char www_password[MAX_MPASSWORD]; - const String& SuplaIconEdit(); - String supla_webpage_start(int save); + void supla_webpage_start(int save); - void sendContent(const String& content); + //void sendContent(const String& content); void sendContent(); MyWebServer httpServer; @@ -107,8 +106,7 @@ class SuplaWebServer : public Supla::Element { void createWebServer(); void supla_webpage_reboot(); - String deviceSettings(int save); - String loginSettings(); + void deviceSettings(int save); void handleNotFound(); }; From 78e68b963adaadddea923ac436cde120b23b07e1 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Sun, 17 Jan 2021 16:36:39 +0100 Subject: [PATCH 038/165] poprawki --- platformio.ini | 2 +- src/GUIGenericCommon.cpp | 18 +++++++++++++++++ src/Markup.cpp | 4 ++-- src/SuplaCommonPROGMEM.h | 2 +- src/SuplaConfigESP.cpp | 36 +++++++++------------------------ src/SuplaConfigESP.h | 2 +- src/SuplaConfigManager.cpp | 4 ++-- src/SuplaHTTPUpdateServer.cpp | 7 ++++--- src/SuplaWebPageSensor.cpp | 38 +++++++++++++++++------------------ src/SuplaWebServer.cpp | 3 ++- 10 files changed, 59 insertions(+), 57 deletions(-) diff --git a/platformio.ini b/platformio.ini index bd7f22d3..d19e5603 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = [common] -build_flags = -D BUILD_VERSION='"GUI 1.1.10"' +build_flags = -D BUILD_VERSION='"GUI 1.1.11"' -w -DATOMIC_FS_UPDATE -DBEARSSL_SSL_BASIC diff --git a/src/GUIGenericCommon.cpp b/src/GUIGenericCommon.cpp index 86e92967..5b8efdb9 100644 --- a/src/GUIGenericCommon.cpp +++ b/src/GUIGenericCommon.cpp @@ -35,4 +35,22 @@ int getNumberChannels() { } return maxFrame; +} + +uint32_t lowestRAM = 0; +uint32_t lowestFreeStack = 0; + +void checkRAM() { + uint32_t freeRAM = ESP.getFreeHeap(); + Serial.print(F("freeRAM: ")); + Serial.println(freeRAM); + if (freeRAM <= lowestRAM) { + lowestRAM = freeRAM; + } + uint32_t freeStack = ESP.getFreeContStack(); + Serial.print(F("freeStack: ")); + Serial.println(freeStack); + if (freeStack <= lowestFreeStack) { + lowestFreeStack = freeStack; + } } \ No newline at end of file diff --git a/src/Markup.cpp b/src/Markup.cpp index d58d9739..a9c864c4 100644 --- a/src/Markup.cpp +++ b/src/Markup.cpp @@ -163,7 +163,7 @@ void addListGPIOBox(String& html, const String& input_id, const String& name, ui void addListMCP23017GPIOBox(String& html, const String& input_id, const String& name, uint8_t function, uint8_t nr) { if (nr == 1) { uint8_t address = ConfigESP->getAdressMCP23017(nr, function); - addListBox(html, INPUT_ADRESS_MCP23017, F("MCP23017 Adres"), MCP23017_P, 4, address); + addListBox(html, INPUT_ADRESS_MCP23017, F("MCP23017 Adres"), MCP23017_P, 3, address); } html += F(""); html += F(""); @@ -198,7 +198,7 @@ void addListMCP23017GPIOLinkBox(String& html, const String& input_id, const Stri } html += name; if (ConfigESP->getGpioMCP23017(nr, function) != OFF_GPIO) { - html += PGMT(ICON_EDIT); + // html += PGMT(ICON_EDIT); html += F(""); } html += F(""); diff --git a/src/SuplaConfigESP.h b/src/SuplaConfigESP.h index f5711b8a..61c3e614 100644 --- a/src/SuplaConfigESP.h +++ b/src/SuplaConfigESP.h @@ -37,6 +37,7 @@ enum _ConfigMode FACTORYRESET }; +#define OFF_GPIO 17 #define OFF_MCP23017 2 typedef struct { diff --git a/src/SuplaConfigManager.cpp b/src/SuplaConfigManager.cpp index 4f9fc3d3..24f60372 100644 --- a/src/SuplaConfigManager.cpp +++ b/src/SuplaConfigManager.cpp @@ -176,6 +176,7 @@ SuplaConfigManager::SuplaConfigManager() { this->addKey(KEY_CFG_MODE, "0", 2); this->addKey(KEY_ADDR_DS18B20, MAX_DS18B20_ADDRESS_HEX * MAX_DS18B20); this->addKey(KEY_NAME_SENSOR, MAX_DS18B20_NAME * MAX_DS18B20); + this->addKey(KEY_LEVEL_LED, "0", 1); this->load(); // switch (this->load()) { diff --git a/src/SuplaConfigManager.h b/src/SuplaConfigManager.h index 8d0d1efe..ef012d53 100644 --- a/src/SuplaConfigManager.h +++ b/src/SuplaConfigManager.h @@ -73,8 +73,8 @@ enum _key KEY_CFG_MODE, KEY_ADDR_DS18B20, KEY_NAME_SENSOR, - KEY_GPIO, + KEY_LEVEL_LED = KEY_GPIO + MAX_GPIO + 1 //KEY_DS = KEY_GPIO + MAX_GPIO + MAX_DS18B20, //KEY_DS_NAME = KEY_DS + MAX_DS18B20 }; diff --git a/src/SuplaDeviceGUI.cpp b/src/SuplaDeviceGUI.cpp index c35c7e8e..c73f7fc8 100644 --- a/src/SuplaDeviceGUI.cpp +++ b/src/SuplaDeviceGUI.cpp @@ -56,12 +56,13 @@ void begin() { #if defined(SUPLA_RELAY) || defined(SUPLA_ROLLERSHUTTER) void addRelayButton(uint8_t nr) { uint8_t pinRelay, pinButton, pinLED; - bool highIsOn; + bool highIsOn, levelLed; pinRelay = ConfigESP->getGpio(nr, FUNCTION_RELAY); pinButton = ConfigESP->getGpio(nr, FUNCTION_BUTTON); pinLED = ConfigESP->getGpio(nr, FUNCTION_LED); highIsOn = ConfigESP->getLevel(nr, FUNCTION_RELAY); + levelLed = ConfigManager->get(KEY_LEVEL_LED)->getValueInt(); if (pinRelay != OFF_GPIO) { relay.push_back(new Supla::Control::Relay(pinRelay, highIsOn)); @@ -90,7 +91,7 @@ void addRelayButton(uint8_t nr) { } if (pinLED != OFF_GPIO) { - new Supla::Control::PinStatusLed(pinRelay, pinLED, highIsOn); + new Supla::Control::PinStatusLed(pinRelay, pinLED, !levelLed); } } } @@ -123,7 +124,7 @@ std::vector RollerShutterButtonClose; void addRolleShutter(uint8_t nr) { int pinRelayUp, pinRelayDown, pinButtonUp, pinButtonDown, pinLedUP, pinLedDown; - bool highIsOn; + bool highIsOn, levelLed; pinRelayUp = ConfigESP->getGpio(nr, FUNCTION_RELAY); pinRelayDown = ConfigESP->getGpio(nr + 1, FUNCTION_RELAY); @@ -133,6 +134,7 @@ void addRolleShutter(uint8_t nr) { pinLedDown = ConfigESP->getGpio(nr + 1, FUNCTION_LED); highIsOn = ConfigESP->getLevel(nr, FUNCTION_RELAY); + levelLed = ConfigManager->get(KEY_LEVEL_LED)->getValueInt(); RollerShutterRelay.push_back(new Supla::Control::RollerShutter(pinRelayUp, pinRelayDown, highIsOn)); if (pinButtonUp != OFF_GPIO) @@ -150,16 +152,16 @@ void addRolleShutter(uint8_t nr) { eeprom.setStateSavePeriod(TIME_SAVE_PERIOD_SEK * 1000); if (pinLedUP != OFF_GPIO) { - new Supla::Control::PinStatusLed(pinRelayUp, pinLedUP, highIsOn); + new Supla::Control::PinStatusLed(pinRelayUp, pinLedUP, !levelLed); } if (pinLedDown != OFF_GPIO) { - new Supla::Control::PinStatusLed(pinRelayDown, pinLedDown, highIsOn); + new Supla::Control::PinStatusLed(pinRelayDown, pinLedDown, !levelLed); } } void addRolleShutterMomentary(uint8_t nr) { int pinRelayUp, pinRelayDown, pinButtonUp, pinButtonDown, pinLedUP, pinLedDown; - bool highIsOn; + bool highIsOn, levelLed; pinRelayUp = ConfigESP->getGpio(nr, FUNCTION_RELAY); pinRelayDown = ConfigESP->getGpio(nr + 1, FUNCTION_RELAY); @@ -169,6 +171,7 @@ void addRolleShutterMomentary(uint8_t nr) { pinLedDown = ConfigESP->getGpio(nr + 1, FUNCTION_LED); highIsOn = ConfigESP->getLevel(nr, FUNCTION_RELAY); + levelLed = ConfigManager->get(KEY_LEVEL_LED)->getValueInt(); RollerShutterRelay.push_back(new Supla::Control::RollerShutter(pinRelayUp, pinRelayDown, highIsOn)); if (pinButtonUp != OFF_GPIO) @@ -186,10 +189,10 @@ void addRolleShutterMomentary(uint8_t nr) { eeprom.setStateSavePeriod(TIME_SAVE_PERIOD_SEK * 1000); if (pinLedUP != OFF_GPIO) { - new Supla::Control::PinStatusLed(pinRelayUp, pinLedUP, highIsOn); + new Supla::Control::PinStatusLed(pinRelayUp, pinLedUP, !levelLed); } if (pinLedDown != OFF_GPIO) { - new Supla::Control::PinStatusLed(pinRelayDown, pinLedDown, highIsOn); + new Supla::Control::PinStatusLed(pinRelayDown, pinLedDown, !levelLed); } } #endif diff --git a/src/SuplaWebPageStatusLed.cpp b/src/SuplaWebPageStatusLed.cpp index 1c09d085..b6d110eb 100644 --- a/src/SuplaWebPageStatusLed.cpp +++ b/src/SuplaWebPageStatusLed.cpp @@ -25,6 +25,10 @@ void handleStatusLedSave() { return WebServer->httpServer.requestAuthentication(); } + if (strcmp(WebServer->httpServer.arg(INPUT_LEVEL_LED).c_str(), "") != 0) { + ConfigManager->set(KEY_LEVEL_LED, WebServer->httpServer.arg(INPUT_LEVEL_LED).c_str()); + } + for (nr = 1; nr <= ConfigManager->get(KEY_MAX_RELAY)->getValueInt(); nr++) { if (!WebServer->saveGPIO(INPUT_LED, FUNCTION_LED, nr)) { webStatusLed(6); @@ -43,13 +47,15 @@ void handleStatusLedSave() { } void webStatusLed(int save) { - uint8_t nr; + uint8_t nr, selected; webContentBuffer += SuplaSaveResult(save); webContentBuffer += SuplaJavaScript(PATH_LED); addForm(webContentBuffer, F("post"), PATH_SAVE_LED); addFormHeader(webContentBuffer, String(S_GPIO_SETTINGS_FOR) + F(" LED")); + selected = ConfigManager->get(KEY_LEVEL_LED)->getValueInt(); + addListBox(webContentBuffer, INPUT_LEVEL_LED, S_STATE_CONTROL, LEVEL_P, 2, selected); for (nr = 1; nr <= ConfigManager->get(KEY_MAX_RELAY)->getValueInt(); nr++) { addListGPIOBox(webContentBuffer, INPUT_LED, F("LED"), FUNCTION_LED, nr); diff --git a/src/SuplaWebPageStatusLed.h b/src/SuplaWebPageStatusLed.h index 332eef9c..3da59441 100644 --- a/src/SuplaWebPageStatusLed.h +++ b/src/SuplaWebPageStatusLed.h @@ -1,9 +1,10 @@ #ifndef SuplaWebPageStatusLed_h #define SuplaWebPageStatusLed_h -#define PATH_LED "led" -#define PATH_SAVE_LED "saveled" -#define INPUT_LED "led" +#define PATH_LED "led" +#define PATH_SAVE_LED "saveled" +#define INPUT_LED "led" +#define INPUT_LEVEL_LED "ill" void createWebStatusLed(); void handleStatusLed(); diff --git a/src/SuplaWebServer.h b/src/SuplaWebServer.h index 9479bf2b..ce5e8d77 100644 --- a/src/SuplaWebServer.h +++ b/src/SuplaWebServer.h @@ -32,9 +32,6 @@ #define DEFAULT_LOGIN "admin" #define DEFAULT_PASSWORD "password" -#define MAX_GPIO 13 -#define OFF_GPIO 17 - #define PATH_START "/" #define PATH_SAVE_LOGIN "savelogin" #define PATH_REBOT "rbt" diff --git a/src/language/pl.h b/src/language/pl.h index f414b9d3..62eecf04 100644 --- a/src/language/pl.h +++ b/src/language/pl.h @@ -22,11 +22,11 @@ #define S_TYPE "Rodzaj" #define S_RELAYS "PRZEKAŹNIKI" #define S_BUTTONS "PRZYCISKI" -#define S_SENSORS_1WIRE "SENSORY 1Wire" -#define S_SENSORS_I2C "i2c" -#define S_SENSORS_SPI "SENSORY SPI" -#define S_SENSORS_OTHER "SENSORY INNE" -#define S_LED_BUTTON_CFG "LED, BUTTON CONFIG" +#define S_SENSORS_1WIRE "1WIRE" +#define S_SENSORS_I2C "I2C" +#define S_SENSORS_SPI "SPI" +#define S_SENSORS_OTHER "INNE" +#define S_LED_BUTTON_CFG "KONFIGURACJA" #define S_CFG_MODE "Tryb" #define S_QUANTITY "ILOŚĆ" #define S_GPIO_SETTINGS_FOR_RELAYS "Ustawienie GPIO dla przekaźników" From 2a37099c40ae7811063bc93177199dd2c7649196 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Wed, 27 Jan 2021 06:13:03 +0100 Subject: [PATCH 049/165] dodatkowe wsparcie dla OLEDa --- .../.github/stale.yml | 17 - lib/esp8266-oled-ssd1306-master/.travis.yml | 8 +- lib/esp8266-oled-ssd1306-master/README.md | 8 +- lib/esp8266-oled-ssd1306-master/keywords.txt | 100 +++ lib/esp8266-oled-ssd1306-master/library.json | 3 +- .../library.properties | 3 +- .../platformio.ini | 2 +- .../resources/DemoFrame1.jpg | Bin 16026 -> 0 bytes .../resources/DemoFrame2.jpg | Bin 19502 -> 0 bytes .../resources/DemoFrame3.jpg | Bin 21777 -> 0 bytes .../resources/DemoFrame4.jpg | Bin 25325 -> 0 bytes .../resources/FontTool.png | Bin 14052 -> 0 bytes .../resources/SPI_version.jpg | Bin 26912 -> 0 bytes .../resources/glyphEditor.html | 664 ------------------ .../resources/glyphEditor.png | Bin 68751 -> 0 bytes .../resources/xbmPreview.png | Bin 41692 -> 0 bytes .../src/OLEDDisplay.cpp | 18 +- .../src/OLEDDisplay.h | 1 + .../src/OLEDDisplayUi.cpp | 7 +- .../src/SSD1306Wire.h | 4 +- platformio.ini | 2 +- src/SuplaCommonPROGMEM.h | 5 + src/SuplaConfigManager.cpp | 2 + src/SuplaConfigManager.h | 12 +- src/SuplaOled.cpp | 32 +- src/SuplaOled.h | 10 +- src/SuplaWebPageSensor.cpp | 21 +- src/SuplaWebPageSensor.h | 3 + 28 files changed, 208 insertions(+), 714 deletions(-) delete mode 100644 lib/esp8266-oled-ssd1306-master/.github/stale.yml create mode 100644 lib/esp8266-oled-ssd1306-master/keywords.txt delete mode 100644 lib/esp8266-oled-ssd1306-master/resources/DemoFrame1.jpg delete mode 100644 lib/esp8266-oled-ssd1306-master/resources/DemoFrame2.jpg delete mode 100644 lib/esp8266-oled-ssd1306-master/resources/DemoFrame3.jpg delete mode 100644 lib/esp8266-oled-ssd1306-master/resources/DemoFrame4.jpg delete mode 100644 lib/esp8266-oled-ssd1306-master/resources/FontTool.png delete mode 100644 lib/esp8266-oled-ssd1306-master/resources/SPI_version.jpg delete mode 100644 lib/esp8266-oled-ssd1306-master/resources/glyphEditor.html delete mode 100644 lib/esp8266-oled-ssd1306-master/resources/glyphEditor.png delete mode 100644 lib/esp8266-oled-ssd1306-master/resources/xbmPreview.png diff --git a/lib/esp8266-oled-ssd1306-master/.github/stale.yml b/lib/esp8266-oled-ssd1306-master/.github/stale.yml deleted file mode 100644 index 9bcd4eb1..00000000 --- a/lib/esp8266-oled-ssd1306-master/.github/stale.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 180 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 14 -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - security -# Label to use when marking an issue as stale -staleLabel: stale -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false diff --git a/lib/esp8266-oled-ssd1306-master/.travis.yml b/lib/esp8266-oled-ssd1306-master/.travis.yml index 1ade30e9..49ea5b62 100644 --- a/lib/esp8266-oled-ssd1306-master/.travis.yml +++ b/lib/esp8266-oled-ssd1306-master/.travis.yml @@ -1,12 +1,15 @@ +# documentation at https://docs.platformio.org/en/latest/integration/ci/travis.html + language: python python: - - "2.7" + - "3.7" # Cache PlatformIO packages using Travis CI container-based infrastructure sudo: false cache: directories: - "~/.platformio" + - $HOME/.cache/pip env: - PLATFORMIO_CI_SRC=examples/SSD1306UiDemo @@ -19,7 +22,8 @@ env: install: - pip install -U platformio - - platformio lib install 44 + - pio update + - platformio lib -g install "paulstoffregen/Time@^1.6" script: - platformio ci --lib="." --board=nodemcuv2 diff --git a/lib/esp8266-oled-ssd1306-master/README.md b/lib/esp8266-oled-ssd1306-master/README.md index ff3a6ca4..ed2d8b13 100644 --- a/lib/esp8266-oled-ssd1306-master/README.md +++ b/lib/esp8266-oled-ssd1306-master/README.md @@ -2,14 +2,16 @@ # ThingPulse OLED SSD1306 (ESP8266/ESP32/Mbed-OS) -> We just released version 4.0.0. Please have a look at our [upgrade guide](UPGRADE-4.0.md) - This is a driver for SSD1306 128x64, 128x32, 64x48 and 64x32 OLED displays running on the Arduino/ESP8266 & ESP32 and mbed-os platforms. Can be used with either the I2C or SPI version of the display. +This library drives the OLED display included in the [ThingPulse IoT starter kit](https://thingpulse.com/product/esp8266-iot-electronics-starter-kit-weatherstation-planespotter-worldclock/) aka classic kit aka weather station kit. + +[![ThingPulse ESP8266 WeatherStation Classic Kit](https://github.com/ThingPulse/esp8266-weather-station/blob/master/resources/ThingPulse-ESP8266-Weather-Station.jpeg?raw=true)](https://thingpulse.com/product/esp8266-iot-electronics-starter-kit-weatherstation-planespotter-worldclock/) + You can either download this library as a zip file and unpack it to your Arduino/libraries folder or find it in the Arduino library manager under "ESP8266 and ESP32 Oled Driver for SSD1306 display". For mbed-os a copy of the files are available as an mbed-os library. -It is also available as a platformio library. Just execute the following command: +It is also available as a [PlatformIO library](https://platformio.org/lib/show/562/ESP8266%20and%20ESP32%20OLED%20driver%20for%20SSD1306%20displays/examples). Just execute the following command: ``` platformio lib install 562 ``` diff --git a/lib/esp8266-oled-ssd1306-master/keywords.txt b/lib/esp8266-oled-ssd1306-master/keywords.txt new file mode 100644 index 00000000..db59d6e8 --- /dev/null +++ b/lib/esp8266-oled-ssd1306-master/keywords.txt @@ -0,0 +1,100 @@ +####################################### +# Syntax Coloring Map List +####################################### + + +####################################### +# Constants (LITERAL1) +####################################### +INVERSE LITERAL1 + +TEXT_ALIGN_LEFT LITERAL1 +TEXT_ALIGN_RIGHT LITERAL1 +TEXT_ALIGN_CENTER LITERAL1 +TEXT_ALIGN_CENTER_BOTH LITERAL1 + +GEOMETRY_128_64 LITERAL1 +GEOMETRY_128_32 LITERAL1 +GEOMETRY_RAWMODE LITERAL1 + +ArialMT_Plain_10 LITERAL1 +ArialMT_Plain_16 LITERAL1 +ArialMT_Plain_24 LITERAL1 + +SLIDE_UP LITERAL1 +SLIDE_DOWN LITERAL1 +SLIDE_LEFT LITERAL1 +SLIDE_RIGHT LITERAL1 + +TOP LITERAL1 +RIGHT LITERAL1 +BOTTOM LITERAL1 +LEFT LITERAL1 + +LEFT_RIGHT LITERAL1 +RIGHT_LEFT LITERAL1 + +IN_TRANSITION LITERAL1 +FIXED LITERAL1 + + +####################################### +# Datatypes (KEYWORD1) +####################################### +OLEDDisplay KEYWORD1 +OLEDDisplayUi KEYWORD1 + +SH1106Wire KEYWORD1 +SH1106Brzo KEYWORD1 +SH1106Spi KEYWORD1 + +SSD1306Wire KEYWORD1 +SSD1306Brzo KEYWORD1 +SSD1306I2C KEYWORD1 +SSD1306Spi KEYWORD1 + + +####################################### +# Methods and Functions (KEYWORD2) +####################################### +allocateBuffer KEYWORD2 +init KEYWORD2 +resetDisplay KEYWORD2 +setColor KEYWORD2 +getColor KEYWORD2 +setPixel KEYWORD2 +setPixelColor KEYWORD2 +clearPixel KEYWORD2 +drawLine KEYWORD2 +drawRect KEYWORD2 +fillRect KEYWORD2 +drawCircle KEYWORD2 +drawCircleQuads KEYWORD2 +fillCircle KEYWORD2 +fillRing KEYWORD2 +drawHorizontalLine KEYWORD2 +drawVerticalLine KEYWORD2 +drawProgressBar KEYWORD2 +drawFastImage KEYWORD2 +drawXbm KEYWORD2 +drawIco16x16 KEYWORD2 +drawString KEYWORD2 +drawStringMaxWidth KEYWORD2 +getStringWidth KEYWORD2 +setTextAlignment KEYWORD2 +setFont KEYWORD2 +setFontTableLookupFunction KEYWORD2 +displayOn KEYWORD2 +displayOff KEYWORD2 +invertDisplay KEYWORD2 +normalDisplay KEYWORD2 +setContrast KEYWORD2 +setBrightness KEYWORD2 +resetOrientation KEYWORD2 +flipScreenVertically KEYWORD2 +mirrorScreen KEYWORD2 +display KEYWORD2 +setLogBuffer KEYWORD2 +drawLogBuffer KEYWORD2 +getWidth KEYWORD2 +getHeight KEYWORD2 diff --git a/lib/esp8266-oled-ssd1306-master/library.json b/lib/esp8266-oled-ssd1306-master/library.json index 446d8e08..acd11ce9 100644 --- a/lib/esp8266-oled-ssd1306-master/library.json +++ b/lib/esp8266-oled-ssd1306-master/library.json @@ -1,8 +1,9 @@ { "name": "ESP8266 and ESP32 OLED driver for SSD1306 displays", - "version": "4.1.0", + "version": "4.2.0", "keywords": "ssd1306, oled, display, i2c", "description": "I2C display driver for SSD1306 OLED displays connected to ESP8266, ESP32, Mbed-OS", + "license": "MIT", "repository": { "type": "git", diff --git a/lib/esp8266-oled-ssd1306-master/library.properties b/lib/esp8266-oled-ssd1306-master/library.properties index a62c352f..e931a3f6 100644 --- a/lib/esp8266-oled-ssd1306-master/library.properties +++ b/lib/esp8266-oled-ssd1306-master/library.properties @@ -1,5 +1,5 @@ name=ESP8266 and ESP32 OLED driver for SSD1306 displays -version=4.1.0 +version=4.2.0 author=ThingPulse, Fabrice Weinberg maintainer=ThingPulse sentence=I2C display driver for SSD1306 OLED displays connected to ESP8266, ESP32, Mbed-OS @@ -7,3 +7,4 @@ paragraph=The following geometries are currently supported: 128x64, 128x32, 64x4 category=Display url=https://github.com/ThingPulse/esp8266-oled-ssd1306 architectures=esp8266,esp32 +license=MIT diff --git a/lib/esp8266-oled-ssd1306-master/platformio.ini b/lib/esp8266-oled-ssd1306-master/platformio.ini index db055d8d..530c046e 100644 --- a/lib/esp8266-oled-ssd1306-master/platformio.ini +++ b/lib/esp8266-oled-ssd1306-master/platformio.ini @@ -13,7 +13,7 @@ platform = espressif8266 board = d1_mini framework = arduino upload_speed = 921600 -board_f_cpu = 160000000L +board_build.f_cpu = 160000000L upload_port = /dev/cu.SLAB_USBtoUART monitor_port = /dev/cu.SLAB_USBtoUART lib_deps = diff --git a/lib/esp8266-oled-ssd1306-master/resources/DemoFrame1.jpg b/lib/esp8266-oled-ssd1306-master/resources/DemoFrame1.jpg deleted file mode 100644 index 536b570db09931ec634b38777033432276bafed8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16026 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 diff --git a/lib/esp8266-oled-ssd1306-master/resources/DemoFrame2.jpg b/lib/esp8266-oled-ssd1306-master/resources/DemoFrame2.jpg deleted file mode 100644 index 8dccbcd0210b5c1dd7bad824982c1ecbf5c1edc2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19502 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 C1xwrj diff --git a/lib/esp8266-oled-ssd1306-master/resources/DemoFrame4.jpg b/lib/esp8266-oled-ssd1306-master/resources/DemoFrame4.jpg deleted file mode 100644 index 99cbe1bc41ce7fe01700f5cd00bf04da68f9602a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25325 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 EUrx=8H~;_u diff --git a/lib/esp8266-oled-ssd1306-master/resources/SPI_version.jpg b/lib/esp8266-oled-ssd1306-master/resources/SPI_version.jpg deleted file mode 100644 index 115c9f3bccef1177204c88ef8b8cb8f22daa9e7c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26912 zcmeFXXH*nj^Do*1NDw6FETBl193&Y?A|N0+j3i;mISGt{pk&EP6af(hM#&k;nUO4U zfFTG7%z(fM1KjpW@Bf~4KHT?yxNF^Yp54>a)wOGfs@~yO)qVc$d=a3&tFEOEKp+r6 z6Fh+PC8lh(K-ePy(9r<|0RSKcC;=|u0!Tr?n|25lKmgLjAl(n4{(~k1>8}u4fbb6) zFb5+3kIW22;t!f0lvyO8`%4C_={XD_`m4^LFRuJeGhQo{_@gZ_kLg~a7~cKy+O~Li{D+-<><|WZTbZ!#lPZBAqzuE#dNhbS?2BA*!#eZZ*lj;B9gP4hSCOPDHTZHN$ zy_}r%M;{2qL7kJ6|3km|yU*g+ufKu(RVRp`-Vpqa!zjMIA^e*rd-;~>FBwqxfo6qg`AKZ4){Xe*k+&kE%2`e{H3oL_zsiAcFag@tnZ>ZZPL>ncqn6 zZ@LMLi2Y4Bg3^D}5HJ>X5iASF#t210?q7Te9T-*nTL$dWKlJaI7+~w^3iI*3eqE1- z0MG^Q0k;5szyL4@OaTzZg559%x`1!M6mSF#0y^NK0XzWnT>)<(1n>cz!7_$GFMtAY z01~9k0TVzOFai<*UqA`C21o(Y057Q2Hn0KA0v3Qh@DPvzB!C>CAC$TWo@Kx|kOt;n zfE)uIkTF0WETsZ;0z+UKE`SpV1P*~*@WcS8z!N|jatERc2mmX<87RXIN(ce3foSmV z2s|hu;eZIh1h4>a05re{I08jrJ{Ry9xCEhv5JM6m7LZ6V_ZnFHEZDC~@Lv$%0D=KB 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 Vmt>cr&zI7iF#5{vw(shH|JlZ6us8q! diff --git a/lib/esp8266-oled-ssd1306-master/resources/glyphEditor.html b/lib/esp8266-oled-ssd1306-master/resources/glyphEditor.html deleted file mode 100644 index 31fbd907..00000000 --- a/lib/esp8266-oled-ssd1306-master/resources/glyphEditor.html +++ /dev/null @@ -1,664 +0,0 @@ - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - -
Font array name: -
First char code:
Width:
Height:
- - - - -
-
-
-
-
-

- -
-
- -
-
-
-
- - - diff --git a/lib/esp8266-oled-ssd1306-master/resources/glyphEditor.png b/lib/esp8266-oled-ssd1306-master/resources/glyphEditor.png deleted file mode 100644 index 800efc56f4bd50ba347eab57f3205ec8d0c0dd31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68751 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 M 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℞9m-8fv#DxWbiNUN1>5} zp2keeP;!@8(X7YAz&cJ$&@(0&*9;ly9^@=rIo^KK&8;k>z823jZn^ymnWB_)zdcvW znIQUMp#Bdn007(B>f_c=hWtQqWn$hmgLu9f7kz7RXL=x5!R*A$RYL+rV3(L6vxtpm z?J@g)$Q`>Deu?;sxQF<9&gL6{6<24|pL?^!r9d!VbZzDC1Mx;>GKk)y^0d=8sSL86 zs&`XvCvx#!+ZpH$YP}UZ%uZe?3ep5HWf70C#)pf@p~HX_+I?GZs6HX(Ea03e=dBi%6AWT8@45VO ztCy1pYtg=VmA2fRdI>GBOopbaxU>Y?9^k-Blm6{th0^EfK_1%M2e zM)89j!I^_Wo-^+!Bk%y*rQvSYn@w?J@4m($<}>=#pCe3L#?~1N{Vp_=*@Kg85bc>l z2C-mt8C{}N%eFpw6Lpqf7FXbqE5T$^k7Ro;(9s4Qv8_E;^#Xgvy!5U29VPCwU~2dJSa~7 z`Vs9&E8|s#SftR$w;-x|(9j6W~f8MG$NHUYN!P$(%eQbSN z1N<>(ZV6ys#2eXEc|T`fplGY!dUOko-a)Tzd|dDIj^?*03uZ>j6j0C%$*VSXb_^3k z>TA3g6nhWYwiU+Yf&*-o@EaG5;R!*wOtSg0 z|6Em0AS97JS-J#PWIqV)*(XiL`S8@`A#e602Y`CxBIW&`O);}Lodd}#9)|Ut}iiX3Asu@#Ra{3x@L1Org{{zhTROER!SIykp96kq; zdM1GVe6dlm(mxD%g3mCBQ4Nh5k`S3C1g^FFdW74OrCB@ejK+Pfs8~t*A*N)++fhIm z=FPnpou&vv-u`ilZybpIwR@nf--jsSpgQ*j#I(n$f9Op0P5`QS(x(M<8u&hcs@a0> zez5DGe5X|Ei?=i^!)dzXuS+20Uo^8}#a#Yphb6Kl&110r%p#IR3}D{W+7{y>?@V&w^VjWZsR89~uhsKg3I*`Xe}Qk~gT1=vrag!# zd4FCM*_T;Rg#WiZt9Gm2=x{E!Y%wQa(O5p`1O?{q0%(=8O6pScRF+GJTa=Zs-q-|N z)k=Orosa$D&1l8q5ibY?hL3SxawZK*7e7ASivtsZx4&?odZWs&+B}cd7ahn@w-0E3 zpkk^u=(*jT)RSx-49i|)e2@LMJvbU2qF)=qE{vwK){xDc)vK$n`vZ-g1cPpKi z+Bn^Po!sIQQ5@`{Zg3ldGb;8M2F0A_Q<11pw2~ah2h}K?Me}2#PH7eM{|Uep8al;& zt4=uj8iQ6(x4GU>7jnXB1bL}cDq@xIPLrB_xFE;O?E%=7+myn6J2N7=ld)9nD9{q{ zzcD9cY~*(3#pJAPfGM5=`noO(eD-`+YNw#AAq&oXE;SBcDH=$p_xf4BM1-3%3~B28 z#*N|yic%#HOCuwcIS~SW{g6!SJj+Ux?pK{_=x<>nH4V;%4KF|;MS zvuG}D8UpRcrHEE_3&2jDD-8Ex*RsS3=8?$Zj+koSffw;755#>m8~8Bm^rx@)5NU-jdbN(?t;6 zmsmTmk{@!w=K`Xn$27ADZ6nL{sV3mU;>I__zg5os7l2?8r95o38Y-L{l2qf2ROt2= zDqFq&r=R#cQYf@EcSy*K@^l1V=~UekcCv9qw534{Ekk+*?nd=<74(!KYbre-OjH)c zoR{eQaaZrf!l$}YBM6926{PVGhq^K`uLN0rmq?x13x36VtohzXW&Tui2p;+t_U#@A zzHWB%aJOOo4!#}rfQM|565?Iu5qm?8UNgp*lW8sot@?7vkox^(jF_J-HB$`tUxVt< zmd~C4nPbqlK8Zj2&Dv`}?A3bb)cD!Ot<2-x<7lOzp5&0=#%H)#enXfqKWxvf&0_Ju z+u5cJu5@H4Az(N9vjHSn{3PvQcG^ zqf(_>WxH+DF5tj7Jx+#%AtnhGr68BwG`p|7H3udjOJ$4d+XX}PHrd%^B{4&*#*aAI z8(17qK3IIq?0Ls)gIe5sx6%hs`vL+2hK7c?6(>2Irju)lws*LDDctIX=wG*0hSXx{ zwsg5O(+>nU-Fl5Yhb|)TheNj&>9!I+W^{A%+3Dsm^?Vo(LLRC^teAh;A5-y_5g)F* zn)l;csJTCM>p`8n!_%MtwAOzW(WTuIGQIu0#;=+5xg#&*XWjd?muxzrUHI=(vYBmJ zW8e3`PusLw>I+*S{E`iL&HI=1VkoS)eG5RG`|V`Cbt;lu=)B<_iAbA>ybn_43l@Py zzb?LiCJQ-ZRtDCJAX(t9foB7VMmwMcTzYsg%7;!-07#x@KcBzJV+#3HIiFav98fu- zE`j9Y^9ad$UmGR<7wA6-2B7~3HZ_Ye3nN)1X9g)Ha%HOIK&J5OU+N97WAKGAL&J;c zeDJ`a(f|oA+wv~E_@h-;xvl}MUEzVmQK#W=^1Pp_$gOFJgrKZy+@kwG-e}xX5Wuyp_wDMrGDi7D6u4v7)_MTYE{!5 z>E(SoA4&3Ey*cUMChcCuWGrxQr0Hz2^|}K4i6gYmy_+P2A2@wb zjWuT6!$dhmeqe0_}*>Z*`rmP;o|f2TgFrL2U^DbMO_lk zFt%@(K8mg?$Wy%XN32cn8-{O_*^Ng$C%c1#_x|n}SeXcMyI>1z38bRQ_JuHOH1nVr zWVaXVB_(4A+@$MN{bdf4rm>16LQ#;QfRkr=aLN|a=D!JV2=LFtFINFtI_;p8Z2p={ zeW=8ciAB#{2Ij7eF_x!KRa8}1q-`c^HfJcija6MO1{9)(21Mjzw$5w=T#mp@_9{u&+@f z9=@_=Xn3Db_$MmE6Aw*Wn2`Js08}XyGeR?eYOf9!E?7RgFy2J>yohmZ2bxI!1z>sR zOerp2ROu0BfUF1+C$Ww4|M$2RU=|i1%YEljc_0A1{0FxyMV75LOvg)%k8;mn zx@F39@r6F*sL{B!VN~v2zKRRK&lG2&ibN(U7=V6VPUFt(4h$b}-J3TnB#ODTv#9-3 zs`Klc-x+orlHdWP4u}xvukhqFM1vryM4X3t?AJYi>ik;jZZ`Qqey3Ls1(m1%*65CM z9>GbqhN^G>M6zf9YKUzq*;9YWEIZU4yZJ71Mfk8GChW5(!^M?iPQS~8Qo&Z#$D8W)Z9)2j|(-yQ=Nzm}RZitplgAF5P$ z`&xq_Bg8kC?jrU1f-zcN5vWpEX)n(_{Su*Azzae=o}W_+Gea@1urjagv?jZ;d`z3Z zumf~0$8?b_|N(@vlkpIn2=T-wgsTq%*vUHgO93Q-^fKV?yDa$|Icww zBEH-8IJ%F&g}XqyO1;rTOm5*#Roe5Pr`{jL9|QYCWv(wk_O09Ruo^BI5=V|k52>G z?UZr<)K(MA?dQ8F#mK4$Ah?A?C-*kZ(+8~z*|Ii?LO__(7jpK}PgfHA<=zYZxcB7d z|E1N=5*AyI$xE__l1+PEwMyxLbni(XzN6uY#Q);$t;3>RyS{%E0XGN;2q>u_-Q6Hk zN=o-2-ObP?C?VY-DIv|!T}nwycX#IilJ7O6?(N?9{oMC+yua9gcB2b@&?hA{m|U}~0{(2%yL13;`eDDwxU#sI z&@RFmV4%%(LXI8byc7u@K`#LFJj@lOS#}HFor~rHZZ8iY%>%B3jFR}bS3KuSh^Dm7@q}E5t;pt|_S-eGVZVPm{pezymIRk(*1A z3e`)57hXdy-6eK0hRt3FVX$qEo?pK z(}0PDrpcLM`2e=EF^t;y9zh_vz}9*0tY&mj7e5Jk>BWpH&aE`?;^^jXWO2ScV2-Tv z+6I)VkY1kK-X&u_+%!)_H_gVTh7o#s^2*VH5|EiU-%p%Z!!zYwecs0_u z+jN%P_p6jt2OrLV3iU*=cfL|?qE?Xolp8(C=KW$718Sb7lx>&rUspNKS(_hYAwGc| zHc(I=4>2+ywf?=Jl(3|B;c-*n$f+L)I)0sKitwRKkv7S85#AlxB?eIZK9Z%mdYI5w zw+i3ct5pC^0K*OH6M(VW^{_*y8Q2isva0nb;`F<|>0#@u)n5&C(YTdj0{0fOnRUDx zTk3r(vMz^A*YSb`SCUQ`@U?Vh80u+MF-9TFz^+O8tAE#5E0e2rTYb@pV!tEE%GAoh z>ZB<*^hqG!cm!qfWgGQd!SO-@c#8g{kmvI1E1pLtRJxF>-- zKsGRd^UHWMiF%(aceegu9Oedpyza5v_^BpV6_DI=EutOqwQ%9th63iD_%Cezq--_G zF~2Z$yTX@|@+ireVcitrdg9sC&Tz`eb!6TE&+!1u!&Tu0K&lPs6X4&0q%FmX(T;Z# z!Y8k4o&IgF>(+cKuJ?15=&OPFgEDUw^}8(fZms+Y#-VGw2^o1asXbaIs|ODwW|#~v zE*N1Z9Kdwy$nie!1lSo1X8$SSkw9QR`S1v)fobo!CTdtKc8FYQtmqtvyn?UKn*W@* zjgMCvxH#=~0FU@mrZEZ}l?0HJz?l~hB;D59#w0zoS^q0{FaMhA%J~oGx_||Nr>g-# zs{Lc}it6q9-bhJt&wfyP6Xm;1Zn~Fr-+FSM_F5dK3R!6@BKb=qDy_=RAZ@N!_ZsP_ z-8_^6+7I`Ys~WP2vxav^PL6ru-8hpcvy%K>xGFk`Oyyt9U@a?USS=eVV9N7{W`9`6 zysR;Wg)bKH%oWzWnVUMiwpw~Q`C0^^@Zu_e%yZb*B^bHfthBAKuC-NJEjJ~AwT8!f+=H?()6vUKG)AZ&rY+tD1a2l0~ik_wTh3eazgh;tpxLnG4I7Wzm13{?*3OK>R8bMt=J^Abil(g(gkAx>mv90I{(6&jLNF z&iJamcao<))`C=p{BfkjPFI~4xjzj2cT*(~KckR7CD{wQNiK{AdaloN6NpCW+s3#} z`kXFDTjC$_Gxs4Gdv<;v!$m`|v3HswS6Mk9O^8CL*jD01hz7jfn2SQ7Tpq`P16}H- zJ88>vtYvdomQI-9N@Y%bRb>+&2%-!hW&$?~s|oIS4hFkJzprV7Mfm1NZ2=;oX&2@) zWXV@#rtyC4$M5bH@W055lvKE~ADK9ro$HSZt3S?%W|xO~f}}KV7WP!%Um7#Lij0JED7>yVbpBQB}Fvj(&7 zgIbN|L=CHxhXGV%WV{L^J$upOTVRT(hC_{0^H*6qCd;{IQYlNf#9-UQI7^Mn5?9#M zG+15U8EfUu3xUi-et7{j^?AHV`5nHPvZ(rnIishm20kF3V@xGHsMG&4X4HNYlbS2; zqQ8HnbG_%~dYF2(Yi;Zq$3S#>FYZTyi z#s=KJ?!LJ`sY(Ri#ti$fS;eCCH3v(Ks<~oa`}nkvvY3pZZFZi7R49aYwqHL#NZZSk z*QQRHOhNj3ZxGX`veX>4!s{A82AKUiX`A!Exgxz8-CA!t+j6}~T`sV>^u2jfaS5$j z@(%EH1zup2A-y14O9$8@_aw%eUIj!8ZDZ`pdZrZlkTj0i*OmbTm#~8fwJd(k0m`Js zp;KMMD~KK8?OovnS7^Dri;bv|)pR94pZ+RWG1}x4&m=?10gvaHF%)ob_? zM_APBn#uE^`zCwmT8B#L<|?VQ>GT3AOy=6)!1!{v>gI46-`GpoYn=E}U8-Aj6r%;d zpc2BWspsm^SfS0D0d7K5Jhn*a?d!td*KdEgMX{RY+gM^k{%wTJEjejMwCa9Mn)ElW zFWOvjsOS9WggxO@zE8RKla%eoHCgE4$KMSczw%wy^qp>FI@O`B)efom_!+3Sat;RT zD~n61Ga3`*xlLy{7pv8o;fJZ0LvxpHsh4CBG|rQ{x$AD6tJH(^`iOv>NmMQ>qWCOP z4cvYG)to8UCaLCr3w`>mOX|M&hf(lDT!E_Py39+`XqLq#yH>5v8-hQjSr0SX3GTK< z1a;l%v(J*MEOU?Kw|RkngS_;Iq!vC2al04uL2b;wN4p?b`N?1a{E|ksCWG8aT$O>Uy^ShR>XArIqF@(6xSHI=-+QYH7(!43-@0 zq~9|1tLpU)GfD_xze;mSyx=#h>@hN4IpD3pO6}=2awC>lf#Q!O0H2DI4rdT$q$fAI zSzbn*NR5@bf1caJa!@!@?yQC=JV5U;QQo0q8v@^{tJ30X*tQbltvAb{!I78Tp-8cD zS-gIhm~ojAV<${@ZVFwFyIG8*0``3D88PjI)=g=BPE3(gIo%>Z5UF;ICbpjqjFmC! zS|*yHXZ?P;nXc)=ShB2NE{N~XaAgSY6S60aK${YLa14^4B|L5@d+Kzo%pr~Cp>T9V zhQr;znIE48+sD-#(obov_72j|wK~(vn|_X>JqH>4ZZJ|a9YThZy8W4A!(TzN%1wR? zJ&<~SE~0TZY4bgJR@)$bV{oX{ie9s96YHH<^SiMlSjCtKqJB9#`#fXOdQk5mrV9Z$kD$w@{hkVZW=o0k}!_ z*z0J^$Uck(&+=YW-LXT=*GHc#$=2c?@h-Qaz3&)J=g1ck*45y4TeMAaW{eMkhmmoC z%+T;`S3Lo3+tjaXs&b<*V*=`DAI(fpRH~ZpLhGkld5kUb)b~JIwyBG%ELZre+RIiJ{WUj46!ync<)#{e{rKP1X^)NDX zWyE(?5-bpO{V`DY=bXS~2j3{u3CL9|2MtS&vC!S5&7B&S^L13;Pao1+ zgmDDcrdX`VR#+0K?+{YhKVxUkc`QH8Zudz#*F{d$^;jc!!ScxS*=f&e6d^A|fYb`bDM?oZlHpB}=F%R3-Gn$_+2;NW1U&WX;?S>T;2 zea#!y!DeFIS%y^mIrgdw^RJp={2a^m7FGvY)$nuSdxw-?EcN%SNqZ;``yuY*~9@h!yS zb&Sf&R$T-*aq$VoJ6v1hMiRbmF=vqW5Z^l&Kbd!pKmKt*M(dFlv4#>TEtV}_s6C8T4 zL0UA=_1Fh^zdX9zmo0mghR_Tnj3WG+;^4Has3=SWFAdJU^=wuN!xJ=_Ae|0^y9(W+ zhGiqK0s``=MhBifhVNMEJtMMtm*+N1-d_Gf|E8zMQ_uO1V3zh!o;}fSN;X5gPzyX4 zl}TXb72TIt=gM)J#Qf^Cji}w-uFYi^+25usHRot8N`;Bkpc;LO?6F7RN$jDFt|*#1 zd@2bFY<5wB4qJu4cP-|ohMifn<74n!Wq18qnF8`WuW!&R*JQbEaVx8`m9L+vu&CtU zyI4l3>lr)PrzG-NX{~vCNu0aZ(H9L64*V4i(9zr#x?Lzf1v;c3`V`<;V8NNL&VLw7 zB{Sa1s*OH*u=nJ!y)9xW%0lpwewEuCJs6b)tVM43<=aQ=Ug%Vrp0#u61U*YTkT8VQ zs*Of)$dbzM5ea|i*`<&0)Lnr;TBCl$Sa#HAhWqqiQrnKw(rHB(5dFcdaxJH&2Hnf4 z6`srS_aFH0*VxLqs+)HPLLFi*^y0x}a!8l86Vv*^C6PQ2j)hMCpKVyXVCQr2U*P%7>ddIOX zNcjQG!okyN&x%7{-@WYSg`*=K!| z1gRg(00>$X;k?gpr4k0S814HM-aYEJa8wyB?s%vl4D~D17oNL|NpSUe{S;MBhe9x7 zOjq2?Q=~P`+5i(+a4-rQKaeD3M)n}F6y{m(d<^oJ5sL{Hq|1H+|8-OEfTs!RvWK%> zp(|{$fO?@bc4E7L9+DkWP!c^zQHZ}P5Au&`-e3g@hTpCzs7OA%+TWZ z);)EBU{NS$ZKDq4C~#(6B`=atodv?cDzzPK)mK5>+K+nXlnJRCTO{znW)NW5YA@x; zCy^hYZ^q2VV{@0-mNO zG@oDW#~SAWXh1}s&es_0nmz>ui2SIe!Ua(-1dS(bnv^#MQ1Yn$5c9!q4Jp}zY`Wd3 z!`b-$Q1`y06izLg>w0e;u0*$B78O<$#IoqR9B`T76EXq-KCG)BX$FlJU?9CSUDyun za9QMGBUI-U0^y1MKoAT#f?%&&EFa}lZDmS|g7$)~b}gpz*-u?TRtZUEobRPYPoF>S zr%fbfLg}60lWLe%|9n1(yOd20;K&4kBZEV|=WoBdy-)%#d>GlFQ}&h1T!o}Si3Ztt zvex*sNbmf(8g5Nv%>r8S&7Y0=DNsj3#qf|sJA+NzZxY@L6DCbjoS zCy8b`_fQ=AEd*;m4n@8k9p}s_=>HXET3}HI_iEc-AHwtE1R1sroBNn)3{TbpUaE82 zQ@K}SwV<32Y}Zhik4$hze!z1oW8c8_tv>Xuu80QB{}??( zXig4xjtH7>e-AbEQT4d6Q1gYCyFZ3;LYB7W*p;A0p{vAIIo_{O!LMGndP(m?KZFra zX@eqlmuGkf#NBP*@iFamfivWdNFFDjY;ACL94`38Ol5DBh&}|q!|XoxzW|Ey0t`^{ z!+(;;2a0&%8?#n9b~4(n_95ln`W0HkfERpmyDOKumwc?Nu&7xa%rx8*>p9_w$B_{5 z6(`f*^kZrlQEpz_4c1+S5!3IX8HeYQY4DR`@&!)nj-Jw7m>mwdNf28IR*&j;Bso3Q z?^#DlVC9kE3?jlMMsu>rv5W0EsHK4-bw=esqV_XGYu_Ht=HxEfGWpBR}}W!z2ksjkbl-* zpQt9Gbd$Dq0p(2{M>iac$mpypwd#SWYsZa7TE*qKndG4iLk;M>xWpi*X&+XmbsCsi zdt*~npcLjR9>OctC;{d5WS^-%w$o{Rcgobae*yb;ScOnb)>dy%XS5M*RT;;{7_iM_ zRMz@XKWp{sag7m}T*KTVgng9=&&WwwXML?EVN!}52kU#NB~X$Ek4%=E^qTFzd==g3 z`h2<18@aEHpwNVv_qecT<}>W;5x&Aw&rgssrepSC;mrdAj$l(9LzLI(c|>4prwBoVbo--D^08(`l-Rr>ymBJqrVC!70LnXO_eOxBNCf8EgJOGjV z)>r!~8zk)^Yb&vxJmwi+VeCR|q}Q7_c&5f;Eg;!wWek#i1dvg!hq2sQPVCH%Z@>%l zUJyv58dh_;a7g#y3<8lh@N}^U6w7A3{!Ld+9DpHSxid#qEcgz?Jr^$rm`F%TRl`bE z7Am%j2z1;Lb;SQSl~FcZNmj(o?wEwv<(3=SpkcL-gY#59={JG0@2yrQ4E?&nSYYj3 zEctI-l9cNDd=cV(F$b=0y`Lj+!#CZ9(k_HI;whaztrt%s4IpLsh}tfFJu*})lu?=+ zYZ+!-*QqdlRxktA8Z_IHQf)^w1fnSC7g7OX*WE+)y*I~4G7|9jU%yuTR}=zl6uWtE zkgs33_jwFWZ{%zFCB^k}%-I~(kW1X2#W<HKvEBU{58rV$`~2`z4DG2mLuO{0`reL zU))TX7}JuaPVK&3GFsyeIc;Z;2*P7ILM3`G`GftJAw~p<3Sf$+S9+A+*2Jqwzl^&8Pr7H8xXmB$*x~#grv1rPXu*;z342vv+D>*h2b{uTkz24f<3=X65R- zf-Lt$>TS401kIeG{wgA3SRpIAJo5opRg$N2-HnPm@nk%YR2^qx5D<9~Rois~fiQUJ z-WK>HT#o~V>sMTEpLCuMIUhTfd_PQT*#bl|vP0s;;r$4S9nE~tg||`O+K>uI$+e(c zSP`R<43^{iz%!@y22ga(0c`B~{_6q)#T9>}h*|tG(opIl^=b`|kiVJ=oy2CBbzj1o zK>3Ek*cato%vvH%==WM%UzW93vS*h+YTMTmz&VG`K`CLiohh+v4bis*Ms>035&+w=2wf?V1c1abfato;XfxU3{8<`0S zu#h|$S9Q`hp};Z_RjO*QIxN%E+e5Q zUFWSujCGgWzML{f#KNcB45@Y=zfV3r?_kRwOZ#Z#ZOLqSz3itfh{3mPU3*!r!4m_* z7Ckgq@_}?PB>C)&rGHd^swMe-?@||vM+(h@5!yzzNF6C49aX_78}%%~GB_)mv`xKF zMol;Y$@t0ZSUp%-z3Ixj_()V#P>6%BDB8|q{D{g`AViC1MXiChItbkzN{PxQhJqCQDXHx9e3=%}mwq^IElc?=uijjtc1^>_V89;)lpWJyg<@9d$@a6c8W zDuL-7NLR&XQD_9~_P!(3?{oQ%_yUT)Iij|fGj*sq=`gH^R6E=ZUo%H2c)APr$~eR7 z32?1Lb|Kdn5O#C+|ZmJprmh<5sChk@}7|l}-LCqhiE-%JsKtQNx_!lDC zD=#&~OM2)1-6`Kxd59k8yPi$1xu}u~-NqcsRX&!`KtN6bp`7aX?=O`V(@EbwR_??z zi9bL$Z-z*WRuN#)?1c<~<-a8HPFa{N2Mt7gj0=3B95&dblQp7jsc}m+Bd1|7xn+ppHvPO~QBXD{8Vj8^nTo{;Sn4_;cu ze1F<&LCs!oIqNiYL>=nLNR=d-o&YiYMsr^dywvbqbkkEn`*lpExdunko5vMNip>_S zxO2yoSpB}cAH_u_R|h9fY_gqwj*G>v-B%y3Osx(1-fFVqJU#rh1w7XvnCM7;KjYlf zK?(vSvqk?6R_58jvBciEP97icAVkUw_e$xKJSQfzh9?8x9(jC=;woNrc@?;8f(w29 z_;Ta+^Ar#kuuH6nKgQaw1LEweW#> zY53kait8cUsCmU+fCun0Cl9NdRaco5j&JsiIf-VCdMSy4{ThIliMXLGnQ$V0oT}&W zAp5gn#r0Fq_#)ojQ@hw#7;d_~4gQ?kpUz*ImiLGnK{b$*A6y}F^gBO$bL2rWLbz2S zV3VTk!3Yj^Bd0CdsK_98oYLqgKO_-vzZlLcTUn6(<3&hQdt=@wS3ezX2fF^OWj)m^ zjjj@o!Ek}w2+dOfj;I{SRPz^H3y+;{fdxwlCxkl5=lyO?5$My`NXwhM%v83I`!;Ti zw15MLfue~<1P5s9$8Zw~5H)szkk^{clQ%B77o{BeGe?dx+@Vf>TlB!45 zaoaZJu(Zc>HhDT4rka0yX*#VoOkbi{3o0EfE#8U#?)F)-q2#ba(Xu9Phd{sYqvfi! zMbS62^a}qee5KIMC9JIL$iq$&tw>MmEQgFHl!)}`z^+dMl`4scKuP8-7S8+SJ&@T8 zabnY$JbYmh1MH9W{G z{DX8PK7-2oQ%grf`=|wOnq2$%&McQy8xwILE_ms3Td|T9b(zy;1(`8?%CSp&#n3Io zd$4%M*JvZyDzZlL%prBmLs(2`C@m@t69>rP)0)?wahH|G*PvoM>jLMUJ9=};`>vXr zXQlbyV*U8eo>HF^xIOQIG!^ZonkbVE9!p6UNv4%rh2QUni8cFqKzbv3uknaq!il2{ zY6mD8Bb~t@-v|I?@owXxNDuZcU5x2Mjtk5ThhnYT>Sbm#s770n7jsD%nval(N2cr{ z`3Fcl#;IgUn&r895*h0ywXGjS4y2(Ojtq%7vArtJW4rcCAC5~$BXQ`ij%Roa`@Cqa zM=`fJc}AYeG-aNMY@ULIcsk3(r;bv46Jd|B*@|G_9d{P)yNP%K*{%Xw(|p;JsAYmX z#ZvgPn@4{96Y{WCJ>Q{v3>xTLc^g&~$)J(Fv&aKsTbd0ptKXr8S$F=)(d=vC3{m!Yw;AuLEx%mu2i~gS~Ho`fCfzjJ@ai`$mzUi2CZBZNQ^suLnZ&5!;L&>j3>){su8GEa`!)1 zZU)BmfZVP4b2{5%Atv8UA0K8Ps#sr4*PFb-*W%0YkusiYA(pDgH$Ev%QWrVi6+5en zY5ty~5T=7+z0>2{2xMnSa?g?K{>e({HbJ{evPtFYPC!8Iu7#!ASTa+T0G)-4~8slt1RSFRw# zwDYVCdkcBX9eXTkGp^}KR)9Y-OIOPf88nelsXOqW%;*UPShLm>kv zCI$^wt^q?0CGX6QC-;jXj=a;lbOmQZT0z;a=8RvYwVX9H)1yi*&=X@L3we$8$y{vHu!Fi~L@ZRsb#ofYhXzwaxRRB+*!7XVO9{v#*_ zZzE3!bN^w9f-GGSGSf{rz%q>rlKkq8sxLdSBhMn0B&}HUvpKso+x$S90s86#Z zVXIQUpHv5E}w3np}ag1s7tK^Vz#{O@K_rqv7rzGA}Z?^u&&d&aCG3 z{Nj6h;tXai)VdeIKK{j%ZHr6hcW$to5FQ-M?O@{2taPjmO>K75&e5MAhA?5rwpu70dKo_=xj# zl}QuEwQ3GJDM}o@tPxDeakb*2h82<59^aY>4sb#Ox@r#`V!SZ5M-~h3zJ1J5$M9y5 z8F+20w@z4rH3yl(EMdmhC85b)-Rk_HrI;qA2?I~bi#wu=F5d)(17370*}p4(uM(2K zx~6Jh37(}D*f`1$ss$5UwxqFJY$(6gad$V1nOC-))b~;1$ZobcZBuR!0IH&kBEq#l zCTo&xusshB##PUe9`B77xLZ(*W)iD>m@8!E$WB0i=gz{*dd!NIyD5KuIz@8&Kq!Xi z(GVg((`WN(J6cNc=?Nu{iW)i*Mnn`uU=+0Cwq)e?IV(5DwKd@)B(X%=hvKFzX^@k0r77MpW`8#X81NQ*z?T zPoOpQ##S;LwED{e4Fee%QwHi?n<7mqw!wEO3^cY?%mf*N@fis+i)LTZ z4TJIf-zsVD8rHSP+nX>=)OgCP^?z&?xbgtBsVVbNyL4$w4-*x15_jxQ>Bnp~HFzNd zK_1CE29E%ZEsqbA5GW+NSt>NNNmbK=MF5-1$SxuiyYtn8-+()U|#rw$wR zpM`qNWr=7@Zh$JyiJ5@G6@+Fwp!xP!GcR^`7>gNOkaq>t*M;;P+@W%nu5zppey5P5 zQk=jZW6dt|nAqhwXR4J~4G%K;VP8Ik=ll@Y;mx=cRsHN}Y<+`~{CG`5$-aD5iE@IF zSKp|=QOx|CqXJ4Q<}yUpG?U=U#jVTR;lI%3lMA{0aY4pda?DZZ!@8LZBU2ZZ%0k=I zx34mAkb1rN!iJ;r#6>4{<5EP!;i0Ai2sJzpDp;NI7W&ILesPK7?_8p}`8O`vr~JVs zGA3s}p_@B3Ro-VlCuNw%Vp8bY2g4ci$JE+-GHK4A&IcTBiw-g6UWLEkfx0|fhOxqz zY?Ps})Iw}d?11BP=^Tqn0&|RBw+v5fzD^L7hZK)Qht8S)X5jNg^Sclo-QaSV=#d-x z&~6tW_LJ!zrZu$tg{~n}9Nc#$p4^v%x>l2b`y6CS|7<)QXMEHoJx%zSX7k?eq*Wgz z(q$%a`s=O$a$Y(gL#mE|I+Zfn;XhiG@4V3v@o3`RUlEXI7}%XhV*aJ#IWL-91!~`F z;}5brZ`k_~!>0HI_1w?iWXcI=i{``8aAU>hLRp;dGJqZO=Pa~2tF*c;ukDX`zN>6> zmVQ65`W|(jMbb> z6wL3|4QUe`hFN2~6R_>>v^HC^iVrz?_Bm#L6}pb`y!GX5z>`oOu?2^!o&@aa4xxxWj%nb$0tz^95OnR0n5$p)DX2I?MbCAH5ep;e!zdA444e zn&tocSr~-G9<^7o@4~L~y?@Id|D88hq5mUqpgsBrZ-DVE*%D}Q#`sGj<~MIGjkdM< zC7@FF&se;|PSrd1O`A&;@^Be9XnHiwL{#u~(3XZ82aK=Z@Wds#XG3SorXw|G{Gx=L zKq@-8Wp(?Y8d@Sd;xAD_dT5c5Q%M|#jo6R|hMfx0-%KG%7i~VQXnoI{>DyrKf?tZVq_^{yXp=~D}&b~gLV_dX?*XxR)tZV~<8!>b)q$|aX7E#IX z2jAJbG!+R>-i}2jk`c&D{GyBA*>g2GU08d==;E{hP8ZAeAzw3EOv|kREpW;bC`~-? z{fqEojamj5Uc{U>U;ZcI<#be;p^*XC$IePV#_y4})BXLe2xy_WXqTJ5L$aHGvZ6RY zX01P<#3Y~=-G4QZXPb3-iQ0E^FlF(|-4iTdRg|Gk2CJOj@8hRLDvx|>c=|%w9<`_P zP}5M$t_wh)Q|(j!bD|wBS?)i)7HA&n4BJFGnX3qLJ|G*!pzGbklL={OFZ&qIBrf3x zq9jumki7An(obH@X6;a#@@trk;)p2ZF8$QT=X1wL$pUzP8G)j5j1-pYdslsW-qRah$RcfFQb$0N)4g=FS z+sM-ogSc-e5Px6x@ER0wA?bjxsX}-9Tw3)C+QQ)}z@+K-H?9Cqo_S!|zX!t&;0hgn z>eOxF=ohVdg({1_ql5%=SQqD7?Ep16#MpL5KiT^zvoLp8PckD$TE4MzffBF@E%p=^ zjHbe1XzB{Y{VdnqXgtU@t$=Wte+Olm??gCHs!DKl;~eO+lcf$T_20Ryoe{X7B~5i7 zsjn=L9PkKvSN)MoUa**GgQR-pTeLT}<*vsCs-8!^W&A@pQTtsu8TloguqHgt+8ajc z6##5D-^-an)$!rAt|Eo6#*%vzdhqR;ra0P@MBF-!)`M=lVo>ZJ?g19Pagcp9j-lC> zZz;dU{>!IjK0U8jG}sxL_AAi2Ja8E01Xzzx&91(Fm=cQsA zNIbd}mlm>OwW6H&xVi7T0UM$HgbCu`U_u0di7fyonEyx;IEWt$FV&FqY-b|@UhnM9bq~<7 zb5lf}UjI|o_}LvqyM>QT9~uAYg~AV@IQ$(buq`S&(E*-_y!B5}19gqxzU%%#PMQzh zonYGPK^&b#bjgWJXC9M2Q1%A2Z%}TcP>`Jm^ZqJ_+dJD%8byJUNSatcFyW{nJI*Jx z8k9X-&m(=R)4T+h-I%Q0n|`lO>rO|HC8JouGX?C{`NRLvCr597Zg`PLdS8CO^V0-Q zw|!I#kPoaw{tgg_KLFzUFMyE8YgqaP5L1thrw{;1!+iO7Nn`99kTk&FFiC?;$Yx2| z<1a~r4&jHS;m)cNt4`v8rx48|V;|L6b|`cO70hP;aUYUfXBHX?Pfy|l+e|=uu=Sl4>2V$?Sk)5d{8h5pl(bP-D~Fr~ zL$zyjHMmoc303Wt(d2%L1c=F=ce6%TKxZfblWlIj*ub6N_@ZMa&he0yrCP_?aVyCKsOKiDdqbOHD1GPf>l^4Ay``02c0}Sx!vB4~BqD z>$_^Xi>?KiSBlp;1#-=Rzty?N2nOT?Gm5K=G%t=tDgvw+@X`jY6M<;1*+J1C&%-~= z2n(CWA8_8i)kLB`XC|6i&m}%q<21r!*m{)amV*@$nG0kX+(H7?t}xoOQg0r^`n?an zQdPJ+y=fK8tz=jQ_;^tF3UWcxxqtL$RwN6|hJv1l=dC`V9v>KxGYrk`W;iDd7Z50^ z9)Yb;#6Od`$Im35jJwC&9zTHjR~kR%Jy3e7yayU~6-ocdb{<~)2)Ex?GWs4ZJdxA> zNaUvQL>>c6}F zCzCFFhPTQ%j+4+fKTQ_JuXTFdmG9>gSo}k9@7>sM9$SHl>@DrUMwo0MRNYh5ORuBc z(lbZJF>?Q~(R`3Xn*y&6uL>|W&RYUz#{4r%tw!Zv?vxPx+0)1K^shAmC+cKddFJS0 ztz}3FipPY%Uyyw6PooOImsoVQ3>8`WZ?!+t&)VOR&MWc6th>Qt?4doz4n<-n6Y4t8 zgSka(s(G}u6l<&2pXvHRjN+g9w=KD0eGo%1&;U8UhI7qDbip4#?MHNMS^KQj5}Te60*s z`nq~YAh?(wRKc;3L12F@HX-}?WabY@RmD5V0}Uc(4w~8!Z>G1djfGg4sbC__vaHmf z_Fo2f#QRvdI+L)PKUK8{lxUiA-^?LBt)~eKx8WlV0Gt_~Om_xc3t@rD189U}h!x&T z5j#7wh{y!s5a#+#OtDC~EW2OK{W{W-WDXD0Ez4M$F0s;qZq@RTk-LPF6G(n0u`T!x zO4j`i1g-exsUCXfRsxxMBq49*Y}?W$>RZ`H!X_)$ulcfr$7udgRwSrArtRHRz}0&4 z`sw{L^$&4KJ+TAFQf5M#f_+<5s+;L4)~>BMT}E_0SMF9?QJ_%`JH0GYM9`XKj~BnQ zqh;SCRMc#0z)HNz@zm3#=C4OvQ6SK&3GfgbBr5^yJ9(ft+0rd$71ZDT?-~V1{(m(J z>JPlEW`5M@y099(Y@Z_hSBai<)I%d8n&AV3(Y*QT<36<8tI-3ns{JT^!5@k)A+y?( zvzAB#3@-G5+Ba48`j$QiNy7{63f@yGv72Y|p1@R1+yJ(>4?H5G3(=gn0h0Kp{3z|c zmyn87+@ne66Sb(#(Qy^(`HqSc4^=8`OiQvn7CS>aZAv%1f#QyG$FZnF+S^8m*MPcr zeKvL~dKw%VMx&y+dyB4w5- zFeN+KYuIA;IUBJ2j$9XFG2M_uTBr(uNeQz^4&Vo7-5#%lv9xPTCE2%QdW4fvlpMF+ z2&UuieHagMcto}{U096GMpM*Ydn=|UE~1`$-W@&HVp5;=`xZS2DG zx*nNd3=5>bu9OgPf%zKcxnjo}%PaN|=8u@Qn!}`ek1GpCivPh8y>|07%-fv64#YT4@|ql!njf-!XDp7?Mnbn~(zWs0 zd#VY$%Ht8B3)VCI^aRm=Pm#Mb{k6D*WY}^wgg6%T@cRnNwEQ@vG6f)bWEw zNR0m<%5y_5O&ul5qVFvIy9!m3&Bd>o@fw`JMLO_R%Zr-?(C04=nfw7nq&%|$K zIN-`_WeW&!_cT)hitF*>HMu`skT&|nXPYt)tn{?=XiXV59D&K1r3|g)7?Kk+pdmnY zp~}NGjb!L^ZFX-xvUfBv_If}9l}b2f-NM(zS$NXlb@tLqVfMT%9N zK0OCCzC~$Cni=0!cP`kiL;Bb9dj1DIpzY<6|A+FrX6Uh`a*z}EeV^fND-_9=U@mgZ zGw4^q!9%vvz*5`Skl#l`o`NzX5)u$Cjih=P*o;fCi6W!!vH;zD-D8YMtG3WYAGwf% z3NMhOB|Qb=*t$k-Btrh@+)4EH6|JhCaw6yfOw51w*)i0U)?QlMYGBTL`%JXIX5BCG z9neiYZt$H3JomFie_9eQGJjYSchtvooIBr9tL+?4LN#!R*4zH(#2S$`^z9KSuHz^ed&Wfi4yi^Gm)k z>mdJz!Lqy<8O>VUBTg6mBsCmDpb^jBy@1A>SG5L0UqqYi7cAkZB2iEd6aSK3tv5gp zT1?oK*wJvfNmgv%ihi3%gRbcsv+9(K%81EbZ~1`(fd2~43iEZ-lS#uSBVr`p0Jpj^?zySSHZni zcI%gmHr-9Uv*STN!>M7X-S z#|JE0yA4NiO;fYrf%RQ>q%_e;F$FPOkS?o-rF9YmyP0cCMawCHnPHAK%@>ltWoXl$ zBC4G@`Z>U4bLRfHVMn#9{XFFh-oI7IDjRh7UWkhHbt6q7TC{H2SCnidO3fNI6TiDM z+xmWvXmukwePRgnxKzIWOuHS~A5ymLCWvK2-a_K`sw#oYaeURf-aYY_(}R=z!nG~T z>MJ_iskkSwQrP$ZQ7L@RdK>#4Ul~h#`py|h8@+)9t(UU^?OfdXE36k^e|BT?%L~Be zbxy16FF(9N`F3jkCF*&*Lv~|zB*e&smBZcT8Kgyl568$2+~94mFL2dgkJA(^=zcZV zQe`LRHS0wf<;TxBf3Q4)m7HSbc8sJ}&BTa+uze3$5r2+TvVn^KaDvs4jltf4O&mVU z{b={(ftHHrO@?97?*+$-@KzgrD6*{3NYijL#Dm(9i(062OQ#4ZE-*!|KN(h9yj0*xp6yF zDO&Hpu+~3k7k1?_qiINOFyPuBNnGFJ2fUITmv?^{C^sLw+(9_I2NVc@o2)X*7Paf9 z=ichR`(hbvMlKk!HhEYkD2I*+v&3hu8WaIzR^alTH}EN|&X?ZGU(WS_`FCJ|Y9zyw zJt-id9+-4+Jtl>ZX1|y#7bAn62Ed>J?~fF`VHs7cLmEi>#qh;;{E=+E zq*3wPK6Agc%~YflyXczZ{|q=X>-j_s{tL}@v#^%V&k=-JM_JMcpsg^is977GgP!1-n92(!W{ zdM&P~+2;gve<;vFj%m=y&FHGwoBTiBePvW!%eHP4k`Mw3n?R6YI}O2t2G@?@uE8yW zHSP`@f&>C=EVzY0AdN$#jeF4G?%uf5G~IV~_CELC^WJ&mjrZe>amV8aH7I(mHEVTM zee?Tj)+|1}(EGkncBuk`rE)7pzD=6k{q!s|uAQto5@ox4CJ%Tf9yQeY6t3g|Jas!# z+4|x(>R`;D#AIC-@uk{mFtSnn55{ibtle9gHwBysMxEvCZF7c!Im0RG@3^>#gN><2 zj{y&D`&~weY7%ar>S^$8t#f3sg>U1dJlcHrn4|gpg&CEjYRQd{B7Y>bqGoHD8wvqE z#j9=^LqtO_(bz%tG&$%GJq4A0*2w5N7ct4Mq191`)K_FA?S>_G^asCRzI%IOI!QSh zYQOD$ky7$&*nC;PUtm^8rR4u3o;1GVKmZ`aClOhIGSLQU!dj-rrAlH(s2uO>=1PR6oaC8t!{a^nv<>y_Fw$Iuld%5@>! zBuTq%my9#iNWHbD5J?}`g*G8zI$Zas$pxO_D75GQbWA*%)!9lpmf(x(7DYS(C}8f5 zhQbdIA0MCdJ)<*^VppOP-J_>v^?t?;2X>cfS9!G^iL}2j!tO-`+zAWFe6kqT3h+E! zGg|-Jp=M#=D?)#xak>=v_niy98+)ufND=^7AfkPue&!~aLfnclz$$!`{?bl=ZF>Dz zG4X268@Jznq7J43qMW0t0Trno^Ok5ppPuJo6{Ve%#Ex_(Zy!0@M&N6feV-eNzpucF zC`WCr$mOi*r2)}j`e~~#!M7sC+lCI~M*%&hUE-`4VU)Lg9PbhR@Mu8T85V^YkIIc7 zcOyH3fzEw_bm^-ws(smg?!HZ5dQ3HeBol~`B)XP{r zGB9=NS%|}3%By_;rEW- z*~znQDlE6eScz3u1L^b5)hf9N?=t9f<$oxMC=k}o3XpVpL88@0jF6D}K%RYvjmZF8 zi1t$%81jZRczC_;96Vf>7`rc6V^T|@;M2@q5iFm1l#iF0Kl7mN#IKUB#{$JYSx#vK zHlHXD*^?FQx`hwwqbXpFokeikH?Y5#YVWg8SYS5d-y}%TkdbOf7pi+~2(Lhue^Zm% zsYo|~eN(x9N_g(8X}s!6+7oZAt*WniJ582Ix?K`V-O{-9bn@kOmdNC|p;UWez8hzD z7Sw1~P?AqtYJqd!m}&isdB+vUvs@kRTG!b!I5Q#ZFg!|Os>=HFvz+Dxo*j<^s?<}n{2!FKtvug%zX=+ z|C5TF?@E$i^=V#MnAwg+q>{o?ZY7W4V&Y8e2UXAZ0M+$I$~JB*u~?G#t$EZoD@&rw z{uId?$BpX0ftrW^6b1i*oTWh5-+W_3VV7=?;+rb0n4E1@a>x}i>`mt~oe=dm>YcY` z8i@9iYfi3KldAh%q0__SI{m%UK_lvzt44T$YRWxoB-7J-OF1HPR!TsSs0W@$Z0WE5 z`Fk2v-)GwPfFhdsqML$Qr+T4x>5oI;2rYw>4#l1uUR#x42eZ@Hjd$A%XTWDV)g5yO zyZIlzp1r;<%XH1W*50=BBN1Dx^%)0)g*a>TKR_zRtd=$xvT`dOe!qy9y?600igea_ zC%i0G_lNBq_2rT{HK-I|?Mdyb3OWs)jpW9w$Em@AV^I1JnCN}cH*_gEPgSXgd>0VK z3VwsBoK>1lz&kZgocGdU88ABHua>S-Ylk<68Qv^0CSrK3qwc2s^)rO0TmuU0&aH4x z^cAp0e+Y0xx<0N?_RrB8jCFFEwnZ5zQ6IeERC3a^r)DhiWq9DAPZrXzJv|mjrwrZg zu$Nc<&EaT!1U_UZ@3-xj4_i|r2x{&VH*mv0(Avy}Rpr>yc4&}aHPA{jS;^ngofPMH2ESQ{IqlUrsafg3 z>H58Ea))Xduw2LXOUjCpPq)B9z3|y^_(60+J>l~1^1+MjIfiRyl7uASqS+hP@0{L#R1X5Y;;ks@qbqvXH$#dDjxSuMZu zu1POfPH?fSIu~SuUwMYr$Rtq^LHGqW+FC%>i78qc+ZL_93R#l<+=}T@dDmKM1le1F zDi;elHmfBoUHpMKFc5#XxTC+BQ0r;HwQHG@XC=nsR8E%sRY?9hpGiVo3D~`KN3l2E zQ%riN;$9ceX;5By#TqwwB$DN_@a-7G)XRm%7jQ>JI$2VHbOd57PZjLzbzBPEazDR8XvbfT$ zo$XzYK@_Y?wO4V?ebpbi*|!#?eQ$0&@tKD%GOt*|-8fx_o8$A)%LbPD@WB?ui(v4U}ayi+vMVMR*nxcpe^ zlHdt$xJ^>rPwdeSbfuqxFUfn6on~L^TzyFMJvZKnEG9)lz1dC8Pp#u>!!KvT zevBV)WB6sR)}Nxq=0sP$Fcc>(hvgQ$X=0Lrf3if^_Ix4V^fb^trgfq5I#iuXB2B)_>z7J*vZ#SwFrSIne zX8-J~B)RG*^1FIn9Lx^Ph6}Be-_nE^8O;(je%KLHFMvKI%P?bHe1<=5G5r-8{J=pSZ-e}Ez)i#80Oxx|AYG&YDTxEj1kGX8{71rwT(?yVWr9$-AnkDdAw`9P+7a&$4P z>$aXqZ8k=*xgQKI=*P`k)|aG@BrWwn^s>&I(2v&4@N7<_K1oN0nT5$s0JGFOANUQ` zmDN6mT-6iLSJ*I1Vi_!%FV%O3EupWCftSWy%Zg z+9;~z4>xjyOu0XvQi;5B%GCgP{ILOYh&)EA@nT6l%{#`c5Az9~=J7aCP)~1t;bo>#q z*;bm0Iz&xMg z8B0H!np|trxBn>kT4sN{j0VoloaJ|r_jv8zt{fNp3c(K-PFe+Z<9(%}NL7zjkCk^D zr}%HY;?#4ibe>gxHHRJa+g?5GA;EYW`4wAD%UrhQt5t=GpfyK_4k$>sM3?7o-2!p+ zRJ<1gsRkR@S&L0~{vmn?=xnmJXcy8N_e@2Ogd0PpG}^j(7=;_4baV7E!2X zytP4k3Yp7_CY{KU;HM67nTBEv8u{^&$d6_|M2{A^`64)6|Js}Ex5?Tx3uMqE; z8*N)(haxeFJ!u)>cG-B{1|2+~b(OjSI6MoNs`13@9Iojz*Mae90hZmeR0d#uk^U-x^#}(C#;9Nc}?&A{pyHN{572d+XUyS6sqwf)-nVQOo4M zY0~aqbW9T2mKHX7`qtBV%w~G(=(sLjmpOYpv3G6c3`!%s9<>d3Si|=Up^tb3 zKk4vFJ9>lSX)Tt`SmPsUFjO=|E>|5Uvw_d1Vhs6Hb#`s6a{S_O_t3x(ms^!b=-njW zchf`N>vMf1qK;~T*=!YYE(N>EG>yW;P1aI%`ZHWxtZ^o>-GWs=z}3b`p8Sv^FU-)`O_3`d`(VRzHq4+KX;$!O$vVvn;3&ny}HPX~d070d@mmNwhV8vjVB&OS%VgLcf9adM40i#@x}nS_ z;PdnG@Rmzeb@fUYnX-XLYb?XZ%hTi4;0+j5Z%%`4NuA=xgFgd~iCMwR`iUxArXfTT zK0bIl*r1HA?%J|KrGj`5$iWN1*Ah0ZV*`nMD(QBJyA~}wdQ3^+E+itvUC15%&OJ|W zs*A$m6te00DJ`|`EP=Ndaj3MyD-F^mnRPWRKW#z+x&UZ#Yo+*?Ch+^MztZOY_flq4 zUs`#j@{JGomgD=*QT^xuFu3?sV}!qd;gQ{#e$6)CZrwN~dHrE@u-lBm8zLRZq8bAV z{~5=4oL};Cjl|(H?ka`m#rDA+LhD?D%E6b{9$R&Z9ocrDMvX#a7_(c*c20zk0|;ef zXLVmpZoZDhq{@JAW{yqD)o8mr@8F4 z4{ery5FlcFN(I#FQv{xY-%Rw+bT(*w=UgAkUD8iGn!QrvHUb=~?0!^W5fQ-)?&uV5 zFNFdfciEibfWLr!NOGnY4%Aicc+?)Q+?XyG+AJ%~MiTUAXj9p2=TCR2AzZ0Mz)mYv zn8vV&&RlqLQA}k31x6p0i|P7};b0T9FYrC&F<9tyJVR(&b)f6|Jsyb19l(31<0{1I z_jpoLz9wxynkLG4U2~s$*9ufj0P+%$;6gM3pMh&>O^GJO~&hAkjQ^>4dfCCS6<>%yo~gDdspZu0kMB^t_p z`q04IH~iwy;li$N(uq3O#LQ4T1>hmAF;Z2Go()*d{NvL!YL~i|u!N6NurfA$bdqCMbm4+;*;s#EkEa$|1Dqu!K*VZR{d*mUAGR~ zilllPNjn9Qo-ZM%&r7_ya}|f({6wxt8q-5jYyDdn{j-^8xUV6fL|RHk zNg3$e;_@Wh{e83C>+X@QemU-^le^QDWNHwPkw1?@+?Xg=%wMX^wPq8ePX}(!T}a7B z6%U$Blw-bB+AU#PP$jqDPN~x5vQ@e)A_66qS@hb0 zGVV6GjJZ`t=1?1sh%W^M_rKy}0h>3nZ$x?5cyaoG@#eD>q2W4+WWDsmusxyhy2T4v0c*!m9<{GA1I+27fA#0X7eFlgHFgE<9X{KQ zr}eR^XK@6eKH`7y%&7AUXZKx*@vy7iIg!Q@kO3=Ez8Ic|*YK_Ab!{+=n&&t14koY> z%;WLpCq4Cs&d_6rvcG%6K~NN!n(=DjFvu+($yxFl=hPtzT0a6X$;q8nGv4bwHP){= zR$({DJx2;>CZRf6F&Ykv`e|oa62k}v8-yHg+LwAX$1`TwDH3yEJ6}OMeh8O~7|sBq zk{kyyNSFKnP#Je`D0_C?Vi13X4e z-Y?=C_hDRmYs2~Z+{+bXwy}Q+To%oL0hi5((*G5>78F@^8xky+wh2C+Wa}`0F+Byk zvs7RaGqCA{#GB5zBDB-_s}f@T2!EbHDE(go7fopsR8ipMN@ym4F^ag6N4vxf9(3RS zLlePEuSjbw<6e`I$M1Nc&x|6jH_g`@f0m9YlKvTRGtKdNZfoM!O`ohWY!;*yn2?|R z&C8-+!B%mfFjV@*q&%7G`O+584{sS>lb{%h?|^jamD%7sg1?mJ*N@!)%Cy4vcg7XA zj3@NFE}}$s1mSP7O=u7E1=DA;8;mN(bg?&;5xZ?Lu&05!K%Q8Og$B`!1C3X(uv**Uw*^|rI4e+`) zV>(H$=_44DnHR}6+a?LQ%1Ibc9|1VSMJI1U>Q{E-${Ez`#B28Oqam_J522&b;1t#7 z9TCmVmpATplbp1^*fzd;TA@I}?v_E#5uQC0gU?kZOplQf>^6t6oqd`+{YK`T+5{ju zf73FEOS0WWh;>b(Y(J$$?kKFD5hnH}q3kbO)`j`vBk`iB=f?CvUS_zKQ%)4ear_1_ z$+OgSOMa?xe5Z3h|LsYqsrKxLk~@%{nPFFsyCBoD2V@F+qJCPzWRq}A$iVJ)SQBkm z@23JdJzj#fPL6+o2^{CI|6w|@-=h50_-zx#RII&(3c#)MzZ9&Lx+w=n-Rn?&3baV$ z2C8Jd5EI~O)10CGTg(CMkWo=$ZR*@?=O*P;=uhX0K!Ywuci$Znd*IAF9q7G1(HqU> z0Iw!A+^8zG@_+zfVX#4i%mp~X*>L#)zw$_UqFv!Xp+mM+ikmshn0~cphA+dJtHFS| zF(p9ESl4nCb?S=NUrQpMEC4Pf7{#-bXWaCWeha5MG4Q`7mlR$wq!0jnjW4cx=YjQQ z0Pp%tqbAi3PxE>2{;sG1f3H#f`@}`*%C)HCa*P17Et|~n9fM5Go13qy0R|+*($-dw z)8o*a4xoSz56f*2O%jZbLs4Rf<8g%DcFK6uvg37c)}Ko-TBSZoqSF<{-iMCd$dwH* ziP3+?ji}#zQ=X!mlC;J^=DodKeDfwR6YFw=!jZU)frKwyvJs1*>$w;f;Qh% zy^6P>=NSzJgIV%`7h|6Bl|aJstT1qg^0;2ctm$VxyF^t25#gqBW5V1a5FB!_8 zih!OP>b#%I=lAY=Ky+DBKwRcx5WdO@g^}zt%;gmLBZIZ2NBuZuECb*9Lr^Izi4xr_ z|3~g=?n~cDTX*W*=`?Mh0w-J6g{>fH*fmp;Sc1LxyhpMq_)mCvO4YF_zvt1*D1O@< z7qRNxbo8(rEn``?smDU)YQ>bx9nfM7eUhlC`-I*r%8vt9 zA4`$b10So!^|30gk#9dt5LZGq`XLAQ-Ny%m3;x*abKqB({Es^9!8SLh%>;o8C5#K* zH!v|2!-}f^gm@KQZ176&ppK8=a;p?ld83~=l+Ib}Vj3Eq5t%uet}hfYUWA9wdf?;y zS!H@udQbuVniM?zD8?84#oCBJeL4&CmuATSX6}aXJ#1q?#rSSqyr>^I{jAdqxL9}R zwU*j}>pxQrHHdR33IhE;Be7&5LC9EtkI{gN6pL?p(yf1D){2{62wozWi79%ihZ*Mo zS!^9Y9&7;}Xfa9RC0BJTl$0^(YeTlIJnU`vMjtkGao;$+2sV=`TBGAew@qwhZi7+Z zHcYTQW~=RFO04Kmf7F@@68r_bgFYkKit#Un=7nwMhQt{P>z*Sh_Rll4k0ldjxn0P8 z+Rw8evLP6D^bf`lpB*wnDN2owDI@l=cMJ7_?=%a@E?X~nG1p{I9x@M|Q}M+33L}(M z)N--qSY_eOCAYDwY$GB&ozC9&Xbzz)?oq1>LMmkOS4QMm=&u2X`n!1#sOMkp;(<8& zM+@HiH|2#b?Kp0p=`7!VpFK7sqpQ0u%xq6f=J{px(7mYu{eEc^vLXHTU@AKXhF*7P zix~^s;bo=WSrXj#Un`6G_S=-LH>;!aT+u+W{1GNpRnXLjpj8_Rz@FB3qe%4L zmEyBA|Lo86tz#rqTWueF>4;trV(HJC(imw@Jn8mIPDQB(3k>dXNtg~n7|&hP*iEt6 z0zcI#;c`h|DoD$%^K1=vM8t|IwCnLJ6VeybK(cV%a)LQm+F2{53$a|2EG$I!Vq^7p zMhPC*6K=%vZRLBiXU8&^TN_QNy>n7XJJEUCKAHhsutL&=HF~+k(0IMJ)O2-%MlztV zD}LuNM(5+wt@hNzS0_dO*tydUNP^rM75RP4+YI+@;irt0#w@?%5eg3m0H;+!b=gcR zZZa*B;h-`qR2-R9CliI74G;4ee7=4pKpGSG+C@a+3Klm#?~~!&7oXCyC5@GvQqq63 zG*jJG;

#T@As)AeYsokZA6yCG@q*1jI{i%Nt(D{LOXa%{VMC)0NF9p?vAr|)4AW%26Y%gZJ|pVr0KmAf zhRC&_L5WOHe#g@h_#I^G`GD+4mx}J`9{>G}ZPJer${0Zdfo$rS1pLr_H%Xt??fn^e zTG-|jWc`Ubdg%;pA=87}6WfBRBu;~+_eY=Y--k$G;AqcXl+HswimBOSjm@~@$(PP&xgFEaI2+9O@W@`%$%fC{ zDP||b)RTu{-*3ig(QTx6`|%>Mle88v$v&;A9I8yrm&^6F5H3{HB`gz*qReErQ#e)o z^@L+=&!YsP@UT_(dD}kZu#BaS7oa-Bi3OW*>+L+=Z0Ai6W-C>yjvkVdR-fjQbH{2v zJO<9ZoZ*F&kxtp4fM*D1iYDObt)OYKC)Zh1X{DPi<{hsS2xO&GB+>UEm9$XR=Q&(HTo4S%x}Q@s5IR&>V+6~8ShV+y z23K#YCr8H{4#=583k0dY?-cr}?ZR*`Vp0BQyvAWpx{Af2a*K_vy*)${$fhqIE5z{` z2vi06tjZLZih3^p+;VyS6<9}LFqKwuXlZycj}tGN+dgQ2TE=&Uug^` zRQ!zq$s^BdXAFp91o@=lo{aRlbNy6D{kyI3UiUIk3mT^^V0L+vWdr?yeJG$(RMKA=4CR15pt#Td+sbUV z+-2GSt67NDvNIN8Z9beSro_yJzoJGoaG_ej&%dp-R4BOksq(GjBG0r65X}<*>SNI5 zseDRiRdLbu0j*#b4v~HH!fQg6v0sNLPz4dfa3%$i|CI{z-&dw+u=`bxY;iQQB^B-N z?!Fho^|s~|55eu=9q zR)qasbLfB3Ear?zFSDceoRKXhZ9sZFw(FeET-55vnGnGHKdZa_4;=;nznOK2#S0`g V=l%V&;(J^#ke61GDt>7i_#ZiQOU?iQ diff --git a/lib/esp8266-oled-ssd1306-master/resources/xbmPreview.png b/lib/esp8266-oled-ssd1306-master/resources/xbmPreview.png deleted file mode 100644 index 70ea3a53a843f220f18d2ef7cc0698516a990ba6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41692 zcmYiMWmFx()&+_Ng1fs*Ai&1m-QC^YosC0qcemid-GT+n#@$_mZQT8G&iCHE=STmk z>d{p_R?Ssw&bgwL6{V072oSz}`GPDXEw1|I3k3Xs9yplKkqGNq^Dke5zQ~A+r~_BQ z8?Znut^7I@bO$6(yx0-4#xKECuBterJcqe%lR9MZ2f3JVl;_+lauFp-FzNO{>UU#)u6vPu?Kl4hu(no6F~!m%A2a2x=O*CvU{?&=|$xJpv#LN zCCan`N$+Cq-ySQd)#KEBX+LLZLIrmo`aje(8%3{T9=7@e5lAb;nx9YsWTLCh5@Jk< zfy$cIqF23>q!mb_tMQf49`f%M5Lc_+;i=fq%%NTg8$ycOppX`TkRx{s@g{Zt+Wi-Y zwknbrv$gbsa(gw@u|RmHNWtD+P%0kcMI2db5KvoOYR(-lxmBB6S`PpYOswswXU`Y*Qb?vkBBs!^6}~uTKZT@|1EN8g0)|}HC!uO zlP`Iyg`bVQK!|2uaGgTiF*CQU^?6OOHgeQEe=QCnRCn;V62_X_IcS{-81!M;uw;Oo zm&rb2`aw~xw(^&xmQRSk2+oGyC)A2r5KWNe2WptnTM?wFwhbv&R%W)$TC2lLlKc^t z4+U-6m1uu#91oTckJzr0?cL|$4FU;UgS z*+n!&AIPx3`XO0#cDK7V7QxV^lhP`>Dlh(`(%-PdbD{zrT1JGnCLd@j^{8f*9f$zH!5)v+vs`)P&cSZLj2U`*1n0X39nqLByde*0SCbe*J zfL#u`h^Z^sg-ydba^Nt;m4emgH`uUlS^-xHsnc{z-x4*LOjL5R^OQAa77lC?a#>V# z!8v%iF>>g^wh&PgKy|}a`sZPhN%4~-krnpi6Fxv6qUtq`FL?dIyJKaVbA~1qB4Na0 zRsO&n+QJ>fITseNQ$^0Ob+G~;2wI5^B-tg*7)zD!-*NIsbjYLCOe}u%-Z|X-vfup= z0UfREfpfkzY+z_ z$*uK=>BSgr%^&?<5$qHt7LQA>CN+gcy)~GWV4H*zrbjO!#?^d)Jysnqeb)9wA%tiaKR6j&1zds={dlvy*Y{J>5@=vnG*;Fw;(yY%SI^kTBFmfrg^ z8wsB}@)MYZ>HUy2q8_pbi+>?}4AATmKK zex_6NE;zD+cD)9NugB1$8z?Yftx+f2PZ3MsXV@IdD7ZSiPd{tT#>(7O2i!b zfoJQ{mY(A(R|-9-si{kTwbl<9A{L|A2QPU{5d1oYe?@$LW`)3L1oKEes zhd^`X!BO&=+Y~X1YKvZ7;U)DJ6R#A>$OAUwT5=OwUX9RJ|o+*1idL+_3lGfrt42tBxXBV*}TSYIdaVWHe-32nJ{?b?qUO4)ead2WrT zxk+0;mTJ}OPQl!71vdEMxK$jbRd@6yr^sWaa|iULXsF$!U^G+=3|GDk zTTSTf{#$r97P1%iXaqY4exVBq@>je#B7Vx!bVukUO@UwSy8JxbyCw-Hp0_dCfM;g`CSYSz9SFQg1%JkfX>yJKUO9B z)vqJ(UDV-NKI~f`C+((~5;Zhjg_QA0^E@7+%oI!*Nh?uL^sUEu|iY8YWl!0Rucg$wEz+FtKYifb3aVB zxG1vIqnSG&k3>ZUtFElH%#hcbiQF!*F6t5BpQL*_y9=?vwKQQqM=~b)KB|ed==LQ2 z_;6Y4@qU-AxB1RY0wb#Ke;mZX!>(wKY5s?qu_77;a?^q5wS=sf56kX?a zxCSFqP@AZ6LE#$G!?-tLTta-{m+-Dwa)*foR;ty2`zm@;cB*|g<^cvOZe%)6zmDd_ zyw?t-ytW@K#)+ ztDbKThe_6H`FE3)v?Po9bGMuO4D@GTOOH2s{ML0jikQ8vNqbh!kEtdwh#jKb_PbG& z@P(R#5@|NF4w4uFu2FlK)t0D$ZTZXx(Rb%1=In>YQ(PB$in8!trH$9;%j+Yqqm&nJ6iRtWDoj%sC@Lh*)8{vmslKvQ0`VYht!*n1A}>ui;x;T=e#U$E z2Dysu&E#(=^Ke#AIS((5k74?}+7rsXa8X7VDy+H+k=cH*mzx12;G$*!`4GuUeIpxp zC>`lpyO?m^7_}RAQ)6^PuriD)Sj{>(%tAx6nT<>onpmV(0`pA)yoVv^lwzBVAE5{R z2?HR=8#EqoRU$%PA<`pF__CGbC~9BVo75HVCapv>3T8+46)PVt!jdT180h`I9B#IeJVe?pyet4$6WK^1}}6Rw)ryxT!cfDbP>(+Sy1CYH&}4 zV~dU6y^~!LY6u5W$|k4c_-oq)w81~l%AiNP8?k5tqx?b7tYSLafbqt$voQ#k>p-Z_ zx}YfMCo=}IAxbK&-I{vrI73#K*D_4#=-9xsshD@|GHKISf7-*a19JjSE|x}kN28k? zqL|0iQjqx;g-QYqYsNmR&Nczf1kS*%ORxqi`$!f4tTt`_4Vl9jPRF1$Cgf<~Bci*w zax8MC**`Ee>Usdb#UiQ_b@{Vl!goRFS&F4)SVPYztQ#it-8G=6=0F1ys)NFt%EbYB zb*#{3aQaS4@`pAnMvNtR z-wg3IEu^H^{`t-w%LqwHOF#5fv`Ybsj*kmU2jOfGyD}07n8$`eib((Y(LUGB>CHt15n>ft2A1T`S z$kws*cAe!oHS6Yck>P6+WqboYNZX1`xW|Ji_)07~zRvzlE}m_Y>C|`#G@`*dV;Eaw zs-YL85WuhERcj!tGRjV$_r2KiEp13TOV%|`DM?}@J5EQHO^}HEEqDoAtBjRUfVT#4 z0PuaQ*>KU$X50k59mc{rq*z9NY-#(5>t=U;1a>Wv8gcPGe zd1nzNr93Z#FE2M1scZREbyY@+%EL|5fsD_gr&D-nJr8AGk2YIXH<6m-y<3PeDe|d= zt0$oO&1>A_U%Vd630MjM>_yXYLKHih&WAID0#^JBW&o39G_udlq?zh1(N8D1zO3QYyk z`nk$?9#q6j^>Ka(^eeM|8Ch2BF+bcMBbABE*xF}#YZFSeEImgj?ozSsxlVT%eyx3Z z^3re^t9#Od6h+A@`N~x~d3qroH`V?9uIDnJu6gicp6y;XWAm*g)eS5$(t3q7l_Ul@ zn+BvG!%7JY-X)9O=v4gcKUH0vS(~lk3ZS+i>H$>nXh`!)z?Qx-1&|r|e#li^4vr;f z>v3%9euF)tXvjDGcr6&bAq=o71qt0`mDl6c^xAln2b#W!9L7BoMhv#8-5Oo^U;L@N zQ0VP{nqmF0~zY93+Y11 zgL?>Pk2?z*yb979r3e74`MJ}S)FePdS9?Jk1@t;T^0JsEcC;}@*v-AqLX5)NGIE<2 zcqsN=rfbW_^o^eqHW3csJ&4i$zzcz~6ezYzyIHI&T+Gf`sTK|H{Ps4``)~Yfc{}bvl@7382B=fn4!nhejC|4BWM4* zwD}=RfI0Jx!yrJ6wL(+_}Ftj|2rsP~3MQ7R9eBxCWx;Rc^4S#Md7wc6&5kh1jf zAlyjNd$YR#yN694p1?2f`RV-Ep4Uz$OiNygh?4xAiz_}J{`*ZIBUv=c>g`Q>q4VX@ zOa7Oup>C%B_apMBeM9Eg_d>?1*(B0CkulBhdPwC1&$Y5zgO|CJCwfd0QIlsYE+Rgn zu{iUu1RA~)b-KJ}(ih!{iDR+7i|FF8`DwHC*iiy)>q-0FkAf`XOE`H7BntdAhIW4P z9)#ue#ZJl?YL^=oslOa9<1-Wy{($vjfxPnQUY zv*`d)wLJQ7z42cA?|QM_z*5&l-!Sa`8hkT-dXQEsA=$s%w;;|UJCD&6(Cje^Kz&%6 zS>A`RSbMHGe>lh#sO$m6tk%$d?d77{N4*24(T-FozvdC(Yd;8u>@uN#i@UB;*Gw0^ zdLY6mbl3`%Hyw1&q){$Ze&t`fhvIs3mZrsu?mbky2aK*Dhlv*ELTb=r$$kzZP*Y(c zgkfMHgo(n#e@=vkiWWX@iSa;JD$}MWD$`<(4yhxA?at_@zZsB@0e@1dd=6Q0sjsG@ z0>5JgP8C2R+X90U;T-?Fx|glG@EPh94GYCD5gi?bp{O(Iv18(PiOzUT0JL%t&GMgdz2;v&7%GpNU02 z=XHM6j&nPfvz4U!8E=@C)@<%2w8-qAfaNrBXkRpQ$tshWLmADiDFeE73(|oJ4EM^v z@btWUY1bJM%}dMbS(1>SM^DlUy5T)v)2-KP5*mvZjy9W+hlb>)XBx1(yZ_?-;`~D> za1|+&6~{L9WER*L>Y`&2~PFjJ|aYQ z&=KX96J~T?`nGZf7o_~*ZrL)9c?KQiG=W9SR*btQtDERbI*{iC$1BG2z3l3il$fLQ1bt2=WfM-~7&=DyLRB^H>&mR8dT} z4tW>l_$$gKC}+pj{0WZ&WFlZ2GLd-E1tG8!KYY0m-gL!4#cNGAZA0ujfIO|Y9rIqv zy|2Ucr{`A5fFTdZM%R~_>wNnWbiT(8(Ff@V`dEzbD``m#wU@>w5o zzk^Xk<NI$rL!0te4h~<$|+NwKNFe+pr@0J~!nbO~S8ylq!UdvFW>C z=Krjuc!ZQ^zE4OO`1o zJTOrXSGU8`opY;@Bu%P$ObxV(|JYG7EGkI-TtHYpl9xz5kll~47tZR%NB9x9?a+zi zfPa1z%`WQu8~1n0dJ=kV#OkZ??GnwNauVsY#4$OaK6=C+A0(Q27l8yP0kDt_>j|sF)w|mQ@K72e|1=&X^)(FGLl4pNB)P29 z3f%_dcexN|i^a9N)3Fh-ykQCp0?;>TNDUd!5!gR$hUUCgJk|wv2GgcIT{i2-CmWmP zo{aZLH8N9i*Y%^L<2ABPB(>A={$}`V2FT^ADCW>71rbs>TNr(pEs~$fw+}O*Shg^L zMrohRi5oaThnv-dBb=y^zFCj0+p-=^yLTu0r8u~mr&8ZK^CM?P_VG6%`Iay8{WpR= zc`1ZRO$l;we$DjrCK%$C09q{SW_cxF&haHh4Rk*Ctmnt1;X6ZE-57v1 zZ?fWRb;7wT6t8KC)9E-9SvM0Z%KsN)QTV6}?MRZ@yxjI;}sc#d*YyH=QWb~GFG0I&&V^#pO7bQ?J&j{Yh~ zjkNF!VOzsjH+I%TI#n(Ru$0^Q+rL3|37?9kI0iwd*NHYb6Uf(2zwXds~WgZV>`te zR2VKd+H#sYXi+Yk^HfrvsZfP)HU^)V&zC(?2#unL?RZLGbBJkF!Zvox?-aIu+4(z` zxZ6m=OJCMMW=b9HhcunL9TRQr299q7RY}n^lX2qdI7N|0^K+67d2_fLESh*_(V-)oIK{**5{iSJ3*q*FF$=ymzSlxFx!V)uH z0hQA+*AGCK+x?ivFblee;#;>y7}Ar9c4_}cFm*BZ*A9fxAeYM7s8$2-yv7P!Q#6=l zaLOba8}kk3l)~jf9@px*T*t&urB4MXcvqffK<&R0emT)7TYV9)x)|mt^jAjH2oH9KKIl zXcY-OH|n!!qsP8j$QRZL#$!M4wrg|h7*u~v7Pxb+?T2QHG)}Qtt^ISwue82m6kAv8 zu{#&zv8;vusX?B3szVkjP;~DsB{s?Ub?bpDu(4V z7;l(v+CtEvS(Ug~6(Ly=_y4M0|$>!z=Hm=8^edldu=$9P=1z?IAc}Q1qdeeVN)M{l z*Sf`iqMY-^L`jD(rk+FeMO@#pX}}U(q(?jK!O1f8;Z`OI>T}h~W$+M}{E5EGD&Slx z2v0KvLo;^09I3oU&8>|Wv|)1&Jl*!Zwdx!&9)SWQZAG%3>9jL&GOt{z6Jkl(iv#3+wJk=UyyGFwVf1zS54a zHPzDJN(*26y5_<@MiOHwKRC{l(x9tG98Ty_Z&CA26HpLwN1xtr{6>HUTHSN(rWbT@ z>yxs8X~$zmO#64^B4|my$+R$VmfCWJFfSSq1Tkr)yR(qbo`#vWY_J`}sFFSU+|=dr z`y1q8&fewumF;cG+D~elFNY|eeFLjsVsNU=uD~|Rf%o{ic2NTd`R40K@`GX5WVc#> zcV+$YCR2S(jam`iwS*8qYTTwXjpMLxN`W1eh|qm@NsO{YI=yhLE64FvV5dka+eG|z z`3%Z=duaW;j3UuyfZG2RQ~wR=vQr+SUY^x-l6AyiDA@TD-W_=j$mbX_Wn9}(ZCHi* z&g$TXNqKGCs(l>>Nr!qtpSfIg>n=PO6gqG8XKB|>D7yh{?!?28WMP3&LV#rQgsVCh z^zK0q2332lm_rG5G=;oNEdUX>6Hz&lIOts83n~3+M^cUbH-^BWy~(z zS0d~_s+b1-(0A5AU^YOcg|@~VJ)PdAixG~#NLJyLaeZrzx$+7*Q z_LPLxE9d-+qsVn@7%d{pP}H^Z{wEcn?!$0scwVOG)L&V-m*(%~!#z80jeJ=A%YyFL zD2?kd4^tLCodE2RE}cF4ak0HR^9p;A{PqG9*2Q%mIWfM*$o zEFN?_O~ygnIPc7-TOdErjtf6W0D~&=opaGFcE*XRi$ZE^qk6`{uc_v~7|`ieCl{>p zVI3@P@v(LEl34qSeKWM}o}UG`V{3TdR={^fElJ8D8A1;klwY6eRO6EK2tX9UJfIQj z%o>V;@yYxCWAsgnQB126rm5hx2g=e6O=0O2rY{%l#WQz$M0!mJ5B(dublZ$3xYE`M z?`5&2s8+!?hEZy=7)Ip)B~g$z59*o(X44ah-m0J##k^$jl}9h7KH*P_=sa?Q$2`T_ z?_Al#vx_*jaZ8JeM}Mo@H}GvCOu#=Mjk${IRdeW?wieDhVV>+U%w(R%9X>dKHu`L# zvTB8u);Y0YPsx|8zOLWm7V)otf%qHaT*C&Nc5MgWmOXIQS4iY;zEb2AA*84QwP&x# za86|RR}OUI(gYx(sr+$u6~(G9={QuHj)#;Eq*Q>)sHT-AeL>tXK4?a$yD!}nZ-nUK z$NDk0;CO_z5Z*Eg$&kYZt@f?5Pg;vm&@R9404+@%nd3HFD13^3X|M7D=$GFnUg}Q0 zIjjtV4a;s%s)fEu!(?$RAy`Ew1s(jfx|A&B^wG}`fBTySyuO;j;}Au*wGDJES%u>l z2;e6nVvB>P-wT2em zLqwxrgD4i5P0X93fubHJVs&M%Ul-T^w8#y{p{`_@b>X2)A!-QN+~Zcq3ohn^{ABF^ z7g3hmL^!S`QMX*=AIS4F+Z_bnhI-Qk){&-O{`)*OyQU6Q&(Ym3Z9?z`NF_Ua!ThRc0){i1V6$Z!cD@Je+9#ci+Bk&Pkp+$AHcz4H!+)EmvS}0sQ+IF0Z zoEE#r5*R+$1BuwyKY*xT*-M_cbMcDC2zJ3N%9ua?vE!zO>HcI~fL<$Bxa@zow^v$@ zKIex8%Wz5+o$9V1O$d8$+pg0-ho2v=zG5N~yhSjPa0Z^OLnITMVQvI&|>#>ir%hVr3)d%XR)RRfmdya-%3&yw_>@c@&Wm z^YMf26ApkE>O6wovq9s^fbjwzn5$NTBQ)x8hItF(d727x^BV^6>P{3xf`WAGjBo@T z$_hZ``4b27CQOhjIQ9*YBbzY4>J38O`G`aD5+!#KSYfbjERbFuxHPn^?wE#+^au_XQfyV-56kUunifox4>JoYOfJ7oqlCt#BAQX$si}P!=e@Sb$n# zcZBo#6t!TAXr{hq47r&<2+?X#YyPUHm`C8O%J0icM_NZamGtc9!mhi=HdU@BTuG{= z1y9wB7)C<)9LSA@2W{fGc9C zVl3KMGo!z@^gTYSBCfK6XEH3q$0#z*d%I`C^8{D94Xoo%F5}t_?IR@E%6{?=!Zlc` zu^4^=zkP3WUow{1MAV_Zqcayan8tqJ_ zsb7FU;b@|}rVY1)a8XLOwQy}+j63-z)1|fp$}ePG`jaRE!CNGe#h)fhEe}LD^tm;r z7_W{^GqvZVlSJF&z{tt#Vi8{e=JTAr%C6IoqDuwt*~I7^KVlT@-^Kd&WQ~1( zr1Rl~Kj|lkL#WBy*Ym^QiQl=qHejuy)i=-N`or=ZkCeDTHQ{JPQ*J|RoynI4bsl2R zci2Sq4S#V%EZQ44P~L3Pft^CwM)`~)Zmj^r%Emt5Ji|wiHXcDImOmiAr9Ib62Cz$J z?60`JX~H&a($w1iCFIk~Q#yyP^x|M9+r=HlNeqSpL4u|NypSiQ&#XLXHxke`h4JCR zil^muwCf)Gt#Z5|{ICf9auF|ztY!voCJ(9;+f4b4A2sP;Q~iJ=ry#F(WK){SaHI12iR`ChXGQhfNhCjk(G!$o z=9G=OUc;e+wi8r@N7x*uQ^>@H2Dr9y)=9@aC*9WkpXvxa)hwnI3m~8vckbdv_$$mwtQoEc#j;e2%TV&Ak_Lrqn-Kw`MB}$`*h~3#Z>lyPFI^Ge_a>SipVLCx z`LttDAIf(ey@(9YCjabQbEiT%_8*TL6HPYvZ-hty1GW^wBWyz(-;BT*V#+Hw6X-{> zO?_50b}Bo)@DsC&x+ zkr{fFF0>$ssRhvPyScEHovBMX4NRb41I$G$&vu^e*REDGOv4G?XoJ=i<2HK)QB9$! zy0y5g0)eu}5>sSBMy)3hqgt0%S=dx1*Iw}~Hqh?S)_iR|>?Ibx9W10mZG--7@OO8} z)r}p-ws_tRepbPu+p|smbXM?Yzcf~uW61xc;3C#psJJ8_T34fprm)RrY=<~Ci&}0q z%+PcyxvurBNH3+-T~iN+?7>7S9Mj)K6Dt3fcghmJh6UqEdkv{v{k535x<8V2mn?>Z zTzL{$v5sLD@o!~QTnyWBmO$cl>r$o(H*efn^Jmc}iJFA6(?1*SukmH{*x!+tfg0_X zSIM`Iq2vphNppF+5Xn?WxD(NB47@^}^Y&tPljg_VOVEizhHM0TJ}0p*lYcI9T-7k$h~4OQ}D;oo2M}uZEwirabfrAvh3F|<7W(G3f^0_`7C#m z@c1mTNJwkK-70h+tG7@u?jrVzCBiTa+( zMYOp=w`IkyvQqf-Z%C;x;D+I-E-yiW4O9`UC3aFdL%YlkBbCy+?Wx4rXrJ*%)rzFD z`1a~NiSBiv6i44NW^m;nEqKxkWNh+f^y|2DAmqkN3OzQIgG3%2o=yv`hebKBsWP^0 zY<`decl!WF2=qI*-u{nnQz?cq4@1=dS4%347_4!XYJOrE(sS6fMw#g=JyYb?1INFl z{2&_fkO}u}Cx-j_F_oQk? zME~&D*o}L$eL7lXrqJAqAlcdo%?DC$@~TH}w75t=Ed3RB`7${#xs&>+H|p48 zu_tapJI<*jWtUC;{vssw#|oCxW!34MW4Mx3%M2bv=1-VCD-A_)JL9%7^K}KI=)P&4 zdN%E7ntZ&dnc{h6{6^%D_f47CXZi1f)+``CXgL!eJ1YGpr5`W5E|1mr*puP%!SUMP zR*Z+`{T6M0%#%^J=UD$N<%f_c{Kw3qhZqq?S;%4m1<4^<@a&YtGY?F`HAsDv;%e)> zSaJS*Z-qQgbf#QNeK9OkyS9&5U1+7rZBHzI2E8~;L-L`|a2(^l`a>$OXBL64C#>&# zReFg54-mB^PJ^Vq?XeF^ID0W~o4`7Ej6!@%^u56|6w-*1 zdl~y=LJ01qd=y(^m>hkQ-1M@{E2Z&3{j=CVhz8`=C|g0ezbTzNIN_$2pm!Cez9=pM zDM!U%aT8j0+v&4FPoK%L)z%vjpdS6{4A;mH!%KwDxJ_;dpMt=b)A3t zW!C@L5Xq?<Y)bt-2;>y;0&bYPZf%Z{H_HkatgjBb)$oYd(7Y0E%U#3zl|FnKzI zX%(LirKhO%IOZ95{!PONGuUo^|73J!`(~ zTyuC_NK&VQ$=-8Lf>i~#{4Z}ka<~x-elvaawWXk0gBoERLv!jTks$xg{Vlw#k@v&n zvn5PCv6eTU3&zAMlf1P&d*^*f_s-;-G`m6NZWpAEAn)={X=C{`DyYiLMcZ9{B_9g2 zYttZ7ENYTlAN$9UA+f>`kBcKw*ypIEIyqRrA-m z8Az}~Q~yq0K>KWBC}rBtBUqmJ9t*e|aI|?op}s;KTQ`YQ$m-0NuBaOF?w96PRFc^3 zLq%AS%P6H$CZK<7wR0e_GL1%^4HoxC%8k?Pvp#hP>e?04euh)6#<_hO`gq5)-TODD z*7HrhcO^kt9A2?s$0hk{h9>tY|X;2ca!KWCqM4y}=G);CN!~X&9<@T>H2XMWByK zU0(fd1A#^Yp7zGz1uE7mUb1n6;Zs2vkoIMi1FxnTC*apRx48HInY7vL&e%_ZdYop8%F-Rwqerz`W?zKJI=bvg4@760e6*61=lfXRo$P8$96~74f3~} zfHV670si@^3Js*hKT{r9fBf^|yVcm21Bo9CuInem{r@rR7+getzu9)J>P7oq)NpHo z!#4Q?Z_A+zDOaFj**JGqV-O9oPNna!vRtu-bVR*2%_2GT%S^j548u?0*m5Bb@05zT zt$B89=4TiKgA=By0`Z%+e|x|P^613=R@X~&Hn{zip!|Y~pJj`jyS!*d$G`pY+l0%F zo)@0(SK$Wpz}w{$1vhihB}A!;-p_QHx@%G!KQE)#Z#1H1BU+k;!+lwKuXOg?JcJ;2 zDX?$M9__to0qGqOUZv7Hm_LlgqY@ZgMT~Vko+VjB>Dm~-;&D9U*>1nZI6OXw%yBoA z!)3U({;|s^Kv+unJ0t92eySuIf5qm!h?gLHQ$BOdjQt{TNU^3yxM8EntoO{op06tA zo(ahAZ`*#qb(NHxH*WZ6!|j-lY7|%QTtWw@c#=Rl-~wPgyX=!Qs5pw83%PAzyB>@; zD*GSex@^sh{Tz;IV$zG1LZUUmyWzsjrTxVyBs)@du=tuS*|Irje|5TXU=qFO(8=Cpp*+^J^{1du9ly9%$UhI~u+qof%Z7G1 z%HJ?xC$NkBk9^S)P$pCX<6Bd1++&L1b2EPF>coiv>G7W?gkp_+>x8yJsSxbTHoXXh zKt%JXFUbbgUIT;zCc}+}2on{>+by012!4y?qFxY-lm191>2fnUCa@oIH`~_yu#2Z< zeZXTZ2CULxhFLL0hn?qTk`9OuU=1?U!=Q+gKT@{8Ci%J|Y!Ch40pEB6M#=FZ@~Noh z9!2CuHz_i8F=TRjNpe*?qU$bA3QhYzjZ{pS%e4kSda~7zCV)hkpx7VTtI}P$g;83g z?pFbw$oZe`&YY*mpsVK{;>TA_+yD<>)J3N3c%C^L%3;T^JpqSU?>A$5(i4%wT0ih7 z9k;^$24tKwYr=c}U@sHaL+L#4B{{t^5R4ay6ikT{U21md?}dGR3izco3)=Wf-5yxc z2hk8WqIj^nqpE^73f3|u17V6}EB#Rj6zpmn7j7#jk0wetW1fUDVDVhWK85<9LO3)Z zNZHr|qhvDjy8+H~{fomDY`S-r-5+&d0(LFIC}UECT$^!8jZ2uvY=+Rzdnk3uOylx8 z!P(Ms>qvX05c*MF5$r6Ju=5a*y* zwl}$hv2K0&M+%g(W5SO4G89fL=b4!hoM-5~!jD{06q?8ZT|JPMcg}3Yga35|roDsa zD@)nUbb(p$q`ul`@)L`GrEb2?nE73~C)+|}YLG!WrGa_+j~}JGCB|jhpYC@qjVYNi z$}@ji;o|G}&^m$`fA4$XF_1nNNo#in1Tr4aOwuNkg<4rWa7*0jz>oPGCtP(f=bR}- za#qxwoToHBnB8G^xxAcoG}7Gn+3PIoQB7>>H(syfOD-D%xSVpzA9UK}i|Mb1vo~PH zydW77X}cf-+Ef+}*>E!Ao)j&c;uI~#hULMj>+~MlgKxjZqxQlLm8#;MW@pkt)X1&*WH!xIbtBI|PuZTir}- zm`Pu*^Ai6Rg)Pi2B;r-aH3@v@R^3MY^y-24D=zIG;~3E2%N{ev0MxyT5}%1Qz#v^axG{6qvQa(Qn5j*7WXug^+28X#t1_X^Bt1r@SHOG!sgY1y!cf-@Is4sg%()EiXnY4H2zG~wxRsE#CjbIgU z7t$TeVq}XEENsK1MF$G~<#UhdBX`Z;ECf^{I#kgr+GBH%E1#pA%-FG=CT9j8LC>dO z#T>>wKbq&2ck04iY$=V4*+`dATKnAI*fXssB!|n&8}w6A8rjg@rY(gaum$a|o(uh6 z|D{26d9#yJO8qM2--P~nAK~!xIi&3Y-r>39ObI(S!ula+>`-3eW!;sfO)q7~pCZoy zJ+x>U4~~k)Z3Lew+!NbukUaS&#Op5nj;8z(1o9E5PbyuhT&Y*tSENX=Eh)pRK#R+6fK`#Z(Tg;4(PWj2o>Xe~Yz|tpf!o4>QdItMOM&Z?9BK z2^klpc1e}givlBH72O^Cdku}k-9IX56@H7MQeIg|5t9G@zy z!hR*f}Za^u|7N-zUDP}KFYdluas7z_#aZGIm;OX`7)O z!wTSFRwit%(*EMjhdYU>-AYp7+wHcz4ROz>q8!$!8Mg*dFmi&(=lqD%ecK?wR|w+YA1 zSP&UhqvpbIK`C3au+#kL(jG0GQA#k;|KS^m_Hn%l|9YcGXVl$H+BUQ$+G>3wE$=OR zFf=gq_-|AAj>PW#5%WXf4rli_?U{M`{n?)@;=acLsq)L27F+PfaY#7Li+*FXlo`uU0q!)5Sz3b~SN{k*mHDb^}r{2-GG2y9}!QE1S2|NaO zNtamic-)ttl>KlaEi*^b4S##Ljt6=eq`E#^^O~L}AD{g&8zq#79l5;s-irBLdIoVi zF_6%H2_eoRcJ7q&k#@9>%@9;100+KLF8~QZo~zsPVYtoze(j{4O)huc+En{VA|ucI zYhS|)_R+OB>g{Byk%5Wj~gvacXxL;f-rQ4G=g*^-Cfe%AV`BSAfV((4GjYf zDBT@1DBaz@{O`RVe(%?_*7QWflhF~=G&yviZ;29^9=IB$nk*t` zSw!g`V7G1NN1$Plrjju#o*i4bW&efWy#*a^^F_#PFeGXE@Ui zK>5YL$y!t$7goTm>m-ER!~Ip(VZ>yKhc^AN{m_eN5eLNRZzK^eu3Yoo8Zttd$E1dt zKM}P|!7L7TJyQJwXE5&ysYLO>+u2Sg7oW=BNy;ZyhW!#`CUEa14A5F&pwSowT@ zKZ^dr-GA$M3C)_MPGnPEnd;l(Xf+AHqIYJgIem6hMIv|-V<|GN&T%gQa|#~HM8g84Gx&-ARRtArP6@d_*oTnIprUrr zV{)5P-izeg@r_F8naNSoo4)(vdhmzC_{(A#poyEh8+EnOx}QZcZ&KDP?=6z% zobGNfSqK)gE|pu%#&MW{g%JAq^n>c#{5Z15<&l*Olo?UUM6+{={7pE>@y0Bzdfqmp z(Vj-UE2nH-1v^xZO7%@a+!rAgW~)PuPkx>FmK#U?!=g7<-49uJ^Vj|#V+zXH#!=%h z&CdJxy=m&PA;VQ_UXQARIb$U;=S&nI=}gJsqoz`KZLgB6!AyHV(r3k+bLa>HUIzYz&>gtZR zEHIbw#vNZSq>}!o!o+s8@|z>}TUqlMTHrYOwpxhrj1=zqq~Yp+qY9SDNL|#PgWq?* zdnGjA`p39I{RBVe7IOu}_Wu^dFqfiS^#8l{+=L5YB~!iOZ=mbTwZY)%T**&ssDBIF zfgM+;ZX_wr{tCR^@-JFFiOQu`@Om?jh{vX`OWZDtp4$0Wa<^+TG;Gr6xkTpi z_UXK%p06Em;y!F+b8@%nR94T8?<6V5)kcC$5q9w6ytZPQxLvcm1;2UEqkR>drHii7 zVyN&ALcl|7mw*#OszG0yYL;7{XVs8|ahZd0TArl%h&R&g6#2y(ZAoT;dLH%wD4wP_ zBoDSZy`W6M@`MUpYTX^qdEW(IDVuxY)CM;zJU9A?9e{49>84j4&}K$M6vSRDB9!!P~jphQI^YHq283$PE;Nn{qqj z4Y$sx)z;jNxEO;WgZ{f{u=VfqYc-?LzXd!;=5NT_+Ws;xuM<$ei3Yv#qxfxO)+ii? z@WU1j?mJ{8X_A|ETb0b%?KoPXFG^EH9kuzG%P`w3YOiEk4)W*Z%ZV+oH-qaa^V1a1 z5gJxq%Xh>ggs!L!=BpP`G(~nky(DR-3o*rXh9|ctjZs%5 zNXc<6Ntoc)7a&#tQXgy9i_lUR8v_{*z8AF{+vD#G4)?3Q_P``%MMSz{F}V97-)oLN zMm3F`wa)T0&bgeWk=@(8lVP@neJAE-0Pv`FHkYIDL(fz{_RvbtIh zMSKU+Ml5Bn$g?Pc<%7=)a>tencKZ{7zLjRMxmmDQ)&ND%kdJt4jK>9K2XOuw9gAsgg<{gFOJkd9yP=UHW zVmReo1w!5-qKY8#czssDj#R8DLBc}+W&K_z$d!Jc3On9q zTh&^?I~qrqY&Wi=EO>;@KH+?#TR>GCx4T1C!}lbG@&&D0lQQD%s}7i=4BZy@t)z@; zVmsw_qvMN)hUZZd2pGwcL+6^@UX}xB@u7nTo7%tz%XCOk4K*wntQX#)&JN=aXibYS z-b@}#0bcd{TJsga9xhDOZF8Lx396uNDIJTvggsgI6wqlB^Ixx}wvR*=Z5b5K6^Y%b z{RxeSuj-3cg{ERE5iQ@Ct2cr9%ts*CpU4K|7y~0Pwmgw}QoBp#f%k35mFyDC%+s{i z{cH*8)G>)5cU4^9xYh!MpQ`^aOT+0B7awy5AV$dew>4aymQ5?F*JfC$^-7 zx9*5d3*Oblyp)4CPYzW9*nz$T48U?EdkRb)HmVGY(uf5huCZ%tzT&rNnw{h*rko`` zI;82_1Wl2QJQq$-p9a&dj2-Y>lF_{tYjrYOLf(&}!Tn!xBdxm7l7zISt-6#tIk!fZ z1wwQpbXij+O3bu%fq@>Wjs&{SG!!Z0 z86t1FQ~)`6vH0`mM<~=(mVNv|zxL>&5Uf}FMGOXJ#`>~U_R}$ic81pFXO~X)QS?kj zzfra%j=KSLYT^Cx3s99M@0UVDO^o%f2!&`ZIT}^7l42<>TUv{9MzRR>p^zykius3{ zJgl}Z#uZ;7k;1U{D;)X%rl~wxqD=bWM|_{7B2wJLG+wV>f3~zS%zjxZR#(*<$V@f@ zO))HryGC!i*@KR%ZO3N#=p4|=MvpD@)EMTE^TXjCO@l_?%)6UC51zixZEk`?x707z z>t~Mk{1wS)Nk;5SiHAUjg!O#ulye|w_tMT(ePE798e?!0xUfDRx!e@4)`b@5Pr}gF zhV)*sH+jh*z|NI|MYjE$WJUCXp1Mt9i(+PMjIY?6j;uj%6)_YiE~e6C1aT}PA4TdE zqxL!k?8kQ+q8Tbb>k+;KSrhYo3(WaUJ|`3>z3<5a}5r9-7dN@6=FRQUda8#!OxiO~86H&#TQEhpctflano1jZ}|DBZ3vZ$;J@$3o- zqO0diQ^oTg$`)+R6ZwrbP?R!dqD7}SP;~YT6*sHnheUV#fH>x9M`ycrGGU2LhgI7eX$(4G}vya-3v%}hg<`tUl9D5=XIoX z=-#>`)p{DrhA4^E`jS~i7fRN?zN00BT6^oB+Uo5+a>Odz3JV`&O;S`R6{NLG#PLz0 z;KH2x@D9=MMQv%9zb~>cXG%AFc){A6!g>lVQQ_Jv)KB=J@)0&rNl}|7BBv9snf(!E z;Fk{Vyblv7P#I;0*-TJ+gpm7!xG~_GhXrj({_G#mhry0yv<{;Vmk{vP>@WpAH%j2u z@JN47qu@aj{0bqv3Z0StH{JBfLv@4J;9s})JRv6m=bad4wJRwbA9%Om?|H0lztsWd zD&!Jc%QSxSA6~BWB!LBf{eNjM8{1gW*cZN80Ub+|aQD|KO=$J4UFML{N1^3h6Ij+# zC`3Rv<67%}lp~GXp)pm5KPgiKS9!3ry=AOqVihZxk?1FIGamne{nkkbD6p9g@mJ48 zhLZgM?WA{?s#gb)m|=@g;7QbK7n8@?6G_WaUjJ7HKEJ%d}UgYcL9?5O5j6X15+&=P^D)FK# zF@bs?OQ?H-!e*1Y|AfqQZx$1o_qvpoADxz$^v}1uzJO9pwp4#vwS>0|Wb9Tx_xh6U zI?+BKh|j+{NrgRqz=vfi-0e=_nXO6FCx7m;T2(-v>8C!nPC5$YmlSUz+jMu0JcET5 zZJNgb0;IaSJ5Y1m{#u8-VMcWyS=H6Vi2xguW5uZ8kQFfih=Zi5OPFv~1rzhx#QLvd z2+em@xs(R!oHXjR*~6W3HYud-CylA*P{M_AyqGFSCpQ1!hJ330vFXL%N?U~c2Cc-w-_!wv>K~Jogki80;^s} zOa`)L-3YA#G6ZaMqy&$Zc8w;>JkfUh>75z#5>SwAI= zM!~47?2B8`R@>bJGo021747x@q=0ypo3yJSDDc#0u7jI!nFLJcK0`& z?PuyV-k>qKDAk)O~S@8sVDm^f&*~Q3(z59 zrlc1RYML$x6c{IIBW|%S=ZZf@bZeBUWp2gOQyN~lP~GR%-Dc`DaY*DXQCm7%L}Tt8 zDQ}9R5$X8NysnQ}w$6g(_JyE|SC(2V@tqV)3Mq?y!8;Os9Yyk>pllpn*$>r(XtyT` zo4I82cY#89Bx!?{zZL@9e&dIQ&QAe0QBQwAZhJ-2k||4+F%D4flErS%KzWLm0o0?F z>4Wt%fFtu9%y1RvYA)>3+3%?GYdivcz7$L+dn!o#HqTA# z){*iwzxDd(i5t_vuPV77A5NXSHGLqXI7rA#2KnK^7ZT3FUfG#GIrtu=)+3)&XAOgg3>MR)n;P+DMaIWDT*fr&KHQ z$+y<%-VOx94sWou9I=!le~i&?j`z|*WkmGraJo>3>iY%7NL-O{COU;@RW%zYBmT1^ zCZourNra7rQZsRA!;-L&(x1Xn>me&U*E45CmuVjsgX0>sCO);hupR=)?xUbn9%081 z4r?c)a7lCG)Pua9JM+W%N1;^Mj9f%_W-(}8Gh!RYRh>K+UFWZ^c;ghwGTq7|vY?BL z4KEgu*}$DyRlJ!wMhVqD8`5$wN#AZEYV+rhIGeZr6z-1&A!na@f+59Brf)w!OA+)) za-Z)f4fbl@U7sMuG)%%rM}-nRTNE$5Y42`zkvBHilKHhK8A5Bh%^q(Rfrc`lTvsq* zt4)q!jp)4|3tJ|c)c(+dGMfq!d%R;jCzg#O%g83uEM2KEfgAF4mERNr!hFdCGm56d z?gg>Fr#%T2&MB#!$eusEu})5SqW(J_jTY&UDB<`0D&Sla=O@EWZfaB#eoM-o>NmLE zjqTOxkEfNZtvtJ;AzWXSG=N&(=)Xg;wkm}#^R*-!3?lrBuOX*R8g#e!9sY|*#cXnV zX#Hq|!fge~klkfFv`6}rIF^8sI$XIxx?bxjfjE<^+Sd*9(g4Y**$_I=Oc$8Ff*A0A zb=$y%z2f?V-1?dS-ik6IpZOn8hEmd-G}mN0AR?bV3s$00ej!#c$M3SKZ=^`Re+7T( zP|in_W~{43l)15rBt|hT09x?VQcja-mleFcTP4CaHjrV@eoO5fh~^R;wpR#lPhmeP zZHs-U*9ekVn>0av7I^IoTmVW=ufXOv^=$sb=kK6-R`oc~tx0+0Hc{7{cmNYYJVXD< zf#+z}f5tKE-x8|_fO~ia#}_e#ol4+3LE0Q#RJ zfbYU6rfQ@NSb}XEHMNV3M3DcJu8FCX{oodNq%^yr@{bU!qnd)qb?ys7$5mWbkU?NT z1;MI)YTrqeV|FF3LU1E1uuj}&RTBSGcQ-C%mHN!f150^tbUUn_y2lj81T(Ox|xH%cG?qEGz%W(9S zJGgiXF}<=7zM|--%CF+Erpz?_9&wo1FVk_JY$my%C%^@l88v7Q#Zmr2*>1csXNh(J z&ji?z_yNywXaON{_Okoz@|rTSEfw(7pGw+n1;HVDL5FbjO!?1E2St%FVftoU?)EX% zs)*ZD2|Xxi?YBr~-W*=4*8zZtE4?N^)JwXKoUMSf+#@|_Nyt{fqKKzIxAMc?^GDI1 zjpexB29N%D>Q!Aae#HD=E5C?)VK?mc{BjM8TJP`)mtVWKl^0K0cd})i2 zrhKXMo;k$B;XR-Vx7Za4uicIDwxCdcUd*_lhTm zREb(?>LOK;=NpR#FoktVr-%O&kBldGo_>O}#B6$~eagBE3n30mh+lt3`_ zaT^Raf^;=um3*yMzC3KY3#`IxsCVselaM&OhiXX@peh{wg0be2yPy-}+PS8+LdNfr zL45M)k#Th(mSyZR1z%md(@0KR79ulE>lr1wFXb8nrCA8bn&J!_3kaQWluP*rkXMsG zGyGqicZBJDH*+bIJo$$r`xrKTL@4%WosxstDqK`>y9Zu{Hl(im8@Qn5uIPj#Hj=*lm-`-7Uq$v0xGjy48c==O_3>U)v>nX=NnxPx|E?s;l=rCu8 zPZo!L}Hf>X%l$wKXP~M%q4CRBbZ#i*3aq(pX>+OkfN_M zq>D{go~)}R@oYCy1znOCb*KPVDk~e_LG<4VAD1|{hl4#Ne_%nrFHeBm+-a!DQGqQ{ zNK`qR{f{BI)s`~k!~Xif!h$y|BfqRe!URreOky_q(}BL9vE%kQiTtvj@#r zn@ARP(v$-aoW~8cM&Z*0M{S=mP3(ThN4x~@IcSQNL|(w6Jk*o=&k#D}%skCd*3VLhqiOHA&w_m@UMz%YAX1rk z%^Ub0BFgeh`b=JSX@lo1Ypb8rcF`T+qcBQ>k}+t3BlYW8f?};ev+?21X5b@zb}DVk z%CC~@ciBdS5(u%ju9?y^nCja_gi8(RHx)*5fxbL1Tr?5wt=TH-c zai^1Lv!VR{@a+1tc+u2VNm%2A4-57%7W5f`puD(tCAsOI4w$Ee^;^8W0G%=4XZitKl9T6 z&Q-N|z9vzyo73=3LWah%butSh%a&u`t0Gu|Fnsi`>fa zWhh5iU10(;af+ak9v-SiF$3+`TjB@UDoiY}!|zR(AE~aW41tfWd1E;fI=}mI?Afg5 z)xk94%<$X&GAvlU57F1cv&rb-6dKngLa1A#camrUKiL*+1!Qzk9x1f^?8P`rf@^)? zs38PFo{r=9FJB{HTukeKOePHs1f!7qf06sp+@D0*Y}mK1=W_ly5cDb7mtxbh7#jbG z4)Ph-KnYxd**w4h6dFj8+NpMaxy+f(c32L~Xa( z+phb8Yb$ywy8Sn2&5o(P{u7F`2m2gaf^wfb1?zdJBKe2y;hTE*cc1K~$8acHps!z}99?i(hIC6}XwY(zmGqTNwM5Qd#sJ$L; zeY<*>=Qiq0{#tvfsqeHh_(ZSfSS)Dr;|~wnne9vQTzyTMCl+6iZ_&U3m1K|3JF-;h ztYSgU9}FO;yDAJ%L9~1wu8yl+8vVv*O)fZ`LZ;*^q%(fF zvEw7wPh~L}tn$VYH~ zFgDc=p!*hg{`(2R##*AfT_m`J10(3wtLl>^_34dP|F)*!T}b!WK`w2QyS?piciRk^ zn<3?AwoTM8=3)d8Ugkz&F#bsn9yI;{1{PdL5-HXf*k~n<*_}Ombma=$$is)d01}xK ziQ2GOCLG=glWwnmMlLYFQr}+E*y1Tcza(CBMV;yag!r~Xeh{P|usRO&i z>8D`&Flnhc?Y4kzIa}UllV7l@0!cH8Ry=+WI>?~}`Wd`Z(LznYzpf~? zl9>B!f5HMId~N9THuvmOk2y@nCE{#9yvJ;~H=GvI^^w22RVN}ghb!%r19~~?7dYEY zY)O9t8Lrd}o8=;gUVRb5NY?@Injb@C) zJwi=(=Oy@agrI)BuvgEA@1VzvLvZXk_}?_0+5gu2fqcze44YCq4q%=YjblmeT1=VV z@#NW6s|npL#teLQsOeNn%lqS+a!l;%B{)D)sN!5^84~1rWjOqoB=o6ig&rbheb+vX z|Bo&u^6W`i*wPql_C{@Q-2h=fHK-7aXX=v|VoX5JJ2ML&uWllliJYy`U&eQxjMVr6 z_FMjLT+k&ah2X$uVWOFVu5Z!zjzK8y4QWHsy!(!M`|%?iKhEZJqOZKzl5*T_zFSC5 zDPO$>MJgZUNzUMfkX70G&smvmnnn>uk29pk%pDeXGP=H~%ip0bgA^E_?}rVm%I;5< z|HR_=2R_z=3up21cT8da9dv1mEl@Okr9F&4rEI|(pbpI7z&07eao|}3rsa{!3jtz<#J&LNWXSDqq0o*G&hOHx)kaU{{iQ}iyy zg?EiJN=1Tl&4l`nW`>UbnpyxCDeH{#sT4-D4`p9_;B$xV-vCAmJG6V{HgyB4ii;}0e zvA~WKiB$2q?!Stjy>v>hjp^=yTivmwEzIMw)tSy0OZLHC(!?%V@is#whA75P@bJxg zEvk4|86W+$<7n3M7LfIYPKuym#OEReUL|!zL7`)jy#=+1jpM%=)Q-u~vHf>mc5h?g z)YA!1p&+!yfN_<}?c%fM1GN3qs*U%+`%62F!ms_xD!9ch`V3Fr^rC5XD%VdaUG)@c z!{6!y0ebjtZzQ%I^SDGpFdF%d2rqeAvr#TaD3_=_ZXC+*7o4A70*N-i{@ZOp>*}&d z9#|#o>&#`lX~>V=rrxo!4H7Rg+e`({$7j}juk>|$sO%-Sa)OK&Ep)QT`&Qpm|7SDB zg5v!+!12W0&;uUnr)aAl8TqE=$PaHys&!-PNP3}C~b`_+nrH^kQSNC)<6p?@Q zi}!8xb*)Q4W+eo)aALPPv(g>&LnoeeUxAwx%CHB=ZwwJhj#f2im8BoWsXi_v8XTj( z7GBK5iMmn-0$C*s#C3fZVI@6P!Tq0G;lsP(m7Pf!o`^DT7*FFv?vRJ>a@b~vy}l{^wy?9(7Yml2##cZ%AOcm?(`@3Z1-_oS;! z@;gB~!dvY7LZkPmqirZpt}?Jolo+xbfA+kL`NT^Fw(&SQ8h-Yn)-@F&jTa@A4|aMy zR9PquJN9Ks%=u{Z7B%Z^YfQPT(>R?!;1&kW{N^dBqf*<68gqNI1&K4m_}9b3*t1lB z2ki8i+_G0-r7QXZexIBE5L8w@N{uAStfXp*j~gULvdjq-)TEzOe}EmBvBv$j+tK|{L867}$nr#V_FPjy~r}5;*v`i@ncslz#s?d0@#AW|0S^093I@KQs)|bFn z(oNf|VvKhhpcVKviy~^ExV3!)kgTIg#phPpaso+BuW8cuO_6^MNE^CU*e8(Q zjN%s+fvK4@db24hEPeU_-?qzUSJ~Q#b;ju3cy(!Kp9*v&Rr?9IUe=(BH76BwZ2o+=8QogfrD$l!y4R{7Q}P+fWaO8^!}E`SUI(=(wz?F96;%njsUky} zPFhQ3o9J%Ys5%~H7(u#ugZd;SjL#T1YWEz%mNoqs`uv~TTX44^)1;^!mFO{K_asnvOD`RgT~U=ruPE>(LG6Al3SO835f)fCbC()7U6rA8iaJ zO-R#HWU(n+TA`mgd%!)tD26v`lHzTcZQ6Cit-tq|x_z-0Cer(xDFPJR3v8bRxP?zR z6`U>vj?9SCP2+Q5CIdtjkH&E53DVkT69Gh2g|Qp){8Eqm{Bhi=tE&T1eO7AAZG$oi zNRS}rX8@Jg#&yLxO_@!M0~aq7r|%YQX@YPqa#mn7^7y`PynT+MD!xvP={@VhFgyE5 zhSicO1H)LA&55)98boGN#NgJf=>IG(Hs{&83Eq1cvt&pb{`+XO)AU?9M~j%OiPB3V zAtWUY>3zxb{ekZfk`8Bmy_|mFx!Y5!j~zrm#){kp1PF&&SVl_Va_dSiqi^yb^`|+~ zXKP#!-)jm@HFgk0`>rrTtN5Lh6wVIRcrDhJqD}q&W;73C-vanhzVA)iL>>{YNtq}MU(EB+&E+N(xlRYJ{9 zfP$AAd(igyUbof%c2K!!mip7tp@<1Lm~(AvMp;4QK?Q;b`Rj$`HFV&M?U~^>(F*Ec z<73hQ`dK{D8NYL@dP?_X2;zS-cPbSj1{zORT#m)FByB zFcv-=!b0+G?CX>eVOg=!-a#lb5ul0V{cF8ufwx+@WE&boC#)1n*7QbaC{WZ*<6Kll z^&cKaYbKzV4;3krMm+h&091%Onn^7{z@C#nJYS6c>brocf{U2La&K+>1N(I^7#OAs zU(+~D2?2fwEk!R^f_tYZ8*1XWhoBgrq}vHjwQ_BJ3MBaIOHy8S8#eP0r@gAGNXT~> z1@%^fC%t;g#a`;?eOMmd-xSoWXF~A`(F%_>yEt4$H!mdr50z#1Fo3J+=cJT`$i2$4 z7_A>z7`LslZCP~kT|8T*i+ELwV|~fUu|$s!+kew=x&XrDqTC9eY?FizyTMz!b_fUC zxSrn5;gTiO_zgK7Fuz#lo;jR>$o7&v0s)i)AZ>15bfk!Kr%}IJ^8g9;p$&@P`S2E! zmt?k}eIx3B1B+MuNAQ;}^1x$tSs4e;CDA94YmwB$$S=k@WqR!eO~u(F#K8AkF{_jh z0P-5tec@~UMkYG@eYn33`&waTv`2guvsDCQtM+w_ry-|S)nh+cLEW{4D0Qbi8N}qz z(vgT(pI6y|6emsz8CXDI1AdD#0=M(kd;EszOwU5L%r>xKorgsK)`li;Z!-$t;BCh* zGb~J!S(lKhrg}njYU(`&W*}WoWlLrS3G1YKpl(UZkQyNnhY!xm4-SO~-KMMS?2_t* zx^GBni)8RiONaBrWloH6Nl5SHgxuXZRah?|sw!UHt4RdgbSKWrv{GV6=}MurjH<-F zB&y*$|E^UEtIaTVKxVP6tj-agjV;u6ZC;kP9Q`&TmLt&SWSx}Y)H%^M8EV_yL-gOE zb~-4-r)a`)^a-u(W5kLc{0D2TOz-lV6?6v$gzHOCU@(of;$?F3DheRu4{p3Pr)1nP zE+c#HzSj#gaDFYX;0OGGMeY7JKRw1ZF>K@R*Uh&w{<9VOU}Bi4AbJ*Ebsiy>)HE^c z&Rj3LYpMkN?B_2)&v%^5_edZwT_Q8z*D1&$V!MW|-1mWiBLRugen|4Ayk~#sSuuLM2gk9iwq%w_nAbEj zq!(zBY5g(*zX_Z4o*ErZ1PsP0xdj6xZJ>a%SjVko%$Hu|C6-YU+WCdMhYm~`nKftw z{w>4Piy3@4{3SHblHOgdt25@|1LaaHfR0xGkM0+eBo9$h*!BH9*sL(k4frj8srUdz za{Y5mrJZ1P$rb|hblQ7YupfJ#Uyg#MU*bw<)e`2qqZPQ!t%w(`9MbidkI)%=5W|WV96eNG)ELqC1O4oeh zs-|2f%k_EOnpMMJ3!)s}vxSBD8tsYBR`3n5Q&QseeYpwt?woXl~QZ ztn`10iyPg8F{l<|Y(thgj9PVNMRfr~Gg3zc$F2#UDxrXV7luI)d&O*LS3-YYmQ7m0 z)$|CqA7qU{A{*KPUrRzENfT6@9xSVF7=!-6j6VB&@n&r`6Fr>RZf&_Zx{k7BZe3M=cV^ePF331iHP~NZ{)k3B_syb7F5blopoW@(a`t9vgk;c?; zBQ1_N7o@TQPu~=?l!t%XKnv)?Eryck7v;7nQe7;8Ic`a>{;4DW^eY`~Ah~AmTX|eP ze3+;`;3N8mbbBJd$Jed%{G`+x3GRT$AP(2Cq-dK6CS!Jp}vb2+)zhOCVH^lUL)X1 z*^lO3j>LH55wGygOZ93mcHfppmt;T5j+0q%rXT4N7_Sh_ILpd8kV*V8of7yh(Vx?J6o4b zPi{@&y24o0?IRtE75dR(F}Ow?X&MXlmVZ0Bv^!1gVh!@yPYAN8eF6O4GJ#|rR$r&v z^qm(M`MATy*|RrkjLa#p{hl~Xv^Tc#p0xh$(ZI|62hT$n)P@a{JW5wY4d^+Ijb)#pKk*Uy*r#b0Pf_=1-`|EiMUYr<&(fU^MSh z;~zPiGDMc{{^HdPAQ&c71TqC99Q#OmHk6eP>js!RL#qm|CgKirq1uhCp-$3$D~RBrCO>WX0nya^Q-;JC zMokrpbz}pl0j}XdtkXka^BTIPf7(#nN+wuj9>c>~{=D=zy|#<|tICk(MXD$jKi=p3 za7bEMMk0lt{pWd;7hf2y=0N%b0E6BDw!52k_i`lcY#&=D>@Ogq;XRz#q9?Fmg8TwK zX6){=j;LenXd|%t_%98hFNkSrVWF^iQ&-mTBd+b1>im4(EqhXXK!*Qh`?(Mu9B_tF_FIvo$4CbLAeM&oVvR z?-zU`;ROxRofgFuI4aaq)uqOnNthYaBu(z-Y-yPzDqo*sG4rx6Ik)BPRU*+7YsOr? zjfjtY^)GKx`Y1#D-x`cHux3@kmd>$TKSiY3J*ZFmM~6fLr;0qL)OiEYOpL1!(iUA{ zT&c&dsAAj2IZl{xL!I(qgjIZxWTAGWQM-k-&%7GQK5Gbyx*?X#JJ=@W68?sUFpc%F zJMStEj5kU>?5ov|Xe-ENV}D8&xAu===K9-{k63cRBdL`kHI&JU4Ev2f~l&Bgc?;+cLJx4A3N(=*j0_L9_^2Kv z{UA>3lr*M=M{tw`ziQU=mp;?sESB(YRaYe;M885Dv#93^%>+VyDER}tnPjfX&+70-S9=co z;UEa83M*{_Yxv;E@ydY2Y5+w-n2$2~;@m&bkArxzW#4brA39xHYesw6w5tKqWyNTV;(B%%w0whI|2)%lL8S1%@0E{yTNTFnVvX35%- z=gtGhRi9es&~wS~C2-3A@&`XFk`16nG z(DJuB5?#1j!~3?gc{ku3hv-}CC{Kre*)F5~=;X_I_;DQQ&eM*W@7N-xbGOZU7H+1v zbS>u{LtQ_=v;As!EKJ&N9d0J-itnE6oSTz3t%BFi_Iu=zRW_|&F~5$f64y2LOQbm{ zAc|8&`*sioUrG^d)b~!9Z17Eamq2Y6(NkSWU#Ggt5$8KdWi5gR{V~das;{EwSNBGp z50D`xP+%7usZ0Z}tY@!(f`EH+D_p%SQL&&k`pVzkfeg*sm!5R&@+6J)+~Cevm|FCP zcW{$ES3b~=@s)Op)r=ouVJNi27VZ3FjA0m5HUn0|eu z{>@a&@Tr@Ao^znlaZQIOg(jwp!91nKJs#J%pTgl=DlakGmeaHApve#TL%ogUhBzte zKnuve3x5X5&C_AXmqZa~=(8d^j&nnTTUI=y%m~$}4Nd0$6l$T*+LQu`IA{a-t$b<0 zQ>ch94N5W{$L9~`p(Kn$8qEK0o20-^o}RUZfWVadPEk&O%*Q}^U*H4HJCXix`OmK- z7W(bwSR+Rzy@ATjU}DS(;Ohc))yyMYd0N)&7YO=|^4U{tk?Q(8Tc9~>4jxYyq)^AqzF2{;#)KV>hI5U=b))h!F;$-70S+Y;)@qEz)B$I zB^+XtsJ6}QMf@D^+pp?F+Dsa2GFEM_jx*zu{C(p&!=@_okF~OZflfrSyUQJgzwCMx z$$zUc(nG+YBDNB^RS()oS9gA(L%ColW_4}E%tI$bGm|PcypFK`7bPQ0FQSrz#wMBF z*miTxDmz7F;C`q)gUk+n*xdx6FN{g;9R&(5kg<v(l3nP3{{kM1R7+83~u1yDg`qpQ3H8^)e&1s99#?xokvMvY$QI6B&44l2|P(y^&Cl$Nzk_| z#u~uJC2bF;pu4C$YQX zGs)_&%sX;d;T~%R56L?+vN7;4ey#rym#$^~cX#gO&A6PZ|8T|hUCSdg<@{K6q>qfn7mS#1 zGZPhK(!$!KU_wmYj+;WeoU74vs{Q{}b(K+3eQ#Gmx(6hshGFOy1|&onVCYo3LAsSr z327KQq(Mqj7&;_p04WiqYY;?2hEV!NfB!G#lpw+0TCVe(pKvK2q;U&i=lS zN^i2qEPlyFGOw&CH_{lSrjG}qz_*rYXK&j14o?|vU=dUYa z0W{c$CCfR_1{TQsB-wh@E>$zJj-6w+Eyd}_flYB6D~hD?Zp4mZZsB5Rnsg`Rb!Q|< zU?e?__cyrtQa_~=9}iRSOd-*}knkbjLF0uwv*Pp?8SN5RJV-x|IyC? z2PK1cc4YJm)<7eL^;34Gqv8+jWn)V?l3(>`+uVo~S=mg!A-1=ceR<-bEXSvgdZ{Uy zB3_GkzV`>$v&Wx|%WzjwF7GnAi@`26LK%$iN)Z%B)#E5LtioqL{APEsHilPAUcr#W zk^s?(^hW}VvNyjR_|8vW2}@)>&ig?4PVH{NAcx4?SM2j21K`J_d&ry68<2G(I|0H` zq1bsLJ6h2xzm?fv|g-0gRrYA6Y>`4+jp^0gV94`UIc{QmIo_ZAxR8vWJWyy z&_dO9FP^;{0Uij^3Ml?J?*!}s0e*l7B}q(6Dvm_fBaOzs_sM?aVrW-GKwAh)!XuLd zjaq76wt_BQ71z7dJ@&N`=(TotosSY3?_eY#iRotSY)g%S8vTIAnZYP$GqJcO$#hNE z@8D!>x;WI}JA6-eTW;lidqr+tDcDK!K`Ce~{}1U;6*2LgRJLkxs^-~1%G-{N6wM^N zREh9aAf)jM8A-*FBDS10Mn9PWYRgK&yPQk?O;(ypMpgr&IsPgBkv&?EV}gBZ{H2Xk zp(ILCE)yLbjL(%8MZ#N3M8@xmVda>e08@x5qBq$Xke+15DS~scX+sm>(U%Fp+gC67 z&VRm*w5gtR_hhJCNHKq8tbE3wCnvW@f?D4DVbemTpt=szHiy%&Du*Fi;6IUn87?UMh%qat4}1D8aEQ_)G1JMB6%T#m-G#NZZm5 zl)R(-Q?|yhkrDa(us`uD1Iw*AT!!}Y+yFaDW{4zUT>?&3a|)Cc8%*|Dtpy$~ODK=m zG_n~F)05Yky+)6>uvF@*q0~k*Y-U4SqOEjv9?Y!QCS~h~EUC>9IXgfFlm_F3 z`+C5AeWgEXAig%a+RteILS{HdK0}Yg z`D}_dWjzJICZ*LG%U-!C!j%!%%E(6yo~AeMpP;EP23)j!FoOYYXaQ!bwY|l=;Af#r2(;l4!`_8a!V(uqsTR^sDMxT zsF4ljg`4_mx(w$_O?z@~TRxgB6OpPHLW9Z8(evza6m4tdiT&2(pxo`>6Dw7G_-Ipk`uk$4!}UUHh%-omKL#vmZ(%uKkw@vYP=I(U606&$K$6Jphy)1h5&ugY{WQvKNEOjM zJtPuc_f8cMo~K;Z$@*qw`m_`j@Nm9mFm9}IUmcgOg+=+`J!rXx&Ctd{Hp8d|xWcvw zzu8{_miQxpGMJ$U)RNg0tMEKn&Z@7~)Ocu98Y^vu>EW})fh1OsT56kac`{)ad&b-= z6J?AEgAUzKIgHhJ_=y&cPQz>g)xc%SsYpT$;4awzPJL%9>}rnYEovC@f`a*DMk&kS z+Ye_xY?dEuVA?aUJYRZIofnJ6rPaPuXxu(eZnX76-MDjLMsxVkDv8Vl%yYQ|k|b>B z@B{Svr>~&iV!ZiocV54GkvD<9&XfdV!r)=uPFO_=JDv4H{?b?yM*|ZzXks|_>c*dY zD$2^uu7>qi>to&$Co%XkQzG>Zet>7N6O}{``k6OUivW!?Pi{UOJkXtcf4<#CA2G>mP=U$al!!bJ4G1Oj+d|Wn7Lu>n4?rBgyz})o z;#?yhEDZ8|sIN=sHnq=rtY#Z=8uq86TCLzI~jHy;TBSsjQ5xjjwU$ zS>$I)h>dkoip&9kL#gP}u}1d0H5@jzh5_Fn7dba>Tl{=#C+(M|yPG4xCw4*r2Am@xc{NmJ!+#r&a)`I_*O#16#LkzL#0XHrw zE$0HRGX<`Y?S^FguRNND0H|FOd!M_1DM)3C>LoPJHC2A#-YA6*=LL`TSBmhAvPz{W5;Z>vB6OYO&axncQ7_mm&uk1OS5XSgFGYke2=D z@pIvy@nVa@agW=Mf-|uBB5F=KEg_Cg_2~v=%-WEUX9sp7(>aUv6PkIH!}Cc9ihf*U zF^Bq`qV{_L<;#RFw+bjB~SdPUp^9M{}-aW3FYQ) zB0?egnMl|w?|0uZ09bK3vZUW&{ZPa_zWF1~_2&zBb16+)New+lX)vMp9+l*=PqeM- zhQlkt-RTX0_6r^Ez^;Z** z5FCq6yt4mGBZPvj${ORv_H+D42NS{IS$2*%v+2Ul?Qu#!iMGDawg3{^%Z>S<-N#3N zrr3n6M;;4JcqP(uC_qIJo5P7Q2|ppxxe&M5hBIqU+^4TJnylFrwx%PAfI(ltmNSFN zilqH{-^C2aJPWvl#oVZA#tUdn7ktQH$iI#G)5>0aw)^~O?Iy@_q027MLxuIt@9^XP z{!qx5k4})In}9~M!BXR44XcoI845ZkjU#1~F2w_<(*I;{LGjziReTOVrtM!4 z;TcP=plG5ZVt=HTNADZX_kEq;k^~B=yY_fow+o7hU61F{``AURgcdwaic5$M-d%2Y zDj%O_m}eFHV64Eo`m;U29eB(1S zg7K{(hm7mogSB=LHtm4@oGghTMS1*iL(sp7KJxd{Luqc`jDfzf?JcNE-$5} zjsX34GOuIjBD@axcuKbGWo#J-vCIg}m?d5rn91L7k-u7Kab+WYp%<-6BQR|}GV%Fg zmp?s375O(R2N?JO;N^hmR)x61g$QVapg6R2#Qi=i&cHq3eb!L%-mJ%(SU!%FB}`9u zl>bcpE@K@}_wifWFZiyEwy}#d#fh}~Jo4PCJF~^$5#?w%e#v;0bJ9wjgIg4-ex1pB z9&-?goJbwyzBgwc!dJ`&{2|&a(sxNv{o+e*D_A}ar@LFqdnm1vxWus(xwf>^^B)}y zh!E5Cb*;9u%Lkr*)wl98%GqH#Y|m%M5Q zs~^I#1o799o*U6RiXe^_*X0e-P5O|twJ_R4!3?IMy=*38h>ZUqR%wDgvbqQPB%Qd? zgacTXq;k9uUH?sVyu}Pw6fPMSV`!`D=DgC%>ykb;`_AL8paCtva>d(wiUfV7bN4MO zS{eH9H87tI3x5AHUj90?cohidj#YG)UQ+qC_^nYLABOI8PlRS=X9>s4a=tkeqJQ%X zd*1^8^@h084T->gOPoFilhj$#GbN56p+jtsH-`v@g-A(C#Qqo3xEAP~xVo;Po%>G! zVHrN5`wL=|z7>?07K@r~zl^#SHWAIoPA>L~&4?zA&#Zv`Ck35m`=V!z80ezr<%j=0 zBn(tNKKXA=ll->M=waDOO(IbZdurKE_uOrN~B zEI1&ru!>aREp~(VDZMv~BV8tJNUq%WG={R+>Na?)!9`NYFQvNm^)Xmd%ZKJ+jC^`J zRr(AGgMx|<=C@A$zkuF>fp(uI+p!6W6n2^g)Qm!%ir;KzR5bQb+zW}%-*O|F%mZkWiEkpCo`A9>b2_pWVB zm6k7|ElIg9DlqyO)|tYD{1mG~K~FX2N0qneLoRuItl+s}bP%3gq)%o|h$V%Ce?R5@ zLW^TRIktSYIIeuPkL&99W)@4QEzCo!b}DXA$k#}|>wg*fF7;X<@oAFdLecC?KZNS! zei}e`MWkY^l@bXT;jjGwre&F-60})@xSPY1AWv$GUyEgS@Vsnx>apKj?mP>O($hx% zL`+scGx#=2i6z#QWkJb>k(2$3QKAV78^|Jgpw~|^Z7&RnvXK{$)PIX9@_+q|WZ2vr z7KCOAqn__^@jVO}8Q~;HO7@m=+1Oag;0QxlS%TC9gqFnpigW1R@l%4QQ)xl{Bp}DU z54A6!;$CnjQ-+ahL_Dm1@u5(#$!R`ES*JiMcq|almj~7nMQ$;~rPD!?v(#$FlQG11 zm*mO*)lrI^;CCE{jYKVsC(8R0SFks@xk;2e=&y_(F>S+!$81cnkY;@|`rODWRKR%WyDxdQ9S`1aGWi*@D;}nRcjt z=oHyi&BLy+xiyY{GJHZgs=cHljqM{!gH^2I_{zhjN9^)sdv68rC?Z0EroZTa0W8NU zxK4CUn8or{oQ`lE3$ZR8jZ2K^IiaurtXAyuMdxD!vuwb^JJP042=)a~EyqM9yVEn- zUq+JKNuR}^|5sSbr|ydk>u(hjp=={x?x3Bn7rG)S8ESG3r2z1COn17OcMo~*eQ6EM z=Z>g`${j_;JUIA+&#XmbVDu~+(-km3H(4gHbSCgl zJlLc=lViimS_1H|2C_jy3W@Ka6I!P4FW7O9m?}xSzw+Q_D1f%XVk#?)+A8%#Oe?%G zGVFdo=|7h4`~)oWql87t$zXf(ObW&9SI~s{=PZhRgq|VPqF6tLJ>$g>r=T=_Eyw*& zuWmdH*Cs4gW23>^`ZE<1Gr)tEE<}J+Dv}!$j51`7JE4gu;^5J1ZEVWC{Pz6y%KwB( zCVuDrv{@y_5ih&(#sd(BbyA?ArAh6BstWa&ay83foJL-X;KFfsaBxvlpY>3D6tMH( zw5`a(Lpee*R~6~!XQw|mM#@w_@P)Y-R}{)FRn$DBnJA!%l=PWi>?NcYSRgnsph=JU{(1&^;)sV?-^#W$3(2r09)6>#_Nh!% zWe3DUIyL2LxNHBv=&q}T1@fF5VU$o$RlDuMHwu_^bdNa){BEsU2<)c$2KzCG`E1Bk z$Q?j@)u9`8rwI2&miOY?G94q^>nYj-YBt%;f!=8mNbe+Au(4Z!KM!DaCWKCZOJR6q zI~e!#bu_OC+~pF5sNVhhN8nE7`*Qu6#fq{MnT*)qkhllYq@pvw(HXtj1x8LeBLpPYWaBvuQ zb}3A)SS8Bu%!U$Bo4spA2Dd#Jntn0jbk1QKH=ypv%0{fCyE?rLVKqYj++pX^0~SrT z!Jz3(5QENYfsoAh;9mY*c}}ynHKpmioKhO|q1A|DgTKy%F?L|H{RS_nJ_fhQp1qE1 zD^d=#E%i(FtYQP}lcY9$yepZneItPsy6*%u zmtGup6XPrB{qX&p%wDkozNRqYUR*?Tou$nE?$Bok6pT=6d|ec<*4BpjCD{H&M(0L1 zK26sNuxWV1G!HGxl=L4w5xpqCQl?K{SXoZxcJ<@Hc5*oH2AfqNblAO z2uQ~a%|Acf$6C>Ng<9=#hX0X#AGUkR);^%sXj-69Y^Tr0zYsT~xo?EnNOPtoh{yg~ zY!RCKWm>%1*w`97y-cu-LARwD@cSES9kx@5_u+K43x!X(n}H03QOHfTOAg)K7ZD9EkE}rXzII^re;26by~?xk4MV^ zRep=x61ks*MDebDdKP3+_r&21q>K`NhaUJujX5@ahm|X&?G9*x$>;bNH(BOi6{`XY zSK^>wJ+w`ljPHzOOpdN7@XqM4A6VMZ<+en-Os`p}2|{^`@HR<_2q<#PD43-KvwSRO zs-(Own(#BQ8aVnT@^3z2;XFz@RGCod zJ8UK=D0M2)JYo+31dvc?{0>xy0u5FbHm>HqOmr!&TMW`@cs%4mPn1j4RF^?I5NE18 zl|=!@9CS$qbCHl;7EhmUsb)gA&lm^vAdtWA)^+$v{_n%px??Gj?p7Y7k8NGsF@0Zz zhxJf&Ar29AWB~+E%VHUdM{EIm=d-h&ed?*=Kh!L8ns`aQjANWG`Q#x|Mp1%hgc227 zi#lgjba^Mt6~+J#{#0kwk^2@_)FSvR9e(GV%wJj9va4=>@-l35mQcO4Ceeq~E4pF42@ZZ3EMih0C? zICL*V6)*?q<6Qw%1hmP^_6!~edxjpH7bNuj5p2B^<|du)a92MKZ$zv2Y1Iypglf=5 z^M$@@QYq}OyIpccE3-g*9oH*tE7R-3IU1#cvn#%}m5{d^M$1th?w<>`~7&eE9U%#GirHs&$Jzf5NX1>hb>j0=sc?@aF!@EA?vs#rD8(4_31$~66z|f40X4vuc zwuisdGCT&|d4D-c2SzMCH9~K1#l1#xEhS1qq#oSagg}8{k4Geilmwj+9j3P(ZHP&? z!dt8tLs6NH&dQzc_086_SQGaTZVyZqHqbGSf7P>fy%yPp>Y4+n*05iOz(jRpSJY~C z#k1js8KlZ}Gmtzzt=Z{(eodkfgP3i5qDDw?#-Mk5J{1<5KIy@TU#(S2bIt$pWoz8d zFMR)puKfIW)*vu7gJwp1c+1i|?hDZkKddJh~2R18s(bCi*98-xBH7u@(ISXxpeq7Z<-xE6gcUM3K_-ejh)hVwkHd z|K_}ya+LRc*drcHFV1YXXp~gU!Wut0`z6$}KU!!3tN*PyOH*&a+4RR}3=2T%o&H|2 zrjk=h3(3T}7wmJj>I~Vv4TVR|vhME0okY3tS&~aJsoxMc&+drCDB0V27VnmMC--BM zn@*G-&MA&{{&P|NpRZriF*S7H&CF#_-ic=+2dqi)N|nVZIH^nI;gBNP=IjI9sOVsl zr5})y4v1V>`CVDfzCTwJ>}kmmPaQ;;6aBbb#w7%Ce=6k?GHy3{Ax9sAuv5<8*#5xG zKX{ZdajfgIqiuwEYqqFh3dX>}+X_XG&ANy%>Xf9NT9Ah?k|g=8V`*+elt*9QM4Vtv zwv}%gb;1G!ReZ4pQcnNeKIXe5ls35DPx=*4_Xl54hL6% zcdck+sO!#vwwn1}m(q`{snBt&fj3@YAtwBPUHWJ1iS|^Lbs`>!66wW6B&Naa1)&=lx&W_&>uhcz0fs} zM4ta5T!5|JCILO?=)pAT)Hud06{m(WaX?z{hV%=NzARKs=ih9HeE!HXVb>`AyTDUl zaQkYRrc*McZ~i6HA&EZ*o%|zC3$h=e-8JW~-!q5RoC*vZ8@M54w3uHs;G?~|3Pc6R z+o^TZY70=URteNP0H=mWo*A$(OfSh5VdMV(Q3KsL{{!)cJQix6uL?FB4yhLSH4iPe z{X;snJgA=~K1L3b&y92sQ?A;A!z?1@jxO$BUTY>=3{VY=h0yyg w)9%5zXm!QWm0Fx;%vP}+UN&Vh>0P&E&cP-o+Awavdv}+zf(Eo!&f?Ag0ZA88od5s; diff --git a/lib/esp8266-oled-ssd1306-master/src/OLEDDisplay.cpp b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplay.cpp index bf4a5ee0..89a80108 100644 --- a/lib/esp8266-oled-ssd1306-master/src/OLEDDisplay.cpp +++ b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplay.cpp @@ -65,14 +65,14 @@ bool OLEDDisplay::allocateBuffer() { logBufferMaxLines = 0; logBuffer = NULL; - if (!this->connect()) { + if (!connect()) { DEBUG_OLEDDISPLAY("[OLEDDISPLAY][init] Can't establish connection to display\n"); return false; } if(this->buffer==NULL) { - this->buffer = (uint8_t*) malloc((sizeof(uint8_t) * displayBufferSize) + getBufferOffset()); - this->buffer += getBufferOffset(); + this->buffer = (uint8_t*) malloc((sizeof(uint8_t) * displayBufferSize) + BufferOffset); + this->buffer += BufferOffset; if(!this->buffer) { DEBUG_OLEDDISPLAY("[OLEDDISPLAY][init] Not enough memory to create display\n"); @@ -82,12 +82,12 @@ bool OLEDDisplay::allocateBuffer() { #ifdef OLEDDISPLAY_DOUBLE_BUFFER if(this->buffer_back==NULL) { - this->buffer_back = (uint8_t*) malloc((sizeof(uint8_t) * displayBufferSize) + getBufferOffset()); - this->buffer_back += getBufferOffset(); + this->buffer_back = (uint8_t*) malloc((sizeof(uint8_t) * displayBufferSize) + BufferOffset); + this->buffer_back += BufferOffset; if(!this->buffer_back) { DEBUG_OLEDDISPLAY("[OLEDDISPLAY][init] Not enough memory to create back buffer\n"); - free(this->buffer - getBufferOffset()); + free(this->buffer - BufferOffset); return false; } } @@ -98,6 +98,8 @@ bool OLEDDisplay::allocateBuffer() { bool OLEDDisplay::init() { + BufferOffset = getBufferOffset(); + if(!allocateBuffer()) { return false; } @@ -109,9 +111,9 @@ bool OLEDDisplay::init() { } void OLEDDisplay::end() { - if (this->buffer) { free(this->buffer - getBufferOffset()); this->buffer = NULL; } + if (this->buffer) { free(this->buffer - BufferOffset); this->buffer = NULL; } #ifdef OLEDDISPLAY_DOUBLE_BUFFER - if (this->buffer_back) { free(this->buffer_back - getBufferOffset()); this->buffer_back = NULL; } + if (this->buffer_back) { free(this->buffer_back - BufferOffset); this->buffer_back = NULL; } #endif if (this->logBuffer != NULL) { free(this->logBuffer); this->logBuffer = NULL; } } diff --git a/lib/esp8266-oled-ssd1306-master/src/OLEDDisplay.h b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplay.h index 8500b761..d43ee162 100644 --- a/lib/esp8266-oled-ssd1306-master/src/OLEDDisplay.h +++ b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplay.h @@ -359,6 +359,7 @@ class OLEDDisplay : public Stream { // the header size of the buffer used, e.g. for the SPI command header + int BufferOffset; virtual int getBufferOffset(void) = 0; // Send a command to the display (low level function) diff --git a/lib/esp8266-oled-ssd1306-master/src/OLEDDisplayUi.cpp b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplayUi.cpp index 6fcfafbf..778a2e77 100644 --- a/lib/esp8266-oled-ssd1306-master/src/OLEDDisplayUi.cpp +++ b/lib/esp8266-oled-ssd1306-master/src/OLEDDisplayUi.cpp @@ -41,7 +41,7 @@ void LoadingDrawDefault(OLEDDisplay *display, LoadingStage* stage, uint8_t progr OLEDDisplayUi::OLEDDisplayUi(OLEDDisplay *display) { this->display = display; - + indicatorPosition = BOTTOM; indicatorDirection = LEFT_RIGHT; activeSymbol = ANIMATION_activeSymbol; @@ -300,7 +300,10 @@ void OLEDDisplayUi::resetState() { void OLEDDisplayUi::drawFrame(){ switch (this->state.frameState){ case IN_TRANSITION: { - float progress = (float) this->state.ticksSinceLastStateSwitch / (float) this->ticksPerTransition; + float progress = 0.f; + if (this->ticksPerTransition > 0u) { + progress = (float) this->state.ticksSinceLastStateSwitch / (float) this->ticksPerTransition; + } int16_t x = 0, y = 0, x1 = 0, y1 = 0; switch(this->frameAnimationDirection){ case SLIDE_LEFT: diff --git a/lib/esp8266-oled-ssd1306-master/src/SSD1306Wire.h b/lib/esp8266-oled-ssd1306-master/src/SSD1306Wire.h index 7d27850f..9df32d14 100644 --- a/lib/esp8266-oled-ssd1306-master/src/SSD1306Wire.h +++ b/lib/esp8266-oled-ssd1306-master/src/SSD1306Wire.h @@ -82,7 +82,7 @@ class SSD1306Wire : public OLEDDisplay { } bool connect() { -#if !defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH8266) +#if !defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP8266) _wire->begin(); #else // On ESP32 arduino, -1 means 'don't change pins', someone else has called begin for us. @@ -198,7 +198,7 @@ class SSD1306Wire : public OLEDDisplay { void initI2cIfNeccesary() { if (_doI2cAutoInit) { -#if !defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH8266) +#if !defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP8266) _wire->begin(); #else _wire->begin(this->_sda, this->_scl); diff --git a/platformio.ini b/platformio.ini index 5a340709..3a0ff0bd 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = [common] -build_flags = -D BUILD_VERSION='"GUI 1.1.15"' +build_flags = -D BUILD_VERSION='"GUI 1.1.16"' -w -DATOMIC_FS_UPDATE -DBEARSSL_SSL_BASIC diff --git a/src/SuplaCommonPROGMEM.h b/src/SuplaCommonPROGMEM.h index 18cf7412..b0c7e11c 100644 --- a/src/SuplaCommonPROGMEM.h +++ b/src/SuplaCommonPROGMEM.h @@ -150,6 +150,11 @@ const char SSD1306[] PROGMEM = "SSD1306 - 0,96''"; const char SH1106[] PROGMEM = "SH1106 - 1,3''"; const char SSD1306_WEMOS_SHIELD[] PROGMEM = "SSD1306 - 0,66'' WEMOS OLED shield"; const char* const OLED_P[] PROGMEM = {OFF, SSD1306, SH1106, SSD1306_WEMOS_SHIELD}; + +const char CONTROLL_NORMAL[] PROGMEM = "NORMALNE"; +const char CONTROLL_SLOW[] PROGMEM = "WOLNE"; +const char CONTROLL_MANUAL[] PROGMEM = "RĘCZNE"; +const char* const OLED_CONTROLL_P[] PROGMEM = {CONTROLL_NORMAL, CONTROLL_SLOW, CONTROLL_MANUAL}; #endif #endif // SuplaCommonPROGMEM_h diff --git a/src/SuplaConfigManager.cpp b/src/SuplaConfigManager.cpp index 24f60372..3c8ffe7f 100644 --- a/src/SuplaConfigManager.cpp +++ b/src/SuplaConfigManager.cpp @@ -177,6 +177,8 @@ SuplaConfigManager::SuplaConfigManager() { this->addKey(KEY_ADDR_DS18B20, MAX_DS18B20_ADDRESS_HEX * MAX_DS18B20); this->addKey(KEY_NAME_SENSOR, MAX_DS18B20_NAME * MAX_DS18B20); this->addKey(KEY_LEVEL_LED, "0", 1); + this->addKey(KEY_OLED_ANIMATION, "0", 1); + this->addKey(KEY_OLED_BACK_LIGHT_TIME, "5", 2); this->load(); // switch (this->load()) { diff --git a/src/SuplaConfigManager.h b/src/SuplaConfigManager.h index ef012d53..dcd0f50f 100644 --- a/src/SuplaConfigManager.h +++ b/src/SuplaConfigManager.h @@ -44,8 +44,8 @@ #define MAX_MONOSTABLE_TRIGGER 1 #define MAX_FUNCTION 1 -#define MAX_DS18B20 20 -#define MAX_GPIO 17 +#define MAX_DS18B20 20 +#define MAX_GPIO 17 enum _key { @@ -74,9 +74,11 @@ enum _key KEY_ADDR_DS18B20, KEY_NAME_SENSOR, KEY_GPIO, - KEY_LEVEL_LED = KEY_GPIO + MAX_GPIO + 1 - //KEY_DS = KEY_GPIO + MAX_GPIO + MAX_DS18B20, - //KEY_DS_NAME = KEY_DS + MAX_DS18B20 + KEY_LEVEL_LED = KEY_GPIO + MAX_GPIO + 1, + KEY_OLED_ANIMATION, + KEY_OLED_BACK_LIGHT_TIME + // KEY_DS = KEY_GPIO + MAX_GPIO + MAX_DS18B20, + // KEY_DS_NAME = KEY_DS + MAX_DS18B20 }; //#define GPIO "GPIO" diff --git a/src/SuplaOled.cpp b/src/SuplaOled.cpp index 689c43cf..c3f68126 100644 --- a/src/SuplaOled.cpp +++ b/src/SuplaOled.cpp @@ -340,12 +340,24 @@ SuplaOled::SuplaOled() { ui->disableAutoTransition(); } else { + switch (ConfigManager->get(KEY_OLED_ANIMATION)->getValueInt()) { + case OLED_CONTROLL_NORMAL: + ui->setTimePerFrame(5000); + break; + case OLED_CONTROLL_SLOW: + ui->setTimePerFrame(10000); + break; + case OLED_CONTROLL_MANUAL: + ui->disableAutoTransition(); + ui->setTimePerTransition(250); + break; + } + /*ui->setTargetFPS(30); ui->setIndicatorPosition(BOTTOM); ui->setIndicatorDirection(LEFT_RIGHT); - ui->setFrameAnimation(SLIDE_LEFT); + ui->setFrameAnimation(SLIDE_LEFT);*/ } - ui->setTargetFPS(60); ui->setFrames(frames, frameCount); ui->setOverlays(overlays, overlaysCount); ui->init(); @@ -360,7 +372,8 @@ void SuplaOled::iterateAlways() { return; } - if (millis() - timeLastChangeOled > 30000 && oledON) { + if (millis() - timeLastChangeOled > (ConfigManager->get(KEY_OLED_BACK_LIGHT_TIME)->getValueInt() * 1000) && oledON && + ConfigManager->get(KEY_OLED_BACK_LIGHT_TIME)->getValueInt() != 0) { display->setBrightness(50); oledON = false; // display.displayOff(); @@ -381,11 +394,22 @@ void SuplaOled::iterateAlways() { void SuplaOled::addButtonOled(int pin) { if (pin != OFF_GPIO) { Supla::Control::Button* button = new Supla::Control::Button(pin, true, true); - button->addAction(TURN_ON_OLED, this, Supla::ON_PRESS); + + if (ConfigManager->get(KEY_OLED_BACK_LIGHT_TIME)->getValueInt() != 0) { + button->addAction(TURN_ON_OLED, this, Supla::ON_PRESS); + } + + if (ConfigManager->get(KEY_OLED_ANIMATION)->getValueInt() == OLED_CONTROLL_MANUAL) { + button->addAction(NEXT_FRAME, this, Supla::ON_PRESS); + } } } void SuplaOled::handleAction(int event, int action) { + if (action == NEXT_FRAME) { + ui->nextFrame(); + } + if (action == TURN_ON_OLED) { display->setBrightness(255); timeLastChangeOled = millis(); diff --git a/src/SuplaOled.h b/src/SuplaOled.h index 61e8b787..da33d3e6 100644 --- a/src/SuplaOled.h +++ b/src/SuplaOled.h @@ -14,7 +14,8 @@ enum customActions { - TURN_ON_OLED + TURN_ON_OLED, + NEXT_FRAME }; enum _OLED @@ -24,6 +25,13 @@ enum _OLED OLED_SSD1306_0_66 }; +enum +{ + OLED_CONTROLL_NORMAL, + OLED_CONTROLL_SLOW, + OLED_CONTROLL_MANUAL +}; + String getTempString(double temperature); String getHumidityString(double humidity); String getPressureString(double pressure); diff --git a/src/SuplaWebPageSensor.cpp b/src/SuplaWebPageSensor.cpp index 299964d0..e7e5dbcb 100644 --- a/src/SuplaWebPageSensor.cpp +++ b/src/SuplaWebPageSensor.cpp @@ -452,6 +452,16 @@ void SuplaWebPageSensor::handlei2cSave() { ConfigManager->setElement(KEY_ACTIVE_SENSOR, SENSOR_OLED, WebServer->httpServer.arg(input).toInt()); } + if (!WebServer->saveGPIO(INPUT_BUTTON_GPIO, FUNCTION_CFG_BUTTON)) { + supla_webpage_i2c(6); + return; + } + + input = INPUT_OLED_ANIMATION; + ConfigManager->set(KEY_OLED_ANIMATION, WebServer->httpServer.arg(input).c_str()); + input = INPUT_OLED_BRIGHTNESS; + ConfigManager->set(KEY_OLED_BACK_LIGHT_TIME, WebServer->httpServer.arg(input).c_str()); + for (uint8_t i = 0; i < getCountSensorChannels(); i++) { input = INPUT_DS18B20_NAME; input += i; @@ -521,17 +531,24 @@ void SuplaWebPageSensor::supla_webpage_i2c(int save) { #endif #ifdef SUPLA_OLED - selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt(); addFormHeader(webContentBuffer); + + selected = ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt(); addListBox(webContentBuffer, INPUT_OLED, F("OLED"), OLED_P, 4, selected); + if (ConfigManager->get(KEY_ACTIVE_SENSOR)->getElement(SENSOR_OLED).toInt()) { String name, sensorName, input; + addListGPIOBox(webContentBuffer, INPUT_BUTTON_GPIO, F("PRZYCISK OLED"), FUNCTION_CFG_BUTTON); + selected = ConfigManager->get(KEY_OLED_ANIMATION)->getValueInt(); + addListBox(webContentBuffer, INPUT_OLED_ANIMATION, F("STEROWANIE"), OLED_CONTROLL_P, 3, selected); + addNumberBox(webContentBuffer, INPUT_OLED_BRIGHTNESS, F("PODŚWIETLENIE[s]"), KEY_OLED_BACK_LIGHT_TIME, 99); + for (uint8_t i = 0; i < getCountSensorChannels(); i++) { sensorName = String(ConfigManager->get(KEY_NAME_SENSOR)->getElement(i)); input = INPUT_DS18B20_NAME; input += i; - name = F("Ekran "); + name = F("EKRAN "); name += i + 1; addTextBox(webContentBuffer, input, name, sensorName, 0, MAX_DS18B20_NAME, false); } diff --git a/src/SuplaWebPageSensor.h b/src/SuplaWebPageSensor.h index c0b98eb1..c539868f 100644 --- a/src/SuplaWebPageSensor.h +++ b/src/SuplaWebPageSensor.h @@ -3,6 +3,7 @@ #include "SuplaDeviceGUI.h" #include "SuplaWebServer.h" +#include "SuplaWebPageControl.h" #define PATH_MULTI_DS "multids" #define PATH_SAVE_MULTI_DS "savemultids" @@ -27,6 +28,8 @@ #define INPUT_SHT3x "sht30" #define INPUT_SI7021 "si7021" #define INPUT_OLED "oled" +#define INPUT_OLED_ANIMATION "oleda" +#define INPUT_OLED_BRIGHTNESS "oledb" #define INPUT_MCP23017 "mcp" #define INPUT_SI7021_SONOFF "si7021sonoff" #define INPUT_TRIG_GPIO "trig" From 6533c27e591c95e083360c94e051d557bd5d2f5f Mon Sep 17 00:00:00 2001 From: krycha88 Date: Thu, 28 Jan 2021 21:30:58 +0100 Subject: [PATCH 050/165] poprawki oled --- platformio.ini | 2 +- src/SuplaOled.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/platformio.ini b/platformio.ini index 3a0ff0bd..4caa2fdb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = [common] -build_flags = -D BUILD_VERSION='"GUI 1.1.16"' +build_flags = -D BUILD_VERSION='"GUI 1.1.17"' -w -DATOMIC_FS_UPDATE -DBEARSSL_SSL_BASIC diff --git a/src/SuplaOled.cpp b/src/SuplaOled.cpp index c3f68126..140f31ee 100644 --- a/src/SuplaOled.cpp +++ b/src/SuplaOled.cpp @@ -352,15 +352,17 @@ SuplaOled::SuplaOled() { ui->setTimePerTransition(250); break; } - /*ui->setTargetFPS(30); + ui->setTargetFPS(30); ui->setIndicatorPosition(BOTTOM); ui->setIndicatorDirection(LEFT_RIGHT); - ui->setFrameAnimation(SLIDE_LEFT);*/ + ui->setFrameAnimation(SLIDE_LEFT); } ui->setFrames(frames, frameCount); ui->setOverlays(overlays, overlaysCount); ui->init(); + + display->setBrightness(255); display->flipScreenVertically(); } } From 68559869ff20df1a5a8194f4e8b6ed132aa8698c Mon Sep 17 00:00:00 2001 From: krycha88 Date: Thu, 28 Jan 2021 21:31:17 +0100 Subject: [PATCH 051/165] poprawki HLW8012 --- lib/SuplaDevice/src/supla/sensor/HJ101.cpp | 22 +++++++--------------- lib/SuplaDevice/src/supla/sensor/HJ101.h | 15 +++++++++++---- src/SuplaTemplateBoard.cpp | 1 - 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/lib/SuplaDevice/src/supla/sensor/HJ101.cpp b/lib/SuplaDevice/src/supla/sensor/HJ101.cpp index 13b3f082..37217aa1 100644 --- a/lib/SuplaDevice/src/supla/sensor/HJ101.cpp +++ b/lib/SuplaDevice/src/supla/sensor/HJ101.cpp @@ -50,14 +50,14 @@ void HJ101::readValuesFromDevice() { } void HJ101::onSaveState() { - double current_multiplier = getCurrentMultiplier(); - double voltage_multiplier = getVoltageMultiplier(); - double power_multiplier = getPowerMultiplier(); + //double current_multiplier = getCurrentMultiplier(); + //double voltage_multiplier = getVoltageMultiplier(); + //double power_multiplier = getPowerMultiplier(); Supla::Storage::WriteState((unsigned char *)&energy, sizeof(energy)); - Supla::Storage::WriteState((unsigned char *)¤t_multiplier, sizeof(current_multiplier)); - Supla::Storage::WriteState((unsigned char *)&voltage_multiplier, sizeof(voltage_multiplier)); - Supla::Storage::WriteState((unsigned char *)&power_multiplier, sizeof(power_multiplier)); + //Supla::Storage::WriteState((unsigned char *)¤t_multiplier, sizeof(current_multiplier)); + //Supla::Storage::WriteState((unsigned char *)&voltage_multiplier, sizeof(voltage_multiplier)); + //Supla::Storage::WriteState((unsigned char *)&power_multiplier, sizeof(power_multiplier)); } void HJ101::onLoadState() { @@ -134,18 +134,10 @@ void HJ101::calibrate(double calibPower, double calibVoltage) { Serial.println(voltage_multi); Serial.print(F("[HLW] New power multiplier : ")); Serial.println(power_multi); - saveState(); + Supla::Storage::ScheduleSave(1000); yield(); } -void HJ101::saveState() { - Supla::Storage::PrepareState(); - for (auto element = Supla::Element::begin(); element != nullptr; element = element->next()) { - element->onSaveState(); - } - Supla::Storage::FinalizeSaveState(); -} - HLW8012 *HJ101::hj101 = nullptr; }; // namespace Sensor }; // namespace Supla \ No newline at end of file diff --git a/lib/SuplaDevice/src/supla/sensor/HJ101.h b/lib/SuplaDevice/src/supla/sensor/HJ101.h index c4814604..5ccd921d 100644 --- a/lib/SuplaDevice/src/supla/sensor/HJ101.h +++ b/lib/SuplaDevice/src/supla/sensor/HJ101.h @@ -6,8 +6,9 @@ // https://github.com/xoseperez/hlw8012 #include -#include #include +#include + #include "one_phase_electricity_meter.h" namespace Supla { @@ -15,7 +16,11 @@ namespace Sensor { class HJ101 : public OnePhaseElectricityMeter, public Element { public: - HJ101(int8_t pinCF, int8_t pinCF1, int8_t pinSEL, bool currentWhen = LOW, bool use_interrupts = true); + HJ101(int8_t pinCF, + int8_t pinCF1, + int8_t pinSEL, + bool currentWhen = LOW, + bool use_interrupts = true); void onInit(); void readValuesFromDevice(); @@ -26,7 +31,6 @@ class HJ101 : public OnePhaseElectricityMeter, public Element { static void ICACHE_RAM_ATTR hjl01_cf1_interrupt(); static void ICACHE_RAM_ATTR hjl01_cf_interrupt(); void calibrate(double calibPower, double calibVoltage); - void saveState(); double getCurrentMultiplier() { return hj101->getCurrentMultiplier(); @@ -40,12 +44,15 @@ class HJ101 : public OnePhaseElectricityMeter, public Element { void setCurrentMultiplier(double current_multiplier) { hj101->setCurrentMultiplier(current_multiplier); + Supla::Storage::ScheduleSave(1000); }; void setVoltageMultiplier(double voltage_multiplier) { hj101->setVoltageMultiplier(voltage_multiplier); + Supla::Storage::ScheduleSave(1000); }; void setPowerMultiplier(double power_multiplier) { hj101->setPowerMultiplier(power_multiplier); + Supla::Storage::ScheduleSave(1000); }; protected: @@ -57,7 +64,7 @@ class HJ101 : public OnePhaseElectricityMeter, public Element { bool use_interrupts; unsigned _supla_int64_t energy; - unsigned _supla_int64_t _energy; // ---------- energy value read from memory at startup ---- + unsigned _supla_int64_t _energy; // energy value read from memory at startup }; }; // namespace Sensor diff --git a/src/SuplaTemplateBoard.cpp b/src/SuplaTemplateBoard.cpp index 88aa8ace..1fd78e46 100644 --- a/src/SuplaTemplateBoard.cpp +++ b/src/SuplaTemplateBoard.cpp @@ -205,7 +205,6 @@ void chooseTemplateBoard(uint8_t board) { Supla::GUI::counterHLW8012->setCurrentMultiplier(18388); Supla::GUI::counterHLW8012->setVoltageMultiplier(247704); Supla::GUI::counterHLW8012->setPowerMultiplier(2586583); - Supla::GUI::counterHLW8012->saveState(); #endif break; } From 79aaeac9aa830d40f6d9963b0dd3f836aaaa119f Mon Sep 17 00:00:00 2001 From: krycha88 Date: Fri, 29 Jan 2021 13:32:27 +0100 Subject: [PATCH 052/165] MCP23017 -> LEVEL_RELAY, LEVEL_BUTTON --- platformio.ini | 2 +- src/Markup.cpp | 61 +++++++++++++++++++++++++++---------- src/Markup.h | 19 ++++++++++-- src/SuplaConfigESP.cpp | 30 ++++++++++++++---- src/SuplaConfigManager.h | 2 ++ src/SuplaWebPageControl.cpp | 11 +++---- src/SuplaWebPageRelay.cpp | 11 +++---- src/SuplaWebServer.cpp | 2 ++ 8 files changed, 99 insertions(+), 39 deletions(-) diff --git a/platformio.ini b/platformio.ini index 4caa2fdb..8be24209 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = [common] -build_flags = -D BUILD_VERSION='"GUI 1.1.17"' +build_flags = -D BUILD_VERSION='"GUI 1.1.18"' -w -DATOMIC_FS_UPDATE -DBEARSSL_SSL_BASIC diff --git a/src/Markup.cpp b/src/Markup.cpp index c00b434c..4e5b0069 100644 --- a/src/Markup.cpp +++ b/src/Markup.cpp @@ -141,7 +141,7 @@ void addLinkBox(String& html, const String& name, const String& url) { html += url; html += F("'>"); html += name; - //html += PGMT(ICON_EDIT); + // html += PGMT(ICON_EDIT); html += F(""); html += F(""); html += F(""); @@ -181,26 +181,16 @@ void addListMCP23017GPIOBox(String& html, const String& input_id, const String& void addListMCP23017GPIOLinkBox(String& html, const String& input_id, const String& name, uint8_t function, const String& url, uint8_t nr) { if (nr == 1) { uint8_t address = ConfigESP->getAdressMCP23017(nr, function); - addListBox(html, INPUT_ADRESS_MCP23017, F("MCP23017 Adres"), MCP23017_P, 3, address); + addListLinkBox(html, INPUT_ADRESS_MCP23017, F("MCP23017 Adres"), MCP23017_P, 3, address, url); } - html += F(""); - html += F(""); addListMCP23017GPIO(html, input_id, function, nr); html += F(""); @@ -267,6 +257,46 @@ void addListBox(String& html, const String& input_id, const String& name, const html += F(""); } +void addListLinkBox(String& html, + const String& input_id, + const String& name, + const char* const* array_P, + uint8_t size, + uint8_t selected, + const String& url, + uint8_t nr) { + html += F(""); +} + void addButton(String& html, const String& name, const String& url) { html += F("get(key)->getElement(MCP23017_FUNCTION_1).toInt() == function) { if (ConfigManager->get(key)->getElement(MCP23017_NR_1).toInt() == nr) { - return ConfigManager->get(key)->getElement(LEVEL).toInt(); - ; + switch (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_1).toInt()) { + case FUNCTION_RELAY: + return ConfigManager->get(key)->getElement(LEVEL_RELAY).toInt(); + break; + case FUNCTION_BUTTON: + return ConfigManager->get(key)->getElement(LEVEL_BUTTON).toInt(); + break; + } } } break; case 1: if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_2).toInt() == function) { if (ConfigManager->get(key)->getElement(MCP23017_NR_2).toInt() == nr) { - return ConfigManager->get(key)->getElement(LEVEL).toInt(); - ; + switch (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_2).toInt()) { + case FUNCTION_RELAY: + return ConfigManager->get(key)->getElement(LEVEL_RELAY).toInt(); + break; + case FUNCTION_BUTTON: + return ConfigManager->get(key)->getElement(LEVEL_BUTTON).toInt(); + break; + } } } break; case 2: if (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_3).toInt() == function) { if (ConfigManager->get(key)->getElement(MCP23017_NR_3).toInt() == nr) { - return ConfigManager->get(key)->getElement(LEVEL).toInt(); - ; + switch (ConfigManager->get(key)->getElement(MCP23017_FUNCTION_3).toInt()) { + case FUNCTION_RELAY: + return ConfigManager->get(key)->getElement(LEVEL_RELAY).toInt(); + break; + case FUNCTION_BUTTON: + return ConfigManager->get(key)->getElement(LEVEL_BUTTON).toInt(); + break; + } } } break; diff --git a/src/SuplaConfigManager.h b/src/SuplaConfigManager.h index dcd0f50f..6f7be111 100644 --- a/src/SuplaConfigManager.h +++ b/src/SuplaConfigManager.h @@ -100,6 +100,8 @@ enum _settings MCP23017_FUNCTION_3, MCP23017_NR_4, MCP23017_FUNCTION_4, + LEVEL_RELAY, + LEVEL_BUTTON, SETTINGSCOUNT }; diff --git a/src/SuplaWebPageControl.cpp b/src/SuplaWebPageControl.cpp index 996b70bb..b188a0bf 100644 --- a/src/SuplaWebPageControl.cpp +++ b/src/SuplaWebPageControl.cpp @@ -355,13 +355,10 @@ void SuplaWebPageControl::handleButtonSaveSetMCP23017() { input = INPUT_BUTTON_ACTION; action = WebServer->httpServer.arg(input).toInt(); - for (uint8_t i = 1; i <= OFF_GPIO; i++) { - gpio = ConfigESP->getGpioMCP23017(i, FUNCTION_BUTTON); - if (gpio != OFF_GPIO) { - key = KEY_GPIO + gpio; - ConfigManager->setElement(key, LEVEL, level); - ConfigManager->setElement(key, ACTION, action); - } + for (gpio = 0; gpio <= OFF_GPIO; gpio++) { + key = KEY_GPIO + gpio; + ConfigManager->setElement(key, LEVEL_BUTTON, level); + ConfigManager->setElement(key, ACTION, action); } switch (ConfigManager->save()) { case E_CONFIG_OK: diff --git a/src/SuplaWebPageRelay.cpp b/src/SuplaWebPageRelay.cpp index 09f4bb08..d6fc0862 100644 --- a/src/SuplaWebPageRelay.cpp +++ b/src/SuplaWebPageRelay.cpp @@ -261,13 +261,10 @@ void SuplaWebPageRelay::handleRelaySaveSetMCP23017() { input = INPUT_RELAY_LEVEL; level = WebServer->httpServer.arg(input).toInt(); - for (uint8_t i = 1; i <= OFF_GPIO; i++) { - gpio = ConfigESP->getGpioMCP23017(i, FUNCTION_RELAY); - if (gpio != OFF_GPIO) { - key = KEY_GPIO + gpio; - ConfigManager->setElement(key, MEMORY, memory); - ConfigManager->setElement(key, LEVEL, level); - } + for (gpio = 0; gpio <= OFF_GPIO; gpio++) { + key = KEY_GPIO + gpio; + ConfigManager->setElement(key, MEMORY, memory); + ConfigManager->setElement(key, LEVEL_RELAY, level); } switch (ConfigManager->save()) { diff --git a/src/SuplaWebServer.cpp b/src/SuplaWebServer.cpp index c20e6b4a..62649dff 100644 --- a/src/SuplaWebServer.cpp +++ b/src/SuplaWebServer.cpp @@ -344,6 +344,8 @@ void SuplaWebServer::sendContent() { void SuplaWebServer::handleNotFound() { httpServer.sendHeader("Location", "/", true); httpServer.send(302, "text/plane", ""); + + supla_webpage_reboot(); } bool SuplaWebServer::saveGPIO(const String& _input, uint8_t function, uint8_t nr, const String& input_max) { From 879749900467fed110c56852ef2925d4c30c15c9 Mon Sep 17 00:00:00 2001 From: krycha88 Date: Thu, 4 Feb 2021 12:59:26 +0100 Subject: [PATCH 053/165] wsparcie dla RGBW, RGB, DIMMER --- platformio.ini | 9 +- src/GUI-Generic.ino | 7 + src/GUI-Generic_Config.h | 3 + src/Markup.cpp | 14 +- src/Markup.h | 4 +- src/SuplaConfigESP.cpp | 16 +- src/SuplaConfigManager.cpp | 10 +- src/SuplaConfigManager.h | 13 +- src/SuplaDeviceGUI.cpp | 65 ++++++- src/SuplaDeviceGUI.h | 12 ++ src/SuplaOled.cpp | 10 +- src/SuplaTemplateBoard.cpp | 50 ++++-- src/SuplaTemplateBoard.h | 8 +- src/SuplaWebPageOther.cpp | 359 +++++++++++++++++++++++++++++++++++++ src/SuplaWebPageOther.h | 53 ++++++ src/SuplaWebPageSensor.cpp | 351 ------------------------------------ src/SuplaWebPageSensor.h | 35 ---- src/SuplaWebServer.cpp | 24 +-- src/language/pl.h | 1 - 19 files changed, 605 insertions(+), 439 deletions(-) create mode 100644 src/SuplaWebPageOther.cpp create mode 100644 src/SuplaWebPageOther.h diff --git a/platformio.ini b/platformio.ini index 8be24209..edc13b3c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,7 +22,7 @@ default_envs = [common] -build_flags = -D BUILD_VERSION='"GUI 1.1.18"' +build_flags = -D BUILD_VERSION='"GUI-Generic 1.2.0"' -w -DATOMIC_FS_UPDATE -DBEARSSL_SSL_BASIC @@ -44,7 +44,8 @@ build_flags = -D BUILD_VERSION='"GUI 1.1.18"' -D SUPLA_IMPULSE_COUNTER -D SUPLA_OLED -D SUPLA_HLW8012 - -D SUPLA_MCP23017 + -D SUPLA_MCP23017 + -D SUPLA_RGBW [env] framework = arduino @@ -53,8 +54,10 @@ upload_speed = 256000 monitor_speed = 74880 upload_resetmethod = nodemcu board_build.flash_mode = dout -; set frequency to 160MHz +;set frequency to 160MHz board_build.f_cpu = 160000000L +; set frequency to 80MHz +board_build.f_flash = 80000000L lib_deps = milesburton/DallasTemperature@^3.9.1 adafruit/DHT sensor library@^1.4.0 diff --git a/src/GUI-Generic.ino b/src/GUI-Generic.ino index d43920d0..09c3b128 100644 --- a/src/GUI-Generic.ino +++ b/src/GUI-Generic.ino @@ -195,6 +195,13 @@ void setup() { #endif } #endif + +#ifdef SUPLA_RGBW + for (nr = 1; nr <= ConfigManager->get(KEY_MAX_RGBW)->getValueInt(); nr++) { + Supla::GUI::addRGBWLeds(nr); + } +#endif + Supla::GUI::begin(); } diff --git a/src/GUI-Generic_Config.h b/src/GUI-Generic_Config.h index c3d7cb17..23c604a7 100644 --- a/src/GUI-Generic_Config.h +++ b/src/GUI-Generic_Config.h @@ -1,7 +1,9 @@ #ifndef GUI_Generic_Config_h #define GUI_Generic_Config_h +#ifndef DEBUG_MODE #define supla_lib_config_h_ // silences unnecessary debug messages "should be disabled by default" +#endif //#define USE_CUSTOM @@ -49,6 +51,7 @@ #define SUPLA_HC_SR04 #define SUPLA_IMPULSE_COUNTER #define SUPLA_HLW8012 +#define SUPLA_RGBW #endif #endif // GUI-Generic_Config_h diff --git a/src/Markup.cpp b/src/Markup.cpp index 4e5b0069..98f7127f 100644 --- a/src/Markup.cpp +++ b/src/Markup.cpp @@ -147,8 +147,14 @@ void addLinkBox(String& html, const String& name, const String& url) { html += F(""); } -void addListGPIOBox(String& html, const String& input_id, const String& name, uint8_t function, uint8_t nr) { - html += F("