From fd94716daaaa21ef74cfcee963ec41f8727f51e1 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 23 Apr 2022 19:04:23 +0200 Subject: [PATCH 001/113] [P123] Add FT6206 Capacitive touch screen --- .../Adafruit_FT6206.cpp | 304 +++++ lib/Adafruit_FT6206_Library/Adafruit_FT6206.h | 79 ++ lib/Adafruit_FT6206_Library/README.md | 17 + .../library.properties | 10 + platformio_esp32_envs.ini | 2 +- platformio_esp82xx_base.ini | 2 +- src/Custom-sample.h | 1 + src/_P123_FT62x6Touch.ino | 265 ++++ src/src/CustomBuild/define_plugin_sets.h | 5 +- src/src/Globals/Plugins.cpp | 30 +- src/src/Globals/Plugins.h | 5 + src/src/PluginStructs/P123_data_struct.cpp | 1160 +++++++++++++++++ src/src/PluginStructs/P123_data_struct.h | 259 ++++ src/src/WebServer/Markup.cpp | 5 + src/src/WebServer/Markup.h | 2 + src/src/WebServer/WebServer.cpp | 5 +- src/src/WebServer/WebServer.h | 3 +- 17 files changed, 2145 insertions(+), 9 deletions(-) create mode 100644 lib/Adafruit_FT6206_Library/Adafruit_FT6206.cpp create mode 100644 lib/Adafruit_FT6206_Library/Adafruit_FT6206.h create mode 100644 lib/Adafruit_FT6206_Library/README.md create mode 100644 lib/Adafruit_FT6206_Library/library.properties create mode 100644 src/_P123_FT62x6Touch.ino create mode 100644 src/src/PluginStructs/P123_data_struct.cpp create mode 100644 src/src/PluginStructs/P123_data_struct.h diff --git a/lib/Adafruit_FT6206_Library/Adafruit_FT6206.cpp b/lib/Adafruit_FT6206_Library/Adafruit_FT6206.cpp new file mode 100644 index 0000000000..15cd463e74 --- /dev/null +++ b/lib/Adafruit_FT6206_Library/Adafruit_FT6206.cpp @@ -0,0 +1,304 @@ +/*! + * @file Adafruit_FT6206.cpp + * + * @mainpage Adafruit FT2606 Library + * + * @section intro_sec Introduction + * + * This is a library for the Adafruit Capacitive Touch Screens + * + * ----> http://www.adafruit.com/products/1947 + * + * Adjustment: To avoid a conflict with the XPT2048 library, TS_Point struct is renamed to FT_Point + * + * Check out the links above for our tutorials and wiring diagrams + * This chipset uses I2C to communicate + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * @section author Author + * + * Written by Limor Fried/Ladyada for Adafruit Industries. + * + * @section license License + + * MIT license, all text above must be included in any redistribution + */ + +#include "Arduino.h" +#include +#include + +#if defined(__SAM3X8E__) +#define Wire Wire1 +#endif + +//#define FT6206_DEBUG +//#define I2C_DEBUG + +/**************************************************************************/ +/*! + @brief Instantiates a new FT6206 class +*/ +/**************************************************************************/ +// I2C, no address adjustments or pins +Adafruit_FT6206::Adafruit_FT6206() { touches = 0; } + +/**************************************************************************/ +/*! + @brief Setups the I2C interface and hardware, identifies if chip is found + @param thresh Optional threshhold-for-touch value, default is + FT6206_DEFAULT_THRESSHOLD but you can try changing it if your screen is + too/not sensitive. + @returns True if an FT6206 is found, false on any failure +*/ +/**************************************************************************/ +boolean Adafruit_FT6206::begin(uint8_t thresh) { + // Wire.begin(); + +#ifdef FT6206_DEBUG + Serial.print("Vend ID: 0x"); + Serial.println(readRegister8(FT62XX_REG_VENDID), HEX); + Serial.print("Chip ID: 0x"); + Serial.println(readRegister8(FT62XX_REG_CHIPID), HEX); + Serial.print("Firm V: "); + Serial.println(readRegister8(FT62XX_REG_FIRMVERS)); + Serial.print("Point Rate Hz: "); + Serial.println(readRegister8(FT62XX_REG_POINTRATE)); + Serial.print("Thresh: "); + Serial.println(readRegister8(FT62XX_REG_THRESHHOLD)); + + // dump all registers + for (int16_t i = 0; i < 0x10; i++) { + Serial.print("I2C $"); + Serial.print(i, HEX); + Serial.print(" = 0x"); + Serial.println(readRegister8(i), HEX); + } +#endif + + // change threshhold to be higher/lower + writeRegister8(FT62XX_REG_THRESHHOLD, thresh); + + if (readRegister8(FT62XX_REG_VENDID) != FT62XX_VENDID) { + return false; + } + uint8_t id = readRegister8(FT62XX_REG_CHIPID); + if ((id != FT6206_CHIPID) && (id != FT6236_CHIPID) && + (id != FT6236U_CHIPID)) { + return false; + } + + return true; +} + +/**************************************************************************/ +/*! + @brief Determines if there are any touches detected + @returns Number of touches detected, can be 0, 1 or 2 +*/ +/**************************************************************************/ +uint8_t Adafruit_FT6206::touched(void) { + uint8_t n = readRegister8(FT62XX_REG_NUMTOUCHES); + if (n > 2) { + n = 0; + } + return n; +} + +/**************************************************************************/ +/*! + @brief Queries the chip and retrieves a point data + @param n The # index (0 or 1) to the points we can detect. In theory we can + detect 2 points but we've found that you should only use this for + single-touch since the two points cant share the same half of the screen. + @returns {@link FT_Point} object that has the x and y coordinets set. If the + z coordinate is 0 it means the point is not touched. If z is 1, it is + currently touched. +*/ +/**************************************************************************/ +FT_Point Adafruit_FT6206::getPoint(uint8_t n) { + readData(); + if ((touches == 0) || (n > 1)) { + return FT_Point(0, 0, 0); + } else { + return FT_Point(touchX[n], touchY[n], 1); + } +} + +/************ lower level i/o **************/ + +/**************************************************************************/ +/*! + @brief Reads the bulk of data from captouch chip. Fill in {@link touches}, + {@link touchX}, {@link touchY} and {@link touchID} with results +*/ +/**************************************************************************/ +void Adafruit_FT6206::readData(void) { + + uint8_t i2cdat[16]; + Wire.beginTransmission(FT62XX_ADDR); + Wire.write((byte)0); + Wire.endTransmission(); + + Wire.requestFrom((byte)FT62XX_ADDR, (byte)16); + for (uint8_t i = 0; i < 16; i++) + i2cdat[i] = Wire.read(); + +#ifdef FT6206_DEBUG + for (int16_t i = 0; i < 16; i++) { + Serial.print("I2C $"); + Serial.print(i, HEX); + Serial.print(" = 0x"); + Serial.println(i2cdat[i], HEX); + } +#endif + + touches = i2cdat[0x02]; + if ((touches > 2) || (touches == 0)) { + touches = 0; + } + +#ifdef FT6206_DEBUG + Serial.print("# Touches: "); + Serial.println(touches); + + for (uint8_t i = 0; i < 16; i++) { + Serial.print("0x"); + Serial.print(i2cdat[i], HEX); + Serial.print(" "); + } + Serial.println(); + if (i2cdat[0x01] != 0x00) { + Serial.print("Gesture #"); + Serial.println(i2cdat[0x01]); + } +#endif + + for (uint8_t i = 0; i < 2; i++) { + touchX[i] = i2cdat[0x03 + i * 6] & 0x0F; + touchX[i] <<= 8; + touchX[i] |= i2cdat[0x04 + i * 6]; + touchY[i] = i2cdat[0x05 + i * 6] & 0x0F; + touchY[i] <<= 8; + touchY[i] |= i2cdat[0x06 + i * 6]; + touchID[i] = i2cdat[0x05 + i * 6] >> 4; + } + +#ifdef FT6206_DEBUG + Serial.println(); + for (uint8_t i = 0; i < touches; i++) { + Serial.print("ID #"); + Serial.print(touchID[i]); + Serial.print("\t("); + Serial.print(touchX[i]); + Serial.print(", "); + Serial.print(touchY[i]); + Serial.print(") "); + } + Serial.println(); +#endif +} + +uint8_t Adafruit_FT6206::readRegister8(uint8_t reg) { + uint8_t x; + // use i2c + Wire.beginTransmission(FT62XX_ADDR); + Wire.write((byte)reg); + Wire.endTransmission(); + + Wire.requestFrom((byte)FT62XX_ADDR, (byte)1); + x = Wire.read(); + +#ifdef I2C_DEBUG + Serial.print("$"); + Serial.print(reg, HEX); + Serial.print(": 0x"); + Serial.println(x, HEX); +#endif + + return x; +} + +void Adafruit_FT6206::writeRegister8(uint8_t reg, uint8_t val) { + // use i2c + Wire.beginTransmission(FT62XX_ADDR); + Wire.write((byte)reg); + Wire.write((byte)val); + Wire.endTransmission(); +} + +/* + +// DONT DO THIS - REALLY - IT DOESNT WORK +void Adafruit_FT6206::autoCalibrate(void) { + writeRegister8(FT06_REG_MODE, FT6206_REG_FACTORYMODE); + delay(100); + //Serial.println("Calibrating..."); + writeRegister8(FT6206_REG_CALIBRATE, 4); + delay(300); + for (uint8_t i = 0; i < 100; i++) { + uint8_t temp; + temp = readRegister8(FT6206_REG_MODE); + Serial.println(temp, HEX); + //return to normal mode, calibration finish + if (0x0 == ((temp & 0x70) >> 4)) + break; + } + delay(200); + //Serial.println("Calibrated"); + delay(300); + writeRegister8(FT6206_REG_MODE, FT6206_REG_FACTORYMODE); + delay(100); + writeRegister8(FT6206_REG_CALIBRATE, 5); + delay(300); + writeRegister8(FT6206_REG_MODE, FT6206_REG_WORKMODE); + delay(300); +} +*/ + +/****************/ + +/**************************************************************************/ +/*! + @brief Instantiates a new FT6206 class with x, y and z set to 0 by default +*/ +/**************************************************************************/ +FT_Point::FT_Point(void) { x = y = z = 0; } + +/**************************************************************************/ +/*! + @brief Instantiates a new FT6206 class with x, y and z set by params. + @param _x The X coordinate + @param _y The Y coordinate + @param _z The Z coordinate +*/ +/**************************************************************************/ + +FT_Point::FT_Point(int16_t _x, int16_t _y, int16_t _z) { + x = _x; + y = _y; + z = _z; +} + +/**************************************************************************/ +/*! + @brief Simple == comparator for two FT_Point objects + @returns True if x, y and z are the same for both points, False otherwise. +*/ +/**************************************************************************/ +bool FT_Point::operator==(FT_Point p1) { + return ((p1.x == x) && (p1.y == y) && (p1.z == z)); +} + +/**************************************************************************/ +/*! + @brief Simple != comparator for two FT_Point objects + @returns False if x, y and z are the same for both points, True otherwise. +*/ +/**************************************************************************/ +bool FT_Point::operator!=(FT_Point p1) { + return ((p1.x != x) || (p1.y != y) || (p1.z != z)); +} diff --git a/lib/Adafruit_FT6206_Library/Adafruit_FT6206.h b/lib/Adafruit_FT6206_Library/Adafruit_FT6206.h new file mode 100644 index 0000000000..7ad9de077a --- /dev/null +++ b/lib/Adafruit_FT6206_Library/Adafruit_FT6206.h @@ -0,0 +1,79 @@ +/*! + * @file Adafruit_FT6206.h + */ + +#ifndef ADAFRUIT_FT6206_LIBRARY +#define ADAFRUIT_FT6206_LIBRARY + +#include "Arduino.h" +#include + +#define FT62XX_ADDR 0x38 //!< I2C address +#define FT62XX_G_FT5201ID 0xA8 //!< FocalTech's panel ID +#define FT62XX_REG_NUMTOUCHES 0x02 //!< Number of touch points + +#define FT62XX_NUM_X 0x33 //!< Touch X position +#define FT62XX_NUM_Y 0x34 //!< Touch Y position + +#define FT62XX_REG_MODE 0x00 //!< Device mode, either WORKING or FACTORY +#define FT62XX_REG_CALIBRATE 0x02 //!< Calibrate mode +#define FT62XX_REG_WORKMODE 0x00 //!< Work mode +#define FT62XX_REG_FACTORYMODE 0x40 //!< Factory mode +#define FT62XX_REG_THRESHHOLD 0x80 //!< Threshold for touch detection +#define FT62XX_REG_POINTRATE 0x88 //!< Point rate +#define FT62XX_REG_FIRMVERS 0xA6 //!< Firmware version +#define FT62XX_REG_CHIPID 0xA3 //!< Chip selecting +#define FT62XX_REG_VENDID 0xA8 //!< FocalTech's panel ID + +#define FT62XX_VENDID 0x11 //!< FocalTech's panel ID +#define FT6206_CHIPID 0x06 //!< Chip selecting +#define FT6236_CHIPID 0x36 //!< Chip selecting +#define FT6236U_CHIPID 0x64 //!< Chip selecting + +// calibrated for Adafruit 2.8" ctp screen +#define FT62XX_DEFAULT_THRESHOLD 128 //!< Default threshold for touch detection + +/**************************************************************************/ +/*! + @brief Helper class that stores a TouchScreen Point with x, y, and z + coordinates, for easy math/comparison +*/ +/**************************************************************************/ +class FT_Point { +public: + FT_Point(void); + FT_Point(int16_t x, int16_t y, int16_t z); + + bool operator==(FT_Point); + bool operator!=(FT_Point); + + int16_t x; /*!< X coordinate */ + int16_t y; /*!< Y coordinate */ + int16_t z; /*!< Z coordinate (often used for pressure) */ +}; + +/**************************************************************************/ +/*! + @brief Class that stores state and functions for interacting with FT6206 + capacitive touch chips +*/ +/**************************************************************************/ +class Adafruit_FT6206 { +public: + Adafruit_FT6206(void); + boolean begin(uint8_t thresh = FT62XX_DEFAULT_THRESHOLD); + uint8_t touched(void); + FT_Point getPoint(uint8_t n = 0); + + // void autoCalibrate(void); + +private: + void writeRegister8(uint8_t reg, uint8_t val); + uint8_t readRegister8(uint8_t reg); + + void readData(void); + uint8_t touches; + uint16_t touchX[2], touchY[2], touchID[2]; +}; + +#endif // ADAFRUIT_FT6206_LIBRARY diff --git a/lib/Adafruit_FT6206_Library/README.md b/lib/Adafruit_FT6206_Library/README.md new file mode 100644 index 0000000000..3005110f50 --- /dev/null +++ b/lib/Adafruit_FT6206_Library/README.md @@ -0,0 +1,17 @@ +# Adafruit_FT6206 Library [![Build Status](https://github.com/adafruit/Adafruit_FT6206_Library/workflows/Arduino%20Library%20CI/badge.svg)](https://github.com/adafruit/Adafruit_FT6206_Library/actions)[![Documentation](https://github.com/adafruit/ci-arduino/blob/master/assets/doxygen_badge.svg)](http://adafruit.github.io/Adafruit_FT6206_Library/html/index.html) + + + +This is a library for the Adafruit FT6206-Based capacitive touch screens and displays: + * https://www.adafruit.com/products/1947 + * https://www.adafruit.com/product/1651 + +Also supports FT6236 chips (and maybe other compatible chips!) + +Check out the links above for our tutorials and wiring diagrams. This chip uses I2C to communicate + +Adafruit invests time and resources providing this open source code, please support Adafruit and open-source hardware by purchasing products from Adafruit! + +Written by Limor Fried/Ladyada for Adafruit Industries. +MIT license, all text above must be included in any redistribution + diff --git a/lib/Adafruit_FT6206_Library/library.properties b/lib/Adafruit_FT6206_Library/library.properties new file mode 100644 index 0000000000..aa1c3c5c9f --- /dev/null +++ b/lib/Adafruit_FT6206_Library/library.properties @@ -0,0 +1,10 @@ +name=Adafruit FT6206 ESPEasy +version=1.0.6 +author=Adafruit +maintainer=Adafruit +sentence=Arduino library for FT6206-based Capacitive touch screen adjusted for use in ESPEasy +paragraph=Arduino library for FT6206-based Capacitive touch screen adjusted for use in ESPEasy +category=Display +url=https://github.com/adafruit/Adafruit_FT6206_Library +architectures=* +depends=Adafruit GFX Library diff --git a/platformio_esp32_envs.ini b/platformio_esp32_envs.ini index ad8f21f33d..8e04a1a7c6 100644 --- a/platformio_esp32_envs.ini +++ b/platformio_esp32_envs.ini @@ -11,7 +11,7 @@ lib_ignore = ESP8266WiFi, ESP8266Ping, ESP8266WebServer, ESP8266H [esp32_common] extends = common, core_esp32_3_5_0 platform = ${core_esp32_3_5_0.platform} -lib_deps = td-er/ESPeasySerial @ 2.0.8, I2Cdevlib-Core, adafruit/Adafruit ILI9341 @ ^1.5.6, Adafruit GFX Library, LOLIN_EPD, adafruit/Adafruit BusIO @ ^1.10.0, VL53L0X @ 1.3.0, SparkFun VL53L1X 4m Laser Distance Sensor @ 1.2.9, td-er/SparkFun MAX1704x Fuel Gauge Arduino Library @ ^1.0.1, ArduinoOTA, ESP32HTTPUpdateServer, FrogmoreScd30, Multi Channel Relay Arduino Library, SparkFun ADXL345 Arduino Library, ITG3205, Adafruit_ST77xx, ShiftRegister74HC595_NonTemplate +lib_deps = td-er/ESPeasySerial @ 2.0.8, I2Cdevlib-Core, adafruit/Adafruit ILI9341 @ ^1.5.6, Adafruit GFX Library, LOLIN_EPD, adafruit/Adafruit BusIO @ ^1.10.0, VL53L0X @ 1.3.0, SparkFun VL53L1X 4m Laser Distance Sensor @ 1.2.9, td-er/SparkFun MAX1704x Fuel Gauge Arduino Library @ ^1.0.1, ArduinoOTA, ESP32HTTPUpdateServer, FrogmoreScd30, Multi Channel Relay Arduino Library, SparkFun ADXL345 Arduino Library, ITG3205, Adafruit_ST77xx, ShiftRegister74HC595_NonTemplate, Adafruit FT6206 ESPEasy lib_ignore = ${esp32_always.lib_ignore}, ESP32_ping, IRremoteESP8266, HeatpumpIR board_build.f_flash = 80000000L board_build.flash_mode = dout diff --git a/platformio_esp82xx_base.ini b/platformio_esp82xx_base.ini index 7c16053ce5..a55ff1bdb1 100644 --- a/platformio_esp82xx_base.ini +++ b/platformio_esp82xx_base.ini @@ -49,7 +49,7 @@ board_build.f_cpu = 80000000L build_flags = ${debug_flags.build_flags} ${mqtt_flags.build_flags} -DHTTPCLIENT_1_1_COMPATIBLE=0 build_unflags = -DDEBUG_ESP_PORT -fexceptions -lib_deps = td-er/ESPeasySerial @ 2.0.8, I2Cdevlib-Core, ESP8266WebServer, adafruit/Adafruit ILI9341 @ ^1.5.6, Adafruit GFX Library, LOLIN_EPD, adafruit/Adafruit BusIO @ ^1.10.0, bblanchon/ArduinoJson @ ^6.17.2, VL53L0X @ 1.3.0, SparkFun VL53L1X 4m Laser Distance Sensor @ 1.2.9, td-er/RABurton ESP8266 Mutex @ ^1.0.2, td-er/SparkFun MAX1704x Fuel Gauge Arduino Library @ ^1.0.1, ESP8266HTTPUpdateServer, FrogmoreScd30, Multi Channel Relay Arduino Library, SparkFun ADXL345 Arduino Library, ITG3205, Adafruit_ST77xx, ShiftRegister74HC595_NonTemplate +lib_deps = td-er/ESPeasySerial @ 2.0.8, I2Cdevlib-Core, ESP8266WebServer, adafruit/Adafruit ILI9341 @ ^1.5.6, Adafruit GFX Library, LOLIN_EPD, adafruit/Adafruit BusIO @ ^1.10.0, bblanchon/ArduinoJson @ ^6.17.2, VL53L0X @ 1.3.0, SparkFun VL53L1X 4m Laser Distance Sensor @ 1.2.9, td-er/RABurton ESP8266 Mutex @ ^1.0.2, td-er/SparkFun MAX1704x Fuel Gauge Arduino Library @ ^1.0.1, ESP8266HTTPUpdateServer, FrogmoreScd30, Multi Channel Relay Arduino Library, SparkFun ADXL345 Arduino Library, ITG3205, Adafruit_ST77xx, ShiftRegister74HC595_NonTemplate, Adafruit FT6206 ESPEasy lib_ignore = ${esp82xx_defaults.lib_ignore}, IRremoteESP8266, HeatpumpIR, LittleFS(esp8266), ServoESP32, TinyWireM board = esp12e monitor_filters = esp8266_exception_decoder diff --git a/src/Custom-sample.h b/src/Custom-sample.h index 2d8577c317..75be9c93aa 100644 --- a/src/Custom-sample.h +++ b/src/Custom-sample.h @@ -388,6 +388,7 @@ static const char DATA_ESPEASY_DEFAULT_MIN_CSS[] PROGMEM = { // #define USES_P117 // SCD30 // #define USES_P119 // ITG3205 Gyro // #define USES_P120 // ADXL345 I2C Acceleration / Gravity +// #define USES_P123 // FT6206 // #define USES_P124 // I2C MultiRelay // #define USES_P125 // ADXL345 SPI Acceleration / Gravity // #define USES_P126 // 74HC595 Shift register diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino new file mode 100644 index 0000000000..653df02e90 --- /dev/null +++ b/src/_P123_FT62x6Touch.ino @@ -0,0 +1,265 @@ +#ifdef USES_P123 + +// ####################################################################################################### +// #################################### Plugin 123: FT6206 Touchscreen ################################# +// ####################################################################################################### + +/** + * Changelog: + * 2022-04-23 tonhuisman: Rename struct TS_Point in FT6206 library to FT_Point to avoid conflict with XPT2048 library (P099) + * 2021-11-07 tonhuisman: Initial plugin, based on _P099_XPT2046_Touchscreen.ino plugin and Adafruit FT6206 Library + */ + +/** + * Commands supported: + * ------------------- + * touch,rot,<0..3> : Set rotation to 0(0), 90(1), 180(2), 270(3) degrees + * touch,flip,<0|1> : Set rotation normal(0) or flipped by 180 degrees(1) + * touch,enable, : Enables a disabled objectname (removes a leading underscore) + * touch,disable, : Disables an enabled objectname (adds a leading underscore) + */ + +#define PLUGIN_123 +#define PLUGIN_ID_123 123 +#define PLUGIN_NAME_123 "Touch - FT62x6 touchscreen [TESTING]" +#define PLUGIN_VALUENAME1_123 "X" +#define PLUGIN_VALUENAME2_123 "Y" +#define PLUGIN_VALUENAME3_123 "Z" + +#include "_Plugin_Helper.h" +#include "src/PluginStructs/P123_data_struct.h" + + +boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) +{ + boolean success = false; + + switch (function) + { + case PLUGIN_DEVICE_ADD: + { + Device[++deviceCount].Number = PLUGIN_ID_123; + Device[deviceCount].Type = DEVICE_TYPE_I2C; + Device[deviceCount].VType = Sensor_VType::SENSOR_TYPE_TRIPLE; + Device[deviceCount].Ports = 0; + Device[deviceCount].PullUpOption = false; + Device[deviceCount].InverseLogicOption = false; + Device[deviceCount].FormulaOption = false; + Device[deviceCount].ValueCount = 3; + Device[deviceCount].SendDataOption = false; + Device[deviceCount].TimerOption = false; + success = true; + break; + } + + case PLUGIN_GET_DEVICENAME: + { + string = F(PLUGIN_NAME_123); + success = true; + break; + } + + case PLUGIN_GET_DEVICEVALUENAMES: + { + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_123)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[1], PSTR(PLUGIN_VALUENAME2_123)); + strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[2], PSTR(PLUGIN_VALUENAME3_123)); + success = true; + break; + } + + case PLUGIN_I2C_HAS_ADDRESS: + { + success = (event->Par1 == 0x38); // Fixed I2C address + break; + } + + case PLUGIN_SET_DEFAULTS: + { + // if already configured take it from settings, else use default values + P123_CONFIG_DISPLAY_TASK = event->TaskIndex; // Preselect current task to acvoid pointing to Task 1 by default + P123_CONFIG_ROTATION = P123_TS_ROTATION; + P123_CONFIG_X_RES = P123_TS_X_RES; + P123_CONFIG_Y_RES = P123_TS_Y_RES; + + success = true; + break; + } + case PLUGIN_WEBFORM_LOAD: + { + addFormSubHeader(F("Screen")); + + { + addRowLabel(F("Display task")); + addTaskSelect(F("p123_task"), P123_CONFIG_DISPLAY_TASK); + addFormNote(F("Screen Width, Heigth, Rotation & Color-depth will be fetched from the Display task if possible.")); + } + + uint16_t width_ = P123_CONFIG_X_RES; + uint16_t height_ = P123_CONFIG_Y_RES; + uint16_t rotation_ = P123_CONFIG_ROTATION; + uint16_t colorDepth_ = P123_COLOR_DEPTH; + + if (P123_CONFIG_DISPLAY_TASK != P123_CONFIG_DISPLAY_PREV) { // Changed since last saved? + getPluginDisplayParametersFromTaskIndex(P123_CONFIG_DISPLAY_TASK, width_, height_, rotation_, colorDepth_); + } + P123_COLOR_DEPTH = colorDepth_; + + if (width_ == 0) { + width_ = P123_TS_X_RES; // default value + } + addFormNumericBox(F("Screen Width (px) (x)"), F("p123_width"), width_, 1, 65535); + + + if (height_ == 0) { + height_ = P123_TS_Y_RES; // default value + } + addFormNumericBox(F("Screen Height (px) (y)"), F("p123_height"), height_, 1, 65535); + + AdaGFXFormRotation(F("p123_rotate"), rotation_); + + AdaGFXFormColorDepth(F("p123_colordepth"), P123_COLOR_DEPTH, (colorDepth_ == 0)); + + { + P123_data_struct *P123_data = new (std::nothrow) P123_data_struct(); + + if (nullptr == P123_data) { + return success; + } + P123_data->loadTouchObjects(event); + + P123_data->plugin_webform_load(event); + + delete P123_data; + } + success = true; + break; + } + + case PLUGIN_WEBFORM_SAVE: + { + P123_CONFIG_DISPLAY_PREV = P123_CONFIG_DISPLAY_TASK; + P123_CONFIG_DISPLAY_TASK = getFormItemInt(F("p123_task")); + P123_CONFIG_ROTATION = getFormItemInt(F("p123_rotate")); + P123_CONFIG_X_RES = getFormItemInt(F("p123_width")); + P123_CONFIG_Y_RES = getFormItemInt(F("p123_height")); + + int colorDepth = getFormItemInt(F("p123_colordepth"), -1); + + if (colorDepth != -1) { + P123_COLOR_DEPTH = colorDepth; + } + + if (P123_CONFIG_OBJECTCOUNT > P123_MAX_OBJECT_COUNT) { P123_CONFIG_OBJECTCOUNT = P123_MAX_OBJECT_COUNT; } + + P123_data_struct *P123_data = new (std::nothrow) P123_data_struct(); + + if (nullptr == P123_data) { + return success; // Save other settings even though this didn't initialize properly + } + + success = P123_data->plugin_webform_save(event); + + delete P123_data; + + break; + } + + case PLUGIN_INIT: + { + initPluginTaskData(event->TaskIndex, new (std::nothrow) P123_data_struct()); + P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr == P123_data) { + return success; + } + + success = true; + + if (!(P123_data->init(event, + P123_CONFIG_ROTATION, + P123_CONFIG_X_RES, + P123_CONFIG_Y_RES, + P123_CONFIG_DISPLAY_TASK, + static_cast(P123_COLOR_DEPTH)))) { + delete P123_data; + P123_data = nullptr; + success = false; + } + break; + } + + // case PLUGIN_READ: // Not implemented on purpose, *only* send out events/values when device is touched, and configured to send events + + case PLUGIN_WRITE: + { + String command; + String subcommand; + String arguments; + arguments.reserve(24); + + { + command = parseString(string, 1); + subcommand = parseString(string, 2); + + P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr == P123_data) { + return success; + } + + if (command.equals(F("touch"))) { + #ifdef PLUGIN_123_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("P123 WRITE arguments Par1:"); + log += event->Par1; + log += F(", 2: "); + log += event->Par2; + log += F(", 3: "); + log += event->Par3; + log += F(", 4: "); + log += event->Par4; + addLog(LOG_LEVEL_INFO, log); + } + #endif // ifdef PLUGIN_123_DEBUG + + if (subcommand.equals(F("rot"))) { // touch,rot,<0..3> : Set rotation to 0, 90, 180, 270 degrees + uint8_t rot_ = static_cast(event->Par2 % 4); + + P123_data->setRotation(rot_); + success = true; + } else if (subcommand.equals(F("flip"))) { // touch,flip,<0|1> : Flip rotation by 0 or 180 degrees + bool flip_ = (event->Par2 > 0); + + P123_data->setRotationFlipped(flip_); + success = true; + } else if (subcommand.equals(F("enable"))) { // touch,enable, : Enables a disabled objectname + arguments = parseString(string, 3); + success = P123_data->setTouchObjectState(arguments, true); + } else if (subcommand.equals(F("disable"))) { // touch,disable, : Disables an enabled objectname + arguments = parseString(string, 3); + success = P123_data->setTouchObjectState(arguments, false); + } + } + } + break; + } + + case PLUGIN_TEN_PER_SECOND: // Should be often/fast enough, as this is user-interaction driven + { + P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr == P123_data) { + return success; + } + + success = P123_data->plugin_ten_per_second(event); + + break; + } + } // switch(function) + return success; +} // Plugin_123 + +#endif // USES_P123 diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index d53ebe6932..14b8564089 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -1283,6 +1283,9 @@ To create/register a plugin, you have to : #ifndef USES_P116 #define USES_P116 // ST77xx #endif + #ifndef USES_P123 + #define USES_P123 // FT6206 + #endif #endif @@ -1498,7 +1501,7 @@ To create/register a plugin, you have to : // #define USES_P122 // #endif #ifndef USES_P123 -// #define USES_P123 // + #define USES_P123 // FT6206 #endif #ifndef USES_P124 #define USES_P124 // diff --git a/src/src/Globals/Plugins.cpp b/src/src/Globals/Plugins.cpp index 7a617588bf..81b2455d86 100644 --- a/src/src/Globals/Plugins.cpp +++ b/src/src/Globals/Plugins.cpp @@ -147,18 +147,40 @@ String getPluginNameFromPluginID(pluginID_t pluginID) { #if USE_I2C_DEVICE_SCAN bool checkPluginI2CAddressFromDeviceIndex(deviceIndex_t deviceIndex, uint8_t i2cAddress) { - bool hasI2CAddress = false; - if (validDeviceIndex(deviceIndex)) { String dummy; struct EventStruct TempEvent; TempEvent.Par1 = i2cAddress; - hasI2CAddress = Plugin_ptr[deviceIndex](PLUGIN_I2C_HAS_ADDRESS, &TempEvent, dummy); + return Plugin_ptr[deviceIndex](PLUGIN_I2C_HAS_ADDRESS, &TempEvent, dummy); } - return hasI2CAddress; + return false; } #endif // if USE_I2C_DEVICE_SCAN +bool getPluginDisplayParametersFromTaskIndex(taskIndex_t taskIndex, uint16_t& x, uint16_t& y, uint16_t& r, uint16_t& colorDepth) { + if (!validTaskIndex(taskIndex)) { return false; } + const deviceIndex_t deviceIndex = getDeviceIndex_from_TaskIndex(taskIndex); + + if (validDeviceIndex(deviceIndex)) { + const pluginID_t pluginID = DeviceIndex_to_Plugin_id[deviceIndex]; + + if (validPluginID(pluginID)) { + String dummy; + struct EventStruct TempEvent; + TempEvent.setTaskIndex(taskIndex); + + if (Plugin_ptr[deviceIndex](PLUGIN_GET_DISPLAY_PARAMETERS, &TempEvent, dummy)) { + x = TempEvent.Par1; + y = TempEvent.Par2; + r = TempEvent.Par3; + colorDepth = TempEvent.Par4; + return true; + } + } + } + return false; +} + // ******************************************************************************** // Device Sort routine, actual sorting alfabetically by plugin name. // Sorting does happen case sensitive. diff --git a/src/src/Globals/Plugins.h b/src/src/Globals/Plugins.h index 05abeca1ff..fccedddd87 100644 --- a/src/src/Globals/Plugins.h +++ b/src/src/Globals/Plugins.h @@ -90,6 +90,11 @@ String getPluginNameFromDeviceIndex(deviceIndex_t deviceIndex); #if USE_I2C_DEVICE_SCAN bool checkPluginI2CAddressFromDeviceIndex(deviceIndex_t deviceIndex, uint8_t i2cAddress); #endif // if USE_I2C_DEVICE_SCAN +bool getPluginDisplayParametersFromTaskIndex(taskIndex_t taskIndex, + uint16_t & x, + uint16_t & y, + uint16_t & r, + uint16_t & colorDepth); String getPluginNameFromPluginID(pluginID_t pluginID); void sortDeviceIndexArray(); diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp new file mode 100644 index 0000000000..85eda3882a --- /dev/null +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -0,0 +1,1160 @@ +#include "../PluginStructs/P123_data_struct.h" + +#ifdef USES_P123 + +# include "../ESPEasyCore/ESPEasyNetwork.h" + +# include "../Helpers/ESPEasy_Storage.h" +# include "../Helpers/Scheduler.h" +# include "../Helpers/StringConverter.h" +# include "../Helpers/SystemVariables.h" + +# include "../Commands/InternalCommands.h" + +/**************************************************************************** + * toString: Display-value for the button selected + ***************************************************************************/ +# ifdef P123_USE_EXTENDED_TOUCH +const __FlashStringHelper* toString(Button_type_e button) { + switch (button) { + case Button_type_e::None: return F("None"); + case Button_type_e::Square: return F("Square"); + case Button_type_e::Rounded: return F("Rounded"); + case Button_type_e::Circle: return F("Circle"); + case Button_type_e::Button_MAX: break; + } + return F("Unsupported!"); +} + +# endif // ifdef P123_USE_EXTENDED_TOUCH + +/** + * Constructor + */ +P123_data_struct::P123_data_struct() : touchscreen(nullptr) {} + +/** + * Destructor + */ +P123_data_struct::~P123_data_struct() { + reset(); +} + +/** + * Proper reset and cleanup. + */ +void P123_data_struct::reset() { + # ifdef PLUGIN_123_DEBUG + addLog(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen reset.")); + # endif // PLUGIN_123_DEBUG + + if (isInitialized()) { + delete touchscreen; + touchscreen = nullptr; + } +} + +/** + * Initialize data and set up the touchscreen. + */ +bool P123_data_struct::init(const EventStruct *event, + uint8_t rotation, + uint16_t ts_x_res, + uint16_t ts_y_res, + uint16_t displayTask, + AdaGFXColorDepth colorDepth) { + _rotation = rotation; + _ts_x_res = ts_x_res; + _ts_y_res = ts_y_res; + _displayTask = displayTask; + _colorDepth = colorDepth; + + reset(); + + touchscreen = new (std::nothrow) Adafruit_FT6206(); + + if (isInitialized()) { + loadTouchObjects(event); + + touchscreen->begin(P123_Settings.treshold); + + # ifdef PLUGIN_123_DEBUG + addLog(LOG_LEVEL_INFO, F("P123 DEBUG Plugin & touchscreen initialized.")); + } else { + addLog(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen initialisation FAILED.")); + # endif // PLUGIN_123_DEBUG + } + return isInitialized(); +} + +/** + * Properly initialized? then true + */ +bool P123_data_struct::isInitialized() const { + return touchscreen != nullptr; +} + +int P123_data_struct::parseStringToInt(const String& string, uint8_t indexFind, char separator, int defaultValue) { + String parsed = parseStringKeepCase(string, indexFind, separator); + + // if (parsed.isEmpty()) { + // return defaultValue; + // } + int result = defaultValue; + + validIntFromString(parsed, result); + + return result; +} + +/** + * Load the settings onto the webpage + */ +bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { + addFormSubHeader(F("Touch configuration")); + + addFormCheckBox(F("Flip rotation 180°"), F("p123_rotation_flipped"), bitRead(P123_Settings.flags, P123_FLAGS_ROTATION_FLIPPED)); + addFormNote(F("Some touchscreens are mounted 180° rotated on the display.")); + + addFormNumericBox(F("Touch minimum pressure"), F("p123_treshold"), + P123_Settings.treshold, 0, 255); + + uint8_t choice3 = 0u; + + bitWrite(choice3, P123_FLAGS_SEND_XY, bitRead(P123_Settings.flags, P123_FLAGS_SEND_XY)); + bitWrite(choice3, P123_FLAGS_SEND_Z, bitRead(P123_Settings.flags, P123_FLAGS_SEND_Z)); + bitWrite(choice3, P123_FLAGS_SEND_OBJECTNAME, bitRead(P123_Settings.flags, P123_FLAGS_SEND_OBJECTNAME)); + { + # define P123_EVENTS_OPTIONS 6 + const __FlashStringHelper *options3[P123_EVENTS_OPTIONS] = + { F("None"), + F("X and Y"), + F("X, Y and Z"), + F("Objectnames only"), + F("Objectnames, X and Y"), + F("Objectnames, X, Y and Z") + }; + int optionValues3[P123_EVENTS_OPTIONS] = { 0, 1, 3, 4, 5, 7 }; // Already used as a bitmap! + addFormSelector(F("Events"), F("p123_events"), P123_EVENTS_OPTIONS, options3, optionValues3, choice3); + } + + addFormCheckBox(F("Prevent duplicate events"), F("p123_deduplicate"), bitRead(P123_Settings.flags, P123_FLAGS_DEDUPLICATE)); + + if (!Settings.UseRules) { + addFormNote(F("Tools / Advanced / Rules must be enabled for events to be fired.")); + } + + addFormSubHeader(F("Calibration")); + + { + const __FlashStringHelper *noYesOptions[2] = { F("No"), F("Yes") }; + int noYesOptionValues[2] = { 0, 1 }; + addFormSelector(F("Calibrate to screen resolution"), + F("p123_use_calibration"), + 2, + noYesOptions, + noYesOptionValues, + P123_Settings.calibrationEnabled ? 1 : 0, + true); + } + + if (P123_Settings.calibrationEnabled) { + addRowLabel(F("Calibration")); + html_table(EMPTY_STRING, false); // Sub-table + html_table_header(F("")); + html_table_header(F("x")); + html_table_header(F("y")); + html_table_header(F("")); + html_table_header(F("x")); + html_table_header(F("y")); + + html_TR_TD(); + addHtml(F("Top-left")); + html_TD(); + addNumericBox(F("p123_cal_tl_x"), + P123_Settings.top_left.x, + 0, + 65535); + html_TD(); + addNumericBox(F("p123_cal_tl_y"), + P123_Settings.top_left.y, + 0, + 65535); + html_TD(); + addHtml(F("Bottom-right")); + html_TD(); + addNumericBox(F("p123_cal_br_x"), + P123_Settings.bottom_right.x, + 0, + 65535); + html_TD(); + addNumericBox(F("p123_cal_br_y"), + P123_Settings.bottom_right.y, + 0, + 65535); + + html_end_table(); + + // addFormNote(F("At least 1 x/y value must be <> 0 to enable calibration.")); + } + + addFormCheckBox(F("Enable logging for calibration"), F("p123_log_calibration"), + P123_Settings.logEnabled); + + addFormSubHeader(F("Touch objects")); + + { + addRowLabel(F("Object")); + # ifdef P123_USE_EXTENDED_TOUCH + html_table(F("multirow"), false); // Sub-table + # else // ifdef P123_USE_EXTENDED_TOUCH + html_table(EMPTY_STRING, false); // Sub-table + # endif // ifdef P123_USE_EXTENDED_TOUCH + html_table_header(F(" # ")); + html_table_header(F("On")); + html_table_header(F("Objectname")); + html_table_header(F("Top-left x")); + html_table_header(F("Top-left y")); + html_table_header(F("On/Off button")); + # ifdef P123_USE_EXTENDED_TOUCH + html_table_header(F("ON color")); + html_table_header(F("ON caption")); + html_table_header(F("Buttontype")); + html_table_header(F("Background")); + # endif // ifdef P123_USE_EXTENDED_TOUCH + html_TR(); // New row + html_table_header(EMPTY_STRING); + html_table_header(EMPTY_STRING); + html_table_header(EMPTY_STRING); + html_table_header(F("Width")); + html_table_header(F("Height")); + html_table_header(F("Inverted")); + # ifdef P123_USE_EXTENDED_TOUCH + html_table_header(F("OFF color")); + html_table_header(F("OFF caption")); + html_table_header(F("Caption color")); + html_table_header(F("Highlight color")); + # endif // ifdef P123_USE_EXTENDED_TOUCH + + # ifdef P123_USE_EXTENDED_TOUCH + const __FlashStringHelper *buttonTypeOptions[] = { + toString(Button_type_e::None), + toString(Button_type_e::Square), + toString(Button_type_e::Rounded), + toString(Button_type_e::Circle) + }; + const int buttonTypeValues[] = { + static_cast(Button_type_e::None), + static_cast(Button_type_e::Square), + static_cast(Button_type_e::Rounded), + static_cast(Button_type_e::Circle) + }; + # endif // ifdef P123_USE_EXTENDED_TOUCH + + uint8_t maxIdx = std::min(static_cast(TouchObjects.size() + P123_EXTRA_OBJECT_COUNT), P123_MAX_OBJECT_COUNT); + String parsed; + TouchObjects.resize(maxIdx, tP123_TouchObjects()); + + # ifdef P123_USE_EXTENDED_TOUCH + AdaGFXHtmlColorDepthDataList(F("adagfx65kcolors"), static_cast(P123_COLOR_DEPTH)); + # endif // ifdef P123_USE_EXTENDED_TOUCH + + for (int objectNr = 0; objectNr < maxIdx; objectNr++) { + html_TR_TD(); + addHtml(F(" ")); + addHtmlInt(objectNr + 1); // Arrayindex to objectindex + + html_TD(); + + // Enable new entries + bool enabled = bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED) || TouchObjects[objectNr].objectName.isEmpty(); + addCheckBox(getPluginCustomArgName(objectNr + 0), + enabled, false); + html_TD(); // Name + addTextBox(getPluginCustomArgName(objectNr + 100), + TouchObjects[objectNr].objectName, + P123_MaxObjectNameLength - 1, + false, false, EMPTY_STRING, EMPTY_STRING); + html_TD(); // top-x + addNumericBox(getPluginCustomArgName(objectNr + 200), + TouchObjects[objectNr].top_left.x, 0, 65535 + # ifdef P123_USE_TOOLTIPS + , F("widenumber"), F("Top-left x") + # endif // ifdef P123_USE_TOOLTIPS + ); + html_TD(); // top-y + addNumericBox(getPluginCustomArgName(objectNr + 300), + TouchObjects[objectNr].top_left.y, 0, 65535 + # ifdef P123_USE_TOOLTIPS + , F("widenumber"), F("Top-left y") + # endif // ifdef P123_USE_TOOLTIPS + ); + html_TD(); // on/off button + addCheckBox(getPluginCustomArgName(objectNr + 600), + bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTON), false + # ifdef P123_USE_TOOLTIPS + , F("On/Off button") + # endif // ifdef P123_USE_TOOLTIPS + ); + # ifdef P123_USE_EXTENDED_TOUCH + html_TD(); // ON color + parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorOn, _colorDepth, true); + addTextBox(getPluginCustomArgName(objectNr + 1000), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") // |list=\"adagfx65kcolors\" + # ifdef P123_USE_TOOLTIPS + , F("ON color") + # endif // ifdef P123_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // ON Caption + addTextBox(getPluginCustomArgName(objectNr + 1300), + TouchObjects[objectNr].captionOn, + P123_MaxObjectNameLength - 1, + false, + false, + EMPTY_STRING, + F("wide") + # ifdef P123_USE_TOOLTIPS + , F("ON caption") + # endif // ifdef P123_USE_TOOLTIPS + ); + html_TD(); // button-type + addSelector(getPluginCustomArgName(objectNr + 800), + static_cast(Button_type_e::Button_MAX), + buttonTypeOptions, + buttonTypeValues, + nullptr, + get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTONTYPE), false, true, F("widenumber") + # ifdef P123_USE_TOOLTIPS + , F("Buttontype") + # endif // ifdef P123_USE_TOOLTIPS + ); + html_TD(); // Background color + parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorBackground, _colorDepth, true); + addTextBox(getPluginCustomArgName(objectNr + 1700), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef P123_USE_TOOLTIPS + , F("Background color") + # endif // ifdef P123_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + # endif // ifdef P123_USE_EXTENDED_TOUCH + + html_TR_TD(); // Start new row + + html_TD(3); // Start with some blank columns + // html_TD(); + // Width + addNumericBox(getPluginCustomArgName(objectNr + 400), + TouchObjects[objectNr].width_height.x, 0, 65535 + # ifdef P123_USE_TOOLTIPS + , F("widenumber"), F("Width") + # endif // ifdef P123_USE_TOOLTIPS + ); + html_TD(); // Height + addNumericBox(getPluginCustomArgName(objectNr + 500), + TouchObjects[objectNr].width_height.y, 0, 65535 + # ifdef P123_USE_TOOLTIPS + , F("widenumber"), F("Height") + # endif // ifdef P123_USE_TOOLTIPS + ); + + # ifdef P123_USE_EXTENDED_TOUCH + + // Colored + // addCheckBox(getPluginCustomArgName(objectNr + 900), + // bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_COLORED), false + // # ifdef P123_USE_TOOLTIPS + // , F("Colored") + // # endif // ifdef P123_USE_TOOLTIPS + // ); + // html_TD(); // Use caption + // addCheckBox(getPluginCustomArgName(objectNr + 1200), + // bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_CAPTION), false + // # ifdef P123_USE_TOOLTIPS + // , F("Use Caption") + // # endif // ifdef P123_USE_TOOLTIPS + // ); + # endif // ifdef P123_USE_EXTENDED_TOUCH + html_TD(); // inverted + addCheckBox(getPluginCustomArgName(objectNr + 700), + bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_INVERTED), false + # ifdef P123_USE_TOOLTIPS + , F("Inverted") + # endif // ifdef P123_USE_TOOLTIPS + ); + # ifdef P123_USE_EXTENDED_TOUCH + html_TD(); // OFF color + parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorOff, _colorDepth, true); + addTextBox(getPluginCustomArgName(objectNr + 1100), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef P123_USE_TOOLTIPS + , F("OFF color") + # endif // ifdef P123_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // OFF Caption + addTextBox(getPluginCustomArgName(objectNr + 1400), + TouchObjects[objectNr].captionOff, + P123_MaxObjectNameLength - 1, + false, + false, + EMPTY_STRING, + F("wide") + # ifdef P123_USE_TOOLTIPS + , F("OFF caption") + # endif // ifdef P123_USE_TOOLTIPS + ); + html_TD(); // Caption color + parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorCaption, _colorDepth, true); + addTextBox(getPluginCustomArgName(objectNr + 1500), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef P123_USE_TOOLTIPS + , F("Caption color") + # endif // ifdef P123_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // Highlight color + parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorHighlight, _colorDepth, true); + addTextBox(getPluginCustomArgName(objectNr + 1600), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef P123_USE_TOOLTIPS + , F("Highlight color") + # endif // ifdef P123_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + # endif // ifdef P123_USE_EXTENDED_TOUCH + } + html_end_table(); + + addFormNumericBox(F("Debounce delay for On/Off buttons"), F("p123_debounce"), + P123_Settings.debounceMs, 0, 255); + addUnit(F("0-255 msec.")); + } + return false; +} + +/** + * Save the settings from the web page to flash + */ +bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { + String config; + + config.reserve(80); + + uint32_t lSettings = 0u; + + bitWrite(lSettings, P123_FLAGS_SEND_XY, bitRead(getFormItemInt(F("p123_events")), P123_FLAGS_SEND_XY)); + bitWrite(lSettings, P123_FLAGS_SEND_Z, bitRead(getFormItemInt(F("p123_events")), P123_FLAGS_SEND_Z)); + bitWrite(lSettings, P123_FLAGS_SEND_OBJECTNAME, bitRead(getFormItemInt(F("p123_events")), P123_FLAGS_SEND_OBJECTNAME)); + bitWrite(lSettings, P123_FLAGS_ROTATION_FLIPPED, isFormItemChecked(F("p123_rotation_flipped"))); + bitWrite(lSettings, P123_FLAGS_DEDUPLICATE, isFormItemChecked(F("p123_deduplicate"))); + + config += getFormItemInt(F("p123_use_calibration")); + config += P123_SETTINGS_SEPARATOR; + config += isFormItemChecked(F("p123_log_calibration")) ? 1 : 0; + config += P123_SETTINGS_SEPARATOR; + config += getFormItemInt(F("p123_cal_tl_x")); + config += P123_SETTINGS_SEPARATOR; + config += getFormItemInt(F("p123_cal_tl_y")); + config += P123_SETTINGS_SEPARATOR; + config += getFormItemInt(F("p123_cal_br_x")); + config += P123_SETTINGS_SEPARATOR; + config += getFormItemInt(F("p123_cal_br_y")); + config += P123_SETTINGS_SEPARATOR; + config += getFormItemInt(F("p123_treshold")); + config += P123_SETTINGS_SEPARATOR; + config += getFormItemInt(F("p123_debounce")); + config += P123_SETTINGS_SEPARATOR; + config += lSettings; + config += P123_SETTINGS_SEPARATOR; + + settingsArray[P123_CALIBRATION_START] = config; + { + String log = F("Save settings: "); + config.replace(P123_SETTINGS_SEPARATOR, ','); + log += config; + addLog(LOG_LEVEL_INFO, log); + } + + String error; + + for (int objectNr = 0; objectNr < P123_MAX_OBJECT_COUNT; objectNr++) { + config.clear(); + config += webArg(getPluginCustomArgName(objectNr + 100)); // Name + + if (!config.isEmpty()) { // Empty name => skip entry + if (!ExtraTaskSettings.checkInvalidCharInNames(config.c_str())) { // Check for invalid characters in objectname + error += F("Invalid character in objectname #"); + error += objectNr; + error += F(". Do not use ',-+/*=^%!#[]{}()' or space.\n"); + } + config += P123_SETTINGS_SEPARATOR; + uint32_t flags = 0u; + bitWrite(flags, P123_OBJECT_FLAG_ENABLED, isFormItemChecked(getPluginCustomArgName(objectNr + 0))); // Enabled + bitWrite(flags, P123_OBJECT_FLAG_BUTTON, isFormItemChecked(getPluginCustomArgName(objectNr + 600))); // On/Off button + bitWrite(flags, P123_OBJECT_FLAG_INVERTED, isFormItemChecked(getPluginCustomArgName(objectNr + 700))); // Inverted + # ifdef P123_USE_EXTENDED_TOUCH + + // bitWrite(flags, P123_OBJECT_FLAG_COLORED, isFormItemChecked(getPluginCustomArgName(objectNr + 900))); // Colored + // bitWrite(flags, P123_OBJECT_FLAG_CAPTION, isFormItemChecked(getPluginCustomArgName(objectNr + 1200))); // Use caption + set8BitToUL(flags, P123_OBJECT_FLAG_BUTTONTYPE, getFormItemInt(getPluginCustomArgName(objectNr + 800))); // Buttontype + # endif // ifdef P123_USE_EXTENDED_TOUCH + config += flags; // Flags + config += P123_SETTINGS_SEPARATOR; + config += getFormItemInt(getPluginCustomArgName(objectNr + 200)); // Top x + config += P123_SETTINGS_SEPARATOR; + config += getFormItemInt(getPluginCustomArgName(objectNr + 300)); // Top y + config += P123_SETTINGS_SEPARATOR; + config += getFormItemInt(getPluginCustomArgName(objectNr + 400)); // Bottom x + config += P123_SETTINGS_SEPARATOR; + config += getFormItemInt(getPluginCustomArgName(objectNr + 500)); // Bottom y + config += P123_SETTINGS_SEPARATOR; + + # ifdef P123_USE_EXTENDED_TOUCH + String colorInput; + colorInput = webArg(getPluginCustomArgName(objectNr + 1000)); // Color ON + config += AdaGFXparseColor(colorInput, _colorDepth, true); + config += P123_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(objectNr + 1100)); // Color OFF + config += AdaGFXparseColor(colorInput, _colorDepth, true); + config += P123_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(objectNr + 1500)); // Color caption + config += AdaGFXparseColor(colorInput, _colorDepth, true); + config += P123_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(objectNr + 1600)); // Color Highlight + config += AdaGFXparseColor(colorInput, _colorDepth, true); + config += P123_SETTINGS_SEPARATOR; + config += enquoteString(webArg(getPluginCustomArgName(objectNr + 1300))); // Caption ON + config += P123_SETTINGS_SEPARATOR; + config += enquoteString(webArg(getPluginCustomArgName(objectNr + 1400))); // Caption OFF + config += P123_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(objectNr + 1700)); // Color Background + config += AdaGFXparseColor(colorInput, _colorDepth, true); + config += P123_SETTINGS_SEPARATOR; + # endif // ifdef P123_USE_EXTENDED_TOUCH + } + config.trim(); + + while (!config.isEmpty() && config[config.length() - 1] == P123_SETTINGS_SEPARATOR) { + config.remove(config.length() - 1); + } + + settingsArray[objectNr + P123_OBJECT_INDEX_START] = config; + + # ifdef PLUGIN_123_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("Save object #"); + log += objectNr; + log += F(" settings: "); + config.replace(P123_SETTINGS_SEPARATOR, ','); + log += config; + addLog(LOG_LEVEL_INFO, log); + } + # endif // ifdef PLUGIN_123_DEBUG + } + + if (error.length() > 0) { + addHtmlError(error); + } + + error = SaveCustomTaskSettings(event->TaskIndex, settingsArray, P123_ARRAY_SIZE, 0); + + if (!error.isEmpty()) { + addHtmlError(error); + return false; + } + return true; +} + +/** + * Every 10th second we check if the screen is touched + */ +bool P123_data_struct::plugin_ten_per_second(struct EventStruct *event) { + bool success = false; + + if (isInitialized()) { + if (touched()) { + int16_t x = 0, y = 0, ox = 0, oy = 0, rx, ry; + int16_t z = 0; + readData(x, y, z, ox, oy); + + rx = x; + ry = y; + scaleRawToCalibrated(x, y); // Map to screen coordinates if so configured + + // Avoid event-storms by deduplicating coordinates + if (!_deduplicate || + (_deduplicate && ((P123_VALUE_X != x) || (P123_VALUE_Y != y) || (P123_VALUE_Z != z)))) { + success = true; + P123_VALUE_X = x; + P123_VALUE_Y = y; + P123_VALUE_Z = z; + } + + if (success && + P123_Settings.logEnabled && + loglevelActiveFor(LOG_LEVEL_INFO)) { // REQUIRED for calibration and setting up objects, so do not make this optional! + String log; + log.reserve(72); + log = F("Touch calibration rx= "); // Space before the logged values was added for readability + log += rx; + log += F(", ry= "); + log += ry; + log += F("; z= "); // Always log the z value even if not used. + log += z; + log += F(", x= "); + log += x; + log += F(", y= "); + log += y; + + log += F("; ox= "); + log += ox; + log += F(", oy= "); + log += oy; + addLog(LOG_LEVEL_INFO, log); + } + + if (Settings.UseRules) { // No events to handle if rules not + // enabled + if (success && bitRead(P123_Settings.flags, P123_FLAGS_SEND_XY)) { // Send events for each touch + const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex); + + if (!bitRead(P123_Settings.flags, P123_FLAGS_SEND_Z) && validDeviceIndex(DeviceIndex)) { // Do NOT send a Z event for each + // touch? + Device[DeviceIndex].VType = Sensor_VType::SENSOR_TYPE_DUAL; + Device[DeviceIndex].ValueCount = 2; + } + sendData(event); // Send X/Y(/Z) event + + if (!bitRead(P123_Settings.flags, P123_FLAGS_SEND_Z) && validDeviceIndex(DeviceIndex)) { // Reset device configuration + Device[DeviceIndex].VType = Sensor_VType::SENSOR_TYPE_TRIPLE; + Device[DeviceIndex].ValueCount = 3; + } + } + + if (bitRead(P123_Settings.flags, P123_FLAGS_SEND_OBJECTNAME)) { // Send events for objectname if within reach + String selectedObjectName; + int8_t selectedObjectIndex = -1; + + if (isValidAndTouchedTouchObject(x, y, selectedObjectName, selectedObjectIndex)) { + if ((selectedObjectIndex > -1) && bitRead(TouchObjects[selectedObjectIndex].flags, P123_OBJECT_FLAG_BUTTON)) { + if ((TouchObjects[selectedObjectIndex].TouchTimers == 0) || + + // Not touched yet or too long ago + (TouchObjects[selectedObjectIndex].TouchTimers < (millis() - (2 * P123_Settings.debounceMs)))) { + // From now wait the debounce time + TouchObjects[selectedObjectIndex].TouchTimers = millis() + P123_Settings.debounceMs; + } else { + // Debouncing time elapsed? + if (TouchObjects[selectedObjectIndex].TouchTimers <= millis()) { + TouchObjects[selectedObjectIndex].TouchStates = !TouchObjects[selectedObjectIndex].TouchStates; + TouchObjects[selectedObjectIndex].TouchTimers = 0; + String eventCommand; + eventCommand.reserve(48); + eventCommand = getTaskDeviceName(event->TaskIndex); + eventCommand += '#'; + eventCommand += selectedObjectName; + eventCommand += '='; // Add arguments + + if (bitRead(TouchObjects[selectedObjectIndex].flags, P123_OBJECT_FLAG_INVERTED)) { + eventCommand += TouchObjects[selectedObjectIndex].TouchStates ? '0' : '1'; // Act like an inverted button, 0 = On, + // 1 = Off + } else { + eventCommand += TouchObjects[selectedObjectIndex].TouchStates ? '1' : '0'; // Act like a button, 1 = On, 0 = Off + } + eventQueue.add(eventCommand); + } + } + } else { + // Matching object is found, send # event with x, y and z as %eventvalue1/2/3% + String eventCommand; + eventCommand.reserve(48); + eventCommand = getTaskDeviceName(event->TaskIndex); + eventCommand += '#'; + eventCommand += selectedObjectName; + eventCommand += '='; // Add arguments + eventCommand += x; + eventCommand += ','; + eventCommand += y; + eventCommand += ','; + eventCommand += z; + eventQueue.add(eventCommand); + } + } + } + } + } + } + return success; +} + +/** + * draw a button using the mode and state + * TODO: Complete implementation + * will probably need: + * - Access to the AdafruitGFX_Helper object + * - Access to the Display object in the AdafruitGFX_Helper + * - Access to the list of available fonts, to be able to change the font for button captions, and to center the caption on the button + */ +# ifdef P123_USE_EXTENDED_TOUCH +void P123_data_struct::drawButton(DrawButtonMode_e buttonMode, + int8_t buttonIndex, + const EventStruct *event) { + if ((buttonIndex < 0) || (buttonIndex >= static_cast(TouchObjects.size()))) { return; } // Selfprotection + + if (!Settings.TaskDeviceEnabled[_displayTask]) { return; } // No active DisplayTask is no drawing buttons + + Button_type_e bType = static_cast(get8BitFromUL(TouchObjects[buttonIndex].flags, P123_OBJECT_FLAG_BUTTONTYPE)); + String cmdPrefix; + String btnDrawShape; + int8_t xa = 0, ya = 0, wa = 0, ha = 0; + + cmdPrefix.reserve(30); + cmdPrefix = getTaskDeviceName(_displayTask); + cmdPrefix += '.'; // a period + cmdPrefix += ADAGFX_UNIVERSAL_TRIGGER; + cmdPrefix += ','; + + btnDrawShape.reserve(50); + + if (bType != Button_type_e::None) { + switch (buttonMode) { + case DrawButtonMode_e::Initialize: + break; + case DrawButtonMode_e::State: + { + xa = 1; ya = 1; + wa = -2; ha = -2; + break; + } + case DrawButtonMode_e::Highlight: + break; + } + } + + switch (bType) { + case Button_type_e::None: + break; + case Button_type_e::Square: + btnDrawShape = F("r"); + break; + case Button_type_e::Rounded: + btnDrawShape = F("rr"); + break; + case Button_type_e::Circle: + btnDrawShape = F("c"); + break; + case Button_type_e::Button_MAX: + break; + } + + if ((bType != Button_type_e::None) && + (buttonMode == DrawButtonMode_e::Initialize)) { btnDrawShape += 'f'; } + + switch (bType) { + case Button_type_e::None: + break; + case Button_type_e::Square: + btnDrawShape += ','; + btnDrawShape += TouchObjects[buttonIndex].top_left.x + xa; + btnDrawShape += ','; + btnDrawShape += TouchObjects[buttonIndex].top_left.y + ya; + btnDrawShape += ','; + btnDrawShape += TouchObjects[buttonIndex].width_height.x + wa; + btnDrawShape += ','; + btnDrawShape += TouchObjects[buttonIndex].width_height.y + ha; + btnDrawShape += ','; + + if (buttonMode == DrawButtonMode_e::Initialize) { + btnDrawShape += AdaGFXcolorToString(TouchObjects[buttonIndex].colorCaption, _colorDepth); + } else { + btnDrawShape += AdaGFXcolorToString(TouchObjects[buttonIndex].colorBackground, _colorDepth); + } + + if (buttonMode == DrawButtonMode_e::Initialize) { + btnDrawShape += ','; + btnDrawShape += AdaGFXcolorToString(TouchObjects[buttonIndex].colorBackground, _colorDepth); + } + break; + case Button_type_e::Rounded: + break; + case Button_type_e::Circle: + break; + case Button_type_e::Button_MAX: + break; + } + + if (bType != Button_type_e::None) { + String btnDrawCmd; + btnDrawCmd.reserve(80); + btnDrawCmd = cmdPrefix; + btnDrawCmd += btnDrawShape; + ExecuteCommand_all(EventValueSource::Enum::VALUE_SOURCE_RULES, btnDrawCmd.c_str()); + } +} + +# endif // ifdef P123_USE_EXTENDED_TOUCH + +/** + * Load the touch objects from the settings, and initialize then properly where needed. + */ +void P123_data_struct::loadTouchObjects(const EventStruct *event) { + # ifdef PLUGIN_123_DEBUG + addLog(LOG_LEVEL_INFO, F("P123 DEBUG loadTouchObjects")); + # endif // PLUGIN_123_DEBUG + LoadCustomTaskSettings(event->TaskIndex, settingsArray, P123_ARRAY_SIZE, 0); + + lastObjectIndex = P123_OBJECT_INDEX_START - 1; // START must be > 0!!! + + objectCount = 0; + + for (uint8_t i = P123_OBJECT_INDEX_END; i >= P123_OBJECT_INDEX_START; i--) { + if (!settingsArray[i].isEmpty() && (lastObjectIndex < P123_OBJECT_INDEX_START)) { + lastObjectIndex = i; + objectCount++; // Count actual number of objects + } + } + + // Get calibration and common settings + P123_Settings.calibrationEnabled = parseStringToInt(settingsArray[P123_CALIBRATION_START], + P123_CALIBRATION_ENABLED, P123_SETTINGS_SEPARATOR) == 1; + P123_Settings.logEnabled = parseStringToInt(settingsArray[P123_CALIBRATION_START], + P123_CALIBRATION_LOG_ENABLED, P123_SETTINGS_SEPARATOR) == 1; + int lSettings = 0; + + bitWrite(lSettings, P123_FLAGS_SEND_XY, P123_TS_SEND_XY); + bitWrite(lSettings, P123_FLAGS_SEND_Z, P123_TS_SEND_Z); + bitWrite(lSettings, P123_FLAGS_SEND_OBJECTNAME, P123_TS_SEND_OBJECTNAME); + P123_Settings.flags = parseStringToInt(settingsArray[P123_CALIBRATION_START], + P123_COMMON_FLAGS, P123_SETTINGS_SEPARATOR, lSettings); + P123_Settings.top_left.x = parseStringToInt(settingsArray[P123_CALIBRATION_START], P123_CALIBRATION_TOP_X, P123_SETTINGS_SEPARATOR); + P123_Settings.top_left.y = parseStringToInt(settingsArray[P123_CALIBRATION_START], P123_CALIBRATION_TOP_Y, P123_SETTINGS_SEPARATOR); + P123_Settings.bottom_right.x = parseStringToInt(settingsArray[P123_CALIBRATION_START], P123_CALIBRATION_BOTTOM_X, P123_SETTINGS_SEPARATOR); + P123_Settings.bottom_right.y = parseStringToInt(settingsArray[P123_CALIBRATION_START], P123_CALIBRATION_BOTTOM_Y, P123_SETTINGS_SEPARATOR); + P123_Settings.debounceMs = parseStringToInt(settingsArray[P123_CALIBRATION_START], P123_COMMON_DEBOUNCE_MS, P123_SETTINGS_SEPARATOR, + P123_DEBOUNCE_MILLIS); + P123_Settings.treshold = parseStringToInt(settingsArray[P123_CALIBRATION_START], P123_COMMON_TOUCH_TRESHOLD, P123_SETTINGS_SEPARATOR, + P123_TS_TRESHOLD); + + settingsArray[P123_CALIBRATION_START].clear(); // Free a little memory + + // Buffer some settings, mostly for readability, but also to be able to set from write command + _flipped = bitRead(P123_Settings.flags, P123_FLAGS_ROTATION_FLIPPED); + _deduplicate = bitRead(P123_Settings.flags, P123_FLAGS_DEDUPLICATE); + + TouchObjects.clear(); + + if (objectCount > 0) { + TouchObjects.reserve(objectCount); + uint8_t t = 0u; + + for (uint8_t i = P123_OBJECT_INDEX_START; i <= lastObjectIndex; i++) { + if (!settingsArray[i].isEmpty()) { + TouchObjects.push_back(tP123_TouchObjects()); + TouchObjects[t].flags = parseStringToInt(settingsArray[i], P123_OBJECT_FLAGS, P123_SETTINGS_SEPARATOR); + TouchObjects[t].objectName = parseStringKeepCase(settingsArray[i], P123_OBJECT_NAME, P123_SETTINGS_SEPARATOR); + TouchObjects[t].top_left.x = parseStringToInt(settingsArray[i], P123_OBJECT_COORD_TOP_X, P123_SETTINGS_SEPARATOR); + TouchObjects[t].top_left.y = parseStringToInt(settingsArray[i], P123_OBJECT_COORD_TOP_Y, P123_SETTINGS_SEPARATOR); + TouchObjects[t].width_height.x = parseStringToInt(settingsArray[i], P123_OBJECT_COORD_WIDTH, P123_SETTINGS_SEPARATOR); + TouchObjects[t].width_height.y = parseStringToInt(settingsArray[i], P123_OBJECT_COORD_HEIGHT, P123_SETTINGS_SEPARATOR); + # ifdef P123_USE_EXTENDED_TOUCH + TouchObjects[t].colorOn = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_ON, P123_SETTINGS_SEPARATOR); + TouchObjects[t].colorOff = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_OFF, P123_SETTINGS_SEPARATOR); + TouchObjects[t].colorCaption = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_CAPTION, P123_SETTINGS_SEPARATOR); + TouchObjects[t].colorHighlight = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_HIGHLIGHT, P123_SETTINGS_SEPARATOR); + TouchObjects[t].captionOn = parseStringKeepCase(settingsArray[i], P123_OBJECT_CAPTION_ON, P123_SETTINGS_SEPARATOR); + TouchObjects[t].captionOff = parseStringKeepCase(settingsArray[i], P123_OBJECT_CAPTION_OFF, P123_SETTINGS_SEPARATOR); + TouchObjects[t].colorBackground = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_BACKGROUND, P123_SETTINGS_SEPARATOR); + # endif // ifdef P123_USE_EXTENDED_TOUCH + + TouchObjects[t].SurfaceAreas = 0; // Reset runtime stuff + TouchObjects[t].TouchTimers = 0; + TouchObjects[t].TouchStates = false; + + t++; + + settingsArray[i].clear(); // Free a little memory + } + } + } +} + +/** + * Check if the screen is touched. + */ +bool P123_data_struct::touched() { + if (isInitialized()) { + return touchscreen->touched(); + } + return false; +} + +/** + * Read the raw data if the touchscreen is initialized. + */ +void P123_data_struct::readData(int16_t& x, int16_t& y, int16_t& z, int16_t& ox, int16_t& oy) { + if (isInitialized()) { + FT_Point p = touchscreen->getPoint(); + + int16_t _x = p.x; + int16_t _y = p.y; + + // Rotate, as the driver doesn't provide that, use native touch-panel resolution + switch (_rotation) { + case TOUCHOBJECTS_HELPER_ROTATION_90: + + if (_flipped) { + p.x = map(_y, 0, P123_TOUCH_Y_NATIVE, P123_TOUCH_Y_NATIVE, 0); + p.y = _x; + } else { + p.x = _y; + p.y = map(_x, 0, P123_TOUCH_X_NATIVE, P123_TOUCH_X_NATIVE, 0); + } + break; + case TOUCHOBJECTS_HELPER_ROTATION_180: + + if (!_flipped) { // Change only when not flipped + p.x = map(_x, 0, P123_TOUCH_X_NATIVE, P123_TOUCH_X_NATIVE, 0); + p.y = map(_y, 0, P123_TOUCH_Y_NATIVE, P123_TOUCH_Y_NATIVE, 0); + } + break; + case TOUCHOBJECTS_HELPER_ROTATION_270: + + if (_flipped) { + p.x = _y; + p.y = map(_x, 0, P123_TOUCH_X_NATIVE, P123_TOUCH_X_NATIVE, 0); + } else { + p.x = map(_y, 0, P123_TOUCH_Y_NATIVE, P123_TOUCH_Y_NATIVE, 0); + p.y = _x; + } + break; + default: + + if (_flipped) { + p.x = map(p.x, 0, P123_TOUCH_X_NATIVE, P123_TOUCH_X_NATIVE, 0); + p.y = map(p.y, 0, P123_TOUCH_Y_NATIVE, P123_TOUCH_Y_NATIVE, 0); + } + break; + } + + x = p.x; + y = p.y; + z = p.z; + ox = _x; + oy = _y; + } +} + +/** + * Set rotation + */ +void P123_data_struct::setRotation(uint8_t n) { + _rotation = n; + # ifdef PLUGIN_123_DEBUG + String log = F("P123 DEBUG Rotation set: "); + log += n; + addLog(LOG_LEVEL_INFO, log); + # endif // PLUGIN_123_DEBUG +} + +/** + * Set rotationFlipped + */ +void P123_data_struct::setRotationFlipped(bool flipped) { + _flipped = flipped; + # ifdef PLUGIN_123_DEBUG + String log = F("P123 DEBUG RotationFlipped set: "); + log += flipped; + addLog(LOG_LEVEL_INFO, log); + # endif // PLUGIN_123_DEBUG +} + +/** + * Determine if calibration is enabled and usable. + */ +bool P123_data_struct::isCalibrationActive() { + return _useCalibration + && (P123_Settings.top_left.x != 0 + || P123_Settings.top_left.y != 0 + || P123_Settings.bottom_right.x != 0 + || P123_Settings.bottom_right.y != 0); // Enabled and any value != 0 => Active +} + +/** + * Check within the list of defined objects if we touched one of them. + * The smallest matching surface is selected if multiple objects overlap. + * Returns state, and sets selectedObjectName to the best matching object + */ +bool P123_data_struct::isValidAndTouchedTouchObject(int16_t x, + int16_t y, + String& selectedObjectName, + int8_t& selectedObjectIndex) { + uint32_t lastObjectArea = 0u; + bool selected = false; + + for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { + if (!TouchObjects[objectNr].objectName.isEmpty() + && bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED) + && (TouchObjects[objectNr].width_height.x != 0) + && (TouchObjects[objectNr].width_height.y != 0)) { // Not initial could be valid + if (TouchObjects[objectNr].SurfaceAreas == 0) { // Need to calculate the surface area + TouchObjects[objectNr].SurfaceAreas = TouchObjects[objectNr].width_height.x * TouchObjects[objectNr].width_height.y; + } + + if ((TouchObjects[objectNr].top_left.x <= x) + && (TouchObjects[objectNr].top_left.y <= y) + && ((TouchObjects[objectNr].width_height.x + TouchObjects[objectNr].top_left.x) >= x) + && ((TouchObjects[objectNr].width_height.y + TouchObjects[objectNr].top_left.y) >= y) + && ((lastObjectArea == 0) + || (TouchObjects[objectNr].SurfaceAreas < lastObjectArea))) { // Select smallest area that fits the coordinates + selectedObjectName = TouchObjects[objectNr].objectName; + selectedObjectIndex = objectNr; + lastObjectArea = TouchObjects[objectNr].SurfaceAreas; + selected = true; + } + # ifdef PLUGIN_123_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + String log = F("P123 DEBUG Touched: obj: "); + log += TouchObjects[objectNr].objectName; + log += ','; + log += TouchObjects[objectNr].top_left.x; + log += ','; + log += TouchObjects[objectNr].top_left.y; + log += ','; + log += TouchObjects[objectNr].width_height.x; + log += ','; + log += TouchObjects[objectNr].width_height.y; + log += F(" surface:"); + log += TouchObjects[objectNr].SurfaceAreas; + log += F(" x,y:"); + log += x; + log += ','; + log += y; + log += F(" sel:"); + log += selectedObjectName; + log += '/'; + log += selectedObjectIndex; + log += '/'; + log += selected ? 'T' : 'f'; + addLog(LOG_LEVEL_DEBUG, log); + } + # endif // PLUGIN_123_DEBUG + } + } + return selected; +} + +/** + * Set the enabled/disabled state by inserting or deleting an underscore '_' as the first character of the object name. + * Checks if the name doesn't exceed the max. length. + */ +bool P123_data_struct::setTouchObjectState(const String& touchObject, bool state) { + if (touchObject.isEmpty()) { return false; } + String findObject; // = (state ? F("_") : F("")); // When enabling, try to find a disabled object + + findObject += touchObject; + String thisObject; + bool success = false; + + thisObject.reserve(P123_MaxObjectNameLength); + + for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { + if ((!TouchObjects[objectNr].objectName.isEmpty()) + && findObject.equalsIgnoreCase(TouchObjects[objectNr].objectName)) { + // uint32_t objectFlags = parseStringToInt(settingsArray[objectNr], P123_OBJECT_FLAGS, P123_SETTINGS_SEPARATOR); + bool enabled = bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED); + + if (state != enabled) { + bitWrite(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED, state); + success = true; + + // String config; + // config.reserve(settingsArray[objectNr].length() + 1); + // config += objectFlags; + // config += P123_SETTINGS_SEPARATOR; + + // // Rest of the string + // config += parseStringToEndKeepCase(settingsArray[objectNr], P123_OBJECT_NAME, P123_SETTINGS_SEPARATOR); + // settingsArray[objectNr] = config; // Store + } + # ifdef PLUGIN_123_DEBUG + String log = F("P123 setTouchObjectState: obj: "); + log += thisObject; + + if (success) { + log += F(", new state: "); + log += (state ? F("en") : F("dis")); + log += F("abled."); + } else { + log += F("failed!"); + } + addLog(LOG_LEVEL_INFO, log); + # endif // PLUGIN_123_DEBUG + } + } + return success; +} + +/** + * Scale the provided raw coordinates to screen-resolution coordinates if calibration is enabled/configured + */ +void P123_data_struct::scaleRawToCalibrated(int16_t& x, int16_t& y) { + if (isCalibrationActive()) { + int16_t lx = x - P123_Settings.top_left.x; + + if (lx <= 0) { + x = 0; + } else { + if (lx > P123_Settings.bottom_right.x) { + lx = P123_Settings.bottom_right.x; + } + float x_fact = static_cast(P123_Settings.bottom_right.x - P123_Settings.top_left.x) / + static_cast(_ts_x_res); + x = static_cast(round(lx / x_fact)); + } + int16_t ly = y - P123_Settings.top_left.y; + + if (ly <= 0) { + y = 0; + } else { + if (ly > P123_Settings.bottom_right.y) { + ly = P123_Settings.bottom_right.y; + } + float y_fact = (P123_Settings.bottom_right.y - P123_Settings.top_left.y) / _ts_y_res; + y = static_cast(round(ly / y_fact)); + } + } +} + +/****************************************** + * enquoteString wrap in ", ' or ` unless all 3 quote types are used + * TODO: Replace with wrapWithQuotes() once available + *****************************************/ +String P123_data_struct::enquoteString(const String& input) { + char quoteChar = '"'; + + if (input.indexOf(quoteChar) > -1) { + quoteChar = '\''; + + if (input.indexOf(quoteChar) > -1) { + quoteChar = '`'; + + if (input.indexOf(quoteChar) > -1) { + return input; // All types of supported quotes used, return original string + } + } + } + String result; + + result.reserve(input.length() + 2); + result = quoteChar; + result += input; + result += quoteChar; + + return result; +} + +#endif // ifdef USES_P123 diff --git a/src/src/PluginStructs/P123_data_struct.h b/src/src/PluginStructs/P123_data_struct.h new file mode 100644 index 0000000000..59a04d1cb3 --- /dev/null +++ b/src/src/PluginStructs/P123_data_struct.h @@ -0,0 +1,259 @@ +#ifndef PLUGINSTRUCTS_P123_DATA_STRUCT_H +#define PLUGINSTRUCTS_P123_DATA_STRUCT_H + +#include "../../_Plugin_Helper.h" +#include "../../ESPEasy_common.h" + +#ifdef USES_P123 + +# include + +# define PLUGIN_123_DEBUG // Additional debugging information + +# define P123_USE_TOOLTIPS // Enable tooltips in UI + +// # define P123_USE_EXTENDED_TOUCH // Enable extended touch settings + +# ifdef LIMIT_BUILD_SIZE +# ifdef P123_USE_TOOLTIPS +# undef P123_USE_TOOLTIPS +# endif // ifdef P123_USE_TOOLTIPS +# endif // ifdef LIMIT_BUILD_SIZE +# if defined(P1123_USE_TOOLTIPS) && !defined(ENABLE_TOOLTIPS) +# undef P123_USE_TOOLTIPS +# endif // if defined(P1123_USE_TOOLTIPS) && !defined(ENABLE_TOOLTIPS) + +# define P123_FLAGS_SEND_XY 0 // Set in Global Settings flags +# define P123_FLAGS_SEND_Z 1 // Set in Global Settings flags +# define P123_FLAGS_SEND_OBJECTNAME 2 // Set in Global Settings flags +# define P123_FLAGS_USE_CALIBRATION 3 // Set in Global Settings flags +# define P123_FLAGS_LOG_CALIBRATION 4 // Set in Global Settings flags +# define P123_FLAGS_ROTATION_FLIPPED 5 // Set in P123_CONFIG_FLAGS +# define P123_FLAGS_DEDUPLICATE 6 // Set in Global Settings flags + +# define P123_CONFIG_DISPLAY_TASK PCONFIG(0) + +# define P123_COLOR_DEPTH PCONFIG_LONG(1) +# define P123_CONFIG_ROTATION PCONFIG(2) +# define P123_CONFIG_X_RES PCONFIG(3) +# define P123_CONFIG_Y_RES PCONFIG(4) +# define P123_CONFIG_OBJECTCOUNT PCONFIG(5) +# define P123_CONFIG_DEBOUNCE_MS PCONFIG(6) +# define P123_CONFIG_DISPLAY_PREV PCONFIG(7) + +// # define P123_CONFIG_FLAGS PCONFIG_LONG(0) // 0-31 flags + +# define P123_VALUE_X UserVar[event->BaseVarIndex + 0] +# define P123_VALUE_Y UserVar[event->BaseVarIndex + 1] +# define P123_VALUE_Z UserVar[event->BaseVarIndex + 2] + +// Default settings values +# define P123_TS_TRESHOLD 40 // Treshold before the value is registered as a proper touch +# define P123_TS_ROTATION 0 // Rotation 0-3 = 0/90/180/270 degrees +# define P123_TS_SEND_XY true // Enable/Disable X/Y events +# define P123_TS_SEND_Z false // Disable/Enable Z events +# define P123_TS_SEND_OBJECTNAME true // Enable/Disable objectname events +# define P123_TS_USE_CALIBRATION false // Disable/Enable calibration +# define P123_TS_LOG_CALIBRATION true // Enable/Disable calibration logging +# define P123_TS_ROTATION_FLIPPED false // Enable/Disable rotation flipped 180 deg. +# define P123_TS_X_RES 320 // Pixels, should match with the screen it is mounted on +# define P123_TS_Y_RES 480 +# define P123_DEBOUNCE_MILLIS 150 // Debounce delay for On/Off button function + +# define P123_TOUCH_X_NATIVE 320 // Native touchscreen resolution +# define P123_TOUCH_Y_NATIVE 480 + +# define P123_MAX_COLOR_INPUTLENGTH 11 // 11 Characters is enough to type in all recognized names +# define P123_MaxObjectNameLength 15 // 14 character objectnames + terminating 0 +# define P123_MAX_CALIBRATION_COUNT 1 // +# define P123_MAX_OBJECT_COUNT 40 // This count of touchobjects should be enough, because of limited + // settings storage, 960 bytes + 8 + // bytes calibration coordinates +# define P123_EXTRA_OBJECT_COUNT 5 // The number of empty objects to show if max not reached +# define P123_ARRAY_SIZE (P123_MAX_OBJECT_COUNT + P123_MAX_CALIBRATION_COUNT) + +# define P123_FLAGS_ON_OFF_BUTTON 0 // TouchObjects.flags On/Off Button function +# define P123_FLAGS_INVERT_BUTTON 1 // TouchObjects.flags Inverted On/Off Button function + +# define TOUCHOBJECTS_HELPER_ROTATION_0 0 +# define TOUCHOBJECTS_HELPER_ROTATION_90 1 +# define TOUCHOBJECTS_HELPER_ROTATION_180 2 +# define TOUCHOBJECTS_HELPER_ROTATION_270 3 + +# define P123_SETTINGS_SEPARATOR '\x02' + +// Settings array field offsets: Calibration +# define P123_CALIBRATION_START 0 // Index into settings array +# define P123_CALIBRATION_ENABLED 1 // Enabled 0/1 (parseString index starts at 1) +# define P123_CALIBRATION_LOG_ENABLED 2 // Calibration Log Enabled 0/1 +# define P123_CALIBRATION_TOP_X 3 // Top X offset (uint16_t) +# define P123_CALIBRATION_TOP_Y 4 // Top Y +# define P123_CALIBRATION_BOTTOM_X 5 // Bottom X +# define P123_CALIBRATION_BOTTOM_Y 6 // Bottom Y +# define P123_COMMON_DEBOUNCE_MS 7 // Debounce milliseconds +# define P123_COMMON_TOUCH_TRESHOLD 8 // Treshold setting +# define P123_COMMON_FLAGS 9 // Common flags + +// Settings array field offsets: Touch objects +# define P123_OBJECT_INDEX_START (P123_CALIBRATION_START + 1) +# define P123_OBJECT_INDEX_END (P123_ARRAY_SIZE - (P123_CALIBRATION_START + 1)) +# define P123_OBJECT_NAME 1 // Name (String 14) (parseString index starts at 1) +# define P123_OBJECT_FLAGS 2 // Flags (uint32_t) +# define P123_OBJECT_COORD_TOP_X 3 // Top X (uint16_t) +# define P123_OBJECT_COORD_TOP_Y 4 // Top Y +# define P123_OBJECT_COORD_WIDTH 5 // Width +# define P123_OBJECT_COORD_HEIGHT 6 // Height +# define P123_OBJECT_COLOR_ON 7 // Color ON (rgb565, uint16_t) +# define P123_OBJECT_COLOR_OFF 8 // Color OFF +# define P123_OBJECT_COLOR_CAPTION 9 // Color Caption +# define P123_OBJECT_COLOR_HIGHLIGHT 10 // Color Highlight +# define P123_OBJECT_CAPTION_ON 11 // Caption ON (String 12, quoted) +# define P123_OBJECT_CAPTION_OFF 12 // Caption OFF (String 12, quoted) +# define P123_OBJECT_COLOR_BACKGROUND 13 // Color Background + +# define P123_OBJECT_FLAG_ENABLED 0 // Enabled +# define P123_OBJECT_FLAG_BUTTON 1 // Button behavior +# define P123_OBJECT_FLAG_INVERTED 2 // Inverted button +// # define P123_OBJECT_FLAG_COLORED 3 // Colored button (unused) +// # define P123_OBJECT_FLAG_CAPTION 4 // Use caption on button (unused) +# define P123_OBJECT_FLAG_BUTTONTYPE 8 // 8 bits used as button type + +# ifdef P123_USE_EXTENDED_TOUCH +enum class DrawButtonMode_e : uint8_t { + Initialize = 0, // Draw the base button + State, // Set the button state (on/off) + Highlight // Use Highlight option(s) +}; + +enum class Button_type_e : uint8_t { + None = 0, + Square, + Rounded, + Circle, + Button_MAX // must be last value in enum +}; +# endif // ifdef P123_USE_EXTENDED_TOUCH + +// Lets define our own coordinate point +struct tP123_Point +{ + uint16_t x = 0u; + uint16_t y = 0u; +}; + +// For touch objects we store a name, 2 coordinates, flags and other options +struct tP123_TouchObjects +{ + String objectName; + String captionOn; + String captionOff; + uint32_t flags = 0u; + uint32_t SurfaceAreas = 0u; + uint32_t TouchTimers = 0u; + tP123_Point top_left; + tP123_Point width_height; + # ifdef P123_USE_EXTENDED_TOUCH + uint16_t colorOn = 0u; + uint16_t colorOff = 0u; + uint16_t colorCaption = 0u; + uint16_t colorHighlight = 0u; + uint16_t colorBackground = 0u; + # endif // ifdef P123_USE_EXTENDED_TOUCH + bool TouchStates = false; +}; + + +// Data structure +struct P123_data_struct : public PluginTaskData_base +{ + P123_data_struct(); + ~P123_data_struct(); + + void reset(); + bool init(const EventStruct *event, + uint8_t rotation, + uint16_t ts_x_res, + uint16_t ts_y_res, + uint16_t displayTask, + AdaGFXColorDepth colorDepth); + bool isInitialized() const; + void loadTouchObjects(const EventStruct *event); + bool touched(); + void readData(int16_t& x, + int16_t& y, + int16_t& z, + int16_t& ox, + int16_t& oy); + void setRotation(uint8_t n); + void setRotationFlipped(bool _flipped); + bool isCalibrationActive(); + bool isValidAndTouchedTouchObject(int16_t x, + int16_t y, + String& selectedObjectName, + int8_t& selectedObjectIndex); + bool setTouchObjectState(const String& touchObject, + bool state); + void scaleRawToCalibrated(int16_t& x, + int16_t& y); + bool plugin_webform_load(struct EventStruct *event); + bool plugin_webform_save(struct EventStruct *event); + bool plugin_ten_per_second(struct EventStruct *event); + +private: + + # ifdef P123_USE_EXTENDED_TOUCH + void drawButton(DrawButtonMode_e buttonMode, + int8_t buttonIndex, + const EventStruct *event); + # endif // ifdef P123_USE_EXTENDED_TOUCH + int parseStringToInt(const String& string, + uint8_t indexFind, + char separator = ',', + int defaultValue = 0); + String enquoteString(const String& input); + + // This is initialized by calling init() + Adafruit_FT6206 *touchscreen = nullptr; + uint8_t _rotation = 0u; + bool _useCalibration = false; + uint16_t _ts_x_res = 0u; + uint16_t _ts_y_res = 0u; + uint16_t _displayTask = 0u; + AdaGFXColorDepth _colorDepth = AdaGFXColorDepth::FullColor; + + bool _flipped = false; // buffered settings + bool _deduplicate = false; + + // Calibration and some other settings + struct tP123_Globals + { + uint32_t flags = 0u; + tP123_Point top_left; + tP123_Point bottom_right; + uint16_t treshold = 0u; + uint8_t debounceMs = 0u; + bool calibrationEnabled = false; + bool logEnabled = false; + }; + + tP123_Globals P123_Settings; + + std::vectorTouchObjects; + +public: + + // This is filled during checking of a touchobject + // std::vector < uint32_t; SurfaceAreas; + + // Counters for debouncing touch button + // std::vectorTouchTimers; + // std::vector TouchStates; + + String settingsArray[P123_ARRAY_SIZE]; + uint8_t lastObjectIndex = 0u; + + uint8_t objectCount = 0u; +}; + +#endif // ifdef USED_P123 +#endif // ifndef PLUGINSTRUCTS_P123_DATA_STRUCT_H diff --git a/src/src/WebServer/Markup.cpp b/src/src/WebServer/Markup.cpp index 2ecd43ec24..3d87ba72ba 100644 --- a/src/src/WebServer/Markup.cpp +++ b/src/src/WebServer/Markup.cpp @@ -748,6 +748,8 @@ void addTextBox(const String & id, #ifdef ENABLE_TOOLTIPS , const String& tooltip #endif // ifdef ENABLE_TOOLTIPS + , + const String& datalist ) { addHtml(F("'); diff --git a/src/src/WebServer/WebServer.h b/src/src/WebServer/WebServer.h index 7ee05c568f..66d8e51bfa 100644 --- a/src/src/WebServer/WebServer.h +++ b/src/src/WebServer/WebServer.h @@ -98,7 +98,8 @@ void json_prop(LabelType::Enum label); // This allows to select a task index based on the existing tasks. // ******************************************************************************** void addTaskSelect(const String& name, - taskIndex_t choice); + taskIndex_t choice, + const String& cssclass = "wide"); // ******************************************************************************** // Add a Value select dropdown list, based on TaskIndex From abcab490d12fc41ea95bc10dcb13849e43bf6a9e Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 24 Apr 2022 18:38:03 +0200 Subject: [PATCH 002/113] [AdaGFX] Implement/activate generic 'adagfx_trigger' command-trigger --- src/src/Helpers/AdafruitGFX_helper.cpp | 4 +++- src/src/Helpers/AdafruitGFX_helper.h | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index 3069c25a57..be9a216771 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -503,7 +503,9 @@ bool AdafruitGFX_helper::processCommand(const String& string) { String cmd = parseString(string, 1); // lower case String subcommand = parseString(string, 2); - if (!cmd.equals(_trigger) || subcommand.isEmpty()) { return success; } // Only support own trigger, and at least a non=empty subcommand + if (!(cmd.equals(_trigger) || + isAdaGFXTrigger(cmd)) || + subcommand.isEmpty()) { return success; } // Only support own trigger, and at least a non=empty subcommand String log; String sParams[ADAGFX_PARSE_MAX_ARGS + 1]; diff --git a/src/src/Helpers/AdafruitGFX_helper.h b/src/src/Helpers/AdafruitGFX_helper.h index 34f93c3372..abf366f610 100644 --- a/src/src/Helpers/AdafruitGFX_helper.h +++ b/src/src/Helpers/AdafruitGFX_helper.h @@ -109,12 +109,12 @@ # endif // ifndef ADAGFX_FONTS_EXTRA_20PT_INCLUDED # endif // ifdef PLUGIN_SET_MAX -# define ADAGFX_PARSE_PREFIX F("~") // Subcommand-trigger prefix and postfix strings +# define ADAGFX_PARSE_PREFIX F("~") // Subcommand-trigger prefix and postfix strings # define ADAGFX_PARSE_PREFIX_LEN 1 -# define ADAGFX_PARSE_POSTFIX F("~") // Will be removed before the normal template parsing is done +# define ADAGFX_PARSE_POSTFIX F("~") // Will be removed before the normal template parsing is done # define ADAGFX_PARSE_POSTFIX_LEN 1 -# define ADAGFX_UNIVERSAL_TRIGGER F("adagfx_write") // Universal command trigger +# define ADAGFX_UNIVERSAL_TRIGGER F("adagfx_trigger") // Universal command trigger // Color definitions, borrowed from Adafruit_ILI9341.h From 128ae8ba7d5eca514529de5ad661a1b1ca695191 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 24 Apr 2022 19:06:20 +0200 Subject: [PATCH 003/113] [P123] Add arguments to Object events, allow initial events for On/Off button objects minor corrections --- src/_P123_FT62x6Touch.ino | 4 +- src/src/PluginStructs/P123_data_struct.cpp | 98 +++++++++++++++------- src/src/PluginStructs/P123_data_struct.h | 60 +++++++------ 3 files changed, 107 insertions(+), 55 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index 653df02e90..f5f28853d7 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -6,6 +6,7 @@ /** * Changelog: + * 2022-04-24 tonhuisman: Add event arguments for OnOff button objects, fix addLog statements, minor improvements * 2022-04-23 tonhuisman: Rename struct TS_Point in FT6206 library to FT_Point to avoid conflict with XPT2048 library (P099) * 2021-11-07 tonhuisman: Initial plugin, based on _P099_XPT2046_Touchscreen.ino plugin and Adafruit FT6206 Library */ @@ -76,8 +77,7 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_SET_DEFAULTS: { - // if already configured take it from settings, else use default values - P123_CONFIG_DISPLAY_TASK = event->TaskIndex; // Preselect current task to acvoid pointing to Task 1 by default + P123_CONFIG_DISPLAY_TASK = event->TaskIndex; // Preselect current task to avoid pointing to Task 1 by default P123_CONFIG_ROTATION = P123_TS_ROTATION; P123_CONFIG_X_RES = P123_TS_X_RES; P123_CONFIG_Y_RES = P123_TS_Y_RES; diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index 85eda3882a..c49ba382b2 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -45,7 +45,7 @@ P123_data_struct::~P123_data_struct() { */ void P123_data_struct::reset() { # ifdef PLUGIN_123_DEBUG - addLog(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen reset.")); + addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen reset.")); # endif // PLUGIN_123_DEBUG if (isInitialized()) { @@ -78,10 +78,21 @@ bool P123_data_struct::init(const EventStruct *event, touchscreen->begin(P123_Settings.treshold); + if (bitRead(P123_Settings.flags, P123_FLAGS_SEND_OBJECTNAME) && + bitRead(P123_Settings.flags, P123_FLAGS_INIT_OBJECTEVENT)) { + for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { + if (!TouchObjects[objectNr].objectName.isEmpty() + && bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED) + && bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTON)) { + generateObjectEvent(event, TouchObjects[objectNr].objectName, objectNr, -1); + } + } + } + # ifdef PLUGIN_123_DEBUG - addLog(LOG_LEVEL_INFO, F("P123 DEBUG Plugin & touchscreen initialized.")); + addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Plugin & touchscreen initialized.")); } else { - addLog(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen initialisation FAILED.")); + addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen initialisation FAILED.")); # endif // PLUGIN_123_DEBUG } return isInitialized(); @@ -136,6 +147,8 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { }; int optionValues3[P123_EVENTS_OPTIONS] = { 0, 1, 3, 4, 5, 7 }; // Already used as a bitmap! addFormSelector(F("Events"), F("p123_events"), P123_EVENTS_OPTIONS, options3, optionValues3, choice3); + addFormCheckBox(F("Initial Objectnames events"), F("p123_init_objectevent"), bitRead(P123_Settings.flags, P123_FLAGS_INIT_OBJECTEVENT)); + addFormNote(F("Will send state -1 but only for enabled On/Off button objects.")); } addFormCheckBox(F("Prevent duplicate events"), F("p123_deduplicate"), bitRead(P123_Settings.flags, P123_FLAGS_DEDUPLICATE)); @@ -449,6 +462,7 @@ bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { bitWrite(lSettings, P123_FLAGS_SEND_OBJECTNAME, bitRead(getFormItemInt(F("p123_events")), P123_FLAGS_SEND_OBJECTNAME)); bitWrite(lSettings, P123_FLAGS_ROTATION_FLIPPED, isFormItemChecked(F("p123_rotation_flipped"))); bitWrite(lSettings, P123_FLAGS_DEDUPLICATE, isFormItemChecked(F("p123_deduplicate"))); + bitWrite(lSettings, P123_FLAGS_INIT_OBJECTEVENT, isFormItemChecked(F("p123_init_objectevent"))); config += getFormItemInt(F("p123_use_calibration")); config += P123_SETTINGS_SEPARATOR; @@ -474,7 +488,7 @@ bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { String log = F("Save settings: "); config.replace(P123_SETTINGS_SEPARATOR, ','); log += config; - addLog(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, log); } String error; @@ -550,7 +564,7 @@ bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { log += F(" settings: "); config.replace(P123_SETTINGS_SEPARATOR, ','); log += config; - addLog(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, log); } # endif // ifdef PLUGIN_123_DEBUG } @@ -613,7 +627,7 @@ bool P123_data_struct::plugin_ten_per_second(struct EventStruct *event) { log += ox; log += F(", oy= "); log += oy; - addLog(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, log); } if (Settings.UseRules) { // No events to handle if rules not @@ -643,7 +657,7 @@ bool P123_data_struct::plugin_ten_per_second(struct EventStruct *event) { if ((TouchObjects[selectedObjectIndex].TouchTimers == 0) || // Not touched yet or too long ago - (TouchObjects[selectedObjectIndex].TouchTimers < (millis() - (2 * P123_Settings.debounceMs)))) { + (TouchObjects[selectedObjectIndex].TouchTimers < (millis() - (1.5 * P123_Settings.debounceMs)))) { // From now wait the debounce time TouchObjects[selectedObjectIndex].TouchTimers = millis() + P123_Settings.debounceMs; } else { @@ -651,20 +665,7 @@ bool P123_data_struct::plugin_ten_per_second(struct EventStruct *event) { if (TouchObjects[selectedObjectIndex].TouchTimers <= millis()) { TouchObjects[selectedObjectIndex].TouchStates = !TouchObjects[selectedObjectIndex].TouchStates; TouchObjects[selectedObjectIndex].TouchTimers = 0; - String eventCommand; - eventCommand.reserve(48); - eventCommand = getTaskDeviceName(event->TaskIndex); - eventCommand += '#'; - eventCommand += selectedObjectName; - eventCommand += '='; // Add arguments - - if (bitRead(TouchObjects[selectedObjectIndex].flags, P123_OBJECT_FLAG_INVERTED)) { - eventCommand += TouchObjects[selectedObjectIndex].TouchStates ? '0' : '1'; // Act like an inverted button, 0 = On, - // 1 = Off - } else { - eventCommand += TouchObjects[selectedObjectIndex].TouchStates ? '1' : '0'; // Act like a button, 1 = On, 0 = Off - } - eventQueue.add(eventCommand); + generateObjectEvent(event, selectedObjectName, selectedObjectIndex, TouchObjects[selectedObjectIndex].TouchStates ? 1 : 0); } } } else { @@ -680,7 +681,7 @@ bool P123_data_struct::plugin_ten_per_second(struct EventStruct *event) { eventCommand += y; eventCommand += ','; eventCommand += z; - eventQueue.add(eventCommand); + eventQueue.addMove(std::move(eventCommand)); } } } @@ -690,9 +691,50 @@ bool P123_data_struct::plugin_ten_per_second(struct EventStruct *event) { return success; } +/** + * generate an event for a touch object + **************************************************************************/ +void P123_data_struct::generateObjectEvent(const EventStruct *event, + const String & objectName, + const int8_t objectIndex, + const int8_t onOffState) { + String eventCommand; + + eventCommand.reserve(48); + eventCommand = getTaskDeviceName(event->TaskIndex); + eventCommand += '#'; + eventCommand += objectName; + eventCommand += '='; // Add arguments + + if (onOffState < 0) { // Negative value: pass on unaltered + eventCommand += onOffState; + } else { // Check for inverted output + if (bitRead(TouchObjects[objectIndex].flags, P123_OBJECT_FLAG_INVERTED)) { + eventCommand += onOffState == 1 ? '0' : '1'; // Act like an inverted button, 0 = On, 1 = Off + } else { + eventCommand += onOffState == 1 ? '1' : '0'; // Act like a button, 1 = On, 0 = Off + } + } + + if (P123_CONFIG_DISPLAY_TASK != event->TaskIndex) { + // When a display is configured add x,y coordinate, width,height of the object, and TaskIndex of display + eventCommand += ','; + eventCommand += TouchObjects[objectIndex].top_left.x; + eventCommand += ','; + eventCommand += TouchObjects[objectIndex].top_left.y; + eventCommand += ','; + eventCommand += TouchObjects[objectIndex].width_height.x; + eventCommand += ','; + eventCommand += TouchObjects[objectIndex].width_height.y; + eventCommand += ','; + eventCommand += P123_CONFIG_DISPLAY_TASK + 1; // What TaskIndex? + } + eventQueue.addMove(std::move(eventCommand)); +} + /** * draw a button using the mode and state - * TODO: Complete implementation + * //TODO: Complete implementation * will probably need: * - Access to the AdafruitGFX_Helper object * - Access to the Display object in the AdafruitGFX_Helper @@ -802,7 +844,7 @@ void P123_data_struct::drawButton(DrawButtonMode_e buttonMode, */ void P123_data_struct::loadTouchObjects(const EventStruct *event) { # ifdef PLUGIN_123_DEBUG - addLog(LOG_LEVEL_INFO, F("P123 DEBUG loadTouchObjects")); + addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG loadTouchObjects")); # endif // PLUGIN_123_DEBUG LoadCustomTaskSettings(event->TaskIndex, settingsArray, P123_ARRAY_SIZE, 0); @@ -955,7 +997,7 @@ void P123_data_struct::setRotation(uint8_t n) { # ifdef PLUGIN_123_DEBUG String log = F("P123 DEBUG Rotation set: "); log += n; - addLog(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, log); # endif // PLUGIN_123_DEBUG } @@ -967,7 +1009,7 @@ void P123_data_struct::setRotationFlipped(bool flipped) { # ifdef PLUGIN_123_DEBUG String log = F("P123 DEBUG RotationFlipped set: "); log += flipped; - addLog(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, log); # endif // PLUGIN_123_DEBUG } @@ -1039,7 +1081,7 @@ bool P123_data_struct::isValidAndTouchedTouchObject(int16_t x, log += selectedObjectIndex; log += '/'; log += selected ? 'T' : 'f'; - addLog(LOG_LEVEL_DEBUG, log); + addLogMove(LOG_LEVEL_DEBUG, log); } # endif // PLUGIN_123_DEBUG } @@ -1091,7 +1133,7 @@ bool P123_data_struct::setTouchObjectState(const String& touchObject, bool state } else { log += F("failed!"); } - addLog(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, log); # endif // PLUGIN_123_DEBUG } } diff --git a/src/src/PluginStructs/P123_data_struct.h b/src/src/PluginStructs/P123_data_struct.h index 59a04d1cb3..a42c673ff4 100644 --- a/src/src/PluginStructs/P123_data_struct.h +++ b/src/src/PluginStructs/P123_data_struct.h @@ -3,6 +3,7 @@ #include "../../_Plugin_Helper.h" #include "../../ESPEasy_common.h" +#include "../Helpers/AdafruitGFX_helper.h" #ifdef USES_P123 @@ -18,10 +19,13 @@ # ifdef P123_USE_TOOLTIPS # undef P123_USE_TOOLTIPS # endif // ifdef P123_USE_TOOLTIPS +# ifdef P123_USE_EXTENDED_TOUCH +# undef P123_USE_EXTENDED_TOUCH +# endif // ifdef P123_USE_EXTENDED_TOUCH # endif // ifdef LIMIT_BUILD_SIZE -# if defined(P1123_USE_TOOLTIPS) && !defined(ENABLE_TOOLTIPS) +# if defined(P123_USE_TOOLTIPS) && !defined(ENABLE_TOOLTIPS) # undef P123_USE_TOOLTIPS -# endif // if defined(P1123_USE_TOOLTIPS) && !defined(ENABLE_TOOLTIPS) +# endif // if defined(P123_USE_TOOLTIPS) && !defined(ENABLE_TOOLTIPS) # define P123_FLAGS_SEND_XY 0 // Set in Global Settings flags # define P123_FLAGS_SEND_Z 1 // Set in Global Settings flags @@ -30,6 +34,7 @@ # define P123_FLAGS_LOG_CALIBRATION 4 // Set in Global Settings flags # define P123_FLAGS_ROTATION_FLIPPED 5 // Set in P123_CONFIG_FLAGS # define P123_FLAGS_DEDUPLICATE 6 // Set in Global Settings flags +# define P123_FLAGS_INIT_OBJECTEVENT 7 // Set in Global Settings flags # define P123_CONFIG_DISPLAY_TASK PCONFIG(0) @@ -58,17 +63,16 @@ # define P123_TS_ROTATION_FLIPPED false // Enable/Disable rotation flipped 180 deg. # define P123_TS_X_RES 320 // Pixels, should match with the screen it is mounted on # define P123_TS_Y_RES 480 -# define P123_DEBOUNCE_MILLIS 150 // Debounce delay for On/Off button function +# define P123_DEBOUNCE_MILLIS 100 // Debounce delay for On/Off button function # define P123_TOUCH_X_NATIVE 320 // Native touchscreen resolution # define P123_TOUCH_Y_NATIVE 480 -# define P123_MAX_COLOR_INPUTLENGTH 11 // 11 Characters is enough to type in all recognized names +# define P123_MAX_COLOR_INPUTLENGTH 11 // 11 Characters is enough to type in all recognized color names and values # define P123_MaxObjectNameLength 15 // 14 character objectnames + terminating 0 # define P123_MAX_CALIBRATION_COUNT 1 // # define P123_MAX_OBJECT_COUNT 40 // This count of touchobjects should be enough, because of limited - // settings storage, 960 bytes + 8 - // bytes calibration coordinates + // settings storage, 960 bytes + 8 bytes calibration coordinates # define P123_EXTRA_OBJECT_COUNT 5 // The number of empty objects to show if max not reached # define P123_ARRAY_SIZE (P123_MAX_OBJECT_COUNT + P123_MAX_CALIBRATION_COUNT) @@ -97,26 +101,28 @@ // Settings array field offsets: Touch objects # define P123_OBJECT_INDEX_START (P123_CALIBRATION_START + 1) # define P123_OBJECT_INDEX_END (P123_ARRAY_SIZE - (P123_CALIBRATION_START + 1)) -# define P123_OBJECT_NAME 1 // Name (String 14) (parseString index starts at 1) -# define P123_OBJECT_FLAGS 2 // Flags (uint32_t) -# define P123_OBJECT_COORD_TOP_X 3 // Top X (uint16_t) -# define P123_OBJECT_COORD_TOP_Y 4 // Top Y -# define P123_OBJECT_COORD_WIDTH 5 // Width -# define P123_OBJECT_COORD_HEIGHT 6 // Height -# define P123_OBJECT_COLOR_ON 7 // Color ON (rgb565, uint16_t) -# define P123_OBJECT_COLOR_OFF 8 // Color OFF -# define P123_OBJECT_COLOR_CAPTION 9 // Color Caption -# define P123_OBJECT_COLOR_HIGHLIGHT 10 // Color Highlight -# define P123_OBJECT_CAPTION_ON 11 // Caption ON (String 12, quoted) -# define P123_OBJECT_CAPTION_OFF 12 // Caption OFF (String 12, quoted) -# define P123_OBJECT_COLOR_BACKGROUND 13 // Color Background - -# define P123_OBJECT_FLAG_ENABLED 0 // Enabled -# define P123_OBJECT_FLAG_BUTTON 1 // Button behavior -# define P123_OBJECT_FLAG_INVERTED 2 // Inverted button +# define P123_OBJECT_NAME 1 // Name (String 14) (parseString index starts at 1) +# define P123_OBJECT_FLAGS 2 // Flags (uint32_t) +# define P123_OBJECT_COORD_TOP_X 3 // Top X (uint16_t) +# define P123_OBJECT_COORD_TOP_Y 4 // Top Y +# define P123_OBJECT_COORD_WIDTH 5 // Width +# define P123_OBJECT_COORD_HEIGHT 6 // Height +# ifdef P123_USE_EXTENDED_TOUCH +# define P123_OBJECT_COLOR_ON 7 // Color ON (rgb565, uint16_t) +# define P123_OBJECT_COLOR_OFF 8 // Color OFF +# define P123_OBJECT_COLOR_CAPTION 9 // Color Caption +# define P123_OBJECT_COLOR_HIGHLIGHT 10 // Color Highlight +# define P123_OBJECT_CAPTION_ON 11 // Caption ON (String 12, quoted) +# define P123_OBJECT_CAPTION_OFF 12 // Caption OFF (String 12, quoted) +# define P123_OBJECT_COLOR_BACKGROUND 13 // Color Background +# endif // ifdef P123_USE_EXTENDED_TOUCH + +# define P123_OBJECT_FLAG_ENABLED 0 // Enabled +# define P123_OBJECT_FLAG_BUTTON 1 // Button behavior +# define P123_OBJECT_FLAG_INVERTED 2 // Inverted button // # define P123_OBJECT_FLAG_COLORED 3 // Colored button (unused) // # define P123_OBJECT_FLAG_CAPTION 4 // Use caption on button (unused) -# define P123_OBJECT_FLAG_BUTTONTYPE 8 // 8 bits used as button type +# define P123_OBJECT_FLAG_BUTTONTYPE 8 // 8 bits used as button type # ifdef P123_USE_EXTENDED_TOUCH enum class DrawButtonMode_e : uint8_t { @@ -210,7 +216,11 @@ struct P123_data_struct : public PluginTaskData_base uint8_t indexFind, char separator = ',', int defaultValue = 0); - String enquoteString(const String& input); + String enquoteString(const String& input); // TODO: Replace by wrapWithQuotes + void generateObjectEvent(const EventStruct *event, + const String & objectName, + const int8_t objectIndex, + const int8_t onOffState); // This is initialized by calling init() Adafruit_FT6206 *touchscreen = nullptr; From 807fa040e65b7fa7201c1b1a9f7b97840078a3a8 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 24 Apr 2022 21:30:51 +0200 Subject: [PATCH 004/113] [P123] Code improvements, increased button response --- src/_P123_FT62x6Touch.ino | 3 ++- src/src/PluginStructs/P123_data_struct.cpp | 13 +++++++------ src/src/PluginStructs/P123_data_struct.h | 1 - 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index f5f28853d7..9bc56694c3 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -6,6 +6,7 @@ /** * Changelog: + * 2022-04-24 tonhuisman: Code improvements, increased button response speed * 2022-04-24 tonhuisman: Add event arguments for OnOff button objects, fix addLog statements, minor improvements * 2022-04-23 tonhuisman: Rename struct TS_Point in FT6206 library to FT_Point to avoid conflict with XPT2048 library (P099) * 2021-11-07 tonhuisman: Initial plugin, based on _P099_XPT2046_Touchscreen.ino plugin and Adafruit FT6206 Library @@ -246,7 +247,7 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) break; } - case PLUGIN_TEN_PER_SECOND: // Should be often/fast enough, as this is user-interaction driven + case PLUGIN_FIFTY_PER_SECOND: // Increased response { P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index c49ba382b2..f4ca844e89 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -84,7 +84,7 @@ bool P123_data_struct::init(const EventStruct *event, if (!TouchObjects[objectNr].objectName.isEmpty() && bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED) && bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTON)) { - generateObjectEvent(event, TouchObjects[objectNr].objectName, objectNr, -1); + generateObjectEvent(event, objectNr, -1); } } } @@ -665,7 +665,7 @@ bool P123_data_struct::plugin_ten_per_second(struct EventStruct *event) { if (TouchObjects[selectedObjectIndex].TouchTimers <= millis()) { TouchObjects[selectedObjectIndex].TouchStates = !TouchObjects[selectedObjectIndex].TouchStates; TouchObjects[selectedObjectIndex].TouchTimers = 0; - generateObjectEvent(event, selectedObjectName, selectedObjectIndex, TouchObjects[selectedObjectIndex].TouchStates ? 1 : 0); + generateObjectEvent(event, selectedObjectIndex, TouchObjects[selectedObjectIndex].TouchStates ? 1 : 0); } } } else { @@ -693,9 +693,9 @@ bool P123_data_struct::plugin_ten_per_second(struct EventStruct *event) { /** * generate an event for a touch object + * When a display is configured add x,y coordinate, width,height of the object, objectIndex, and TaskIndex of display **************************************************************************/ void P123_data_struct::generateObjectEvent(const EventStruct *event, - const String & objectName, const int8_t objectIndex, const int8_t onOffState) { String eventCommand; @@ -703,7 +703,7 @@ void P123_data_struct::generateObjectEvent(const EventStruct *event, eventCommand.reserve(48); eventCommand = getTaskDeviceName(event->TaskIndex); eventCommand += '#'; - eventCommand += objectName; + eventCommand += TouchObjects[objectIndex].objectName; eventCommand += '='; // Add arguments if (onOffState < 0) { // Negative value: pass on unaltered @@ -716,8 +716,7 @@ void P123_data_struct::generateObjectEvent(const EventStruct *event, } } - if (P123_CONFIG_DISPLAY_TASK != event->TaskIndex) { - // When a display is configured add x,y coordinate, width,height of the object, and TaskIndex of display + if (P123_CONFIG_DISPLAY_TASK != event->TaskIndex) { // Add arguments for display eventCommand += ','; eventCommand += TouchObjects[objectIndex].top_left.x; eventCommand += ','; @@ -727,6 +726,8 @@ void P123_data_struct::generateObjectEvent(const EventStruct *event, eventCommand += ','; eventCommand += TouchObjects[objectIndex].width_height.y; eventCommand += ','; + eventCommand += objectIndex + 1; // Adjust to displayed index + eventCommand += ','; eventCommand += P123_CONFIG_DISPLAY_TASK + 1; // What TaskIndex? } eventQueue.addMove(std::move(eventCommand)); diff --git a/src/src/PluginStructs/P123_data_struct.h b/src/src/PluginStructs/P123_data_struct.h index a42c673ff4..544e76db4d 100644 --- a/src/src/PluginStructs/P123_data_struct.h +++ b/src/src/PluginStructs/P123_data_struct.h @@ -218,7 +218,6 @@ struct P123_data_struct : public PluginTaskData_base int defaultValue = 0); String enquoteString(const String& input); // TODO: Replace by wrapWithQuotes void generateObjectEvent(const EventStruct *event, - const String & objectName, const int8_t objectIndex, const int8_t onOffState); From 3546699cea6216b58b5683441d702ababd194dba Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 1 May 2022 21:57:26 +0200 Subject: [PATCH 005/113] [AdaGFX] Add support for `btn` subcommand to draw button-like objects --- src/src/Helpers/AdafruitGFX_helper.cpp | 329 ++++++++++++++++++++++++- src/src/Helpers/AdafruitGFX_helper.h | 80 ++++-- 2 files changed, 383 insertions(+), 26 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index be9a216771..a7b69e7ae6 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -8,6 +8,7 @@ # if defined(FEATURE_SD) && defined(ADAGFX_ENABLE_BMP_DISPLAY) # include # endif // if defined(FEATURE_SD) && defined(ADAGFX_ENABLE_BMP_DISPLAY) +# include # if ADAGFX_FONTS_INCLUDED # include "src/Static/Fonts/Seven_Segment24pt7b.h" @@ -71,6 +72,49 @@ const __FlashStringHelper* toString(AdaGFXColorDepth colorDepth) { return F("None"); } +# if ADAGFX_ENABLE_BUTTON_DRAW + +/****************************************************************************************** + * get the display text for a button type enum value + *****************************************************************************************/ +const __FlashStringHelper* toString(Button_type_e button) { + switch (button) { + case Button_type_e::None: return F("None"); + case Button_type_e::Square: return F("Square"); + case Button_type_e::Rounded: return F("Rounded"); + case Button_type_e::Circle: return F("Circle"); + case Button_type_e::ArrowLeft: return F("Arrow, left"); + case Button_type_e::ArrowUp: return F("Arrow, up"); + case Button_type_e::ArrowRight: return F("Arrow, right"); + case Button_type_e::ArrowDown: return F("Arrow, down"); + case Button_type_e::Button_MAX: break; + } + return F("Unsupported!"); +} + +/****************************************************************************************** + * get the display text for a button layout enum value + *****************************************************************************************/ +const __FlashStringHelper* toString(Button_layout_e layout) { + switch (layout) { + case Button_layout_e::CenterAligned: return F("Centered"); + case Button_layout_e::LeftAligned: return F("Left-aligned"); + case Button_layout_e::TopAligned: return F("Top-aligned"); + case Button_layout_e::RightAligned: return F("Right-aligned"); + case Button_layout_e::BottomAligned: return F("Bottom-aligned"); + case Button_layout_e::LeftTopAligned: return F("Left-Top-aligned"); + case Button_layout_e::RightTopAligned: return F("Right-Top-aligned"); + case Button_layout_e::RightBottomAligned: return F("Right-Bottom-aligned"); + case Button_layout_e::LeftBottomAligned: return F("Left-Bottom-aligned"); + case Button_layout_e::NoCaption: return F("No Caption"); + case Button_layout_e::Bitmap: return F("Bitmap image"); + case Button_layout_e::Alignment_MAX: break; + } + return F("Unsupported!"); +} + +# endif // if ADAGFX_ENABLE_BUTTON_DRAW + /***************************************************************************************** * Show a selector for all available 'Text print mode' options, for use in PLUGIN_WEBFORM_LOAD ****************************************************************************************/ @@ -231,7 +275,7 @@ void AdaGFXFormFontScaling(const __FlashStringHelper *fontScalingId, /**************************************************************************** * AdaGFXparseTemplate: Replace variables and adjust unicode special characters to Adafruit font ***************************************************************************/ -String AdaGFXparseTemplate(String & tmpString, +String AdaGFXparseTemplate(const String & tmpString, uint8_t lineSize, AdafruitGFX_helper *gfxHelper) { // Änderung WDS: Tabelle vorerst Abgeschaltet !!!! @@ -439,7 +483,10 @@ AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_SPITFT *display, _useValidation(useValidation), _textBackFill(textBackFill) { _display = _tft; - addLog(LOG_LEVEL_INFO, F("AdaGFX_helper: TFT Init.")); + String log = F("AdaGFX_helper: TFT Init. "); + + log += getFeatures(); + addLog(LOG_LEVEL_INFO, log); initialize(); } @@ -461,6 +508,8 @@ void AdafruitGFX_helper::initialize() log += static_cast(_colorDepth); log += F(", trigger: "); log += _trigger; + log += F(", "); + log += getFeatures(); addLogMove(ADAGFX_LOG_LEVEL, log); } # endif // ifndef BUILD_NO_DEBUG @@ -477,6 +526,24 @@ void AdafruitGFX_helper::initialize() } } +String AdafruitGFX_helper::getFeatures() { + String log = F("Features:"); + + # if (defined(ADAGFX_USE_ASCIITABLE) && ADAGFX_USE_ASCIITABLE) + log += F(" asciitable,"); + # endif // if (defined(ADAGFX_USE_ASCIITABLE) && ADAGFX_USE_ASCIITABLE) + # if (defined(ADAGFX_ENABLE_EXTRA_CMDS) && ADAGFX_ENABLE_EXTRA_CMDS) + log += F(" lm/lmr,"); + # endif // if (defined(ADAGFX_ENABLE_EXTRA_CMDS) && ADAGFX_ENABLE_EXTRA_CMDS) + # if (defined(ADAGFX_ENABLE_BMP_DISPLAY) && ADAGFX_ENABLE_BMP_DISPLAY) + log += F(" bmp,"); + # endif // if (defined(ADAGFX_ENABLE_BMP_DISPLAY) && ADAGFX_ENABLE_BMP_DISPLAY) + # if (defined(ADAGFX_ENABLE_BUTTON_DRAW) && ADAGFX_ENABLE_BUTTON_DRAW) + log += F(" btn,"); + # endif // if (defined(ADAGFX_ENABLE_BUTTON_DRAW) && ADAGFX_ENABLE_BUTTON_DRAW) + return log; +} + /**************************************************************************** * getCursorXY: get the current (text) cursor coordinates, either in pixels or cols/rows, depending on related setting ***************************************************************************/ @@ -508,13 +575,14 @@ bool AdafruitGFX_helper::processCommand(const String& string) { subcommand.isEmpty()) { return success; } // Only support own trigger, and at least a non=empty subcommand String log; - String sParams[ADAGFX_PARSE_MAX_ARGS + 1]; - int nParams[ADAGFX_PARSE_MAX_ARGS + 1]; - int argCount = 0; - bool loop = true; - - while (argCount <= ADAGFX_PARSE_MAX_ARGS && loop) { - sParams[argCount] = parseStringKeepCase(string, argCount + 3); // 0-offset + 1st and 2nd argument used by trigger/subcommand + std::vector sParams; + std::vector nParams; + int argCount = 0; + bool loop = true; + + while (loop) { // Process all provided arguments + sParams.push_back(parseStringKeepCase(string, argCount + 3)); // 0-offset + 1st and 2nd argument used by trigger/subcommand + nParams.push_back(0); validIntFromString(sParams[argCount], nParams[argCount]); loop = !sParams[argCount].isEmpty(); @@ -531,6 +599,15 @@ bool AdafruitGFX_helper::processCommand(const String& string) { if (loop) { argCount++; } } + { // Guarantee minimal nParams/sParams size + int args = argCount; + + while (args <= ADAGFX_PARSE_MAX_ARGS) { + sParams.push_back(EMPTY_STRING); + nParams.push_back(0); + args++; + } + } success = true; // If we get this far, we'll flip the flag if something wrong is found # ifndef BUILD_NO_DEBUG @@ -1160,6 +1237,240 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } } # endif // if ADAGFX_ENABLE_BMP_DISPLAY + # if ADAGFX_ENABLE_BUTTON_DRAW + else if (subcommand.equals(F("btn")) && (argCount >= 7) && (nParams[6] != 0)) + { // btn,state,x,y,w,h,id,type[,ONclr,OFFclr,Captionclr,fontscale,ONcaption,OFFcapt,Borderclr,DisabClr,DisabCaptclr],TaskIndex,Group,SelGrp,objectname + // ev: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17,18,19,20 + // nP: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16,17,18,19 + // : Draw a button + // state: -2 = disabled, -1 = initial (off), 0 = off, 1 = on + // type & 0x0F: 0 = none, < 0 = clear area, 1 = rectangle, 2 = rounded rect., 3 = circle, + // type & 0xF0 = CenterAligned, LeftAligned, TopAligned, RightAligned, BottomAligned, LeftTopAligned, RightTopAligned, + // RightBottomAligned, LeftBottomAligned, NoCaption + // (*clr = color, TaskIndex, Group and SelGrp are ignored) + # if ADAGFX_ARGUMENT_VALIDATION + + if (invalidCoordinates(nParams[1], nParams[2]) || + invalidCoordinates(nParams[1] + nParams[3], nParams[2] + nParams[4])) { + success = false; + } else + # endif // if ADAGFX_ARGUMENT_VALIDATION + { + // All checked out OK + // Default values + uint16_t onColor = ADAGFX_GREEN; + uint16_t offColor = ADAGFX_RED; + uint16_t captionColor = ADAGFX_WHITE; + uint8_t fontScale = 2; + uint16_t borderColor = ADAGFX_WHITE; + uint16_t disabledColor = 0x9410; + uint16_t disabledCaptionColor = 0x5A69; + + if (!sParams[7].isEmpty()) { onColor = AdaGFXparseColor(sParams[7], _colorDepth); } + + if (!sParams[8].isEmpty()) { offColor = AdaGFXparseColor(sParams[8], _colorDepth); } + + if (!sParams[9].isEmpty()) { captionColor = AdaGFXparseColor(sParams[9], _colorDepth); } + + if (nParams[12] > 0) { fontScale = nParams[12]; } + + if (!sParams[13].isEmpty()) { borderColor = AdaGFXparseColor(sParams[13], _colorDepth); } + + if (!sParams[14].isEmpty()) { disabledColor = AdaGFXparseColor(sParams[14], _colorDepth); } + + if (!sParams[15].isEmpty()) { disabledCaptionColor = AdaGFXparseColor(sParams[15], _colorDepth); } + + uint16_t fillColor = onColor; + uint16_t textColor = captionColor; + bool clearArea = nParams[6] < 0; + nParams[6] = std::abs(nParams[6]); + + // Check state: -2, -1, 0, 1 to select used colors + if ((nParams[0] == 0) || (nParams[0] == -1)) { + fillColor = offColor; + } else if (nParams[0] == -2) { + fillColor = disabledColor; + textColor = disabledCaptionColor; + } else if (clearArea) { + fillColor = _bgcolor; // + borderColor = _bgcolor; + } + + if ((static_cast(nParams[6] & 0x0F) != Button_type_e::None) || + clearArea) { + _display->fillRect(nParams[1], nParams[2], nParams[3], nParams[4], _bgcolor); + } + + // Check button-type bits (mask: 0x0F) to draw correct shape + if (!clearArea) { + switch (static_cast(nParams[6] & 0x0F)) { + case Button_type_e::Square: // Rectangle + { + _display->fillRect(nParams[1], nParams[2], nParams[3], nParams[4], fillColor); + _display->drawRect(nParams[1], nParams[2], nParams[3], nParams[4], borderColor); + break; + } + case Button_type_e::Rounded: // Rounded Rectangle + { + int16_t radius = (nParams[3] + nParams[4]) / 20; // 10 % corner radius + _display->fillRoundRect(nParams[1], nParams[2], nParams[3], nParams[4], radius, fillColor); + _display->drawRoundRect(nParams[1], nParams[2], nParams[3], nParams[4], radius, borderColor); + break; + } + case Button_type_e::Circle: // Circle + { + int16_t radius = (nParams[3] + nParams[4]) / 4; // average radius + _display->fillCircle(nParams[1] + (nParams[3] / 2), nParams[2] + (nParams[4] / 2), radius, fillColor); + _display->drawCircle(nParams[1] + (nParams[3] / 2), nParams[2] + (nParams[4] / 2), radius, borderColor); + break; + } + case Button_type_e::ArrowLeft: + { // draw: left-center, right-top, right-bottom + _display->fillTriangle(nParams[1], nParams[2] + nParams[4] / 2, nParams[1] + nParams[3], nParams[2], + nParams[1] + nParams[3], nParams[2] + nParams[4], fillColor); + _display->drawTriangle(nParams[1], nParams[2] + nParams[4] / 2, nParams[1] + nParams[3], nParams[2], + nParams[1] + nParams[3], nParams[2] + nParams[4], borderColor); + break; + } + case Button_type_e::ArrowUp: + { // draw: top-center, right-bottom, left-bottom + _display->fillTriangle(nParams[1] + nParams[3] / 2, nParams[2], nParams[1] + nParams[3], nParams[2] + nParams[4], + nParams[1], nParams[2] + nParams[4], fillColor); + _display->drawTriangle(nParams[1] + nParams[3] / 2, nParams[2], nParams[1] + nParams[3], nParams[2] + nParams[4], + nParams[1], nParams[2] + nParams[4], borderColor); + break; + } + case Button_type_e::ArrowRight: + { // draw: left-top, right-center, left-bottom + _display->fillTriangle(nParams[1], nParams[2], nParams[1] + nParams[3], nParams[2] + nParams[4] / 2, + nParams[1], nParams[2] + nParams[4], fillColor); + _display->drawTriangle(nParams[1], nParams[2], nParams[1] + nParams[3], nParams[2] + nParams[4] / 2, + nParams[1], nParams[2] + nParams[4], borderColor); + break; + } + case Button_type_e::ArrowDown: + { // draw: left-top, right-top, bottom-center + _display->fillTriangle(nParams[1], nParams[2], nParams[1] + nParams[3], nParams[2], + nParams[1] + nParams[3] / 2, nParams[2] + nParams[4], fillColor); + _display->drawTriangle(nParams[1], nParams[2], nParams[1] + nParams[3], nParams[2], + nParams[1] + nParams[3] / 2, nParams[2] + nParams[4], borderColor); + break; + } + case Button_type_e::None: + case Button_type_e::Button_MAX: + break; + } + } + + // Display caption? (or bitmap) + if (!clearArea && + !(static_cast(nParams[6] & 0xF0) == Button_layout_e::NoCaption)) { + int16_t x1, y1; + uint16_t w1, h1, w2, h2; + String newString; + + // Determine alignment parameters + if (nParams[0] == 1) { + newString = sParams[11].isEmpty() ? sParams[5] : sParams[11]; + } else { + newString = sParams[12].isEmpty() ? sParams[5] : sParams[12]; + } + newString = AdaGFXparseTemplate(newString, 20); + + if ((nParams[10] > 0) && (nParams[10] <= 10)) { _display->setTextSize(nParams[10]); } // set scaling + _display->getTextBounds(newString, 0, 0, &x1, &y1, &w1, &h1); // get caption length and height in pixels + _display->getTextBounds(F(" "), 0, 0, &x1, &y1, &w2, &h2); // measure space width for little margins + + // Check button-alignment bits (mask 0xF0) for caption placement, modifies the x/y arguments passed! + // Little margin is: from left/right: half of the width of a space, from top/bottom: half of height of the font used + Button_layout_e buttonLayout = static_cast(nParams[6] & 0xF0); + + switch (buttonLayout) { + case Button_layout_e::CenterAligned: + nParams[1] += (nParams[3] / 2 - w1 / 2); // center horizontically + nParams[2] += (nParams[4] / 2 - h1 / 2); // center vertically + break; + case Button_layout_e::LeftAligned: + nParams[1] += w2 / 2; // A little margin from left + nParams[2] += (nParams[4] / 2 - h1 / 2); // center vertically + break; + case Button_layout_e::TopAligned: + nParams[1] += (nParams[3] / 2 - w1 / 2); // center horizontically + nParams[2] += h1 / 2; // A little margin from top + break; + case Button_layout_e::RightAligned: + nParams[1] += (nParams[3] - w1) - w2 / 2; // right-align + a little margin + nParams[2] += (nParams[4] / 2 - h1 / 2); // center vertically + break; + case Button_layout_e::BottomAligned: + nParams[1] += (nParams[3] / 2 - w1 / 2); // center horizontically + nParams[2] += (nParams[4] - h1 * 1.5); // bottom align + a little margin + break; + case Button_layout_e::LeftTopAligned: + nParams[1] += w2 / 2; // A little margin from left + nParams[2] += h1 / 2; // A little margin from top + break; + case Button_layout_e::RightTopAligned: + nParams[1] += (nParams[3] - w1) - w2 / 2; // right-align + a little margin + nParams[2] += h1 / 2; // A little margin from top + break; + case Button_layout_e::RightBottomAligned: + nParams[1] += (nParams[3] - w1) - w2 / 2; // right-align + a little margin + nParams[2] += (nParams[4] - h1 * 1.5); // bottom align + a little margin + break; + case Button_layout_e::LeftBottomAligned: + nParams[1] += w2 / 2; // A little margin from left + nParams[2] += (nParams[4] - h1 * 1.5); // bottom align + a little margin + break; + case Button_layout_e::Bitmap: + { // Use ON/OFF caption to specify (full) bitmap filename + # if ADAGFX_ENABLE_BMP_DISPLAY + + if (!newString.isEmpty()) { + int offX = 0; // Allow optional arguments for x and y offset values, usage: + int offY = 0; // [x,[y,]]filename.bmp + + if (newString.indexOf(',') > -1) { + String tmp = parseString(newString, 1); + validIntFromString(tmp, offX); + newString = parseStringToEndKeepCase(newString, 2); + + if (newString.indexOf(',') > -1) { + tmp = parseString(newString, 1); + validIntFromString(tmp, offY); + newString = parseStringToEndKeepCase(newString, 2); + } + } + success = showBmp(newString, nParams[1] + offX, nParams[2] + offY); + } else + # endif // if ADAGFX_ENABLE_BMP_DISPLAY + { + success = false; + } + break; + } + case Button_layout_e::NoCaption: + case Button_layout_e::Alignment_MAX: + break; + } + + if ((buttonLayout != Button_layout_e::NoCaption) && + (buttonLayout != Button_layout_e::Bitmap)) { + // Set position and colors, then print + _display->setCursor(nParams[1], nParams[2]); + _display->setTextColor(captionColor, captionColor); // transparent bg results in button color + _display->print(newString); + + // restore colors + _display->setTextColor(_fgcolor, _bgcolor); + } + + // restore font scaling + if ((nParams[10] > 0) && (nParams[10] <= 10)) { _display->setTextSize(_fontscaling); } + } + } + } + # endif // if ADAGFX_ENABLE_BUTTON_DRAW else { success = false; } diff --git a/src/src/Helpers/AdafruitGFX_helper.h b/src/src/Helpers/AdafruitGFX_helper.h index abf366f610..2f663be953 100644 --- a/src/src/Helpers/AdafruitGFX_helper.h +++ b/src/src/Helpers/AdafruitGFX_helper.h @@ -22,28 +22,31 @@ # include "../Helpers/ESPEasy_Storage.h" # include "../ESPEasyCore/ESPEasy_Log.h" -# define ADAGFX_PARSE_MAX_ARGS 7 // Maximum number of arguments needed and supported (corrected) +# define ADAGFX_PARSE_MAX_ARGS 7 // Maximum number of arguments needed and supported (corrected) # ifndef ADAGFX_ARGUMENT_VALIDATION -# define ADAGFX_ARGUMENT_VALIDATION 1 // Validate command arguments +# define ADAGFX_ARGUMENT_VALIDATION 1 // Validate command arguments # endif // ifndef ADAGFX_ARGUMENT_VALIDATION # ifndef ADAGFX_USE_ASCIITABLE -# define ADAGFX_USE_ASCIITABLE 1 // Enable 'asciitable' command (useful for debugging/development) +# define ADAGFX_USE_ASCIITABLE 1 // Enable 'asciitable' command (useful for debugging/development) # endif // ifndef ADAGFX_USE_ASCIITABLE # ifndef ADAGFX_SUPPORT_7COLOR -# define ADAGFX_SUPPORT_7COLOR 1 // Do we support 7-Color displays? +# define ADAGFX_SUPPORT_7COLOR 1 // Do we support 7-Color displays? # endif // ifndef ADAGFX_SUPPORT_7COLOR # ifndef ADAGFX_FONTS_INCLUDED -# define ADAGFX_FONTS_INCLUDED 1 // 3 extra fonts, also controls enable/disable of below 8pt/12pt fonts +# define ADAGFX_FONTS_INCLUDED 1 // 3 extra fonts, also controls enable/disable of below 8pt/12pt fonts # endif // ifndef ADAGFX_FONTS_INCLUDED # ifndef ADAGFX_PARSE_SUBCOMMAND -# define ADAGFX_PARSE_SUBCOMMAND 1 // Enable parsing of subcommands (pre/postfix below) to be executed by the helper +# define ADAGFX_PARSE_SUBCOMMAND 1 // Enable parsing of subcommands (pre/postfix below) to be executed by the helper # endif // ifndef ADAGFX_PARSE_SUBCOMMAND # ifndef ADAGFX_ENABLE_EXTRA_CMDS -# define ADAGFX_ENABLE_EXTRA_CMDS 1 // Enable extra subcommands like lm (line-multi) and lmr (line-multi, relative) +# define ADAGFX_ENABLE_EXTRA_CMDS 1 // Enable extra subcommands like lm (line-multi) and lmr (line-multi, relative) # endif // ifndef ADAGFX_ENABLE_EXTRA_CMDS # ifndef ADAGFX_ENABLE_BMP_DISPLAY -# define ADAGFX_ENABLE_BMP_DISPLAY 1 // Enable subcommands for displaying .bmp files on supported displays (color) +# define ADAGFX_ENABLE_BMP_DISPLAY 1 // Enable subcommands for displaying .bmp files on supported displays (color) # endif // ifndef ADAGFX_ENABLE_BMP_DISPLAY +# ifndef ADAGFX_ENABLE_BUTTON_DRAW +# define ADAGFX_ENABLE_BUTTON_DRAW 1 // Enable subcommands for displaying button-like shapes +# endif // ifndef ADAGFX_ENABLE_BUTTON_DRAW // # define ADAGFX_FONTS_EXTRA_8PT_INCLUDED // 5 extra 8pt fonts, should probably only be enabled in a private custom build, adds ~10,4 kB // # define ADAGFX_FONTS_EXTRA_12PT_INCLUDED // 6 extra 12pt fonts, should probably only be enabled in a private custom build, adds ~19,8 kB @@ -89,6 +92,9 @@ // # ifdef ADAGFX_ENABLE_BMP_DISPLAY // # undef ADAGFX_ENABLE_BMP_DISPLAY // # endif // ifdef ADAGFX_ENABLE_BMP_DISPLAY +// # ifdef ADAGFX_ENABLE_BUTTON_DRAW +// # undef ADAGFX_ENABLE_BUTTON_DRAW +// # endif // ifdef ADAGFX_ENABLE_BUTTON_DRAW # endif // ifdef LIMIT_BUILD_SIZE # ifdef PLUGIN_SET_MAX // Include all fonts in MAX builds @@ -186,7 +192,45 @@ enum class AdaGFXColorDepth : uint16_t { FullColor = 65535u // 65535 colors (max. supported by RGB565) }; -class AdafruitGFX_helper; // Forward declaration +# if ADAGFX_ENABLE_BUTTON_DRAW + +// Only bits 0..3 can be used, masked with: 0x0F +// stored combined with Button_layout_e value +enum class Button_type_e : uint8_t { + None = 0x00, + Square = 0x01, + Rounded = 0x02, + Circle = 0x03, + ArrowLeft = 0x04, + ArrowUp = 0x05, + ArrowRight = 0x06, + ArrowDown = 0x07, + Button_MAX // must be last value in enum, max possible values: 16 +}; + +// Only bits 4..7 can be used, masked with: 0xF0 +// stored combined with Button_type_e value +enum class Button_layout_e : uint8_t { + CenterAligned = 0x00, + LeftAligned = 0x10, + TopAligned = 0x20, + RightAligned = 0x30, + BottomAligned = 0x40, + LeftTopAligned = 0x50, + RightTopAligned = 0x60, + RightBottomAligned = 0x70, + LeftBottomAligned = 0x80, + NoCaption = 0x90, + Bitmap = 0xA0, + Alignment_MAX = 11u // options-count +}; + +const __FlashStringHelper* toString(Button_type_e button); +const __FlashStringHelper* toString(Button_layout_e layout); + +# endif // if ADAGFX_ENABLE_BUTTON_DRAW + +class AdafruitGFX_helper; // Forward declaration // Some generic AdafruitGFX_helper support functions const __FlashStringHelper* toString(AdaGFXTextPrintMode mode); @@ -222,7 +266,7 @@ void AdaGFXFormDisplayButton(const __FlashStringHelper *buttonPinId, void AdaGFXFormFontScaling(const __FlashStringHelper *fontScalingId, uint8_t fontScaling, uint8_t maxScale = 10); -String AdaGFXparseTemplate(String & tmpString, +String AdaGFXparseTemplate(const String & tmpString, uint8_t lineSize, AdafruitGFX_helper *gfxHelper = nullptr); uint16_t AdaGFXparseColor(String & s, @@ -267,14 +311,16 @@ class AdafruitGFX_helper { # endif // ifdef ADAGFX_ENABLE_BMP_DISPLAY virtual ~AdafruitGFX_helper() {} - bool processCommand(const String& string); // Parse the string for recognized commands and apply them on the graphics display + String getFeatures(); + + bool processCommand(const String& string); // Parse the string for recognized commands and apply them on the graphics display - void printText(const char *string, - int X, - int Y, - unsigned int textSize = 0, - unsigned short color = ADAGFX_WHITE, - unsigned short bkcolor = ADAGFX_BLACK); + void printText(const char *string, + int X, + int Y, + unsigned int textSize = 0, + unsigned short color = ADAGFX_WHITE, + unsigned short bkcolor = ADAGFX_BLACK); void calculateTextMetrics(uint8_t fontwidth, uint8_t fontheight, int8_t heightOffset = 0, From 7e2ac13abdaf939c58e2088c8acbffc2243ffa27 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 1 May 2022 22:23:07 +0200 Subject: [PATCH 006/113] [P123] Add support for button-simulation using AdaGFX btn subcommand --- src/_P123_FT62x6Touch.ino | 36 +- src/src/PluginStructs/P123_data_struct.cpp | 947 ++++++++++++++------- src/src/PluginStructs/P123_data_struct.h | 158 ++-- 3 files changed, 761 insertions(+), 380 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index 9bc56694c3..d9573ebbfc 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -1,11 +1,16 @@ #ifdef USES_P123 // ####################################################################################################### -// #################################### Plugin 123: FT6206 Touchscreen ################################# +// #################################### Plugin 123: FT6206 Touchscreen ################################### // ####################################################################################################### /** * Changelog: + * 2022-04-30 tonhuisman: Add support for AdaGFX btn subcommand use and (local) button groups + * Start preparations for refactoring touch objects into separate helper class + * 2022-04-25 tonhuisman: Code cleanup, initialize object event -2 for disabled objects, -1 for enabled objects + * Add on and off subcommands to switch a touchbutton object. Generate init-event for enable + * disable, on and off states when init events option enabled * 2022-04-24 tonhuisman: Code improvements, increased button response speed * 2022-04-24 tonhuisman: Add event arguments for OnOff button objects, fix addLog statements, minor improvements * 2022-04-23 tonhuisman: Rename struct TS_Point in FT6206 library to FT_Point to avoid conflict with XPT2048 library (P099) @@ -19,6 +24,11 @@ * touch,flip,<0|1> : Set rotation normal(0) or flipped by 180 degrees(1) * touch,enable, : Enables a disabled objectname (removes a leading underscore) * touch,disable, : Disables an enabled objectname (adds a leading underscore) + * touch,on, : Switch a TouchButton on (must be enabled) + * touch,off, : Switch a TouchButton off (must be enabled) + * touch,setgrp, : Switch to button group + * touch,incgrp : Switch to next button group + * touch,decgrp : Switch to previous button group */ #define PLUGIN_123 @@ -230,17 +240,27 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) P123_data->setRotation(rot_); success = true; - } else if (subcommand.equals(F("flip"))) { // touch,flip,<0|1> : Flip rotation by 0 or 180 degrees - bool flip_ = (event->Par2 > 0); - - P123_data->setRotationFlipped(flip_); + } else if (subcommand.equals(F("flip"))) { // touch,flip,<0|1> : Flip rotation by 0 or 180 degrees + P123_data->setRotationFlipped(event->Par2 > 0); success = true; } else if (subcommand.equals(F("enable"))) { // touch,enable, : Enables a disabled objectname arguments = parseString(string, 3); - success = P123_data->setTouchObjectState(arguments, true); + success = P123_data->setTouchObjectState(event, arguments, true); } else if (subcommand.equals(F("disable"))) { // touch,disable, : Disables an enabled objectname arguments = parseString(string, 3); - success = P123_data->setTouchObjectState(arguments, false); + success = P123_data->setTouchObjectState(event, arguments, false); + } else if (subcommand.equals(F("on"))) { // touch,on, : Switch a TouchButton on + arguments = parseString(string, 3); + success = P123_data->setTouchButtonOnOff(event, arguments, true); + } else if (subcommand.equals(F("off"))) { // touch,off, : Switch a TouchButton off + arguments = parseString(string, 3); + success = P123_data->setTouchButtonOnOff(event, arguments, false); + } else if (subcommand.equals(F("setgrp"))) { // touch,setgrp, : Activate button group + success = P123_data->setButtonGroup(event, event->Par2); + } else if (subcommand.equals(F("incgrp"))) { // touch,incgrp : increment group and Activate + success = P123_data->incrementButtonGroup(event); + } else if (subcommand.equals(F("decgrp"))) { // touch,decgrp : Decrement group and Activate + success = P123_data->decrementButtonGroup(event); } } } @@ -255,7 +275,7 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) return success; } - success = P123_data->plugin_ten_per_second(event); + success = P123_data->plugin_fifty_per_second(event); break; } diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index f4ca844e89..3ea29a8b39 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -12,16 +12,16 @@ # include "../Commands/InternalCommands.h" /**************************************************************************** - * toString: Display-value for the button selected + * toString: Display-value for the touch action ***************************************************************************/ # ifdef P123_USE_EXTENDED_TOUCH -const __FlashStringHelper* toString(Button_type_e button) { - switch (button) { - case Button_type_e::None: return F("None"); - case Button_type_e::Square: return F("Square"); - case Button_type_e::Rounded: return F("Rounded"); - case Button_type_e::Circle: return F("Circle"); - case Button_type_e::Button_MAX: break; +const __FlashStringHelper* toString(P123_touch_action_e action) { + switch (action) { + case P123_touch_action_e::Default: return F("Default"); + case P123_touch_action_e::ActivateGroup: return F("Activate Group"); + case P123_touch_action_e::IncrementGroup: return F("Next Group"); + case P123_touch_action_e::DecrementGroup: return F("Previous Group"); + case P123_touch_action_e::TouchAction_MAX: break; } return F("Unsupported!"); } @@ -80,13 +80,21 @@ bool P123_data_struct::init(const EventStruct *event, if (bitRead(P123_Settings.flags, P123_FLAGS_SEND_OBJECTNAME) && bitRead(P123_Settings.flags, P123_FLAGS_INIT_OBJECTEVENT)) { - for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { - if (!TouchObjects[objectNr].objectName.isEmpty() - && bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED) - && bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTON)) { - generateObjectEvent(event, objectNr, -1); - } + if (_maxButtonGroup > 0) { // Multiple groups? + displayButtons(event, _buttonGroup, -3); // Clear all groups } + _buttonGroup = get8BitFromUL(P123_Settings.flags, P123_FLAGS_INITIAL_GROUP); + # ifdef PLUGIN_123_DEBUG + String log = F("P123 DEBUG group: "); + log += _buttonGroup; + log += F(", max group: "); + log += _maxButtonGroup; + addLogMove(LOG_LEVEL_INFO, log); + # endif // ifdef PLUGIN_123_DEBUG + displayButtons(event, _buttonGroup); // Initialize selected group and group 0 + # ifdef PLUGIN_123_DEBUG + addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG group done.")); + # endif // ifdef PLUGIN_123_DEBUG } # ifdef PLUGIN_123_DEBUG @@ -98,6 +106,72 @@ bool P123_data_struct::init(const EventStruct *event, return isInitialized(); } +/** + * mode: -2 = clear buttons in group, -3 = clear all buttongroups, -1 = draw buttons in group, 0 = initialize buttons + */ +void P123_data_struct::displayButtons(const EventStruct *event, + int8_t buttonGroup, + int8_t mode) { + for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { + int8_t state = 99; + int8_t group = get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_GROUP); + + if (!TouchObjects[objectNr].objectName.isEmpty() && + ((bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED) && (group == 0)) || (group > 0)) && + bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTON) && + ((group == buttonGroup) || + ((mode != -2) && (group == 0)) || + (mode == -3))) { + if (bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED)) { + if (mode == 0) { + state = -1; + } else { + if (bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_INVERTED)) { + state = TouchObjects[objectNr].TouchStates ? 0 : 1; // Act like an inverted button, 0 = On, 1 = Off + } else { + state = TouchObjects[objectNr].TouchStates ? 1 : 0; // Act like a button, 1 = On, 0 = Off + } + } + } else { + state = -2; + } + generateObjectEvent(event, objectNr, state, mode < 0, mode <= -2 ? -1 : 1); + } + # ifdef XX_PLUGIN_123_DEBUG + + // TODO: remove log? + String log = F("P123: button init, state: "); + log += state; + log += F(", group: "); + log += buttonGroup; + log += F(", mode: "); + log += mode; + log += F(", group: "); + log += get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_GROUP); + log += F(", en: "); + log += bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTON); + log += F(", object: "); + log += objectNr; + addLog(LOG_LEVEL_INFO, log); + # endif // ifdef PLUGIN_123_DEBUG + delay(0); + } + + // Send an event #Group,, with the selected group and the mode (-3..0) + String eventCommand; + eventCommand.reserve(24); + eventCommand += getTaskDeviceName(event->TaskIndex); + eventCommand += '#'; + eventCommand += F("Group"); + eventCommand += '='; // Add arguments + eventCommand += buttonGroup; + eventCommand += ','; + eventCommand += mode; + eventQueue.addMove(std::move(eventCommand)); + + delay(0); +} + /** * Properly initialized? then true */ @@ -108,9 +182,6 @@ bool P123_data_struct::isInitialized() const { int P123_data_struct::parseStringToInt(const String& string, uint8_t indexFind, char separator, int defaultValue) { String parsed = parseStringKeepCase(string, indexFind, separator); - // if (parsed.isEmpty()) { - // return defaultValue; - // } int result = defaultValue; validIntFromString(parsed, result); @@ -124,10 +195,10 @@ int P123_data_struct::parseStringToInt(const String& string, uint8_t indexFind, bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { addFormSubHeader(F("Touch configuration")); - addFormCheckBox(F("Flip rotation 180°"), F("p123_rotation_flipped"), bitRead(P123_Settings.flags, P123_FLAGS_ROTATION_FLIPPED)); + addFormCheckBox(F("Flip rotation 180°"), F("touch_rotation_flipped"), bitRead(P123_Settings.flags, P123_FLAGS_ROTATION_FLIPPED)); addFormNote(F("Some touchscreens are mounted 180° rotated on the display.")); - addFormNumericBox(F("Touch minimum pressure"), F("p123_treshold"), + addFormNumericBox(F("Touch minimum pressure"), F("touch_treshold"), P123_Settings.treshold, 0, 255); uint8_t choice3 = 0u; @@ -146,12 +217,12 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { F("Objectnames, X, Y and Z") }; int optionValues3[P123_EVENTS_OPTIONS] = { 0, 1, 3, 4, 5, 7 }; // Already used as a bitmap! - addFormSelector(F("Events"), F("p123_events"), P123_EVENTS_OPTIONS, options3, optionValues3, choice3); - addFormCheckBox(F("Initial Objectnames events"), F("p123_init_objectevent"), bitRead(P123_Settings.flags, P123_FLAGS_INIT_OBJECTEVENT)); + addFormSelector(F("Events"), F("touch_events"), P123_EVENTS_OPTIONS, options3, optionValues3, choice3); + addFormCheckBox(F("Initial Objectnames events"), F("touch_init_objectevent"), bitRead(P123_Settings.flags, P123_FLAGS_INIT_OBJECTEVENT)); addFormNote(F("Will send state -1 but only for enabled On/Off button objects.")); } - addFormCheckBox(F("Prevent duplicate events"), F("p123_deduplicate"), bitRead(P123_Settings.flags, P123_FLAGS_DEDUPLICATE)); + addFormCheckBox(F("Prevent duplicate events"), F("touch_deduplicate"), bitRead(P123_Settings.flags, P123_FLAGS_DEDUPLICATE)); if (!Settings.UseRules) { addFormNote(F("Tools / Advanced / Rules must be enabled for events to be fired.")); @@ -163,7 +234,7 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { const __FlashStringHelper *noYesOptions[2] = { F("No"), F("Yes") }; int noYesOptionValues[2] = { 0, 1 }; addFormSelector(F("Calibrate to screen resolution"), - F("p123_use_calibration"), + F("touch_use_calibration"), 2, noYesOptions, noYesOptionValues, @@ -184,24 +255,24 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { html_TR_TD(); addHtml(F("Top-left")); html_TD(); - addNumericBox(F("p123_cal_tl_x"), + addNumericBox(F("touch_cal_tl_x"), P123_Settings.top_left.x, 0, 65535); html_TD(); - addNumericBox(F("p123_cal_tl_y"), + addNumericBox(F("touch_cal_tl_y"), P123_Settings.top_left.y, 0, 65535); html_TD(); addHtml(F("Bottom-right")); html_TD(); - addNumericBox(F("p123_cal_br_x"), + addNumericBox(F("touch_cal_br_x"), P123_Settings.bottom_right.x, 0, 65535); html_TD(); - addNumericBox(F("p123_cal_br_y"), + addNumericBox(F("touch_cal_br_y"), P123_Settings.bottom_right.y, 0, 65535); @@ -211,57 +282,196 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { // addFormNote(F("At least 1 x/y value must be <> 0 to enable calibration.")); } - addFormCheckBox(F("Enable logging for calibration"), F("p123_log_calibration"), + addFormCheckBox(F("Enable logging for calibration"), F("touch_log_calibration"), P123_Settings.logEnabled); addFormSubHeader(F("Touch objects")); { - addRowLabel(F("Object")); - # ifdef P123_USE_EXTENDED_TOUCH - html_table(F("multirow"), false); // Sub-table - # else // ifdef P123_USE_EXTENDED_TOUCH - html_table(EMPTY_STRING, false); // Sub-table - # endif // ifdef P123_USE_EXTENDED_TOUCH - html_table_header(F(" # ")); - html_table_header(F("On")); - html_table_header(F("Objectname")); - html_table_header(F("Top-left x")); - html_table_header(F("Top-left y")); - html_table_header(F("On/Off button")); # ifdef P123_USE_EXTENDED_TOUCH + String parsed; + addRowLabel(F("Default On/Off button colors")); + html_table(EMPTY_STRING, false); // Sub-table html_table_header(F("ON color")); - html_table_header(F("ON caption")); - html_table_header(F("Buttontype")); - html_table_header(F("Background")); - # endif // ifdef P123_USE_EXTENDED_TOUCH - html_TR(); // New row - html_table_header(EMPTY_STRING); - html_table_header(EMPTY_STRING); - html_table_header(EMPTY_STRING); - html_table_header(F("Width")); - html_table_header(F("Height")); - html_table_header(F("Inverted")); - # ifdef P123_USE_EXTENDED_TOUCH html_table_header(F("OFF color")); - html_table_header(F("OFF caption")); + html_table_header(F("Border color")); html_table_header(F("Caption color")); - html_table_header(F("Highlight color")); + html_table_header(F("Disabled color")); + html_table_header(F("Disabled caption color")); + + html_TR_TD(); // ON color + parsed = AdaGFXcolorToString(P123_Settings.colorOn, _colorDepth, true); + addTextBox(getPluginCustomArgName(3000), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef P123_USE_TOOLTIPS + , F("ON color") + # endif // ifdef P123_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // OFF color + parsed = AdaGFXcolorToString(P123_Settings.colorOff, _colorDepth, true); + addTextBox(getPluginCustomArgName(3001), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef P123_USE_TOOLTIPS + , F("OFF color") + # endif // ifdef P123_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // Border color + parsed = AdaGFXcolorToString(P123_Settings.colorBorder, _colorDepth, true); + addTextBox(getPluginCustomArgName(3002), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef P123_USE_TOOLTIPS + , F("Border color") + # endif // ifdef P123_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // Caption color + parsed = AdaGFXcolorToString(P123_Settings.colorCaption, _colorDepth, true); + addTextBox(getPluginCustomArgName(3003), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef P123_USE_TOOLTIPS + , F("Caption color") + # endif // ifdef P123_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // Disabled color + parsed = AdaGFXcolorToString(P123_Settings.colorDisabled, _colorDepth, true); + addTextBox(getPluginCustomArgName(3004), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef P123_USE_TOOLTIPS + , F("Disabled color") + # endif // ifdef P123_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // Disabled caption color + parsed = AdaGFXcolorToString(P123_Settings.colorDisabledCaption, _colorDepth, true); + addTextBox(getPluginCustomArgName(3005), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef P123_USE_TOOLTIPS + , F("Disabled caption color") + # endif // ifdef P123_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_end_table(); + # endif // ifdef P123_USE_EXTENDED_TOUCH + } + { + # ifdef P123_USE_EXTENDED_TOUCH + addFormNumericBox(F("Initial button group"), F("touch_initial_group"), + get8BitFromUL(P123_Settings.flags, P123_FLAGS_INITIAL_GROUP), 0, P123_MAX_BUTTON_GROUPS + # ifdef P123_USE_TOOLTIPS + , F("Initial group") + # endif // ifdef P123_USE_TOOLTIPS + ); # endif // ifdef P123_USE_EXTENDED_TOUCH + } + { + addRowLabel(F("Object")); + { + html_table(EMPTY_STRING, false); // Sub-table + html_table_header(F(" # ")); + html_table_header(F("On")); + html_table_header(F("Objectname")); + html_table_header(F("Top-left x")); + html_table_header(F("Top-left y")); + # ifdef P123_USE_EXTENDED_TOUCH + html_table_header(F("Button")); + html_table_header(F("Layout")); + html_table_header(F("ON color")); + html_table_header(F("ON caption")); + html_table_header(F("Border color")); + html_table_header(F("Disab. cap. clr")); + html_table_header(F("Touch action")); + # else // ifdef P123_USE_EXTENDED_TOUCH + html_table_header(F("On/Off button")); + # endif // ifdef P123_USE_EXTENDED_TOUCH + html_TR(); // New row + html_table_header(EMPTY_STRING); + html_table_header(EMPTY_STRING); + # ifdef P123_USE_EXTENDED_TOUCH + html_table_header(F("Button-group")); + # else // ifdef P123_USE_EXTENDED_TOUCH + html_table_header(EMPTY_STRING); + # endif // ifdef P123_USE_EXTENDED_TOUCH + html_table_header(F("Width")); + html_table_header(F("Height")); + html_table_header(F("Inverted")); + # ifdef P123_USE_EXTENDED_TOUCH + html_table_header(F("Font scale")); + html_table_header(F("OFF color")); + html_table_header(F("OFF caption")); + html_table_header(F("Caption color")); + html_table_header(F("Disabled clr")); + html_table_header(F("Action group")); + # endif // ifdef P123_USE_EXTENDED_TOUCH + } # ifdef P123_USE_EXTENDED_TOUCH const __FlashStringHelper *buttonTypeOptions[] = { toString(Button_type_e::None), toString(Button_type_e::Square), toString(Button_type_e::Rounded), - toString(Button_type_e::Circle) + toString(Button_type_e::Circle), + toString(Button_type_e::ArrowLeft), + toString(Button_type_e::ArrowUp), + toString(Button_type_e::ArrowRight), + toString(Button_type_e::ArrowDown), }; - const int buttonTypeValues[] = { + + int buttonTypeValues[] = { static_cast(Button_type_e::None), static_cast(Button_type_e::Square), static_cast(Button_type_e::Rounded), - static_cast(Button_type_e::Circle) + static_cast(Button_type_e::Circle), + static_cast(Button_type_e::ArrowLeft), + static_cast(Button_type_e::ArrowUp), + static_cast(Button_type_e::ArrowRight), + static_cast(Button_type_e::ArrowDown), + }; + + const __FlashStringHelper *buttonLayoutOptions[] = { + toString(Button_layout_e::CenterAligned), + toString(Button_layout_e::LeftAligned), + toString(Button_layout_e::TopAligned), + toString(Button_layout_e::RightAligned), + toString(Button_layout_e::BottomAligned), + toString(Button_layout_e::LeftTopAligned), + toString(Button_layout_e::RightTopAligned), + toString(Button_layout_e::LeftBottomAligned), + toString(Button_layout_e::RightBottomAligned), + toString(Button_layout_e::NoCaption), + toString(Button_layout_e::Bitmap), + }; + + const int buttonLayoutValues[] = { + static_cast(Button_layout_e::CenterAligned), + static_cast(Button_layout_e::LeftAligned), + static_cast(Button_layout_e::TopAligned), + static_cast(Button_layout_e::RightAligned), + static_cast(Button_layout_e::BottomAligned), + static_cast(Button_layout_e::LeftTopAligned), + static_cast(Button_layout_e::RightTopAligned), + static_cast(Button_layout_e::LeftBottomAligned), + static_cast(Button_layout_e::RightBottomAligned), + static_cast(Button_layout_e::NoCaption), + static_cast(Button_layout_e::Bitmap), + }; + + const __FlashStringHelper *touchActionOptions[] = { + toString(P123_touch_action_e::Default), + toString(P123_touch_action_e::ActivateGroup), + toString(P123_touch_action_e::IncrementGroup), + toString(P123_touch_action_e::DecrementGroup), }; + + const int touchActionValues[] = { + static_cast(P123_touch_action_e::Default), + static_cast(P123_touch_action_e::ActivateGroup), + static_cast(P123_touch_action_e::IncrementGroup), + static_cast(P123_touch_action_e::DecrementGroup), + }; + # endif // ifdef P123_USE_EXTENDED_TOUCH uint8_t maxIdx = std::min(static_cast(TouchObjects.size() + P123_EXTRA_OBJECT_COUNT), P123_MAX_OBJECT_COUNT); @@ -303,12 +513,36 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { # endif // ifdef P123_USE_TOOLTIPS ); html_TD(); // on/off button + # ifdef P123_USE_EXTENDED_TOUCH + addSelector(getPluginCustomArgName(objectNr + 800), + static_cast(Button_type_e::Button_MAX), + buttonTypeOptions, + buttonTypeValues, + nullptr, + get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTONTYPE) & 0x0F, false, true, F("widenumber") + # ifdef P123_USE_TOOLTIPS + , F("Buttontype") + # endif // ifdef P123_USE_TOOLTIPS + ); + html_TD(); // button alignment + addSelector(getPluginCustomArgName(objectNr + 900), + static_cast(Button_layout_e::Alignment_MAX), + buttonLayoutOptions, + buttonLayoutValues, + nullptr, + get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTONTYPE) & 0xF0, false, true, F("widenumber") + # ifdef P123_USE_TOOLTIPS + , F("Button alignment") + # endif // ifdef P123_USE_TOOLTIPS + ); + # else // ifdef P123_USE_EXTENDED_TOUCH addCheckBox(getPluginCustomArgName(objectNr + 600), bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTON), false - # ifdef P123_USE_TOOLTIPS + # ifdef P123_USE_TOOLTIPS , F("On/Off button") - # endif // ifdef P123_USE_TOOLTIPS + # endif // ifdef P123_USE_TOOLTIPS ); + # endif // ifdef P123_USE_EXTENDED_TOUCH # ifdef P123_USE_EXTENDED_TOUCH html_TD(); // ON color parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorOn, _colorDepth, true); @@ -331,32 +565,54 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { , F("ON caption") # endif // ifdef P123_USE_TOOLTIPS ); - html_TD(); // button-type - addSelector(getPluginCustomArgName(objectNr + 800), - static_cast(Button_type_e::Button_MAX), - buttonTypeOptions, - buttonTypeValues, - nullptr, - get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTONTYPE), false, true, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("Buttontype") - # endif // ifdef P123_USE_TOOLTIPS - ); - html_TD(); // Background color - parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorBackground, _colorDepth, true); + html_TD(); // Border color + parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorBorder, _colorDepth, true); addTextBox(getPluginCustomArgName(objectNr + 1700), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, EMPTY_STRING, F("widenumber") # ifdef P123_USE_TOOLTIPS - , F("Background color") + , F("Border color") # endif // ifdef P123_USE_TOOLTIPS , F("adagfx65kcolors") ); + html_TD(); // Disabled caption color + parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorDisabledCaption, _colorDepth, true); + addTextBox(getPluginCustomArgName(objectNr + 1900), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef P123_USE_TOOLTIPS + , F("Disabled caption color") + # endif // ifdef P123_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // button action + addSelector(getPluginCustomArgName(objectNr + 2000), + static_cast(P123_touch_action_e::TouchAction_MAX), + touchActionOptions, + touchActionValues, + nullptr, + get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ACTIONGROUP) & 0xC0, false, true, F("widenumber") + # ifdef P123_USE_TOOLTIPS + , F("Touch action") + # endif // ifdef P123_USE_TOOLTIPS + ); # endif // ifdef P123_USE_EXTENDED_TOUCH html_TR_TD(); // Start new row - html_TD(3); // Start with some blank columns - // html_TD(); + html_TD(2); // Start with some blank columns + # ifdef P123_USE_EXTENDED_TOUCH + { + String buttonGroupToolTip = F("Button-group [0.."); + buttonGroupToolTip += P123_MAX_BUTTON_GROUPS; + buttonGroupToolTip += ']'; + addNumericBox(getPluginCustomArgName(objectNr + 1600), + get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_GROUP), 0, P123_MAX_BUTTON_GROUPS + # ifdef P123_USE_TOOLTIPS + , F("widenumber"), buttonGroupToolTip + # endif // ifdef P123_USE_TOOLTIPS + ); + } + # endif // ifdef P123_USE_EXTENDED_TOUCH + html_TD(); // Next column // Width addNumericBox(getPluginCustomArgName(objectNr + 400), TouchObjects[objectNr].width_height.x, 0, 65535 @@ -371,24 +627,6 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { , F("widenumber"), F("Height") # endif // ifdef P123_USE_TOOLTIPS ); - - # ifdef P123_USE_EXTENDED_TOUCH - - // Colored - // addCheckBox(getPluginCustomArgName(objectNr + 900), - // bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_COLORED), false - // # ifdef P123_USE_TOOLTIPS - // , F("Colored") - // # endif // ifdef P123_USE_TOOLTIPS - // ); - // html_TD(); // Use caption - // addCheckBox(getPluginCustomArgName(objectNr + 1200), - // bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_CAPTION), false - // # ifdef P123_USE_TOOLTIPS - // , F("Use Caption") - // # endif // ifdef P123_USE_TOOLTIPS - // ); - # endif // ifdef P123_USE_EXTENDED_TOUCH html_TD(); // inverted addCheckBox(getPluginCustomArgName(objectNr + 700), bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_INVERTED), false @@ -397,6 +635,13 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { # endif // ifdef P123_USE_TOOLTIPS ); # ifdef P123_USE_EXTENDED_TOUCH + html_TD(); // font scale + addNumericBox(getPluginCustomArgName(objectNr + 1200), + get4BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_FONTSCALE), 1, 10 + # ifdef P123_USE_TOOLTIPS + , F("widenumber"), F("Font scaling [1x..10x]") + # endif // ifdef P123_USE_TOOLTIPS + ); html_TD(); // OFF color parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorOff, _colorDepth, true); addTextBox(getPluginCustomArgName(objectNr + 1100), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, @@ -427,20 +672,27 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { # endif // ifdef P123_USE_TOOLTIPS , F("adagfx65kcolors") ); - html_TD(); // Highlight color - parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorHighlight, _colorDepth, true); - addTextBox(getPluginCustomArgName(objectNr + 1600), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, + html_TD(); // Disabled color + parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorDisabled, _colorDepth, true); + addTextBox(getPluginCustomArgName(objectNr + 1800), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, EMPTY_STRING, F("widenumber") # ifdef P123_USE_TOOLTIPS - , F("Highlight color") + , F("Disabled color") # endif // ifdef P123_USE_TOOLTIPS , F("adagfx65kcolors") ); + html_TD(); // Action Group + addNumericBox(getPluginCustomArgName(objectNr + 2100), + get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ACTIONGROUP) & 0x3F, 0, P123_MAX_BUTTON_GROUPS + # ifdef P123_USE_TOOLTIPS + , F("widenumber"), F("Action group") + # endif // ifdef P123_USE_TOOLTIPS + ); # endif // ifdef P123_USE_EXTENDED_TOUCH } html_end_table(); - addFormNumericBox(F("Debounce delay for On/Off buttons"), F("p123_debounce"), + addFormNumericBox(F("Debounce delay for On/Off buttons"), F("touch_debounce"), P123_Settings.debounceMs, 0, 255); addUnit(F("0-255 msec.")); } @@ -453,35 +705,61 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { String config; + # ifdef P123_USE_EXTENDED_TOUCH + String colorInput; + # endif // ifdef P123_USE_EXTENDED_TOUCH config.reserve(80); uint32_t lSettings = 0u; - bitWrite(lSettings, P123_FLAGS_SEND_XY, bitRead(getFormItemInt(F("p123_events")), P123_FLAGS_SEND_XY)); - bitWrite(lSettings, P123_FLAGS_SEND_Z, bitRead(getFormItemInt(F("p123_events")), P123_FLAGS_SEND_Z)); - bitWrite(lSettings, P123_FLAGS_SEND_OBJECTNAME, bitRead(getFormItemInt(F("p123_events")), P123_FLAGS_SEND_OBJECTNAME)); - bitWrite(lSettings, P123_FLAGS_ROTATION_FLIPPED, isFormItemChecked(F("p123_rotation_flipped"))); - bitWrite(lSettings, P123_FLAGS_DEDUPLICATE, isFormItemChecked(F("p123_deduplicate"))); - bitWrite(lSettings, P123_FLAGS_INIT_OBJECTEVENT, isFormItemChecked(F("p123_init_objectevent"))); - - config += getFormItemInt(F("p123_use_calibration")); + bitWrite(lSettings, P123_FLAGS_SEND_XY, bitRead(getFormItemInt(F("touch_events")), P123_FLAGS_SEND_XY)); + bitWrite(lSettings, P123_FLAGS_SEND_Z, bitRead(getFormItemInt(F("touch_events")), P123_FLAGS_SEND_Z)); + bitWrite(lSettings, P123_FLAGS_SEND_OBJECTNAME, bitRead(getFormItemInt(F("touch_events")), P123_FLAGS_SEND_OBJECTNAME)); + bitWrite(lSettings, P123_FLAGS_ROTATION_FLIPPED, isFormItemChecked(F("touch_rotation_flipped"))); + bitWrite(lSettings, P123_FLAGS_DEDUPLICATE, isFormItemChecked(F("touch_deduplicate"))); + bitWrite(lSettings, P123_FLAGS_INIT_OBJECTEVENT, isFormItemChecked(F("touch_init_objectevent"))); + # ifdef P123_USE_EXTENDED_TOUCH + set8BitToUL(lSettings, P123_FLAGS_INITIAL_GROUP, getFormItemInt(F("touch_initial_group"))); // Button group + # endif // ifdef P123_USE_EXTENDED_TOUCH + + config += getFormItemInt(F("touch_use_calibration")); config += P123_SETTINGS_SEPARATOR; - config += isFormItemChecked(F("p123_log_calibration")) ? 1 : 0; + config += isFormItemChecked(F("touch_log_calibration")) ? 1 : 0; config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("p123_cal_tl_x")); + config += getFormItemInt(F("touch_cal_tl_x")); config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("p123_cal_tl_y")); + config += getFormItemInt(F("touch_cal_tl_y")); config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("p123_cal_br_x")); + config += getFormItemInt(F("touch_cal_br_x")); config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("p123_cal_br_y")); + config += getFormItemInt(F("touch_cal_br_y")); config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("p123_treshold")); + config += getFormItemInt(F("touch_debounce")); config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("p123_debounce")); + config += getFormItemInt(F("touch_treshold")); config += P123_SETTINGS_SEPARATOR; config += lSettings; config += P123_SETTINGS_SEPARATOR; + # ifdef P123_USE_EXTENDED_TOUCH + colorInput = webArg(getPluginCustomArgName(3000)); // Default Color ON + config += AdaGFXparseColor(colorInput, _colorDepth); + config += P123_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(3001)); // Default Color OFF + config += AdaGFXparseColor(colorInput, _colorDepth, false); + config += P123_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(3002)); // Default Color Border + config += AdaGFXparseColor(colorInput, _colorDepth, false); + config += P123_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(3003)); // Default Color caption + config += AdaGFXparseColor(colorInput, _colorDepth, false); + config += P123_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(3004)); // Default Disabled Color + config += AdaGFXparseColor(colorInput, _colorDepth); + config += P123_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(3005)); // Default Disabled Caption Color + config += AdaGFXparseColor(colorInput, _colorDepth, false); + config += P123_SETTINGS_SEPARATOR; + # endif // ifdef P123_USE_EXTENDED_TOUCH settingsArray[P123_CALIBRATION_START] = config; { @@ -506,44 +784,55 @@ bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { config += P123_SETTINGS_SEPARATOR; uint32_t flags = 0u; bitWrite(flags, P123_OBJECT_FLAG_ENABLED, isFormItemChecked(getPluginCustomArgName(objectNr + 0))); // Enabled - bitWrite(flags, P123_OBJECT_FLAG_BUTTON, isFormItemChecked(getPluginCustomArgName(objectNr + 600))); // On/Off button bitWrite(flags, P123_OBJECT_FLAG_INVERTED, isFormItemChecked(getPluginCustomArgName(objectNr + 700))); // Inverted # ifdef P123_USE_EXTENDED_TOUCH - - // bitWrite(flags, P123_OBJECT_FLAG_COLORED, isFormItemChecked(getPluginCustomArgName(objectNr + 900))); // Colored - // bitWrite(flags, P123_OBJECT_FLAG_CAPTION, isFormItemChecked(getPluginCustomArgName(objectNr + 1200))); // Use caption - set8BitToUL(flags, P123_OBJECT_FLAG_BUTTONTYPE, getFormItemInt(getPluginCustomArgName(objectNr + 800))); // Buttontype + uint8_t buttonType = getFormItemInt(getPluginCustomArgName(objectNr + 800)); + uint8_t buttonLayout = getFormItemInt(getPluginCustomArgName(objectNr + 900)); + set8BitToUL(flags, P123_OBJECT_FLAG_BUTTONTYPE, buttonType | buttonLayout); // Buttontype + bitWrite(flags, P123_OBJECT_FLAG_BUTTON, (static_cast(buttonType & 0x07) != Button_type_e::None)); // On/Off button + uint8_t buttonAction = getFormItemInt(getPluginCustomArgName(objectNr + 2000)); + uint8_t buttonSelectGroup = getFormItemInt(getPluginCustomArgName(objectNr + 2100)); + set8BitToUL(flags, P123_OBJECT_FLAG_ACTIONGROUP, buttonAction | buttonSelectGroup); // ButtonAction + uint8_t fontScale = getFormItemInt(getPluginCustomArgName(objectNr + 1200)); + set4BitToUL(flags, P123_OBJECT_FLAG_FONTSCALE, fontScale); // Font scaling + uint8_t buttonGroup = getFormItemInt(getPluginCustomArgName(objectNr + 1600)); + set8BitToUL(flags, P123_OBJECT_FLAG_GROUP, buttonGroup); // Button group + # else // ifdef P123_USE_EXTENDED_TOUCH + bitWrite(flags, P123_OBJECT_FLAG_BUTTON, isFormItemChecked(getPluginCustomArgName(objectNr + 600))); // On/Off button # endif // ifdef P123_USE_EXTENDED_TOUCH - config += flags; // Flags + + config += String(flags); // Flags config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(getPluginCustomArgName(objectNr + 200)); // Top x + config += getFormItemInt(getPluginCustomArgName(objectNr + 200)); // Top x config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(getPluginCustomArgName(objectNr + 300)); // Top y + config += getFormItemInt(getPluginCustomArgName(objectNr + 300)); // Top y config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(getPluginCustomArgName(objectNr + 400)); // Bottom x + config += getFormItemInt(getPluginCustomArgName(objectNr + 400)); // Bottom x config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(getPluginCustomArgName(objectNr + 500)); // Bottom y + config += getFormItemInt(getPluginCustomArgName(objectNr + 500)); // Bottom y config += P123_SETTINGS_SEPARATOR; # ifdef P123_USE_EXTENDED_TOUCH - String colorInput; - colorInput = webArg(getPluginCustomArgName(objectNr + 1000)); // Color ON + colorInput = webArg(getPluginCustomArgName(objectNr + 1000)); // Color ON config += AdaGFXparseColor(colorInput, _colorDepth, true); config += P123_SETTINGS_SEPARATOR; - colorInput = webArg(getPluginCustomArgName(objectNr + 1100)); // Color OFF + colorInput = webArg(getPluginCustomArgName(objectNr + 1100)); // Color OFF config += AdaGFXparseColor(colorInput, _colorDepth, true); config += P123_SETTINGS_SEPARATOR; - colorInput = webArg(getPluginCustomArgName(objectNr + 1500)); // Color caption + colorInput = webArg(getPluginCustomArgName(objectNr + 1500)); // Color caption config += AdaGFXparseColor(colorInput, _colorDepth, true); + config += P123_SETTINGS_SEPARATOR; // Caption ON + config += wrapWithQuotesIfContainsParameterSeparatorChar(webArg(getPluginCustomArgName(objectNr + 1300))); + config += P123_SETTINGS_SEPARATOR; // Caption OFF + config += wrapWithQuotesIfContainsParameterSeparatorChar(webArg(getPluginCustomArgName(objectNr + 1400))); config += P123_SETTINGS_SEPARATOR; - colorInput = webArg(getPluginCustomArgName(objectNr + 1600)); // Color Highlight + colorInput = webArg(getPluginCustomArgName(objectNr + 1700)); // Color Border config += AdaGFXparseColor(colorInput, _colorDepth, true); config += P123_SETTINGS_SEPARATOR; - config += enquoteString(webArg(getPluginCustomArgName(objectNr + 1300))); // Caption ON - config += P123_SETTINGS_SEPARATOR; - config += enquoteString(webArg(getPluginCustomArgName(objectNr + 1400))); // Caption OFF + colorInput = webArg(getPluginCustomArgName(objectNr + 1800)); // Disabled Color + config += AdaGFXparseColor(colorInput, _colorDepth, true); config += P123_SETTINGS_SEPARATOR; - colorInput = webArg(getPluginCustomArgName(objectNr + 1700)); // Color Background + colorInput = webArg(getPluginCustomArgName(objectNr + 1900)); // Disabled Caption Color config += AdaGFXparseColor(colorInput, _colorDepth, true); config += P123_SETTINGS_SEPARATOR; # endif // ifdef P123_USE_EXTENDED_TOUCH @@ -558,7 +847,8 @@ bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { # ifdef PLUGIN_123_DEBUG - if (loglevelActiveFor(LOG_LEVEL_INFO)) { + if (loglevelActiveFor(LOG_LEVEL_INFO) && + !config.isEmpty()) { String log = F("Save object #"); log += objectNr; log += F(" settings: "); @@ -585,7 +875,7 @@ bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { /** * Every 10th second we check if the screen is touched */ -bool P123_data_struct::plugin_ten_per_second(struct EventStruct *event) { +bool P123_data_struct::plugin_fifty_per_second(struct EventStruct *event) { bool success = false; if (isInitialized()) { @@ -622,7 +912,6 @@ bool P123_data_struct::plugin_ten_per_second(struct EventStruct *event) { log += x; log += F(", y= "); log += y; - log += F("; ox= "); log += ox; log += F(", oy= "); @@ -697,7 +986,9 @@ bool P123_data_struct::plugin_ten_per_second(struct EventStruct *event) { **************************************************************************/ void P123_data_struct::generateObjectEvent(const EventStruct *event, const int8_t objectIndex, - const int8_t onOffState) { + const int8_t onOffState, + const bool groupSwitch, + const int8_t factor) { String eventCommand; eventCommand.reserve(48); @@ -706,9 +997,9 @@ void P123_data_struct::generateObjectEvent(const EventStruct *event, eventCommand += TouchObjects[objectIndex].objectName; eventCommand += '='; // Add arguments - if (onOffState < 0) { // Negative value: pass on unaltered - eventCommand += onOffState; - } else { // Check for inverted output + if (onOffState < 0) { // Negative value: pass on unaltered (1) + eventCommand += onOffState; // (%eventvalue#%) + } else { // Check for inverted output (1) if (bitRead(TouchObjects[objectIndex].flags, P123_OBJECT_FLAG_INVERTED)) { eventCommand += onOffState == 1 ? '0' : '1'; // Act like an inverted button, 0 = On, 1 = Off } else { @@ -716,130 +1007,92 @@ void P123_data_struct::generateObjectEvent(const EventStruct *event, } } - if (P123_CONFIG_DISPLAY_TASK != event->TaskIndex) { // Add arguments for display + if (P123_CONFIG_DISPLAY_TASK != event->TaskIndex) { // Add arguments for display eventCommand += ','; - eventCommand += TouchObjects[objectIndex].top_left.x; + eventCommand += TouchObjects[objectIndex].top_left.x; // (2) eventCommand += ','; - eventCommand += TouchObjects[objectIndex].top_left.y; + eventCommand += TouchObjects[objectIndex].top_left.y; // (3) eventCommand += ','; - eventCommand += TouchObjects[objectIndex].width_height.x; + eventCommand += TouchObjects[objectIndex].width_height.x; // (4) eventCommand += ','; - eventCommand += TouchObjects[objectIndex].width_height.y; + eventCommand += TouchObjects[objectIndex].width_height.y; // (5) eventCommand += ','; - eventCommand += objectIndex + 1; // Adjust to displayed index - eventCommand += ','; - eventCommand += P123_CONFIG_DISPLAY_TASK + 1; // What TaskIndex? - } - eventQueue.addMove(std::move(eventCommand)); -} - -/** - * draw a button using the mode and state - * //TODO: Complete implementation - * will probably need: - * - Access to the AdafruitGFX_Helper object - * - Access to the Display object in the AdafruitGFX_Helper - * - Access to the list of available fonts, to be able to change the font for button captions, and to center the caption on the button - */ + eventCommand += objectIndex + 1; // Adjust to displayed index (6) + eventCommand += ','; // (7) + eventCommand += get8BitFromUL(TouchObjects[objectIndex].flags, P123_OBJECT_FLAG_BUTTONTYPE) * factor; # ifdef P123_USE_EXTENDED_TOUCH -void P123_data_struct::drawButton(DrawButtonMode_e buttonMode, - int8_t buttonIndex, - const EventStruct *event) { - if ((buttonIndex < 0) || (buttonIndex >= static_cast(TouchObjects.size()))) { return; } // Selfprotection - - if (!Settings.TaskDeviceEnabled[_displayTask]) { return; } // No active DisplayTask is no drawing buttons - - Button_type_e bType = static_cast(get8BitFromUL(TouchObjects[buttonIndex].flags, P123_OBJECT_FLAG_BUTTONTYPE)); - String cmdPrefix; - String btnDrawShape; - int8_t xa = 0, ya = 0, wa = 0, ha = 0; - - cmdPrefix.reserve(30); - cmdPrefix = getTaskDeviceName(_displayTask); - cmdPrefix += '.'; // a period - cmdPrefix += ADAGFX_UNIVERSAL_TRIGGER; - cmdPrefix += ','; - - btnDrawShape.reserve(50); - - if (bType != Button_type_e::None) { - switch (buttonMode) { - case DrawButtonMode_e::Initialize: - break; - case DrawButtonMode_e::State: - { - xa = 1; ya = 1; - wa = -2; ha = -2; - break; + eventCommand += ','; // (8) + eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorOn == 0 + ? P123_Settings.colorOn + : TouchObjects[objectIndex].colorOn, + _colorDepth); + eventCommand += ','; // (9) + eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorOff == 0 + ? P123_Settings.colorOff + : TouchObjects[objectIndex].colorOff, + _colorDepth); + eventCommand += ','; // (10) + eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorCaption == 0 + ? P123_Settings.colorCaption + : TouchObjects[objectIndex].colorCaption, + _colorDepth); + eventCommand += ','; // (11) + eventCommand += get4BitFromUL(TouchObjects[objectIndex].flags, P123_OBJECT_FLAG_FONTSCALE); + eventCommand += ','; // (12) + eventCommand += wrapWithQuotesIfContainsParameterSeparatorChar(TouchObjects[objectIndex].captionOn.isEmpty() ? + TouchObjects[objectIndex].objectName : + TouchObjects[objectIndex].captionOn); + eventCommand += ','; // (13) + eventCommand += wrapWithQuotesIfContainsParameterSeparatorChar(TouchObjects[objectIndex].captionOff.isEmpty() ? + TouchObjects[objectIndex].objectName : + TouchObjects[objectIndex].captionOff); + eventCommand += ','; // (14) + eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorBorder == 0 + ? P123_Settings.colorBorder + : TouchObjects[objectIndex].colorBorder, + _colorDepth); + eventCommand += ','; // (15) + eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorDisabled == 0 + ? P123_Settings.colorDisabled + : TouchObjects[objectIndex].colorDisabled, + _colorDepth); + eventCommand += ','; // (16) + eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorDisabledCaption == 0 + ? P123_Settings.colorDisabledCaption + : TouchObjects[objectIndex].colorDisabledCaption, + _colorDepth); + # endif // ifdef P123_USE_EXTENDED_TOUCH + eventCommand += ','; + eventCommand += P123_CONFIG_DISPLAY_TASK + 1; // What TaskIndex? (17) or (8) + eventCommand += ','; // Group (18) or (9) + eventCommand += get8BitFromUL(TouchObjects[objectIndex].flags, P123_OBJECT_FLAG_GROUP); + eventCommand += ','; // Select Group (19) or (10) + uint8_t action = get8BitFromUL(TouchObjects[objectIndex].flags, P123_OBJECT_FLAG_ACTIONGROUP); + + if (!groupSwitch && (static_cast(action & 0xC0) != P123_touch_action_e::Default)) { + switch (static_cast(action & 0xC0)) { + case P123_touch_action_e::ActivateGroup: + eventCommand += action & 0x3F; + break; + case P123_touch_action_e::IncrementGroup: + eventCommand += -2; + break; + case P123_touch_action_e::DecrementGroup: + eventCommand += -3; + break; + case P123_touch_action_e::Default: + case P123_touch_action_e::TouchAction_MAX: + eventCommand += -1; // Ignore + break; } - case DrawButtonMode_e::Highlight: - break; + } else { + eventCommand += -1; // No group to activate } } - - switch (bType) { - case Button_type_e::None: - break; - case Button_type_e::Square: - btnDrawShape = F("r"); - break; - case Button_type_e::Rounded: - btnDrawShape = F("rr"); - break; - case Button_type_e::Circle: - btnDrawShape = F("c"); - break; - case Button_type_e::Button_MAX: - break; - } - - if ((bType != Button_type_e::None) && - (buttonMode == DrawButtonMode_e::Initialize)) { btnDrawShape += 'f'; } - - switch (bType) { - case Button_type_e::None: - break; - case Button_type_e::Square: - btnDrawShape += ','; - btnDrawShape += TouchObjects[buttonIndex].top_left.x + xa; - btnDrawShape += ','; - btnDrawShape += TouchObjects[buttonIndex].top_left.y + ya; - btnDrawShape += ','; - btnDrawShape += TouchObjects[buttonIndex].width_height.x + wa; - btnDrawShape += ','; - btnDrawShape += TouchObjects[buttonIndex].width_height.y + ha; - btnDrawShape += ','; - - if (buttonMode == DrawButtonMode_e::Initialize) { - btnDrawShape += AdaGFXcolorToString(TouchObjects[buttonIndex].colorCaption, _colorDepth); - } else { - btnDrawShape += AdaGFXcolorToString(TouchObjects[buttonIndex].colorBackground, _colorDepth); - } - - if (buttonMode == DrawButtonMode_e::Initialize) { - btnDrawShape += ','; - btnDrawShape += AdaGFXcolorToString(TouchObjects[buttonIndex].colorBackground, _colorDepth); - } - break; - case Button_type_e::Rounded: - break; - case Button_type_e::Circle: - break; - case Button_type_e::Button_MAX: - break; - } - - if (bType != Button_type_e::None) { - String btnDrawCmd; - btnDrawCmd.reserve(80); - btnDrawCmd = cmdPrefix; - btnDrawCmd += btnDrawShape; - ExecuteCommand_all(EventValueSource::Enum::VALUE_SOURCE_RULES, btnDrawCmd.c_str()); - } + eventQueue.addMove(std::move(eventCommand)); + delay(0); } -# endif // ifdef P123_USE_EXTENDED_TOUCH - /** * Load the touch objects from the settings, and initialize then properly where needed. */ @@ -851,7 +1104,9 @@ void P123_data_struct::loadTouchObjects(const EventStruct *event) { lastObjectIndex = P123_OBJECT_INDEX_START - 1; // START must be > 0!!! - objectCount = 0; + objectCount = 0; + _minButtonGroup = 0; + _maxButtonGroup = 0; for (uint8_t i = P123_OBJECT_INDEX_END; i >= P123_OBJECT_INDEX_START; i--) { if (!settingsArray[i].isEmpty() && (lastObjectIndex < P123_OBJECT_INDEX_START)) { @@ -880,6 +1135,34 @@ void P123_data_struct::loadTouchObjects(const EventStruct *event) { P123_DEBOUNCE_MILLIS); P123_Settings.treshold = parseStringToInt(settingsArray[P123_CALIBRATION_START], P123_COMMON_TOUCH_TRESHOLD, P123_SETTINGS_SEPARATOR, P123_TS_TRESHOLD); + # ifdef P123_USE_EXTENDED_TOUCH + P123_Settings.colorOn = parseStringToInt(settingsArray[P123_CALIBRATION_START], + P123_COMMON_DEF_COLOR_ON, P123_SETTINGS_SEPARATOR); + P123_Settings.colorOff = parseStringToInt(settingsArray[P123_CALIBRATION_START], + P123_COMMON_DEF_COLOR_OFF, P123_SETTINGS_SEPARATOR); + P123_Settings.colorBorder = parseStringToInt(settingsArray[P123_CALIBRATION_START], + P123_COMMON_DEF_COLOR_BORDER, P123_SETTINGS_SEPARATOR); + P123_Settings.colorCaption = parseStringToInt(settingsArray[P123_CALIBRATION_START], + P123_COMMON_DEF_COLOR_CAPTION, P123_SETTINGS_SEPARATOR); + P123_Settings.colorDisabled = parseStringToInt(settingsArray[P123_CALIBRATION_START], + P123_COMMON_DEF_COLOR_DISABLED, P123_SETTINGS_SEPARATOR); + P123_Settings.colorDisabledCaption = parseStringToInt(settingsArray[P123_CALIBRATION_START], + P123_COMMON_DEF_COLOR_DISABCAPT, P123_SETTINGS_SEPARATOR); + + if ((P123_Settings.colorOn == 0u) && + (P123_Settings.colorOff == 0u) && + (P123_Settings.colorCaption == 0u) && + (P123_Settings.colorBorder == 0u) && + (P123_Settings.colorDisabled == 0u) && + (P123_Settings.colorDisabledCaption == 0u)) { + P123_Settings.colorOn = ADAGFX_GREEN; + P123_Settings.colorOff = ADAGFX_RED; + P123_Settings.colorCaption = ADAGFX_WHITE; + P123_Settings.colorBorder = ADAGFX_WHITE; + P123_Settings.colorDisabled = 0x9410; + P123_Settings.colorDisabledCaption = 0x5A69; + } + # endif // ifdef P123_USE_EXTENDED_TOUCH settingsArray[P123_CALIBRATION_START].clear(); // Free a little memory @@ -903,13 +1186,18 @@ void P123_data_struct::loadTouchObjects(const EventStruct *event) { TouchObjects[t].width_height.x = parseStringToInt(settingsArray[i], P123_OBJECT_COORD_WIDTH, P123_SETTINGS_SEPARATOR); TouchObjects[t].width_height.y = parseStringToInt(settingsArray[i], P123_OBJECT_COORD_HEIGHT, P123_SETTINGS_SEPARATOR); # ifdef P123_USE_EXTENDED_TOUCH - TouchObjects[t].colorOn = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_ON, P123_SETTINGS_SEPARATOR); - TouchObjects[t].colorOff = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_OFF, P123_SETTINGS_SEPARATOR); - TouchObjects[t].colorCaption = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_CAPTION, P123_SETTINGS_SEPARATOR); - TouchObjects[t].colorHighlight = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_HIGHLIGHT, P123_SETTINGS_SEPARATOR); - TouchObjects[t].captionOn = parseStringKeepCase(settingsArray[i], P123_OBJECT_CAPTION_ON, P123_SETTINGS_SEPARATOR); - TouchObjects[t].captionOff = parseStringKeepCase(settingsArray[i], P123_OBJECT_CAPTION_OFF, P123_SETTINGS_SEPARATOR); - TouchObjects[t].colorBackground = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_BACKGROUND, P123_SETTINGS_SEPARATOR); + TouchObjects[t].colorOn = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_ON, P123_SETTINGS_SEPARATOR); + TouchObjects[t].colorOff = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_OFF, P123_SETTINGS_SEPARATOR); + TouchObjects[t].colorCaption = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_CAPTION, P123_SETTINGS_SEPARATOR); + TouchObjects[t].captionOn = parseStringKeepCase(settingsArray[i], P123_OBJECT_CAPTION_ON, P123_SETTINGS_SEPARATOR); + TouchObjects[t].captionOff = parseStringKeepCase(settingsArray[i], P123_OBJECT_CAPTION_OFF, P123_SETTINGS_SEPARATOR); + TouchObjects[t].colorBorder = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_BORDER, P123_SETTINGS_SEPARATOR); + TouchObjects[t].colorDisabled = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_DISABLED, P123_SETTINGS_SEPARATOR); + TouchObjects[t].colorDisabledCaption = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_DISABCAPT, P123_SETTINGS_SEPARATOR); + + if (get8BitFromUL(TouchObjects[t].flags, P123_OBJECT_FLAG_GROUP) > _maxButtonGroup) { + _maxButtonGroup = get8BitFromUL(TouchObjects[t].flags, P123_OBJECT_FLAG_GROUP); + } # endif // ifdef P123_USE_EXTENDED_TOUCH TouchObjects[t].SurfaceAreas = 0; // Reset runtime stuff @@ -922,6 +1210,10 @@ void P123_data_struct::loadTouchObjects(const EventStruct *event) { } } } + + if (_maxButtonGroup > 0) { + _minButtonGroup = 1; + } } /** @@ -1038,11 +1330,14 @@ bool P123_data_struct::isValidAndTouchedTouchObject(int16_t x, bool selected = false; for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { + uint8_t group = get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_GROUP); + if (!TouchObjects[objectNr].objectName.isEmpty() && bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED) && (TouchObjects[objectNr].width_height.x != 0) - && (TouchObjects[objectNr].width_height.y != 0)) { // Not initial could be valid - if (TouchObjects[objectNr].SurfaceAreas == 0) { // Need to calculate the surface area + && (TouchObjects[objectNr].width_height.y != 0) // Not initial could be valid + && ((group == 0) || (group == _buttonGroup))) { // Group 0 is always active + if (TouchObjects[objectNr].SurfaceAreas == 0) { // Need to calculate the surface area TouchObjects[objectNr].SurfaceAreas = TouchObjects[objectNr].width_height.x * TouchObjects[objectNr].width_height.y; } @@ -1091,41 +1386,29 @@ bool P123_data_struct::isValidAndTouchedTouchObject(int16_t x, } /** - * Set the enabled/disabled state by inserting or deleting an underscore '_' as the first character of the object name. - * Checks if the name doesn't exceed the max. length. + * Set the enabled/disabled state of an object. */ -bool P123_data_struct::setTouchObjectState(const String& touchObject, bool state) { +bool P123_data_struct::setTouchObjectState(struct EventStruct *event, const String& touchObject, bool state) { if (touchObject.isEmpty()) { return false; } - String findObject; // = (state ? F("_") : F("")); // When enabling, try to find a disabled object - - findObject += touchObject; - String thisObject; - bool success = false; - - thisObject.reserve(P123_MaxObjectNameLength); + bool success = false; for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { if ((!TouchObjects[objectNr].objectName.isEmpty()) - && findObject.equalsIgnoreCase(TouchObjects[objectNr].objectName)) { - // uint32_t objectFlags = parseStringToInt(settingsArray[objectNr], P123_OBJECT_FLAGS, P123_SETTINGS_SEPARATOR); - bool enabled = bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED); + && touchObject.equalsIgnoreCase(TouchObjects[objectNr].objectName)) { + bool currentState = bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED); - if (state != enabled) { + if (state != currentState) { bitWrite(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED, state); success = true; - // String config; - // config.reserve(settingsArray[objectNr].length() + 1); - // config += objectFlags; - // config += P123_SETTINGS_SEPARATOR; - - // // Rest of the string - // config += parseStringToEndKeepCase(settingsArray[objectNr], P123_OBJECT_NAME, P123_SETTINGS_SEPARATOR); - // settingsArray[objectNr] = config; // Store + if (bitRead(P123_Settings.flags, P123_FLAGS_SEND_OBJECTNAME) && + bitRead(P123_Settings.flags, P123_FLAGS_INIT_OBJECTEVENT)) { + generateObjectEvent(event, objectNr, state ? (TouchObjects[objectNr].TouchStates ? 1 : -1) : -2); + } } # ifdef PLUGIN_123_DEBUG String log = F("P123 setTouchObjectState: obj: "); - log += thisObject; + log += touchObject; if (success) { log += F(", new state: "); @@ -1141,6 +1424,42 @@ bool P123_data_struct::setTouchObjectState(const String& touchObject, bool state return success; } +/** + * Set the on/off state of a touch-button object. + */ +bool P123_data_struct::setTouchButtonOnOff(struct EventStruct *event, const String& touchObject, bool state) { + if (touchObject.isEmpty()) { return false; } + bool success = false; + + for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { + if ((!TouchObjects[objectNr].objectName.isEmpty()) + && touchObject.equalsIgnoreCase(TouchObjects[objectNr].objectName) + && bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED) + && bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTON)) { + bool currentState = TouchObjects[objectNr].TouchStates; + + success = true; // Always success if matched button + + if (state != currentState) { + TouchObjects[objectNr].TouchStates = state; + + if (bitRead(P123_Settings.flags, P123_FLAGS_SEND_OBJECTNAME) && + bitRead(P123_Settings.flags, P123_FLAGS_INIT_OBJECTEVENT)) { + generateObjectEvent(event, objectNr, state ? 1 : 0); + } + } + # ifdef PLUGIN_123_DEBUG + String log = F("P123 setTouchButtonOnOff: obj: "); + log += touchObject; + log += F(", (new) state: "); + log += (state ? F("on") : F("off")); + addLogMove(LOG_LEVEL_INFO, log); + # endif // PLUGIN_123_DEBUG + } + } + return success; +} + /** * Scale the provided raw coordinates to screen-resolution coordinates if calibration is enabled/configured */ @@ -1172,32 +1491,46 @@ void P123_data_struct::scaleRawToCalibrated(int16_t& x, int16_t& y) { } } -/****************************************** - * enquoteString wrap in ", ' or ` unless all 3 quote types are used - * TODO: Replace with wrapWithQuotes() once available - *****************************************/ -String P123_data_struct::enquoteString(const String& input) { - char quoteChar = '"'; - - if (input.indexOf(quoteChar) > -1) { - quoteChar = '\''; - - if (input.indexOf(quoteChar) > -1) { - quoteChar = '`'; - - if (input.indexOf(quoteChar) > -1) { - return input; // All types of supported quotes used, return original string - } +/** + * Set the desired button group, muxt be between the minimum and maximum found values + */ +bool P123_data_struct::setButtonGroup(const EventStruct *event, + int8_t buttonGroup) { + if ((buttonGroup >= 0) && (buttonGroup <= _maxButtonGroup)) { + if (buttonGroup != _buttonGroup) { + displayButtons(event, _buttonGroup, -2); + _buttonGroup = buttonGroup; + displayButtons(event, _buttonGroup, -1); } + return true; } - String result; + return false; +} - result.reserve(input.length() + 2); - result = quoteChar; - result += input; - result += quoteChar; +/** + * increment button group, if max. group > 0 then min. group = 1 + */ +bool P123_data_struct::incrementButtonGroup(const EventStruct *event) { + if (_buttonGroup < _maxButtonGroup) { + displayButtons(event, _buttonGroup, -2); + _buttonGroup++; + displayButtons(event, _buttonGroup, -1); + return true; + } + return false; +} - return result; +/** + * decrement button group, if max. group > 0 then min. group = 1 + */ +bool P123_data_struct::decrementButtonGroup(const EventStruct *event) { + if (_buttonGroup > _minButtonGroup) { + displayButtons(event, _buttonGroup, -2); + _buttonGroup--; + displayButtons(event, _buttonGroup, -1); + return true; + } + return false; } #endif // ifdef USES_P123 diff --git a/src/src/PluginStructs/P123_data_struct.h b/src/src/PluginStructs/P123_data_struct.h index 544e76db4d..a1ae3cdb23 100644 --- a/src/src/PluginStructs/P123_data_struct.h +++ b/src/src/PluginStructs/P123_data_struct.h @@ -9,11 +9,11 @@ # include -# define PLUGIN_123_DEBUG // Additional debugging information +# define PLUGIN_123_DEBUG // Additional debugging information -# define P123_USE_TOOLTIPS // Enable tooltips in UI +# define P123_USE_TOOLTIPS // Enable tooltips in UI -// # define P123_USE_EXTENDED_TOUCH // Enable extended touch settings +# define P123_USE_EXTENDED_TOUCH // Enable extended touch settings # ifdef LIMIT_BUILD_SIZE # ifdef P123_USE_TOOLTIPS @@ -27,14 +27,15 @@ # undef P123_USE_TOOLTIPS # endif // if defined(P123_USE_TOOLTIPS) && !defined(ENABLE_TOOLTIPS) -# define P123_FLAGS_SEND_XY 0 // Set in Global Settings flags -# define P123_FLAGS_SEND_Z 1 // Set in Global Settings flags -# define P123_FLAGS_SEND_OBJECTNAME 2 // Set in Global Settings flags -# define P123_FLAGS_USE_CALIBRATION 3 // Set in Global Settings flags -# define P123_FLAGS_LOG_CALIBRATION 4 // Set in Global Settings flags -# define P123_FLAGS_ROTATION_FLIPPED 5 // Set in P123_CONFIG_FLAGS -# define P123_FLAGS_DEDUPLICATE 6 // Set in Global Settings flags -# define P123_FLAGS_INIT_OBJECTEVENT 7 // Set in Global Settings flags +# define P123_FLAGS_SEND_XY 0 // Set in Global Settings flags +# define P123_FLAGS_SEND_Z 1 // Set in Global Settings flags +# define P123_FLAGS_SEND_OBJECTNAME 2 // Set in Global Settings flags +# define P123_FLAGS_USE_CALIBRATION 3 // Set in Global Settings flags +# define P123_FLAGS_LOG_CALIBRATION 4 // Set in Global Settings flags +# define P123_FLAGS_ROTATION_FLIPPED 5 // Set in P123_CONFIG_FLAGS +# define P123_FLAGS_DEDUPLICATE 6 // Set in Global Settings flags +# define P123_FLAGS_INIT_OBJECTEVENT 7 // Set in Global Settings flags +# define P123_FLAGS_INITIAL_GROUP 8 // Initial group to activate, 8 bits uses only 6 # define P123_CONFIG_DISPLAY_TASK PCONFIG(0) @@ -76,6 +77,8 @@ # define P123_EXTRA_OBJECT_COUNT 5 // The number of empty objects to show if max not reached # define P123_ARRAY_SIZE (P123_MAX_OBJECT_COUNT + P123_MAX_CALIBRATION_COUNT) +# define P123_MAX_BUTTON_GROUPS 63 // Max. allowed button groups, technically limited to 6 bits = 0..63! + # define P123_FLAGS_ON_OFF_BUTTON 0 // TouchObjects.flags On/Off Button function # define P123_FLAGS_INVERT_BUTTON 1 // TouchObjects.flags Inverted On/Off Button function @@ -87,16 +90,24 @@ # define P123_SETTINGS_SEPARATOR '\x02' // Settings array field offsets: Calibration -# define P123_CALIBRATION_START 0 // Index into settings array -# define P123_CALIBRATION_ENABLED 1 // Enabled 0/1 (parseString index starts at 1) -# define P123_CALIBRATION_LOG_ENABLED 2 // Calibration Log Enabled 0/1 -# define P123_CALIBRATION_TOP_X 3 // Top X offset (uint16_t) -# define P123_CALIBRATION_TOP_Y 4 // Top Y -# define P123_CALIBRATION_BOTTOM_X 5 // Bottom X -# define P123_CALIBRATION_BOTTOM_Y 6 // Bottom Y -# define P123_COMMON_DEBOUNCE_MS 7 // Debounce milliseconds -# define P123_COMMON_TOUCH_TRESHOLD 8 // Treshold setting -# define P123_COMMON_FLAGS 9 // Common flags +# define P123_CALIBRATION_START 0 // Index into settings array +# define P123_CALIBRATION_ENABLED 1 // Enabled 0/1 (parseString index starts at 1) +# define P123_CALIBRATION_LOG_ENABLED 2 // Calibration Log Enabled 0/1 +# define P123_CALIBRATION_TOP_X 3 // Top X offset (uint16_t) +# define P123_CALIBRATION_TOP_Y 4 // Top Y +# define P123_CALIBRATION_BOTTOM_X 5 // Bottom X +# define P123_CALIBRATION_BOTTOM_Y 6 // Bottom Y +# define P123_COMMON_DEBOUNCE_MS 7 // Debounce milliseconds +# define P123_COMMON_TOUCH_TRESHOLD 8 // Treshold setting +# define P123_COMMON_FLAGS 9 // Common flags +# ifdef P123_USE_EXTENDED_TOUCH +# define P123_COMMON_DEF_COLOR_ON 10 // Default Color ON (rgb565, uint16_t) +# define P123_COMMON_DEF_COLOR_OFF 11 // Default Color OFF +# define P123_COMMON_DEF_COLOR_BORDER 12 // Default Color Border +# define P123_COMMON_DEF_COLOR_CAPTION 13 // Default Color Caption +# define P123_COMMON_DEF_COLOR_DISABLED 14 // Default Disabled Color +# define P123_COMMON_DEF_COLOR_DISABCAPT 15 // Default Disabled Caption Color +# endif // ifdef P123_USE_EXTENDED_TOUCH // Settings array field offsets: Touch objects # define P123_OBJECT_INDEX_START (P123_CALIBRATION_START + 1) @@ -111,34 +122,20 @@ # define P123_OBJECT_COLOR_ON 7 // Color ON (rgb565, uint16_t) # define P123_OBJECT_COLOR_OFF 8 // Color OFF # define P123_OBJECT_COLOR_CAPTION 9 // Color Caption -# define P123_OBJECT_COLOR_HIGHLIGHT 10 // Color Highlight -# define P123_OBJECT_CAPTION_ON 11 // Caption ON (String 12, quoted) -# define P123_OBJECT_CAPTION_OFF 12 // Caption OFF (String 12, quoted) -# define P123_OBJECT_COLOR_BACKGROUND 13 // Color Background +# define P123_OBJECT_CAPTION_ON 10 // Caption ON (String 12, quoted) +# define P123_OBJECT_CAPTION_OFF 11 // Caption OFF (String 12, quoted) +# define P123_OBJECT_COLOR_BORDER 12 // Color Border +# define P123_OBJECT_COLOR_DISABLED 13 // Disabled Color +# define P123_OBJECT_COLOR_DISABCAPT 14 // Disabled Caption Color # endif // ifdef P123_USE_EXTENDED_TOUCH # define P123_OBJECT_FLAG_ENABLED 0 // Enabled # define P123_OBJECT_FLAG_BUTTON 1 // Button behavior # define P123_OBJECT_FLAG_INVERTED 2 // Inverted button -// # define P123_OBJECT_FLAG_COLORED 3 // Colored button (unused) -// # define P123_OBJECT_FLAG_CAPTION 4 // Use caption on button (unused) -# define P123_OBJECT_FLAG_BUTTONTYPE 8 // 8 bits used as button type - -# ifdef P123_USE_EXTENDED_TOUCH -enum class DrawButtonMode_e : uint8_t { - Initialize = 0, // Draw the base button - State, // Set the button state (on/off) - Highlight // Use Highlight option(s) -}; - -enum class Button_type_e : uint8_t { - None = 0, - Square, - Rounded, - Circle, - Button_MAX // must be last value in enum -}; -# endif // ifdef P123_USE_EXTENDED_TOUCH +# define P123_OBJECT_FLAG_FONTSCALE 3 // 4 bits used as button alignment +# define P123_OBJECT_FLAG_BUTTONTYPE 7 // 8 bits used as button type +# define P123_OBJECT_FLAG_GROUP 15 // 8 bits used as button group +# define P123_OBJECT_FLAG_ACTIONGROUP 23 // 8 bits, 6 bits used as action group 0..63, 2 bits used as action option // Lets define our own coordinate point struct tP123_Point @@ -159,15 +156,27 @@ struct tP123_TouchObjects tP123_Point top_left; tP123_Point width_height; # ifdef P123_USE_EXTENDED_TOUCH - uint16_t colorOn = 0u; - uint16_t colorOff = 0u; - uint16_t colorCaption = 0u; - uint16_t colorHighlight = 0u; - uint16_t colorBackground = 0u; + uint16_t colorOn = 0u; + uint16_t colorOff = 0u; + uint16_t colorCaption = 0u; + uint16_t colorBorder = 0u; + uint16_t colorDisabled = 0u; + uint16_t colorDisabledCaption = 0u; # endif // ifdef P123_USE_EXTENDED_TOUCH bool TouchStates = false; }; +// Touch actions, use with mask 0xC0, other 6 bits are group/code to activate +enum class P123_touch_action_e : uint8_t { + Default = 0b00000000, // 0x00 + ActivateGroup = 0b01000000, // 0x40 + IncrementGroup = 0b10000000, // 0x80 + DecrementGroup = 0b11000000, // 0xC0 + TouchAction_MAX = 4 // Last item is count, max 4! +}; + +const __FlashStringHelper* toString(P123_touch_action_e action); + // Data structure struct P123_data_struct : public PluginTaskData_base @@ -197,29 +206,36 @@ struct P123_data_struct : public PluginTaskData_base int16_t y, String& selectedObjectName, int8_t& selectedObjectIndex); - bool setTouchObjectState(const String& touchObject, - bool state); + bool setTouchObjectState(struct EventStruct *event, + const String & touchObject, + bool state); + bool setTouchButtonOnOff(struct EventStruct *event, + const String & touchObject, + bool state); void scaleRawToCalibrated(int16_t& x, int16_t& y); bool plugin_webform_load(struct EventStruct *event); bool plugin_webform_save(struct EventStruct *event); - bool plugin_ten_per_second(struct EventStruct *event); + bool plugin_fifty_per_second(struct EventStruct *event); + bool setButtonGroup(const EventStruct *event, + int8_t buttonGroup); + bool incrementButtonGroup(const EventStruct *event); + bool decrementButtonGroup(const EventStruct *event); + void displayButtons(const EventStruct *event, + int8_t buttonGroup, + int8_t mode = 0); private: - # ifdef P123_USE_EXTENDED_TOUCH - void drawButton(DrawButtonMode_e buttonMode, - int8_t buttonIndex, - const EventStruct *event); - # endif // ifdef P123_USE_EXTENDED_TOUCH int parseStringToInt(const String& string, uint8_t indexFind, char separator = ',', int defaultValue = 0); - String enquoteString(const String& input); // TODO: Replace by wrapWithQuotes - void generateObjectEvent(const EventStruct *event, - const int8_t objectIndex, - const int8_t onOffState); + void generateObjectEvent(const EventStruct *event, + const int8_t objectIndex, + const int8_t onOffState, + const bool groupSwitch = false, + const int8_t factor = 1); // This is initialized by calling init() Adafruit_FT6206 *touchscreen = nullptr; @@ -239,16 +255,28 @@ struct P123_data_struct : public PluginTaskData_base uint32_t flags = 0u; tP123_Point top_left; tP123_Point bottom_right; - uint16_t treshold = 0u; - uint8_t debounceMs = 0u; - bool calibrationEnabled = false; - bool logEnabled = false; + uint16_t treshold = 0u; + # ifdef P123_USE_EXTENDED_TOUCH + uint16_t colorOn = 0u; + uint16_t colorOff = 0u; + uint16_t colorCaption = 0u; + uint16_t colorBorder = 0u; + uint16_t colorDisabled = 0u; + uint16_t colorDisabledCaption = 0u; + # endif // ifdef P123_USE_EXTENDED_TOUCH + uint8_t debounceMs = 0u; + bool calibrationEnabled = false; + bool logEnabled = false; }; tP123_Globals P123_Settings; std::vectorTouchObjects; + int8_t _buttonGroup = 0; + int8_t _minButtonGroup = 0; + int8_t _maxButtonGroup = 0; + public: // This is filled during checking of a touchobject From eb25a7c75b014ad6c7e4da9732bbf609413eb832 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 1 May 2022 22:25:19 +0200 Subject: [PATCH 007/113] [Build] Add -Wswitch compiler option for ESP32 (already active for ESP8266) --- platformio_core_defs.ini | 8 ++++---- platformio_esp32_envs.ini | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/platformio_core_defs.ini b/platformio_core_defs.ini index 948010d823..08dbd207ca 100644 --- a/platformio_core_defs.ini +++ b/platformio_core_defs.ini @@ -159,7 +159,7 @@ platform_packages = [core_esp32_2_1_0] platform = espressif32@2.1.0 -build_flags = -DESP32_STAGE +build_flags = -DESP32_STAGE -Wswitch ; Updated ESP-IDF to the latest stable 4.0.1 @@ -169,15 +169,15 @@ build_flags = -DESP32_STAGE [core_esp32_IDF4_4__2_0_3] platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.3rc1/platform-espressif32-2.0.3new.zip platform_packages = -build_flags = -DESP32_STAGE +build_flags = -DESP32_STAGE -Wswitch [core_esp32_3_5_0] platform = espressif32 @ 3.5.0 platform_packages = -build_flags = -DESP32_STAGE +build_flags = -DESP32_STAGE -Wswitch [core_esp32_stage] platform = https://github.com/platformio/platform-espressif32.git#feature/arduino-idf-master platform_packages = framework-arduinoespressif32 @ https://github.com/tasmota/arduino-esp32/releases/download/2.0.1/framework-arduinoespressif32-release_IDF4.4.tar.gz platformio/tool-esptoolpy @ https://github.com/tasmota/esptool/releases/download/v3.2/esptool-v3.2.zip -build_flags = -DESP32_STAGE +build_flags = -DESP32_STAGE -Wswitch diff --git a/platformio_esp32_envs.ini b/platformio_esp32_envs.ini index 12cfd6d104..3f1421f442 100644 --- a/platformio_esp32_envs.ini +++ b/platformio_esp32_envs.ini @@ -187,6 +187,7 @@ board = esp32_4M lib_deps = ${esp32_common.lib_deps}, ServoESP32 build_flags = ${esp32_common.build_flags} -DFEATURE_ARDUINO_OTA + -DPLUGIN_DISPLAY_COLLECTION [env:custom_ESP32_4M316k_ETH] From 4f3f7fa38e02ea0ba79df91bb732fddefd12e0de Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 2 May 2022 22:33:23 +0200 Subject: [PATCH 008/113] [P123] Minor updates and improvements --- src/_P123_FT62x6Touch.ino | 1 + src/src/PluginStructs/P123_data_struct.cpp | 220 ++++++++++++--------- src/src/PluginStructs/P123_data_struct.h | 6 +- 3 files changed, 126 insertions(+), 101 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index d9573ebbfc..b44074dbfc 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -6,6 +6,7 @@ /** * Changelog: + * 2022-05-02 tonhuisman: Small updates and improvements * 2022-04-30 tonhuisman: Add support for AdaGFX btn subcommand use and (local) button groups * Start preparations for refactoring touch objects into separate helper class * 2022-04-25 tonhuisman: Code cleanup, initialize object event -2 for disabled objects, -1 for enabled objects diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index 3ea29a8b39..b6c8fff55a 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -80,18 +80,23 @@ bool P123_data_struct::init(const EventStruct *event, if (bitRead(P123_Settings.flags, P123_FLAGS_SEND_OBJECTNAME) && bitRead(P123_Settings.flags, P123_FLAGS_INIT_OBJECTEVENT)) { - if (_maxButtonGroup > 0) { // Multiple groups? - displayButtons(event, _buttonGroup, -3); // Clear all groups + if (_maxButtonGroup > 0) { // Multiple groups? + displayButtonGroup(event, _buttonGroup, -3); // Clear all groups } _buttonGroup = get8BitFromUL(P123_Settings.flags, P123_FLAGS_INITIAL_GROUP); # ifdef PLUGIN_123_DEBUG - String log = F("P123 DEBUG group: "); - log += _buttonGroup; - log += F(", max group: "); - log += _maxButtonGroup; - addLogMove(LOG_LEVEL_INFO, log); + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("P123 DEBUG group: "); + log += _buttonGroup; + log += F(", max group: "); + log += _maxButtonGroup; + addLogMove(LOG_LEVEL_INFO, log); + } # endif // ifdef PLUGIN_123_DEBUG - displayButtons(event, _buttonGroup); // Initialize selected group and group 0 + + displayButtonGroup(event, _buttonGroup); // Initialize selected group and group 0 + # ifdef PLUGIN_123_DEBUG addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG group done.")); # endif // ifdef PLUGIN_123_DEBUG @@ -109,9 +114,9 @@ bool P123_data_struct::init(const EventStruct *event, /** * mode: -2 = clear buttons in group, -3 = clear all buttongroups, -1 = draw buttons in group, 0 = initialize buttons */ -void P123_data_struct::displayButtons(const EventStruct *event, - int8_t buttonGroup, - int8_t mode) { +void P123_data_struct::displayButtonGroup(const EventStruct *event, + int8_t buttonGroup, + int8_t mode) { for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { int8_t state = 99; int8_t group = get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_GROUP); @@ -137,23 +142,25 @@ void P123_data_struct::displayButtons(const EventStruct *event, } generateObjectEvent(event, objectNr, state, mode < 0, mode <= -2 ? -1 : 1); } - # ifdef XX_PLUGIN_123_DEBUG - - // TODO: remove log? - String log = F("P123: button init, state: "); - log += state; - log += F(", group: "); - log += buttonGroup; - log += F(", mode: "); - log += mode; - log += F(", group: "); - log += get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_GROUP); - log += F(", en: "); - log += bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTON); - log += F(", object: "); - log += objectNr; - addLog(LOG_LEVEL_INFO, log); + # ifdef XX_PLUGIN_123_DEBUG // Temporarily disabled + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("P123: button init, state: "); + log += state; + log += F(", group: "); + log += buttonGroup; + log += F(", mode: "); + log += mode; + log += F(", group: "); + log += get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_GROUP); + log += F(", en: "); + log += bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTON); + log += F(", object: "); + log += objectNr; + addLog(LOG_LEVEL_INFO, log); + } # endif // ifdef PLUGIN_123_DEBUG + delay(0); } @@ -218,6 +225,7 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { }; int optionValues3[P123_EVENTS_OPTIONS] = { 0, 1, 3, 4, 5, 7 }; // Already used as a bitmap! addFormSelector(F("Events"), F("touch_events"), P123_EVENTS_OPTIONS, options3, optionValues3, choice3); + addFormCheckBox(F("Initial Objectnames events"), F("touch_init_objectevent"), bitRead(P123_Settings.flags, P123_FLAGS_INIT_OBJECTEVENT)); addFormNote(F("Will send state -1 but only for enabled On/Off button objects.")); } @@ -278,8 +286,6 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { 65535); html_end_table(); - - // addFormNote(F("At least 1 x/y value must be <> 0 to enable calibration.")); } addFormCheckBox(F("Enable logging for calibration"), F("touch_log_calibration"), @@ -287,8 +293,11 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { addFormSubHeader(F("Touch objects")); + # ifdef P123_USE_EXTENDED_TOUCH + + AdaGFXHtmlColorDepthDataList(F("adagfx65kcolors"), static_cast(P123_COLOR_DEPTH)); + { - # ifdef P123_USE_EXTENDED_TOUCH String parsed; addRowLabel(F("Default On/Off button colors")); html_table(EMPTY_STRING, false); // Sub-table @@ -354,18 +363,16 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { , F("adagfx65kcolors") ); html_end_table(); - # endif // ifdef P123_USE_EXTENDED_TOUCH } { - # ifdef P123_USE_EXTENDED_TOUCH addFormNumericBox(F("Initial button group"), F("touch_initial_group"), get8BitFromUL(P123_Settings.flags, P123_FLAGS_INITIAL_GROUP), 0, P123_MAX_BUTTON_GROUPS # ifdef P123_USE_TOOLTIPS , F("Initial group") # endif // ifdef P123_USE_TOOLTIPS ); - # endif // ifdef P123_USE_EXTENDED_TOUCH } + # endif // ifdef P123_USE_EXTENDED_TOUCH { addRowLabel(F("Object")); @@ -419,7 +426,7 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { toString(Button_type_e::ArrowDown), }; - int buttonTypeValues[] = { + const int buttonTypeValues[] = { static_cast(Button_type_e::None), static_cast(Button_type_e::Square), static_cast(Button_type_e::Rounded), @@ -478,10 +485,6 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { String parsed; TouchObjects.resize(maxIdx, tP123_TouchObjects()); - # ifdef P123_USE_EXTENDED_TOUCH - AdaGFXHtmlColorDepthDataList(F("adagfx65kcolors"), static_cast(P123_COLOR_DEPTH)); - # endif // ifdef P123_USE_EXTENDED_TOUCH - for (int objectNr = 0; objectNr < maxIdx; objectNr++) { html_TR_TD(); addHtml(F(" ")); @@ -492,7 +495,11 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { // Enable new entries bool enabled = bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED) || TouchObjects[objectNr].objectName.isEmpty(); addCheckBox(getPluginCustomArgName(objectNr + 0), - enabled, false); + enabled, false + # ifdef P123_USE_TOOLTIPS + , F("Enabled") + # endif // ifdef P123_USE_TOOLTIPS + ); html_TD(); // Name addTextBox(getPluginCustomArgName(objectNr + 100), TouchObjects[objectNr].objectName, @@ -512,7 +519,7 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { , F("widenumber"), F("Top-left y") # endif // ifdef P123_USE_TOOLTIPS ); - html_TD(); // on/off button + html_TD(); // (on/off) button (type) # ifdef P123_USE_EXTENDED_TOUCH addSelector(getPluginCustomArgName(objectNr + 800), static_cast(Button_type_e::Button_MAX), @@ -544,10 +551,10 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { ); # endif // ifdef P123_USE_EXTENDED_TOUCH # ifdef P123_USE_EXTENDED_TOUCH - html_TD(); // ON color + html_TD(); // ON color parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorOn, _colorDepth, true); addTextBox(getPluginCustomArgName(objectNr + 1000), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, - EMPTY_STRING, F("widenumber") // |list=\"adagfx65kcolors\" + EMPTY_STRING, F("widenumber") # ifdef P123_USE_TOOLTIPS , F("ON color") # endif // ifdef P123_USE_TOOLTIPS @@ -561,9 +568,9 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { false, EMPTY_STRING, F("wide") - # ifdef P123_USE_TOOLTIPS + # ifdef P123_USE_TOOLTIPS , F("ON caption") - # endif // ifdef P123_USE_TOOLTIPS + # endif // ifdef P123_USE_TOOLTIPS ); html_TD(); // Border color parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorBorder, _colorDepth, true); @@ -612,8 +619,7 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { ); } # endif // ifdef P123_USE_EXTENDED_TOUCH - html_TD(); // Next column - // Width + html_TD(); // Width addNumericBox(getPluginCustomArgName(objectNr + 400), TouchObjects[objectNr].width_height.x, 0, 65535 # ifdef P123_USE_TOOLTIPS @@ -659,9 +665,9 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { false, EMPTY_STRING, F("wide") - # ifdef P123_USE_TOOLTIPS + # ifdef P123_USE_TOOLTIPS , F("OFF caption") - # endif // ifdef P123_USE_TOOLTIPS + # endif // ifdef P123_USE_TOOLTIPS ); html_TD(); // Caption color parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorCaption, _colorDepth, true); @@ -738,7 +744,7 @@ bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { config += P123_SETTINGS_SEPARATOR; config += getFormItemInt(F("touch_treshold")); config += P123_SETTINGS_SEPARATOR; - config += lSettings; + config += ull2String(lSettings); config += P123_SETTINGS_SEPARATOR; # ifdef P123_USE_EXTENDED_TOUCH colorInput = webArg(getPluginCustomArgName(3000)); // Default Color ON @@ -762,12 +768,16 @@ bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { # endif // ifdef P123_USE_EXTENDED_TOUCH settingsArray[P123_CALIBRATION_START] = config; - { + + # ifdef PLUGIN_123_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("Save settings: "); config.replace(P123_SETTINGS_SEPARATOR, ','); log += config; addLogMove(LOG_LEVEL_INFO, log); } + # endif // ifdef PLUGIN_123_DEBUG String error; @@ -801,7 +811,7 @@ bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { bitWrite(flags, P123_OBJECT_FLAG_BUTTON, isFormItemChecked(getPluginCustomArgName(objectNr + 600))); // On/Off button # endif // ifdef P123_USE_EXTENDED_TOUCH - config += String(flags); // Flags + config += ull2String(flags); // Flags config += P123_SETTINGS_SEPARATOR; config += getFormItemInt(getPluginCustomArgName(objectNr + 200)); // Top x config += P123_SETTINGS_SEPARATOR; @@ -902,7 +912,7 @@ bool P123_data_struct::plugin_fifty_per_second(struct EventStruct *event) { loglevelActiveFor(LOG_LEVEL_INFO)) { // REQUIRED for calibration and setting up objects, so do not make this optional! String log; log.reserve(72); - log = F("Touch calibration rx= "); // Space before the logged values was added for readability + log = F("Touch calibration rx= "); // Space before the logged values added for readability log += rx; log += F(", ry= "); log += ry; @@ -919,13 +929,13 @@ bool P123_data_struct::plugin_fifty_per_second(struct EventStruct *event) { addLogMove(LOG_LEVEL_INFO, log); } - if (Settings.UseRules) { // No events to handle if rules not - // enabled - if (success && bitRead(P123_Settings.flags, P123_FLAGS_SEND_XY)) { // Send events for each touch + // No events to handle if rules not enabled + if (Settings.UseRules) { + if (success && bitRead(P123_Settings.flags, P123_FLAGS_SEND_XY)) { // Send events for each touch const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex); - if (!bitRead(P123_Settings.flags, P123_FLAGS_SEND_Z) && validDeviceIndex(DeviceIndex)) { // Do NOT send a Z event for each - // touch? + // Do NOT send a Z event for each touch? + if (!bitRead(P123_Settings.flags, P123_FLAGS_SEND_Z) && validDeviceIndex(DeviceIndex)) { Device[DeviceIndex].VType = Sensor_VType::SENSOR_TYPE_DUAL; Device[DeviceIndex].ValueCount = 2; } @@ -943,9 +953,8 @@ bool P123_data_struct::plugin_fifty_per_second(struct EventStruct *event) { if (isValidAndTouchedTouchObject(x, y, selectedObjectName, selectedObjectIndex)) { if ((selectedObjectIndex > -1) && bitRead(TouchObjects[selectedObjectIndex].flags, P123_OBJECT_FLAG_BUTTON)) { + // Not touched yet or too long ago if ((TouchObjects[selectedObjectIndex].TouchTimers == 0) || - - // Not touched yet or too long ago (TouchObjects[selectedObjectIndex].TouchTimers < (millis() - (1.5 * P123_Settings.debounceMs)))) { // From now wait the debounce time TouchObjects[selectedObjectIndex].TouchTimers = millis() + P123_Settings.debounceMs; @@ -989,6 +998,7 @@ void P123_data_struct::generateObjectEvent(const EventStruct *event, const int8_t onOffState, const bool groupSwitch, const int8_t factor) { + if ((objectIndex < 0) || (objectIndex >= TouchObjects.size())) { return; } // Range check String eventCommand; eventCommand.reserve(48); @@ -1020,7 +1030,7 @@ void P123_data_struct::generateObjectEvent(const EventStruct *event, eventCommand += objectIndex + 1; // Adjust to displayed index (6) eventCommand += ','; // (7) eventCommand += get8BitFromUL(TouchObjects[objectIndex].flags, P123_OBJECT_FLAG_BUTTONTYPE) * factor; -# ifdef P123_USE_EXTENDED_TOUCH + # ifdef P123_USE_EXTENDED_TOUCH eventCommand += ','; // (8) eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorOn == 0 ? P123_Settings.colorOn @@ -1089,7 +1099,9 @@ void P123_data_struct::generateObjectEvent(const EventStruct *event, eventCommand += -1; // No group to activate } } + eventQueue.addMove(std::move(eventCommand)); + delay(0); } @@ -1117,7 +1129,7 @@ void P123_data_struct::loadTouchObjects(const EventStruct *event) { // Get calibration and common settings P123_Settings.calibrationEnabled = parseStringToInt(settingsArray[P123_CALIBRATION_START], - P123_CALIBRATION_ENABLED, P123_SETTINGS_SEPARATOR) == 1; + P123_CALIBRATION_ENABLED, P123_SETTINGS_SEPARATOR) == 1; P123_Settings.logEnabled = parseStringToInt(settingsArray[P123_CALIBRATION_START], P123_CALIBRATION_LOG_ENABLED, P123_SETTINGS_SEPARATOR) == 1; int lSettings = 0; @@ -1288,9 +1300,12 @@ void P123_data_struct::readData(int16_t& x, int16_t& y, int16_t& z, int16_t& ox, void P123_data_struct::setRotation(uint8_t n) { _rotation = n; # ifdef PLUGIN_123_DEBUG - String log = F("P123 DEBUG Rotation set: "); - log += n; - addLogMove(LOG_LEVEL_INFO, log); + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("P123 DEBUG Rotation set: "); + log += n; + addLogMove(LOG_LEVEL_INFO, log); + } # endif // PLUGIN_123_DEBUG } @@ -1300,9 +1315,12 @@ void P123_data_struct::setRotation(uint8_t n) { void P123_data_struct::setRotationFlipped(bool flipped) { _flipped = flipped; # ifdef PLUGIN_123_DEBUG - String log = F("P123 DEBUG RotationFlipped set: "); - log += flipped; - addLogMove(LOG_LEVEL_INFO, log); + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("P123 DEBUG RotationFlipped set: "); + log += flipped; + addLogMove(LOG_LEVEL_INFO, log); + } # endif // PLUGIN_123_DEBUG } @@ -1311,10 +1329,10 @@ void P123_data_struct::setRotationFlipped(bool flipped) { */ bool P123_data_struct::isCalibrationActive() { return _useCalibration - && (P123_Settings.top_left.x != 0 - || P123_Settings.top_left.y != 0 - || P123_Settings.bottom_right.x != 0 - || P123_Settings.bottom_right.y != 0); // Enabled and any value != 0 => Active + && (P123_Settings.top_left.x != 0 || + P123_Settings.top_left.y != 0 || + P123_Settings.bottom_right.x != 0 || + P123_Settings.bottom_right.y != 0); // Enabled and any value != 0 => Active } /** @@ -1392,8 +1410,8 @@ bool P123_data_struct::setTouchObjectState(struct EventStruct *event, const Stri if (touchObject.isEmpty()) { return false; } bool success = false; - for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { - if ((!TouchObjects[objectNr].objectName.isEmpty()) + for (size_t objectNr = 0; objectNr < TouchObjects.size(); objectNr++) { + if (!TouchObjects[objectNr].objectName.isEmpty() && touchObject.equalsIgnoreCase(TouchObjects[objectNr].objectName)) { bool currentState = bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED); @@ -1407,17 +1425,20 @@ bool P123_data_struct::setTouchObjectState(struct EventStruct *event, const Stri } } # ifdef PLUGIN_123_DEBUG - String log = F("P123 setTouchObjectState: obj: "); - log += touchObject; - if (success) { - log += F(", new state: "); - log += (state ? F("en") : F("dis")); - log += F("abled."); - } else { - log += F("failed!"); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("P123 setTouchObjectState: obj: "); + log += touchObject; + + if (success) { + log += F(", new state: "); + log += (state ? F("en") : F("dis")); + log += F("abled."); + } else { + log += F("failed!"); + } + addLogMove(LOG_LEVEL_INFO, log); } - addLogMove(LOG_LEVEL_INFO, log); # endif // PLUGIN_123_DEBUG } } @@ -1431,8 +1452,8 @@ bool P123_data_struct::setTouchButtonOnOff(struct EventStruct *event, const Stri if (touchObject.isEmpty()) { return false; } bool success = false; - for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { - if ((!TouchObjects[objectNr].objectName.isEmpty()) + for (size_t objectNr = 0; objectNr < TouchObjects.size(); objectNr++) { + if (!TouchObjects[objectNr].objectName.isEmpty() && touchObject.equalsIgnoreCase(TouchObjects[objectNr].objectName) && bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED) && bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTON)) { @@ -1449,11 +1470,14 @@ bool P123_data_struct::setTouchButtonOnOff(struct EventStruct *event, const Stri } } # ifdef PLUGIN_123_DEBUG - String log = F("P123 setTouchButtonOnOff: obj: "); - log += touchObject; - log += F(", (new) state: "); - log += (state ? F("on") : F("off")); - addLogMove(LOG_LEVEL_INFO, log); + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("P123 setTouchButtonOnOff: obj: "); + log += touchObject; + log += F(", (new) state: "); + log += (state ? F("on") : F("off")); + addLogMove(LOG_LEVEL_INFO, log); + } # endif // PLUGIN_123_DEBUG } } @@ -1492,15 +1516,15 @@ void P123_data_struct::scaleRawToCalibrated(int16_t& x, int16_t& y) { } /** - * Set the desired button group, muxt be between the minimum and maximum found values + * Set the desired button group, must be between the minimum and maximum found values */ bool P123_data_struct::setButtonGroup(const EventStruct *event, int8_t buttonGroup) { if ((buttonGroup >= 0) && (buttonGroup <= _maxButtonGroup)) { if (buttonGroup != _buttonGroup) { - displayButtons(event, _buttonGroup, -2); + displayButtonGroup(event, _buttonGroup, -2); _buttonGroup = buttonGroup; - displayButtons(event, _buttonGroup, -1); + displayButtonGroup(event, _buttonGroup, -1); } return true; } @@ -1508,26 +1532,26 @@ bool P123_data_struct::setButtonGroup(const EventStruct *event, } /** - * increment button group, if max. group > 0 then min. group = 1 + * Increment button group, if max. group > 0 then min. group = 1 */ bool P123_data_struct::incrementButtonGroup(const EventStruct *event) { if (_buttonGroup < _maxButtonGroup) { - displayButtons(event, _buttonGroup, -2); + displayButtonGroup(event, _buttonGroup, -2); _buttonGroup++; - displayButtons(event, _buttonGroup, -1); + displayButtonGroup(event, _buttonGroup, -1); return true; } return false; } /** - * decrement button group, if max. group > 0 then min. group = 1 + * Decrement button group, if max. group > 0 then min. group = 1 */ bool P123_data_struct::decrementButtonGroup(const EventStruct *event) { if (_buttonGroup > _minButtonGroup) { - displayButtons(event, _buttonGroup, -2); + displayButtonGroup(event, _buttonGroup, -2); _buttonGroup--; - displayButtons(event, _buttonGroup, -1); + displayButtonGroup(event, _buttonGroup, -1); return true; } return false; diff --git a/src/src/PluginStructs/P123_data_struct.h b/src/src/PluginStructs/P123_data_struct.h index a1ae3cdb23..0037e8ee94 100644 --- a/src/src/PluginStructs/P123_data_struct.h +++ b/src/src/PluginStructs/P123_data_struct.h @@ -221,9 +221,9 @@ struct P123_data_struct : public PluginTaskData_base int8_t buttonGroup); bool incrementButtonGroup(const EventStruct *event); bool decrementButtonGroup(const EventStruct *event); - void displayButtons(const EventStruct *event, - int8_t buttonGroup, - int8_t mode = 0); + void displayButtonGroup(const EventStruct *event, + int8_t buttonGroup, + int8_t mode = 0); private: From 2786009f1cc9139efa97151c258fe1639b7cb625 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 21 May 2022 18:33:51 +0200 Subject: [PATCH 009/113] [P123] Limit code & bin size for LIMIT_BUILD_SIZE builds --- src/src/Helpers/AdafruitGFX_helper.cpp | 36 +++++++++-- src/src/Helpers/AdafruitGFX_helper.h | 36 +++++++++-- src/src/PluginStructs/P123_data_struct.cpp | 74 ++++++++++++---------- src/src/PluginStructs/P123_data_struct.h | 37 ++++++----- 4 files changed, 125 insertions(+), 58 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index a7b69e7ae6..fa88772b4c 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -65,8 +65,10 @@ const __FlashStringHelper* toString(AdaGFXColorDepth colorDepth) { # if ADAGFX_SUPPORT_7COLOR case AdaGFXColorDepth::SevenColor: return F("eInk - 7 colors"); # endif // if ADAGFX_SUPPORT_7COLOR + # if ADAGFX_SUPPORT_8and16COLOR case AdaGFXColorDepth::EightColor: return F("TFT - 8 colors"); case AdaGFXColorDepth::SixteenColor: return F("TFT - 16 colors"); + # endif // if ADAGFX_SUPPORT_8and16COLOR case AdaGFXColorDepth::FullColor: return F("Full color - 65535 colors"); } return F("None"); @@ -139,9 +141,17 @@ void AdaGFXFormColorDepth(const __FlashStringHelper *id, uint16_t selectedIndex, bool enabled) { # if ADAGFX_SUPPORT_7COLOR + # if ADAGFX_SUPPORT_8and16COLOR const int colorDepthCount = 7 + 1; + # else // if ADAGFX_SUPPORT_8and16COLOR + const int colorDepthCount = 5 + 1; + # endif // if ADAGFX_SUPPORT_8and16COLOR # else // if ADAGFX_SUPPORT_7COLOR + # if ADAGFX_SUPPORT_8and16COLOR const int colorDepthCount = 6 + 1; + # else // if ADAGFX_SUPPORT_8and16COLOR + const int colorDepthCount = 4 + 1; + # endif // if ADAGFX_SUPPORT_8and16COLOR # endif // if ADAGFX_SUPPORT_7COLOR const __FlashStringHelper *colorDepths[colorDepthCount] = { // Be sure to use all available modes from enum! toString(static_cast(0)), // include None @@ -151,8 +161,10 @@ void AdaGFXFormColorDepth(const __FlashStringHelper *id, # if ADAGFX_SUPPORT_7COLOR toString(AdaGFXColorDepth::SevenColor), # endif // if ADAGFX_SUPPORT_7COLOR + # if ADAGFX_SUPPORT_8and16COLOR toString(AdaGFXColorDepth::EightColor), toString(AdaGFXColorDepth::SixteenColor), + # endif // if ADAGFX_SUPPORT_8and16COLOR toString(AdaGFXColorDepth::FullColor) }; const int colorDepthOptions[colorDepthCount] = { @@ -163,8 +175,10 @@ void AdaGFXFormColorDepth(const __FlashStringHelper *id, # if ADAGFX_SUPPORT_7COLOR static_cast(AdaGFXColorDepth::SevenColor), # endif // if ADAGFX_SUPPORT_7COLOR + # if ADAGFX_SUPPORT_8and16COLOR static_cast(AdaGFXColorDepth::EightColor), static_cast(AdaGFXColorDepth::SixteenColor), + # endif // if ADAGFX_SUPPORT_8and16COLOR static_cast(AdaGFXColorDepth::FullColor) }; @@ -540,7 +554,11 @@ String AdafruitGFX_helper::getFeatures() { # endif // if (defined(ADAGFX_ENABLE_BMP_DISPLAY) && ADAGFX_ENABLE_BMP_DISPLAY) # if (defined(ADAGFX_ENABLE_BUTTON_DRAW) && ADAGFX_ENABLE_BUTTON_DRAW) log += F(" btn,"); - # endif // if (defined(ADAGFX_ENABLE_BUTTON_DRAW) && ADAGFX_ENABLE_BUTTON_DRAW) + # endif // if (defined(ADAGFX_ENABLE_BUTTON_DRAW) && ADAGFX_ENABLE_BUTTON_DRAW)` + + if (log.endsWith(F(","))) { + log.remove(log.length() - 1); + } return log; } @@ -1688,13 +1706,17 @@ uint16_t AdaGFXparseColor(String& s, AdaGFXColorDepth colorDepth, bool emptyIsBl } if ((result == -1) || (result == ADAGFX_WHITE)) { // Default & don't convert white + # if ADAGFX_SUPPORT_8and16COLOR + if ( - # if ADAGFX_SUPPORT_7COLOR + # if ADAGFX_SUPPORT_7COLOR (colorDepth >= AdaGFXColorDepth::SevenColor) && - # endif // if ADAGFX_SUPPORT_7COLOR + # endif // if ADAGFX_SUPPORT_7COLOR (colorDepth <= AdaGFXColorDepth::SixteenColor)) { result = static_cast(AdaGFXMonoRedGreyscaleColors::ADAGFXEPD_BLACK); // Monochrome fallback, compatible 7-color - } else { + } else + # endif // if ADAGFX_SUPPORT_8and16COLOR + { if (emptyIsBlack) { result = ADAGFX_BLACK; } else { @@ -1714,12 +1736,14 @@ uint16_t AdaGFXparseColor(String& s, AdaGFXColorDepth colorDepth, bool emptyIsBl result = AdaGFXrgb565ToColor7(result); // Convert break; # endif // if ADAGFX_SUPPORT_7COLOR + # if ADAGFX_SUPPORT_8and16COLOR case AdaGFXColorDepth::EightColor: result = color565((result >> 11 & 0x1F) / 4, (result >> 5 & 0x3F) / 4, (result & 0x1F) / 4); // reduce colors factor 4 break; case AdaGFXColorDepth::SixteenColor: result = color565((result >> 11 & 0x1F) / 2, (result >> 5 & 0x3F) / 2, (result & 0x1F) / 2); // reduce colors factor 2 break; + # endif // if ADAGFX_SUPPORT_8and16COLOR case AdaGFXColorDepth::FullColor: // No color reduction break; @@ -1779,8 +1803,10 @@ void AdaGFXHtmlColorDepthDataList(const __FlashStringHelper *id, break; } # endif // if ADAGFX_SUPPORT_7COLOR + # if ADAGFX_SUPPORT_8and16COLOR case AdaGFXColorDepth::EightColor: // TODO: Sort out the actual 8/16 color options case AdaGFXColorDepth::SixteenColor: + # endif // if ADAGFX_SUPPORT_8and16COLOR case AdaGFXColorDepth::FullColor: { AdaGFXaddHtmlDataListColorOptionValue(ADAGFX_BLACK, colorDepth); @@ -1863,8 +1889,10 @@ const __FlashStringHelper* AdaGFXcolorToString_internal(uint16_t color, break; } # endif // if ADAGFX_SUPPORT_7COLOR + # if ADAGFX_SUPPORT_8and16COLOR case AdaGFXColorDepth::EightColor: case AdaGFXColorDepth::SixteenColor: + # endif // if ADAGFX_SUPPORT_8and16COLOR case AdaGFXColorDepth::FullColor: { switch (color) { diff --git a/src/src/Helpers/AdafruitGFX_helper.h b/src/src/Helpers/AdafruitGFX_helper.h index 2f663be953..1155901f4b 100644 --- a/src/src/Helpers/AdafruitGFX_helper.h +++ b/src/src/Helpers/AdafruitGFX_helper.h @@ -30,8 +30,11 @@ # define ADAGFX_USE_ASCIITABLE 1 // Enable 'asciitable' command (useful for debugging/development) # endif // ifndef ADAGFX_USE_ASCIITABLE # ifndef ADAGFX_SUPPORT_7COLOR -# define ADAGFX_SUPPORT_7COLOR 1 // Do we support 7-Color displays? +// # define ADAGFX_SUPPORT_7COLOR 1 // Do we support 7-Color displays? # endif // ifndef ADAGFX_SUPPORT_7COLOR +# ifndef ADAGFX_SUPPORT_8and16COLOR +// # define ADAGFX_SUPPORT_8and16COLOR 1 // Do we support 8 and 16-Color displays? +# endif // ifndef ADAGFX_SUPPORT_8and16COLOR # ifndef ADAGFX_FONTS_INCLUDED # define ADAGFX_FONTS_INCLUDED 1 // 3 extra fonts, also controls enable/disable of below 8pt/12pt fonts # endif // ifndef ADAGFX_FONTS_INCLUDED @@ -89,6 +92,9 @@ # ifdef ADAGFX_USE_ASCIITABLE # undef ADAGFX_USE_ASCIITABLE # endif // ifdef ADAGFX_USE_ASCIITABLE +# ifdef ADAGFX_SUPPORT_8and16COLOR +# undef ADAGFX_SUPPORT_8and16COLOR +# endif // ifdef ADAGFX_SUPPORT_8and16COLOR // # ifdef ADAGFX_ENABLE_BMP_DISPLAY // # undef ADAGFX_ENABLE_BMP_DISPLAY // # endif // ifdef ADAGFX_ENABLE_BMP_DISPLAY @@ -113,6 +119,12 @@ # ifndef ADAGFX_FONTS_EXTRA_20PT_INCLUDED # define ADAGFX_FONTS_EXTRA_20PT_INCLUDED # endif // ifndef ADAGFX_FONTS_EXTRA_20PT_INCLUDED +# ifndef ADAGFX_SUPPORT_7COLOR +# define ADAGFX_SUPPORT_7COLOR 1 +# endif // ifndef ADAGFX_SUPPORT_7COLOR +# ifndef ADAGFX_SUPPORT_8and16COLOR +# define ADAGFX_SUPPORT_8and16COLOR 1 +# endif // ifndef ADAGFX_SUPPORT_8and16COLOR # endif // ifdef PLUGIN_SET_MAX # define ADAGFX_PARSE_PREFIX F("~") // Subcommand-trigger prefix and postfix strings @@ -174,11 +186,21 @@ enum class AdaGFXTextPrintMode : uint8_t { }; # if ADAGFX_SUPPORT_7COLOR -# define ADAGFX_COLORDEPTH_COUNT 7 -# define ADAGFX_MONOCOLORS_COUNT 4 +# if ADAGFX_SUPPORT_8and16COLOR +# define ADAGFX_COLORDEPTH_COUNT 7 +# define ADAGFX_MONOCOLORS_COUNT 4 +# else // if ADAGFX_SUPPORT_8and16COLOR +# define ADAGFX_COLORDEPTH_COUNT 5 +# define ADAGFX_MONOCOLORS_COUNT 4 +# endif // if ADAGFX_SUPPORT_8and16COLOR # else // if ADAGFX_SUPPORT_7COLOR -# define ADAGFX_COLORDEPTH_COUNT 6 -# define ADAGFX_MONOCOLORS_COUNT 3 +# if ADAGFX_SUPPORT_8and16COLOR +# define ADAGFX_COLORDEPTH_COUNT 6 +# define ADAGFX_MONOCOLORS_COUNT 3 +# else // if ADAGFX_SUPPORT_8and16COLOR +# define ADAGFX_COLORDEPTH_COUNT 4 +# define ADAGFX_MONOCOLORS_COUNT 3 +# endif // if ADAGFX_SUPPORT_8and16COLOR # endif // if ADAGFX_SUPPORT_7COLOR enum class AdaGFXColorDepth : uint16_t { Monochrome = 2u, // Black & white @@ -187,9 +209,11 @@ enum class AdaGFXColorDepth : uint16_t { # if ADAGFX_SUPPORT_7COLOR SevenColor = 7u, // Black, white, red, yellow, blue, green, orange # endif // if ADAGFX_SUPPORT_7COLOR + # if ADAGFX_SUPPORT_8and16COLOR EightColor = 8u, // 8 regular colors SixteenColor = 16u, // 16 colors - FullColor = 65535u // 65535 colors (max. supported by RGB565) + # endif // if ADAGFX_SUPPORT_8and16COLOR + FullColor = 65535u // 65535 colors (max. supported by RGB565) }; # if ADAGFX_ENABLE_BUTTON_DRAW diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index b6c8fff55a..f16fba7377 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -142,7 +142,7 @@ void P123_data_struct::displayButtonGroup(const EventStruct *event, } generateObjectEvent(event, objectNr, state, mode < 0, mode <= -2 ? -1 : 1); } - # ifdef XX_PLUGIN_123_DEBUG // Temporarily disabled + # ifdef XX_PLUGIN_123_DEBUG // TODO Temporarily disabled if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("P123: button init, state: "); @@ -202,10 +202,12 @@ int P123_data_struct::parseStringToInt(const String& string, uint8_t indexFind, bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { addFormSubHeader(F("Touch configuration")); - addFormCheckBox(F("Flip rotation 180°"), F("touch_rotation_flipped"), bitRead(P123_Settings.flags, P123_FLAGS_ROTATION_FLIPPED)); + addFormCheckBox(F("Flip rotation 180°"), F("tch_rotation_flipped"), bitRead(P123_Settings.flags, P123_FLAGS_ROTATION_FLIPPED)); + # ifndef LIMIT_BUILD_SIZE addFormNote(F("Some touchscreens are mounted 180° rotated on the display.")); + # endif // ifndef LIMIT_BUILD_SIZE - addFormNumericBox(F("Touch minimum pressure"), F("touch_treshold"), + addFormNumericBox(F("Touch minimum pressure"), F("tch_treshold"), P123_Settings.treshold, 0, 255); uint8_t choice3 = 0u; @@ -224,17 +226,20 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { F("Objectnames, X, Y and Z") }; int optionValues3[P123_EVENTS_OPTIONS] = { 0, 1, 3, 4, 5, 7 }; // Already used as a bitmap! - addFormSelector(F("Events"), F("touch_events"), P123_EVENTS_OPTIONS, options3, optionValues3, choice3); + addFormSelector(F("Events"), F("tch_events"), P123_EVENTS_OPTIONS, options3, optionValues3, choice3); - addFormCheckBox(F("Initial Objectnames events"), F("touch_init_objectevent"), bitRead(P123_Settings.flags, P123_FLAGS_INIT_OBJECTEVENT)); + addFormCheckBox(F("Initial Objectnames events"), F("tch_init_objectevent"), bitRead(P123_Settings.flags, P123_FLAGS_INIT_OBJECTEVENT)); addFormNote(F("Will send state -1 but only for enabled On/Off button objects.")); } - addFormCheckBox(F("Prevent duplicate events"), F("touch_deduplicate"), bitRead(P123_Settings.flags, P123_FLAGS_DEDUPLICATE)); + addFormCheckBox(F("Prevent duplicate events"), F("tch_deduplicate"), bitRead(P123_Settings.flags, P123_FLAGS_DEDUPLICATE)); + + # ifndef LIMIT_BUILD_SIZE if (!Settings.UseRules) { addFormNote(F("Tools / Advanced / Rules must be enabled for events to be fired.")); } + # endif // ifndef LIMIT_BUILD_SIZE addFormSubHeader(F("Calibration")); @@ -242,7 +247,7 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { const __FlashStringHelper *noYesOptions[2] = { F("No"), F("Yes") }; int noYesOptionValues[2] = { 0, 1 }; addFormSelector(F("Calibrate to screen resolution"), - F("touch_use_calibration"), + F("tch_use_calibration"), 2, noYesOptions, noYesOptionValues, @@ -263,24 +268,24 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { html_TR_TD(); addHtml(F("Top-left")); html_TD(); - addNumericBox(F("touch_cal_tl_x"), + addNumericBox(F("tch_cal_tl_x"), P123_Settings.top_left.x, 0, 65535); html_TD(); - addNumericBox(F("touch_cal_tl_y"), + addNumericBox(F("tch_cal_tl_y"), P123_Settings.top_left.y, 0, 65535); html_TD(); addHtml(F("Bottom-right")); html_TD(); - addNumericBox(F("touch_cal_br_x"), + addNumericBox(F("tch_cal_br_x"), P123_Settings.bottom_right.x, 0, 65535); html_TD(); - addNumericBox(F("touch_cal_br_y"), + addNumericBox(F("tch_cal_br_y"), P123_Settings.bottom_right.y, 0, 65535); @@ -288,7 +293,7 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { html_end_table(); } - addFormCheckBox(F("Enable logging for calibration"), F("touch_log_calibration"), + addFormCheckBox(F("Enable logging for calibration"), F("tch_log_calibration"), P123_Settings.logEnabled); addFormSubHeader(F("Touch objects")); @@ -365,7 +370,7 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { html_end_table(); } { - addFormNumericBox(F("Initial button group"), F("touch_initial_group"), + addFormNumericBox(F("Initial button group"), F("tch_initial_group"), get8BitFromUL(P123_Settings.flags, P123_FLAGS_INITIAL_GROUP), 0, P123_MAX_BUTTON_GROUPS # ifdef P123_USE_TOOLTIPS , F("Initial group") @@ -608,9 +613,11 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { html_TD(2); // Start with some blank columns # ifdef P123_USE_EXTENDED_TOUCH { + # ifdef P123_USE_TOOLTIPS String buttonGroupToolTip = F("Button-group [0.."); buttonGroupToolTip += P123_MAX_BUTTON_GROUPS; buttonGroupToolTip += ']'; + # endif // ifdef P123_USE_TOOLTIPS addNumericBox(getPluginCustomArgName(objectNr + 1600), get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_GROUP), 0, P123_MAX_BUTTON_GROUPS # ifdef P123_USE_TOOLTIPS @@ -698,7 +705,7 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { } html_end_table(); - addFormNumericBox(F("Debounce delay for On/Off buttons"), F("touch_debounce"), + addFormNumericBox(F("Debounce delay for On/Off buttons"), F("tch_debounce"), P123_Settings.debounceMs, 0, 255); addUnit(F("0-255 msec.")); } @@ -718,31 +725,31 @@ bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { uint32_t lSettings = 0u; - bitWrite(lSettings, P123_FLAGS_SEND_XY, bitRead(getFormItemInt(F("touch_events")), P123_FLAGS_SEND_XY)); - bitWrite(lSettings, P123_FLAGS_SEND_Z, bitRead(getFormItemInt(F("touch_events")), P123_FLAGS_SEND_Z)); - bitWrite(lSettings, P123_FLAGS_SEND_OBJECTNAME, bitRead(getFormItemInt(F("touch_events")), P123_FLAGS_SEND_OBJECTNAME)); - bitWrite(lSettings, P123_FLAGS_ROTATION_FLIPPED, isFormItemChecked(F("touch_rotation_flipped"))); - bitWrite(lSettings, P123_FLAGS_DEDUPLICATE, isFormItemChecked(F("touch_deduplicate"))); - bitWrite(lSettings, P123_FLAGS_INIT_OBJECTEVENT, isFormItemChecked(F("touch_init_objectevent"))); + bitWrite(lSettings, P123_FLAGS_SEND_XY, bitRead(getFormItemInt(F("tch_events")), P123_FLAGS_SEND_XY)); + bitWrite(lSettings, P123_FLAGS_SEND_Z, bitRead(getFormItemInt(F("tch_events")), P123_FLAGS_SEND_Z)); + bitWrite(lSettings, P123_FLAGS_SEND_OBJECTNAME, bitRead(getFormItemInt(F("tch_events")), P123_FLAGS_SEND_OBJECTNAME)); + bitWrite(lSettings, P123_FLAGS_ROTATION_FLIPPED, isFormItemChecked(F("tch_rotation_flipped"))); + bitWrite(lSettings, P123_FLAGS_DEDUPLICATE, isFormItemChecked(F("tch_deduplicate"))); + bitWrite(lSettings, P123_FLAGS_INIT_OBJECTEVENT, isFormItemChecked(F("tch_init_objectevent"))); # ifdef P123_USE_EXTENDED_TOUCH - set8BitToUL(lSettings, P123_FLAGS_INITIAL_GROUP, getFormItemInt(F("touch_initial_group"))); // Button group + set8BitToUL(lSettings, P123_FLAGS_INITIAL_GROUP, getFormItemInt(F("tch_initial_group"))); // Button group # endif // ifdef P123_USE_EXTENDED_TOUCH - config += getFormItemInt(F("touch_use_calibration")); + config += getFormItemInt(F("tch_use_calibration")); config += P123_SETTINGS_SEPARATOR; - config += isFormItemChecked(F("touch_log_calibration")) ? 1 : 0; + config += isFormItemChecked(F("tch_log_calibration")) ? 1 : 0; config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("touch_cal_tl_x")); + config += getFormItemInt(F("tch_cal_tl_x")); config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("touch_cal_tl_y")); + config += getFormItemInt(F("tch_cal_tl_y")); config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("touch_cal_br_x")); + config += getFormItemInt(F("tch_cal_br_x")); config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("touch_cal_br_y")); + config += getFormItemInt(F("tch_cal_br_y")); config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("touch_debounce")); + config += getFormItemInt(F("tch_debounce")); config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("touch_treshold")); + config += getFormItemInt(F("tch_treshold")); config += P123_SETTINGS_SEPARATOR; config += ull2String(lSettings); config += P123_SETTINGS_SEPARATOR; @@ -998,7 +1005,10 @@ void P123_data_struct::generateObjectEvent(const EventStruct *event, const int8_t onOffState, const bool groupSwitch, const int8_t factor) { - if ((objectIndex < 0) || (objectIndex >= TouchObjects.size())) { return; } // Range check + if ((objectIndex < 0) || // Range check + (objectIndex >= static_cast(TouchObjects.size()))) { + return; + } String eventCommand; eventCommand.reserve(48); @@ -1171,8 +1181,8 @@ void P123_data_struct::loadTouchObjects(const EventStruct *event) { P123_Settings.colorOff = ADAGFX_RED; P123_Settings.colorCaption = ADAGFX_WHITE; P123_Settings.colorBorder = ADAGFX_WHITE; - P123_Settings.colorDisabled = 0x9410; - P123_Settings.colorDisabledCaption = 0x5A69; + P123_Settings.colorDisabled = P123_DEFAULT_COLOR_DISABLED; + P123_Settings.colorDisabledCaption = P123_DEFAULT_COLOR_DISABLED_CAPTION; } # endif // ifdef P123_USE_EXTENDED_TOUCH diff --git a/src/src/PluginStructs/P123_data_struct.h b/src/src/PluginStructs/P123_data_struct.h index 0037e8ee94..baf7c0fd31 100644 --- a/src/src/PluginStructs/P123_data_struct.h +++ b/src/src/PluginStructs/P123_data_struct.h @@ -9,7 +9,9 @@ # include -# define PLUGIN_123_DEBUG // Additional debugging information +# ifndef LIMIT_BUILD_SIZE +// # define PLUGIN_123_DEBUG // Additional debugging information +# endif // ifndef LIMIT_BUILD_SIZE # define P123_USE_TOOLTIPS // Enable tooltips in UI @@ -110,14 +112,14 @@ # endif // ifdef P123_USE_EXTENDED_TOUCH // Settings array field offsets: Touch objects -# define P123_OBJECT_INDEX_START (P123_CALIBRATION_START + 1) -# define P123_OBJECT_INDEX_END (P123_ARRAY_SIZE - (P123_CALIBRATION_START + 1)) -# define P123_OBJECT_NAME 1 // Name (String 14) (parseString index starts at 1) -# define P123_OBJECT_FLAGS 2 // Flags (uint32_t) -# define P123_OBJECT_COORD_TOP_X 3 // Top X (uint16_t) -# define P123_OBJECT_COORD_TOP_Y 4 // Top Y -# define P123_OBJECT_COORD_WIDTH 5 // Width -# define P123_OBJECT_COORD_HEIGHT 6 // Height +# define P123_OBJECT_INDEX_START (P123_CALIBRATION_START + 1) +# define P123_OBJECT_INDEX_END (P123_ARRAY_SIZE - (P123_CALIBRATION_START + 1)) +# define P123_OBJECT_NAME 1 // Name (String 14) (parseString index starts at 1) +# define P123_OBJECT_FLAGS 2 // Flags (uint32_t) +# define P123_OBJECT_COORD_TOP_X 3 // Top X (uint16_t) +# define P123_OBJECT_COORD_TOP_Y 4 // Top Y +# define P123_OBJECT_COORD_WIDTH 5 // Width +# define P123_OBJECT_COORD_HEIGHT 6 // Height # ifdef P123_USE_EXTENDED_TOUCH # define P123_OBJECT_COLOR_ON 7 // Color ON (rgb565, uint16_t) # define P123_OBJECT_COLOR_OFF 8 // Color OFF @@ -129,13 +131,16 @@ # define P123_OBJECT_COLOR_DISABCAPT 14 // Disabled Caption Color # endif // ifdef P123_USE_EXTENDED_TOUCH -# define P123_OBJECT_FLAG_ENABLED 0 // Enabled -# define P123_OBJECT_FLAG_BUTTON 1 // Button behavior -# define P123_OBJECT_FLAG_INVERTED 2 // Inverted button -# define P123_OBJECT_FLAG_FONTSCALE 3 // 4 bits used as button alignment -# define P123_OBJECT_FLAG_BUTTONTYPE 7 // 8 bits used as button type -# define P123_OBJECT_FLAG_GROUP 15 // 8 bits used as button group -# define P123_OBJECT_FLAG_ACTIONGROUP 23 // 8 bits, 6 bits used as action group 0..63, 2 bits used as action option +# define P123_OBJECT_FLAG_ENABLED 0 // Enabled +# define P123_OBJECT_FLAG_BUTTON 1 // Button behavior +# define P123_OBJECT_FLAG_INVERTED 2 // Inverted button +# define P123_OBJECT_FLAG_FONTSCALE 3 // 4 bits used as button alignment +# define P123_OBJECT_FLAG_BUTTONTYPE 7 // 8 bits used as button type +# define P123_OBJECT_FLAG_GROUP 15 // 8 bits used as button group +# define P123_OBJECT_FLAG_ACTIONGROUP 23 // 8 bits, 6 bits used as action group 0..63, 2 bits used as action option + +# define P123_DEFAULT_COLOR_DISABLED 0x9410 +# define P123_DEFAULT_COLOR_DISABLED_CAPTION 0x5A69 // Lets define our own coordinate point struct tP123_Point From 8deee266b67a207e5c9c308692965126455bc62a Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 4 Jun 2022 12:47:12 +0200 Subject: [PATCH 010/113] [AdaGFX] Improve btn subcommand, slim down bin size, other minor improvements --- docs/source/Plugin/AdaGFX_commands.repl | 44 ++++ src/src/CustomBuild/define_plugin_sets.h | 3 - src/src/Helpers/AdafruitGFX_helper.cpp | 322 ++++++++++++++--------- src/src/Helpers/AdafruitGFX_helper.h | 44 +++- 4 files changed, 279 insertions(+), 134 deletions(-) diff --git a/docs/source/Plugin/AdaGFX_commands.repl b/docs/source/Plugin/AdaGFX_commands.repl index dd86b1a34e..1c928095bd 100644 --- a/docs/source/Plugin/AdaGFX_commands.repl +++ b/docs/source/Plugin/AdaGFX_commands.repl @@ -221,3 +221,47 @@ The file can be read from SD-card, when available, and the bmp file is not found on the internal file storage. " + " + ``,btn,,,,,,,,,,,,,,,,,,,,,`` + "," + As a companion to the ESPEasy_TouchHelper, the AdafruitGFX_helper takes care of drawing button objects via this subcommand. + + All arguments *must* be provided, though most can be empty, but all separator-commas must still be provided. + + * ``state`` : 0 = off, 1 = on, -1 = off + disabled, -2 = on + disabled. + + * ``mode`` : 0 = normal, -1 = initial, -2 = clear button area. + + * ``x,y,w,h`` : button left-top coordinate, width and height. + + * ``id`` : object id nr. Not used for drawing the button, but passed to be used in rules from the ESPEasy_TouchHelper. + + * ``type`` : combination (addition/and operation) of ``button type`` (bits 0..3) and ``button layout`` (bits 4..7), when negative (multiplied by -1) will clear the button area. + + * ``button types`` : 0 = none, 0x01 = rectangle, 0x02 = rounded rectangle, 0x03 = circle, 0x04 = left arrow, 0x05 = up arrow, 0x06 = right arrow, 0x07 = down arrow. + + * ``button layouts`` : 0 = center aligned, 0x10 = left aligned, 0x20 = top aligned, 0x30 = right aligned, 0x40 = bottom aligned, 0x50 = left top aligned, 0x60 = right top aligned, 0x70 = right bottom aligned, 0x80 = left bottom aligned, 0x90 = no caption, 0xA0 = ``bitmap`` (see ``ONcaption`` / ``OFFcaption``). + + * ``ONcolor`` / ``OFFcolor`` : fill color when button state is on or off and enabled. + + * ``CaptionColor`` : color used for caption text. + + * ``fontsize`` : size of the font for writing the caption, range 1..10. + + * ``ONcaption`` / ``OFFcaption`` : The caption to show when state is on or off, when empty the objectname will be used. For ``button layout`` = ``bitmap`` a .bmp filename should be entered, that is available on the filesystem, optionally *prefixed* with an ``x`` and ``y`` offset, in pixels, to enable f.e. drawing a small bitmap centered on a button. The format with these optional arguments is: ``[[,],]bmpfile.bmp`` + + * If a caption contains space(s), comma(s) or quote(s), it *must* be quoted with a different quote (double/single/backtick). + * Captions can use variables as available in rules, like plugin values via ``[taskname#valuename]``, ``%vNN%``, ``%%`` and ``{}`` formats. + + * ``BorderColor``, ``DisabledColor``, ``DisabledCaptionColor`` : A separate border color can be specified, and a fill-color and caption color for disabled buttons. + + * ``TaskIndex`` : The Task number for the display the button is to be drawn on. Not used, but passed to be used in rules from the ESPEasy_TouchHelper. + + * ``Group`` : The group this button is a member of. Not used for drawing the button, but passed to be used in rules from the ESPEasy_TouchHelper. + + * ``SelectGroup`` : The group that will be activated by this button. Not used for drawing the button, but passed to be used in rules from the ESPEasy_TouchHelper. + + * ``objectname`` : **Required** The name of the button object, will be used as a caption if no ``ONcaption`` and/or ``OFFcaption`` are provided. *Not required* if both an ``ONcaption`` and ``OFFcaption`` are provided. + + NB: This command wil only *draw* a button, it will not respond to any action. The action is usually provided by a touch screen like :ref:`P099_page` and :ref:`P123_page`. + " diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index e3b5ad3c15..12814856ca 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -1253,9 +1253,6 @@ To create/register a plugin, you have to : #if !defined(LIMIT_BUILD_SIZE) && (defined(ESP8266) || !(ESP_IDF_VERSION_MAJOR > 3)) #define LIMIT_BUILD_SIZE // Reduce buildsize (on ESP8266 / pre-IDF4.x) to fit in all Display plugins #endif - #ifndef USES_ADAFRUITGFX_HELPER - #define USES_ADAFRUITGFX_HELPER - #endif #ifndef USES_P012 #define USES_P012 // LCD #endif diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index fa88772b4c..e0fba9832c 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -203,7 +203,9 @@ void AdaGFXFormRotation(const __FlashStringHelper *id, void AdaGFXFormTextBackgroundFill(const __FlashStringHelper *id, uint8_t selectedIndex) { addFormCheckBox(F("Background-fill for text"), id, selectedIndex); + # ifndef LIMIT_BUILD_SIZE addFormNote(F("Fill entire line-height with background color.")); + # endif // ifndef LIMIT_BUILD_SIZE } /***************************************************************************************** @@ -212,7 +214,9 @@ void AdaGFXFormTextBackgroundFill(const __FlashStringHelper *id, void AdaGFXFormTextColRowMode(const __FlashStringHelper *id, bool selectedState) { addFormCheckBox(F("Text Coordinates in col/row"), id, selectedState); + # ifndef LIMIT_BUILD_SIZE addFormNote(F("Unchecked: Coordinates in pixels. Applies only to 'txp', 'txz' and 'txtfull' subcommands.")); + # endif // ifndef LIMIT_BUILD_SIZE } /***************************************************************************************** @@ -221,7 +225,9 @@ void AdaGFXFormTextColRowMode(const __FlashStringHelper *id, void AdaGFXFormOnePixelCompatibilityOption(const __FlashStringHelper *id, uint8_t selectedIndex) { addFormCheckBox(F("Use -1px offset for txp & txtfull"), id, selectedIndex); + # ifndef LIMIT_BUILD_SIZE addFormNote(F("This is for compatibility with the original plugin implementation.")); + # endif // ifndef LIMIT_BUILD_SIZE } /***************************************************************************************** @@ -238,8 +244,12 @@ void AdaGFXFormForeAndBackColors(const __FlashStringHelper *foregroundId, addFormTextBox(F("Foreground color"), foregroundId, color, 11); color = AdaGFXcolorToString(backgroundColor, colorDepth); addFormTextBox(F("Background color"), backgroundId, color, 11); + # ifndef LIMIT_BUILD_SIZE addFormNote(F("Use Color name, '#RGB565' (# + 1..4 hex nibbles) or '#RRGGBB' (# + 6 hex nibbles RGB color).")); addFormNote(F("NB: Colors stored as RGB565 value!")); + # else // ifndef LIMIT_BUILD_SIZE + addFormNote(F("Use Color name, # + 1..4 hex RGB565 or # + 6 hex nibbles RGB color.")); + # endif // ifndef LIMIT_BUILD_SIZE } /***************************************************************************************** @@ -292,9 +302,6 @@ void AdaGFXFormFontScaling(const __FlashStringHelper *fontScalingId, String AdaGFXparseTemplate(const String & tmpString, uint8_t lineSize, AdafruitGFX_helper *gfxHelper) { - // Änderung WDS: Tabelle vorerst Abgeschaltet !!!! - // Perform some specific changes for LCD display - // https://www.letscontrolit.com/forum/viewtopic.php?t=2368 # if ADAGFX_PARSE_SUBCOMMAND String result = tmpString; @@ -396,6 +403,23 @@ String AdaGFXparseTemplate(const String & tmpString, const char quart[3] = { 0xc2, 0xbc, 0 }; // Unicode quart 1/4 symbol const char quart_ascii[2] = { 0xac, 0 }; // quart 1/4 symbol result.replace(quart, quart_ascii); + + const char sup2[3] = { 0xc2, 0xb2, 0 }; // Unicode superscript 2 symbol + const char sup2_ascii[2] = { 0xfc, 0 }; // superscript 2 symbol + result.replace(sup2, sup2_ascii); + + // Unsupported characters, replace by something useful + const char sup1[3] = { 0xc2, 0xb9, 0 }; // Unicode superscript 1 symbol + const char sup1_ascii[2] = { 0x31, 0 }; // regular 1 (missing from font) + result.replace(sup1, sup1_ascii); + + const char sup3[3] = { 0xc2, 0xb3, 0 }; // Unicode superscript 3 symbol + const char sup3_ascii[2] = { 0x33, 0 }; // regular 3 (missing from font) + result.replace(sup3, sup3_ascii); + + const char frac34[3] = { 0xc2, 0xbe, 0 }; // Unicode fraction 3/4 symbol + const char frac34_ascii[2] = { 0x5c, 0 }; // regular \ (missing from font) + result.replace(frac34, frac34_ascii); delay(0); } @@ -433,11 +457,42 @@ String AdaGFXparseTemplate(const String & tmpString, result.replace(divide_uni, divide_ascii); const char umlaut_sz_uni[3] = { 0xc3, 0x9f, 0 }; // Unicode Umlaute sz - const char umlaut_sz_ascii[2] = { 0xe0, 0 }; // Umlaute + const char umlaut_sz_ascii[2] = { 0xe0, 0 }; // Umlaute B result.replace(umlaut_sz_uni, umlaut_sz_ascii); + + // Unsupported characters, replace by something useful + const char times[3] = { 0xc3, 0x97, 0 }; // Unicode multiplication symbol + const char times_ascii[2] = { 0x78, 0 }; // regular x (missing from font) + result.replace(times, times_ascii); delay(0); } + // Handle '{0xNN...}' hex values in template, where NN can be any hex value from 01..FF (practically 20..FF). + int16_t hexPrefix = 0; + int16_t hexPostfix; + const String hexSeparators = F(" ,.:;-"); + + while (((hexPrefix = result.indexOf(F("{0x"), hexPrefix)) > -1) && + ((hexPostfix = result.indexOf('}', hexPrefix)) > -1)) { + String replace; + + for (int16_t ci = hexPrefix + 3; ci < hexPostfix - 1; ci += 2) { // Multiple of 2 only + uint32_t hexValue = hexToUL(result.substring(ci, ci + 2)); + + if (hexValue > 0) { + replace += static_cast(hexValue); + } + + while (hexSeparators.indexOf(result.substring(ci + 2, ci + 3)) > -1 && ci < hexPostfix) { + ci++; + } + } + + if (!replace.isEmpty()) { + result.replace(result.substring(hexPrefix, hexPostfix + 1), replace); + } + } + for (uint16_t l = result.length(); l > 0 && isSpace(result[l - 1]); l--) { // Right-trim result.remove(l - 1); } @@ -506,8 +561,7 @@ AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_SPITFT *display, # endif // ifdef ADAGFX_ENABLE_BMP_DISPLAY -void AdafruitGFX_helper::initialize() -{ +void AdafruitGFX_helper::initialize() { _trigger.toLowerCase(); // store trigger in lowercase # ifndef BUILD_NO_DEBUG @@ -1256,57 +1310,64 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } # endif // if ADAGFX_ENABLE_BMP_DISPLAY # if ADAGFX_ENABLE_BUTTON_DRAW - else if (subcommand.equals(F("btn")) && (argCount >= 7) && (nParams[6] != 0)) - { // btn,state,x,y,w,h,id,type[,ONclr,OFFclr,Captionclr,fontscale,ONcaption,OFFcapt,Borderclr,DisabClr,DisabCaptclr],TaskIndex,Group,SelGrp,objectname - // ev: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17,18,19,20 - // nP: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16,17,18,19 + else if (subcommand.equals(F("btn")) && (argCount >= 8) && (nParams[7] != 0)) + { // btn,state,m,x,y,w,h,id,type[,ONclr,OFFclr,Captionclr,fontscale,ONcaption,OFFcapt,Borderclr,DisabClr,DisabCaptclr],TaskIndex,Group,SelGrp,objectname + // ev: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17,18,19,20,21 + // nP: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16,17,18,19,20 // : Draw a button - // state: -2 = disabled, -1 = initial (off), 0 = off, 1 = on - // type & 0x0F: 0 = none, < 0 = clear area, 1 = rectangle, 2 = rounded rect., 3 = circle, + // m=mode: -2 = disabled, -1 = initial, 0 = default + // state: 0 = off, 1 = on, -2 = off + disabled, -1 = on + disabled + // id: < 0 = clear area + // type & 0x0F: 0 = none, 1 = rectangle, 2 = rounded rect., 3 = circle, // type & 0xF0 = CenterAligned, LeftAligned, TopAligned, RightAligned, BottomAligned, LeftTopAligned, RightTopAligned, // RightBottomAligned, LeftBottomAligned, NoCaption // (*clr = color, TaskIndex, Group and SelGrp are ignored) # if ADAGFX_ARGUMENT_VALIDATION - if (invalidCoordinates(nParams[1], nParams[2]) || - invalidCoordinates(nParams[1] + nParams[3], nParams[2] + nParams[4])) { + if (invalidCoordinates(nParams[2], nParams[3]) || + invalidCoordinates(nParams[2] + nParams[4], nParams[3] + nParams[5])) { success = false; } else # endif // if ADAGFX_ARGUMENT_VALIDATION { // All checked out OK // Default values - uint16_t onColor = ADAGFX_GREEN; + uint16_t onColor = ADAGFX_BLUE; uint16_t offColor = ADAGFX_RED; uint16_t captionColor = ADAGFX_WHITE; - uint8_t fontScale = 2; + uint8_t fontScale = 0; uint16_t borderColor = ADAGFX_WHITE; - uint16_t disabledColor = 0x9410; - uint16_t disabledCaptionColor = 0x5A69; + uint16_t disabledColor = 0x9410; // Medium grey + uint16_t disabledCaptionColor = 0x5A69; // Dark grey - if (!sParams[7].isEmpty()) { onColor = AdaGFXparseColor(sParams[7], _colorDepth); } + if (!sParams[8].isEmpty()) { onColor = AdaGFXparseColor(sParams[8], _colorDepth); } - if (!sParams[8].isEmpty()) { offColor = AdaGFXparseColor(sParams[8], _colorDepth); } + if (!sParams[9].isEmpty()) { offColor = AdaGFXparseColor(sParams[9], _colorDepth); } - if (!sParams[9].isEmpty()) { captionColor = AdaGFXparseColor(sParams[9], _colorDepth); } + if (!sParams[10].isEmpty()) { captionColor = AdaGFXparseColor(sParams[10], _colorDepth); } - if (nParams[12] > 0) { fontScale = nParams[12]; } + if (nParams[11] > 0) { fontScale = nParams[11]; } - if (!sParams[13].isEmpty()) { borderColor = AdaGFXparseColor(sParams[13], _colorDepth); } + if (!sParams[14].isEmpty()) { borderColor = AdaGFXparseColor(sParams[14], _colorDepth); } - if (!sParams[14].isEmpty()) { disabledColor = AdaGFXparseColor(sParams[14], _colorDepth); } + if (!sParams[15].isEmpty()) { disabledColor = AdaGFXparseColor(sParams[15], _colorDepth); } - if (!sParams[15].isEmpty()) { disabledCaptionColor = AdaGFXparseColor(sParams[15], _colorDepth); } + if (!sParams[16].isEmpty()) { disabledCaptionColor = AdaGFXparseColor(sParams[16], _colorDepth); } uint16_t fillColor = onColor; uint16_t textColor = captionColor; - bool clearArea = nParams[6] < 0; - nParams[6] = std::abs(nParams[6]); + bool clearArea = nParams[7] < 0; + nParams[7] = std::abs(nParams[7]); - // Check state: -2, -1, 0, 1 to select used colors - if ((nParams[0] == 0) || (nParams[0] == -1)) { + Button_type_e buttonType = static_cast(nParams[7] & 0x0F); + Button_layout_e buttonLayout = static_cast(nParams[7] & 0xF0); + + // Check mode & state: -2, -1, 0, 1 to select used colors + if (nParams[0] == 0) { fillColor = offColor; - } else if (nParams[0] == -2) { + } + + if ((nParams[1] == -2) || (nParams[0] < 0)) { fillColor = disabledColor; textColor = disabledCaptionColor; } else if (clearArea) { @@ -1314,131 +1375,79 @@ bool AdafruitGFX_helper::processCommand(const String& string) { borderColor = _bgcolor; } - if ((static_cast(nParams[6] & 0x0F) != Button_type_e::None) || + // Clear the area? + if ((buttonType != Button_type_e::None) || clearArea) { - _display->fillRect(nParams[1], nParams[2], nParams[3], nParams[4], _bgcolor); + drawButtonShape(buttonType, + nParams[2], nParams[3], nParams[4], nParams[5], + _bgcolor, _bgcolor); } // Check button-type bits (mask: 0x0F) to draw correct shape if (!clearArea) { - switch (static_cast(nParams[6] & 0x0F)) { - case Button_type_e::Square: // Rectangle - { - _display->fillRect(nParams[1], nParams[2], nParams[3], nParams[4], fillColor); - _display->drawRect(nParams[1], nParams[2], nParams[3], nParams[4], borderColor); - break; - } - case Button_type_e::Rounded: // Rounded Rectangle - { - int16_t radius = (nParams[3] + nParams[4]) / 20; // 10 % corner radius - _display->fillRoundRect(nParams[1], nParams[2], nParams[3], nParams[4], radius, fillColor); - _display->drawRoundRect(nParams[1], nParams[2], nParams[3], nParams[4], radius, borderColor); - break; - } - case Button_type_e::Circle: // Circle - { - int16_t radius = (nParams[3] + nParams[4]) / 4; // average radius - _display->fillCircle(nParams[1] + (nParams[3] / 2), nParams[2] + (nParams[4] / 2), radius, fillColor); - _display->drawCircle(nParams[1] + (nParams[3] / 2), nParams[2] + (nParams[4] / 2), radius, borderColor); - break; - } - case Button_type_e::ArrowLeft: - { // draw: left-center, right-top, right-bottom - _display->fillTriangle(nParams[1], nParams[2] + nParams[4] / 2, nParams[1] + nParams[3], nParams[2], - nParams[1] + nParams[3], nParams[2] + nParams[4], fillColor); - _display->drawTriangle(nParams[1], nParams[2] + nParams[4] / 2, nParams[1] + nParams[3], nParams[2], - nParams[1] + nParams[3], nParams[2] + nParams[4], borderColor); - break; - } - case Button_type_e::ArrowUp: - { // draw: top-center, right-bottom, left-bottom - _display->fillTriangle(nParams[1] + nParams[3] / 2, nParams[2], nParams[1] + nParams[3], nParams[2] + nParams[4], - nParams[1], nParams[2] + nParams[4], fillColor); - _display->drawTriangle(nParams[1] + nParams[3] / 2, nParams[2], nParams[1] + nParams[3], nParams[2] + nParams[4], - nParams[1], nParams[2] + nParams[4], borderColor); - break; - } - case Button_type_e::ArrowRight: - { // draw: left-top, right-center, left-bottom - _display->fillTriangle(nParams[1], nParams[2], nParams[1] + nParams[3], nParams[2] + nParams[4] / 2, - nParams[1], nParams[2] + nParams[4], fillColor); - _display->drawTriangle(nParams[1], nParams[2], nParams[1] + nParams[3], nParams[2] + nParams[4] / 2, - nParams[1], nParams[2] + nParams[4], borderColor); - break; - } - case Button_type_e::ArrowDown: - { // draw: left-top, right-top, bottom-center - _display->fillTriangle(nParams[1], nParams[2], nParams[1] + nParams[3], nParams[2], - nParams[1] + nParams[3] / 2, nParams[2] + nParams[4], fillColor); - _display->drawTriangle(nParams[1], nParams[2], nParams[1] + nParams[3], nParams[2], - nParams[1] + nParams[3] / 2, nParams[2] + nParams[4], borderColor); - break; - } - case Button_type_e::None: - case Button_type_e::Button_MAX: - break; - } + drawButtonShape(buttonType, + nParams[2], nParams[3], nParams[4], nParams[5], + fillColor, borderColor); } // Display caption? (or bitmap) if (!clearArea && - !(static_cast(nParams[6] & 0xF0) == Button_layout_e::NoCaption)) { + (buttonLayout != Button_layout_e::NoCaption)) { int16_t x1, y1; uint16_t w1, h1, w2, h2; String newString; // Determine alignment parameters - if (nParams[0] == 1) { - newString = sParams[11].isEmpty() ? sParams[5] : sParams[11]; + if ((nParams[0] == 1) || (nParams[0] == -1)) { // 1 = on+enabled, -1 = on+disabled + newString = sParams[12].isEmpty() ? sParams[6] : sParams[12]; } else { - newString = sParams[12].isEmpty() ? sParams[5] : sParams[12]; + newString = sParams[13].isEmpty() ? sParams[6] : sParams[13]; } newString = AdaGFXparseTemplate(newString, 20); - if ((nParams[10] > 0) && (nParams[10] <= 10)) { _display->setTextSize(nParams[10]); } // set scaling + if ((nParams[11] > 0) && (nParams[11] <= 10)) { _display->setTextSize(nParams[11]); } // set scaling _display->getTextBounds(newString, 0, 0, &x1, &y1, &w1, &h1); // get caption length and height in pixels _display->getTextBounds(F(" "), 0, 0, &x1, &y1, &w2, &h2); // measure space width for little margins // Check button-alignment bits (mask 0xF0) for caption placement, modifies the x/y arguments passed! // Little margin is: from left/right: half of the width of a space, from top/bottom: half of height of the font used - Button_layout_e buttonLayout = static_cast(nParams[6] & 0xF0); switch (buttonLayout) { case Button_layout_e::CenterAligned: - nParams[1] += (nParams[3] / 2 - w1 / 2); // center horizontically - nParams[2] += (nParams[4] / 2 - h1 / 2); // center vertically + nParams[2] += (nParams[4] / 2 - w1 / 2); // center horizontically + nParams[3] += (nParams[5] / 2 - h1 / 2); // center vertically break; case Button_layout_e::LeftAligned: - nParams[1] += w2 / 2; // A little margin from left - nParams[2] += (nParams[4] / 2 - h1 / 2); // center vertically + nParams[2] += w2 / 2; // A little margin from left + nParams[3] += (nParams[5] / 2 - h1 / 2); // center vertically break; case Button_layout_e::TopAligned: - nParams[1] += (nParams[3] / 2 - w1 / 2); // center horizontically - nParams[2] += h1 / 2; // A little margin from top + nParams[2] += (nParams[4] / 2 - w1 / 2); // center horizontically + nParams[3] += h1 / 2; // A little margin from top break; case Button_layout_e::RightAligned: - nParams[1] += (nParams[3] - w1) - w2 / 2; // right-align + a little margin - nParams[2] += (nParams[4] / 2 - h1 / 2); // center vertically + nParams[2] += (nParams[4] - w1) - w2 / 2; // right-align + a little margin + nParams[3] += (nParams[5] / 2 - h1 / 2); // center vertically break; case Button_layout_e::BottomAligned: - nParams[1] += (nParams[3] / 2 - w1 / 2); // center horizontically - nParams[2] += (nParams[4] - h1 * 1.5); // bottom align + a little margin + nParams[2] += (nParams[4] / 2 - w1 / 2); // center horizontically + nParams[3] += (nParams[5] - h1 * 1.5); // bottom align + a little margin break; case Button_layout_e::LeftTopAligned: - nParams[1] += w2 / 2; // A little margin from left - nParams[2] += h1 / 2; // A little margin from top + nParams[2] += w2 / 2; // A little margin from left + nParams[3] += h1 / 2; // A little margin from top break; case Button_layout_e::RightTopAligned: - nParams[1] += (nParams[3] - w1) - w2 / 2; // right-align + a little margin - nParams[2] += h1 / 2; // A little margin from top + nParams[2] += (nParams[4] - w1) - w2 / 2; // right-align + a little margin + nParams[3] += h1 / 2; // A little margin from top break; case Button_layout_e::RightBottomAligned: - nParams[1] += (nParams[3] - w1) - w2 / 2; // right-align + a little margin - nParams[2] += (nParams[4] - h1 * 1.5); // bottom align + a little margin + nParams[2] += (nParams[4] - w1) - w2 / 2; // right-align + a little margin + nParams[3] += (nParams[5] - h1 * 1.5); // bottom align + a little margin break; case Button_layout_e::LeftBottomAligned: - nParams[1] += w2 / 2; // A little margin from left - nParams[2] += (nParams[4] - h1 * 1.5); // bottom align + a little margin + nParams[2] += w2 / 2; // A little margin from left + nParams[3] += (nParams[5] - h1 * 1.5); // bottom align + a little margin break; case Button_layout_e::Bitmap: { // Use ON/OFF caption to specify (full) bitmap filename @@ -1459,7 +1468,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { newString = parseStringToEndKeepCase(newString, 2); } } - success = showBmp(newString, nParams[1] + offX, nParams[2] + offY); + success = showBmp(newString, nParams[2] + offX, nParams[3] + offY); } else # endif // if ADAGFX_ENABLE_BMP_DISPLAY { @@ -1475,7 +1484,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { if ((buttonLayout != Button_layout_e::NoCaption) && (buttonLayout != Button_layout_e::Bitmap)) { // Set position and colors, then print - _display->setCursor(nParams[1], nParams[2]); + _display->setCursor(nParams[2], nParams[3]); _display->setTextColor(captionColor, captionColor); // transparent bg results in button color _display->print(newString); @@ -1484,7 +1493,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } // restore font scaling - if ((nParams[10] > 0) && (nParams[10] <= 10)) { _display->setTextSize(_fontscaling); } + if ((nParams[11] > 0) && (nParams[11] <= 10)) { _display->setTextSize(_fontscaling); } } } } @@ -1496,6 +1505,75 @@ bool AdafruitGFX_helper::processCommand(const String& string) { return success; } +/**************************************************************************** + * draw a button shape with provided color, can also clear a previously drawn button + ***************************************************************************/ +void AdafruitGFX_helper::drawButtonShape(Button_type_e buttonType, + int x, + int y, + int w, + int h, + uint16_t fillColor, + uint16_t borderColor) { + switch (buttonType) { + case Button_type_e::Square: // Rectangle + { + _display->fillRect(x, y, w, h, fillColor); + _display->drawRect(x, y, w, h, borderColor); + break; + } + case Button_type_e::Rounded: // Rounded Rectangle + { + int16_t radius = (w + h) / 20; // average 10 % corner radius w/h + _display->fillRoundRect(x, y, w, h, radius, fillColor); + _display->drawRoundRect(x, y, w, h, radius, borderColor); + break; + } + case Button_type_e::Circle: // Circle + { + int16_t radius = (w + h) / 4; // average radius + _display->fillCircle(x + (w / 2), y + (h / 2), radius, fillColor); + _display->drawCircle(x + (w / 2), y + (h / 2), radius, borderColor); + break; + } + case Button_type_e::ArrowLeft: + { // draw: left-center, right-top, right-bottom + _display->fillTriangle(x, y + h / 2, x + w, y, + x + w, y + h, fillColor); + _display->drawTriangle(x, y + h / 2, x + w, y, + x + w, y + h, borderColor); + break; + } + case Button_type_e::ArrowUp: + { // draw: top-center, right-bottom, left-bottom + _display->fillTriangle(x + w / 2, y, x + w, y + h, + x, y + h, fillColor); + _display->drawTriangle(x + w / 2, y, x + w, y + h, + x, y + h, borderColor); + break; + } + case Button_type_e::ArrowRight: + { // draw: left-top, right-center, left-bottom + _display->fillTriangle(x, y, x + w, y + h / 2, + x, y + h, fillColor); + _display->drawTriangle(x, y, x + w, y + h / 2, + x, y + h, borderColor); + break; + } + case Button_type_e::ArrowDown: + { // draw: left-top, right-top, bottom-center + _display->fillTriangle(x, y, x + w, y, + x + w / 2, y + h, fillColor); + _display->drawTriangle(x, y, x + w, y, + x + w / 2, y + h, borderColor); + break; + } + case Button_type_e::None: + case Button_type_e::Button_MAX: + break; + } +} + /**************************************************************************** * printText: Print text on display at a specific pixel or column/row location ***************************************************************************/ @@ -1594,7 +1672,9 @@ void AdafruitGFX_helper::printText(const char *string, /**************************************************************************** * color565: convert r, g, b colors to rgb565 (by bit-shifting) ***************************************************************************/ -uint16_t color565(uint8_t red, uint8_t green, uint8_t blue) { +uint16_t color565(uint8_t red, + uint8_t green, + uint8_t blue) { return ((red & 0xF8) << 8) | ((green & 0xFC) << 3) | (blue >> 3); } @@ -1608,7 +1688,9 @@ uint16_t color565(uint8_t red, uint8_t green, uint8_t blue) { // Param [in] colorDepth: The requiresed color depth, default: FullColor // param [in] defaultWhite: Return White color if empty, default: true // return : color (default ADAGFX_WHITE) -uint16_t AdaGFXparseColor(String& s, AdaGFXColorDepth colorDepth, bool emptyIsBlack) { +uint16_t AdaGFXparseColor(String & s, + AdaGFXColorDepth colorDepth, + bool emptyIsBlack) { s.toLowerCase(); int32_t result = -1; // No result yet @@ -1749,7 +1831,7 @@ uint16_t AdaGFXparseColor(String& s, AdaGFXColorDepth colorDepth, bool emptyIsBl break; } } - return result; + return static_cast(result); } const __FlashStringHelper* AdaGFXcolorToString_internal(uint16_t color, @@ -2272,7 +2354,7 @@ bool AdafruitGFX_helper::showBmp(const String& filename, if (loglevelActiveFor(LOG_LEVEL_INFO)) { log.clear(); - log += F("showBmp: x: "); + log += F("showBmp: x:"); log += x; log += F(", y:"); log += y; diff --git a/src/src/Helpers/AdafruitGFX_helper.h b/src/src/Helpers/AdafruitGFX_helper.h index 1155901f4b..4aea7d173b 100644 --- a/src/src/Helpers/AdafruitGFX_helper.h +++ b/src/src/Helpers/AdafruitGFX_helper.h @@ -10,6 +10,17 @@ /**************************************************************************** * helper class and functions for displays that use Adafruit_GFX library ***************************************************************************/ +/************ + * Changelog: + * 2022-06-02 tonhuisman: Leave out some Notes from UI to save a few bytes from size limited builds + * 2022-05-27 tonhuisman: Change btn subcommand to split state and mode arguments, state = 0/1, -2/-1, mode = -2, -1, 0 + * 2022-05-27 tonhuisman: Fix a few character mappings in AdaGFXparseTemplate, add surrogates for chars not in font + * Add support for {0xNN...} to insert any ascii character in template, supports multiple 2-digit hex values > 00 + * space, comma, dot, colon, semicolon or dash (' ,.:;-') as separators in hex value are allowed + * 2022-05-23 tonhuisman: Fix cast for returned value from AdaGFXparseColor + * Make 8 and 16 color support optional to squeeze a few bytes from size limited builds + * 2022-05-23 tonhuisman: Add changelog, older changes have not been logged. + ***************************************************************************/ # include # include # include @@ -22,17 +33,19 @@ # include "../Helpers/ESPEasy_Storage.h" # include "../ESPEasyCore/ESPEasy_Log.h" -# define ADAGFX_PARSE_MAX_ARGS 7 // Maximum number of arguments needed and supported (corrected) +# define ADAGFX_PARSE_MAX_ARGS 7 // Maximum number of arguments needed and supported (corrected) # ifndef ADAGFX_ARGUMENT_VALIDATION -# define ADAGFX_ARGUMENT_VALIDATION 1 // Validate command arguments +# define ADAGFX_ARGUMENT_VALIDATION 1 // Validate command arguments # endif // ifndef ADAGFX_ARGUMENT_VALIDATION # ifndef ADAGFX_USE_ASCIITABLE -# define ADAGFX_USE_ASCIITABLE 1 // Enable 'asciitable' command (useful for debugging/development) +# define ADAGFX_USE_ASCIITABLE 1 // Enable 'asciitable' command (useful for debugging/development) # endif // ifndef ADAGFX_USE_ASCIITABLE # ifndef ADAGFX_SUPPORT_7COLOR + // # define ADAGFX_SUPPORT_7COLOR 1 // Do we support 7-Color displays? # endif // ifndef ADAGFX_SUPPORT_7COLOR # ifndef ADAGFX_SUPPORT_8and16COLOR + // # define ADAGFX_SUPPORT_8and16COLOR 1 // Do we support 8 and 16-Color displays? # endif // ifndef ADAGFX_SUPPORT_8and16COLOR # ifndef ADAGFX_FONTS_INCLUDED @@ -119,12 +132,12 @@ # ifndef ADAGFX_FONTS_EXTRA_20PT_INCLUDED # define ADAGFX_FONTS_EXTRA_20PT_INCLUDED # endif // ifndef ADAGFX_FONTS_EXTRA_20PT_INCLUDED -# ifndef ADAGFX_SUPPORT_7COLOR -# define ADAGFX_SUPPORT_7COLOR 1 -# endif // ifndef ADAGFX_SUPPORT_7COLOR -# ifndef ADAGFX_SUPPORT_8and16COLOR -# define ADAGFX_SUPPORT_8and16COLOR 1 -# endif // ifndef ADAGFX_SUPPORT_8and16COLOR +# ifndef ADAGFX_SUPPORT_7COLOR +# define ADAGFX_SUPPORT_7COLOR 1 +# endif // ifndef ADAGFX_SUPPORT_7COLOR +# ifndef ADAGFX_SUPPORT_8and16COLOR +# define ADAGFX_SUPPORT_8and16COLOR 1 +# endif // ifndef ADAGFX_SUPPORT_8and16COLOR # endif // ifdef PLUGIN_SET_MAX # define ADAGFX_PARSE_PREFIX F("~") // Subcommand-trigger prefix and postfix strings @@ -229,7 +242,7 @@ enum class Button_type_e : uint8_t { ArrowUp = 0x05, ArrowRight = 0x06, ArrowDown = 0x07, - Button_MAX // must be last value in enum, max possible values: 16 + Button_MAX = 8u // must be last value in enum, max possible values: 16 }; // Only bits 4..7 can be used, masked with: 0xF0 @@ -246,7 +259,7 @@ enum class Button_layout_e : uint8_t { LeftBottomAligned = 0x80, NoCaption = 0x90, Bitmap = 0xA0, - Alignment_MAX = 11u // options-count + Alignment_MAX = 11u // options-count, max possible values: 16 }; const __FlashStringHelper* toString(Button_type_e button); @@ -395,6 +408,15 @@ class AdafruitGFX_helper { int Y, bool colRowMode = false); # endif // if ADAGFX_ARGUMENT_VALIDATION + # if ADAGFX_ENABLE_BUTTON_DRAW + void drawButtonShape(Button_type_e buttonType, + int x, + int y, + int w, + int h, + uint16_t fillColor, + uint16_t borderColor); + # endif // if ADAGFX_ENABLE_BUTTON_DRAW Adafruit_GFX *_display = nullptr; Adafruit_SPITFT *_tft = nullptr; From 1ba63cf5cacea6a35eb1abf24ad5bf4766c62c73 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 4 Jun 2022 12:48:40 +0200 Subject: [PATCH 011/113] [TouchHandler] Introduce ESPEasy_TouchHandler, refactored out of P123 touch support --- src/src/CustomBuild/define_plugin_sets.h | 8 +- src/src/Helpers/ESPEasy_TouchHandler.cpp | 1635 ++++++++++++++++++++++ src/src/Helpers/ESPEasy_TouchHandler.h | 292 ++++ 3 files changed, 1934 insertions(+), 1 deletion(-) create mode 100644 src/src/Helpers/ESPEasy_TouchHandler.cpp create mode 100644 src/src/Helpers/ESPEasy_TouchHandler.h diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index 12814856ca..27e4a7521a 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -1298,7 +1298,7 @@ To create/register a plugin, you have to : #ifndef USES_P116 #define USES_P116 // ST77xx #endif - #ifndef USES_P123 + #if !defined(USES_P123) && defined(ESP32) #define USES_P123 // FT6206 #endif #endif @@ -1619,6 +1619,12 @@ To create/register a plugin, you have to : #endif #endif +#if defined(USES_P099) || defined(USES_P123) + #ifndef PLUGIN_USES_TOUCHHANDLER + #define PLUGIN_USES_TOUCHHANDLER + #endif +#endif + /* #if defined(USES_P00x) || defined(USES_P00y) #include diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp new file mode 100644 index 0000000000..344ae0c03a --- /dev/null +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -0,0 +1,1635 @@ +#include "../Helpers/ESPEasy_TouchHandler.h" + +#ifdef PLUGIN_USES_TOUCHHANDLER + +/**************************************************************************** + * toString: Display-value for the touch action + ***************************************************************************/ +# ifdef TOUCH_USE_EXTENDED_TOUCH +const __FlashStringHelper* toString(Touch_action_e action) { + switch (action) { + case Touch_action_e::Default: return F("Default"); + case Touch_action_e::ActivateGroup: return F("Activate Group"); + case Touch_action_e::IncrementGroup: return F("Next Group"); + case Touch_action_e::DecrementGroup: return F("Previous Group"); + case Touch_action_e::IncrementPage: return F("Next Page (+10)"); + case Touch_action_e::DecrementPage: return F("Previous page (-10)"); + case Touch_action_e::TouchAction_MAX: break; + } + return F("Unsupported!"); +} + +# endif // ifdef TOUCH_USE_EXTENDED_TOUCH + +/** + * Constructors + */ +ESPEasy_TouchHandler::ESPEasy_TouchHandler() {} + +ESPEasy_TouchHandler::ESPEasy_TouchHandler(uint16_t displayTask, + AdaGFXColorDepth colorDepth) + : _displayTask(displayTask), _colorDepth(colorDepth) {} + +/** + * Load the touch objects from the settings, and initialize then properly where needed. + */ +void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { + # ifdef TOUCH_DEBUG + addLogMove(LOG_LEVEL_INFO, F("TOUCH DEBUG loadTouchObjects")); + # endif // TOUCH_DEBUG + LoadCustomTaskSettings(event->TaskIndex, settingsArray, TOUCH_ARRAY_SIZE, 0); + + lastObjectIndex = TOUCH_OBJECT_INDEX_START - 1; // START must be > 0!!! + + objectCount = 0; + _buttonGroups.clear(); // Clear groups + _buttonGroups.insert(0u); // Always have group 0 + + for (uint8_t i = TOUCH_OBJECT_INDEX_END; i >= TOUCH_OBJECT_INDEX_START; i--) { + if (!settingsArray[i].isEmpty() && (lastObjectIndex < TOUCH_OBJECT_INDEX_START)) { + lastObjectIndex = i; + objectCount++; // Count actual number of objects + } + } + + // Get calibration and common settings + Touch_Settings.calibrationEnabled = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], + TOUCH_CALIBRATION_ENABLED, TOUCH_SETTINGS_SEPARATOR) == 1; + Touch_Settings.logEnabled = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], + TOUCH_CALIBRATION_LOG_ENABLED, TOUCH_SETTINGS_SEPARATOR) == 1; + int lSettings = 0; + + bitWrite(lSettings, TOUCH_FLAGS_SEND_XY, TOUCH_TS_SEND_XY); + bitWrite(lSettings, TOUCH_FLAGS_SEND_Z, TOUCH_TS_SEND_Z); + bitWrite(lSettings, TOUCH_FLAGS_SEND_OBJECTNAME, TOUCH_TS_SEND_OBJECTNAME); + Touch_Settings.flags = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], + TOUCH_COMMON_FLAGS, TOUCH_SETTINGS_SEPARATOR, lSettings); + Touch_Settings.top_left.x = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], TOUCH_CALIBRATION_TOP_X, TOUCH_SETTINGS_SEPARATOR); + Touch_Settings.top_left.y = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], TOUCH_CALIBRATION_TOP_Y, TOUCH_SETTINGS_SEPARATOR); + Touch_Settings.bottom_right.x = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], + TOUCH_CALIBRATION_BOTTOM_X, + TOUCH_SETTINGS_SEPARATOR); + Touch_Settings.bottom_right.y = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], + TOUCH_CALIBRATION_BOTTOM_Y, + TOUCH_SETTINGS_SEPARATOR); + Touch_Settings.debounceMs = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], TOUCH_COMMON_DEBOUNCE_MS, TOUCH_SETTINGS_SEPARATOR, + TOUCH_DEBOUNCE_MILLIS); + # ifdef TOUCH_USE_EXTENDED_TOUCH + Touch_Settings.colorOn = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], + TOUCH_COMMON_DEF_COLOR_ON, TOUCH_SETTINGS_SEPARATOR); + Touch_Settings.colorOff = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], + TOUCH_COMMON_DEF_COLOR_OFF, TOUCH_SETTINGS_SEPARATOR); + Touch_Settings.colorBorder = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], + TOUCH_COMMON_DEF_COLOR_BORDER, TOUCH_SETTINGS_SEPARATOR); + Touch_Settings.colorCaption = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], + TOUCH_COMMON_DEF_COLOR_CAPTION, TOUCH_SETTINGS_SEPARATOR); + Touch_Settings.colorDisabled = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], + TOUCH_COMMON_DEF_COLOR_DISABLED, TOUCH_SETTINGS_SEPARATOR); + Touch_Settings.colorDisabledCaption = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], + TOUCH_COMMON_DEF_COLOR_DISABCAPT, TOUCH_SETTINGS_SEPARATOR); + + if ((Touch_Settings.colorOn == 0u) && + (Touch_Settings.colorOff == 0u) && + (Touch_Settings.colorCaption == 0u) && + (Touch_Settings.colorBorder == 0u) && + (Touch_Settings.colorDisabled == 0u) && + (Touch_Settings.colorDisabledCaption == 0u)) { + Touch_Settings.colorOn = ADAGFX_BLUE; + Touch_Settings.colorOff = ADAGFX_RED; + Touch_Settings.colorCaption = ADAGFX_WHITE; + Touch_Settings.colorBorder = ADAGFX_WHITE; + Touch_Settings.colorDisabled = TOUCH_DEFAULT_COLOR_DISABLED; + Touch_Settings.colorDisabledCaption = TOUCH_DEFAULT_COLOR_DISABLED_CAPTION; + } + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + + settingsArray[TOUCH_CALIBRATION_START].clear(); // Free a little memory + + // Buffer some settings, mostly for readability, but also to be able to set from write command + _flipped = bitRead(Touch_Settings.flags, TOUCH_FLAGS_ROTATION_FLIPPED); + _deduplicate = bitRead(Touch_Settings.flags, TOUCH_FLAGS_DEDUPLICATE); + + TouchObjects.clear(); + + if (objectCount > 0) { + TouchObjects.reserve(objectCount); + uint8_t t = 0u; + + for (uint8_t i = TOUCH_OBJECT_INDEX_START; i <= lastObjectIndex; i++) { + if (!settingsArray[i].isEmpty()) { + TouchObjects.push_back(tTouchObjects()); + TouchObjects[t].flags = parseStringToInt(settingsArray[i], TOUCH_OBJECT_FLAGS, TOUCH_SETTINGS_SEPARATOR); + TouchObjects[t].objectName = parseStringKeepCase(settingsArray[i], TOUCH_OBJECT_NAME, TOUCH_SETTINGS_SEPARATOR); + TouchObjects[t].top_left.x = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COORD_TOP_X, TOUCH_SETTINGS_SEPARATOR); + TouchObjects[t].top_left.y = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COORD_TOP_Y, TOUCH_SETTINGS_SEPARATOR); + TouchObjects[t].width_height.x = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COORD_WIDTH, TOUCH_SETTINGS_SEPARATOR); + TouchObjects[t].width_height.y = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COORD_HEIGHT, TOUCH_SETTINGS_SEPARATOR); + # ifdef TOUCH_USE_EXTENDED_TOUCH + TouchObjects[t].colorOn = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_ON, TOUCH_SETTINGS_SEPARATOR); + TouchObjects[t].colorOff = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_OFF, TOUCH_SETTINGS_SEPARATOR); + TouchObjects[t].colorCaption = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_CAPTION, TOUCH_SETTINGS_SEPARATOR); + TouchObjects[t].captionOn = parseStringKeepCase(settingsArray[i], TOUCH_OBJECT_CAPTION_ON, TOUCH_SETTINGS_SEPARATOR); + TouchObjects[t].captionOff = parseStringKeepCase(settingsArray[i], TOUCH_OBJECT_CAPTION_OFF, TOUCH_SETTINGS_SEPARATOR); + TouchObjects[t].colorBorder = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_BORDER, TOUCH_SETTINGS_SEPARATOR); + TouchObjects[t].colorDisabled = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_DISABLED, TOUCH_SETTINGS_SEPARATOR); + TouchObjects[t].colorDisabledCaption = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_DISABCAPT, TOUCH_SETTINGS_SEPARATOR); + TouchObjects[t].groupFlags = parseStringToInt(settingsArray[i], TOUCH_OBJECT_GROUPFLAGS, TOUCH_SETTINGS_SEPARATOR); + + if (!validButtonGroup(get8BitFromUL(TouchObjects[t].flags, TOUCH_OBJECT_FLAG_GROUP))) { + _buttonGroups.insert(get8BitFromUL(TouchObjects[t].flags, TOUCH_OBJECT_FLAG_GROUP)); + } + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + + TouchObjects[t].SurfaceAreas = 0; // Reset runtime stuff + TouchObjects[t].TouchTimers = 0; + TouchObjects[t].TouchStates = false; + + t++; + + settingsArray[i].clear(); // Free a little memory + } + } + } + + // if (_maxButtonGroup > 0) { + // _minButtonGroup = 1; + // } +} + +/** + * init + */ +void ESPEasy_TouchHandler::init(struct EventStruct *event) { + if (!_settingsLoaded) { + loadTouchObjects(event); + _settingsLoaded = true; + } + + if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME) && + bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)) { + if (_buttonGroups.size() > 1) { // Multiple groups? + displayButtonGroup(event, _buttonGroup, -3); // Clear all displayed groups + } + _buttonGroup = get8BitFromUL(Touch_Settings.flags, TOUCH_FLAGS_INITIAL_GROUP); + # ifdef TOUCH_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("TOUCH DEBUG group: "); + log += _buttonGroup; + log += F(", max group: "); + log += *_buttonGroups.crbegin(); + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // ifdef TOUCH_DEBUG + + displayButtonGroup(event, _buttonGroup); // Initialize selected group and group 0 + + # ifdef TOUCH_DEBUG + addLogMove(LOG_LEVEL_INFO, F("TOUCH DEBUG group done.")); + # endif // ifdef TOUCH_DEBUG + } +} + +/** + * helper function: use parseString() to read an argument, and convert that to an int value + */ +int ESPEasy_TouchHandler::parseStringToInt(const String& string, + uint8_t indexFind, + char separator, + int defaultValue) { + String parsed = parseStringKeepCase(string, indexFind, separator); + + int result = defaultValue; + + validIntFromString(parsed, result); + + return result; +} + +/** + * Determine if calibration is enabled and usable. + */ +bool ESPEasy_TouchHandler::isCalibrationActive() { + return _useCalibration + && (Touch_Settings.top_left.x != 0 || + Touch_Settings.top_left.y != 0 || + Touch_Settings.bottom_right.x > Touch_Settings.top_left.x || + Touch_Settings.bottom_right.y > Touch_Settings.top_left.y); // Enabled and any value != 0 => Active +} + +/** + * Check within the list of defined objects if we touched one of them. + * Must be in the current button group or in button group 0. + * The smallest matching surface is selected if multiple objects overlap. + * Returns state, sets selectedObjectName to the best matching object name + * and selectedObjectIndex to the index into the TouchObjects vector. + */ +bool ESPEasy_TouchHandler::isValidAndTouchedTouchObject(int16_t x, + int16_t y, + String& selectedObjectName, + int8_t& selectedObjectIndex) { + uint32_t lastObjectArea = 0u; + bool selected = false; + + for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { + uint8_t group = get8BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_GROUP); + + if (!TouchObjects[objectNr].objectName.isEmpty() + && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED) + && (TouchObjects[objectNr].width_height.x != 0) + && (TouchObjects[objectNr].width_height.y != 0) // Not initial could be valid + && ((group == 0) || (group == _buttonGroup))) { // Group 0 is always active + if (TouchObjects[objectNr].SurfaceAreas == 0) { // Need to calculate the surface area + TouchObjects[objectNr].SurfaceAreas = TouchObjects[objectNr].width_height.x * TouchObjects[objectNr].width_height.y; + } + + if ((TouchObjects[objectNr].top_left.x <= x) + && (TouchObjects[objectNr].top_left.y <= y) + && ((TouchObjects[objectNr].width_height.x + TouchObjects[objectNr].top_left.x) >= x) + && ((TouchObjects[objectNr].width_height.y + TouchObjects[objectNr].top_left.y) >= y) + && ((lastObjectArea == 0) || + (TouchObjects[objectNr].SurfaceAreas < lastObjectArea))) { // Select smallest area that fits the coordinates + selectedObjectName = TouchObjects[objectNr].objectName; + selectedObjectIndex = objectNr; + lastObjectArea = TouchObjects[objectNr].SurfaceAreas; + selected = true; + } + # if defined(TOUCH_DEBUG) && !defined(BUILD_NO_DEBUG) + + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + String log = F("TOUCH DEBUG Touched: obj: "); + log += TouchObjects[objectNr].objectName; + log += ','; + log += TouchObjects[objectNr].top_left.x; + log += ','; + log += TouchObjects[objectNr].top_left.y; + log += ','; + log += TouchObjects[objectNr].width_height.x; + log += ','; + log += TouchObjects[objectNr].width_height.y; + log += F(" surface:"); + log += TouchObjects[objectNr].SurfaceAreas; + log += F(" x,y:"); + log += x; + log += ','; + log += y; + log += F(" sel:"); + log += selectedObjectName; + log += '/'; + log += selectedObjectIndex; + log += '/'; + log += selected ? 'T' : 'f'; + addLogMove(LOG_LEVEL_DEBUG, log); + } + # endif // if defined(TOUCH_DEBUG) && !defined(BUILD_NO_DEBUG) + } + } + return selected; +} + +/** + * Get either the int value or index of the objectName provided, optionally a button object + */ +int8_t ESPEasy_TouchHandler::getTouchObjectIndex(struct EventStruct *event, + const String & touchObject, + bool isButton) { + if (touchObject.isEmpty()) { return -1; } + + int index = -1; + + // ATTENTION: Any externally provided objectNumber is 1-based, result is 0-based + if (validIntFromString(touchObject, index) && + (index > 0) && + (index <= TouchObjects.size())) { + return static_cast(index - 1); + } + + for (size_t objectNr = 0; objectNr < TouchObjects.size(); objectNr++) { + if (!TouchObjects[objectNr].objectName.isEmpty() + && touchObject.equalsIgnoreCase(TouchObjects[objectNr].objectName) + && (!isButton || bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTON))) { + return static_cast(objectNr); + } + } + return -1; +} + +/** + * Set the enabled/disabled state of an object. Will redraw if a button object. + */ +bool ESPEasy_TouchHandler::setTouchObjectState(struct EventStruct *event, + const String & touchObject, + bool state) { + if (touchObject.isEmpty()) { return false; } + bool success = false; + + int8_t objectNr = getTouchObjectIndex(event, touchObject); + + if (objectNr > -1) { + success = true; // Succes if matched object + + if (state != bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED)) { + bitWrite(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED, state); // Store in settings, no save + + // Event when enabling/disabling + if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME) && + bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)) { + generateObjectEvent(event, objectNr, TouchObjects[objectNr].TouchStates ? 1 : 0, state ? -1 : -2); // Redraw only, no activation + } + } + # ifdef TOUCH_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("TOUCH setTouchObjectState: obj: "); + log += touchObject; + log += '/'; + log += objectNr; + + if (success) { + log += F(", new state: "); + log += (state ? F("en") : F("dis")); + log += F("abled."); + } else { + log += F(" failed!"); + } + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // ifdef TOUCH_DEBUG + } + + return success; +} + +/** + * Set the on/off state of an enabled touch-button object. Will generate an event if so configured. + */ +bool ESPEasy_TouchHandler::setTouchButtonOnOff(struct EventStruct *event, + const String & touchObject, + bool state) { + if (touchObject.isEmpty()) { return false; } + bool success = false; + + int8_t objectNr = getTouchObjectIndex(event, touchObject, true); + + if ((objectNr > -1) + && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED) + && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTON)) { + success = true; // Always success if matched button + + if (state != TouchObjects[objectNr].TouchStates) { + TouchObjects[objectNr].TouchStates = state; + + // Send event like it was pressed + if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME) && + bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)) { + generateObjectEvent(event, objectNr, state ? 1 : 0); + } + } + # ifdef TOUCH_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("TOUCH setTouchButtonOnOff: obj: "); + log += touchObject; + log += '/'; + log += objectNr; + log += F(", new state: "); + log += (state ? F("on") : F("off")); + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // ifdef TOUCH_DEBUG + } + return success; +} + +/** + * mode: -2 = clear buttons in group, -3 = clear all buttongroups, -1 = draw buttons in group, 0 = initialize buttons + */ +void ESPEasy_TouchHandler::displayButtonGroup(struct EventStruct *event, + int16_t buttonGroup, + int8_t mode) { + for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { + displayButton(event, objectNr, buttonGroup, mode); + + delay(0); + } + + if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME)) { + // Send an event #Group,, with the selected group and the mode (-3..0) + String eventCommand; + eventCommand.reserve(24); + eventCommand += getTaskDeviceName(event->TaskIndex); + eventCommand += '#'; + eventCommand += F("Group"); + eventCommand += '='; // Add arguments + eventCommand += buttonGroup; + eventCommand += ','; + eventCommand += mode; + eventQueue.addMove(std::move(eventCommand)); + } + + delay(0); +} + +/** + * Display a single button, using mode from displayButtonGroup + */ +bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, + int8_t buttonNr, + int16_t buttonGroup, + int8_t mode) { + if ((buttonNr < 0) || (buttonNr >= static_cast(TouchObjects.size()))) { return false; } // sanity check + int8_t state = 99; + int16_t group = get8BitFromUL(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_GROUP); + Touch_action_e action = static_cast(get4BitFromUL(TouchObjects[buttonNr].groupFlags, TOUCH_OBJECT_GROUP_ACTION)); + bool isArrow = false; + + if ((mode > -2) && // Not on clear (-2 and -3) + bitRead(Touch_Settings.flags, TOUCH_FLAGS_AUTO_PAGE_ARROWS) && + ((action == Touch_action_e::DecrementGroup) || // Arrow buttons + (action == Touch_action_e::IncrementGroup) || + (action == Touch_action_e::DecrementPage) || + (action == Touch_action_e::IncrementPage))) { + isArrow = true; + } + + if (!TouchObjects[buttonNr].objectName.isEmpty() && + ((bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED) && (group == 0)) || (group > 0) || isArrow) && + bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_BUTTON) && + (((group == buttonGroup) || (buttonGroup < 0)) || + ((mode != -2) && (group == 0)) || + (mode == -3))) { + // Act like a button, 1 = On, 0 = Off, inversion is handled in generateObjectEvent() + state = TouchObjects[buttonNr].TouchStates ? 1 : 0; + + if (isArrow) { // Auto-Enable/Disable the arrow buttons + bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); + state = 1; // always get ON state! + + if (action == Touch_action_e::DecrementGroup) { // Left arrow + bitWrite(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED, validButtonGroup(buttonGroup - 1, true)); + } else + if (action == Touch_action_e::IncrementGroup) { // Right arrow + bitWrite(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED, validButtonGroup(buttonGroup + 1, true)); + } else + if (action == Touch_action_e::DecrementPage) { // Down arrow or Up arrow + bitWrite(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED, validButtonGroup(buttonGroup + (pgupInvert ? 10 : -10), true)); + } else + if (action == Touch_action_e::IncrementPage) { // Up arrow or Down arrow + bitWrite(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED, validButtonGroup(buttonGroup + (pgupInvert ? -10 : 10), true)); + } + } + + if (bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED)) { + if (mode == 0) { + mode = -1; + } + } else { + state -= 2; // disabled + } + generateObjectEvent(event, buttonNr, state, mode, mode < 0, mode <= -2 ? -1 : 1); + } + # ifdef TOUCH_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + String log = F("TOUCH: button init, state: "); + log += state; + log += F(", group: "); + log += buttonGroup; + log += F(", mode: "); + log += mode; + log += F(", group: "); + log += get8BitFromUL(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_GROUP); + log += F(", en: "); + log += bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_BUTTON); + log += F(", object: "); + log += buttonNr; + addLog(LOG_LEVEL_DEBUG, log); + } + # endif // ifdef TOUCH_DEBUG + return true; +} + +/** + * Check if this is a valid button group + * When ignoreZero = true will return false for group 0 if the number of groups > 1. + * NB: Group 0 is always available, even without button definitions! + */ +bool ESPEasy_TouchHandler::validButtonGroup(int16_t group, + bool ignoreZero) { + return _buttonGroups.find(group) != _buttonGroups.end() && + (!ignoreZero || group > 0 || (group == 0 && _buttonGroups.size() == 1)); +} + +/** + * Set the desired button group, must be a known group, previous group will be erased and new group drawn + */ +bool ESPEasy_TouchHandler::setButtonGroup(struct EventStruct *event, + int16_t buttonGroup) { + if (validButtonGroup(buttonGroup)) { + if (buttonGroup != _buttonGroup) { + displayButtonGroup(event, _buttonGroup, -2); + _buttonGroup = buttonGroup; + displayButtonGroup(event, _buttonGroup, -1); + } + return true; + } + return false; +} + +/** + * Increment button group if that group exists, if max. group > 0 then min. group = 1 + */ +bool ESPEasy_TouchHandler::incrementButtonGroup(struct EventStruct *event) { + if (validButtonGroup(_buttonGroup + 1)) { + return setButtonGroup(event, _buttonGroup + 1); + } + return false; +} + +/** + * Decrement button group if that group exists, if max. group > 0 then min. group = 1 + */ +bool ESPEasy_TouchHandler::decrementButtonGroup(struct EventStruct *event) { + if (validButtonGroup(_buttonGroup - 1)) { + return setButtonGroup(event, _buttonGroup - 1); + } + return false; +} + +/** + * Increment button group by page (+10), if max. group > 0 then min. group = 1 + */ +bool ESPEasy_TouchHandler::incrementButtonPage(struct EventStruct *event) { + bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); + + if (validButtonGroup(_buttonGroup + (pgupInvert ? -10 : 10))) { + return setButtonGroup(event, _buttonGroup + (pgupInvert ? -10 : 10)); + } + return false; +} + +/** + * Decrement button group by page (+10), if max. group > 0 then min. group = 1 + */ +bool ESPEasy_TouchHandler::decrementButtonPage(struct EventStruct *event) { + bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); + + if (validButtonGroup(_buttonGroup + (pgupInvert ? 10 : -10))) { + return setButtonGroup(event, _buttonGroup + (pgupInvert ? 10 : -10)); + } + return false; +} + +/** + * Load the settings onto the webpage + */ +bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { + addFormSubHeader(F("Touch configuration")); + + addFormCheckBox(F("Flip rotation 180°"), F("tch_rotation_flipped"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_ROTATION_FLIPPED)); + # ifndef LIMIT_BUILD_SIZE + addFormNote(F("Some touchscreens are mounted 180° rotated on the display.")); + # endif // ifndef LIMIT_BUILD_SIZE + + uint8_t choice3 = 0u; + + bitWrite(choice3, TOUCH_FLAGS_SEND_XY, bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_XY)); + bitWrite(choice3, TOUCH_FLAGS_SEND_Z, bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z)); + bitWrite(choice3, TOUCH_FLAGS_SEND_OBJECTNAME, bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME)); + { + # define TOUCH_EVENTS_OPTIONS 6 + const __FlashStringHelper *options3[TOUCH_EVENTS_OPTIONS] = + { F("None"), + F("X and Y"), + F("X, Y and Z"), + # ifdef TOUCH_USE_EXTENDED_TOUCH + F("Objectnames and Button groups"), + F("Objectnames, Button groups, X and Y"), + F("Objectnames, Button groups, X, Y and Z") + # else // ifdef TOUCH_USE_EXTENDED_TOUCH + F("Objectnames only"), + F("Objectnames, X and Y"), + F("Objectnames, X, Y and Z") + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + }; + int optionValues3[TOUCH_EVENTS_OPTIONS] = { 0, 1, 3, 4, 5, 7 }; // Already used as a bitmap! + addFormSelector(F("Events"), F("tch_events"), TOUCH_EVENTS_OPTIONS, options3, optionValues3, choice3); + + addFormCheckBox(F("Draw buttons when started"), F("tch_init_objectevent"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)); + addFormNote(F("Needs Objectnames 'Events' to be enabled.")); + } + + addFormCheckBox(F("Prevent duplicate events"), F("tch_deduplicate"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_DEDUPLICATE)); + + # ifndef LIMIT_BUILD_SIZE + + if (!Settings.UseRules) { + addFormNote(F("Tools / Advanced / Rules must be enabled for events to be fired.")); + } + # endif // ifndef LIMIT_BUILD_SIZE + + addFormSubHeader(F("Calibration")); + + { + const __FlashStringHelper *noYesOptions[2] = { F("No"), F("Yes") }; + int noYesOptionValues[2] = { 0, 1 }; + addFormSelector(F("Calibrate to screen resolution"), + F("tch_use_calibration"), + 2, + noYesOptions, + noYesOptionValues, + Touch_Settings.calibrationEnabled ? 1 : 0, + true); + } + + if (Touch_Settings.calibrationEnabled) { + addRowLabel(F("Calibration")); + html_table(EMPTY_STRING, false); // Sub-table + html_table_header(F("")); + html_table_header(F("x")); + html_table_header(F("y")); + html_table_header(F("")); + html_table_header(F("x")); + html_table_header(F("y")); + + html_TR_TD(); + addHtml(F("Top-left")); + html_TD(); + addNumericBox(F("tch_cal_tl_x"), + Touch_Settings.top_left.x, + 0, + 65535); + html_TD(); + addNumericBox(F("tch_cal_tl_y"), + Touch_Settings.top_left.y, + 0, + 65535); + html_TD(); + addHtml(F("Bottom-right")); + html_TD(); + addNumericBox(F("tch_cal_br_x"), + Touch_Settings.bottom_right.x, + 0, + 65535); + html_TD(); + addNumericBox(F("tch_cal_br_y"), + Touch_Settings.bottom_right.y, + 0, + 65535); + + html_end_table(); + } + + addFormCheckBox(F("Enable logging for calibration"), F("tch_log_calibration"), + Touch_Settings.logEnabled); + + addFormSubHeader(F("Touch objects")); + + # ifdef TOUCH_USE_EXTENDED_TOUCH + + AdaGFXHtmlColorDepthDataList(F("adagfx65kcolors"), _colorDepth); + + { + String parsed; + addRowLabel(F("Default On/Off button colors")); + html_table(EMPTY_STRING, false); // Sub-table + html_table_header(F("ON color")); + html_table_header(F("OFF color")); + html_table_header(F("Border color")); + html_table_header(F("Caption color")); + html_table_header(F("Disabled color")); + html_table_header(F("Disabled caption color")); + + html_TR_TD(); // ON color + parsed = AdaGFXcolorToString(Touch_Settings.colorOn, _colorDepth, true); + addTextBox(getPluginCustomArgName(3000), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef TOUCH_USE_TOOLTIPS + , F("ON color") + # endif // ifdef TOUCH_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // OFF color + parsed = AdaGFXcolorToString(Touch_Settings.colorOff, _colorDepth, true); + addTextBox(getPluginCustomArgName(3001), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef TOUCH_USE_TOOLTIPS + , F("OFF color") + # endif // ifdef TOUCH_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // Border color + parsed = AdaGFXcolorToString(Touch_Settings.colorBorder, _colorDepth, true); + addTextBox(getPluginCustomArgName(3002), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef TOUCH_USE_TOOLTIPS + , F("Border color") + # endif // ifdef TOUCH_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // Caption color + parsed = AdaGFXcolorToString(Touch_Settings.colorCaption, _colorDepth, true); + addTextBox(getPluginCustomArgName(3003), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef TOUCH_USE_TOOLTIPS + , F("Caption color") + # endif // ifdef TOUCH_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // Disabled color + parsed = AdaGFXcolorToString(Touch_Settings.colorDisabled, _colorDepth, true); + addTextBox(getPluginCustomArgName(3004), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef TOUCH_USE_TOOLTIPS + , F("Disabled color") + # endif // ifdef TOUCH_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // Disabled caption color + parsed = AdaGFXcolorToString(Touch_Settings.colorDisabledCaption, _colorDepth, true); + addTextBox(getPluginCustomArgName(3005), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef TOUCH_USE_TOOLTIPS + , F("Disabled caption color") + # endif // ifdef TOUCH_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_end_table(); + } + { + addFormNumericBox(F("Initial button group"), F("tch_initial_group"), + get8BitFromUL(Touch_Settings.flags, TOUCH_FLAGS_INITIAL_GROUP), 0, TOUCH_MAX_BUTTON_GROUPS + # ifdef TOUCH_USE_TOOLTIPS + , F("Initial group") + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + addFormCheckBox(F("Draw buttons via Rules"), F("tch_via_rules"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_DRAWBTN_VIA_RULES)); + addFormCheckBox(F("Enable/Disable page buttons"), F("tch_page_buttons"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_AUTO_PAGE_ARROWS)); + addFormCheckBox(F("PageUp/PageDown reversed"), F("tch_page_below"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU)); + } + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + { + addRowLabel(F("Object")); + + { + html_table(EMPTY_STRING, false); // Sub-table + html_table_header(F(" # ")); + html_table_header(F("On")); + html_table_header(F("Objectname")); + html_table_header(F("Top-left x")); + html_table_header(F("Top-left y")); + # ifdef TOUCH_USE_EXTENDED_TOUCH + html_table_header(F("Button")); + html_table_header(F("Layout")); + html_table_header(F("ON color")); + html_table_header(F("ON caption")); + html_table_header(F("Border color")); + html_table_header(F("Disab. cap. clr")); + html_table_header(F("Touch action")); + # else // ifdef TOUCH_USE_EXTENDED_TOUCH + html_table_header(F("On/Off button")); + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + html_TR(); // New row + html_table_header(EMPTY_STRING); + html_table_header(EMPTY_STRING); + # ifdef TOUCH_USE_EXTENDED_TOUCH + html_table_header(F("Button-group")); + # else // ifdef TOUCH_USE_EXTENDED_TOUCH + html_table_header(EMPTY_STRING); + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + html_table_header(F("Width")); + html_table_header(F("Height")); + html_table_header(F("Inverted")); + # ifdef TOUCH_USE_EXTENDED_TOUCH + html_table_header(F("Font scale")); + html_table_header(F("OFF color")); + html_table_header(F("OFF caption")); + html_table_header(F("Caption color")); + html_table_header(F("Disabled clr")); + html_table_header(F("Action group")); + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + } + # ifdef TOUCH_USE_EXTENDED_TOUCH + const __FlashStringHelper *buttonTypeOptions[] = { + toString(Button_type_e::None), + toString(Button_type_e::Square), + toString(Button_type_e::Rounded), + toString(Button_type_e::Circle), + toString(Button_type_e::ArrowLeft), + toString(Button_type_e::ArrowUp), + toString(Button_type_e::ArrowRight), + toString(Button_type_e::ArrowDown), + }; + + const int buttonTypeValues[] = { + static_cast(Button_type_e::None), + static_cast(Button_type_e::Square), + static_cast(Button_type_e::Rounded), + static_cast(Button_type_e::Circle), + static_cast(Button_type_e::ArrowLeft), + static_cast(Button_type_e::ArrowUp), + static_cast(Button_type_e::ArrowRight), + static_cast(Button_type_e::ArrowDown), + }; + + const __FlashStringHelper *buttonLayoutOptions[] = { + toString(Button_layout_e::CenterAligned), + toString(Button_layout_e::LeftAligned), + toString(Button_layout_e::TopAligned), + toString(Button_layout_e::RightAligned), + toString(Button_layout_e::BottomAligned), + toString(Button_layout_e::LeftTopAligned), + toString(Button_layout_e::RightTopAligned), + toString(Button_layout_e::LeftBottomAligned), + toString(Button_layout_e::RightBottomAligned), + toString(Button_layout_e::NoCaption), + toString(Button_layout_e::Bitmap), + }; + + const int buttonLayoutValues[] = { + static_cast(Button_layout_e::CenterAligned), + static_cast(Button_layout_e::LeftAligned), + static_cast(Button_layout_e::TopAligned), + static_cast(Button_layout_e::RightAligned), + static_cast(Button_layout_e::BottomAligned), + static_cast(Button_layout_e::LeftTopAligned), + static_cast(Button_layout_e::RightTopAligned), + static_cast(Button_layout_e::LeftBottomAligned), + static_cast(Button_layout_e::RightBottomAligned), + static_cast(Button_layout_e::NoCaption), + static_cast(Button_layout_e::Bitmap), + }; + + const __FlashStringHelper *touchActionOptions[] = { + toString(Touch_action_e::Default), + toString(Touch_action_e::ActivateGroup), + toString(Touch_action_e::IncrementGroup), + toString(Touch_action_e::DecrementGroup), + toString(Touch_action_e::IncrementPage), + toString(Touch_action_e::DecrementPage), + }; + + const int touchActionValues[] = { + static_cast(Touch_action_e::Default), + static_cast(Touch_action_e::ActivateGroup), + static_cast(Touch_action_e::IncrementGroup), + static_cast(Touch_action_e::DecrementGroup), + static_cast(Touch_action_e::IncrementPage), + static_cast(Touch_action_e::DecrementPage), + }; + + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + + uint8_t maxIdx = std::min(static_cast(TouchObjects.size() + TOUCH_EXTRA_OBJECT_COUNT), TOUCH_MAX_OBJECT_COUNT); + String parsed; + TouchObjects.resize(maxIdx, tTouchObjects()); + + for (int objectNr = 0; objectNr < maxIdx; objectNr++) { + html_TR_TD(); + addHtml(F(" ")); + addHtmlInt(objectNr + 1); // Arrayindex to objectindex + + html_TD(); + + // Enable new entries + bool enabled = bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED) || TouchObjects[objectNr].objectName.isEmpty(); + addCheckBox(getPluginCustomArgName(objectNr + 0), + enabled, false + # ifdef TOUCH_USE_TOOLTIPS + , F("Enabled") + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + html_TD(); // Name + addTextBox(getPluginCustomArgName(objectNr + 100), + TouchObjects[objectNr].objectName, + TOUCH_MaxObjectNameLength, + false, false, EMPTY_STRING, EMPTY_STRING); + html_TD(); // top-x + addNumericBox(getPluginCustomArgName(objectNr + 200), + TouchObjects[objectNr].top_left.x, 0, 65535 + # ifdef TOUCH_USE_TOOLTIPS + , F("widenumber"), F("Top-left x") + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + html_TD(); // top-y + addNumericBox(getPluginCustomArgName(objectNr + 300), + TouchObjects[objectNr].top_left.y, 0, 65535 + # ifdef TOUCH_USE_TOOLTIPS + , F("widenumber"), F("Top-left y") + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + html_TD(); // (on/off) button (type) + # ifdef TOUCH_USE_EXTENDED_TOUCH + addSelector(getPluginCustomArgName(objectNr + 800), + static_cast(Button_type_e::Button_MAX), + buttonTypeOptions, + buttonTypeValues, + nullptr, + get4BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTONTYPE), false, true, F("widenumber") + # ifdef TOUCH_USE_TOOLTIPS + , F("Buttontype") + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + html_TD(); // button alignment + addSelector(getPluginCustomArgName(objectNr + 900), + static_cast(Button_layout_e::Alignment_MAX), + buttonLayoutOptions, + buttonLayoutValues, + nullptr, + get4BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTONALIGN) << 4, false, true, F("widenumber") + # ifdef TOUCH_USE_TOOLTIPS + , F("Button alignment") + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + # else // ifdef TOUCH_USE_EXTENDED_TOUCH + addCheckBox(getPluginCustomArgName(objectNr + 600), + bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTON), false + # ifdef TOUCH_USE_TOOLTIPS + , F("On/Off button") + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # ifdef TOUCH_USE_EXTENDED_TOUCH + html_TD(); // ON color + parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorOn, _colorDepth, true); + addTextBox(getPluginCustomArgName(objectNr + 1000), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef TOUCH_USE_TOOLTIPS + , F("ON color") + # endif // ifdef TOUCH_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // ON Caption + addTextBox(getPluginCustomArgName(objectNr + 1300), + TouchObjects[objectNr].captionOn, + TOUCH_MaxCaptionNameLength, + false, + false, + EMPTY_STRING, + F("wide") + # ifdef TOUCH_USE_TOOLTIPS + , F("ON caption") + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + html_TD(); // Border color + parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorBorder, _colorDepth, true); + addTextBox(getPluginCustomArgName(objectNr + 1700), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef TOUCH_USE_TOOLTIPS + , F("Border color") + # endif // ifdef TOUCH_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // Disabled caption color + parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorDisabledCaption, _colorDepth, true); + addTextBox(getPluginCustomArgName(objectNr + 1900), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef TOUCH_USE_TOOLTIPS + , F("Disabled caption color") + # endif // ifdef TOUCH_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // button action + addSelector(getPluginCustomArgName(objectNr + 2000), + static_cast(Touch_action_e::TouchAction_MAX), + touchActionOptions, + touchActionValues, + nullptr, + get4BitFromUL(TouchObjects[objectNr].groupFlags, TOUCH_OBJECT_GROUP_ACTION), + false, + true, + F("widenumber") + # ifdef TOUCH_USE_TOOLTIPS + , F("Touch action") + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + + html_TR_TD(); // Start new row + + html_TD(2); // Start with some blank columns + # ifdef TOUCH_USE_EXTENDED_TOUCH + { + # ifdef TOUCH_USE_TOOLTIPS + String buttonGroupToolTip = F("Button-group [0.."); + buttonGroupToolTip += TOUCH_MAX_BUTTON_GROUPS; + buttonGroupToolTip += ']'; + # endif // ifdef TOUCH_USE_TOOLTIPS + addNumericBox(getPluginCustomArgName(objectNr + 1600), + get8BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_GROUP), 0, TOUCH_MAX_BUTTON_GROUPS + # ifdef TOUCH_USE_TOOLTIPS + , F("widenumber"), buttonGroupToolTip + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + } + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + html_TD(); // Width + addNumericBox(getPluginCustomArgName(objectNr + 400), + TouchObjects[objectNr].width_height.x, 0, 65535 + # ifdef TOUCH_USE_TOOLTIPS + , F("widenumber"), F("Width") + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + html_TD(); // Height + addNumericBox(getPluginCustomArgName(objectNr + 500), + TouchObjects[objectNr].width_height.y, 0, 65535 + # ifdef TOUCH_USE_TOOLTIPS + , F("widenumber"), F("Height") + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + html_TD(); // inverted + addCheckBox(getPluginCustomArgName(objectNr + 700), + bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_INVERTED), false + # ifdef TOUCH_USE_TOOLTIPS + , F("Inverted") + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + # ifdef TOUCH_USE_EXTENDED_TOUCH + html_TD(); // font scale + addNumericBox(getPluginCustomArgName(objectNr + 1200), + get4BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_FONTSCALE), 1, 10 + # ifdef TOUCH_USE_TOOLTIPS + , F("widenumber"), F("Font scaling [1x..10x]") + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + html_TD(); // OFF color + parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorOff, _colorDepth, true); + addTextBox(getPluginCustomArgName(objectNr + 1100), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef TOUCH_USE_TOOLTIPS + , F("OFF color") + # endif // ifdef TOUCH_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // OFF Caption + addTextBox(getPluginCustomArgName(objectNr + 1400), + TouchObjects[objectNr].captionOff, + TOUCH_MaxCaptionNameLength, + false, + false, + EMPTY_STRING, + F("wide") + # ifdef TOUCH_USE_TOOLTIPS + , F("OFF caption") + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + html_TD(); // Caption color + parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorCaption, _colorDepth, true); + addTextBox(getPluginCustomArgName(objectNr + 1500), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef TOUCH_USE_TOOLTIPS + , F("Caption color") + # endif // ifdef TOUCH_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // Disabled color + parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorDisabled, _colorDepth, true); + addTextBox(getPluginCustomArgName(objectNr + 1800), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, + EMPTY_STRING, F("widenumber") + # ifdef TOUCH_USE_TOOLTIPS + , F("Disabled color") + # endif // ifdef TOUCH_USE_TOOLTIPS + , F("adagfx65kcolors") + ); + html_TD(); // Action Group + addNumericBox(getPluginCustomArgName(objectNr + 2100), + get8BitFromUL(TouchObjects[objectNr].groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP), 0, TOUCH_MAX_BUTTON_GROUPS + # ifdef TOUCH_USE_TOOLTIPS + , F("widenumber") + , F("Action group") + # endif // ifdef TOUCH_USE_TOOLTIPS + ); + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + } + html_end_table(); + + addFormNumericBox(F("Debounce delay for On/Off buttons"), F("tch_debounce"), + Touch_Settings.debounceMs, 0, 255); + addUnit(F("0-255 msec.")); + } + return false; +} + +/** + * Helper: Convert an integer to string, but return an empty string for 0, to save a little space in settings + */ +String toStringNoZero(int64_t value) { + if (value != 0) { + return toString(value, 0); + } else { + return EMPTY_STRING; + } +} + +/** + * Save the settings from the web page to flash + */ +bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { + String config; + + # ifdef TOUCH_DEBUG + uint16_t saveSize = 0; + # endif // ifdef TOUCH_DEBUG + + # ifdef TOUCH_USE_EXTENDED_TOUCH + String colorInput; + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + config.reserve(80); + + uint32_t lSettings = 0u; + + bitWrite(lSettings, TOUCH_FLAGS_SEND_XY, bitRead(getFormItemInt(F("tch_events")), TOUCH_FLAGS_SEND_XY)); + bitWrite(lSettings, TOUCH_FLAGS_SEND_Z, bitRead(getFormItemInt(F("tch_events")), TOUCH_FLAGS_SEND_Z)); + bitWrite(lSettings, TOUCH_FLAGS_SEND_OBJECTNAME, bitRead(getFormItemInt(F("tch_events")), TOUCH_FLAGS_SEND_OBJECTNAME)); + bitWrite(lSettings, TOUCH_FLAGS_ROTATION_FLIPPED, isFormItemChecked(F("tch_rotation_flipped"))); + bitWrite(lSettings, TOUCH_FLAGS_DEDUPLICATE, isFormItemChecked(F("tch_deduplicate"))); + bitWrite(lSettings, TOUCH_FLAGS_INIT_OBJECTEVENT, isFormItemChecked(F("tch_init_objectevent"))); + # ifdef TOUCH_USE_EXTENDED_TOUCH + set8BitToUL(lSettings, TOUCH_FLAGS_INITIAL_GROUP, getFormItemInt(F("tch_initial_group"))); // Button group + bitWrite(lSettings, TOUCH_FLAGS_DRAWBTN_VIA_RULES, isFormItemChecked(F("tch_via_rules"))); + bitWrite(lSettings, TOUCH_FLAGS_AUTO_PAGE_ARROWS, isFormItemChecked(F("tch_page_buttons"))); + bitWrite(lSettings, TOUCH_FLAGS_PGUP_BELOW_MENU, isFormItemChecked(F("tch_page_below"))); + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + + config += getFormItemInt(F("tch_use_calibration")); // First value should NEVER be empty, or parseString() wil get confused + config += TOUCH_SETTINGS_SEPARATOR; + config += toStringNoZero(isFormItemChecked(F("tch_log_calibration")) ? 1 : 0); + config += TOUCH_SETTINGS_SEPARATOR; + config += toStringNoZero(getFormItemInt(F("tch_cal_tl_x"))); + config += TOUCH_SETTINGS_SEPARATOR; + config += toStringNoZero(getFormItemInt(F("tch_cal_tl_y"))); + config += TOUCH_SETTINGS_SEPARATOR; + config += toStringNoZero(getFormItemInt(F("tch_cal_br_x"))); + config += TOUCH_SETTINGS_SEPARATOR; + config += toStringNoZero(getFormItemInt(F("tch_cal_br_y"))); + config += TOUCH_SETTINGS_SEPARATOR; + config += toStringNoZero(getFormItemInt(F("tch_debounce"))); + config += TOUCH_SETTINGS_SEPARATOR; + config += ull2String(lSettings); + # ifdef TOUCH_USE_EXTENDED_TOUCH + config += TOUCH_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(3000)); // Default Color ON + config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth)); + config += TOUCH_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(3001)); // Default Color OFF + config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, false)); + config += TOUCH_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(3002)); // Default Color Border + config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, false)); + config += TOUCH_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(3003)); // Default Color caption + config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, false)); + config += TOUCH_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(3004)); // Default Disabled Color + config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth)); + config += TOUCH_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(3005)); // Default Disabled Caption Color + config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, false)); + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + + settingsArray[TOUCH_CALIBRATION_START] = config; + # ifdef TOUCH_DEBUG + saveSize += config.length() + 1; + # endif // ifdef TOUCH_DEBUG + + # ifdef TOUCH_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("Save settings: "); + config.replace(TOUCH_SETTINGS_SEPARATOR, ','); + log += config; + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // ifdef TOUCH_DEBUG + + String error; + + for (int objectNr = 0; objectNr < TOUCH_MAX_OBJECT_COUNT; objectNr++) { + config.clear(); + config += webArg(getPluginCustomArgName(objectNr + 100)); // Name + + if (!config.isEmpty()) { // Empty name => skip entry + bool numStart = (config[0] >= '0' && config[0] <= '9'); // Numeric start? + + if (!ExtraTaskSettings.checkInvalidCharInNames(config.c_str()) || + numStart) { // Check for invalid characters in objectname + error += F("Invalid character in objectname #"); + error += objectNr + 1; + error += numStart ? F(". Should not start with a digit.\n") : F(". Do not use ',-+/*=^%!#[]{}()' or space.\n"); + } + config += TOUCH_SETTINGS_SEPARATOR; + uint32_t flags = 0u; + bitWrite(flags, TOUCH_OBJECT_FLAG_ENABLED, isFormItemChecked(getPluginCustomArgName(objectNr + 0))); // Enabled + bitWrite(flags, TOUCH_OBJECT_FLAG_INVERTED, isFormItemChecked(getPluginCustomArgName(objectNr + 700))); // Inverted + # ifdef TOUCH_USE_EXTENDED_TOUCH + uint32_t groupFlags = 0u; + uint8_t buttonType = getFormItemInt(getPluginCustomArgName(objectNr + 800)); + set4BitToUL(flags, TOUCH_OBJECT_FLAG_BUTTONTYPE, buttonType); // Buttontype + set4BitToUL(flags, TOUCH_OBJECT_FLAG_BUTTONALIGN, getFormItemInt(getPluginCustomArgName(objectNr + 900)) >> 4); // Button layout + bitWrite(flags, TOUCH_OBJECT_FLAG_BUTTON, (static_cast(buttonType) != Button_type_e::None)); // On/Off button + // uint8_t buttonAction = getFormItemInt(getPluginCustomArgName(objectNr + 2000)); + // uint8_t buttonSelectGroup = ); + set4BitToUL(groupFlags, TOUCH_OBJECT_GROUP_ACTION, getFormItemInt(getPluginCustomArgName(objectNr + 2000))); // ButtonAction + set8BitToUL(groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP, getFormItemInt(getPluginCustomArgName(objectNr + 2100))); // ActionGroup + // uint8_t fontScale = getFormItemInt(getPluginCustomArgName(objectNr + 1200)); + set4BitToUL(flags, TOUCH_OBJECT_FLAG_FONTSCALE, getFormItemInt(getPluginCustomArgName(objectNr + 1200))); // Font scaling + uint8_t buttonGroup = getFormItemInt(getPluginCustomArgName(objectNr + 1600)); + set8BitToUL(flags, TOUCH_OBJECT_FLAG_GROUP, buttonGroup); // Button group + # else // ifdef TOUCH_USE_EXTENDED_TOUCH + bitWrite(flags, TOUCH_OBJECT_FLAG_BUTTON, isFormItemChecked(getPluginCustomArgName(objectNr + 600))); // On/Off button + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + + config += ull2String(flags); // Flags + config += TOUCH_SETTINGS_SEPARATOR; + config += toStringNoZero(getFormItemInt(getPluginCustomArgName(objectNr + 200))); // Top x + config += TOUCH_SETTINGS_SEPARATOR; + config += toStringNoZero(getFormItemInt(getPluginCustomArgName(objectNr + 300))); // Top y + config += TOUCH_SETTINGS_SEPARATOR; + config += toStringNoZero(getFormItemInt(getPluginCustomArgName(objectNr + 400))); // Bottom x + config += TOUCH_SETTINGS_SEPARATOR; + config += toStringNoZero(getFormItemInt(getPluginCustomArgName(objectNr + 500))); // Bottom y + + # ifdef TOUCH_USE_EXTENDED_TOUCH + config += TOUCH_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(objectNr + 1000)); // Color ON + config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true)); + config += TOUCH_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(objectNr + 1100)); // Color OFF + config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true)); + config += TOUCH_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(objectNr + 1500)); // Color caption + config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true)); + config += TOUCH_SETTINGS_SEPARATOR; // Caption ON + config += wrapWithQuotesIfContainsParameterSeparatorChar(webArg(getPluginCustomArgName(objectNr + 1300))); + config += TOUCH_SETTINGS_SEPARATOR; // Caption OFF + config += wrapWithQuotesIfContainsParameterSeparatorChar(webArg(getPluginCustomArgName(objectNr + 1400))); + config += TOUCH_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(objectNr + 1700)); // Color Border + config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true)); + config += TOUCH_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(objectNr + 1800)); // Disabled Color + config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true)); + config += TOUCH_SETTINGS_SEPARATOR; + colorInput = webArg(getPluginCustomArgName(objectNr + 1900)); // Disabled Caption Color + config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true)); + config += TOUCH_SETTINGS_SEPARATOR; + config += ull2String(groupFlags); // Group Flags + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + } + config.trim(); + + String endZero; // Trim off and 0 from the end + endZero += TOUCH_SETTINGS_SEPARATOR; + endZero += '0'; + uint8_t endZeroLen = endZero.length(); + + while (!config.isEmpty() && (config.endsWith(endZero) || config[config.length() - 1] == TOUCH_SETTINGS_SEPARATOR)) { + if (config[config.length() - 1] == TOUCH_SETTINGS_SEPARATOR) { + config.remove(config.length() - 1); + } else { + config.remove(config.length() - endZeroLen, endZeroLen); + } + } + + settingsArray[objectNr + TOUCH_OBJECT_INDEX_START] = config; + # ifdef TOUCH_DEBUG + saveSize += config.length() + 1; + # endif // ifdef TOUCH_DEBUG + + # ifdef TOUCH_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO) && + !config.isEmpty()) { + String log = F("Save object #"); + log += objectNr; + log += F(" settings: "); + config.replace(TOUCH_SETTINGS_SEPARATOR, ','); + log += config; + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // ifdef TOUCH_DEBUG + } + + if (!error.isEmpty()) { + addLog(LOG_LEVEL_ERROR, error); + addHtmlError(error); + } + + error = SaveCustomTaskSettings(event->TaskIndex, settingsArray, TOUCH_ARRAY_SIZE, 0); + + # ifdef TOUCH_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("TOUCH Save settings size: "); + log += saveSize; + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // ifdef TOUCH_DEBUG + + if (!error.isEmpty()) { + addLog(LOG_LEVEL_ERROR, error); + addHtmlError(error); + return false; + } + return true; +} + +/** + * Every 10th second we check if the screen is touched + */ +bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, + int16_t x, + int16_t y, + int16_t ox, + int16_t oy, + int16_t rx, + int16_t ry, + int16_t z) { + bool success = false; + + // Avoid event-storms by deduplicating coordinates + if (!_deduplicate || + (_deduplicate && ((TOUCH_VALUE_X != x) || (TOUCH_VALUE_Y != y) || (TOUCH_VALUE_Z != z)))) { + success = true; + TOUCH_VALUE_X = x; + TOUCH_VALUE_Y = y; + TOUCH_VALUE_Z = z; + } + + if (success && + Touch_Settings.logEnabled && + loglevelActiveFor(LOG_LEVEL_INFO)) { // REQUIRED for calibration and setting up objects, so do not make this optional! + String log; + log.reserve(72); + log = F("Touch calibration rx= "); // Space before the logged values added for readability + log += rx; + log += F(", ry= "); + log += ry; + log += F("; z= "); // Always log the z value even if not used. + log += z; + log += F(", x= "); + log += x; + log += F(", y= "); + log += y; + log += F("; ox= "); + log += ox; + log += F(", oy= "); + log += oy; + addLogMove(LOG_LEVEL_INFO, log); + } + + // No events to handle if rules not enabled + if (Settings.UseRules) { + if (success && bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_XY)) { // Send events for each touch + const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex); + + // Do NOT send a Z event for each touch? + if (!bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z) && validDeviceIndex(DeviceIndex)) { + Device[DeviceIndex].VType = Sensor_VType::SENSOR_TYPE_DUAL; + Device[DeviceIndex].ValueCount = 2; + } + sendData(event); // Send X/Y(/Z) event + + if (!bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z) && validDeviceIndex(DeviceIndex)) { // Reset device configuration + Device[DeviceIndex].VType = Sensor_VType::SENSOR_TYPE_TRIPLE; + Device[DeviceIndex].ValueCount = 3; + } + } + + if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME)) { // Send events for objectname if within reach + String selectedObjectName; + int8_t selectedObjectIndex = -1; + + if (isValidAndTouchedTouchObject(x, y, selectedObjectName, selectedObjectIndex)) { + // Not touched yet or too long ago + if ((TouchObjects[selectedObjectIndex].TouchTimers == 0) || + (TouchObjects[selectedObjectIndex].TouchTimers < (millis() - (1.5 * Touch_Settings.debounceMs)))) { + // From now wait the debounce time + TouchObjects[selectedObjectIndex].TouchTimers = millis() + Touch_Settings.debounceMs; + } else { + // Debouncing time elapsed? + if (TouchObjects[selectedObjectIndex].TouchTimers <= millis()) { + TouchObjects[selectedObjectIndex].TouchTimers = 0; + + if ((selectedObjectIndex > -1) && bitRead(TouchObjects[selectedObjectIndex].flags, TOUCH_OBJECT_FLAG_BUTTON)) { + TouchObjects[selectedObjectIndex].TouchStates = !TouchObjects[selectedObjectIndex].TouchStates; + generateObjectEvent(event, selectedObjectIndex, TouchObjects[selectedObjectIndex].TouchStates ? 1 : 0); + } else { + // Matching object is found, send # event with x, y and z as %eventvalue1/2/3% + String eventCommand; + eventCommand.reserve(48); + eventCommand = getTaskDeviceName(event->TaskIndex); + eventCommand += '#'; + eventCommand += selectedObjectName; + eventCommand += '='; // Add arguments + eventCommand += x; + eventCommand += ','; + eventCommand += y; + eventCommand += ','; + eventCommand += z; + eventQueue.addMove(std::move(eventCommand)); + } + } + } + } + } + } + return success; +} + +/** + * generate an event for a touch object + * When a display is configured add x,y coordinate, width,height of the object, objectIndex, and TaskIndex of display + **************************************************************************/ +void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, + const int8_t objectIndex, + const int8_t onOffState, + const int8_t mode, + const bool groupSwitch, + const int8_t factor) { + if ((objectIndex < 0) || // Range check + (objectIndex >= static_cast(TouchObjects.size()))) { + return; + } + delay(0); + String eventCommand; + String extraCommand; + + eventCommand.reserve(120); + extraCommand.reserve(48); + + extraCommand += getTaskDeviceName(event->TaskIndex); + extraCommand += '#'; + extraCommand += TouchObjects[objectIndex].objectName; + extraCommand += '='; // Add arguments: (%eventvalue#%) + + if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_DRAWBTN_VIA_RULES)) { + eventCommand = extraCommand; + } else { // Handle via direct btn commands + if (_displayTask != event->TaskIndex) { // Add arguments for display + eventCommand += '['; + eventCommand += _displayTask + 1; + eventCommand += F("].adagfx_trigger,btn,"); // Internal command trigger + } else { + addLog(LOG_LEVEL_ERROR, F("TOUCH: No valid Display task selected.")); + return; + } + } + + if (onOffState < 0) { // Negative value: pass on unaltered (1 = state) + eventCommand += onOffState; + extraCommand += onOffState; // duplicate + } else { // Check for inverted output (1 = state) + if (bitRead(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_INVERTED)) { + eventCommand += onOffState == 1 ? '0' : '1'; // Act like an inverted button, 0 = On, 1 = Off + extraCommand += onOffState == 1 ? '0' : '1'; // Act like an inverted button, 0 = On, 1 = Off // duplicate + } else { + eventCommand += onOffState == 1 ? '1' : '0'; // Act like a button, 1 = On, 0 = Off + extraCommand += onOffState == 1 ? '1' : '0'; // Act like a button, 1 = On, 0 = Off // duplicate + } + } + eventCommand += ','; + eventCommand += mode; // (2 = mode) + extraCommand += ','; + extraCommand += mode; // (2 = mode) // duplicate + + if (_displayTask != event->TaskIndex) { // Add arguments for display + eventCommand += ','; + eventCommand += TouchObjects[objectIndex].top_left.x; // (3 = x) + eventCommand += ','; + eventCommand += TouchObjects[objectIndex].top_left.y; // (4 = y) + eventCommand += ','; + eventCommand += TouchObjects[objectIndex].width_height.x; // (5 = width) + eventCommand += ','; + eventCommand += TouchObjects[objectIndex].width_height.y; // (6 = height) + eventCommand += ','; + eventCommand += objectIndex + 1; // Adjust to displayed index (7 = id) + eventCommand += ','; // (8 = type + layout, 4+4 bit, side by side) + eventCommand += get8BitFromUL(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_BUTTONTYPE) * factor; + # ifdef TOUCH_USE_EXTENDED_TOUCH + eventCommand += ','; // (9 = ON color) + eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorOn == 0 + ? Touch_Settings.colorOn + : TouchObjects[objectIndex].colorOn, + _colorDepth); + eventCommand += ','; // (10 = OFF color) + eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorOff == 0 + ? Touch_Settings.colorOff + : TouchObjects[objectIndex].colorOff, + _colorDepth); + eventCommand += ','; // (11 = Caption color) + eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorCaption == 0 + ? Touch_Settings.colorCaption + : TouchObjects[objectIndex].colorCaption, + _colorDepth); + eventCommand += ','; // (12 = Font scaling) + eventCommand += get4BitFromUL(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_FONTSCALE); + eventCommand += ','; // (13 = ON caption, default=object name) + eventCommand += wrapWithQuotesIfContainsParameterSeparatorChar(TouchObjects[objectIndex].captionOn.isEmpty() ? + TouchObjects[objectIndex].objectName : + TouchObjects[objectIndex].captionOn); + eventCommand += ','; // (14 = OFF caption) + eventCommand += wrapWithQuotesIfContainsParameterSeparatorChar(TouchObjects[objectIndex].captionOff.isEmpty() ? + TouchObjects[objectIndex].objectName : + TouchObjects[objectIndex].captionOff); + eventCommand += ','; // (15 = Border color) + eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorBorder == 0 + ? Touch_Settings.colorBorder + : TouchObjects[objectIndex].colorBorder, + _colorDepth); + eventCommand += ','; // (16 = Disabled color) + eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorDisabled == 0 + ? Touch_Settings.colorDisabled + : TouchObjects[objectIndex].colorDisabled, + _colorDepth); + eventCommand += ','; // (17 = Disabled caption color) + eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorDisabledCaption == 0 + ? Touch_Settings.colorDisabledCaption + : TouchObjects[objectIndex].colorDisabledCaption, + _colorDepth); + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + eventCommand += ','; + eventCommand += _displayTask + 1; // What TaskIndex? (18) or (9) + eventCommand += ','; // Group (19) or (10) + eventCommand += get8BitFromUL(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_GROUP); + # ifdef TOUCH_USE_EXTENDED_TOUCH + eventCommand += ','; // Group mode (20) + uint8_t action = get4BitFromUL(TouchObjects[objectIndex].groupFlags, TOUCH_OBJECT_GROUP_ACTION); + + if (!groupSwitch && (static_cast(action) != Touch_action_e::Default)) { + switch (static_cast(action)) { + case Touch_action_e::ActivateGroup: + eventCommand += get8BitFromUL(TouchObjects[objectIndex].groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP); + break; + case Touch_action_e::IncrementGroup: + eventCommand += -2; + break; + case Touch_action_e::DecrementGroup: + eventCommand += -3; + break; + case Touch_action_e::IncrementPage: + eventCommand += -4; + break; + case Touch_action_e::DecrementPage: + eventCommand += -5; + break; + case Touch_action_e::Default: + case Touch_action_e::TouchAction_MAX: + eventCommand += -1; // Ignore + break; + } + } else { + eventCommand += -1; // No group to activate + } + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + } + + # ifdef TOUCH_USE_EXTENDED_TOUCH + + if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_DRAWBTN_VIA_RULES)) { + eventQueue.addMove(std::move(eventCommand)); + } else { + eventCommand += ','; + eventCommand += wrapWithQuotesIfContainsParameterSeparatorChar(TouchObjects[objectIndex].objectName); + ExecuteCommand_all(EventValueSource::Enum::VALUE_SOURCE_RULES, eventCommand.c_str()); // Simulate like from rules + addLogMove(LOG_LEVEL_INFO, eventCommand); + delay(0); + + // Handle group actions + Touch_action_e action = static_cast(get4BitFromUL(TouchObjects[objectIndex].groupFlags, TOUCH_OBJECT_GROUP_ACTION)); + + if ((onOffState >= 0) && (mode >= 0)) { + if ((action == Touch_action_e::Default)) { + eventQueue.addMove(std::move(extraCommand)); // Issue the extra command for regular button presses + } else { + switch (action) { + case Touch_action_e::ActivateGroup: + setButtonGroup(event, get8BitFromUL(TouchObjects[objectIndex].groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP)); + break; + case Touch_action_e::IncrementGroup: + incrementButtonGroup(event); + break; + case Touch_action_e::DecrementGroup: + decrementButtonGroup(event); + break; + case Touch_action_e::IncrementPage: + incrementButtonPage(event); + break; + case Touch_action_e::DecrementPage: + decrementButtonPage(event); + break; + case Touch_action_e::Default: + case Touch_action_e::TouchAction_MAX: // no action + break; + } + String log = F("TOUCH event: "); + log += toString(action); + addLogMove(LOG_LEVEL_INFO, log); + } + } + } + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + delay(0); +} + +#endif // ifdef PLUGIN_USES_TOUCHHANDLER diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h new file mode 100644 index 0000000000..0623a4b2f9 --- /dev/null +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -0,0 +1,292 @@ +#ifndef HELPERS_ESPEASY_TOUCHHANDLER_H +#define HELPERS_ESPEASY_TOUCHHANDLER_H + +#include "../../_Plugin_Helper.h" +#include "../../ESPEasy_common.h" +#include "../Helpers/AdafruitGFX_helper.h" + +#ifdef PLUGIN_USES_TOUCHHANDLER +# include "../Commands/InternalCommands.h" +# include + +/***** + * Changelog: + * 2022-06-03 tonhuisman: Change default ON color to blue (from green, too bright/bad contrast with white captions) + * Add options for auto Enable/Disable arrow buttons and invert pgup/pgdn + * Bugfix: Also apply debouncing to non-button objects + * 2022-06-02 tonhuisman: Reduce saved settings size by eliminating 0 and similar unneeded values + * Move Touch minimal touch pressure back to P123 + * 2022-05-26 tonhuisman: Expand Captions to 30 characters + * 2022-05 tonhuisman: Testing, improving, bugfixing. + * 2022-05-23 tonhuisman: Created from refactoring P123 Touch object handling into ESPEasy_TouchHandler + *********************************************************************************************************************/ + +# define TOUCH_DEBUG // Additional debugging information + +# define TOUCH_USE_TOOLTIPS // Enable tooltips in UI + +# define TOUCH_USE_EXTENDED_TOUCH // Enable extended touch settings + +# ifdef LIMIT_BUILD_SIZE +# ifdef TOUCH_USE_TOOLTIPS +# undef TOUCH_USE_TOOLTIPS +# endif // ifdef TOUCH_USE_TOOLTIPS +# ifdef TOUCH_DEBUG +# undef TOUCH_DEBUG +# endif // ifdef TOUCH_DEBUG +# ifdef TOUCH_USE_EXTENDED_TOUCH +# undef TOUCH_USE_EXTENDED_TOUCH +# endif // ifdef TOUCH_USE_EXTENDED_TOUCH +# endif // ifdef LIMIT_BUILD_SIZE +# if defined(TOUCH_USE_TOOLTIPS) && !defined(ENABLE_TOOLTIPS) +# undef TOUCH_USE_TOOLTIPS +# endif // if defined(TOUCH_USE_TOOLTIPS) && !defined(ENABLE_TOOLTIPS) + +// Global Settings flags +# define TOUCH_FLAGS_SEND_XY 0 // Send X and Y coordinate events +# define TOUCH_FLAGS_SEND_Z 1 // Send Z coordinate (pressure) events +# define TOUCH_FLAGS_SEND_OBJECTNAME 2 // Send onjectname events +# define TOUCH_FLAGS_USE_CALIBRATION 3 // Enable calibration entry +# define TOUCH_FLAGS_LOG_CALIBRATION 4 // Enable logging for calibration +# define TOUCH_FLAGS_ROTATION_FLIPPED 5 // Rotation flipped 180 degrees +# define TOUCH_FLAGS_DEDUPLICATE 6 // Avoid duplicate events +# define TOUCH_FLAGS_INIT_OBJECTEVENT 7 // Draw button objects when started +# define TOUCH_FLAGS_INITIAL_GROUP 8 // Initial group to activate, 8 bits +# define TOUCH_FLAGS_DRAWBTN_VIA_RULES 16 // Draw buttons using rule +# define TOUCH_FLAGS_AUTO_PAGE_ARROWS 17 // Automatically enable/disable paging buttons +# define TOUCH_FLAGS_PGUP_BELOW_MENU 18 // Group-page below current menu (reverts Up/Down buttons) + +# define TOUCH_VALUE_X UserVar[event->BaseVarIndex + 0] +# define TOUCH_VALUE_Y UserVar[event->BaseVarIndex + 1] +# define TOUCH_VALUE_Z UserVar[event->BaseVarIndex + 2] + +# define TOUCH_TS_ROTATION 0 // Rotation 0-3 = 0/90/180/270 degrees +# define TOUCH_TS_SEND_XY true // Enable/Disable X/Y events +# define TOUCH_TS_SEND_Z false // Disable/Enable Z events +# define TOUCH_TS_SEND_OBJECTNAME true // Enable/Disable objectname events +# define TOUCH_TS_USE_CALIBRATION false // Disable/Enable calibration +# define TOUCH_TS_LOG_CALIBRATION true // Enable/Disable calibration logging +# define TOUCH_TS_ROTATION_FLIPPED false // Enable/Disable rotation flipped 180 deg. +# define TOUCH_TS_X_RES 320 // Pixels, should match with the screen it is mounted on +# define TOUCH_TS_Y_RES 480 +# define TOUCH_DEBOUNCE_MILLIS 100 // Debounce delay for On/Off button function + +# define TOUCH_MAX_COLOR_INPUTLENGTH 11 // 11 Characters is enough to type in all recognized color names and values +# define TOUCH_MaxObjectNameLength 15 // 15 character objectnames +# define TOUCH_MaxCaptionNameLength 30 // 30 character captions, to allow variable names +# define TOUCH_MAX_CALIBRATION_COUNT 1 // +# define TOUCH_MAX_OBJECT_COUNT 40 // This count of touchobjects should be enough, because of limited + // settings storage, 1024 bytes +# define TOUCH_EXTRA_OBJECT_COUNT 5 // The number of empty objects to show if max not reached +# define TOUCH_ARRAY_SIZE (TOUCH_MAX_OBJECT_COUNT + TOUCH_MAX_CALIBRATION_COUNT) + +# define TOUCH_MAX_BUTTON_GROUPS 255 // Max. allowed button groups + +# define TOUCH_SETTINGS_SEPARATOR '\x02' + +// Settings array field offsets: Calibration +# define TOUCH_CALIBRATION_START 0 // Index into settings array +# define TOUCH_CALIBRATION_ENABLED 1 // Enabled 0/1 (parseString index starts at 1) +# define TOUCH_CALIBRATION_LOG_ENABLED 2 // Calibration Log Enabled 0/1 +# define TOUCH_CALIBRATION_TOP_X 3 // Top X offset (uint16_t) +# define TOUCH_CALIBRATION_TOP_Y 4 // Top Y +# define TOUCH_CALIBRATION_BOTTOM_X 5 // Bottom X +# define TOUCH_CALIBRATION_BOTTOM_Y 6 // Bottom Y +# define TOUCH_COMMON_DEBOUNCE_MS 7 // Debounce milliseconds +# define TOUCH_COMMON_FLAGS 8 // Common flags +# ifdef TOUCH_USE_EXTENDED_TOUCH +# define TOUCH_COMMON_DEF_COLOR_ON 9 // Default Color ON (rgb565, uint16_t) +# define TOUCH_COMMON_DEF_COLOR_OFF 10 // Default Color OFF +# define TOUCH_COMMON_DEF_COLOR_BORDER 11 // Default Color Border +# define TOUCH_COMMON_DEF_COLOR_CAPTION 12 // Default Color Caption +# define TOUCH_COMMON_DEF_COLOR_DISABLED 13 // Default Disabled Color +# define TOUCH_COMMON_DEF_COLOR_DISABCAPT 14 // Default Disabled Caption Color +# endif // ifdef TOUCH_USE_EXTENDED_TOUCH + +// Settings array field offsets: Touch objects +# define TOUCH_OBJECT_INDEX_START (TOUCH_CALIBRATION_START + 1) +# define TOUCH_OBJECT_INDEX_END (TOUCH_ARRAY_SIZE - (TOUCH_CALIBRATION_START + 1)) +# define TOUCH_OBJECT_NAME 1 // Name (String 14) (parseString index starts at 1) +# define TOUCH_OBJECT_FLAGS 2 // Flags (uint32_t) +# define TOUCH_OBJECT_COORD_TOP_X 3 // Top X (uint16_t) +# define TOUCH_OBJECT_COORD_TOP_Y 4 // Top Y +# define TOUCH_OBJECT_COORD_WIDTH 5 // Width +# define TOUCH_OBJECT_COORD_HEIGHT 6 // Height +# ifdef TOUCH_USE_EXTENDED_TOUCH +# define TOUCH_OBJECT_COLOR_ON 7 // Color ON (rgb565, uint16_t) +# define TOUCH_OBJECT_COLOR_OFF 8 // Color OFF +# define TOUCH_OBJECT_COLOR_CAPTION 9 // Color Caption +# define TOUCH_OBJECT_CAPTION_ON 10 // Caption ON (String 12, quoted) +# define TOUCH_OBJECT_CAPTION_OFF 11 // Caption OFF (String 12, quoted) +# define TOUCH_OBJECT_COLOR_BORDER 12 // Color Border +# define TOUCH_OBJECT_COLOR_DISABLED 13 // Disabled Color +# define TOUCH_OBJECT_COLOR_DISABCAPT 14 // Disabled Caption Color +# define TOUCH_OBJECT_GROUPFLAGS 15 // Group flags +# endif // ifdef TOUCH_USE_EXTENDED_TOUCH + +# define TOUCH_OBJECT_FLAG_ENABLED 0 // Enabled +# define TOUCH_OBJECT_FLAG_BUTTON 1 // Button behavior +# define TOUCH_OBJECT_FLAG_INVERTED 2 // Inverted button +# define TOUCH_OBJECT_FLAG_FONTSCALE 3 // 4 bits used as button alignment +# define TOUCH_OBJECT_FLAG_BUTTONTYPE 7 // 4 bits used as button type (low 4 bits) +# define TOUCH_OBJECT_FLAG_BUTTONALIGN 11 // 4 bits used as button caption layout (high 4 bits) +# define TOUCH_OBJECT_FLAG_GROUP 16 // 8 bits used as button group + +# define TOUCH_OBJECT_GROUP_ACTIONGROUP 8 // 8 bits used as action group +# define TOUCH_OBJECT_GROUP_ACTION 16 // 4 bits used as action option + +# define TOUCH_DEFAULT_COLOR_DISABLED 0x9410 +# define TOUCH_DEFAULT_COLOR_DISABLED_CAPTION 0x5A69 + +// Lets Touchne our own coordinate point +struct tTouch_Point +{ + uint16_t x = 0u; + uint16_t y = 0u; +}; + +// For touch objects we store a name, 2 coordinates, flags and other options +struct tTouchObjects +{ + String objectName; + String captionOn; + String captionOff; + uint32_t flags = 0u; + uint32_t SurfaceAreas = 0u; + uint32_t TouchTimers = 0u; + tTouch_Point top_left; + tTouch_Point width_height; + # ifdef TOUCH_USE_EXTENDED_TOUCH + uint32_t groupFlags = 0u; + uint16_t colorOn = 0u; + uint16_t colorOff = 0u; + uint16_t colorCaption = 0u; + uint16_t colorBorder = 0u; + uint16_t colorDisabled = 0u; + uint16_t colorDisabledCaption = 0u; + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + bool TouchStates = false; +}; + +// Touch actions +enum class Touch_action_e : uint8_t { + Default = 0u, + ActivateGroup = 1u, + IncrementGroup = 2u, + DecrementGroup = 3u, + IncrementPage = 4u, + DecrementPage = 5u, + TouchAction_MAX = 6u // Last item is count, max 16! +}; + +const __FlashStringHelper* toString(Touch_action_e action); + +class ESPEasy_TouchHandler { +public: + + ESPEasy_TouchHandler(); + ESPEasy_TouchHandler(uint16_t displayTask, + AdaGFXColorDepth colorDepth); + virtual ~ESPEasy_TouchHandler() {} + + void loadTouchObjects(struct EventStruct *event); + void init(struct EventStruct *event); + bool isCalibrationActive(); + bool isValidAndTouchedTouchObject(int16_t x, + int16_t y, + String& selectedObjectName, + int8_t& selectedObjectIndex); + int8_t getTouchObjectIndex(struct EventStruct *event, + const String & touchObject, + bool isButton = false); + bool setTouchObjectState(struct EventStruct *event, + const String & touchObject, + bool state); + bool setTouchButtonOnOff(struct EventStruct *event, + const String & touchObject, + bool state); + bool plugin_webform_load(struct EventStruct *event); + bool plugin_webform_save(struct EventStruct *event); + bool plugin_fifty_per_second(struct EventStruct *event, + int16_t x, + int16_t y, + int16_t ox, + int16_t oy, + int16_t rx, + int16_t ry, + int16_t z); + int16_t getButtonGroup() { + return _buttonGroup; + } + + bool validButtonGroup(int16_t group, + bool ignoreZero = false); + bool setButtonGroup(struct EventStruct *event, + int16_t buttonGroup); + bool incrementButtonGroup(struct EventStruct *event); + bool decrementButtonGroup(struct EventStruct *event); + bool incrementButtonPage(struct EventStruct *event); + bool decrementButtonPage(struct EventStruct *event); + void displayButtonGroup(struct EventStruct *event, + int16_t buttonGroup, + int8_t mode = 0); + bool displayButton(struct EventStruct *event, + int8_t buttonNr, + int16_t buttonGroup = -1, + int8_t mode = 0); + +private: + + int parseStringToInt(const String& string, + uint8_t indexFind, + char separator = ',', + int defaultValue = 0); + void generateObjectEvent(struct EventStruct *event, + const int8_t objectIndex, + const int8_t onOffState, + const int8_t mode = 0, + const bool groupSwitch = false, + const int8_t factor = 1); + + bool _deduplicate = false; + uint16_t _displayTask = 0u; + AdaGFXColorDepth _colorDepth = AdaGFXColorDepth::FullColor; + int16_t _buttonGroup = 0; + + std::set_buttonGroups; + + bool _settingsLoaded = false; + + struct tTouch_Globals + { + uint32_t flags = 0u; + tTouch_Point top_left; + tTouch_Point bottom_right; + # ifdef TOUCH_USE_EXTENDED_TOUCH + uint16_t colorOn = 0u; + uint16_t colorOff = 0u; + uint16_t colorCaption = 0u; + uint16_t colorBorder = 0u; + uint16_t colorDisabled = 0u; + uint16_t colorDisabledCaption = 0u; + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + uint8_t debounceMs = 0u; + bool calibrationEnabled = false; + bool logEnabled = false; + }; + + std::vectorTouchObjects; + +public: + + bool _flipped = false; // buffered settings + bool _useCalibration = false; + + tTouch_Globals Touch_Settings; + + String settingsArray[TOUCH_ARRAY_SIZE]; + uint8_t lastObjectIndex = 0u; + uint8_t objectCount = 0u; +}; +#endif // ifdef PLUGIN_USES_TOUCHHANDLER +#endif // ifndef HELPERS_ESPEASY_TOUCHHANDLER_H From 504d612d986991e7f44ffedef5d9ba6ffc5166dd Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 4 Jun 2022 12:51:32 +0200 Subject: [PATCH 012/113] [P123] Refactor and implement ESPEasy_TouchHandler --- src/_P123_FT62x6Touch.ino | 119 +- src/src/PluginStructs/P123_data_struct.cpp | 1515 +++----------------- src/src/PluginStructs/P123_data_struct.h | 299 +--- 3 files changed, 304 insertions(+), 1629 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index b44074dbfc..875eecc7bd 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -6,6 +6,10 @@ /** * Changelog: + * 2022-05-29 tonhuisman: Extend enable,disable subcommands to support a list of objects + * 2022-05-28 tonhuisman: Add incpage and decpage subcommands that + and - 10 to the current buttongroup + * 2022-05-26 tonhuisman: Add touch,updatebutton command + * 2022-05-23 tonhuisman: Refactor touch settings and button emulation into ESPEasy_TouchHandler class for reuse in P099 * 2022-05-02 tonhuisman: Small updates and improvements * 2022-04-30 tonhuisman: Add support for AdaGFX btn subcommand use and (local) button groups * Start preparations for refactoring touch objects into separate helper class @@ -21,15 +25,18 @@ /** * Commands supported: * ------------------- - * touch,rot,<0..3> : Set rotation to 0(0), 90(1), 180(2), 270(3) degrees - * touch,flip,<0|1> : Set rotation normal(0) or flipped by 180 degrees(1) - * touch,enable, : Enables a disabled objectname (removes a leading underscore) - * touch,disable, : Disables an enabled objectname (adds a leading underscore) - * touch,on, : Switch a TouchButton on (must be enabled) - * touch,off, : Switch a TouchButton off (must be enabled) - * touch,setgrp, : Switch to button group - * touch,incgrp : Switch to next button group - * touch,decgrp : Switch to previous button group + * touch,rot,<0..3> : Set rotation to 0(0), 90(1), 180(2), 270(3) degrees + * touch,flip,<0|1> : Set rotation normal(0) or flipped by 180 degrees(1) + * touch,enable,[,...] : Enable disabled objectname(s) + * touch,disable,[,...] : Disable enabled objectname(s) + * touch,on, : Switch a TouchButton on (must be enabled) + * touch,off, : Switch a TouchButton off (must be enabled) + * touch,setgrp, : Switch to button group + * touch,incgrp : Switch to next button group + * touch,decgrp : Switch to previous button group + * touch,incpage : Switch to next button group page (+10) + * touch,decpage : Switch to previous button group page (-10) + * touch,updatebutton,[,[,]] : Update a button by name or number */ #define PLUGIN_123 @@ -61,6 +68,7 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) Device[deviceCount].ValueCount = 3; Device[deviceCount].SendDataOption = false; Device[deviceCount].TimerOption = false; + Device[deviceCount].ExitTaskBeforeSave = false; success = true; break; } @@ -132,6 +140,8 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) AdaGFXFormColorDepth(F("p123_colordepth"), P123_COLOR_DEPTH, (colorDepth_ == 0)); + addFormNumericBox(F("Touch minimum pressure"), F("p123_treshold"), P123_CONFIG_TRESHOLD, 0, 255); + { P123_data_struct *P123_data = new (std::nothrow) P123_data_struct(); @@ -151,6 +161,7 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SAVE: { P123_CONFIG_DISPLAY_PREV = P123_CONFIG_DISPLAY_TASK; + P123_CONFIG_TRESHOLD = getFormItemInt(F("p123_treshold")); P123_CONFIG_DISPLAY_TASK = getFormItemInt(F("p123_task")); P123_CONFIG_ROTATION = getFormItemInt(F("p123_rotate")); P123_CONFIG_X_RES = getFormItemInt(F("p123_width")); @@ -162,8 +173,6 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) P123_COLOR_DEPTH = colorDepth; } - if (P123_CONFIG_OBJECTCOUNT > P123_MAX_OBJECT_COUNT) { P123_CONFIG_OBJECTCOUNT = P123_MAX_OBJECT_COUNT; } - P123_data_struct *P123_data = new (std::nothrow) P123_data_struct(); if (nullptr == P123_data) { @@ -188,15 +197,9 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) success = true; - if (!(P123_data->init(event, - P123_CONFIG_ROTATION, - P123_CONFIG_X_RES, - P123_CONFIG_Y_RES, - P123_CONFIG_DISPLAY_TASK, - static_cast(P123_COLOR_DEPTH)))) { + if (!(P123_data->init(event))) { delete P123_data; - P123_data = nullptr; - success = false; + success = false; } break; } @@ -205,65 +208,13 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WRITE: { - String command; - String subcommand; - String arguments; - arguments.reserve(24); - { - command = parseString(string, 1); - subcommand = parseString(string, 2); - P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); if (nullptr == P123_data) { return success; } - - if (command.equals(F("touch"))) { - #ifdef PLUGIN_123_DEBUG - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("P123 WRITE arguments Par1:"); - log += event->Par1; - log += F(", 2: "); - log += event->Par2; - log += F(", 3: "); - log += event->Par3; - log += F(", 4: "); - log += event->Par4; - addLog(LOG_LEVEL_INFO, log); - } - #endif // ifdef PLUGIN_123_DEBUG - - if (subcommand.equals(F("rot"))) { // touch,rot,<0..3> : Set rotation to 0, 90, 180, 270 degrees - uint8_t rot_ = static_cast(event->Par2 % 4); - - P123_data->setRotation(rot_); - success = true; - } else if (subcommand.equals(F("flip"))) { // touch,flip,<0|1> : Flip rotation by 0 or 180 degrees - P123_data->setRotationFlipped(event->Par2 > 0); - success = true; - } else if (subcommand.equals(F("enable"))) { // touch,enable, : Enables a disabled objectname - arguments = parseString(string, 3); - success = P123_data->setTouchObjectState(event, arguments, true); - } else if (subcommand.equals(F("disable"))) { // touch,disable, : Disables an enabled objectname - arguments = parseString(string, 3); - success = P123_data->setTouchObjectState(event, arguments, false); - } else if (subcommand.equals(F("on"))) { // touch,on, : Switch a TouchButton on - arguments = parseString(string, 3); - success = P123_data->setTouchButtonOnOff(event, arguments, true); - } else if (subcommand.equals(F("off"))) { // touch,off, : Switch a TouchButton off - arguments = parseString(string, 3); - success = P123_data->setTouchButtonOnOff(event, arguments, false); - } else if (subcommand.equals(F("setgrp"))) { // touch,setgrp, : Activate button group - success = P123_data->setButtonGroup(event, event->Par2); - } else if (subcommand.equals(F("incgrp"))) { // touch,incgrp : increment group and Activate - success = P123_data->incrementButtonGroup(event); - } else if (subcommand.equals(F("decgrp"))) { // touch,decgrp : Decrement group and Activate - success = P123_data->decrementButtonGroup(event); - } - } + success = P123_data->plugin_write(event, string); } break; } @@ -280,6 +231,30 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) break; } + case PLUGIN_GET_CONFIG_VALUE: + { + P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr == P123_data) { + return success; + } + String command = parseString(string, 1); + + if (command == F("buttongroup")) { + string = P123_data->getButtonGroup(); + success = true; + } else if (command == F("hasgroup")) { + int group; // We'll be ignoring group 0 if there are multiple button groups + + if (validIntFromString(parseString(string, 2), group)) { + string = P123_data->validButtonGroup(group, true) ? 1 : 0; + success = true; + } else { + string = '0'; // invalid number + } + } + break; + } } // switch(function) return success; } // Plugin_123 diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index f16fba7377..f291797894 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -11,27 +11,13 @@ # include "../Commands/InternalCommands.h" -/**************************************************************************** - * toString: Display-value for the touch action - ***************************************************************************/ -# ifdef P123_USE_EXTENDED_TOUCH -const __FlashStringHelper* toString(P123_touch_action_e action) { - switch (action) { - case P123_touch_action_e::Default: return F("Default"); - case P123_touch_action_e::ActivateGroup: return F("Activate Group"); - case P123_touch_action_e::IncrementGroup: return F("Next Group"); - case P123_touch_action_e::DecrementGroup: return F("Previous Group"); - case P123_touch_action_e::TouchAction_MAX: break; - } - return F("Unsupported!"); -} - -# endif // ifdef P123_USE_EXTENDED_TOUCH - /** * Constructor */ -P123_data_struct::P123_data_struct() : touchscreen(nullptr) {} +P123_data_struct::P123_data_struct() + : touchscreen(nullptr) { + touchHandler = new (std::nothrow) ESPEasy_TouchHandler(); // Temporary object to be able to call loadTouchObjects +} /** * Destructor @@ -48,59 +34,33 @@ void P123_data_struct::reset() { addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen reset.")); # endif // PLUGIN_123_DEBUG - if (isInitialized()) { - delete touchscreen; - touchscreen = nullptr; - } + delete touchscreen; + touchscreen = nullptr; + delete touchHandler; + touchHandler = nullptr; } /** * Initialize data and set up the touchscreen. */ -bool P123_data_struct::init(const EventStruct *event, - uint8_t rotation, - uint16_t ts_x_res, - uint16_t ts_y_res, - uint16_t displayTask, - AdaGFXColorDepth colorDepth) { - _rotation = rotation; - _ts_x_res = ts_x_res; - _ts_y_res = ts_y_res; - _displayTask = displayTask; - _colorDepth = colorDepth; +bool P123_data_struct::init(struct EventStruct *event) { + _rotation = P123_CONFIG_ROTATION; + _ts_x_res = P123_CONFIG_X_RES; + _ts_y_res = P123_CONFIG_Y_RES; reset(); touchscreen = new (std::nothrow) Adafruit_FT6206(); - if (isInitialized()) { - loadTouchObjects(event); - - touchscreen->begin(P123_Settings.treshold); - - if (bitRead(P123_Settings.flags, P123_FLAGS_SEND_OBJECTNAME) && - bitRead(P123_Settings.flags, P123_FLAGS_INIT_OBJECTEVENT)) { - if (_maxButtonGroup > 0) { // Multiple groups? - displayButtonGroup(event, _buttonGroup, -3); // Clear all groups - } - _buttonGroup = get8BitFromUL(P123_Settings.flags, P123_FLAGS_INITIAL_GROUP); - # ifdef PLUGIN_123_DEBUG - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("P123 DEBUG group: "); - log += _buttonGroup; - log += F(", max group: "); - log += _maxButtonGroup; - addLogMove(LOG_LEVEL_INFO, log); - } - # endif // ifdef PLUGIN_123_DEBUG + if (touchscreen != nullptr) { + touchHandler = new (std::nothrow) ESPEasy_TouchHandler(P123_CONFIG_DISPLAY_TASK, + static_cast(P123_COLOR_DEPTH)); + } - displayButtonGroup(event, _buttonGroup); // Initialize selected group and group 0 + if (isInitialized()) { + touchscreen->begin(P123_CONFIG_TRESHOLD); - # ifdef PLUGIN_123_DEBUG - addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG group done.")); - # endif // ifdef PLUGIN_123_DEBUG - } + touchHandler->init(event); # ifdef PLUGIN_123_DEBUG addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Plugin & touchscreen initialized.")); @@ -114,783 +74,138 @@ bool P123_data_struct::init(const EventStruct *event, /** * mode: -2 = clear buttons in group, -3 = clear all buttongroups, -1 = draw buttons in group, 0 = initialize buttons */ -void P123_data_struct::displayButtonGroup(const EventStruct *event, - int8_t buttonGroup, +void P123_data_struct::displayButtonGroup(struct EventStruct *event, + int16_t buttonGroup, int8_t mode) { - for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { - int8_t state = 99; - int8_t group = get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_GROUP); - - if (!TouchObjects[objectNr].objectName.isEmpty() && - ((bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED) && (group == 0)) || (group > 0)) && - bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTON) && - ((group == buttonGroup) || - ((mode != -2) && (group == 0)) || - (mode == -3))) { - if (bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED)) { - if (mode == 0) { - state = -1; - } else { - if (bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_INVERTED)) { - state = TouchObjects[objectNr].TouchStates ? 0 : 1; // Act like an inverted button, 0 = On, 1 = Off - } else { - state = TouchObjects[objectNr].TouchStates ? 1 : 0; // Act like a button, 1 = On, 0 = Off - } - } - } else { - state = -2; - } - generateObjectEvent(event, objectNr, state, mode < 0, mode <= -2 ? -1 : 1); - } - # ifdef XX_PLUGIN_123_DEBUG // TODO Temporarily disabled - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("P123: button init, state: "); - log += state; - log += F(", group: "); - log += buttonGroup; - log += F(", mode: "); - log += mode; - log += F(", group: "); - log += get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_GROUP); - log += F(", en: "); - log += bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTON); - log += F(", object: "); - log += objectNr; - addLog(LOG_LEVEL_INFO, log); - } - # endif // ifdef PLUGIN_123_DEBUG - - delay(0); - } - - // Send an event #Group,, with the selected group and the mode (-3..0) - String eventCommand; - eventCommand.reserve(24); - eventCommand += getTaskDeviceName(event->TaskIndex); - eventCommand += '#'; - eventCommand += F("Group"); - eventCommand += '='; // Add arguments - eventCommand += buttonGroup; - eventCommand += ','; - eventCommand += mode; - eventQueue.addMove(std::move(eventCommand)); + touchHandler->displayButtonGroup(event, buttonGroup, mode); +} - delay(0); +/** + * (Re)Display a button + */ +bool P123_data_struct::displayButton(struct EventStruct *event, + int8_t buttonNr, + int16_t buttonGroup, + int8_t mode) { + return touchHandler->displayButton(event, buttonNr, buttonGroup, mode); } /** * Properly initialized? then true */ bool P123_data_struct::isInitialized() const { - return touchscreen != nullptr; -} - -int P123_data_struct::parseStringToInt(const String& string, uint8_t indexFind, char separator, int defaultValue) { - String parsed = parseStringKeepCase(string, indexFind, separator); - - int result = defaultValue; - - validIntFromString(parsed, result); - - return result; + return touchscreen != nullptr && touchHandler != nullptr; } /** * Load the settings onto the webpage */ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { - addFormSubHeader(F("Touch configuration")); - - addFormCheckBox(F("Flip rotation 180°"), F("tch_rotation_flipped"), bitRead(P123_Settings.flags, P123_FLAGS_ROTATION_FLIPPED)); - # ifndef LIMIT_BUILD_SIZE - addFormNote(F("Some touchscreens are mounted 180° rotated on the display.")); - # endif // ifndef LIMIT_BUILD_SIZE - - addFormNumericBox(F("Touch minimum pressure"), F("tch_treshold"), - P123_Settings.treshold, 0, 255); - - uint8_t choice3 = 0u; - - bitWrite(choice3, P123_FLAGS_SEND_XY, bitRead(P123_Settings.flags, P123_FLAGS_SEND_XY)); - bitWrite(choice3, P123_FLAGS_SEND_Z, bitRead(P123_Settings.flags, P123_FLAGS_SEND_Z)); - bitWrite(choice3, P123_FLAGS_SEND_OBJECTNAME, bitRead(P123_Settings.flags, P123_FLAGS_SEND_OBJECTNAME)); - { - # define P123_EVENTS_OPTIONS 6 - const __FlashStringHelper *options3[P123_EVENTS_OPTIONS] = - { F("None"), - F("X and Y"), - F("X, Y and Z"), - F("Objectnames only"), - F("Objectnames, X and Y"), - F("Objectnames, X, Y and Z") - }; - int optionValues3[P123_EVENTS_OPTIONS] = { 0, 1, 3, 4, 5, 7 }; // Already used as a bitmap! - addFormSelector(F("Events"), F("tch_events"), P123_EVENTS_OPTIONS, options3, optionValues3, choice3); - - addFormCheckBox(F("Initial Objectnames events"), F("tch_init_objectevent"), bitRead(P123_Settings.flags, P123_FLAGS_INIT_OBJECTEVENT)); - addFormNote(F("Will send state -1 but only for enabled On/Off button objects.")); - } - - addFormCheckBox(F("Prevent duplicate events"), F("tch_deduplicate"), bitRead(P123_Settings.flags, P123_FLAGS_DEDUPLICATE)); - - # ifndef LIMIT_BUILD_SIZE - - if (!Settings.UseRules) { - addFormNote(F("Tools / Advanced / Rules must be enabled for events to be fired.")); - } - # endif // ifndef LIMIT_BUILD_SIZE - - addFormSubHeader(F("Calibration")); - - { - const __FlashStringHelper *noYesOptions[2] = { F("No"), F("Yes") }; - int noYesOptionValues[2] = { 0, 1 }; - addFormSelector(F("Calibrate to screen resolution"), - F("tch_use_calibration"), - 2, - noYesOptions, - noYesOptionValues, - P123_Settings.calibrationEnabled ? 1 : 0, - true); - } - - if (P123_Settings.calibrationEnabled) { - addRowLabel(F("Calibration")); - html_table(EMPTY_STRING, false); // Sub-table - html_table_header(F("")); - html_table_header(F("x")); - html_table_header(F("y")); - html_table_header(F("")); - html_table_header(F("x")); - html_table_header(F("y")); - - html_TR_TD(); - addHtml(F("Top-left")); - html_TD(); - addNumericBox(F("tch_cal_tl_x"), - P123_Settings.top_left.x, - 0, - 65535); - html_TD(); - addNumericBox(F("tch_cal_tl_y"), - P123_Settings.top_left.y, - 0, - 65535); - html_TD(); - addHtml(F("Bottom-right")); - html_TD(); - addNumericBox(F("tch_cal_br_x"), - P123_Settings.bottom_right.x, - 0, - 65535); - html_TD(); - addNumericBox(F("tch_cal_br_y"), - P123_Settings.bottom_right.y, - 0, - 65535); - - html_end_table(); - } - - addFormCheckBox(F("Enable logging for calibration"), F("tch_log_calibration"), - P123_Settings.logEnabled); - - addFormSubHeader(F("Touch objects")); - - # ifdef P123_USE_EXTENDED_TOUCH - - AdaGFXHtmlColorDepthDataList(F("adagfx65kcolors"), static_cast(P123_COLOR_DEPTH)); - - { - String parsed; - addRowLabel(F("Default On/Off button colors")); - html_table(EMPTY_STRING, false); // Sub-table - html_table_header(F("ON color")); - html_table_header(F("OFF color")); - html_table_header(F("Border color")); - html_table_header(F("Caption color")); - html_table_header(F("Disabled color")); - html_table_header(F("Disabled caption color")); - - html_TR_TD(); // ON color - parsed = AdaGFXcolorToString(P123_Settings.colorOn, _colorDepth, true); - addTextBox(getPluginCustomArgName(3000), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, - EMPTY_STRING, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("ON color") - # endif // ifdef P123_USE_TOOLTIPS - , F("adagfx65kcolors") - ); - html_TD(); // OFF color - parsed = AdaGFXcolorToString(P123_Settings.colorOff, _colorDepth, true); - addTextBox(getPluginCustomArgName(3001), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, - EMPTY_STRING, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("OFF color") - # endif // ifdef P123_USE_TOOLTIPS - , F("adagfx65kcolors") - ); - html_TD(); // Border color - parsed = AdaGFXcolorToString(P123_Settings.colorBorder, _colorDepth, true); - addTextBox(getPluginCustomArgName(3002), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, - EMPTY_STRING, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("Border color") - # endif // ifdef P123_USE_TOOLTIPS - , F("adagfx65kcolors") - ); - html_TD(); // Caption color - parsed = AdaGFXcolorToString(P123_Settings.colorCaption, _colorDepth, true); - addTextBox(getPluginCustomArgName(3003), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, - EMPTY_STRING, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("Caption color") - # endif // ifdef P123_USE_TOOLTIPS - , F("adagfx65kcolors") - ); - html_TD(); // Disabled color - parsed = AdaGFXcolorToString(P123_Settings.colorDisabled, _colorDepth, true); - addTextBox(getPluginCustomArgName(3004), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, - EMPTY_STRING, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("Disabled color") - # endif // ifdef P123_USE_TOOLTIPS - , F("adagfx65kcolors") - ); - html_TD(); // Disabled caption color - parsed = AdaGFXcolorToString(P123_Settings.colorDisabledCaption, _colorDepth, true); - addTextBox(getPluginCustomArgName(3005), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, - EMPTY_STRING, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("Disabled caption color") - # endif // ifdef P123_USE_TOOLTIPS - , F("adagfx65kcolors") - ); - html_end_table(); - } - { - addFormNumericBox(F("Initial button group"), F("tch_initial_group"), - get8BitFromUL(P123_Settings.flags, P123_FLAGS_INITIAL_GROUP), 0, P123_MAX_BUTTON_GROUPS - # ifdef P123_USE_TOOLTIPS - , F("Initial group") - # endif // ifdef P123_USE_TOOLTIPS - ); - } - # endif // ifdef P123_USE_EXTENDED_TOUCH - { - addRowLabel(F("Object")); - - { - html_table(EMPTY_STRING, false); // Sub-table - html_table_header(F(" # ")); - html_table_header(F("On")); - html_table_header(F("Objectname")); - html_table_header(F("Top-left x")); - html_table_header(F("Top-left y")); - # ifdef P123_USE_EXTENDED_TOUCH - html_table_header(F("Button")); - html_table_header(F("Layout")); - html_table_header(F("ON color")); - html_table_header(F("ON caption")); - html_table_header(F("Border color")); - html_table_header(F("Disab. cap. clr")); - html_table_header(F("Touch action")); - # else // ifdef P123_USE_EXTENDED_TOUCH - html_table_header(F("On/Off button")); - # endif // ifdef P123_USE_EXTENDED_TOUCH - html_TR(); // New row - html_table_header(EMPTY_STRING); - html_table_header(EMPTY_STRING); - # ifdef P123_USE_EXTENDED_TOUCH - html_table_header(F("Button-group")); - # else // ifdef P123_USE_EXTENDED_TOUCH - html_table_header(EMPTY_STRING); - # endif // ifdef P123_USE_EXTENDED_TOUCH - html_table_header(F("Width")); - html_table_header(F("Height")); - html_table_header(F("Inverted")); - # ifdef P123_USE_EXTENDED_TOUCH - html_table_header(F("Font scale")); - html_table_header(F("OFF color")); - html_table_header(F("OFF caption")); - html_table_header(F("Caption color")); - html_table_header(F("Disabled clr")); - html_table_header(F("Action group")); - # endif // ifdef P123_USE_EXTENDED_TOUCH - } - # ifdef P123_USE_EXTENDED_TOUCH - const __FlashStringHelper *buttonTypeOptions[] = { - toString(Button_type_e::None), - toString(Button_type_e::Square), - toString(Button_type_e::Rounded), - toString(Button_type_e::Circle), - toString(Button_type_e::ArrowLeft), - toString(Button_type_e::ArrowUp), - toString(Button_type_e::ArrowRight), - toString(Button_type_e::ArrowDown), - }; - - const int buttonTypeValues[] = { - static_cast(Button_type_e::None), - static_cast(Button_type_e::Square), - static_cast(Button_type_e::Rounded), - static_cast(Button_type_e::Circle), - static_cast(Button_type_e::ArrowLeft), - static_cast(Button_type_e::ArrowUp), - static_cast(Button_type_e::ArrowRight), - static_cast(Button_type_e::ArrowDown), - }; - - const __FlashStringHelper *buttonLayoutOptions[] = { - toString(Button_layout_e::CenterAligned), - toString(Button_layout_e::LeftAligned), - toString(Button_layout_e::TopAligned), - toString(Button_layout_e::RightAligned), - toString(Button_layout_e::BottomAligned), - toString(Button_layout_e::LeftTopAligned), - toString(Button_layout_e::RightTopAligned), - toString(Button_layout_e::LeftBottomAligned), - toString(Button_layout_e::RightBottomAligned), - toString(Button_layout_e::NoCaption), - toString(Button_layout_e::Bitmap), - }; - - const int buttonLayoutValues[] = { - static_cast(Button_layout_e::CenterAligned), - static_cast(Button_layout_e::LeftAligned), - static_cast(Button_layout_e::TopAligned), - static_cast(Button_layout_e::RightAligned), - static_cast(Button_layout_e::BottomAligned), - static_cast(Button_layout_e::LeftTopAligned), - static_cast(Button_layout_e::RightTopAligned), - static_cast(Button_layout_e::LeftBottomAligned), - static_cast(Button_layout_e::RightBottomAligned), - static_cast(Button_layout_e::NoCaption), - static_cast(Button_layout_e::Bitmap), - }; - - const __FlashStringHelper *touchActionOptions[] = { - toString(P123_touch_action_e::Default), - toString(P123_touch_action_e::ActivateGroup), - toString(P123_touch_action_e::IncrementGroup), - toString(P123_touch_action_e::DecrementGroup), - }; - - const int touchActionValues[] = { - static_cast(P123_touch_action_e::Default), - static_cast(P123_touch_action_e::ActivateGroup), - static_cast(P123_touch_action_e::IncrementGroup), - static_cast(P123_touch_action_e::DecrementGroup), - }; - - # endif // ifdef P123_USE_EXTENDED_TOUCH - - uint8_t maxIdx = std::min(static_cast(TouchObjects.size() + P123_EXTRA_OBJECT_COUNT), P123_MAX_OBJECT_COUNT); - String parsed; - TouchObjects.resize(maxIdx, tP123_TouchObjects()); - - for (int objectNr = 0; objectNr < maxIdx; objectNr++) { - html_TR_TD(); - addHtml(F(" ")); - addHtmlInt(objectNr + 1); // Arrayindex to objectindex - - html_TD(); - - // Enable new entries - bool enabled = bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED) || TouchObjects[objectNr].objectName.isEmpty(); - addCheckBox(getPluginCustomArgName(objectNr + 0), - enabled, false - # ifdef P123_USE_TOOLTIPS - , F("Enabled") - # endif // ifdef P123_USE_TOOLTIPS - ); - html_TD(); // Name - addTextBox(getPluginCustomArgName(objectNr + 100), - TouchObjects[objectNr].objectName, - P123_MaxObjectNameLength - 1, - false, false, EMPTY_STRING, EMPTY_STRING); - html_TD(); // top-x - addNumericBox(getPluginCustomArgName(objectNr + 200), - TouchObjects[objectNr].top_left.x, 0, 65535 - # ifdef P123_USE_TOOLTIPS - , F("widenumber"), F("Top-left x") - # endif // ifdef P123_USE_TOOLTIPS - ); - html_TD(); // top-y - addNumericBox(getPluginCustomArgName(objectNr + 300), - TouchObjects[objectNr].top_left.y, 0, 65535 - # ifdef P123_USE_TOOLTIPS - , F("widenumber"), F("Top-left y") - # endif // ifdef P123_USE_TOOLTIPS - ); - html_TD(); // (on/off) button (type) - # ifdef P123_USE_EXTENDED_TOUCH - addSelector(getPluginCustomArgName(objectNr + 800), - static_cast(Button_type_e::Button_MAX), - buttonTypeOptions, - buttonTypeValues, - nullptr, - get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTONTYPE) & 0x0F, false, true, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("Buttontype") - # endif // ifdef P123_USE_TOOLTIPS - ); - html_TD(); // button alignment - addSelector(getPluginCustomArgName(objectNr + 900), - static_cast(Button_layout_e::Alignment_MAX), - buttonLayoutOptions, - buttonLayoutValues, - nullptr, - get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTONTYPE) & 0xF0, false, true, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("Button alignment") - # endif // ifdef P123_USE_TOOLTIPS - ); - # else // ifdef P123_USE_EXTENDED_TOUCH - addCheckBox(getPluginCustomArgName(objectNr + 600), - bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTON), false - # ifdef P123_USE_TOOLTIPS - , F("On/Off button") - # endif // ifdef P123_USE_TOOLTIPS - ); - # endif // ifdef P123_USE_EXTENDED_TOUCH - # ifdef P123_USE_EXTENDED_TOUCH - html_TD(); // ON color - parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorOn, _colorDepth, true); - addTextBox(getPluginCustomArgName(objectNr + 1000), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, - EMPTY_STRING, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("ON color") - # endif // ifdef P123_USE_TOOLTIPS - , F("adagfx65kcolors") - ); - html_TD(); // ON Caption - addTextBox(getPluginCustomArgName(objectNr + 1300), - TouchObjects[objectNr].captionOn, - P123_MaxObjectNameLength - 1, - false, - false, - EMPTY_STRING, - F("wide") - # ifdef P123_USE_TOOLTIPS - , F("ON caption") - # endif // ifdef P123_USE_TOOLTIPS - ); - html_TD(); // Border color - parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorBorder, _colorDepth, true); - addTextBox(getPluginCustomArgName(objectNr + 1700), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, - EMPTY_STRING, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("Border color") - # endif // ifdef P123_USE_TOOLTIPS - , F("adagfx65kcolors") - ); - html_TD(); // Disabled caption color - parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorDisabledCaption, _colorDepth, true); - addTextBox(getPluginCustomArgName(objectNr + 1900), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, - EMPTY_STRING, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("Disabled caption color") - # endif // ifdef P123_USE_TOOLTIPS - , F("adagfx65kcolors") - ); - html_TD(); // button action - addSelector(getPluginCustomArgName(objectNr + 2000), - static_cast(P123_touch_action_e::TouchAction_MAX), - touchActionOptions, - touchActionValues, - nullptr, - get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ACTIONGROUP) & 0xC0, false, true, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("Touch action") - # endif // ifdef P123_USE_TOOLTIPS - ); - # endif // ifdef P123_USE_EXTENDED_TOUCH - - html_TR_TD(); // Start new row - - html_TD(2); // Start with some blank columns - # ifdef P123_USE_EXTENDED_TOUCH - { - # ifdef P123_USE_TOOLTIPS - String buttonGroupToolTip = F("Button-group [0.."); - buttonGroupToolTip += P123_MAX_BUTTON_GROUPS; - buttonGroupToolTip += ']'; - # endif // ifdef P123_USE_TOOLTIPS - addNumericBox(getPluginCustomArgName(objectNr + 1600), - get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_GROUP), 0, P123_MAX_BUTTON_GROUPS - # ifdef P123_USE_TOOLTIPS - , F("widenumber"), buttonGroupToolTip - # endif // ifdef P123_USE_TOOLTIPS - ); - } - # endif // ifdef P123_USE_EXTENDED_TOUCH - html_TD(); // Width - addNumericBox(getPluginCustomArgName(objectNr + 400), - TouchObjects[objectNr].width_height.x, 0, 65535 - # ifdef P123_USE_TOOLTIPS - , F("widenumber"), F("Width") - # endif // ifdef P123_USE_TOOLTIPS - ); - html_TD(); // Height - addNumericBox(getPluginCustomArgName(objectNr + 500), - TouchObjects[objectNr].width_height.y, 0, 65535 - # ifdef P123_USE_TOOLTIPS - , F("widenumber"), F("Height") - # endif // ifdef P123_USE_TOOLTIPS - ); - html_TD(); // inverted - addCheckBox(getPluginCustomArgName(objectNr + 700), - bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_INVERTED), false - # ifdef P123_USE_TOOLTIPS - , F("Inverted") - # endif // ifdef P123_USE_TOOLTIPS - ); - # ifdef P123_USE_EXTENDED_TOUCH - html_TD(); // font scale - addNumericBox(getPluginCustomArgName(objectNr + 1200), - get4BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_FONTSCALE), 1, 10 - # ifdef P123_USE_TOOLTIPS - , F("widenumber"), F("Font scaling [1x..10x]") - # endif // ifdef P123_USE_TOOLTIPS - ); - html_TD(); // OFF color - parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorOff, _colorDepth, true); - addTextBox(getPluginCustomArgName(objectNr + 1100), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, - EMPTY_STRING, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("OFF color") - # endif // ifdef P123_USE_TOOLTIPS - , F("adagfx65kcolors") - ); - html_TD(); // OFF Caption - addTextBox(getPluginCustomArgName(objectNr + 1400), - TouchObjects[objectNr].captionOff, - P123_MaxObjectNameLength - 1, - false, - false, - EMPTY_STRING, - F("wide") - # ifdef P123_USE_TOOLTIPS - , F("OFF caption") - # endif // ifdef P123_USE_TOOLTIPS - ); - html_TD(); // Caption color - parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorCaption, _colorDepth, true); - addTextBox(getPluginCustomArgName(objectNr + 1500), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, - EMPTY_STRING, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("Caption color") - # endif // ifdef P123_USE_TOOLTIPS - , F("adagfx65kcolors") - ); - html_TD(); // Disabled color - parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorDisabled, _colorDepth, true); - addTextBox(getPluginCustomArgName(objectNr + 1800), parsed, P123_MAX_COLOR_INPUTLENGTH, false, false, - EMPTY_STRING, F("widenumber") - # ifdef P123_USE_TOOLTIPS - , F("Disabled color") - # endif // ifdef P123_USE_TOOLTIPS - , F("adagfx65kcolors") - ); - html_TD(); // Action Group - addNumericBox(getPluginCustomArgName(objectNr + 2100), - get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ACTIONGROUP) & 0x3F, 0, P123_MAX_BUTTON_GROUPS - # ifdef P123_USE_TOOLTIPS - , F("widenumber"), F("Action group") - # endif // ifdef P123_USE_TOOLTIPS - ); - # endif // ifdef P123_USE_EXTENDED_TOUCH - } - html_end_table(); - - addFormNumericBox(F("Debounce delay for On/Off buttons"), F("tch_debounce"), - P123_Settings.debounceMs, 0, 255); - addUnit(F("0-255 msec.")); - } - return false; + return touchHandler->plugin_webform_load(event); } /** * Save the settings from the web page to flash */ bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { - String config; - - # ifdef P123_USE_EXTENDED_TOUCH - String colorInput; - # endif // ifdef P123_USE_EXTENDED_TOUCH - config.reserve(80); - - uint32_t lSettings = 0u; - - bitWrite(lSettings, P123_FLAGS_SEND_XY, bitRead(getFormItemInt(F("tch_events")), P123_FLAGS_SEND_XY)); - bitWrite(lSettings, P123_FLAGS_SEND_Z, bitRead(getFormItemInt(F("tch_events")), P123_FLAGS_SEND_Z)); - bitWrite(lSettings, P123_FLAGS_SEND_OBJECTNAME, bitRead(getFormItemInt(F("tch_events")), P123_FLAGS_SEND_OBJECTNAME)); - bitWrite(lSettings, P123_FLAGS_ROTATION_FLIPPED, isFormItemChecked(F("tch_rotation_flipped"))); - bitWrite(lSettings, P123_FLAGS_DEDUPLICATE, isFormItemChecked(F("tch_deduplicate"))); - bitWrite(lSettings, P123_FLAGS_INIT_OBJECTEVENT, isFormItemChecked(F("tch_init_objectevent"))); - # ifdef P123_USE_EXTENDED_TOUCH - set8BitToUL(lSettings, P123_FLAGS_INITIAL_GROUP, getFormItemInt(F("tch_initial_group"))); // Button group - # endif // ifdef P123_USE_EXTENDED_TOUCH - - config += getFormItemInt(F("tch_use_calibration")); - config += P123_SETTINGS_SEPARATOR; - config += isFormItemChecked(F("tch_log_calibration")) ? 1 : 0; - config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("tch_cal_tl_x")); - config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("tch_cal_tl_y")); - config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("tch_cal_br_x")); - config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("tch_cal_br_y")); - config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("tch_debounce")); - config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(F("tch_treshold")); - config += P123_SETTINGS_SEPARATOR; - config += ull2String(lSettings); - config += P123_SETTINGS_SEPARATOR; - # ifdef P123_USE_EXTENDED_TOUCH - colorInput = webArg(getPluginCustomArgName(3000)); // Default Color ON - config += AdaGFXparseColor(colorInput, _colorDepth); - config += P123_SETTINGS_SEPARATOR; - colorInput = webArg(getPluginCustomArgName(3001)); // Default Color OFF - config += AdaGFXparseColor(colorInput, _colorDepth, false); - config += P123_SETTINGS_SEPARATOR; - colorInput = webArg(getPluginCustomArgName(3002)); // Default Color Border - config += AdaGFXparseColor(colorInput, _colorDepth, false); - config += P123_SETTINGS_SEPARATOR; - colorInput = webArg(getPluginCustomArgName(3003)); // Default Color caption - config += AdaGFXparseColor(colorInput, _colorDepth, false); - config += P123_SETTINGS_SEPARATOR; - colorInput = webArg(getPluginCustomArgName(3004)); // Default Disabled Color - config += AdaGFXparseColor(colorInput, _colorDepth); - config += P123_SETTINGS_SEPARATOR; - colorInput = webArg(getPluginCustomArgName(3005)); // Default Disabled Caption Color - config += AdaGFXparseColor(colorInput, _colorDepth, false); - config += P123_SETTINGS_SEPARATOR; - # endif // ifdef P123_USE_EXTENDED_TOUCH - - settingsArray[P123_CALIBRATION_START] = config; - - # ifdef PLUGIN_123_DEBUG - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("Save settings: "); - config.replace(P123_SETTINGS_SEPARATOR, ','); - log += config; - addLogMove(LOG_LEVEL_INFO, log); - } - # endif // ifdef PLUGIN_123_DEBUG - - String error; - - for (int objectNr = 0; objectNr < P123_MAX_OBJECT_COUNT; objectNr++) { - config.clear(); - config += webArg(getPluginCustomArgName(objectNr + 100)); // Name - - if (!config.isEmpty()) { // Empty name => skip entry - if (!ExtraTaskSettings.checkInvalidCharInNames(config.c_str())) { // Check for invalid characters in objectname - error += F("Invalid character in objectname #"); - error += objectNr; - error += F(". Do not use ',-+/*=^%!#[]{}()' or space.\n"); - } - config += P123_SETTINGS_SEPARATOR; - uint32_t flags = 0u; - bitWrite(flags, P123_OBJECT_FLAG_ENABLED, isFormItemChecked(getPluginCustomArgName(objectNr + 0))); // Enabled - bitWrite(flags, P123_OBJECT_FLAG_INVERTED, isFormItemChecked(getPluginCustomArgName(objectNr + 700))); // Inverted - # ifdef P123_USE_EXTENDED_TOUCH - uint8_t buttonType = getFormItemInt(getPluginCustomArgName(objectNr + 800)); - uint8_t buttonLayout = getFormItemInt(getPluginCustomArgName(objectNr + 900)); - set8BitToUL(flags, P123_OBJECT_FLAG_BUTTONTYPE, buttonType | buttonLayout); // Buttontype - bitWrite(flags, P123_OBJECT_FLAG_BUTTON, (static_cast(buttonType & 0x07) != Button_type_e::None)); // On/Off button - uint8_t buttonAction = getFormItemInt(getPluginCustomArgName(objectNr + 2000)); - uint8_t buttonSelectGroup = getFormItemInt(getPluginCustomArgName(objectNr + 2100)); - set8BitToUL(flags, P123_OBJECT_FLAG_ACTIONGROUP, buttonAction | buttonSelectGroup); // ButtonAction - uint8_t fontScale = getFormItemInt(getPluginCustomArgName(objectNr + 1200)); - set4BitToUL(flags, P123_OBJECT_FLAG_FONTSCALE, fontScale); // Font scaling - uint8_t buttonGroup = getFormItemInt(getPluginCustomArgName(objectNr + 1600)); - set8BitToUL(flags, P123_OBJECT_FLAG_GROUP, buttonGroup); // Button group - # else // ifdef P123_USE_EXTENDED_TOUCH - bitWrite(flags, P123_OBJECT_FLAG_BUTTON, isFormItemChecked(getPluginCustomArgName(objectNr + 600))); // On/Off button - # endif // ifdef P123_USE_EXTENDED_TOUCH - - config += ull2String(flags); // Flags - config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(getPluginCustomArgName(objectNr + 200)); // Top x - config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(getPluginCustomArgName(objectNr + 300)); // Top y - config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(getPluginCustomArgName(objectNr + 400)); // Bottom x - config += P123_SETTINGS_SEPARATOR; - config += getFormItemInt(getPluginCustomArgName(objectNr + 500)); // Bottom y - config += P123_SETTINGS_SEPARATOR; - - # ifdef P123_USE_EXTENDED_TOUCH - colorInput = webArg(getPluginCustomArgName(objectNr + 1000)); // Color ON - config += AdaGFXparseColor(colorInput, _colorDepth, true); - config += P123_SETTINGS_SEPARATOR; - colorInput = webArg(getPluginCustomArgName(objectNr + 1100)); // Color OFF - config += AdaGFXparseColor(colorInput, _colorDepth, true); - config += P123_SETTINGS_SEPARATOR; - colorInput = webArg(getPluginCustomArgName(objectNr + 1500)); // Color caption - config += AdaGFXparseColor(colorInput, _colorDepth, true); - config += P123_SETTINGS_SEPARATOR; // Caption ON - config += wrapWithQuotesIfContainsParameterSeparatorChar(webArg(getPluginCustomArgName(objectNr + 1300))); - config += P123_SETTINGS_SEPARATOR; // Caption OFF - config += wrapWithQuotesIfContainsParameterSeparatorChar(webArg(getPluginCustomArgName(objectNr + 1400))); - config += P123_SETTINGS_SEPARATOR; - colorInput = webArg(getPluginCustomArgName(objectNr + 1700)); // Color Border - config += AdaGFXparseColor(colorInput, _colorDepth, true); - config += P123_SETTINGS_SEPARATOR; - colorInput = webArg(getPluginCustomArgName(objectNr + 1800)); // Disabled Color - config += AdaGFXparseColor(colorInput, _colorDepth, true); - config += P123_SETTINGS_SEPARATOR; - colorInput = webArg(getPluginCustomArgName(objectNr + 1900)); // Disabled Caption Color - config += AdaGFXparseColor(colorInput, _colorDepth, true); - config += P123_SETTINGS_SEPARATOR; - # endif // ifdef P123_USE_EXTENDED_TOUCH - } - config.trim(); - - while (!config.isEmpty() && config[config.length() - 1] == P123_SETTINGS_SEPARATOR) { - config.remove(config.length() - 1); - } - - settingsArray[objectNr + P123_OBJECT_INDEX_START] = config; + return touchHandler->plugin_webform_save(event); +} +/** + * Parse and execute the plugin commands + */ +bool P123_data_struct::plugin_write(struct EventStruct *event, + const String & string) { + bool success = false; + String command; + String subcommand; + String arguments; + + arguments.reserve(24); + command = parseString(string, 1); + subcommand = parseString(string, 2); + + if (command.equals(F("touch"))) { # ifdef PLUGIN_123_DEBUG - if (loglevelActiveFor(LOG_LEVEL_INFO) && - !config.isEmpty()) { - String log = F("Save object #"); - log += objectNr; - log += F(" settings: "); - config.replace(P123_SETTINGS_SEPARATOR, ','); - log += config; - addLogMove(LOG_LEVEL_INFO, log); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("P123 WRITE arguments Par1:"); + log += event->Par1; + log += F(", 2: "); + log += event->Par2; + log += F(", 3: "); + log += event->Par3; + log += F(", 4: "); + log += event->Par4; + addLog(LOG_LEVEL_INFO, log); } # endif // ifdef PLUGIN_123_DEBUG - } - - if (error.length() > 0) { - addHtmlError(error); - } - - error = SaveCustomTaskSettings(event->TaskIndex, settingsArray, P123_ARRAY_SIZE, 0); - if (!error.isEmpty()) { - addHtmlError(error); - return false; + if (subcommand.equals(F("rot"))) { // touch,rot,<0..3> : Set rotation to 0, 90, 180, 270 degrees + setRotation(static_cast(event->Par2 % 4)); + success = true; + } else if (subcommand.equals(F("flip"))) { // touch,flip,<0|1> : Flip rotation by 0 or 180 degrees + setRotationFlipped(event->Par2 > 0); + success = true; + } else if (subcommand.equals(F("enable"))) { // touch,enable,[,...] : Enable disabled objectname(s) + uint8_t arg = 3; + arguments = parseString(string, arg); + + while (!arguments.isEmpty()) { + success |= setTouchObjectState(event, arguments, true); + arg++; + arguments = parseString(string, arg); + } + } else if (subcommand.equals(F("disable"))) { // touch,disable,[,...] : Disable enabled objectname(s) + uint8_t arg = 3; + arguments = parseString(string, arg); + + while (!arguments.isEmpty()) { + success |= setTouchObjectState(event, arguments, false); + arg++; + arguments = parseString(string, arg); + } + } else if (subcommand.equals(F("on"))) { // touch,on, : Switch a TouchButton on + arguments = parseString(string, 3); + success = setTouchButtonOnOff(event, arguments, true); + } else if (subcommand.equals(F("off"))) { // touch,off, : Switch a TouchButton off + arguments = parseString(string, 3); + success = setTouchButtonOnOff(event, arguments, false); + } else if (subcommand.equals(F("setgrp"))) { // touch,setgrp, : Activate button group + success = setButtonGroup(event, event->Par2); + } else if (subcommand.equals(F("incgrp"))) { // touch,incgrp : increment group and Activate + success = incrementButtonGroup(event); + } else if (subcommand.equals(F("decgrp"))) { // touch,decgrp : Decrement group and Activate + success = decrementButtonGroup(event); + } else if (subcommand.equals(F("incpage"))) { // touch,incpage : increment page and Activate + success = incrementButtonPage(event); + } else if (subcommand.equals(F("decpage"))) { // touch,decpage : Decrement page and Activate + success = decrementButtonPage(event); + } else if (subcommand.equals(F("updatebutton"))) { // touch,updatebutton,[,[,]] : Update a button + arguments = parseString(string, 3); + + // Check for a valid button name or number, returns a 0-based index + int index = getTouchObjectIndex(event, arguments, true); + + if (index > -1) { + bool hasPar3 = !parseString(string, 4).isEmpty(); + bool hasPar4 = !parseString(string, 5).isEmpty(); + + if (hasPar4) { + success = displayButton(event, index, event->Par3, event->Par4); + } else if (hasPar3) { + success = displayButton(event, index, event->Par3); + } else { + success = displayButton(event, index); // Use default argument values + } + } + } } - return true; + return success; } /** - * Every 10th second we check if the screen is touched + * Every 1/50th second we check if the screen is touched */ bool P123_data_struct::plugin_fifty_per_second(struct EventStruct *event) { bool success = false; @@ -905,337 +220,17 @@ bool P123_data_struct::plugin_fifty_per_second(struct EventStruct *event) { ry = y; scaleRawToCalibrated(x, y); // Map to screen coordinates if so configured - // Avoid event-storms by deduplicating coordinates - if (!_deduplicate || - (_deduplicate && ((P123_VALUE_X != x) || (P123_VALUE_Y != y) || (P123_VALUE_Z != z)))) { - success = true; - P123_VALUE_X = x; - P123_VALUE_Y = y; - P123_VALUE_Z = z; - } - - if (success && - P123_Settings.logEnabled && - loglevelActiveFor(LOG_LEVEL_INFO)) { // REQUIRED for calibration and setting up objects, so do not make this optional! - String log; - log.reserve(72); - log = F("Touch calibration rx= "); // Space before the logged values added for readability - log += rx; - log += F(", ry= "); - log += ry; - log += F("; z= "); // Always log the z value even if not used. - log += z; - log += F(", x= "); - log += x; - log += F(", y= "); - log += y; - log += F("; ox= "); - log += ox; - log += F(", oy= "); - log += oy; - addLogMove(LOG_LEVEL_INFO, log); - } - - // No events to handle if rules not enabled - if (Settings.UseRules) { - if (success && bitRead(P123_Settings.flags, P123_FLAGS_SEND_XY)) { // Send events for each touch - const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex); - - // Do NOT send a Z event for each touch? - if (!bitRead(P123_Settings.flags, P123_FLAGS_SEND_Z) && validDeviceIndex(DeviceIndex)) { - Device[DeviceIndex].VType = Sensor_VType::SENSOR_TYPE_DUAL; - Device[DeviceIndex].ValueCount = 2; - } - sendData(event); // Send X/Y(/Z) event - - if (!bitRead(P123_Settings.flags, P123_FLAGS_SEND_Z) && validDeviceIndex(DeviceIndex)) { // Reset device configuration - Device[DeviceIndex].VType = Sensor_VType::SENSOR_TYPE_TRIPLE; - Device[DeviceIndex].ValueCount = 3; - } - } - - if (bitRead(P123_Settings.flags, P123_FLAGS_SEND_OBJECTNAME)) { // Send events for objectname if within reach - String selectedObjectName; - int8_t selectedObjectIndex = -1; - - if (isValidAndTouchedTouchObject(x, y, selectedObjectName, selectedObjectIndex)) { - if ((selectedObjectIndex > -1) && bitRead(TouchObjects[selectedObjectIndex].flags, P123_OBJECT_FLAG_BUTTON)) { - // Not touched yet or too long ago - if ((TouchObjects[selectedObjectIndex].TouchTimers == 0) || - (TouchObjects[selectedObjectIndex].TouchTimers < (millis() - (1.5 * P123_Settings.debounceMs)))) { - // From now wait the debounce time - TouchObjects[selectedObjectIndex].TouchTimers = millis() + P123_Settings.debounceMs; - } else { - // Debouncing time elapsed? - if (TouchObjects[selectedObjectIndex].TouchTimers <= millis()) { - TouchObjects[selectedObjectIndex].TouchStates = !TouchObjects[selectedObjectIndex].TouchStates; - TouchObjects[selectedObjectIndex].TouchTimers = 0; - generateObjectEvent(event, selectedObjectIndex, TouchObjects[selectedObjectIndex].TouchStates ? 1 : 0); - } - } - } else { - // Matching object is found, send # event with x, y and z as %eventvalue1/2/3% - String eventCommand; - eventCommand.reserve(48); - eventCommand = getTaskDeviceName(event->TaskIndex); - eventCommand += '#'; - eventCommand += selectedObjectName; - eventCommand += '='; // Add arguments - eventCommand += x; - eventCommand += ','; - eventCommand += y; - eventCommand += ','; - eventCommand += z; - eventQueue.addMove(std::move(eventCommand)); - } - } - } - } + success = touchHandler->plugin_fifty_per_second(event, x, y, ox, oy, rx, ry, z); } } return success; } -/** - * generate an event for a touch object - * When a display is configured add x,y coordinate, width,height of the object, objectIndex, and TaskIndex of display - **************************************************************************/ -void P123_data_struct::generateObjectEvent(const EventStruct *event, - const int8_t objectIndex, - const int8_t onOffState, - const bool groupSwitch, - const int8_t factor) { - if ((objectIndex < 0) || // Range check - (objectIndex >= static_cast(TouchObjects.size()))) { - return; - } - String eventCommand; - - eventCommand.reserve(48); - eventCommand = getTaskDeviceName(event->TaskIndex); - eventCommand += '#'; - eventCommand += TouchObjects[objectIndex].objectName; - eventCommand += '='; // Add arguments - - if (onOffState < 0) { // Negative value: pass on unaltered (1) - eventCommand += onOffState; // (%eventvalue#%) - } else { // Check for inverted output (1) - if (bitRead(TouchObjects[objectIndex].flags, P123_OBJECT_FLAG_INVERTED)) { - eventCommand += onOffState == 1 ? '0' : '1'; // Act like an inverted button, 0 = On, 1 = Off - } else { - eventCommand += onOffState == 1 ? '1' : '0'; // Act like a button, 1 = On, 0 = Off - } - } - - if (P123_CONFIG_DISPLAY_TASK != event->TaskIndex) { // Add arguments for display - eventCommand += ','; - eventCommand += TouchObjects[objectIndex].top_left.x; // (2) - eventCommand += ','; - eventCommand += TouchObjects[objectIndex].top_left.y; // (3) - eventCommand += ','; - eventCommand += TouchObjects[objectIndex].width_height.x; // (4) - eventCommand += ','; - eventCommand += TouchObjects[objectIndex].width_height.y; // (5) - eventCommand += ','; - eventCommand += objectIndex + 1; // Adjust to displayed index (6) - eventCommand += ','; // (7) - eventCommand += get8BitFromUL(TouchObjects[objectIndex].flags, P123_OBJECT_FLAG_BUTTONTYPE) * factor; - # ifdef P123_USE_EXTENDED_TOUCH - eventCommand += ','; // (8) - eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorOn == 0 - ? P123_Settings.colorOn - : TouchObjects[objectIndex].colorOn, - _colorDepth); - eventCommand += ','; // (9) - eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorOff == 0 - ? P123_Settings.colorOff - : TouchObjects[objectIndex].colorOff, - _colorDepth); - eventCommand += ','; // (10) - eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorCaption == 0 - ? P123_Settings.colorCaption - : TouchObjects[objectIndex].colorCaption, - _colorDepth); - eventCommand += ','; // (11) - eventCommand += get4BitFromUL(TouchObjects[objectIndex].flags, P123_OBJECT_FLAG_FONTSCALE); - eventCommand += ','; // (12) - eventCommand += wrapWithQuotesIfContainsParameterSeparatorChar(TouchObjects[objectIndex].captionOn.isEmpty() ? - TouchObjects[objectIndex].objectName : - TouchObjects[objectIndex].captionOn); - eventCommand += ','; // (13) - eventCommand += wrapWithQuotesIfContainsParameterSeparatorChar(TouchObjects[objectIndex].captionOff.isEmpty() ? - TouchObjects[objectIndex].objectName : - TouchObjects[objectIndex].captionOff); - eventCommand += ','; // (14) - eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorBorder == 0 - ? P123_Settings.colorBorder - : TouchObjects[objectIndex].colorBorder, - _colorDepth); - eventCommand += ','; // (15) - eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorDisabled == 0 - ? P123_Settings.colorDisabled - : TouchObjects[objectIndex].colorDisabled, - _colorDepth); - eventCommand += ','; // (16) - eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorDisabledCaption == 0 - ? P123_Settings.colorDisabledCaption - : TouchObjects[objectIndex].colorDisabledCaption, - _colorDepth); - # endif // ifdef P123_USE_EXTENDED_TOUCH - eventCommand += ','; - eventCommand += P123_CONFIG_DISPLAY_TASK + 1; // What TaskIndex? (17) or (8) - eventCommand += ','; // Group (18) or (9) - eventCommand += get8BitFromUL(TouchObjects[objectIndex].flags, P123_OBJECT_FLAG_GROUP); - eventCommand += ','; // Select Group (19) or (10) - uint8_t action = get8BitFromUL(TouchObjects[objectIndex].flags, P123_OBJECT_FLAG_ACTIONGROUP); - - if (!groupSwitch && (static_cast(action & 0xC0) != P123_touch_action_e::Default)) { - switch (static_cast(action & 0xC0)) { - case P123_touch_action_e::ActivateGroup: - eventCommand += action & 0x3F; - break; - case P123_touch_action_e::IncrementGroup: - eventCommand += -2; - break; - case P123_touch_action_e::DecrementGroup: - eventCommand += -3; - break; - case P123_touch_action_e::Default: - case P123_touch_action_e::TouchAction_MAX: - eventCommand += -1; // Ignore - break; - } - } else { - eventCommand += -1; // No group to activate - } - } - - eventQueue.addMove(std::move(eventCommand)); - - delay(0); -} - /** * Load the touch objects from the settings, and initialize then properly where needed. */ -void P123_data_struct::loadTouchObjects(const EventStruct *event) { - # ifdef PLUGIN_123_DEBUG - addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG loadTouchObjects")); - # endif // PLUGIN_123_DEBUG - LoadCustomTaskSettings(event->TaskIndex, settingsArray, P123_ARRAY_SIZE, 0); - - lastObjectIndex = P123_OBJECT_INDEX_START - 1; // START must be > 0!!! - - objectCount = 0; - _minButtonGroup = 0; - _maxButtonGroup = 0; - - for (uint8_t i = P123_OBJECT_INDEX_END; i >= P123_OBJECT_INDEX_START; i--) { - if (!settingsArray[i].isEmpty() && (lastObjectIndex < P123_OBJECT_INDEX_START)) { - lastObjectIndex = i; - objectCount++; // Count actual number of objects - } - } - - // Get calibration and common settings - P123_Settings.calibrationEnabled = parseStringToInt(settingsArray[P123_CALIBRATION_START], - P123_CALIBRATION_ENABLED, P123_SETTINGS_SEPARATOR) == 1; - P123_Settings.logEnabled = parseStringToInt(settingsArray[P123_CALIBRATION_START], - P123_CALIBRATION_LOG_ENABLED, P123_SETTINGS_SEPARATOR) == 1; - int lSettings = 0; - - bitWrite(lSettings, P123_FLAGS_SEND_XY, P123_TS_SEND_XY); - bitWrite(lSettings, P123_FLAGS_SEND_Z, P123_TS_SEND_Z); - bitWrite(lSettings, P123_FLAGS_SEND_OBJECTNAME, P123_TS_SEND_OBJECTNAME); - P123_Settings.flags = parseStringToInt(settingsArray[P123_CALIBRATION_START], - P123_COMMON_FLAGS, P123_SETTINGS_SEPARATOR, lSettings); - P123_Settings.top_left.x = parseStringToInt(settingsArray[P123_CALIBRATION_START], P123_CALIBRATION_TOP_X, P123_SETTINGS_SEPARATOR); - P123_Settings.top_left.y = parseStringToInt(settingsArray[P123_CALIBRATION_START], P123_CALIBRATION_TOP_Y, P123_SETTINGS_SEPARATOR); - P123_Settings.bottom_right.x = parseStringToInt(settingsArray[P123_CALIBRATION_START], P123_CALIBRATION_BOTTOM_X, P123_SETTINGS_SEPARATOR); - P123_Settings.bottom_right.y = parseStringToInt(settingsArray[P123_CALIBRATION_START], P123_CALIBRATION_BOTTOM_Y, P123_SETTINGS_SEPARATOR); - P123_Settings.debounceMs = parseStringToInt(settingsArray[P123_CALIBRATION_START], P123_COMMON_DEBOUNCE_MS, P123_SETTINGS_SEPARATOR, - P123_DEBOUNCE_MILLIS); - P123_Settings.treshold = parseStringToInt(settingsArray[P123_CALIBRATION_START], P123_COMMON_TOUCH_TRESHOLD, P123_SETTINGS_SEPARATOR, - P123_TS_TRESHOLD); - # ifdef P123_USE_EXTENDED_TOUCH - P123_Settings.colorOn = parseStringToInt(settingsArray[P123_CALIBRATION_START], - P123_COMMON_DEF_COLOR_ON, P123_SETTINGS_SEPARATOR); - P123_Settings.colorOff = parseStringToInt(settingsArray[P123_CALIBRATION_START], - P123_COMMON_DEF_COLOR_OFF, P123_SETTINGS_SEPARATOR); - P123_Settings.colorBorder = parseStringToInt(settingsArray[P123_CALIBRATION_START], - P123_COMMON_DEF_COLOR_BORDER, P123_SETTINGS_SEPARATOR); - P123_Settings.colorCaption = parseStringToInt(settingsArray[P123_CALIBRATION_START], - P123_COMMON_DEF_COLOR_CAPTION, P123_SETTINGS_SEPARATOR); - P123_Settings.colorDisabled = parseStringToInt(settingsArray[P123_CALIBRATION_START], - P123_COMMON_DEF_COLOR_DISABLED, P123_SETTINGS_SEPARATOR); - P123_Settings.colorDisabledCaption = parseStringToInt(settingsArray[P123_CALIBRATION_START], - P123_COMMON_DEF_COLOR_DISABCAPT, P123_SETTINGS_SEPARATOR); - - if ((P123_Settings.colorOn == 0u) && - (P123_Settings.colorOff == 0u) && - (P123_Settings.colorCaption == 0u) && - (P123_Settings.colorBorder == 0u) && - (P123_Settings.colorDisabled == 0u) && - (P123_Settings.colorDisabledCaption == 0u)) { - P123_Settings.colorOn = ADAGFX_GREEN; - P123_Settings.colorOff = ADAGFX_RED; - P123_Settings.colorCaption = ADAGFX_WHITE; - P123_Settings.colorBorder = ADAGFX_WHITE; - P123_Settings.colorDisabled = P123_DEFAULT_COLOR_DISABLED; - P123_Settings.colorDisabledCaption = P123_DEFAULT_COLOR_DISABLED_CAPTION; - } - # endif // ifdef P123_USE_EXTENDED_TOUCH - - settingsArray[P123_CALIBRATION_START].clear(); // Free a little memory - - // Buffer some settings, mostly for readability, but also to be able to set from write command - _flipped = bitRead(P123_Settings.flags, P123_FLAGS_ROTATION_FLIPPED); - _deduplicate = bitRead(P123_Settings.flags, P123_FLAGS_DEDUPLICATE); - - TouchObjects.clear(); - - if (objectCount > 0) { - TouchObjects.reserve(objectCount); - uint8_t t = 0u; - - for (uint8_t i = P123_OBJECT_INDEX_START; i <= lastObjectIndex; i++) { - if (!settingsArray[i].isEmpty()) { - TouchObjects.push_back(tP123_TouchObjects()); - TouchObjects[t].flags = parseStringToInt(settingsArray[i], P123_OBJECT_FLAGS, P123_SETTINGS_SEPARATOR); - TouchObjects[t].objectName = parseStringKeepCase(settingsArray[i], P123_OBJECT_NAME, P123_SETTINGS_SEPARATOR); - TouchObjects[t].top_left.x = parseStringToInt(settingsArray[i], P123_OBJECT_COORD_TOP_X, P123_SETTINGS_SEPARATOR); - TouchObjects[t].top_left.y = parseStringToInt(settingsArray[i], P123_OBJECT_COORD_TOP_Y, P123_SETTINGS_SEPARATOR); - TouchObjects[t].width_height.x = parseStringToInt(settingsArray[i], P123_OBJECT_COORD_WIDTH, P123_SETTINGS_SEPARATOR); - TouchObjects[t].width_height.y = parseStringToInt(settingsArray[i], P123_OBJECT_COORD_HEIGHT, P123_SETTINGS_SEPARATOR); - # ifdef P123_USE_EXTENDED_TOUCH - TouchObjects[t].colorOn = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_ON, P123_SETTINGS_SEPARATOR); - TouchObjects[t].colorOff = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_OFF, P123_SETTINGS_SEPARATOR); - TouchObjects[t].colorCaption = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_CAPTION, P123_SETTINGS_SEPARATOR); - TouchObjects[t].captionOn = parseStringKeepCase(settingsArray[i], P123_OBJECT_CAPTION_ON, P123_SETTINGS_SEPARATOR); - TouchObjects[t].captionOff = parseStringKeepCase(settingsArray[i], P123_OBJECT_CAPTION_OFF, P123_SETTINGS_SEPARATOR); - TouchObjects[t].colorBorder = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_BORDER, P123_SETTINGS_SEPARATOR); - TouchObjects[t].colorDisabled = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_DISABLED, P123_SETTINGS_SEPARATOR); - TouchObjects[t].colorDisabledCaption = parseStringToInt(settingsArray[i], P123_OBJECT_COLOR_DISABCAPT, P123_SETTINGS_SEPARATOR); - - if (get8BitFromUL(TouchObjects[t].flags, P123_OBJECT_FLAG_GROUP) > _maxButtonGroup) { - _maxButtonGroup = get8BitFromUL(TouchObjects[t].flags, P123_OBJECT_FLAG_GROUP); - } - # endif // ifdef P123_USE_EXTENDED_TOUCH - - TouchObjects[t].SurfaceAreas = 0; // Reset runtime stuff - TouchObjects[t].TouchTimers = 0; - TouchObjects[t].TouchStates = false; - - t++; - - settingsArray[i].clear(); // Free a little memory - } - } - } - - if (_maxButtonGroup > 0) { - _minButtonGroup = 1; - } +void P123_data_struct::loadTouchObjects(struct EventStruct *event) { + touchHandler->loadTouchObjects(event); } /** @@ -1251,7 +246,11 @@ bool P123_data_struct::touched() { /** * Read the raw data if the touchscreen is initialized. */ -void P123_data_struct::readData(int16_t& x, int16_t& y, int16_t& z, int16_t& ox, int16_t& oy) { +void P123_data_struct::readData(int16_t& x, + int16_t& y, + int16_t& z, + int16_t& ox, + int16_t& oy) { if (isInitialized()) { FT_Point p = touchscreen->getPoint(); @@ -1260,9 +259,9 @@ void P123_data_struct::readData(int16_t& x, int16_t& y, int16_t& z, int16_t& ox, // Rotate, as the driver doesn't provide that, use native touch-panel resolution switch (_rotation) { - case TOUCHOBJECTS_HELPER_ROTATION_90: + case P123_ROTATION_90: - if (_flipped) { + if (touchHandler->_flipped) { p.x = map(_y, 0, P123_TOUCH_Y_NATIVE, P123_TOUCH_Y_NATIVE, 0); p.y = _x; } else { @@ -1270,16 +269,16 @@ void P123_data_struct::readData(int16_t& x, int16_t& y, int16_t& z, int16_t& ox, p.y = map(_x, 0, P123_TOUCH_X_NATIVE, P123_TOUCH_X_NATIVE, 0); } break; - case TOUCHOBJECTS_HELPER_ROTATION_180: + case P123_ROTATION_180: - if (!_flipped) { // Change only when not flipped + if (!touchHandler->_flipped) { // Change only when not flipped p.x = map(_x, 0, P123_TOUCH_X_NATIVE, P123_TOUCH_X_NATIVE, 0); p.y = map(_y, 0, P123_TOUCH_Y_NATIVE, P123_TOUCH_Y_NATIVE, 0); } break; - case TOUCHOBJECTS_HELPER_ROTATION_270: + case P123_ROTATION_270: - if (_flipped) { + if (touchHandler->_flipped) { p.x = _y; p.y = map(_x, 0, P123_TOUCH_X_NATIVE, P123_TOUCH_X_NATIVE, 0); } else { @@ -1287,9 +286,10 @@ void P123_data_struct::readData(int16_t& x, int16_t& y, int16_t& z, int16_t& ox, p.y = _x; } break; + case P123_ROTATION_0: default: - if (_flipped) { + if (touchHandler->_flipped) { p.x = map(p.x, 0, P123_TOUCH_X_NATIVE, P123_TOUCH_X_NATIVE, 0); p.y = map(p.y, 0, P123_TOUCH_Y_NATIVE, P123_TOUCH_Y_NATIVE, 0); } @@ -1323,7 +323,7 @@ void P123_data_struct::setRotation(uint8_t n) { * Set rotationFlipped */ void P123_data_struct::setRotationFlipped(bool flipped) { - _flipped = flipped; + touchHandler->_flipped = flipped; # ifdef PLUGIN_123_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { @@ -1334,17 +334,6 @@ void P123_data_struct::setRotationFlipped(bool flipped) { # endif // PLUGIN_123_DEBUG } -/** - * Determine if calibration is enabled and usable. - */ -bool P123_data_struct::isCalibrationActive() { - return _useCalibration - && (P123_Settings.top_left.x != 0 || - P123_Settings.top_left.y != 0 || - P123_Settings.bottom_right.x != 0 || - P123_Settings.bottom_right.y != 0); // Enabled and any value != 0 => Active -} - /** * Check within the list of defined objects if we touched one of them. * The smallest matching surface is selected if multiple objects overlap. @@ -1354,217 +343,117 @@ bool P123_data_struct::isValidAndTouchedTouchObject(int16_t x, int16_t y, String& selectedObjectName, int8_t& selectedObjectIndex) { - uint32_t lastObjectArea = 0u; - bool selected = false; - - for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { - uint8_t group = get8BitFromUL(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_GROUP); - - if (!TouchObjects[objectNr].objectName.isEmpty() - && bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED) - && (TouchObjects[objectNr].width_height.x != 0) - && (TouchObjects[objectNr].width_height.y != 0) // Not initial could be valid - && ((group == 0) || (group == _buttonGroup))) { // Group 0 is always active - if (TouchObjects[objectNr].SurfaceAreas == 0) { // Need to calculate the surface area - TouchObjects[objectNr].SurfaceAreas = TouchObjects[objectNr].width_height.x * TouchObjects[objectNr].width_height.y; - } - - if ((TouchObjects[objectNr].top_left.x <= x) - && (TouchObjects[objectNr].top_left.y <= y) - && ((TouchObjects[objectNr].width_height.x + TouchObjects[objectNr].top_left.x) >= x) - && ((TouchObjects[objectNr].width_height.y + TouchObjects[objectNr].top_left.y) >= y) - && ((lastObjectArea == 0) - || (TouchObjects[objectNr].SurfaceAreas < lastObjectArea))) { // Select smallest area that fits the coordinates - selectedObjectName = TouchObjects[objectNr].objectName; - selectedObjectIndex = objectNr; - lastObjectArea = TouchObjects[objectNr].SurfaceAreas; - selected = true; - } - # ifdef PLUGIN_123_DEBUG + return touchHandler->isValidAndTouchedTouchObject(x, y, selectedObjectName, selectedObjectIndex); +} - if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("P123 DEBUG Touched: obj: "); - log += TouchObjects[objectNr].objectName; - log += ','; - log += TouchObjects[objectNr].top_left.x; - log += ','; - log += TouchObjects[objectNr].top_left.y; - log += ','; - log += TouchObjects[objectNr].width_height.x; - log += ','; - log += TouchObjects[objectNr].width_height.y; - log += F(" surface:"); - log += TouchObjects[objectNr].SurfaceAreas; - log += F(" x,y:"); - log += x; - log += ','; - log += y; - log += F(" sel:"); - log += selectedObjectName; - log += '/'; - log += selectedObjectIndex; - log += '/'; - log += selected ? 'T' : 'f'; - addLogMove(LOG_LEVEL_DEBUG, log); - } - # endif // PLUGIN_123_DEBUG - } - } - return selected; +/** + * Get the index of a touch object by name or number + */ +int8_t P123_data_struct::getTouchObjectIndex(struct EventStruct *event, + const String & touchObject, + bool isButton) { + return touchHandler->getTouchObjectIndex(event, touchObject, isButton); } /** * Set the enabled/disabled state of an object. */ -bool P123_data_struct::setTouchObjectState(struct EventStruct *event, const String& touchObject, bool state) { - if (touchObject.isEmpty()) { return false; } - bool success = false; - - for (size_t objectNr = 0; objectNr < TouchObjects.size(); objectNr++) { - if (!TouchObjects[objectNr].objectName.isEmpty() - && touchObject.equalsIgnoreCase(TouchObjects[objectNr].objectName)) { - bool currentState = bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED); - - if (state != currentState) { - bitWrite(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED, state); - success = true; - - if (bitRead(P123_Settings.flags, P123_FLAGS_SEND_OBJECTNAME) && - bitRead(P123_Settings.flags, P123_FLAGS_INIT_OBJECTEVENT)) { - generateObjectEvent(event, objectNr, state ? (TouchObjects[objectNr].TouchStates ? 1 : -1) : -2); - } - } - # ifdef PLUGIN_123_DEBUG - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("P123 setTouchObjectState: obj: "); - log += touchObject; - - if (success) { - log += F(", new state: "); - log += (state ? F("en") : F("dis")); - log += F("abled."); - } else { - log += F("failed!"); - } - addLogMove(LOG_LEVEL_INFO, log); - } - # endif // PLUGIN_123_DEBUG - } - } - return success; +bool P123_data_struct::setTouchObjectState(struct EventStruct *event, + const String & touchObject, + bool state) { + return touchHandler->setTouchObjectState(event, touchObject, state); } /** * Set the on/off state of a touch-button object. */ -bool P123_data_struct::setTouchButtonOnOff(struct EventStruct *event, const String& touchObject, bool state) { - if (touchObject.isEmpty()) { return false; } - bool success = false; - - for (size_t objectNr = 0; objectNr < TouchObjects.size(); objectNr++) { - if (!TouchObjects[objectNr].objectName.isEmpty() - && touchObject.equalsIgnoreCase(TouchObjects[objectNr].objectName) - && bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_ENABLED) - && bitRead(TouchObjects[objectNr].flags, P123_OBJECT_FLAG_BUTTON)) { - bool currentState = TouchObjects[objectNr].TouchStates; - - success = true; // Always success if matched button - - if (state != currentState) { - TouchObjects[objectNr].TouchStates = state; - - if (bitRead(P123_Settings.flags, P123_FLAGS_SEND_OBJECTNAME) && - bitRead(P123_Settings.flags, P123_FLAGS_INIT_OBJECTEVENT)) { - generateObjectEvent(event, objectNr, state ? 1 : 0); - } - } - # ifdef PLUGIN_123_DEBUG - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("P123 setTouchButtonOnOff: obj: "); - log += touchObject; - log += F(", (new) state: "); - log += (state ? F("on") : F("off")); - addLogMove(LOG_LEVEL_INFO, log); - } - # endif // PLUGIN_123_DEBUG - } - } - return success; +bool P123_data_struct::setTouchButtonOnOff(struct EventStruct *event, + const String & touchObject, + bool state) { + return touchHandler->setTouchButtonOnOff(event, touchObject, state); } /** * Scale the provided raw coordinates to screen-resolution coordinates if calibration is enabled/configured */ -void P123_data_struct::scaleRawToCalibrated(int16_t& x, int16_t& y) { - if (isCalibrationActive()) { - int16_t lx = x - P123_Settings.top_left.x; +void P123_data_struct::scaleRawToCalibrated(int16_t& x, + int16_t& y) { + if (touchHandler->isCalibrationActive()) { + int16_t lx = x - touchHandler->Touch_Settings.top_left.x; if (lx <= 0) { x = 0; } else { - if (lx > P123_Settings.bottom_right.x) { - lx = P123_Settings.bottom_right.x; + if (lx > touchHandler->Touch_Settings.bottom_right.x) { + lx = touchHandler->Touch_Settings.bottom_right.x; } - float x_fact = static_cast(P123_Settings.bottom_right.x - P123_Settings.top_left.x) / + float x_fact = static_cast(touchHandler->Touch_Settings.bottom_right.x - touchHandler->Touch_Settings.top_left.x) / static_cast(_ts_x_res); x = static_cast(round(lx / x_fact)); } - int16_t ly = y - P123_Settings.top_left.y; + int16_t ly = y - touchHandler->Touch_Settings.top_left.y; if (ly <= 0) { y = 0; } else { - if (ly > P123_Settings.bottom_right.y) { - ly = P123_Settings.bottom_right.y; + if (ly > touchHandler->Touch_Settings.bottom_right.y) { + ly = touchHandler->Touch_Settings.bottom_right.y; } - float y_fact = (P123_Settings.bottom_right.y - P123_Settings.top_left.y) / _ts_y_res; + float y_fact = (touchHandler->Touch_Settings.bottom_right.y - touchHandler->Touch_Settings.top_left.y) / _ts_y_res; y = static_cast(round(ly / y_fact)); } } } +/** + * Get the current button group + */ +int16_t P123_data_struct::getButtonGroup() { + return touchHandler->getButtonGroup(); +} + +/** + * Check if a valid button group, optionally ignoring group 0 + */ +bool P123_data_struct::validButtonGroup(int16_t buttonGroup, + bool ignoreZero) { + return touchHandler->validButtonGroup(buttonGroup, ignoreZero); +} + /** * Set the desired button group, must be between the minimum and maximum found values */ -bool P123_data_struct::setButtonGroup(const EventStruct *event, - int8_t buttonGroup) { - if ((buttonGroup >= 0) && (buttonGroup <= _maxButtonGroup)) { - if (buttonGroup != _buttonGroup) { - displayButtonGroup(event, _buttonGroup, -2); - _buttonGroup = buttonGroup; - displayButtonGroup(event, _buttonGroup, -1); - } - return true; - } - return false; +bool P123_data_struct::setButtonGroup(struct EventStruct *event, + int16_t buttonGroup) { + return touchHandler->setButtonGroup(event, buttonGroup); } /** * Increment button group, if max. group > 0 then min. group = 1 */ -bool P123_data_struct::incrementButtonGroup(const EventStruct *event) { - if (_buttonGroup < _maxButtonGroup) { - displayButtonGroup(event, _buttonGroup, -2); - _buttonGroup++; - displayButtonGroup(event, _buttonGroup, -1); - return true; - } - return false; +bool P123_data_struct::incrementButtonGroup(struct EventStruct *event) { + return touchHandler->incrementButtonGroup(event); } /** * Decrement button group, if max. group > 0 then min. group = 1 */ -bool P123_data_struct::decrementButtonGroup(const EventStruct *event) { - if (_buttonGroup > _minButtonGroup) { - displayButtonGroup(event, _buttonGroup, -2); - _buttonGroup--; - displayButtonGroup(event, _buttonGroup, -1); - return true; - } - return false; +bool P123_data_struct::decrementButtonGroup(struct EventStruct *event) { + return touchHandler->decrementButtonGroup(event); +} + +/** + * Increment button group page (+10), if max. group > 0 then min. group page (+10) = 1 + */ +bool P123_data_struct::incrementButtonPage(struct EventStruct *event) { + return touchHandler->incrementButtonPage(event); +} + +/** + * Decrement button group page (-10), if max. group > 0 then min. group = 1 + */ +bool P123_data_struct::decrementButtonPage(struct EventStruct *event) { + return touchHandler->decrementButtonPage(event); } #endif // ifdef USES_P123 diff --git a/src/src/PluginStructs/P123_data_struct.h b/src/src/PluginStructs/P123_data_struct.h index baf7c0fd31..7997fd0f91 100644 --- a/src/src/PluginStructs/P123_data_struct.h +++ b/src/src/PluginStructs/P123_data_struct.h @@ -4,184 +4,39 @@ #include "../../_Plugin_Helper.h" #include "../../ESPEasy_common.h" #include "../Helpers/AdafruitGFX_helper.h" +#include "../Helpers/ESPEasy_TouchHandler.h" #ifdef USES_P123 # include # ifndef LIMIT_BUILD_SIZE -// # define PLUGIN_123_DEBUG // Additional debugging information +# define PLUGIN_123_DEBUG // Additional debugging information # endif // ifndef LIMIT_BUILD_SIZE -# define P123_USE_TOOLTIPS // Enable tooltips in UI - -# define P123_USE_EXTENDED_TOUCH // Enable extended touch settings - -# ifdef LIMIT_BUILD_SIZE -# ifdef P123_USE_TOOLTIPS -# undef P123_USE_TOOLTIPS -# endif // ifdef P123_USE_TOOLTIPS -# ifdef P123_USE_EXTENDED_TOUCH -# undef P123_USE_EXTENDED_TOUCH -# endif // ifdef P123_USE_EXTENDED_TOUCH -# endif // ifdef LIMIT_BUILD_SIZE -# if defined(P123_USE_TOOLTIPS) && !defined(ENABLE_TOOLTIPS) -# undef P123_USE_TOOLTIPS -# endif // if defined(P123_USE_TOOLTIPS) && !defined(ENABLE_TOOLTIPS) - -# define P123_FLAGS_SEND_XY 0 // Set in Global Settings flags -# define P123_FLAGS_SEND_Z 1 // Set in Global Settings flags -# define P123_FLAGS_SEND_OBJECTNAME 2 // Set in Global Settings flags -# define P123_FLAGS_USE_CALIBRATION 3 // Set in Global Settings flags -# define P123_FLAGS_LOG_CALIBRATION 4 // Set in Global Settings flags -# define P123_FLAGS_ROTATION_FLIPPED 5 // Set in P123_CONFIG_FLAGS -# define P123_FLAGS_DEDUPLICATE 6 // Set in Global Settings flags -# define P123_FLAGS_INIT_OBJECTEVENT 7 // Set in Global Settings flags -# define P123_FLAGS_INITIAL_GROUP 8 // Initial group to activate, 8 bits uses only 6 - # define P123_CONFIG_DISPLAY_TASK PCONFIG(0) # define P123_COLOR_DEPTH PCONFIG_LONG(1) +# define P123_CONFIG_TRESHOLD PCONFIG(1) # define P123_CONFIG_ROTATION PCONFIG(2) # define P123_CONFIG_X_RES PCONFIG(3) # define P123_CONFIG_Y_RES PCONFIG(4) -# define P123_CONFIG_OBJECTCOUNT PCONFIG(5) -# define P123_CONFIG_DEBOUNCE_MS PCONFIG(6) -# define P123_CONFIG_DISPLAY_PREV PCONFIG(7) - -// # define P123_CONFIG_FLAGS PCONFIG_LONG(0) // 0-31 flags -# define P123_VALUE_X UserVar[event->BaseVarIndex + 0] -# define P123_VALUE_Y UserVar[event->BaseVarIndex + 1] -# define P123_VALUE_Z UserVar[event->BaseVarIndex + 2] +# define P123_CONFIG_DISPLAY_PREV PCONFIG(7) // Default settings values -# define P123_TS_TRESHOLD 40 // Treshold before the value is registered as a proper touch -# define P123_TS_ROTATION 0 // Rotation 0-3 = 0/90/180/270 degrees -# define P123_TS_SEND_XY true // Enable/Disable X/Y events -# define P123_TS_SEND_Z false // Disable/Enable Z events -# define P123_TS_SEND_OBJECTNAME true // Enable/Disable objectname events -# define P123_TS_USE_CALIBRATION false // Disable/Enable calibration -# define P123_TS_LOG_CALIBRATION true // Enable/Disable calibration logging -# define P123_TS_ROTATION_FLIPPED false // Enable/Disable rotation flipped 180 deg. -# define P123_TS_X_RES 320 // Pixels, should match with the screen it is mounted on +# define P123_TS_TRESHOLD 40 // Treshold before the value is registered as a proper touch +# define P123_TS_ROTATION 0 // Rotation 0-3 = 0/90/180/270 degrees +# define P123_TS_X_RES 320 // Pixels, should match with the screen it is mounted on # define P123_TS_Y_RES 480 -# define P123_DEBOUNCE_MILLIS 100 // Debounce delay for On/Off button function -# define P123_TOUCH_X_NATIVE 320 // Native touchscreen resolution +# define P123_TOUCH_X_NATIVE 320 // Native touchscreen resolution # define P123_TOUCH_Y_NATIVE 480 -# define P123_MAX_COLOR_INPUTLENGTH 11 // 11 Characters is enough to type in all recognized color names and values -# define P123_MaxObjectNameLength 15 // 14 character objectnames + terminating 0 -# define P123_MAX_CALIBRATION_COUNT 1 // -# define P123_MAX_OBJECT_COUNT 40 // This count of touchobjects should be enough, because of limited - // settings storage, 960 bytes + 8 bytes calibration coordinates -# define P123_EXTRA_OBJECT_COUNT 5 // The number of empty objects to show if max not reached -# define P123_ARRAY_SIZE (P123_MAX_OBJECT_COUNT + P123_MAX_CALIBRATION_COUNT) - -# define P123_MAX_BUTTON_GROUPS 63 // Max. allowed button groups, technically limited to 6 bits = 0..63! - -# define P123_FLAGS_ON_OFF_BUTTON 0 // TouchObjects.flags On/Off Button function -# define P123_FLAGS_INVERT_BUTTON 1 // TouchObjects.flags Inverted On/Off Button function - -# define TOUCHOBJECTS_HELPER_ROTATION_0 0 -# define TOUCHOBJECTS_HELPER_ROTATION_90 1 -# define TOUCHOBJECTS_HELPER_ROTATION_180 2 -# define TOUCHOBJECTS_HELPER_ROTATION_270 3 - -# define P123_SETTINGS_SEPARATOR '\x02' - -// Settings array field offsets: Calibration -# define P123_CALIBRATION_START 0 // Index into settings array -# define P123_CALIBRATION_ENABLED 1 // Enabled 0/1 (parseString index starts at 1) -# define P123_CALIBRATION_LOG_ENABLED 2 // Calibration Log Enabled 0/1 -# define P123_CALIBRATION_TOP_X 3 // Top X offset (uint16_t) -# define P123_CALIBRATION_TOP_Y 4 // Top Y -# define P123_CALIBRATION_BOTTOM_X 5 // Bottom X -# define P123_CALIBRATION_BOTTOM_Y 6 // Bottom Y -# define P123_COMMON_DEBOUNCE_MS 7 // Debounce milliseconds -# define P123_COMMON_TOUCH_TRESHOLD 8 // Treshold setting -# define P123_COMMON_FLAGS 9 // Common flags -# ifdef P123_USE_EXTENDED_TOUCH -# define P123_COMMON_DEF_COLOR_ON 10 // Default Color ON (rgb565, uint16_t) -# define P123_COMMON_DEF_COLOR_OFF 11 // Default Color OFF -# define P123_COMMON_DEF_COLOR_BORDER 12 // Default Color Border -# define P123_COMMON_DEF_COLOR_CAPTION 13 // Default Color Caption -# define P123_COMMON_DEF_COLOR_DISABLED 14 // Default Disabled Color -# define P123_COMMON_DEF_COLOR_DISABCAPT 15 // Default Disabled Caption Color -# endif // ifdef P123_USE_EXTENDED_TOUCH - -// Settings array field offsets: Touch objects -# define P123_OBJECT_INDEX_START (P123_CALIBRATION_START + 1) -# define P123_OBJECT_INDEX_END (P123_ARRAY_SIZE - (P123_CALIBRATION_START + 1)) -# define P123_OBJECT_NAME 1 // Name (String 14) (parseString index starts at 1) -# define P123_OBJECT_FLAGS 2 // Flags (uint32_t) -# define P123_OBJECT_COORD_TOP_X 3 // Top X (uint16_t) -# define P123_OBJECT_COORD_TOP_Y 4 // Top Y -# define P123_OBJECT_COORD_WIDTH 5 // Width -# define P123_OBJECT_COORD_HEIGHT 6 // Height -# ifdef P123_USE_EXTENDED_TOUCH -# define P123_OBJECT_COLOR_ON 7 // Color ON (rgb565, uint16_t) -# define P123_OBJECT_COLOR_OFF 8 // Color OFF -# define P123_OBJECT_COLOR_CAPTION 9 // Color Caption -# define P123_OBJECT_CAPTION_ON 10 // Caption ON (String 12, quoted) -# define P123_OBJECT_CAPTION_OFF 11 // Caption OFF (String 12, quoted) -# define P123_OBJECT_COLOR_BORDER 12 // Color Border -# define P123_OBJECT_COLOR_DISABLED 13 // Disabled Color -# define P123_OBJECT_COLOR_DISABCAPT 14 // Disabled Caption Color -# endif // ifdef P123_USE_EXTENDED_TOUCH - -# define P123_OBJECT_FLAG_ENABLED 0 // Enabled -# define P123_OBJECT_FLAG_BUTTON 1 // Button behavior -# define P123_OBJECT_FLAG_INVERTED 2 // Inverted button -# define P123_OBJECT_FLAG_FONTSCALE 3 // 4 bits used as button alignment -# define P123_OBJECT_FLAG_BUTTONTYPE 7 // 8 bits used as button type -# define P123_OBJECT_FLAG_GROUP 15 // 8 bits used as button group -# define P123_OBJECT_FLAG_ACTIONGROUP 23 // 8 bits, 6 bits used as action group 0..63, 2 bits used as action option - -# define P123_DEFAULT_COLOR_DISABLED 0x9410 -# define P123_DEFAULT_COLOR_DISABLED_CAPTION 0x5A69 - -// Lets define our own coordinate point -struct tP123_Point -{ - uint16_t x = 0u; - uint16_t y = 0u; -}; - -// For touch objects we store a name, 2 coordinates, flags and other options -struct tP123_TouchObjects -{ - String objectName; - String captionOn; - String captionOff; - uint32_t flags = 0u; - uint32_t SurfaceAreas = 0u; - uint32_t TouchTimers = 0u; - tP123_Point top_left; - tP123_Point width_height; - # ifdef P123_USE_EXTENDED_TOUCH - uint16_t colorOn = 0u; - uint16_t colorOff = 0u; - uint16_t colorCaption = 0u; - uint16_t colorBorder = 0u; - uint16_t colorDisabled = 0u; - uint16_t colorDisabledCaption = 0u; - # endif // ifdef P123_USE_EXTENDED_TOUCH - bool TouchStates = false; -}; - -// Touch actions, use with mask 0xC0, other 6 bits are group/code to activate -enum class P123_touch_action_e : uint8_t { - Default = 0b00000000, // 0x00 - ActivateGroup = 0b01000000, // 0x40 - IncrementGroup = 0b10000000, // 0x80 - DecrementGroup = 0b11000000, // 0xC0 - TouchAction_MAX = 4 // Last item is count, max 4! -}; - -const __FlashStringHelper* toString(P123_touch_action_e action); - +# define P123_ROTATION_0 0 +# define P123_ROTATION_90 1 +# define P123_ROTATION_180 2 +# define P123_ROTATION_270 3 // Data structure struct P123_data_struct : public PluginTaskData_base @@ -190,111 +45,67 @@ struct P123_data_struct : public PluginTaskData_base ~P123_data_struct(); void reset(); - bool init(const EventStruct *event, - uint8_t rotation, - uint16_t ts_x_res, - uint16_t ts_y_res, - uint16_t displayTask, - AdaGFXColorDepth colorDepth); + bool init(struct EventStruct *event); bool isInitialized() const; - void loadTouchObjects(const EventStruct *event); + + bool plugin_webform_load(struct EventStruct *event); + bool plugin_webform_save(struct EventStruct *event); + bool plugin_write(struct EventStruct *event, + const String & string); + bool plugin_fifty_per_second(struct EventStruct *event); + + void loadTouchObjects(struct EventStruct *event); bool touched(); void readData(int16_t& x, int16_t& y, int16_t& z, int16_t& ox, int16_t& oy); + void setRotation(uint8_t n); void setRotationFlipped(bool _flipped); - bool isCalibrationActive(); bool isValidAndTouchedTouchObject(int16_t x, int16_t y, String& selectedObjectName, int8_t& selectedObjectIndex); - bool setTouchObjectState(struct EventStruct *event, - const String & touchObject, - bool state); - bool setTouchButtonOnOff(struct EventStruct *event, - const String & touchObject, - bool state); - void scaleRawToCalibrated(int16_t& x, - int16_t& y); - bool plugin_webform_load(struct EventStruct *event); - bool plugin_webform_save(struct EventStruct *event); - bool plugin_fifty_per_second(struct EventStruct *event); - bool setButtonGroup(const EventStruct *event, - int8_t buttonGroup); - bool incrementButtonGroup(const EventStruct *event); - bool decrementButtonGroup(const EventStruct *event); - void displayButtonGroup(const EventStruct *event, - int8_t buttonGroup, - int8_t mode = 0); + int8_t getTouchObjectIndex(struct EventStruct *event, + const String & touchObject, + bool isButton = false); + bool setTouchObjectState(struct EventStruct *event, + const String & touchObject, + bool state); + bool setTouchButtonOnOff(struct EventStruct *event, + const String & touchObject, + bool state); + void scaleRawToCalibrated(int16_t& x, + int16_t& y); + + int16_t getButtonGroup(); + bool validButtonGroup(int16_t buttonGroup, + bool ignoreZero = false); + bool setButtonGroup(struct EventStruct *event, + int16_t buttonGroup); + bool incrementButtonGroup(struct EventStruct *event); + bool decrementButtonGroup(struct EventStruct *event); + bool incrementButtonPage(struct EventStruct *event); + bool decrementButtonPage(struct EventStruct *event); + void displayButtonGroup(struct EventStruct *event, + int16_t buttonGroup, + int8_t mode = 0); + bool displayButton(struct EventStruct *event, + int8_t buttonNr, + int16_t buttonGroup = -1, + int8_t mode = 0); private: - int parseStringToInt(const String& string, - uint8_t indexFind, - char separator = ',', - int defaultValue = 0); - void generateObjectEvent(const EventStruct *event, - const int8_t objectIndex, - const int8_t onOffState, - const bool groupSwitch = false, - const int8_t factor = 1); - // This is initialized by calling init() - Adafruit_FT6206 *touchscreen = nullptr; - uint8_t _rotation = 0u; - bool _useCalibration = false; - uint16_t _ts_x_res = 0u; - uint16_t _ts_y_res = 0u; - uint16_t _displayTask = 0u; - AdaGFXColorDepth _colorDepth = AdaGFXColorDepth::FullColor; - - bool _flipped = false; // buffered settings - bool _deduplicate = false; - - // Calibration and some other settings - struct tP123_Globals - { - uint32_t flags = 0u; - tP123_Point top_left; - tP123_Point bottom_right; - uint16_t treshold = 0u; - # ifdef P123_USE_EXTENDED_TOUCH - uint16_t colorOn = 0u; - uint16_t colorOff = 0u; - uint16_t colorCaption = 0u; - uint16_t colorBorder = 0u; - uint16_t colorDisabled = 0u; - uint16_t colorDisabledCaption = 0u; - # endif // ifdef P123_USE_EXTENDED_TOUCH - uint8_t debounceMs = 0u; - bool calibrationEnabled = false; - bool logEnabled = false; - }; - - tP123_Globals P123_Settings; - - std::vectorTouchObjects; - - int8_t _buttonGroup = 0; - int8_t _minButtonGroup = 0; - int8_t _maxButtonGroup = 0; - -public: - - // This is filled during checking of a touchobject - // std::vector < uint32_t; SurfaceAreas; - - // Counters for debouncing touch button - // std::vectorTouchTimers; - // std::vector TouchStates; - - String settingsArray[P123_ARRAY_SIZE]; - uint8_t lastObjectIndex = 0u; + Adafruit_FT6206 *touchscreen = nullptr; + uint8_t _rotation = 0u; + uint16_t _ts_x_res = 0u; + uint16_t _ts_y_res = 0u; - uint8_t objectCount = 0u; + ESPEasy_TouchHandler *touchHandler = nullptr; }; #endif // ifdef USED_P123 From 5c20b9184e38a8739c854e58d2b2b13d0c7fc2dc Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 4 Jun 2022 13:27:48 +0200 Subject: [PATCH 013/113] [TouchHandler] Fix conditional compilation issue --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 344ae0c03a..00a060faa2 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -300,7 +300,7 @@ int8_t ESPEasy_TouchHandler::getTouchObjectIndex(struct EventStruct *event, // ATTENTION: Any externally provided objectNumber is 1-based, result is 0-based if (validIntFromString(touchObject, index) && (index > 0) && - (index <= TouchObjects.size())) { + (index <= static_cast(TouchObjects.size()))) { return static_cast(index - 1); } @@ -438,10 +438,15 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, int16_t buttonGroup, int8_t mode) { if ((buttonNr < 0) || (buttonNr >= static_cast(TouchObjects.size()))) { return false; } // sanity check - int8_t state = 99; - int16_t group = get8BitFromUL(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_GROUP); + int8_t state = 99; + int16_t group = get8BitFromUL(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_GROUP); + + # ifdef TOUCH_USE_EXTENDED_TOUCH Touch_action_e action = static_cast(get4BitFromUL(TouchObjects[buttonNr].groupFlags, TOUCH_OBJECT_GROUP_ACTION)); - bool isArrow = false; + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + bool isArrow = false; + + # ifdef TOUCH_USE_EXTENDED_TOUCH if ((mode > -2) && // Not on clear (-2 and -3) bitRead(Touch_Settings.flags, TOUCH_FLAGS_AUTO_PAGE_ARROWS) && @@ -451,6 +456,7 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, (action == Touch_action_e::IncrementPage))) { isArrow = true; } + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH if (!TouchObjects[buttonNr].objectName.isEmpty() && ((bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED) && (group == 0)) || (group > 0) || isArrow) && @@ -461,6 +467,8 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, // Act like a button, 1 = On, 0 = Off, inversion is handled in generateObjectEvent() state = TouchObjects[buttonNr].TouchStates ? 1 : 0; + # ifdef TOUCH_USE_EXTENDED_TOUCH + if (isArrow) { // Auto-Enable/Disable the arrow buttons bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); state = 1; // always get ON state! @@ -478,6 +486,7 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, bitWrite(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED, validButtonGroup(buttonGroup + (pgupInvert ? -10 : 10), true)); } } + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH if (bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED)) { if (mode == 0) { From 6899e94586fb4c8f12ca6c8eeabe7ff7030ce2c8 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 6 Jun 2022 15:05:15 +0200 Subject: [PATCH 014/113] [AdaGFX] Add support for confined windows to print/draw in [AdaGFX] Add support for getting config values --- src/src/Helpers/AdafruitGFX_helper.cpp | 580 +++++++++++++++++++++---- src/src/Helpers/AdafruitGFX_helper.h | 88 +++- 2 files changed, 573 insertions(+), 95 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index e0fba9832c..c4a0eb4e78 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -535,7 +535,7 @@ AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_GFX *display, initialize(); } -# ifdef ADAGFX_ENABLE_BMP_DISPLAY +# if ADAGFX_ENABLE_BMP_DISPLAY AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_SPITFT *display, const String & trigger, uint16_t res_x, @@ -559,8 +559,11 @@ AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_SPITFT *display, initialize(); } -# endif // ifdef ADAGFX_ENABLE_BMP_DISPLAY +# endif // if ADAGFX_ENABLE_BMP_DISPLAY +/**************************************************************************** + * Initialize the AdafruitGFX_Helper + ***************************************************************************/ void AdafruitGFX_helper::initialize() { _trigger.toLowerCase(); // store trigger in lowercase # ifndef BUILD_NO_DEBUG @@ -585,6 +588,10 @@ void AdafruitGFX_helper::initialize() { _display_x = _res_x; // Store initial resolution _display_y = _res_y; + # if ADAGFX_ENABLE_FRAMED_WINDOW + defineWindow(0, 0, _res_x, _res_y, 0, 0); // Add window 0 at rotation 0 + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW + if (_fontscaling < 1) { _fontscaling = 1; } if (nullptr != _display) { @@ -594,6 +601,9 @@ void AdafruitGFX_helper::initialize() { } } +/**************************************************************************** + * Show enabled features of the helper + ***************************************************************************/ String AdafruitGFX_helper::getFeatures() { String log = F("Features:"); @@ -609,6 +619,12 @@ String AdafruitGFX_helper::getFeatures() { # if (defined(ADAGFX_ENABLE_BUTTON_DRAW) && ADAGFX_ENABLE_BUTTON_DRAW) log += F(" btn,"); # endif // if (defined(ADAGFX_ENABLE_BUTTON_DRAW) && ADAGFX_ENABLE_BUTTON_DRAW)` + # if (defined(ADAGFX_ENABLE_FRAMED_WINDOW) && ADAGFX_ENABLE_FRAMED_WINDOW) + log += F(" win,"); + # endif // if (defined(ADAGFX_ENABLE_FRAMED_WINDOW) && ADAGFX_ENABLE_FRAMED_WINDOW) + # if (defined(ADAGFX_ENABLE_GET_CONFIG_VALUE) && ADAGFX_ENABLE_GET_CONFIG_VALUE) + log += F(" getconf,"); + # endif // if (defined(ADAGFX_ENABLE_GET_CONFIG_VALUE) && ADAGFX_ENABLE_GET_CONFIG_VALUE) if (log.endsWith(F(","))) { log.remove(log.length() - 1); @@ -639,8 +655,16 @@ bool AdafruitGFX_helper::processCommand(const String& string) { if ((nullptr == _display) || _trigger.isEmpty()) { return success; } - String cmd = parseString(string, 1); // lower case - String subcommand = parseString(string, 2); + String cmd = parseString(string, 1); // lower case + String subcommand = parseString(string, 2); + uint16_t res_x = _res_x; + uint16_t res_y = _res_y; + uint16_t _xo = 0, _yo = 0; + + # if ADAGFX_ENABLE_FRAMED_WINDOW + getWindowLimits(res_x, res_y); + getWindowOffsets(_xo, _yo); + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW if (!(cmd.equals(_trigger) || isAdaGFXTrigger(cmd)) || @@ -713,7 +737,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { if (_columnRowMode) { _display->setCursor(nParams[0] * _fontwidth, nParams[1] * _fontheight); } else { - _display->setCursor(nParams[0] - _p095_compensation, nParams[1] - _p095_compensation); + _display->setCursor(nParams[0] + _xo - _p095_compensation, nParams[1] + _yo - _p095_compensation); } } } @@ -729,7 +753,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { if (_columnRowMode) { _display->setCursor(nParams[0] * _fontwidth, nParams[1] * _fontheight); } else { - _display->setCursor(nParams[0], nParams[1]); + _display->setCursor(nParams[0] + _xo, nParams[1] + _yo); } _display->println(parseStringToEndKeepCase(string, 5)); // Print entire rest of provided line } @@ -768,8 +792,8 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # endif // if ADAGFX_ARGUMENT_VALIDATION { printText(sParams[2].c_str(), - nParams[0] - _p095_compensation, - nParams[1] - _p095_compensation, + nParams[0] + _xo - _p095_compensation, + nParams[1] + _yo - _p095_compensation, _fontscaling, _fgcolor, _fgcolor); // transparent bg @@ -785,8 +809,8 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # endif // if ADAGFX_ARGUMENT_VALIDATION { printText(sParams[3].c_str(), - nParams[0] - _p095_compensation, - nParams[1] - _p095_compensation, + nParams[0] + _xo - _p095_compensation, + nParams[1] + _yo - _p095_compensation, nParams[2], _fgcolor, _fgcolor); // transparent bg @@ -803,8 +827,8 @@ bool AdafruitGFX_helper::processCommand(const String& string) { { uint16_t color = AdaGFXparseColor(sParams[3], _colorDepth); printText(sParams[4].c_str(), - nParams[0] - _p095_compensation, - nParams[1] - _p095_compensation, + nParams[0] + _xo - _p095_compensation, + nParams[1] + _yo - _p095_compensation, nParams[2], color, color); // transparent bg @@ -820,8 +844,8 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # endif // if ADAGFX_ARGUMENT_VALIDATION { printText(sParams[5].c_str(), - nParams[0] - _p095_compensation, - nParams[1] - _p095_compensation, + nParams[0] + _xo - _p095_compensation, + nParams[1] + _yo - _p095_compensation, nParams[2], AdaGFXparseColor(sParams[3], _colorDepth), AdaGFXparseColor(sParams[4], _colorDepth)); @@ -834,11 +858,22 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } else if (subcommand.equals(F("clear"))) // Clear display { - if (argCount >= 1) { - _display->fillScreen(AdaGFXparseColor(sParams[0], _colorDepth)); - } else { - _display->fillScreen(_bgcolor); + # if ADAGFX_ENABLE_FRAMED_WINDOW + + if (_window == 0) + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW + { + _display->fillScreen(argCount == 0 ? _bgcolor : AdaGFXparseColor(sParams[0], _colorDepth)); } + # if ADAGFX_ENABLE_FRAMED_WINDOW + else { + // logWindows(F("clear ")); // Use for debugging only + uint16_t _w = 0, _h = 0; + getWindowLimits(_w, _h); + _display->fillRect(_xo, _yo, _w, _h, + argCount == 0 ? _bgcolor : AdaGFXparseColor(sParams[0], _colorDepth)); + } + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW } else if (subcommand.equals(F("rot")) && (argCount == 1)) // Rotation { @@ -1026,29 +1061,29 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } else # endif // if ADAGFX_ARGUMENT_VALIDATION { - _display->drawLine(nParams[0], nParams[1], nParams[2], nParams[3], AdaGFXparseColor(sParams[4], _colorDepth)); + _display->drawLine(nParams[0] + _xo, nParams[1] + _yo, nParams[2] + _xo, nParams[3] + _yo, AdaGFXparseColor(sParams[4], _colorDepth)); } } else if (subcommand.equals(F("lh")) && (argCount == 3)) { // lh: Horizontal line # if ADAGFX_ARGUMENT_VALIDATION - if ((nParams[0] < 0) || (nParams[0] > _res_x)) { + if ((nParams[0] < 0) || (nParams[0] > res_x)) { success = false; } else # endif // if ADAGFX_ARGUMENT_VALIDATION { - _display->drawFastHLine(0, nParams[0], nParams[1], AdaGFXparseColor(sParams[2], _colorDepth)); + _display->drawFastHLine(_xo, nParams[0] + _yo, nParams[1], AdaGFXparseColor(sParams[2], _colorDepth)); } } else if (subcommand.equals(F("lv")) && (argCount == 3)) { // lv: Vertical line # if ADAGFX_ARGUMENT_VALIDATION - if ((nParams[0] < 0) || (nParams[0] > _res_y)) { + if ((nParams[0] < 0) || (nParams[0] > res_y)) { success = false; } else # endif // if ADAGFX_ARGUMENT_VALIDATION { - _display->drawFastVLine(nParams[0], 0, nParams[1], AdaGFXparseColor(sParams[2], _colorDepth)); + _display->drawFastVLine(nParams[0] + _xo, _yo, nParams[1], AdaGFXparseColor(sParams[2], _colorDepth)); } } # if ADAGFX_ENABLE_EXTRA_CMDS @@ -1114,7 +1149,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { log += AdaGFXcolorToString(mcolor, _colorDepth); addLog(LOG_LEVEL_INFO, log); # endif // ifndef BUILD_NO_DEBUG - _display->drawLine(nParams[0], nParams[1], nParams[2], nParams[3], mcolor); + _display->drawLine(nParams[0] + _xo, nParams[1] + _yo, nParams[2] + _xo, nParams[3] + _yo, mcolor); if ((cx == -1) && (cy == -1)) { cx = nParams[0]; @@ -1137,7 +1172,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } else # endif // if ADAGFX_ARGUMENT_VALIDATION { - _display->drawRect(nParams[0], nParams[1], nParams[2], nParams[3], AdaGFXparseColor(sParams[4], _colorDepth)); + _display->drawRect(nParams[0] + _xo, nParams[1] + _yo, nParams[2], nParams[3], AdaGFXparseColor(sParams[4], _colorDepth)); } } else if (subcommand.equals(F("rf")) && (argCount == 6)) { // rf: Rectangled, filled @@ -1149,8 +1184,8 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } else # endif // if ADAGFX_ARGUMENT_VALIDATION { - _display->fillRect(nParams[0], nParams[1], nParams[2], nParams[3], AdaGFXparseColor(sParams[5], _colorDepth)); - _display->drawRect(nParams[0], nParams[1], nParams[2], nParams[3], AdaGFXparseColor(sParams[4], _colorDepth)); + _display->fillRect(nParams[0] + _xo, nParams[1] + _yo, nParams[2], nParams[3], AdaGFXparseColor(sParams[5], _colorDepth)); + _display->drawRect(nParams[0] + _xo, nParams[1] + _yo, nParams[2], nParams[3], AdaGFXparseColor(sParams[4], _colorDepth)); } } else if (subcommand.equals(F("c")) && (argCount == 4)) { // c: Circle @@ -1162,7 +1197,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } else # endif // if ADAGFX_ARGUMENT_VALIDATION { - _display->drawCircle(nParams[0], nParams[1], nParams[2], AdaGFXparseColor(sParams[3], _colorDepth)); + _display->drawCircle(nParams[0] + _xo, nParams[1] + _yo, nParams[2], AdaGFXparseColor(sParams[3], _colorDepth)); } } else if (subcommand.equals(F("cf")) && (argCount == 5)) { // cf: Circle, filled @@ -1174,8 +1209,8 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } else # endif // if ADAGFX_ARGUMENT_VALIDATION { - _display->fillCircle(nParams[0], nParams[1], nParams[2], AdaGFXparseColor(sParams[4], _colorDepth)); - _display->drawCircle(nParams[0], nParams[1], nParams[2], AdaGFXparseColor(sParams[3], _colorDepth)); + _display->fillCircle(nParams[0] + _xo, nParams[1] + _yo, nParams[2], AdaGFXparseColor(sParams[4], _colorDepth)); + _display->drawCircle(nParams[0] + _xo, nParams[1] + _yo, nParams[2], AdaGFXparseColor(sParams[3], _colorDepth)); } } else if (subcommand.equals(F("t")) && (argCount == 7)) { // t: Triangle @@ -1188,7 +1223,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } else # endif // if ADAGFX_ARGUMENT_VALIDATION { - _display->drawTriangle(nParams[0], nParams[1], nParams[2], nParams[3], nParams[4], nParams[5], + _display->drawTriangle(nParams[0] + _xo, nParams[1] + _yo, nParams[2] + _xo, nParams[3] + _yo, nParams[4] + _xo, nParams[5] + _yo, AdaGFXparseColor(sParams[6], _colorDepth)); } } @@ -1202,19 +1237,19 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } else # endif // if ADAGFX_ARGUMENT_VALIDATION { - _display->fillTriangle(nParams[0], - nParams[1], - nParams[2], - nParams[3], - nParams[4], - nParams[5], + _display->fillTriangle(nParams[0] + _xo, + nParams[1] + _yo, + nParams[2] + _xo, + nParams[3] + _yo, + nParams[4] + _xo, + nParams[5] + _yo, AdaGFXparseColor(sParams[7], _colorDepth)); - _display->drawTriangle(nParams[0], - nParams[1], - nParams[2], - nParams[3], - nParams[4], - nParams[5], + _display->drawTriangle(nParams[0] + _xo, + nParams[1] + _yo, + nParams[2] + _xo, + nParams[3] + _yo, + nParams[4] + _xo, + nParams[5] + _yo, AdaGFXparseColor(sParams[6], _colorDepth)); } } @@ -1228,7 +1263,12 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } else # endif // if ADAGFX_ARGUMENT_VALIDATION { - _display->drawRoundRect(nParams[0], nParams[1], nParams[2], nParams[3], nParams[4], AdaGFXparseColor(sParams[5], _colorDepth)); + _display->drawRoundRect(nParams[0] + _xo, + nParams[1] + _yo, + nParams[2], + nParams[3], + nParams[4], + AdaGFXparseColor(sParams[5], _colorDepth)); } } else if (subcommand.equals(F("rrf")) && (argCount == 7)) { // rrf: Rounded rectangle, filled @@ -1241,8 +1281,18 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } else # endif // if ADAGFX_ARGUMENT_VALIDATION { - _display->fillRoundRect(nParams[0], nParams[1], nParams[2], nParams[3], nParams[4], AdaGFXparseColor(sParams[6], _colorDepth)); - _display->drawRoundRect(nParams[0], nParams[1], nParams[2], nParams[3], nParams[4], AdaGFXparseColor(sParams[5], _colorDepth)); + _display->fillRoundRect(nParams[0] + _xo, + nParams[1] + _yo, + nParams[2], + nParams[3], + nParams[4], + AdaGFXparseColor(sParams[6], _colorDepth)); + _display->drawRoundRect(nParams[0] + _xo, + nParams[1] + _yo, + nParams[2], + nParams[3], + nParams[4], + AdaGFXparseColor(sParams[5], _colorDepth)); } } else if (subcommand.equals(F("px")) && (argCount == 3)) { // px: Pixel @@ -1253,7 +1303,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } else # endif // if ADAGFX_ARGUMENT_VALIDATION { - _display->drawPixel(nParams[0], nParams[1], AdaGFXparseColor(sParams[2], _colorDepth)); + _display->drawPixel(nParams[0] + _xo, nParams[1] + _yo, AdaGFXparseColor(sParams[2], _colorDepth)); } } else if ((subcommand.equals(F("pxh")) || subcommand.equals(F("pxv"))) && (argCount > 2)) { // pxh/pxv: Pixels, hor./vert. incremented @@ -1265,7 +1315,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # endif // if ADAGFX_ARGUMENT_VALIDATION { _display->startWrite(); - _display->writePixel(nParams[0], nParams[1], AdaGFXparseColor(sParams[2], _colorDepth)); + _display->writePixel(nParams[0] + _xo, nParams[1] + _yo, AdaGFXparseColor(sParams[2], _colorDepth)); loop = true; uint8_t h = 0; uint8_t v = 0; @@ -1282,12 +1332,12 @@ bool AdafruitGFX_helper::processCommand(const String& string) { if (color.isEmpty() # if ADAGFX_ARGUMENT_VALIDATION - || invalidCoordinates(nParams[0] + h, nParams[1] + v) + || invalidCoordinates(nParams[0] + h + _xo, nParams[1] + v + _yo) # endif // if ADAGFX_ARGUMENT_VALIDATION ) { loop = false; } else { - _display->writePixel(nParams[0] + h, nParams[1] + v, AdaGFXparseColor(color, _colorDepth)); + _display->writePixel(nParams[0] + h + _xo, nParams[1] + v + _yo, AdaGFXparseColor(color, _colorDepth)); if (isPxh) { h++; @@ -1303,7 +1353,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # if ADAGFX_ENABLE_BMP_DISPLAY else if (subcommand.equals(F("bmp")) && (argCount == 3)) { // bmp,x,y,filename.bmp : show bmp from file if (!sParams[2].isEmpty()) { - success = showBmp(sParams[2], nParams[0], nParams[1]); + success = showBmp(sParams[2], nParams[0] + _xo, nParams[1] + _yo); } else { success = false; } @@ -1346,7 +1396,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { if (!sParams[10].isEmpty()) { captionColor = AdaGFXparseColor(sParams[10], _colorDepth); } - if (nParams[11] > 0) { fontScale = nParams[11]; } + if ((nParams[11] > 0) && (nParams[11] <= 10)) { fontScale = nParams[11]; } if (!sParams[14].isEmpty()) { borderColor = AdaGFXparseColor(sParams[14], _colorDepth); } @@ -1379,14 +1429,14 @@ bool AdafruitGFX_helper::processCommand(const String& string) { if ((buttonType != Button_type_e::None) || clearArea) { drawButtonShape(buttonType, - nParams[2], nParams[3], nParams[4], nParams[5], + nParams[2] + _xo, nParams[3] + _yo, nParams[4], nParams[5], _bgcolor, _bgcolor); } // Check button-type bits (mask: 0x0F) to draw correct shape if (!clearArea) { drawButtonShape(buttonType, - nParams[2], nParams[3], nParams[4], nParams[5], + nParams[2] + _xo, nParams[3] + _yo, nParams[4], nParams[5], fillColor, borderColor); } @@ -1405,7 +1455,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } newString = AdaGFXparseTemplate(newString, 20); - if ((nParams[11] > 0) && (nParams[11] <= 10)) { _display->setTextSize(nParams[11]); } // set scaling + _display->setTextSize(fontScale); // set scaling _display->getTextBounds(newString, 0, 0, &x1, &y1, &w1, &h1); // get caption length and height in pixels _display->getTextBounds(F(" "), 0, 0, &x1, &y1, &w2, &h2); // measure space width for little margins @@ -1468,7 +1518,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { newString = parseStringToEndKeepCase(newString, 2); } } - success = showBmp(newString, nParams[2] + offX, nParams[3] + offY); + success = showBmp(newString, nParams[2] + _xo + offX, nParams[3] + _yo + offY); } else # endif // if ADAGFX_ENABLE_BMP_DISPLAY { @@ -1484,8 +1534,8 @@ bool AdafruitGFX_helper::processCommand(const String& string) { if ((buttonLayout != Button_layout_e::NoCaption) && (buttonLayout != Button_layout_e::Bitmap)) { // Set position and colors, then print - _display->setCursor(nParams[2], nParams[3]); - _display->setTextColor(captionColor, captionColor); // transparent bg results in button color + _display->setCursor(nParams[2] + _xo, nParams[3] + _yo); + _display->setTextColor(textColor, textColor); // transparent bg results in button color _display->print(newString); // restore colors @@ -1493,11 +1543,73 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } // restore font scaling - if ((nParams[11] > 0) && (nParams[11] <= 10)) { _display->setTextSize(_fontscaling); } + _display->setTextSize(_fontscaling); } } } # endif // if ADAGFX_ENABLE_BUTTON_DRAW + # if ADAGFX_ENABLE_FRAMED_WINDOW + else if (subcommand.equals(F("win")) && (argCount >= 1) && (argCount <= 2)) { // win: select window by id + success = selectWindow(nParams[0], nParams[1]); + } + else if (subcommand.equals(F("defwin")) && (argCount >= 5) && (argCount <= 6)) { // defwin: define window + int8_t rot = _rotation; + # if ADAGFX_ARGUMENT_VALIDATION + int16_t curWin = getWindow(); + + if (curWin != 0) { selectWindow(0); } // Validate against raw window coordinates + + if (argCount == 6) { setRotation(nParams[5]); } // Use requested rotation + + if (invalidCoordinates(nParams[0], nParams[1]) || + invalidCoordinates(nParams[0] + nParams[2], nParams[1] + nParams[3])) { + success = false; + + if (curWin != 0) { selectWindow(curWin); } // restore current window + + if (rot != _rotation) { setRotation(rot); } // Restore rotation + } else + # endif // if ADAGFX_ARGUMENT_VALIDATION + { + # if ADAGFX_ARGUMENT_VALIDATION + + if (curWin != 0) { selectWindow(curWin); } // restore current window + # endif // if ADAGFX_ARGUMENT_VALIDATION + + if (nParams[4] > 0) { // Window 0 is the raw window, having the full size, created at initialization of this helper instance + # ifndef BUILD_NO_DEBUG + int16_t win = // avoid compiler warning + # endif // ifndef BUILD_NO_DEBUG + defineWindow(nParams[0], + nParams[1], + nParams[2], + nParams[3], + nParams[4], + argCount == 6 ? nParams[5] : _rotation); + # ifndef BUILD_NO_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("AdaGFX defined window id: "); + log += win; + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // ifndef BUILD_NO_DEBUG + + if (rot != _rotation) { setRotation(rot); } // Restore rotation, also update new window + } else { + success = false; + } + // logWindows(F(" deFwin ")); // Use for debugging only? + } + } + else if (subcommand.equals(F("delwin")) && (argCount == 1)) { // delwin: delete window + // logWindows(F(" deLwin ")); // use for debugging only + + if (nParams[0] > 0) { // don't delete window 0 + success = deleteWindow(nParams[0]); + } + } + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW else { success = false; } @@ -1505,6 +1617,73 @@ bool AdafruitGFX_helper::processCommand(const String& string) { return success; } +/**************************************************************************** + * Get a config value from the plugin + ***************************************************************************/ +# if ADAGFX_ENABLE_GET_CONFIG_VALUE +bool AdafruitGFX_helper::pluginGetConfigValue(String& string) { + bool success = false; + String command = parseString(string, 1); + + if (command == F("win")) { // win: get current window id + # if ADAGFX_ENABLE_FRAMED_WINDOW // if feature enabled + string = getWindow(); + success = true; + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW + } else if (command == F("iswin")) { // iswin: check if windows exists + # if ADAGFX_ENABLE_FRAMED_WINDOW // if feature enabled + command = parseString(string, 2); + int win = 0; + + if (validIntFromString(command, win)) { + string = validWindow(static_cast(win)); + } else { + string = '0'; + } + success = true; // Always correct, just return 'false' if wrong + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW + } else if ((command == F("width")) || // width/height: get window width or height + (command == F("height"))) { + # if ADAGFX_ENABLE_FRAMED_WINDOW // if feature enabled + uint16_t w = 0, h = 0; + getWindowLimits(w, h); + + if (command == F("width")) { + string = w; + } else { + string = h; + } + success = true; + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW + } else if ((command == F("length")) || // length/textheight: get text length or height + (command == F("textheight"))) { + int16_t x1, y1; + uint16_t w1, h1; + String newString = parseStringToEndKeepCase(string, 2); + _display->getTextBounds(newString, 0, 0, &x1, &y1, &w1, &h1); // Count length and height + + if (command == F("length")) { + string = w1; + } else { + string = h1; + } + success = true; + } else if (command == F("rot")) { // rot: get current rotation setting + string = _rotation; + success = true; + } else if (command == F("txs")) { // txs: get current text scaling setting + string = _fontscaling; + success = true; + } else if (command == F("tpm")) { // tpm: get current text print mode setting + string = static_cast(_textPrintMode); + success = true; + } + + return success; +} + +# endif // if ADAGFX_ENABLE_GET_CONFIG_VALUE + /**************************************************************************** * draw a button shape with provided color, can also clear a previously drawn button ***************************************************************************/ @@ -1589,6 +1768,14 @@ void AdafruitGFX_helper::printText(const char *string, uint8_t _h = 0; int16_t x1, y1; uint16_t w0, w1, h1; + uint16_t res_x = _res_x; + uint16_t res_y = _res_y; + uint16_t xo = 0, yo = 0; + + # if ADAGFX_ENABLE_FRAMED_WINDOW + getWindowLimits(res_x, res_y); + getWindowOffsets(xo, yo); + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW if (_columnRowMode) { _x = X * (_fontwidth * textSize); // We need this multiple times @@ -1615,18 +1802,11 @@ void AdafruitGFX_helper::printText(const char *string, String newString = string; if (_textPrintMode != AdaGFXTextPrintMode::ContinueToNextLine) { - if (_isProportional) { // Proportional font. This is rather slow! - _display->getTextBounds(newString, _x, _y, &x1, &y1, &w1, &h1); // Count length + _display->getTextBounds(newString, _x, _y, &x1, &y1, &w1, &h1); // Count length - while ((newString.length() > 0) && ((_x + w1) > _res_x)) { - newString.remove(newString.length() - 1); // Cut last character off - _display->getTextBounds(newString, _x, _y, &x1, &y1, &w1, &h1); // Re-count length - } - } - else { // Fixed width font - if (newString.length() > static_cast(_textcols - (_x / (_fontwidth * textSize)))) { - newString = newString.substring(0, _textcols - (_x / (_fontwidth * textSize))); - } + while ((newString.length() > 0) && ((_x + w1) > (res_x + xo))) { + newString.remove(newString.length() - 1); // Cut last character off + _display->getTextBounds(newString, _x, _y, &x1, &y1, &w1, &h1); // Re-count length } } @@ -1654,7 +1834,7 @@ void AdafruitGFX_helper::printText(const char *string, _display->getTextBounds(newString, _x, _y, &x1, &y1, &w0, &h1); // Count length in pixels - for (; ((_x + w0) < _res_x) && _textPrintMode != AdaGFXTextPrintMode::ContinueToNextLine; w0 += w1) { + for (; ((_x + w0) < res_x) && _textPrintMode != AdaGFXTextPrintMode::ContinueToNextLine; w0 += w1) { _display->print(' '); } @@ -2079,8 +2259,12 @@ void AdafruitGFX_helper::getTextMetrics(uint16_t& textcols, fontheight = _fontheight; fontscaling = _fontscaling; heightOffset = _heightOffset; - xpix = _res_x; - ypix = _res_y; + # if ADAGFX_ENABLE_FRAMED_WINDOW + getWindowLimits(xpix, ypix); + # else // if ADAGFX_ENABLE_FRAMED_WINDOW + xpix = _res_x; + ypix = _res_y; + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW } /**************************************************************************** @@ -2099,12 +2283,19 @@ void AdafruitGFX_helper::calculateTextMetrics(uint8_t fontwidth, uint8_t fontheight, int8_t heightOffset, bool isProportional) { + uint16_t res_x = _res_x; + uint16_t res_y = _res_y; + + # if ADAGFX_ENABLE_FRAMED_WINDOW + getWindowLimits(res_x, res_y); + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW + _fontwidth = fontwidth; _fontheight = fontheight; _heightOffset = heightOffset; _isProportional = isProportional; - _textcols = _res_x / (_fontwidth * _fontscaling); - _textrows = _res_y / ((_fontheight + _heightOffset) * _fontscaling); + _textcols = res_x / (_fontwidth * _fontscaling); + _textrows = res_y / ((_fontheight + _heightOffset) * _fontscaling); # ifndef BUILD_NO_DEBUG @@ -2118,9 +2309,9 @@ void AdafruitGFX_helper::calculateTextMetrics(uint8_t fontwidth, log += _trigger; } log += F(" x: "); - log += _res_x; + log += res_x; log += F(", y: "); - log += _res_y; + log += res_y; log += F(", text columns: "); log += _textcols; log += F(" rows: "); @@ -2141,6 +2332,13 @@ void AdafruitGFX_helper::calculateTextMetrics(uint8_t fontwidth, bool AdafruitGFX_helper::invalidCoordinates(int X, int Y, bool colRowMode) { + uint16_t res_x = _res_x; + uint16_t res_y = _res_y; + + # if ADAGFX_ENABLE_FRAMED_WINDOW + getWindowLimits(res_x, res_y); + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW + # ifndef BUILD_NO_DEBUG if (loglevelActiveFor(ADAGFX_LOG_LEVEL)) { @@ -2150,11 +2348,11 @@ bool AdafruitGFX_helper::invalidCoordinates(int X, log += F("invalidCoordinates: X:"); log += X; log += '/'; - log += (colRowMode ? _textcols : _res_x); + log += (colRowMode ? _textcols : res_x); log += F(" Y:"); log += Y; log += '/'; - log += (colRowMode ? _textrows : _res_y); + log += (colRowMode ? _textrows : res_y); addLogMove(ADAGFX_LOG_LEVEL, log); } # endif // ifndef BUILD_NO_DEBUG @@ -2166,20 +2364,24 @@ bool AdafruitGFX_helper::invalidCoordinates(int X, (Y >= 0) && (Y <= _textrows)); } else { if (Y == 0) { // Y == 0: Accept largest x/y size value for x - return !((X >= 0) && (X <= std::max(_res_x, _res_y))); + return !((X >= 0) && (X <= std::max(res_x, res_y))); } else { - return !((X >= 0) && (X <= _res_x) && - (Y >= 0) && (Y <= _res_y)); + return !((X >= 0) && (X <= res_x) && + (Y >= 0) && (Y <= res_y)); } } } # endif // if ADAGFX_ARGUMENT_VALIDATION +/**************************************************************************** + * rotate the display (and all windows) + ***************************************************************************/ void AdafruitGFX_helper::setRotation(uint8_t m) { uint8_t rotation = m & 3; _display->setRotation(m); // Set rotation 0/1/2/3 + _rotation = rotation; switch (rotation) { case 0: @@ -2193,18 +2395,52 @@ void AdafruitGFX_helper::setRotation(uint8_t m) { _res_y = _display_x; break; } + # if ADAGFX_ENABLE_FRAMED_WINDOW + + for (uint8_t i = 0; i < _windows.size(); i++) { // Swap x/y for all matching windows + switch (rotation) { + case 0: // 0 degrees + _windows[i].top_left.x = _windows[i].org_top_left.x; // All original + _windows[i].top_left.y = _windows[i].org_top_left.y; + _windows[i].width_height.x = _windows[i].org_width_height.x; + _windows[i].width_height.y = _windows[i].org_width_height.y; + break; + case 1: // +90 degrees + _windows[i].top_left.x = _windows[i].org_top_left.y; + _windows[i].top_left.y = _display_x - (_windows[i].org_top_left.x + _windows[i].org_width_height.x); + _windows[i].width_height.x = _windows[i].org_width_height.y; // swapped width/height + _windows[i].width_height.y = _windows[i].org_width_height.x; + break; + case 2: // +180 degrees + _windows[i].top_left.x = _display_x - (_windows[i].org_top_left.x + _windows[i].org_width_height.x); + _windows[i].top_left.y = _display_y - (_windows[i].org_top_left.y + _windows[i].org_width_height.y); + _windows[i].width_height.x = _windows[i].org_width_height.x; + _windows[i].width_height.y = _windows[i].org_width_height.y; + break; + case 3: // +270 degrees + _windows[i].top_left.x = _display_y - (_windows[i].org_top_left.y + _windows[i].org_width_height.y); + _windows[i].top_left.y = _windows[i].org_top_left.x; + _windows[i].width_height.x = _windows[i].org_width_height.y; // swapped width/height + _windows[i].width_height.y = _windows[i].org_width_height.x; + break; + } + _windows[i].rotation = rotation; + } + + // logWindows(F("rot ")); // For debugging only + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW calculateTextMetrics(_fontwidth, _fontheight, _heightOffset, _isProportional); } # if ADAGFX_ENABLE_BMP_DISPLAY -/** +/**************************************************************************** * CPA (Copy/paste/adapt) from Adafruit_ImageReader::coreBMP() * Changes: * - No 'load to memory' feature * - No special handling of SD Filesystem/FAT, but File only * - Adds support for non-SPI displays (like NeoPixel Matrix, and possibly I2C displays, once supported) - */ + ***************************************************************************/ bool AdafruitGFX_helper::showBmp(const String& filename, int16_t x, int16_t y) { @@ -2558,4 +2794,180 @@ uint32_t AdafruitGFX_helper::readLE32(void) { # endif // if ADAGFX_ENABLE_BMP_DISPLAY +# if ADAGFX_ENABLE_FRAMED_WINDOW + +/**************************************************************************** + * Check if the requested id is a valid window id + ***************************************************************************/ +bool AdafruitGFX_helper::validWindow(uint8_t windowId) { + return getWindowIndex(windowId) != -1; +} + +/**************************************************************************** + * Select this window id as the default + ***************************************************************************/ +bool AdafruitGFX_helper::selectWindow(uint8_t windowId, + int8_t rotation) { + int16_t result = getWindowIndex(windowId); + + if (result != -1) { + _windowIndex = result; + _window = windowId; + } + return result != -1; +} + +/**************************************************************************** + * Return the index of the windowId in _windows, -1 if not found + ***************************************************************************/ +int16_t AdafruitGFX_helper::getWindowIndex(int16_t windowId) { + size_t result = 0; + + for (auto win = _windows.begin(); win != _windows.end(); win++, result++) { + if ((*win).id == windowId) { + break; + } + } + return result == _windows.size() ? -1 : result; +} + +/**************************************************************************** + * Get the offset for the currently active window + ***************************************************************************/ +void AdafruitGFX_helper::getWindowOffsets(uint16_t& xOffset, + uint16_t& yOffset) { + xOffset = _windows[_windowIndex].top_left.x; + yOffset = _windows[_windowIndex].top_left.y; +} + +/**************************************************************************** + * Get the limits for the currently active window + ***************************************************************************/ +void AdafruitGFX_helper::getWindowLimits(uint16_t& xLimit, + uint16_t& yLimit) { + xLimit = _windows[_windowIndex].width_height.x; + yLimit = _windows[_windowIndex].width_height.y; +} + +/**************************************************************************** + * Define a window and return the ID + ***************************************************************************/ +uint8_t AdafruitGFX_helper::defineWindow(int16_t x, + int16_t y, + int16_t w, + int16_t h, + int16_t windowId, + int8_t rotation) { + int16_t result = getWindowIndex(windowId); + + if (result < 0) { + result = _windows.size(); // previous size + _windows.push_back(tWindowObject()); // add new + + if (windowId < 0) { + windowId = 0; + + for (auto it = _windows.begin(); it != _windows.end(); it++) { + if ((*it).id == windowId) { windowId++; } // Generate a new window id + } + } + _windows[result].id = windowId; + } + _windows[result].top_left.x = x; + _windows[result].top_left.y = y; + _windows[result].width_height.x = w; + _windows[result].width_height.y = h; + + if (rotation >= 0) { + _windows[result].rotation = rotation & 3; + } else { + _windows[result].rotation = _rotation; + } + + // Adjust original coordinate/sizes based on rotation + switch (_windows[result].rotation) { + case 0: // 0 degrees + _windows[result].org_top_left.x = x; // All original + _windows[result].org_top_left.y = y; + _windows[result].org_width_height.x = w; + _windows[result].org_width_height.y = h; + break; + case 1: // +90 degrees + _windows[result].org_top_left.x = _display_x - (y + h); // swapped x/y + _windows[result].org_top_left.y = x; + _windows[result].org_width_height.x = h; // swapped width/height + _windows[result].org_width_height.y = w; + break; + case 2: // +180 degrees + _windows[result].org_top_left.x = _display_x - (x + w); + _windows[result].org_top_left.y = _display_y - (y + h); + _windows[result].org_width_height.x = w; // unchanged + _windows[result].org_width_height.y = h; + break; + case 3: // +270 degrees + _windows[result].org_top_left.x = y; + _windows[result].org_top_left.y = _display_x - (x + w); + _windows[result].org_width_height.x = h; // swapped width/height + _windows[result].org_width_height.y = w; + break; + } + + return _windows[result].id; +} + +/**************************************************************************** + * Remove a window definition + ***************************************************************************/ +bool AdafruitGFX_helper::deleteWindow(uint8_t windowId) { + int16_t result = getWindowIndex(windowId); + + if (result > -1) { + _windows.erase(_windows.begin() + result); + return true; + } + return false; +} + +/**************************************************************************** + * log all current known window definitions + ***************************************************************************/ +void AdafruitGFX_helper::logWindows(const String& prefix) { + # ifndef BUILD_NO_DEBUG + String log; + + log.reserve(50); + + for (auto it = _windows.begin(); it != _windows.end(); it++) { + log.clear(); + log += F("AdaGFX window "); + log += prefix; + log += F(": "); + log += (*it).id; + log += F(", x:"); + log += (*it).top_left.x; + log += F(", y:"); + log += (*it).top_left.y; + log += F(", w:"); + log += (*it).width_height.x; + log += F(", h:"); + log += (*it).width_height.y; + log += F(", rot:"); + log += (*it).rotation; + log += F(", current: "); + log += getWindow(); + log += F(", org x:"); + log += (*it).org_top_left.x; + log += F(", y:"); + log += (*it).org_top_left.y; + log += F(", w:"); + log += (*it).org_width_height.x; + log += F(", h:"); + log += (*it).org_width_height.y; + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // ifndef BUILD_NO_DEBUG +} + +# endif // if ADAGFX_ENABLE_FRAMED_WINDOW + #endif // ifdef PLUGIN_USES_ADAFRUITGFX diff --git a/src/src/Helpers/AdafruitGFX_helper.h b/src/src/Helpers/AdafruitGFX_helper.h index 4aea7d173b..913a1fa8d4 100644 --- a/src/src/Helpers/AdafruitGFX_helper.h +++ b/src/src/Helpers/AdafruitGFX_helper.h @@ -12,6 +12,10 @@ ***************************************************************************/ /************ * Changelog: + * 2022-06-05 tonhuisman: Add support for getting config values: win (current window id), iswin (exists?), width and height (current window), + * (text)length and textheight of a provided text, rot (current rotation), txs (fontscaling), tpm (textprintmode) + * 2022-06-04 tonhuisman: Add Window support for drawing and printing within confined areas (windows) + * Always use exact font calculation for determining allowable text length * 2022-06-02 tonhuisman: Leave out some Notes from UI to save a few bytes from size limited builds * 2022-05-27 tonhuisman: Change btn subcommand to split state and mode arguments, state = 0/1, -2/-1, mode = -2, -1, 0 * 2022-05-27 tonhuisman: Fix a few character mappings in AdaGFXparseTemplate, add surrogates for chars not in font @@ -25,6 +29,7 @@ # include # include # include +# include // Used for bmp support # define BUFPIXELS 200 ///< 200 * 5 = 1000 bytes @@ -49,20 +54,26 @@ // # define ADAGFX_SUPPORT_8and16COLOR 1 // Do we support 8 and 16-Color displays? # endif // ifndef ADAGFX_SUPPORT_8and16COLOR # ifndef ADAGFX_FONTS_INCLUDED -# define ADAGFX_FONTS_INCLUDED 1 // 3 extra fonts, also controls enable/disable of below 8pt/12pt fonts +# define ADAGFX_FONTS_INCLUDED 1 // 3 extra fonts, also controls enable/disable of below 8pt/12pt fonts # endif // ifndef ADAGFX_FONTS_INCLUDED # ifndef ADAGFX_PARSE_SUBCOMMAND -# define ADAGFX_PARSE_SUBCOMMAND 1 // Enable parsing of subcommands (pre/postfix below) to be executed by the helper +# define ADAGFX_PARSE_SUBCOMMAND 1 // Enable parsing of subcommands (pre/postfix below) to be executed by the helper # endif // ifndef ADAGFX_PARSE_SUBCOMMAND # ifndef ADAGFX_ENABLE_EXTRA_CMDS -# define ADAGFX_ENABLE_EXTRA_CMDS 1 // Enable extra subcommands like lm (line-multi) and lmr (line-multi, relative) +# define ADAGFX_ENABLE_EXTRA_CMDS 1 // Enable extra subcommands like lm (line-multi) and lmr (line-multi, relative) # endif // ifndef ADAGFX_ENABLE_EXTRA_CMDS # ifndef ADAGFX_ENABLE_BMP_DISPLAY -# define ADAGFX_ENABLE_BMP_DISPLAY 1 // Enable subcommands for displaying .bmp files on supported displays (color) +# define ADAGFX_ENABLE_BMP_DISPLAY 1 // Enable subcommands for displaying .bmp files on supported displays (color) # endif // ifndef ADAGFX_ENABLE_BMP_DISPLAY # ifndef ADAGFX_ENABLE_BUTTON_DRAW -# define ADAGFX_ENABLE_BUTTON_DRAW 1 // Enable subcommands for displaying button-like shapes +# define ADAGFX_ENABLE_BUTTON_DRAW 1 // Enable subcommands for displaying button-like shapes # endif // ifndef ADAGFX_ENABLE_BUTTON_DRAW +# ifndef ADAGFX_ENABLE_FRAMED_WINDOW +# define ADAGFX_ENABLE_FRAMED_WINDOW 1 // Enable framed window features +# endif // ifndef ADAGFX_ENABLE_BUTTON_DRAW +# ifndef ADAGFX_ENABLE_GET_CONFIG_VALUE +# define ADAGFX_ENABLE_GET_CONFIG_VALUE 1 // Enable getting values features +# endif // ifndef ADAGFX_ENABLE_GET_CONFIG_VALUE // # define ADAGFX_FONTS_EXTRA_8PT_INCLUDED // 5 extra 8pt fonts, should probably only be enabled in a private custom build, adds ~10,4 kB // # define ADAGFX_FONTS_EXTRA_12PT_INCLUDED // 6 extra 12pt fonts, should probably only be enabled in a private custom build, adds ~19,8 kB @@ -114,6 +125,12 @@ // # ifdef ADAGFX_ENABLE_BUTTON_DRAW // # undef ADAGFX_ENABLE_BUTTON_DRAW // # endif // ifdef ADAGFX_ENABLE_BUTTON_DRAW +// # ifdef ADAGFX_ENABLE_FRAMED_WINDOW +// # undef ADAGFX_ENABLE_FRAMED_WINDOW +// # endif // ifdef ADAGFX_ENABLE_FRAMED_WINDOW +// # ifdef ADAGFX_ENABLE_GET_CONFIG_VALUE +// # undef ADAGFX_ENABLE_GET_CONFIG_VALUE +// # endif // ifdef ADAGFX_ENABLE_GET_CONFIG_VALUE # endif // ifdef LIMIT_BUILD_SIZE # ifdef PLUGIN_SET_MAX // Include all fonts in MAX builds @@ -267,6 +284,22 @@ const __FlashStringHelper* toString(Button_layout_e layout); # endif // if ADAGFX_ENABLE_BUTTON_DRAW +# if ADAGFX_ENABLE_FRAMED_WINDOW + +struct tWindowPoint { + uint16_t x = 0; + uint16_t y = 0; +}; +struct tWindowObject { + tWindowPoint top_left; + tWindowPoint width_height; + tWindowPoint org_top_left; + tWindowPoint org_width_height; + uint8_t id = 0u; + int8_t rotation = 0; +}; +# endif // if ADAGFX_ENABLE_FRAMED_WINDOW + class AdafruitGFX_helper; // Forward declaration // Some generic AdafruitGFX_helper support functions @@ -333,7 +366,7 @@ class AdafruitGFX_helper { uint16_t bgcolor = ADAGFX_BLACK, bool useValidation = true, bool textBackFill = false); - # ifdef ADAGFX_ENABLE_BMP_DISPLAY + # if ADAGFX_ENABLE_BMP_DISPLAY AdafruitGFX_helper(Adafruit_SPITFT *display, const String & trigger, uint16_t res_x, @@ -345,13 +378,17 @@ class AdafruitGFX_helper { uint16_t bgcolor = ADAGFX_BLACK, bool useValidation = true, bool textBackFill = false); - # endif // ifdef ADAGFX_ENABLE_BMP_DISPLAY + # endif // if ADAGFX_ENABLE_BMP_DISPLAY virtual ~AdafruitGFX_helper() {} String getFeatures(); bool processCommand(const String& string); // Parse the string for recognized commands and apply them on the graphics display + # if ADAGFX_ENABLE_GET_CONFIG_VALUE + bool pluginGetConfigValue(String& string); // Get a config value from the plugin + # endif // if ADAGFX_ENABLE_GET_CONFIG_VALUE + void printText(const char *string, int X, int Y, @@ -393,11 +430,28 @@ class AdafruitGFX_helper { return trigger.equalsIgnoreCase(ADAGFX_UNIVERSAL_TRIGGER); } - # ifdef ADAGFX_ENABLE_BMP_DISPLAY + # if ADAGFX_ENABLE_BMP_DISPLAY bool showBmp(const String& filename, int16_t x, int16_t y); - # endif // ifdef ADAGFX_ENABLE_BMP_DISPLAY + # endif // if ADAGFX_ENABLE_BMP_DISPLAY + + # if ADAGFX_ENABLE_FRAMED_WINDOW + uint8_t getWindow() { + return _window; + } + + bool validWindow(uint8_t windowId); + bool selectWindow(uint8_t windowId, + int8_t rotation = -1); + uint8_t defineWindow(int16_t x, + int16_t y, + int16_t w, + int16_t h, + int16_t windowId = -1, + int8_t rotation = -1); + bool deleteWindow(uint8_t windowId); + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW private: @@ -440,14 +494,26 @@ class AdafruitGFX_helper { bool _isProportional = false; uint8_t _p095_compensation = 0; bool _columnRowMode = false; + int8_t _rotation = 0; uint16_t _display_x; uint16_t _display_y; - # ifdef ADAGFX_ENABLE_BMP_DISPLAY + # if ADAGFX_ENABLE_BMP_DISPLAY uint16_t readLE16(void); uint32_t readLE32(void); fs::File file; - # endif // ifdef ADAGFX_ENABLE_BMP_DISPLAY + # endif // if ADAGFX_ENABLE_BMP_DISPLAY + # if ADAGFX_ENABLE_FRAMED_WINDOW + int16_t getWindowIndex(int16_t windowId); + void logWindows(const String& prefix = EMPTY_STRING); + void getWindowOffsets(uint16_t& xOffset, + uint16_t& yOffset); + void getWindowLimits(uint16_t& xLimit, + uint16_t& yLimit); + std::vector_windows; + uint8_t _window = 0; // current window + uint8_t _windowIndex = 0; // current window Index + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW }; #endif // ifdef PLUGIN_USES_ADAFRUITGFX From 1e3c936e6cc6616b2380220bb697b5f0da996c0b Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 6 Jun 2022 16:46:44 +0200 Subject: [PATCH 015/113] [P095] [CP] Fix for displaying text on previous location was not overwriting old content Add Text print mode for centered text Extend txtfull subcommand to enable centered text --- src/src/Helpers/AdafruitGFX_helper.cpp | 169 ++++++++++++++++--------- src/src/Helpers/AdafruitGFX_helper.h | 22 ++-- 2 files changed, 121 insertions(+), 70 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index c4a0eb4e78..6b1b97ae1e 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -49,6 +49,7 @@ const __FlashStringHelper* toString(AdaGFXTextPrintMode mode) { case AdaGFXTextPrintMode::ContinueToNextLine: return F("Continue to next line"); case AdaGFXTextPrintMode::TruncateExceedingMessage: return F("Truncate exceeding message"); case AdaGFXTextPrintMode::ClearThenTruncate: return F("Clear then truncate exceeding message"); + case AdaGFXTextPrintMode::TruncateExceedingCentered: return F("Truncate, centered if maxWidth set"); case AdaGFXTextPrintMode::MAX: break; } return F("None"); @@ -126,12 +127,14 @@ void AdaGFXFormTextPrintMode(const __FlashStringHelper *id, const __FlashStringHelper *textModes[textModeCount] = { // Be sure to use all available modes from enum! toString(AdaGFXTextPrintMode::ContinueToNextLine), toString(AdaGFXTextPrintMode::TruncateExceedingMessage), - toString(AdaGFXTextPrintMode::ClearThenTruncate) + toString(AdaGFXTextPrintMode::ClearThenTruncate), + toString(AdaGFXTextPrintMode::TruncateExceedingCentered), }; const int textModeOptions[textModeCount] = { static_cast(AdaGFXTextPrintMode::ContinueToNextLine), static_cast(AdaGFXTextPrintMode::TruncateExceedingMessage), - static_cast(AdaGFXTextPrintMode::ClearThenTruncate) + static_cast(AdaGFXTextPrintMode::ClearThenTruncate), + static_cast(AdaGFXTextPrintMode::TruncateExceedingCentered), }; addFormSelector(F("Text print Mode"), id, textModeCount, textModes, textModeOptions, selectedIndex); @@ -562,7 +565,7 @@ AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_SPITFT *display, # endif // if ADAGFX_ENABLE_BMP_DISPLAY /**************************************************************************** - * Initialize the AdafruitGFX_Helper + * common initialization, called from constructors ***************************************************************************/ void AdafruitGFX_helper::initialize() { _trigger.toLowerCase(); // store trigger in lowercase @@ -735,7 +738,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # endif // if ADAGFX_ARGUMENT_VALIDATION { if (_columnRowMode) { - _display->setCursor(nParams[0] * _fontwidth, nParams[1] * _fontheight); + _display->setCursor(nParams[0] * _fontwidth + _xo, nParams[1] * _fontheight + _yo); } else { _display->setCursor(nParams[0] + _xo - _p095_compensation, nParams[1] + _yo - _p095_compensation); } @@ -751,7 +754,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # endif // if ADAGFX_ARGUMENT_VALIDATION { if (_columnRowMode) { - _display->setCursor(nParams[0] * _fontwidth, nParams[1] * _fontheight); + _display->setCursor(nParams[0] * _fontwidth + _xo, nParams[1] * _fontheight + _yo); } else { _display->setCursor(nParams[0] + _xo, nParams[1] + _yo); } @@ -770,7 +773,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { _display->setTextColor(_fgcolor, _bgcolor); } } - else if (subcommand.equals(F("txs")) && (argCount == 1)) + else if (subcommand.equals(F("txs")) && (argCount == 1)) // txs: Text size = font scaling, 1..10 { if ((nParams[0] >= 0) || (nParams[0] <= 10)) { _fontscaling = nParams[0]; @@ -780,7 +783,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { success = false; } } - else if (subcommand.equals(F("txtfull")) && (argCount >= 3) && (argCount <= 6)) { // txtfull: Text at position, with size and color + else if (subcommand.equals(F("txtfull")) && (argCount >= 3) && (argCount <= 8)) { // txtfull: Text at position, with size and color switch (argCount) { case 3: // single text @@ -851,6 +854,36 @@ bool AdafruitGFX_helper::processCommand(const String& string) { AdaGFXparseColor(sParams[4], _colorDepth)); } break; + case 7: // 7: text + size + color + bkcolor + printmode + case 8: // as 7 but: + maxwidth + + # if ADAGFX_ARGUMENT_VALIDATION + + if (invalidCoordinates(nParams[0] - _p095_compensation, nParams[1] - _p095_compensation, _columnRowMode)) { + success = false; + } else + # endif // if ADAGFX_ARGUMENT_VALIDATION + { + AdaGFXTextPrintMode tmpPrintMode = _textPrintMode; + + if ((nParams[5] >= 0) && (nParams[5] < static_cast(AdaGFXTextPrintMode::MAX))) { + _textPrintMode = static_cast(nParams[5]); + _display->setTextWrap(_textPrintMode == AdaGFXTextPrintMode::ContinueToNextLine); + } + printText(sParams[argCount - 1].c_str(), + nParams[0] + _xo - _p095_compensation, + nParams[1] + _yo - _p095_compensation, + nParams[2], + AdaGFXparseColor(sParams[3], _colorDepth), + AdaGFXparseColor(sParams[4], _colorDepth), + argCount == 8 ? nParams[argCount - 2] : 0); + + if (_textPrintMode != tmpPrintMode) { + _textPrintMode = tmpPrintMode; + _display->setTextWrap(_textPrintMode == AdaGFXTextPrintMode::ContinueToNextLine); + } + } + break; default: success = false; break; @@ -947,10 +980,10 @@ bool AdafruitGFX_helper::processCommand(const String& string) { if (sParams[0].equals(F("sevenseg24"))) { _display->setFont(&Seven_Segment24pt7b); - calculateTextMetrics(21, 42, 35); + calculateTextMetrics(21, 42, 35, true); } else if (sParams[0].equals(F("sevenseg18"))) { _display->setFont(&Seven_Segment18pt7b); - calculateTextMetrics(16, 32, 25); + calculateTextMetrics(16, 33, 26, true); } else if (sParams[0].equals(F("freesans"))) { _display->setFont(&FreeSans9pt7b); calculateTextMetrics(10, 16, 12); @@ -1044,7 +1077,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # endif // ifdef ADAGFX_FONTS_EXTRA_20PT_INCLUDED } else if (sParams[0].equals(F("default"))) { // font,default is always available! _display->setFont(); - calculateTextMetrics(6, 10); + calculateTextMetrics(6, 9); } else { success = false; } @@ -1557,7 +1590,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # if ADAGFX_ARGUMENT_VALIDATION int16_t curWin = getWindow(); - if (curWin != 0) { selectWindow(0); } // Validate against raw window coordinates + if (curWin != 0) { selectWindow(0); } // Validate against raw window coordinates if (argCount == 6) { setRotation(nParams[5]); } // Use requested rotation @@ -1599,6 +1632,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } else { success = false; } + // logWindows(F(" deFwin ")); // Use for debugging only? } } @@ -1756,21 +1790,24 @@ void AdafruitGFX_helper::drawButtonShape(Button_type_e buttonType, /**************************************************************************** * printText: Print text on display at a specific pixel or column/row location ***************************************************************************/ -void AdafruitGFX_helper::printText(const char *string, - int X, - int Y, - unsigned int textSize, - unsigned short color, - unsigned short bkcolor) { +void AdafruitGFX_helper::printText(const char *string, + int16_t X, + int16_t Y, + uint8_t textSize, + uint16_t color, + uint16_t bkcolor, + uint16_t maxWidth) { int16_t _x = X; int16_t _y = Y + (_heightOffset * textSize); uint16_t _w = 0; uint8_t _h = 0; - int16_t x1, y1; - uint16_t w0, w1, h1; + int16_t x1 = 0, y1 = 0; + uint16_t w1 = 0, h1 = 0; + int16_t ot = 0, ob = 0, ol = 0; uint16_t res_x = _res_x; uint16_t res_y = _res_y; uint16_t xo = 0, yo = 0; + String newString = string; # if ADAGFX_ENABLE_FRAMED_WINDOW getWindowLimits(res_x, res_y); @@ -1782,25 +1819,10 @@ void AdafruitGFX_helper::printText(const char *string, _y = (Y * (_fontheight * textSize)) + (_heightOffset * textSize); } - if (_textBackFill && (color != bkcolor)) { // Draw extra lines above text - // Estimate used width - _w = _textPrintMode == AdaGFXTextPrintMode::ContinueToNextLine ? - strlen(string) * _fontwidth * textSize : - ((_textcols + 1) * _fontwidth * textSize) - _x; - - do { - _display->drawLine(_x, _y, _x + _w - 1, _y, bkcolor); - _h++; - _y++; // Shift down entire line - } while (_h < textSize); - } - _display->setCursor(_x, _y); _display->setTextColor(color, bkcolor); _display->setTextSize(textSize); - String newString = string; - if (_textPrintMode != AdaGFXTextPrintMode::ContinueToNextLine) { _display->getTextBounds(newString, _x, _y, &x1, &y1, &w1, &h1); // Count length @@ -1810,43 +1832,70 @@ void AdafruitGFX_helper::printText(const char *string, } } - w1 = 0; + _display->getTextBounds(newString, _x, _y, &x1, &y1, &w1, &h1); // Count length - _display->getTextBounds(F(" "), _x, _y, &x1, &y1, &w1, &h1); + if ((maxWidth > 0) && (_x + maxWidth <= _res_x)) { + res_x = _x + maxWidth; + _w = maxWidth; - if (w1 == 0) { w1 = _fontwidth; } // Some fonts seem to have a 0-wide space, this is an endless loop protection + if ((_textPrintMode == AdaGFXTextPrintMode::TruncateExceedingCentered) && + (maxWidth > w1)) { + ol = (_w - (w1 + 2 * (x1 - _x))) / 2; + } + } else { + _w = w1 + 2 * (x1 - _x); + } - if (_textPrintMode == AdaGFXTextPrintMode::ClearThenTruncate) { // Clear before print - _display->setCursor(_x, _y); - w0 = 0; - uint16_t w2 = 0; + if (_textBackFill && (color != bkcolor)) { // Fill extra space above and below text + ot -= textSize; + ob += textSize; + _y += textSize; + } - _display->getTextBounds(newString, _x, _y, &x1, &y1, &w2, &h1); // Count length in pixels + if ((_textPrintMode == AdaGFXTextPrintMode::ClearThenTruncate) || + (color != bkcolor)) { // Clear before print + # ifndef BUILD_NO_DEBUG - for (; (w0 < w2); w0 += w1) { // Clear previously used text with spaces - _display->print(' '); + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + String log = F("printText: clear: _x:"); + log += _x; + log += F(", ot:"); + log += ot; + log += F(", _y:"); + log += _y; + log += F(", x1:"); + log += x1; + log += F(", y1:"); + log += y1; + log += F(", w1:"); + log += w1; + log += F(", h1:"); + log += h1; + log += F(", ob:"); + log += ob; + log += F(", _res_x/max:"); + log += _res_x; + log += '/'; + log += res_x; + log += F(", str:"); + log += newString; + addLogMove(LOG_LEVEL_DEBUG, log); } - delay(0); - } + # endif // ifndef BUILD_NO_DEBUG - _display->setCursor(_x, _y); - _display->print(newString); + if (bkcolor == color) { bkcolor = _bgcolor; } // To get at least the text readable - _display->getTextBounds(newString, _x, _y, &x1, &y1, &w0, &h1); // Count length in pixels + if (_textPrintMode == AdaGFXTextPrintMode::ClearThenTruncate) { + _display->fillRect(_x + ot, y1, (res_x + xo) - _x, h1 + ob, bkcolor); // Clear text area to right edge of screen + } else { + _display->fillRect(_x + ot, y1, _w, h1 + ob, bkcolor); // Clear text area + } - for (; ((_x + w0) < res_x) && _textPrintMode != AdaGFXTextPrintMode::ContinueToNextLine; w0 += w1) { - _display->print(' '); + delay(0); } - if (_textBackFill && (color != bkcolor)) { // Draw extra lines below text - _y += ((_fontheight - 1) * textSize); - _h = 0; - - do { - _display->drawLine(_x, _y - _h, _x + _w - 1, _y - _h, bkcolor); - _h++; - } while (_h <= textSize); - } + _display->setCursor(_x + ol, _y); // add left offset to center, _y may be updated + _display->print(newString); } /**************************************************************************** diff --git a/src/src/Helpers/AdafruitGFX_helper.h b/src/src/Helpers/AdafruitGFX_helper.h index 913a1fa8d4..2ad29c77a7 100644 --- a/src/src/Helpers/AdafruitGFX_helper.h +++ b/src/src/Helpers/AdafruitGFX_helper.h @@ -208,11 +208,12 @@ enum class AdaGFX7Colors: uint16_t { # endif // if ADAGFX_SUPPORT_7COLOR enum class AdaGFXTextPrintMode : uint8_t { - ContinueToNextLine = 0u, - TruncateExceedingMessage = 1u, - ClearThenTruncate = 2u, // Should have max. 16 options + ContinueToNextLine = 0u, + TruncateExceedingMessage = 1u, + ClearThenTruncate = 2u, + TruncateExceedingCentered = 3u, // Should have max. 16 options - MAX // Keep as last + MAX // Keep as last }; # if ADAGFX_SUPPORT_7COLOR @@ -389,12 +390,13 @@ class AdafruitGFX_helper { bool pluginGetConfigValue(String& string); // Get a config value from the plugin # endif // if ADAGFX_ENABLE_GET_CONFIG_VALUE - void printText(const char *string, - int X, - int Y, - unsigned int textSize = 0, - unsigned short color = ADAGFX_WHITE, - unsigned short bkcolor = ADAGFX_BLACK); + void printText(const char *string, + int16_t X, + int16_t Y, + uint8_t textSize = 0, + uint16_t color = ADAGFX_WHITE, + uint16_t bkcolor = ADAGFX_BLACK, + uint16_t maxWidth = 0); void calculateTextMetrics(uint8_t fontwidth, uint8_t fontheight, int8_t heightOffset = 0, From 863fb2a7f5a2ab5cf0f0720bc82ec0538b056e2b Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 6 Jun 2022 16:50:37 +0200 Subject: [PATCH 016/113] [TouchHandler] Move PLUGIN_WRITE and _GET_CONFIG_VALUE to helper, add getters for state and enabled, add toggle subcommand, handle multiple objects for on, off and toggle --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 186 ++++++++++++++++++++++- src/src/Helpers/ESPEasy_TouchHandler.h | 29 ++++ 2 files changed, 214 insertions(+), 1 deletion(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 00a060faa2..6aa0c370c8 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -13,7 +13,7 @@ const __FlashStringHelper* toString(Touch_action_e action) { case Touch_action_e::IncrementGroup: return F("Next Group"); case Touch_action_e::DecrementGroup: return F("Previous Group"); case Touch_action_e::IncrementPage: return F("Next Page (+10)"); - case Touch_action_e::DecrementPage: return F("Previous page (-10)"); + case Touch_action_e::DecrementPage: return F("Previous Page (-10)"); case Touch_action_e::TouchAction_MAX: break; } return F("Unsupported!"); @@ -360,6 +360,22 @@ bool ESPEasy_TouchHandler::setTouchObjectState(struct EventStruct *event, return success; } +/** + * Get the enabled/disabled state of an object. + */ +int8_t ESPEasy_TouchHandler::getTouchObjectState(struct EventStruct *event, + const String & touchObject) { + if (touchObject.isEmpty()) { return false; } + int8_t result = -1; + + int8_t objectNr = getTouchObjectIndex(event, touchObject); + + if (objectNr > -1) { + result = bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED) ? 1 : 0; + } + return result; +} + /** * Set the on/off state of an enabled touch-button object. Will generate an event if so configured. */ @@ -401,6 +417,24 @@ bool ESPEasy_TouchHandler::setTouchButtonOnOff(struct EventStruct *event, return success; } +/** + * Get the on/off state of an enabled touch-button object. + */ +int8_t ESPEasy_TouchHandler::getTouchButtonOnOff(struct EventStruct *event, + const String & touchObject) { + if (touchObject.isEmpty()) { return false; } + int8_t result = -1; // invalid button + + int8_t objectNr = getTouchObjectIndex(event, touchObject, true); + + if ((objectNr > -1) + && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED) + && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTON)) { + result = TouchObjects[objectNr].TouchStates && !bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_INVERTED) ? 1 : 0; + } + return result; +} + /** * mode: -2 = clear buttons in group, -3 = clear all buttongroups, -1 = draw buttons in group, 0 = initialize buttons */ @@ -1446,6 +1480,156 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, return success; } +/** + * Parse and execute the plugin commands + */ +bool ESPEasy_TouchHandler::plugin_write(struct EventStruct *event, + const String & string) { + bool success = false; + String command; + String subcommand; + String arguments; + uint8_t arg = 3; + + arguments.reserve(24); + command = parseString(string, 1); + subcommand = parseString(string, 2); + + if (command.equals(F("touch"))) { + # ifdef TOUCH_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("TOUCH PLUGIN_WRITE arguments Par1:"); + log += event->Par1; + log += F(", 2: "); + log += event->Par2; + log += F(", 3: "); + log += event->Par3; + log += F(", 4: "); + log += event->Par4; + log += F(", string: "); + log += string; + addLog(LOG_LEVEL_INFO, log); + } + # endif // ifdef TOUCH_DEBUG + + if (subcommand.equals(F("enable"))) { // touch,enable,[,...] : Enable disabled objectname(s) + arguments = parseString(string, arg); + + while (!arguments.isEmpty()) { + success |= setTouchObjectState(event, arguments, true); + arg++; + arguments = parseString(string, arg); + } + } else if (subcommand.equals(F("disable"))) { // touch,disable,[,...] : Disable enabled objectname(s) + arguments = parseString(string, arg); + + while (!arguments.isEmpty()) { + success |= setTouchObjectState(event, arguments, false); + arg++; + arguments = parseString(string, arg); + } + } else if (subcommand.equals(F("on"))) { // touch,on,[,...] : Switch TouchButton(s) on + arguments = parseString(string, arg); + + while (!arguments.isEmpty()) { + success |= setTouchButtonOnOff(event, arguments, true); + arg++; + arguments = parseString(string, arg); + } + } else if (subcommand.equals(F("off"))) { // touch,off,[,...] : Switch TouchButton(s) off + arguments = parseString(string, arg); + + while (!arguments.isEmpty()) { + success |= setTouchButtonOnOff(event, arguments, false); + arg++; + arguments = parseString(string, arg); + } + } else if (subcommand.equals(F("toggle"))) { // touch,toggle,[,...] : Switch TouchButton(s) to the other state + arguments = parseString(string, arg); + + while (!arguments.isEmpty()) { + int8_t state = getTouchButtonOnOff(event, arguments); + + if (state > -1) { + success |= setTouchButtonOnOff(event, arguments, state == 0); + } + arg++; + arguments = parseString(string, arg); + } + } else if (subcommand.equals(F("setgrp"))) { // touch,setgrp, : Activate button group + success = setButtonGroup(event, event->Par2); + } else if (subcommand.equals(F("incgrp"))) { // touch,incgrp : increment group and Activate + success = incrementButtonGroup(event); + } else if (subcommand.equals(F("decgrp"))) { // touch,decgrp : Decrement group and Activate + success = decrementButtonGroup(event); + } else if (subcommand.equals(F("incpage"))) { // touch,incpage : increment page and Activate + success = incrementButtonPage(event); + } else if (subcommand.equals(F("decpage"))) { // touch,decpage : Decrement page and Activate + success = decrementButtonPage(event); + } else if (subcommand.equals(F("updatebutton"))) { // touch,updatebutton,[,[,]] : Update a button + arguments = parseString(string, 3); + + // Check for a valid button name or number, returns a 0-based index + int index = getTouchObjectIndex(event, arguments, true); + + if (index > -1) { + bool hasPar3 = !parseString(string, 4).isEmpty(); + bool hasPar4 = !parseString(string, 5).isEmpty(); + + if (hasPar4) { + success = displayButton(event, index, event->Par3, event->Par4); + } else if (hasPar3) { + success = displayButton(event, index, event->Par3); + } else { + success = displayButton(event, index); // Use default argument values + } + } + } + } + return success; +} + +/** + * Handle getting config values from plugin/handler + */ +bool ESPEasy_TouchHandler::plugin_get_config_value(struct EventStruct *event, + String & string) { + bool success = false; + String command = parseString(string, 1); + + if (command == F("buttongroup")) { + string = getButtonGroup(); + success = true; + } else if (command == F("hasgroup")) { + int group; // We'll be ignoring group 0 if there are multiple button groups + + if (validIntFromString(parseString(string, 2), group)) { + string = validButtonGroup(group, true) ? 1 : 0; + success = true; + } else { + string = '0'; // invalid number = false + } + } else if (command == F("enabled")) { + String arguments = parseStringKeepCase(string, 2); + int8_t enabled = getTouchObjectState(event, arguments); + + if (enabled > -1) { + string = enabled; + success = true; + } + } else if (command == F("state")) { + String arguments = parseStringKeepCase(string, 2); + int8_t state = getTouchButtonOnOff(event, arguments); + + if (state > -1) { + string = state; + success = true; + } + } + return success; +} + /** * generate an event for a touch object * When a display is configured add x,y coordinate, width,height of the object, objectIndex, and TaskIndex of display diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index 0623a4b2f9..3bdb027df0 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -11,6 +11,11 @@ /***** * Changelog: + * 2022-06-06 tonhuisman: Move PLUGIN_WRITE handling from P123 + * Move PLUGIN_GET_CONFIG_VALUE handling from P123. + * Add getters for on/off (state) and (enabled), and matching GET_CONFIG_VALUE commands + * Add toggle subcommand for switching enabled on/off buttons to the other state + * Extend on, off, toggle subcommands to support a list of objects * 2022-06-03 tonhuisman: Change default ON color to blue (from green, too bright/bad contrast with white captions) * Add options for auto Enable/Disable arrow buttons and invert pgup/pgdn * Bugfix: Also apply debouncing to non-button objects @@ -21,6 +26,22 @@ * 2022-05-23 tonhuisman: Created from refactoring P123 Touch object handling into ESPEasy_TouchHandler *********************************************************************************************************************/ +/** + * Commands supported: + * ------------------- + * touch,enable,[,...] : Enable disabled objectname(s) + * touch,disable,[,...] : Disable enabled objectname(s) + * touch,on,[,...] : Switch TouchButton(s) on (must be enabled) + * touch,off,[,...] : Switch TouchButton(s) off (must be enabled) + * touch,toggle,[,...] : Switch TouchButton(s) to the other state (must be enabled) + * touch,setgrp, : Switch to button group + * touch,incgrp : Switch to next button group + * touch,decgrp : Switch to previous button group + * touch,incpage : Switch to next button group page (+10) + * touch,decpage : Switch to previous button group page (-10) + * touch,updatebutton,[,[,]] : Update a button by name or number + */ + # define TOUCH_DEBUG // Additional debugging information # define TOUCH_USE_TOOLTIPS // Enable tooltips in UI @@ -202,9 +223,13 @@ class ESPEasy_TouchHandler { bool setTouchObjectState(struct EventStruct *event, const String & touchObject, bool state); + int8_t getTouchObjectState(struct EventStruct *event, + const String & touchObject); bool setTouchButtonOnOff(struct EventStruct *event, const String & touchObject, bool state); + int8_t getTouchButtonOnOff(struct EventStruct *event, + const String & touchObject); bool plugin_webform_load(struct EventStruct *event); bool plugin_webform_save(struct EventStruct *event); bool plugin_fifty_per_second(struct EventStruct *event, @@ -215,6 +240,10 @@ class ESPEasy_TouchHandler { int16_t rx, int16_t ry, int16_t z); + bool plugin_write(struct EventStruct *event, + const String & string); + bool plugin_get_config_value(struct EventStruct *event, + String & string); int16_t getButtonGroup() { return _buttonGroup; } From a64b3892c2b7bcd5af41fc8d351fcdf69c9c748d Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 6 Jun 2022 16:53:07 +0200 Subject: [PATCH 017/113] [P123] Refactor PLUGIN_WRITE and _GET_CONFIG_VALUE to TouchHandler, minor improvements --- src/_P123_FT62x6Touch.ino | 43 +++--------- src/src/PluginStructs/P123_data_struct.cpp | 82 +++++----------------- src/src/PluginStructs/P123_data_struct.h | 2 + 3 files changed, 31 insertions(+), 96 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index 875eecc7bd..b5b093d3ad 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -6,6 +6,8 @@ /** * Changelog: + * 2022-06-06 tonhuisman: Move PLUGIN_WRITE handling mostly to ESPEasy_TouchHandler (only rot and flip subcommands remain) + * Move PLUGIN_GET_CONFIG_VALUE handling to ESPEasy_TouchHandler * 2022-05-29 tonhuisman: Extend enable,disable subcommands to support a list of objects * 2022-05-28 tonhuisman: Add incpage and decpage subcommands that + and - 10 to the current buttongroup * 2022-05-26 tonhuisman: Add touch,updatebutton command @@ -27,16 +29,8 @@ * ------------------- * touch,rot,<0..3> : Set rotation to 0(0), 90(1), 180(2), 270(3) degrees * touch,flip,<0|1> : Set rotation normal(0) or flipped by 180 degrees(1) - * touch,enable,[,...] : Enable disabled objectname(s) - * touch,disable,[,...] : Disable enabled objectname(s) - * touch,on, : Switch a TouchButton on (must be enabled) - * touch,off, : Switch a TouchButton off (must be enabled) - * touch,setgrp, : Switch to button group - * touch,incgrp : Switch to next button group - * touch,decgrp : Switch to previous button group - * touch,incpage : Switch to next button group page (+10) - * touch,decpage : Switch to previous button group page (-10) - * touch,updatebutton,[,[,]] : Update a button by name or number + * + * Other commands: see ESPEasy_TouchHandler.h */ #define PLUGIN_123 @@ -208,14 +202,12 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WRITE: { - { - P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); + P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); - if (nullptr == P123_data) { - return success; - } - success = P123_data->plugin_write(event, string); + if (nullptr == P123_data) { + return success; } + success = P123_data->plugin_write(event, string); break; } @@ -235,23 +227,8 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) { P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); - if (nullptr == P123_data) { - return success; - } - String command = parseString(string, 1); - - if (command == F("buttongroup")) { - string = P123_data->getButtonGroup(); - success = true; - } else if (command == F("hasgroup")) { - int group; // We'll be ignoring group 0 if there are multiple button groups - - if (validIntFromString(parseString(string, 2), group)) { - string = P123_data->validButtonGroup(group, true) ? 1 : 0; - success = true; - } else { - string = '0'; // invalid number - } + if (nullptr != P123_data) { + return P123_data->plugin_get_config_value(event, string); } break; } diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index f291797894..e98201342d 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -75,8 +75,8 @@ bool P123_data_struct::init(struct EventStruct *event) { * mode: -2 = clear buttons in group, -3 = clear all buttongroups, -1 = draw buttons in group, 0 = initialize buttons */ void P123_data_struct::displayButtonGroup(struct EventStruct *event, - int16_t buttonGroup, - int8_t mode) { + int16_t buttonGroup, + int8_t mode) { touchHandler->displayButtonGroup(event, buttonGroup, mode); } @@ -84,9 +84,9 @@ void P123_data_struct::displayButtonGroup(struct EventStruct *event, * (Re)Display a button */ bool P123_data_struct::displayButton(struct EventStruct *event, - int8_t buttonNr, - int16_t buttonGroup, - int8_t mode) { + int8_t buttonNr, + int16_t buttonGroup, + int8_t mode) { return touchHandler->displayButton(event, buttonNr, buttonGroup, mode); } @@ -112,16 +112,14 @@ bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { } /** - * Parse and execute the plugin commands + * Parse and execute the plugin commands, delegated to ESPEasy_TouchHandler */ bool P123_data_struct::plugin_write(struct EventStruct *event, const String & string) { bool success = false; String command; String subcommand; - String arguments; - arguments.reserve(24); command = parseString(string, 1); subcommand = parseString(string, 2); @@ -141,64 +139,14 @@ bool P123_data_struct::plugin_write(struct EventStruct *event, } # endif // ifdef PLUGIN_123_DEBUG - if (subcommand.equals(F("rot"))) { // touch,rot,<0..3> : Set rotation to 0, 90, 180, 270 degrees + if (subcommand.equals(F("rot"))) { // touch,rot,<0..3> : Set rotation to 0, 90, 180, 270 degrees setRotation(static_cast(event->Par2 % 4)); success = true; - } else if (subcommand.equals(F("flip"))) { // touch,flip,<0|1> : Flip rotation by 0 or 180 degrees + } else if (subcommand.equals(F("flip"))) { // touch,flip,<0|1> : Flip rotation by 0 or 180 degrees setRotationFlipped(event->Par2 > 0); success = true; - } else if (subcommand.equals(F("enable"))) { // touch,enable,[,...] : Enable disabled objectname(s) - uint8_t arg = 3; - arguments = parseString(string, arg); - - while (!arguments.isEmpty()) { - success |= setTouchObjectState(event, arguments, true); - arg++; - arguments = parseString(string, arg); - } - } else if (subcommand.equals(F("disable"))) { // touch,disable,[,...] : Disable enabled objectname(s) - uint8_t arg = 3; - arguments = parseString(string, arg); - - while (!arguments.isEmpty()) { - success |= setTouchObjectState(event, arguments, false); - arg++; - arguments = parseString(string, arg); - } - } else if (subcommand.equals(F("on"))) { // touch,on, : Switch a TouchButton on - arguments = parseString(string, 3); - success = setTouchButtonOnOff(event, arguments, true); - } else if (subcommand.equals(F("off"))) { // touch,off, : Switch a TouchButton off - arguments = parseString(string, 3); - success = setTouchButtonOnOff(event, arguments, false); - } else if (subcommand.equals(F("setgrp"))) { // touch,setgrp, : Activate button group - success = setButtonGroup(event, event->Par2); - } else if (subcommand.equals(F("incgrp"))) { // touch,incgrp : increment group and Activate - success = incrementButtonGroup(event); - } else if (subcommand.equals(F("decgrp"))) { // touch,decgrp : Decrement group and Activate - success = decrementButtonGroup(event); - } else if (subcommand.equals(F("incpage"))) { // touch,incpage : increment page and Activate - success = incrementButtonPage(event); - } else if (subcommand.equals(F("decpage"))) { // touch,decpage : Decrement page and Activate - success = decrementButtonPage(event); - } else if (subcommand.equals(F("updatebutton"))) { // touch,updatebutton,[,[,]] : Update a button - arguments = parseString(string, 3); - - // Check for a valid button name or number, returns a 0-based index - int index = getTouchObjectIndex(event, arguments, true); - - if (index > -1) { - bool hasPar3 = !parseString(string, 4).isEmpty(); - bool hasPar4 = !parseString(string, 5).isEmpty(); - - if (hasPar4) { - success = displayButton(event, index, event->Par3, event->Par4); - } else if (hasPar3) { - success = displayButton(event, index, event->Par3); - } else { - success = displayButton(event, index); // Use default argument values - } - } + } else { // Rest of the commands handled by + success = touchHandler->plugin_write(event, string); } } return success; @@ -226,6 +174,14 @@ bool P123_data_struct::plugin_fifty_per_second(struct EventStruct *event) { return success; } +/** + * Handle getting config values, mostly delegated to ESPEasy_TouchHandler + */ +bool P123_data_struct::plugin_get_config_value(struct EventStruct *event, + String & string) { + return touchHandler->plugin_get_config_value(event, string); +} + /** * Load the touch objects from the settings, and initialize then properly where needed. */ @@ -424,7 +380,7 @@ bool P123_data_struct::validButtonGroup(int16_t buttonGroup, * Set the desired button group, must be between the minimum and maximum found values */ bool P123_data_struct::setButtonGroup(struct EventStruct *event, - int16_t buttonGroup) { + int16_t buttonGroup) { return touchHandler->setButtonGroup(event, buttonGroup); } diff --git a/src/src/PluginStructs/P123_data_struct.h b/src/src/PluginStructs/P123_data_struct.h index 7997fd0f91..590657f5fb 100644 --- a/src/src/PluginStructs/P123_data_struct.h +++ b/src/src/PluginStructs/P123_data_struct.h @@ -53,6 +53,8 @@ struct P123_data_struct : public PluginTaskData_base bool plugin_write(struct EventStruct *event, const String & string); bool plugin_fifty_per_second(struct EventStruct *event); + bool plugin_get_config_value(struct EventStruct *event, + String & string); void loadTouchObjects(struct EventStruct *event); bool touched(); From 6b77e1dd79fb5fdafcc2e9ae88841916c93a6648 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 6 Jun 2022 17:02:42 +0200 Subject: [PATCH 018/113] [AdaGFX] Remove unused variable --- src/src/Helpers/AdafruitGFX_helper.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index 6b1b97ae1e..b4e90d80cc 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -1800,7 +1800,6 @@ void AdafruitGFX_helper::printText(const char *string, int16_t _x = X; int16_t _y = Y + (_heightOffset * textSize); uint16_t _w = 0; - uint8_t _h = 0; int16_t x1 = 0, y1 = 0; uint16_t w1 = 0, h1 = 0; int16_t ot = 0, ob = 0, ol = 0; From bcc772bab857a13b3b3eb89dac9fd13d70cace5d Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 6 Jun 2022 21:51:16 +0200 Subject: [PATCH 019/113] [AdaGFX] Process like template for length and textheight values --- src/src/Helpers/AdafruitGFX_helper.cpp | 4 ++-- src/src/Helpers/AdafruitGFX_helper.h | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index b4e90d80cc..975272b3ba 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -1590,7 +1590,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # if ADAGFX_ARGUMENT_VALIDATION int16_t curWin = getWindow(); - if (curWin != 0) { selectWindow(0); } // Validate against raw window coordinates + if (curWin != 0) { selectWindow(0); } // Validate against raw window coordinates if (argCount == 6) { setRotation(nParams[5]); } // Use requested rotation @@ -1693,7 +1693,7 @@ bool AdafruitGFX_helper::pluginGetConfigValue(String& string) { (command == F("textheight"))) { int16_t x1, y1; uint16_t w1, h1; - String newString = parseStringToEndKeepCase(string, 2); + String newString = AdaGFXparseTemplate(parseStringToEndKeepCase(string, 2), 0); _display->getTextBounds(newString, 0, 0, &x1, &y1, &w1, &h1); // Count length and height if (command == F("length")) { diff --git a/src/src/Helpers/AdafruitGFX_helper.h b/src/src/Helpers/AdafruitGFX_helper.h index 2ad29c77a7..1b0bbadb1c 100644 --- a/src/src/Helpers/AdafruitGFX_helper.h +++ b/src/src/Helpers/AdafruitGFX_helper.h @@ -12,6 +12,7 @@ ***************************************************************************/ /************ * Changelog: + * 2022-06-06 tonhuisman: Process any special characters for lenght and textheight values for correct sizing * 2022-06-05 tonhuisman: Add support for getting config values: win (current window id), iswin (exists?), width and height (current window), * (text)length and textheight of a provided text, rot (current rotation), txs (fontscaling), tpm (textprintmode) * 2022-06-04 tonhuisman: Add Window support for drawing and printing within confined areas (windows) From 0cc64f1708c9c9a9c7666973363fb7c630b86edf Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 6 Jun 2022 21:52:14 +0200 Subject: [PATCH 020/113] [AdaGFX] Add/update documentation for window commands and config values --- docs/source/Plugin/AdaGFX_commands.repl | 62 ++++++++++++++++ docs/source/Plugin/AdaGFX_values.repl | 94 +++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 docs/source/Plugin/AdaGFX_values.repl diff --git a/docs/source/Plugin/AdaGFX_commands.repl b/docs/source/Plugin/AdaGFX_commands.repl index 1c928095bd..8f1967df1a 100644 --- a/docs/source/Plugin/AdaGFX_commands.repl +++ b/docs/source/Plugin/AdaGFX_commands.repl @@ -33,6 +33,7 @@ * 0 : Continue to next line (wrap text onto the next line) * 1 : Truncate exceeding message (cut-off text that won't fit on the screen width) * 2 : Clear then truncate exceeding message (Clear to width of screen, then print the message) + * 3 : Truncate, centered if maxWidth set (like 1, but allows to center when used via ``txtfull`` subcommand) " " ``,txt,`` @@ -265,3 +266,64 @@ NB: This command wil only *draw* a button, it will not respond to any action. The action is usually provided by a touch screen like :ref:`P099_page` and :ref:`P123_page`. " + " + ``,defwin,,,,,[,]`` + + Example: (Display Task is named ``st7796``, using trigger ``st77xx``) + + .. code:: none + + on window1 do + if %eventvalue1|1%=1 // on = default + st77xx,defwin,20,50,390,219,1,1 // Window 1, using rotation 1 + st77xx,win,1 // Select window + st77xx,rot,1 // Select rotation + st77xx,clear // Clear area + st77xx,r,0,0,[st7796#width],[st7796#height],white // Draw a white border + st77xx,win,0 // Return to Window 0 + else + asyncevent,removewindow=1 // Remove Window 1 + endif + endon + on removewindow do + if [st7796#iswin,%eventvalue1|-1%]=1 // Does the Window exist? + st77xx,win,%eventvalue1% // Select window + st77xx,clear // Clear area + st77xx,delwin,%eventvalue1% // Delete Window + st77xx,win,0 // Return to Window 0 + endif + endon + + "," + Define Window. + + When enabled, the Window feature of the plugin can be used to define custom areas of the screen as a window. Any printing and drawing can then be limited to that area, having a coordinate system from ``0,0`` to ``,``. + + Window 0 is the default window, having the native size of the display. The coordinates, width and height for the ``defwin`` subcommand must be specified in pixels. + + * ``x,y``: The top-left coordinate of the window, according to the native resolution of the screen. This coordinate will become the ``0,0`` coordinate of the new window. + + * ``w,h``: The width and height of the window. + + * ``windowId``: The Id of the new window, can be any value from 1 to 255. When re-using an Id, the previous definition will be overwritten. + + * ``rotation``: Optional. The rotation, identical to the ``rot`` subcommand, that the window dimensions are related to. When not specified, the current rotation setting will be used. Internally, the dimensions are transformed to the dimensions for rotation 0, and on each change of ``rot``, all windows will be re-calculated to have the dimensions corresponding with the new rotation setting. + " + " + ``,delwin,`` + "," + Delete Window. + + When no longer needed, a window definition can be deleted using this subcommand. + + * ``windowId``: The window Id of a previously defined window. + " + " + ``,win,`` + "," + Select Window. + + To make a window active, is must be selected using this subcommand. + + * ``windowId``: The window Id of a previously defined window. + " diff --git a/docs/source/Plugin/AdaGFX_values.repl b/docs/source/Plugin/AdaGFX_values.repl new file mode 100644 index 0000000000..ecfd4797f6 --- /dev/null +++ b/docs/source/Plugin/AdaGFX_values.repl @@ -0,0 +1,94 @@ +.. csv-table:: + :escape: ^ + :widths: 20, 30 + + " + Generic variables, available for all ``AdafruitGFX_Helper`` enabled plugins, currently: :ref:`P095_page`, :ref:`P096_page`, :ref:`P116_page` and :ref:`P131_page`. + "," + Generic notes: + + * If an argument has comma's or spaces, then that part should be 'wrapped' in either double quotes ``^"``, single quotes ``^'`` or back-ticks ``^```. + * The ```` part is the name of the Device task. + * True(1)/False(0) values can optionally return -1 to indicate an invalid request, like a missing Window Id. + * All sizes, lengths etc. are returned in pixels. + " + " + ``[#win]`` + "," + Get the currently active Window Id, expected range: 0..255. + " + " + ``[#iswin,]`` + "," + Is the request Window Id valid, expected result: 1 = true, 0 = false. + " + " + ``[#width]`` + "," + Get the width of the currently active Window and rotation, expected range 0... + " + " + ``[#height]`` + "," + Get the height of the currently active Window and rotation, expected range 0... + " + " + ``[#length,^"^"]`` + "," + Get the length of the text in pixels for the current font and text scaling. Needs quotes if text contains space(s), comma(s) or quote(s). + " + " + ``[#textheight,^"^"]`` + "," + Get the height of the text in pixels for the current font and text scaling. Needs quotes if the text contains space(s), comma(s) or quote(s). + " + " + ``[#rot]`` + "," + Get the currently active rotation, expected range 0..3 (0 = 0 degrees, 1 = +90 degrees, 2 = +180 degrees, 3 = +270 degrees). + " + " + ``[#txs]`` + "," + Get the currently active text scaling, expected range 1..10, limited to the max. font scaling allowed for the display. + " + " + ``[#tpm]`` + "," + Get the currently active text print mode, expected range 0..3, see the ``tpm`` subcommand for details. + " + +.. csv-table:: + :escape: ^ + :widths: 20, 10 + + " + Example rules for centering a value (time) in a window: + + .. code:: none + + on centertime do // NB: Comments & extra spaces should be removed to reduce rules size! + if [st7796#iswin,%eventvalue1|2%]=1 // default window: 2 + let,120,[st7796#win] // store current window + st77xx,win,%eventvalue1|2% // switch to window + let,121,[st7796#txs] // store textscaling + st77xx,txs,3 // set text scaling + let,122,[st7796#rot] // store rotation + st77xx,rot,%eventvalue2|0% // set rotation, default: 0 + let,123,([st7796#width]-[st7796#length,%systm_hm%])/2 // (width - textlength)/2 + let,124,([st7796#height]-[st7796#textheight,%systm_hm%])/2 // (height - textheight)/2 + st77xx,txtfull,[int#123],[int#124],3,red,black,%systm_hm% // Display time red on black + st77xx,rot,%v122% // restore rotation + st77xx,txs,%v121% // restore text scaling + st77xx,win,%v120% // restore window + endif + endon + on Clock#Time=All,**:** do + asyncevent,centertime=2 // Update the display every minute + endon + + "," + Display Task is named ``st7796``, using trigger ``st77xx`` + + Usage: ``asyncevent,centertime[=[,]]`` + " From 8ce3690709e2af10b53e9637a3d9f9148734df7d Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 7 Jun 2022 22:55:14 +0200 Subject: [PATCH 021/113] [AdaGFX] Code improvements, initialization, const, move window offset into printText() --- src/src/Helpers/AdafruitGFX_helper.cpp | 333 +++++++++++++------------ src/src/Helpers/AdafruitGFX_helper.h | 126 +++++----- 2 files changed, 237 insertions(+), 222 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index 975272b3ba..81a0d8020c 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -44,7 +44,7 @@ /****************************************************************************************** * get the display text for a 'text print mode' enum value *****************************************************************************************/ -const __FlashStringHelper* toString(AdaGFXTextPrintMode mode) { +const __FlashStringHelper* toString(const AdaGFXTextPrintMode mode) { switch (mode) { case AdaGFXTextPrintMode::ContinueToNextLine: return F("Continue to next line"); case AdaGFXTextPrintMode::TruncateExceedingMessage: return F("Truncate exceeding message"); @@ -58,7 +58,7 @@ const __FlashStringHelper* toString(AdaGFXTextPrintMode mode) { /****************************************************************************************** * get the display text for a color depth enum value *****************************************************************************************/ -const __FlashStringHelper* toString(AdaGFXColorDepth colorDepth) { +const __FlashStringHelper* toString(const AdaGFXColorDepth colorDepth) { switch (colorDepth) { case AdaGFXColorDepth::Monochrome: return F("Monochrome"); case AdaGFXColorDepth::BlackWhiteRed: return F("Monochrome + 1 color"); @@ -80,7 +80,7 @@ const __FlashStringHelper* toString(AdaGFXColorDepth colorDepth) { /****************************************************************************************** * get the display text for a button type enum value *****************************************************************************************/ -const __FlashStringHelper* toString(Button_type_e button) { +const __FlashStringHelper* toString(const Button_type_e button) { switch (button) { case Button_type_e::None: return F("None"); case Button_type_e::Square: return F("Square"); @@ -98,7 +98,7 @@ const __FlashStringHelper* toString(Button_type_e button) { /****************************************************************************************** * get the display text for a button layout enum value *****************************************************************************************/ -const __FlashStringHelper* toString(Button_layout_e layout) { +const __FlashStringHelper* toString(const Button_layout_e layout) { switch (layout) { case Button_layout_e::CenterAligned: return F("Centered"); case Button_layout_e::LeftAligned: return F("Left-aligned"); @@ -241,9 +241,8 @@ void AdaGFXFormForeAndBackColors(const __FlashStringHelper *foregroundId, const __FlashStringHelper *backgroundId, uint16_t backgroundColor, AdaGFXColorDepth colorDepth) { - String color; + String color = AdaGFXcolorToString(foregroundColor, colorDepth); - color = AdaGFXcolorToString(foregroundColor, colorDepth); addFormTextBox(F("Foreground color"), foregroundId, color, 11); color = AdaGFXcolorToString(backgroundColor, colorDepth); addFormTextBox(F("Background color"), backgroundId, color, 11); @@ -303,7 +302,7 @@ void AdaGFXFormFontScaling(const __FlashStringHelper *fontScalingId, * AdaGFXparseTemplate: Replace variables and adjust unicode special characters to Adafruit font ***************************************************************************/ String AdaGFXparseTemplate(const String & tmpString, - uint8_t lineSize, + const uint8_t lineSize, AdafruitGFX_helper *gfxHelper) { # if ADAGFX_PARSE_SUBCOMMAND @@ -519,17 +518,17 @@ String AdaGFXparseTemplate(const String & tmpString, /**************************************************************************** * parameterized constructors ***************************************************************************/ -AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_GFX *display, - const String & trigger, - uint16_t res_x, - uint16_t res_y, - AdaGFXColorDepth colorDepth, - AdaGFXTextPrintMode textPrintMode, - uint8_t fontscaling, - uint16_t fgcolor, - uint16_t bgcolor, - bool useValidation, - bool textBackFill) +AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_GFX *display, + const String & trigger, + const uint16_t res_x, + const uint16_t res_y, + const AdaGFXColorDepth colorDepth, + const AdaGFXTextPrintMode textPrintMode, + const uint8_t fontscaling, + const uint16_t fgcolor, + const uint16_t bgcolor, + const bool useValidation, + const bool textBackFill) : _display(display), _trigger(trigger), _res_x(res_x), _res_y(res_y), _colorDepth(colorDepth), _textPrintMode(textPrintMode), _fontscaling(fontscaling), _fgcolor(fgcolor), _bgcolor(bgcolor), _useValidation(useValidation), _textBackFill(textBackFill) @@ -539,17 +538,17 @@ AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_GFX *display, } # if ADAGFX_ENABLE_BMP_DISPLAY -AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_SPITFT *display, - const String & trigger, - uint16_t res_x, - uint16_t res_y, - AdaGFXColorDepth colorDepth, - AdaGFXTextPrintMode textPrintMode, - uint8_t fontscaling, - uint16_t fgcolor, - uint16_t bgcolor, - bool useValidation, - bool textBackFill) +AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_SPITFT *display, + const String & trigger, + const uint16_t res_x, + const uint16_t res_y, + const AdaGFXColorDepth colorDepth, + const AdaGFXTextPrintMode textPrintMode, + const uint8_t fontscaling, + const uint16_t fgcolor, + const uint16_t bgcolor, + const bool useValidation, + const bool textBackFill) : _tft(display), _trigger(trigger), _res_x(res_x), _res_y(res_y), _colorDepth(colorDepth), _textPrintMode(textPrintMode), _fontscaling(fontscaling), _fgcolor(fgcolor), _bgcolor(bgcolor), _useValidation(useValidation), _textBackFill(textBackFill) @@ -658,11 +657,12 @@ bool AdafruitGFX_helper::processCommand(const String& string) { if ((nullptr == _display) || _trigger.isEmpty()) { return success; } - String cmd = parseString(string, 1); // lower case + String cmd = parseString(string, 1); // lower case String subcommand = parseString(string, 2); - uint16_t res_x = _res_x; - uint16_t res_y = _res_y; - uint16_t _xo = 0, _yo = 0; + uint16_t res_x = _res_x; + uint16_t res_y = _res_y; + uint16_t _xo = 0; + uint16_t _yo = 0; # if ADAGFX_ENABLE_FRAMED_WINDOW getWindowLimits(res_x, res_y); @@ -795,8 +795,8 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # endif // if ADAGFX_ARGUMENT_VALIDATION { printText(sParams[2].c_str(), - nParams[0] + _xo - _p095_compensation, - nParams[1] + _yo - _p095_compensation, + nParams[0] - _p095_compensation, + nParams[1] - _p095_compensation, _fontscaling, _fgcolor, _fgcolor); // transparent bg @@ -812,8 +812,8 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # endif // if ADAGFX_ARGUMENT_VALIDATION { printText(sParams[3].c_str(), - nParams[0] + _xo - _p095_compensation, - nParams[1] + _yo - _p095_compensation, + nParams[0] - _p095_compensation, + nParams[1] - _p095_compensation, nParams[2], _fgcolor, _fgcolor); // transparent bg @@ -830,8 +830,8 @@ bool AdafruitGFX_helper::processCommand(const String& string) { { uint16_t color = AdaGFXparseColor(sParams[3], _colorDepth); printText(sParams[4].c_str(), - nParams[0] + _xo - _p095_compensation, - nParams[1] + _yo - _p095_compensation, + nParams[0] - _p095_compensation, + nParams[1] - _p095_compensation, nParams[2], color, color); // transparent bg @@ -847,8 +847,8 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # endif // if ADAGFX_ARGUMENT_VALIDATION { printText(sParams[5].c_str(), - nParams[0] + _xo - _p095_compensation, - nParams[1] + _yo - _p095_compensation, + nParams[0] - _p095_compensation, + nParams[1] - _p095_compensation, nParams[2], AdaGFXparseColor(sParams[3], _colorDepth), AdaGFXparseColor(sParams[4], _colorDepth)); @@ -871,8 +871,8 @@ bool AdafruitGFX_helper::processCommand(const String& string) { _display->setTextWrap(_textPrintMode == AdaGFXTextPrintMode::ContinueToNextLine); } printText(sParams[argCount - 1].c_str(), - nParams[0] + _xo - _p095_compensation, - nParams[1] + _yo - _p095_compensation, + nParams[0] - _p095_compensation, + nParams[1] - _p095_compensation, nParams[2], AdaGFXparseColor(sParams[3], _colorDepth), AdaGFXparseColor(sParams[4], _colorDepth), @@ -928,10 +928,10 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # if ADAGFX_USE_ASCIITABLE else if (subcommand.equals(F("asciitable"))) // Show ASCII table { - String line; - int16_t start = 0x80 + (argCount >= 1 && nParams[0] >= -4 && nParams[0] < 4 ? nParams[0] * 0x20 : 0); - uint8_t scale = (argCount == 2 && nParams[1] > 0 && nParams[1] <= 10 ? nParams[1] : 2); - uint8_t currentScale = _fontscaling; + String line; + const int16_t start = 0x80 + (argCount >= 1 && nParams[0] >= -4 && nParams[0] < 4 ? nParams[0] * 0x20 : 0); + const uint8_t scale = (argCount == 2 && nParams[1] > 0 && nParams[1] <= 10 ? nParams[1] : 2); + const uint8_t currentScale = _fontscaling; if (_fontscaling != scale) { // Set fontscaling _fontscaling = scale; @@ -940,8 +940,8 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } line.reserve(_textcols); _display->setCursor(0, 0); - int16_t row = 0; - bool colMode = _columnRowMode; + int16_t row = 0; + const bool colMode = _columnRowMode; _columnRowMode = true; for (int16_t i = start; i <= 0xFF && row < _textrows; i++) { @@ -1586,11 +1586,11 @@ bool AdafruitGFX_helper::processCommand(const String& string) { success = selectWindow(nParams[0], nParams[1]); } else if (subcommand.equals(F("defwin")) && (argCount >= 5) && (argCount <= 6)) { // defwin: define window - int8_t rot = _rotation; + const int8_t rot = _rotation; # if ADAGFX_ARGUMENT_VALIDATION - int16_t curWin = getWindow(); + const int16_t curWin = getWindow(); - if (curWin != 0) { selectWindow(0); } // Validate against raw window coordinates + if (curWin != 0) { selectWindow(0); } // Validate against raw window coordinates if (argCount == 6) { setRotation(nParams[5]); } // Use requested rotation @@ -1790,27 +1790,34 @@ void AdafruitGFX_helper::drawButtonShape(Button_type_e buttonType, /**************************************************************************** * printText: Print text on display at a specific pixel or column/row location ***************************************************************************/ -void AdafruitGFX_helper::printText(const char *string, - int16_t X, - int16_t Y, - uint8_t textSize, - uint16_t color, - uint16_t bkcolor, - uint16_t maxWidth) { - int16_t _x = X; - int16_t _y = Y + (_heightOffset * textSize); - uint16_t _w = 0; - int16_t x1 = 0, y1 = 0; - uint16_t w1 = 0, h1 = 0; - int16_t ot = 0, ob = 0, ol = 0; - uint16_t res_x = _res_x; - uint16_t res_y = _res_y; - uint16_t xo = 0, yo = 0; +void AdafruitGFX_helper::printText(const char *string, + const int16_t X, + const int16_t Y, + const uint8_t textSize, + const uint16_t color, + uint16_t bkcolor, + const uint16_t maxWidth) { + int16_t _x = X; + int16_t _y = Y + (_heightOffset * textSize); + uint16_t _w = 0; + int16_t xText = 0; + int16_t yText = 0; + uint16_t wText = 0; + uint16_t hText = 0; + int16_t oTop = 0; + int16_t oBottom = 0; + int16_t oLeft = 0; + uint16_t res_x = _res_x; + uint16_t res_y = _res_y; + uint16_t xOffset = 0; + uint16_t yOffset = 0; String newString = string; # if ADAGFX_ENABLE_FRAMED_WINDOW getWindowLimits(res_x, res_y); - getWindowOffsets(xo, yo); + getWindowOffsets(xOffset, yOffset); + _x += xOffset; + _y += yOffset; # endif // if ADAGFX_ENABLE_FRAMED_WINDOW if (_columnRowMode) { @@ -1823,32 +1830,32 @@ void AdafruitGFX_helper::printText(const char *string, _display->setTextSize(textSize); if (_textPrintMode != AdaGFXTextPrintMode::ContinueToNextLine) { - _display->getTextBounds(newString, _x, _y, &x1, &y1, &w1, &h1); // Count length + _display->getTextBounds(newString, _x, _y, &xText, &yText, &wText, &hText); // Count length - while ((newString.length() > 0) && ((_x + w1) > (res_x + xo))) { - newString.remove(newString.length() - 1); // Cut last character off - _display->getTextBounds(newString, _x, _y, &x1, &y1, &w1, &h1); // Re-count length + while ((newString.length() > 0) && (((_x - xOffset) + wText) > res_x)) { + newString.remove(newString.length() - 1); // Cut last character off + _display->getTextBounds(newString, _x, _y, &xText, &yText, &wText, &hText); // Re-count length } } - _display->getTextBounds(newString, _x, _y, &x1, &y1, &w1, &h1); // Count length + _display->getTextBounds(newString, _x, _y, &xText, &yText, &wText, &hText); // Count length - if ((maxWidth > 0) && (_x + maxWidth <= _res_x)) { - res_x = _x + maxWidth; + if ((maxWidth > 0) && ((_x - xOffset) + maxWidth <= res_x)) { + res_x = (_x - xOffset) + maxWidth; _w = maxWidth; if ((_textPrintMode == AdaGFXTextPrintMode::TruncateExceedingCentered) && - (maxWidth > w1)) { - ol = (_w - (w1 + 2 * (x1 - _x))) / 2; + (maxWidth > wText)) { + oLeft = (_w - (wText + 2 * (xText - _x))) / 2; } } else { - _w = w1 + 2 * (x1 - _x); + _w = wText + 2 * (xText - _x); } if (_textBackFill && (color != bkcolor)) { // Fill extra space above and below text - ot -= textSize; - ob += textSize; - _y += textSize; + oTop -= textSize; + oBottom += textSize; + _y += textSize; } if ((_textPrintMode == AdaGFXTextPrintMode::ClearThenTruncate) || @@ -1858,20 +1865,20 @@ void AdafruitGFX_helper::printText(const char *string, if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log = F("printText: clear: _x:"); log += _x; - log += F(", ot:"); - log += ot; + log += F(", oTop:"); + log += oTop; log += F(", _y:"); log += _y; - log += F(", x1:"); - log += x1; - log += F(", y1:"); - log += y1; - log += F(", w1:"); - log += w1; - log += F(", h1:"); - log += h1; - log += F(", ob:"); - log += ob; + log += F(", xTx:"); + log += xText; + log += F(", yTx:"); + log += yText; + log += F(", wTx:"); + log += wText; + log += F(", hTx:"); + log += hText; + log += F(", oBot:"); + log += oBottom; log += F(", _res_x/max:"); log += _res_x; log += '/'; @@ -1885,24 +1892,24 @@ void AdafruitGFX_helper::printText(const char *string, if (bkcolor == color) { bkcolor = _bgcolor; } // To get at least the text readable if (_textPrintMode == AdaGFXTextPrintMode::ClearThenTruncate) { - _display->fillRect(_x + ot, y1, (res_x + xo) - _x, h1 + ob, bkcolor); // Clear text area to right edge of screen + _display->fillRect(_x + oTop, yText, res_x - (_x - xOffset), hText + oBottom, bkcolor); // Clear text area to right edge of screen } else { - _display->fillRect(_x + ot, y1, _w, h1 + ob, bkcolor); // Clear text area + _display->fillRect(_x + oTop, yText, _w, hText + oBottom, bkcolor); // Clear text area } delay(0); } - _display->setCursor(_x + ol, _y); // add left offset to center, _y may be updated + _display->setCursor(_x + oLeft, _y); // add left offset to center, _y may be updated _display->print(newString); } /**************************************************************************** * color565: convert r, g, b colors to rgb565 (by bit-shifting) ***************************************************************************/ -uint16_t color565(uint8_t red, - uint8_t green, - uint8_t blue) { +uint16_t color565(const uint8_t red, + const uint8_t green, + const uint8_t blue) { return ((red & 0xF8) << 8) | ((green & 0xFC) << 3) | (blue >> 3); } @@ -1916,9 +1923,9 @@ uint16_t color565(uint8_t red, // Param [in] colorDepth: The requiresed color depth, default: FullColor // param [in] defaultWhite: Return White color if empty, default: true // return : color (default ADAGFX_WHITE) -uint16_t AdaGFXparseColor(String & s, - AdaGFXColorDepth colorDepth, - bool emptyIsBlack) { +uint16_t AdaGFXparseColor(String & s, + const AdaGFXColorDepth colorDepth, + const bool emptyIsBlack) { s.toLowerCase(); int32_t result = -1; // No result yet @@ -2006,7 +2013,7 @@ uint16_t AdaGFXparseColor(String & s, // Parse default hex #RRGGBB string (must be 6 hex nibbles!) if ((result == -1) && (s.length() == 7) && (s[0] == '#')) { // convrt to long value in base16, then split up into r, g, b values - uint32_t number = hexToUL(&s[1]); + const uint32_t number = hexToUL(&s[1]); // uint32_t r = number >> 16 & 0xFF; // uint32_t g = number >> 8 & 0xFF; @@ -2084,7 +2091,7 @@ void AdaGFXaddHtmlDataListColorOptionValue(uint16_t color, * Generate a html 'datalist' of the colors available for selected colorDepth, with id provided ****************************************************************************************/ void AdaGFXHtmlColorDepthDataList(const __FlashStringHelper *id, - AdaGFXColorDepth colorDepth) { + const AdaGFXColorDepth colorDepth) { addHtml(F("")); @@ -2147,9 +2154,9 @@ void AdaGFXHtmlColorDepthDataList(const __FlashStringHelper *id, /***************************************************************************************** * Convert an RGB565 color (number) to it's name or the #rgb565 hex string, based on depth ****************************************************************************************/ -String AdaGFXcolorToString(uint16_t color, - AdaGFXColorDepth colorDepth, - bool blackIsEmpty) { +String AdaGFXcolorToString(const uint16_t color, + const AdaGFXColorDepth colorDepth, + bool blackIsEmpty) { String result = AdaGFXcolorToString_internal(color, colorDepth, blackIsEmpty); if (result.equals(F("*"))) { @@ -2240,18 +2247,17 @@ const __FlashStringHelper* AdaGFXcolorToString_internal(uint16_t color, * AdaGFXrgb565ToColor7: Convert a rgb565 color to the 7 colors supported by 7-color eInk displays * Borrowed from https://github.com/ZinggJM/GxEPD2 color7() routine ***************************************************************************/ -uint16_t AdaGFXrgb565ToColor7(uint16_t color) { - uint16_t cv7 = static_cast(AdaGFX7Colors::ADAGFX7C_WHITE); // Default = white - - uint16_t red = (color & 0xF800); - uint16_t green = (color & 0x07E0) << 5; - uint16_t blue = (color & 0x001F) << 11; +uint16_t AdaGFXrgb565ToColor7(const uint16_t color) { + const uint16_t red = (color & 0xF800); + const uint16_t green = (color & 0x07E0) << 5; + const uint16_t blue = (color & 0x001F) << 11; + uint16_t cv7 = static_cast(AdaGFX7Colors::ADAGFX7C_WHITE); // Default = white if ((red < 0x8000) && (green < 0x8000) && (blue < 0x8000)) { - cv7 = static_cast(AdaGFX7Colors::ADAGFX7C_BLACK); // black + cv7 = static_cast(AdaGFX7Colors::ADAGFX7C_BLACK); // black } else if ((red >= 0x8000) && (green >= 0x8000) && (blue >= 0x8000)) { - cv7 = static_cast(AdaGFX7Colors::ADAGFX7C_WHITE); // white + cv7 = static_cast(AdaGFX7Colors::ADAGFX7C_WHITE); // white } else if ((red >= 0x8000) && (blue >= 0x8000)) { if (red > blue) { @@ -2327,10 +2333,10 @@ void AdafruitGFX_helper::getColors(uint16_t& fgcolor, /**************************************************************************** * calculateTextMetrics: Recalculate the text mertics based on supplied font parameters ***************************************************************************/ -void AdafruitGFX_helper::calculateTextMetrics(uint8_t fontwidth, - uint8_t fontheight, - int8_t heightOffset, - bool isProportional) { +void AdafruitGFX_helper::calculateTextMetrics(const uint8_t fontwidth, + const uint8_t fontheight, + const int8_t heightOffset, + const bool isProportional) { uint16_t res_x = _res_x; uint16_t res_y = _res_y; @@ -2377,9 +2383,9 @@ void AdafruitGFX_helper::calculateTextMetrics(uint8_t fontwidth, * If Y == 0 then X is allowed the max. value of the display size. * *** Returns TRUE when invalid !! *** ***************************************************************************/ -bool AdafruitGFX_helper::invalidCoordinates(int X, - int Y, - bool colRowMode) { +bool AdafruitGFX_helper::invalidCoordinates(const int X, + const int Y, + const bool colRowMode) { uint16_t res_x = _res_x; uint16_t res_y = _res_y; @@ -2426,7 +2432,7 @@ bool AdafruitGFX_helper::invalidCoordinates(int X, * rotate the display (and all windows) ***************************************************************************/ void AdafruitGFX_helper::setRotation(uint8_t m) { - uint8_t rotation = m & 3; + const uint8_t rotation = m & 3; _display->setRotation(m); // Set rotation 0/1/2/3 _rotation = rotation; @@ -2492,36 +2498,43 @@ void AdafruitGFX_helper::setRotation(uint8_t m) { bool AdafruitGFX_helper::showBmp(const String& filename, int16_t x, int16_t y) { - bool transact = true; // Enable transaction support to work proper with SD czrd, when enabled - bool status = false; // IMAGE_SUCCESS on valid file + uint32_t offset; // Start of image data in file + uint32_t headerSize; // Indicates BMP version + uint32_t compression = 0; // BMP compression mode + uint32_t colors = 0; // Number of colors in palette + uint32_t rowSize; // >bmpWidth if scanline padding + uint8_t sdbuf[3 * BUFPIXELS]; // BMP read buf (R+G+B/pixel) + + uint32_t destidx = 0; + uint32_t bmpPos = 0; // Next pixel position in file + int bmpWidth; // BMP width & height in pixels + int bmpHeight; + int loadWidth; + int loadHeight; // Region being loaded (clipped) + int loadX; + int loadY; // " + int row; // Current pixel pos. + int col; + uint16_t *quantized = NULL; // 16-bit 5/6/5 color palette uint16_t tftbuf[BUFPIXELS]; - uint16_t *dest = tftbuf; // TFT working buffer, or NULL if to canvas - uint32_t offset; // Start of image data in file - uint32_t headerSize; // Indicates BMP version - uint8_t planes; // BMP planes - uint8_t depth; // BMP bit depth - uint32_t compression = 0; // BMP compression mode - uint32_t colors = 0; // Number of colors in palette - uint16_t *quantized = NULL; // 16-bit 5/6/5 color palette - uint32_t rowSize; // >bmpWidth if scanline padding - uint8_t sdbuf[3 * BUFPIXELS]; // BMP read buf (R+G+B/pixel) - int bmpWidth, bmpHeight; // BMP width & height in pixels + uint16_t *dest = tftbuf; // TFT working buffer, or NULL if to canvas + int16_t drow = 0; + int16_t dcol = 0; + uint8_t planes; // BMP planes + uint8_t depth; // BMP bit depth + uint8_t r; // Current pixel colors + uint8_t g; + uint8_t b; + uint8_t bitIn = 0; // Bit number for 1-bit data in # if ((3 * BUFPIXELS) <= 255) - uint8_t srcidx = sizeof sdbuf; // Current position in sdbuf + uint8_t srcidx = sizeof sdbuf; // Current position in sdbuf # else // if ((3 * BUFPIXELS) <= 255) uint16_t srcidx = sizeof sdbuf; # endif // if ((3 * BUFPIXELS) <= 255) - uint32_t destidx = 0; - bool flip = true; // BMP is stored bottom-to-top - uint32_t bmpPos = 0; // Next pixel position in file - int loadWidth, loadHeight, // Region being loaded (clipped) - loadX, loadY; // " - int row, col; // Current pixel pos. - uint8_t r, g, b; // Current pixel color - uint8_t bitIn = 0; // Bit number for 1-bit data in - int16_t drow = 0; - int16_t dcol = 0; + bool flip = true; // BMP is stored bottom-to-top + bool transact = true; // Enable transaction support to work proper with SD czrd, when enabled + bool status = false; // IMAGE_SUCCESS on valid file bool canTransact = (nullptr != _tft); @@ -2854,9 +2867,9 @@ bool AdafruitGFX_helper::validWindow(uint8_t windowId) { /**************************************************************************** * Select this window id as the default ***************************************************************************/ -bool AdafruitGFX_helper::selectWindow(uint8_t windowId, - int8_t rotation) { - int16_t result = getWindowIndex(windowId); +bool AdafruitGFX_helper::selectWindow(const uint8_t windowId, + const int8_t rotation) { + const int16_t result = getWindowIndex(windowId); if (result != -1) { _windowIndex = result; @@ -2868,7 +2881,7 @@ bool AdafruitGFX_helper::selectWindow(uint8_t windowId, /**************************************************************************** * Return the index of the windowId in _windows, -1 if not found ***************************************************************************/ -int16_t AdafruitGFX_helper::getWindowIndex(int16_t windowId) { +int16_t AdafruitGFX_helper::getWindowIndex(const int16_t windowId) { size_t result = 0; for (auto win = _windows.begin(); win != _windows.end(); win++, result++) { @@ -2900,12 +2913,12 @@ void AdafruitGFX_helper::getWindowLimits(uint16_t& xLimit, /**************************************************************************** * Define a window and return the ID ***************************************************************************/ -uint8_t AdafruitGFX_helper::defineWindow(int16_t x, - int16_t y, - int16_t w, - int16_t h, - int16_t windowId, - int8_t rotation) { +uint8_t AdafruitGFX_helper::defineWindow(const int16_t x, + const int16_t y, + const int16_t w, + const int16_t h, + int16_t windowId, + const int8_t rotation) { int16_t result = getWindowIndex(windowId); if (result < 0) { @@ -2966,8 +2979,8 @@ uint8_t AdafruitGFX_helper::defineWindow(int16_t x, /**************************************************************************** * Remove a window definition ***************************************************************************/ -bool AdafruitGFX_helper::deleteWindow(uint8_t windowId) { - int16_t result = getWindowIndex(windowId); +bool AdafruitGFX_helper::deleteWindow(const uint8_t windowId) { + const int16_t result = getWindowIndex(windowId); if (result > -1) { _windows.erase(_windows.begin() + result); diff --git a/src/src/Helpers/AdafruitGFX_helper.h b/src/src/Helpers/AdafruitGFX_helper.h index 1b0bbadb1c..a21b618782 100644 --- a/src/src/Helpers/AdafruitGFX_helper.h +++ b/src/src/Helpers/AdafruitGFX_helper.h @@ -12,8 +12,9 @@ ***************************************************************************/ /************ * Changelog: + * 2022-06-07 tonhuisman: Code improvements in initialization, move offset calculation to printText() function * 2022-06-06 tonhuisman: Process any special characters for lenght and textheight values for correct sizing - * 2022-06-05 tonhuisman: Add support for getting config values: win (current window id), iswin (exists?), width and height (current window), + * 2022-06-05 tonhuisman: Add support for getting config values: win (current window id), iswin (exists?), width & height (current window), * (text)length and textheight of a provided text, rot (current rotation), txs (fontscaling), tpm (textprintmode) * 2022-06-04 tonhuisman: Add Window support for drawing and printing within confined areas (windows) * Always use exact font calculation for determining allowable text length @@ -281,8 +282,8 @@ enum class Button_layout_e : uint8_t { Alignment_MAX = 11u // options-count, max possible values: 16 }; -const __FlashStringHelper* toString(Button_type_e button); -const __FlashStringHelper* toString(Button_layout_e layout); +const __FlashStringHelper* toString(const Button_type_e button); +const __FlashStringHelper* toString(const Button_layout_e layout); # endif // if ADAGFX_ENABLE_BUTTON_DRAW @@ -305,8 +306,8 @@ struct tWindowObject { class AdafruitGFX_helper; // Forward declaration // Some generic AdafruitGFX_helper support functions -const __FlashStringHelper* toString(AdaGFXTextPrintMode mode); -const __FlashStringHelper* toString(AdaGFXColorDepth colorDepth); +const __FlashStringHelper* toString(const AdaGFXTextPrintMode mode); +const __FlashStringHelper* toString(const AdaGFXColorDepth colorDepth); void AdaGFXFormTextPrintMode(const __FlashStringHelper *id, uint8_t selectedIndex); void AdaGFXFormColorDepth(const __FlashStringHelper *id, @@ -339,47 +340,48 @@ void AdaGFXFormFontScaling(const __FlashStringHelper *fontScalingId, uint8_t fontScaling, uint8_t maxScale = 10); String AdaGFXparseTemplate(const String & tmpString, - uint8_t lineSize, + const uint8_t lineSize, AdafruitGFX_helper *gfxHelper = nullptr); -uint16_t AdaGFXparseColor(String & s, - AdaGFXColorDepth colorDepth = AdaGFXColorDepth::FullColor, - bool emptyIsBlack = false); // Parse either a color by name, 6 digit hex rrggbb color, or 1..4 digit - // #rgb565 color (hex with # prefix) +uint16_t AdaGFXparseColor(String & s, + const AdaGFXColorDepth colorDepth = AdaGFXColorDepth::FullColor, + const bool emptyIsBlack = false); // Parse either a color by name, 6 digit hex rrggbb color, + // or 1..4 digit + // #rgb565 color (hex with # prefix) void AdaGFXHtmlColorDepthDataList(const __FlashStringHelper *id, - AdaGFXColorDepth colorDepth); -String AdaGFXcolorToString(uint16_t color, - AdaGFXColorDepth colorDepth = AdaGFXColorDepth::FullColor, - bool blackIsEmpty = false); + const AdaGFXColorDepth colorDepth); +String AdaGFXcolorToString(const uint16_t color, + const AdaGFXColorDepth colorDepth = AdaGFXColorDepth::FullColor, + bool blackIsEmpty = false); # if ADAGFX_SUPPORT_7COLOR -uint16_t AdaGFXrgb565ToColor7(uint16_t color); // Convert rgb565 color to 7-color +uint16_t AdaGFXrgb565ToColor7(const uint16_t color); // Convert rgb565 color to 7-color # endif // if ADAGFX_SUPPORT_7COLOR class AdafruitGFX_helper { public: - AdafruitGFX_helper(Adafruit_GFX *display, - const String & trigger, - uint16_t res_x, - uint16_t res_y, - AdaGFXColorDepth colorDepth = AdaGFXColorDepth::FullColor, - AdaGFXTextPrintMode textPrintMode = AdaGFXTextPrintMode::ContinueToNextLine, - uint8_t fontscaling = 1, - uint16_t fgcolor = ADAGFX_WHITE, - uint16_t bgcolor = ADAGFX_BLACK, - bool useValidation = true, - bool textBackFill = false); + AdafruitGFX_helper(Adafruit_GFX *display, + const String & trigger, + const uint16_t res_x, + const uint16_t res_y, + const AdaGFXColorDepth colorDepth = AdaGFXColorDepth::FullColor, + const AdaGFXTextPrintMode textPrintMode = AdaGFXTextPrintMode::ContinueToNextLine, + const uint8_t fontscaling = 1, + const uint16_t fgcolor = ADAGFX_WHITE, + const uint16_t bgcolor = ADAGFX_BLACK, + const bool useValidation = true, + const bool textBackFill = false); # if ADAGFX_ENABLE_BMP_DISPLAY - AdafruitGFX_helper(Adafruit_SPITFT *display, - const String & trigger, - uint16_t res_x, - uint16_t res_y, - AdaGFXColorDepth colorDepth = AdaGFXColorDepth::FullColor, - AdaGFXTextPrintMode textPrintMode = AdaGFXTextPrintMode::ContinueToNextLine, - uint8_t fontscaling = 1, - uint16_t fgcolor = ADAGFX_WHITE, - uint16_t bgcolor = ADAGFX_BLACK, - bool useValidation = true, - bool textBackFill = false); + AdafruitGFX_helper(Adafruit_SPITFT *display, + const String & trigger, + const uint16_t res_x, + const uint16_t res_y, + const AdaGFXColorDepth colorDepth = AdaGFXColorDepth::FullColor, + const AdaGFXTextPrintMode textPrintMode = AdaGFXTextPrintMode::ContinueToNextLine, + const uint8_t fontscaling = 1, + const uint16_t fgcolor = ADAGFX_WHITE, + const uint16_t bgcolor = ADAGFX_BLACK, + const bool useValidation = true, + const bool textBackFill = false); # endif // if ADAGFX_ENABLE_BMP_DISPLAY virtual ~AdafruitGFX_helper() {} @@ -391,17 +393,17 @@ class AdafruitGFX_helper { bool pluginGetConfigValue(String& string); // Get a config value from the plugin # endif // if ADAGFX_ENABLE_GET_CONFIG_VALUE - void printText(const char *string, - int16_t X, - int16_t Y, - uint8_t textSize = 0, - uint16_t color = ADAGFX_WHITE, - uint16_t bkcolor = ADAGFX_BLACK, - uint16_t maxWidth = 0); - void calculateTextMetrics(uint8_t fontwidth, - uint8_t fontheight, - int8_t heightOffset = 0, - bool isProportional = false); + void printText(const char *string, + const int16_t X, + const int16_t Y, + const uint8_t textSize = 0, + const uint16_t color = ADAGFX_WHITE, + uint16_t bkcolor = ADAGFX_BLACK, + const uint16_t maxWidth = 0); + void calculateTextMetrics(const uint8_t fontwidth, + const uint8_t fontheight, + const int8_t heightOffset = 0, + const bool isProportional = false); void getTextMetrics(uint16_t& textcols, uint16_t& textrows, uint8_t & fontwidth, @@ -444,16 +446,16 @@ class AdafruitGFX_helper { return _window; } - bool validWindow(uint8_t windowId); - bool selectWindow(uint8_t windowId, - int8_t rotation = -1); - uint8_t defineWindow(int16_t x, - int16_t y, - int16_t w, - int16_t h, - int16_t windowId = -1, - int8_t rotation = -1); - bool deleteWindow(uint8_t windowId); + bool validWindow(const uint8_t windowId); + bool selectWindow(const uint8_t windowId, + const int8_t rotation = -1); + uint8_t defineWindow(const int16_t x, + const int16_t y, + const int16_t w, + const int16_t h, + int16_t windowId = -1, + const int8_t rotation = -1); + bool deleteWindow(const uint8_t windowId); # endif // if ADAGFX_ENABLE_FRAMED_WINDOW private: @@ -461,9 +463,9 @@ class AdafruitGFX_helper { void initialize(); # if ADAGFX_ARGUMENT_VALIDATION - bool invalidCoordinates(int X, - int Y, - bool colRowMode = false); + bool invalidCoordinates(const int X, + const int Y, + const bool colRowMode = false); # endif // if ADAGFX_ARGUMENT_VALIDATION # if ADAGFX_ENABLE_BUTTON_DRAW void drawButtonShape(Button_type_e buttonType, @@ -507,7 +509,7 @@ class AdafruitGFX_helper { fs::File file; # endif // if ADAGFX_ENABLE_BMP_DISPLAY # if ADAGFX_ENABLE_FRAMED_WINDOW - int16_t getWindowIndex(int16_t windowId); + int16_t getWindowIndex(const int16_t windowId); void logWindows(const String& prefix = EMPTY_STRING); void getWindowOffsets(uint16_t& xOffset, uint16_t& yOffset); From 555e30d261ccd8d57d7af83e85821709a3e650f5 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 8 Jun 2022 22:27:35 +0200 Subject: [PATCH 022/113] [AdaGFX] Code improvements as suggested by feedback --- src/src/Helpers/AdafruitGFX_helper.cpp | 134 ++++++++++++------------- src/src/Helpers/AdafruitGFX_helper.h | 118 +++++++++++----------- 2 files changed, 126 insertions(+), 126 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index 81a0d8020c..4bd99f0f07 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -44,7 +44,7 @@ /****************************************************************************************** * get the display text for a 'text print mode' enum value *****************************************************************************************/ -const __FlashStringHelper* toString(const AdaGFXTextPrintMode mode) { +const __FlashStringHelper* toString(const AdaGFXTextPrintMode& mode) { switch (mode) { case AdaGFXTextPrintMode::ContinueToNextLine: return F("Continue to next line"); case AdaGFXTextPrintMode::TruncateExceedingMessage: return F("Truncate exceeding message"); @@ -58,7 +58,7 @@ const __FlashStringHelper* toString(const AdaGFXTextPrintMode mode) { /****************************************************************************************** * get the display text for a color depth enum value *****************************************************************************************/ -const __FlashStringHelper* toString(const AdaGFXColorDepth colorDepth) { +const __FlashStringHelper* toString(const AdaGFXColorDepth& colorDepth) { switch (colorDepth) { case AdaGFXColorDepth::Monochrome: return F("Monochrome"); case AdaGFXColorDepth::BlackWhiteRed: return F("Monochrome + 1 color"); @@ -518,17 +518,17 @@ String AdaGFXparseTemplate(const String & tmpString, /**************************************************************************** * parameterized constructors ***************************************************************************/ -AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_GFX *display, - const String & trigger, - const uint16_t res_x, - const uint16_t res_y, - const AdaGFXColorDepth colorDepth, - const AdaGFXTextPrintMode textPrintMode, - const uint8_t fontscaling, - const uint16_t fgcolor, - const uint16_t bgcolor, - const bool useValidation, - const bool textBackFill) +AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_GFX *display, + const String & trigger, + const uint16_t res_x, + const uint16_t res_y, + const AdaGFXColorDepth & colorDepth, + const AdaGFXTextPrintMode& textPrintMode, + const uint8_t fontscaling, + const uint16_t fgcolor, + const uint16_t bgcolor, + const bool useValidation, + const bool textBackFill) : _display(display), _trigger(trigger), _res_x(res_x), _res_y(res_y), _colorDepth(colorDepth), _textPrintMode(textPrintMode), _fontscaling(fontscaling), _fgcolor(fgcolor), _bgcolor(bgcolor), _useValidation(useValidation), _textBackFill(textBackFill) @@ -538,17 +538,17 @@ AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_GFX *display, } # if ADAGFX_ENABLE_BMP_DISPLAY -AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_SPITFT *display, - const String & trigger, - const uint16_t res_x, - const uint16_t res_y, - const AdaGFXColorDepth colorDepth, - const AdaGFXTextPrintMode textPrintMode, - const uint8_t fontscaling, - const uint16_t fgcolor, - const uint16_t bgcolor, - const bool useValidation, - const bool textBackFill) +AdafruitGFX_helper::AdafruitGFX_helper(Adafruit_SPITFT *display, + const String & trigger, + const uint16_t res_x, + const uint16_t res_y, + const AdaGFXColorDepth & colorDepth, + const AdaGFXTextPrintMode& textPrintMode, + const uint8_t fontscaling, + const uint16_t fgcolor, + const uint16_t bgcolor, + const bool useValidation, + const bool textBackFill) : _tft(display), _trigger(trigger), _res_x(res_x), _res_y(res_y), _colorDepth(colorDepth), _textPrintMode(textPrintMode), _fontscaling(fontscaling), _fgcolor(fgcolor), _bgcolor(bgcolor), _useValidation(useValidation), _textBackFill(textBackFill) @@ -1590,7 +1590,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # if ADAGFX_ARGUMENT_VALIDATION const int16_t curWin = getWindow(); - if (curWin != 0) { selectWindow(0); } // Validate against raw window coordinates + if (curWin != 0) { selectWindow(0); } // Validate against raw window coordinates if (argCount == 6) { setRotation(nParams[5]); } // Use requested rotation @@ -1721,13 +1721,13 @@ bool AdafruitGFX_helper::pluginGetConfigValue(String& string) { /**************************************************************************** * draw a button shape with provided color, can also clear a previously drawn button ***************************************************************************/ -void AdafruitGFX_helper::drawButtonShape(Button_type_e buttonType, - int x, - int y, - int w, - int h, - uint16_t fillColor, - uint16_t borderColor) { +void AdafruitGFX_helper::drawButtonShape(const Button_type_e& buttonType, + const int & x, + const int & y, + const int & w, + const int & h, + const uint16_t & fillColor, + const uint16_t & borderColor) { switch (buttonType) { case Button_type_e::Square: // Rectangle { @@ -1790,13 +1790,13 @@ void AdafruitGFX_helper::drawButtonShape(Button_type_e buttonType, /**************************************************************************** * printText: Print text on display at a specific pixel or column/row location ***************************************************************************/ -void AdafruitGFX_helper::printText(const char *string, - const int16_t X, - const int16_t Y, - const uint8_t textSize, - const uint16_t color, - uint16_t bkcolor, - const uint16_t maxWidth) { +void AdafruitGFX_helper::printText(const char *string, + const int16_t & X, + const int16_t & Y, + const uint8_t & textSize, + const uint16_t& color, + uint16_t bkcolor, + const uint16_t& maxWidth) { int16_t _x = X; int16_t _y = Y + (_heightOffset * textSize); uint16_t _w = 0; @@ -1907,9 +1907,9 @@ void AdafruitGFX_helper::printText(const char *string, /**************************************************************************** * color565: convert r, g, b colors to rgb565 (by bit-shifting) ***************************************************************************/ -uint16_t color565(const uint8_t red, - const uint8_t green, - const uint8_t blue) { +uint16_t color565(const uint8_t& red, + const uint8_t& green, + const uint8_t& blue) { return ((red & 0xF8) << 8) | ((green & 0xFC) << 3) | (blue >> 3); } @@ -1923,9 +1923,9 @@ uint16_t color565(const uint8_t red, // Param [in] colorDepth: The requiresed color depth, default: FullColor // param [in] defaultWhite: Return White color if empty, default: true // return : color (default ADAGFX_WHITE) -uint16_t AdaGFXparseColor(String & s, - const AdaGFXColorDepth colorDepth, - const bool emptyIsBlack) { +uint16_t AdaGFXparseColor(String & s, + const AdaGFXColorDepth& colorDepth, + const bool emptyIsBlack) { s.toLowerCase(); int32_t result = -1; // No result yet @@ -2069,9 +2069,9 @@ uint16_t AdaGFXparseColor(String & s, return static_cast(result); } -const __FlashStringHelper* AdaGFXcolorToString_internal(uint16_t color, - AdaGFXColorDepth colorDepth, - bool blackIsEmpty); +const __FlashStringHelper* AdaGFXcolorToString_internal(const uint16_t & color, + const AdaGFXColorDepth& colorDepth, + bool blackIsEmpty); // Add a single optionvalue of a color to a datalist (internal/private) void AdaGFXaddHtmlDataListColorOptionValue(uint16_t color, @@ -2091,7 +2091,7 @@ void AdaGFXaddHtmlDataListColorOptionValue(uint16_t color, * Generate a html 'datalist' of the colors available for selected colorDepth, with id provided ****************************************************************************************/ void AdaGFXHtmlColorDepthDataList(const __FlashStringHelper *id, - const AdaGFXColorDepth colorDepth) { + const AdaGFXColorDepth & colorDepth) { addHtml(F("")); @@ -2154,9 +2154,9 @@ void AdaGFXHtmlColorDepthDataList(const __FlashStringHelper *id, /***************************************************************************************** * Convert an RGB565 color (number) to it's name or the #rgb565 hex string, based on depth ****************************************************************************************/ -String AdaGFXcolorToString(const uint16_t color, - const AdaGFXColorDepth colorDepth, - bool blackIsEmpty) { +String AdaGFXcolorToString(const uint16_t & color, + const AdaGFXColorDepth& colorDepth, + bool blackIsEmpty) { String result = AdaGFXcolorToString_internal(color, colorDepth, blackIsEmpty); if (result.equals(F("*"))) { @@ -2167,9 +2167,9 @@ String AdaGFXcolorToString(const uint16_t color, return result; } -const __FlashStringHelper* AdaGFXcolorToString_internal(uint16_t color, - AdaGFXColorDepth colorDepth, - bool blackIsEmpty) { +const __FlashStringHelper* AdaGFXcolorToString_internal(const uint16_t & color, + const AdaGFXColorDepth& colorDepth, + bool blackIsEmpty) { switch (colorDepth) { case AdaGFXColorDepth::Monochrome: case AdaGFXColorDepth::BlackWhiteRed: @@ -2247,7 +2247,7 @@ const __FlashStringHelper* AdaGFXcolorToString_internal(uint16_t color, * AdaGFXrgb565ToColor7: Convert a rgb565 color to the 7 colors supported by 7-color eInk displays * Borrowed from https://github.com/ZinggJM/GxEPD2 color7() routine ***************************************************************************/ -uint16_t AdaGFXrgb565ToColor7(const uint16_t color) { +uint16_t AdaGFXrgb565ToColor7(const uint16_t& color) { const uint16_t red = (color & 0xF800); const uint16_t green = (color & 0x07E0) << 5; const uint16_t blue = (color & 0x001F) << 11; @@ -2860,15 +2860,15 @@ uint32_t AdafruitGFX_helper::readLE32(void) { /**************************************************************************** * Check if the requested id is a valid window id ***************************************************************************/ -bool AdafruitGFX_helper::validWindow(uint8_t windowId) { +bool AdafruitGFX_helper::validWindow(const uint8_t& windowId) { return getWindowIndex(windowId) != -1; } /**************************************************************************** * Select this window id as the default ***************************************************************************/ -bool AdafruitGFX_helper::selectWindow(const uint8_t windowId, - const int8_t rotation) { +bool AdafruitGFX_helper::selectWindow(const uint8_t& windowId, + const int8_t & rotation) { const int16_t result = getWindowIndex(windowId); if (result != -1) { @@ -2881,7 +2881,7 @@ bool AdafruitGFX_helper::selectWindow(const uint8_t windowId, /**************************************************************************** * Return the index of the windowId in _windows, -1 if not found ***************************************************************************/ -int16_t AdafruitGFX_helper::getWindowIndex(const int16_t windowId) { +int16_t AdafruitGFX_helper::getWindowIndex(const int16_t& windowId) { size_t result = 0; for (auto win = _windows.begin(); win != _windows.end(); win++, result++) { @@ -2913,12 +2913,12 @@ void AdafruitGFX_helper::getWindowLimits(uint16_t& xLimit, /**************************************************************************** * Define a window and return the ID ***************************************************************************/ -uint8_t AdafruitGFX_helper::defineWindow(const int16_t x, - const int16_t y, - const int16_t w, - const int16_t h, - int16_t windowId, - const int8_t rotation) { +uint8_t AdafruitGFX_helper::defineWindow(const int16_t& x, + const int16_t& y, + const int16_t& w, + const int16_t& h, + int16_t windowId, + const int8_t & rotation) { int16_t result = getWindowIndex(windowId); if (result < 0) { @@ -2979,7 +2979,7 @@ uint8_t AdafruitGFX_helper::defineWindow(const int16_t x, /**************************************************************************** * Remove a window definition ***************************************************************************/ -bool AdafruitGFX_helper::deleteWindow(const uint8_t windowId) { +bool AdafruitGFX_helper::deleteWindow(const uint8_t& windowId) { const int16_t result = getWindowIndex(windowId); if (result > -1) { diff --git a/src/src/Helpers/AdafruitGFX_helper.h b/src/src/Helpers/AdafruitGFX_helper.h index a21b618782..b46f2a84c0 100644 --- a/src/src/Helpers/AdafruitGFX_helper.h +++ b/src/src/Helpers/AdafruitGFX_helper.h @@ -306,8 +306,8 @@ struct tWindowObject { class AdafruitGFX_helper; // Forward declaration // Some generic AdafruitGFX_helper support functions -const __FlashStringHelper* toString(const AdaGFXTextPrintMode mode); -const __FlashStringHelper* toString(const AdaGFXColorDepth colorDepth); +const __FlashStringHelper* toString(const AdaGFXTextPrintMode& mode); +const __FlashStringHelper* toString(const AdaGFXColorDepth& colorDepth); void AdaGFXFormTextPrintMode(const __FlashStringHelper *id, uint8_t selectedIndex); void AdaGFXFormColorDepth(const __FlashStringHelper *id, @@ -342,46 +342,46 @@ void AdaGFXFormFontScaling(const __FlashStringHelper *fontScalingId, String AdaGFXparseTemplate(const String & tmpString, const uint8_t lineSize, AdafruitGFX_helper *gfxHelper = nullptr); -uint16_t AdaGFXparseColor(String & s, - const AdaGFXColorDepth colorDepth = AdaGFXColorDepth::FullColor, - const bool emptyIsBlack = false); // Parse either a color by name, 6 digit hex rrggbb color, - // or 1..4 digit - // #rgb565 color (hex with # prefix) +uint16_t AdaGFXparseColor(String & s, + const AdaGFXColorDepth& colorDepth = AdaGFXColorDepth::FullColor, + const bool emptyIsBlack = false); // Parse either a color by name, 6 digit hex rrggbb color, + // or 1..4 digit + // #rgb565 color (hex with # prefix) void AdaGFXHtmlColorDepthDataList(const __FlashStringHelper *id, - const AdaGFXColorDepth colorDepth); -String AdaGFXcolorToString(const uint16_t color, - const AdaGFXColorDepth colorDepth = AdaGFXColorDepth::FullColor, - bool blackIsEmpty = false); + const AdaGFXColorDepth & colorDepth); +String AdaGFXcolorToString(const uint16_t & color, + const AdaGFXColorDepth& colorDepth = AdaGFXColorDepth::FullColor, + bool blackIsEmpty = false); # if ADAGFX_SUPPORT_7COLOR -uint16_t AdaGFXrgb565ToColor7(const uint16_t color); // Convert rgb565 color to 7-color +uint16_t AdaGFXrgb565ToColor7(const uint16_t& color); // Convert rgb565 color to 7-color # endif // if ADAGFX_SUPPORT_7COLOR class AdafruitGFX_helper { public: - AdafruitGFX_helper(Adafruit_GFX *display, - const String & trigger, - const uint16_t res_x, - const uint16_t res_y, - const AdaGFXColorDepth colorDepth = AdaGFXColorDepth::FullColor, - const AdaGFXTextPrintMode textPrintMode = AdaGFXTextPrintMode::ContinueToNextLine, - const uint8_t fontscaling = 1, - const uint16_t fgcolor = ADAGFX_WHITE, - const uint16_t bgcolor = ADAGFX_BLACK, - const bool useValidation = true, - const bool textBackFill = false); + AdafruitGFX_helper(Adafruit_GFX *display, + const String & trigger, + const uint16_t res_x, + const uint16_t res_y, + const AdaGFXColorDepth & colorDepth = AdaGFXColorDepth::FullColor, + const AdaGFXTextPrintMode& textPrintMode = AdaGFXTextPrintMode::ContinueToNextLine, + const uint8_t fontscaling = 1, + const uint16_t fgcolor = ADAGFX_WHITE, + const uint16_t bgcolor = ADAGFX_BLACK, + const bool useValidation = true, + const bool textBackFill = false); # if ADAGFX_ENABLE_BMP_DISPLAY - AdafruitGFX_helper(Adafruit_SPITFT *display, - const String & trigger, - const uint16_t res_x, - const uint16_t res_y, - const AdaGFXColorDepth colorDepth = AdaGFXColorDepth::FullColor, - const AdaGFXTextPrintMode textPrintMode = AdaGFXTextPrintMode::ContinueToNextLine, - const uint8_t fontscaling = 1, - const uint16_t fgcolor = ADAGFX_WHITE, - const uint16_t bgcolor = ADAGFX_BLACK, - const bool useValidation = true, - const bool textBackFill = false); + AdafruitGFX_helper(Adafruit_SPITFT *display, + const String & trigger, + const uint16_t res_x, + const uint16_t res_y, + const AdaGFXColorDepth & colorDepth = AdaGFXColorDepth::FullColor, + const AdaGFXTextPrintMode& textPrintMode = AdaGFXTextPrintMode::ContinueToNextLine, + const uint8_t fontscaling = 1, + const uint16_t fgcolor = ADAGFX_WHITE, + const uint16_t bgcolor = ADAGFX_BLACK, + const bool useValidation = true, + const bool textBackFill = false); # endif // if ADAGFX_ENABLE_BMP_DISPLAY virtual ~AdafruitGFX_helper() {} @@ -393,13 +393,13 @@ class AdafruitGFX_helper { bool pluginGetConfigValue(String& string); // Get a config value from the plugin # endif // if ADAGFX_ENABLE_GET_CONFIG_VALUE - void printText(const char *string, - const int16_t X, - const int16_t Y, - const uint8_t textSize = 0, - const uint16_t color = ADAGFX_WHITE, - uint16_t bkcolor = ADAGFX_BLACK, - const uint16_t maxWidth = 0); + void printText(const char *string, + const int16_t & X, + const int16_t & Y, + const uint8_t & textSize = 0, + const uint16_t& color = ADAGFX_WHITE, + uint16_t bkcolor = ADAGFX_BLACK, + const uint16_t& maxWidth = 0); void calculateTextMetrics(const uint8_t fontwidth, const uint8_t fontheight, const int8_t heightOffset = 0, @@ -446,16 +446,16 @@ class AdafruitGFX_helper { return _window; } - bool validWindow(const uint8_t windowId); - bool selectWindow(const uint8_t windowId, - const int8_t rotation = -1); - uint8_t defineWindow(const int16_t x, - const int16_t y, - const int16_t w, - const int16_t h, - int16_t windowId = -1, - const int8_t rotation = -1); - bool deleteWindow(const uint8_t windowId); + bool validWindow(const uint8_t& windowId); + bool selectWindow(const uint8_t& windowId, + const int8_t & rotation = -1); + uint8_t defineWindow(const int16_t& x, + const int16_t& y, + const int16_t& w, + const int16_t& h, + int16_t windowId = -1, + const int8_t & rotation = -1); + bool deleteWindow(const uint8_t& windowId); # endif // if ADAGFX_ENABLE_FRAMED_WINDOW private: @@ -468,13 +468,13 @@ class AdafruitGFX_helper { const bool colRowMode = false); # endif // if ADAGFX_ARGUMENT_VALIDATION # if ADAGFX_ENABLE_BUTTON_DRAW - void drawButtonShape(Button_type_e buttonType, - int x, - int y, - int w, - int h, - uint16_t fillColor, - uint16_t borderColor); + void drawButtonShape(const Button_type_e& buttonType, + const int & x, + const int & y, + const int & w, + const int & h, + const uint16_t & fillColor, + const uint16_t & borderColor); # endif // if ADAGFX_ENABLE_BUTTON_DRAW Adafruit_GFX *_display = nullptr; @@ -509,7 +509,7 @@ class AdafruitGFX_helper { fs::File file; # endif // if ADAGFX_ENABLE_BMP_DISPLAY # if ADAGFX_ENABLE_FRAMED_WINDOW - int16_t getWindowIndex(const int16_t windowId); + int16_t getWindowIndex(const int16_t& windowId); void logWindows(const String& prefix = EMPTY_STRING); void getWindowOffsets(uint16_t& xOffset, uint16_t& yOffset); From 6c25b139aaa639b2098fa0a20148114a0baaa9e1 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 9 Jun 2022 20:23:09 +0200 Subject: [PATCH 023/113] [AdaGFX] Correct font include paths, add #ifndef filename checks to fonts --- src/src/Helpers/AdafruitGFX_helper.cpp | 36 +- src/src/Static/Fonts/AmerikaSans16pt7b.h | 195 ++++----- src/src/Static/Fonts/FreeSans9pt7b.h | 391 +++++++++--------- src/src/Static/Fonts/NovaMono12pt7b.h | 195 ++++----- src/src/Static/Fonts/NovaMono8pt7b.h | 195 ++++----- .../Static/Fonts/RepetitionScrolling12pt7b.h | 195 ++++----- src/src/Static/Fonts/Seven_Segment18pt7b.h | 195 ++++----- src/src/Static/Fonts/Seven_Segment24pt7b.h | 195 ++++----- src/src/Static/Fonts/angelina12pt7b.h | 195 ++++----- src/src/Static/Fonts/angelina8pt7b.h | 195 ++++----- src/src/Static/Fonts/unispace12pt7b.h | 195 ++++----- src/src/Static/Fonts/unispace8pt7b.h | 195 ++++----- src/src/Static/Fonts/unispace_italic12pt7b.h | 195 ++++----- src/src/Static/Fonts/unispace_italic8pt7b.h | 195 ++++----- src/src/Static/Fonts/whitrabt12pt7b.h | 195 ++++----- src/src/Static/Fonts/whitrabt16pt7b.h | 195 ++++----- src/src/Static/Fonts/whitrabt18pt7b.h | 195 ++++----- src/src/Static/Fonts/whitrabt20pt7b.h | 195 ++++----- src/src/Static/Fonts/whitrabt8pt7b.h | 195 ++++----- 19 files changed, 1898 insertions(+), 1844 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index 4bd99f0f07..00b0e68fe7 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -11,33 +11,33 @@ # include # if ADAGFX_FONTS_INCLUDED -# include "src/Static/Fonts/Seven_Segment24pt7b.h" -# include "src/Static/Fonts/Seven_Segment18pt7b.h" -# include "src/Static/Fonts/FreeSans9pt7b.h" +# include "../Static/Fonts/Seven_Segment24pt7b.h" +# include "../Static/Fonts/Seven_Segment18pt7b.h" +# include "../Static/Fonts/FreeSans9pt7b.h" # ifdef ADAGFX_FONTS_EXTRA_8PT_INCLUDED -# include "src/Static/Fonts/angelina8pt7b.h" -# include "src/Static/Fonts/NovaMono8pt7b.h" -# include "src/Static/Fonts/unispace8pt7b.h" -# include "src/Static/Fonts/unispace_italic8pt7b.h" -# include "src/Static/Fonts/whitrabt8pt7b.h" +# include "../Static/Fonts/angelina8pt7b.h" +# include "../Static/Fonts/NovaMono8pt7b.h" +# include "../Static/Fonts/unispace8pt7b.h" +# include "../Static/Fonts/unispace_italic8pt7b.h" +# include "../Static/Fonts/whitrabt8pt7b.h" # endif // ifdef ADAGFX_FONTS_EXTRA_8PT_INCLUDED # ifdef ADAGFX_FONTS_EXTRA_12PT_INCLUDED -# include "src/Static/Fonts/angelina12pt7b.h" -# include "src/Static/Fonts/NovaMono12pt7b.h" -# include "src/Static/Fonts/RepetitionScrolling12pt7b.h" -# include "src/Static/Fonts/unispace12pt7b.h" -# include "src/Static/Fonts/unispace_italic12pt7b.h" -# include "src/Static/Fonts/whitrabt12pt7b.h" +# include "../Static/Fonts/angelina12pt7b.h" +# include "../Static/Fonts/NovaMono12pt7b.h" +# include "../Static/Fonts/RepetitionScrolling12pt7b.h" +# include "../Static/Fonts/unispace12pt7b.h" +# include "../Static/Fonts/unispace_italic12pt7b.h" +# include "../Static/Fonts/whitrabt12pt7b.h" # endif // ifdef ADAGFX_FONTS_EXTRA_12PT_INCLUDED # ifdef ADAGFX_FONTS_EXTRA_16PT_INCLUDED -# include "src/Static/Fonts/AmerikaSans16pt7b.h" -# include "src/Static/Fonts/whitrabt16pt7b.h" +# include "../Static/Fonts/AmerikaSans16pt7b.h" +# include "../Static/Fonts/whitrabt16pt7b.h" # endif // ifdef ADAGFX_FONTS_EXTRA_16PT_INCLUDED # ifdef ADAGFX_FONTS_EXTRA_18PT_INCLUDED -# include "src/Static/Fonts/whitrabt18pt7b.h" +# include "../Static/Fonts/whitrabt18pt7b.h" # endif // ifdef ADAGFX_FONTS_EXTRA_18PT_INCLUDED # ifdef ADAGFX_FONTS_EXTRA_20PT_INCLUDED -# include "src/Static/Fonts/whitrabt20pt7b.h" +# include "../Static/Fonts/whitrabt20pt7b.h" # endif // ifdef ADAGFX_FONTS_EXTRA_20PT_INCLUDED # endif // if ADAGFX_FONTS_INCLUDED diff --git a/src/src/Static/Fonts/AmerikaSans16pt7b.h b/src/src/Static/Fonts/AmerikaSans16pt7b.h index d98ffd03f1..138bb4538e 100644 --- a/src/src/Static/Fonts/AmerikaSans16pt7b.h +++ b/src/src/Static/Fonts/AmerikaSans16pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_AMERIKASANS16PT7B_H +#define FONTS_AMERIKASANS16PT7B_H const uint8_t AmerikaSans16pt7bBitmaps[] PROGMEM = { 0x00, 0xFF, 0xFF, 0xFF, 0xFE, 0xE6, 0x66, 0x66, 0x66, 0x66, 0x66, 0xFF, 0x60, 0xE7, 0x66, 0x66, 0x66, 0x66, 0x66, 0x62, 0x03, 0x0C, 0x02, 0x08, @@ -302,105 +304,106 @@ const uint8_t AmerikaSans16pt7bBitmaps[] PROGMEM = { 0x3E, 0x0B, 0xFF, 0xF0, 0x7C, 0x01, 0xC0 }; const GFXglyph AmerikaSans16pt7bGlyphs[] PROGMEM = { - { 0, 1, 1, 11, 0, 0 }, // 0x20 ' ' - { 1, 4, 23, 7, 2, -22 }, // 0x21 '!' - { 13, 8, 7, 9, 1, -22 }, // 0x22 '"' - { 20, 16, 15, 18, 1, -22 }, // 0x23 '#' - { 50, 13, 23, 15, 1, -22 }, // 0x24 '$' - { 88, 24, 23, 27, 2, -22 }, // 0x25 '%' - { 157, 17, 23, 19, 2, -22 }, // 0x26 '&' - { 206, 3, 7, 5, 1, -22 }, // 0x27 ''' - { 209, 10, 27, 10, 1, -24 }, // 0x28 '(' - { 243, 10, 27, 10, -1, -24 }, // 0x29 ')' - { 277, 10, 11, 13, 2, -22 }, // 0x2A '*' - { 291, 14, 14, 16, 1, -17 }, // 0x2B '+' - { 316, 4, 8, 7, 2, -3 }, // 0x2C ',' - { 320, 8, 2, 12, 2, -8 }, // 0x2D '-' - { 322, 4, 4, 6, 1, -3 }, // 0x2E '.' - { 324, 18, 26, 15, -1, -23 }, // 0x2F '/' - { 383, 18, 23, 21, 2, -22 }, // 0x30 '0' - { 435, 7, 23, 9, 1, -22 }, // 0x31 '1' - { 456, 16, 23, 18, 1, -22 }, // 0x32 '2' - { 502, 14, 23, 17, 2, -22 }, // 0x33 '3' - { 543, 17, 23, 19, 1, -22 }, // 0x34 '4' - { 592, 15, 23, 16, 1, -22 }, // 0x35 '5' - { 636, 16, 23, 19, 2, -22 }, // 0x36 '6' - { 682, 14, 23, 17, 2, -22 }, // 0x37 '7' - { 723, 16, 23, 19, 2, -22 }, // 0x38 '8' - { 769, 16, 23, 19, 2, -22 }, // 0x39 '9' - { 815, 4, 12, 6, 1, -11 }, // 0x3A ':' - { 821, 4, 16, 6, 1, -11 }, // 0x3B ';' - { 829, 15, 14, 16, 1, -15 }, // 0x3C '<' - { 856, 12, 8, 13, 1, -13 }, // 0x3D '=' - { 868, 15, 14, 16, 1, -15 }, // 0x3E '>' - { 895, 15, 23, 17, 1, -22 }, // 0x3F '?' - { 939, 27, 27, 30, 2, -22 }, // 0x40 '@' - { 1031, 18, 23, 21, 2, -22 }, // 0x41 'A' - { 1083, 16, 23, 17, 1, -22 }, // 0x42 'B' - { 1129, 21, 23, 23, 2, -22 }, // 0x43 'C' - { 1190, 18, 23, 20, 1, -22 }, // 0x44 'D' - { 1242, 16, 23, 18, 1, -22 }, // 0x45 'E' - { 1288, 12, 23, 14, 2, -22 }, // 0x46 'F' - { 1323, 19, 29, 22, 2, -22 }, // 0x47 'G' - { 1392, 15, 23, 17, 1, -22 }, // 0x48 'H' - { 1436, 4, 23, 6, 1, -22 }, // 0x49 'I' - { 1448, 10, 29, 7, -4, -22 }, // 0x4A 'J' - { 1485, 17, 23, 19, 1, -22 }, // 0x4B 'K' - { 1534, 16, 23, 18, 1, -22 }, // 0x4C 'L' - { 1580, 24, 26, 26, 1, -25 }, // 0x4D 'M' - { 1658, 19, 23, 21, 1, -22 }, // 0x4E 'N' - { 1713, 19, 23, 21, 1, -22 }, // 0x4F 'O' - { 1768, 19, 23, 20, 1, -22 }, // 0x50 'P' - { 1823, 19, 29, 21, 1, -22 }, // 0x51 'Q' - { 1892, 20, 23, 22, 1, -22 }, // 0x52 'R' - { 1950, 16, 23, 18, 1, -22 }, // 0x53 'S' - { 1996, 20, 23, 20, 0, -22 }, // 0x54 'T' - { 2054, 19, 23, 21, 1, -22 }, // 0x55 'U' - { 2109, 21, 23, 22, 1, -22 }, // 0x56 'V' - { 2170, 24, 23, 26, 1, -22 }, // 0x57 'W' - { 2239, 16, 23, 19, 2, -22 }, // 0x58 'X' - { 2285, 19, 23, 21, 1, -22 }, // 0x59 'Y' - { 2340, 21, 23, 22, 1, -22 }, // 0x5A 'Z' - { 2401, 9, 23, 11, 2, -22 }, // 0x5B '[' - { 2427, 18, 27, 15, -1, -24 }, // 0x5C '\' - { 2488, 9, 23, 11, 1, -22 }, // 0x5D ']' - { 2514, 12, 8, 13, 1, -21 }, // 0x5E '^' - { 2526, 12, 2, 13, 1, 0 }, // 0x5F '_' - { 2529, 8, 5, 20, 6, -30 }, // 0x60 '`' - { 2534, 17, 16, 18, 1, -15 }, // 0x61 'a' - { 2568, 16, 23, 18, 2, -22 }, // 0x62 'b' - { 2614, 14, 16, 17, 2, -15 }, // 0x63 'c' - { 2642, 17, 23, 18, 1, -22 }, // 0x64 'd' - { 2691, 15, 15, 18, 2, -14 }, // 0x65 'e' - { 2720, 12, 29, 14, 1, -22 }, // 0x66 'f' - { 2764, 19, 21, 21, 1, -14 }, // 0x67 'g' - { 2814, 15, 23, 18, 2, -22 }, // 0x68 'h' - { 2858, 5, 23, 7, 2, -22 }, // 0x69 'i' - { 2873, 10, 29, 7, -3, -22 }, // 0x6A 'j' - { 2910, 17, 29, 20, 2, -22 }, // 0x6B 'k' - { 2972, 9, 23, 10, 2, -22 }, // 0x6C 'l' - { 2998, 28, 16, 30, 1, -15 }, // 0x6D 'm' - { 3054, 16, 15, 18, 1, -14 }, // 0x6E 'n' - { 3084, 17, 16, 18, 1, -15 }, // 0x6F 'o' - { 3118, 17, 22, 18, 1, -15 }, // 0x70 'p' - { 3165, 19, 21, 21, 1, -14 }, // 0x71 'q' - { 3215, 11, 16, 12, 1, -15 }, // 0x72 'r' - { 3237, 11, 16, 13, 1, -15 }, // 0x73 's' - { 3259, 12, 23, 11, 0, -22 }, // 0x74 't' - { 3294, 16, 16, 18, 1, -15 }, // 0x75 'u' - { 3326, 15, 16, 16, 1, -15 }, // 0x76 'v' - { 3356, 28, 16, 30, 1, -15 }, // 0x77 'w' - { 3412, 13, 16, 16, 2, -15 }, // 0x78 'x' - { 3438, 16, 22, 18, 1, -15 }, // 0x79 'y' - { 3482, 15, 16, 17, 1, -15 }, // 0x7A 'z' - { 3512, 13, 23, 15, 1, -22 }, // 0x7B '{' - { 3550, 3, 31, 7, 2, -26 }, // 0x7C '|' - { 3562, 13, 23, 15, 1, -22 }, // 0x7D '}' - { 3600, 13, 4, 15, 1, -18 } }; // 0x7E '~' + { 0, 1, 1, 11, 0, 0 }, // 0x20 ' ' + { 1, 4, 23, 7, 2, -22 }, // 0x21 '!' + { 13, 8, 7, 9, 1, -22 }, // 0x22 '"' + { 20, 16, 15, 18, 1, -22 }, // 0x23 '#' + { 50, 13, 23, 15, 1, -22 }, // 0x24 '$' + { 88, 24, 23, 27, 2, -22 }, // 0x25 '%' + { 157, 17, 23, 19, 2, -22 }, // 0x26 '&' + { 206, 3, 7, 5, 1, -22 }, // 0x27 ''' + { 209, 10, 27, 10, 1, -24 }, // 0x28 '(' + { 243, 10, 27, 10, -1, -24 }, // 0x29 ')' + { 277, 10, 11, 13, 2, -22 }, // 0x2A '*' + { 291, 14, 14, 16, 1, -17 }, // 0x2B '+' + { 316, 4, 8, 7, 2, -3 }, // 0x2C ',' + { 320, 8, 2, 12, 2, -8 }, // 0x2D '-' + { 322, 4, 4, 6, 1, -3 }, // 0x2E '.' + { 324, 18, 26, 15, -1, -23 }, // 0x2F '/' + { 383, 18, 23, 21, 2, -22 }, // 0x30 '0' + { 435, 7, 23, 9, 1, -22 }, // 0x31 '1' + { 456, 16, 23, 18, 1, -22 }, // 0x32 '2' + { 502, 14, 23, 17, 2, -22 }, // 0x33 '3' + { 543, 17, 23, 19, 1, -22 }, // 0x34 '4' + { 592, 15, 23, 16, 1, -22 }, // 0x35 '5' + { 636, 16, 23, 19, 2, -22 }, // 0x36 '6' + { 682, 14, 23, 17, 2, -22 }, // 0x37 '7' + { 723, 16, 23, 19, 2, -22 }, // 0x38 '8' + { 769, 16, 23, 19, 2, -22 }, // 0x39 '9' + { 815, 4, 12, 6, 1, -11 }, // 0x3A ':' + { 821, 4, 16, 6, 1, -11 }, // 0x3B ';' + { 829, 15, 14, 16, 1, -15 }, // 0x3C '<' + { 856, 12, 8, 13, 1, -13 }, // 0x3D '=' + { 868, 15, 14, 16, 1, -15 }, // 0x3E '>' + { 895, 15, 23, 17, 1, -22 }, // 0x3F '?' + { 939, 27, 27, 30, 2, -22 }, // 0x40 '@' + { 1031, 18, 23, 21, 2, -22 }, // 0x41 'A' + { 1083, 16, 23, 17, 1, -22 }, // 0x42 'B' + { 1129, 21, 23, 23, 2, -22 }, // 0x43 'C' + { 1190, 18, 23, 20, 1, -22 }, // 0x44 'D' + { 1242, 16, 23, 18, 1, -22 }, // 0x45 'E' + { 1288, 12, 23, 14, 2, -22 }, // 0x46 'F' + { 1323, 19, 29, 22, 2, -22 }, // 0x47 'G' + { 1392, 15, 23, 17, 1, -22 }, // 0x48 'H' + { 1436, 4, 23, 6, 1, -22 }, // 0x49 'I' + { 1448, 10, 29, 7, -4, -22 }, // 0x4A 'J' + { 1485, 17, 23, 19, 1, -22 }, // 0x4B 'K' + { 1534, 16, 23, 18, 1, -22 }, // 0x4C 'L' + { 1580, 24, 26, 26, 1, -25 }, // 0x4D 'M' + { 1658, 19, 23, 21, 1, -22 }, // 0x4E 'N' + { 1713, 19, 23, 21, 1, -22 }, // 0x4F 'O' + { 1768, 19, 23, 20, 1, -22 }, // 0x50 'P' + { 1823, 19, 29, 21, 1, -22 }, // 0x51 'Q' + { 1892, 20, 23, 22, 1, -22 }, // 0x52 'R' + { 1950, 16, 23, 18, 1, -22 }, // 0x53 'S' + { 1996, 20, 23, 20, 0, -22 }, // 0x54 'T' + { 2054, 19, 23, 21, 1, -22 }, // 0x55 'U' + { 2109, 21, 23, 22, 1, -22 }, // 0x56 'V' + { 2170, 24, 23, 26, 1, -22 }, // 0x57 'W' + { 2239, 16, 23, 19, 2, -22 }, // 0x58 'X' + { 2285, 19, 23, 21, 1, -22 }, // 0x59 'Y' + { 2340, 21, 23, 22, 1, -22 }, // 0x5A 'Z' + { 2401, 9, 23, 11, 2, -22 }, // 0x5B '[' + { 2427, 18, 27, 15, -1, -24 }, // 0x5C '\' + { 2488, 9, 23, 11, 1, -22 }, // 0x5D ']' + { 2514, 12, 8, 13, 1, -21 }, // 0x5E '^' + { 2526, 12, 2, 13, 1, 0 }, // 0x5F '_' + { 2529, 8, 5, 20, 6, -30 }, // 0x60 '`' + { 2534, 17, 16, 18, 1, -15 }, // 0x61 'a' + { 2568, 16, 23, 18, 2, -22 }, // 0x62 'b' + { 2614, 14, 16, 17, 2, -15 }, // 0x63 'c' + { 2642, 17, 23, 18, 1, -22 }, // 0x64 'd' + { 2691, 15, 15, 18, 2, -14 }, // 0x65 'e' + { 2720, 12, 29, 14, 1, -22 }, // 0x66 'f' + { 2764, 19, 21, 21, 1, -14 }, // 0x67 'g' + { 2814, 15, 23, 18, 2, -22 }, // 0x68 'h' + { 2858, 5, 23, 7, 2, -22 }, // 0x69 'i' + { 2873, 10, 29, 7, -3, -22 }, // 0x6A 'j' + { 2910, 17, 29, 20, 2, -22 }, // 0x6B 'k' + { 2972, 9, 23, 10, 2, -22 }, // 0x6C 'l' + { 2998, 28, 16, 30, 1, -15 }, // 0x6D 'm' + { 3054, 16, 15, 18, 1, -14 }, // 0x6E 'n' + { 3084, 17, 16, 18, 1, -15 }, // 0x6F 'o' + { 3118, 17, 22, 18, 1, -15 }, // 0x70 'p' + { 3165, 19, 21, 21, 1, -14 }, // 0x71 'q' + { 3215, 11, 16, 12, 1, -15 }, // 0x72 'r' + { 3237, 11, 16, 13, 1, -15 }, // 0x73 's' + { 3259, 12, 23, 11, 0, -22 }, // 0x74 't' + { 3294, 16, 16, 18, 1, -15 }, // 0x75 'u' + { 3326, 15, 16, 16, 1, -15 }, // 0x76 'v' + { 3356, 28, 16, 30, 1, -15 }, // 0x77 'w' + { 3412, 13, 16, 16, 2, -15 }, // 0x78 'x' + { 3438, 16, 22, 18, 1, -15 }, // 0x79 'y' + { 3482, 15, 16, 17, 1, -15 }, // 0x7A 'z' + { 3512, 13, 23, 15, 1, -22 }, // 0x7B '{' + { 3550, 3, 31, 7, 2, -26 }, // 0x7C '|' + { 3562, 13, 23, 15, 1, -22 }, // 0x7D '}' + { 3600, 13, 4, 15, 1, -18 } }; // 0x7E '~' const GFXfont AmerikaSans16pt7b PROGMEM = { (uint8_t *)AmerikaSans16pt7bBitmaps, (GFXglyph *)AmerikaSans16pt7bGlyphs, - 0x20, 0x7E, 40 }; + 0x20, 0x7E, 40 }; // Approx. 4279 bytes +#endif // ifndef FONTS_AMERIKASANS16PT7B_H diff --git a/src/src/Static/Fonts/FreeSans9pt7b.h b/src/src/Static/Fonts/FreeSans9pt7b.h index f151f326dd..ea02fce7c0 100644 --- a/src/src/Static/Fonts/FreeSans9pt7b.h +++ b/src/src/Static/Fonts/FreeSans9pt7b.h @@ -1,200 +1,203 @@ +#ifndef FONTS_FREESANS9PT7B_H +#define FONTS_FREESANS9PT7B_H const uint8_t FreeSans9pt7bBitmaps[] PROGMEM = { - 0xFF, 0xFF, 0xF8, 0xC0, 0xDE, 0xF7, 0x20, 0x09, 0x86, 0x41, 0x91, 0xFF, - 0x13, 0x04, 0xC3, 0x20, 0xC8, 0xFF, 0x89, 0x82, 0x61, 0x90, 0x10, 0x1F, - 0x14, 0xDA, 0x3D, 0x1E, 0x83, 0x40, 0x78, 0x17, 0x08, 0xF4, 0x7A, 0x35, - 0x33, 0xF0, 0x40, 0x20, 0x38, 0x10, 0xEC, 0x20, 0xC6, 0x20, 0xC6, 0x40, - 0xC6, 0x40, 0x6C, 0x80, 0x39, 0x00, 0x01, 0x3C, 0x02, 0x77, 0x02, 0x63, - 0x04, 0x63, 0x04, 0x77, 0x08, 0x3C, 0x0E, 0x06, 0x60, 0xCC, 0x19, 0x81, - 0xE0, 0x18, 0x0F, 0x03, 0x36, 0xC2, 0xD8, 0x73, 0x06, 0x31, 0xE3, 0xC4, - 0xFE, 0x13, 0x26, 0x6C, 0xCC, 0xCC, 0xC4, 0x66, 0x23, 0x10, 0x8C, 0x46, - 0x63, 0x33, 0x33, 0x32, 0x66, 0x4C, 0x80, 0x25, 0x7E, 0xA5, 0x00, 0x30, - 0xC3, 0x3F, 0x30, 0xC3, 0x0C, 0xD6, 0xF0, 0xC0, 0x08, 0x44, 0x21, 0x10, - 0x84, 0x42, 0x11, 0x08, 0x00, 0x3C, 0x66, 0x42, 0xC3, 0xC3, 0xC3, 0xC3, - 0xC3, 0xC3, 0xC3, 0x42, 0x66, 0x3C, 0x11, 0x3F, 0x33, 0x33, 0x33, 0x33, - 0x30, 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x1C, 0x1C, 0x1C, 0x18, 0x18, - 0x10, 0x08, 0x07, 0xF8, 0x3C, 0x66, 0xC3, 0xC3, 0x03, 0x06, 0x1C, 0x07, - 0x03, 0xC3, 0xC3, 0x66, 0x3C, 0x0C, 0x18, 0x71, 0x62, 0xC9, 0xA3, 0x46, - 0xFE, 0x18, 0x30, 0x60, 0xC0, 0x7F, 0x20, 0x10, 0x08, 0x08, 0x07, 0xF3, - 0x8C, 0x03, 0x01, 0x80, 0xF0, 0x6C, 0x63, 0xE0, 0x1E, 0x31, 0x98, 0x78, - 0x0C, 0x06, 0xF3, 0x8D, 0x83, 0xC1, 0xE0, 0xD0, 0x6C, 0x63, 0xE0, 0xFF, - 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x18, 0x18, 0x18, 0x10, 0x30, 0x30, - 0x3E, 0x31, 0xB0, 0x78, 0x3C, 0x1B, 0x18, 0xF8, 0xC6, 0xC1, 0xE0, 0xF0, - 0x6C, 0x63, 0xE0, 0x3C, 0x66, 0xC2, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, - 0x03, 0xC2, 0x66, 0x3C, 0xC0, 0x00, 0x30, 0xC0, 0x00, 0x00, 0x64, 0xA0, - 0x00, 0x81, 0xC7, 0x8E, 0x0C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0x80, 0xFF, - 0x80, 0x00, 0x1F, 0xF0, 0x00, 0x70, 0x0E, 0x01, 0xC0, 0x18, 0x38, 0x71, - 0xC0, 0x80, 0x00, 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x18, 0x38, 0x18, - 0x18, 0x0C, 0x00, 0x00, 0x01, 0x80, 0x03, 0xF0, 0x06, 0x0E, 0x06, 0x01, - 0x86, 0x00, 0x66, 0x1D, 0xBB, 0x31, 0xCF, 0x18, 0xC7, 0x98, 0x63, 0xCC, - 0x31, 0xE6, 0x11, 0xB3, 0x99, 0xCC, 0xF7, 0x86, 0x00, 0x01, 0x80, 0x00, - 0x70, 0x40, 0x0F, 0xE0, 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, - 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, - 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, - 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, - 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, - 0xFF, 0x18, 0x33, 0x03, 0x60, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, - 0x78, 0x0F, 0x03, 0x60, 0xCF, 0xF0, 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, - 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, 0xFF, 0xC0, 0xC0, - 0xC0, 0xC0, 0xC0, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x0F, 0x83, - 0x0E, 0x60, 0x66, 0x03, 0xC0, 0x0C, 0x00, 0xC1, 0xFC, 0x03, 0xC0, 0x36, - 0x03, 0x60, 0x73, 0x0F, 0x0F, 0x10, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, - 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, - 0xFF, 0xFF, 0xFF, 0xC0, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, - 0x8F, 0x1E, 0x27, 0x80, 0xC0, 0xD8, 0x33, 0x0C, 0x63, 0x0C, 0xC1, 0xB8, - 0x3F, 0x07, 0x30, 0xC3, 0x18, 0x63, 0x06, 0x60, 0x6C, 0x0C, 0xC0, 0xC0, - 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, 0xE0, - 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, - 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, 0x80, 0xE0, 0x7C, 0x0F, - 0xC1, 0xE8, 0x3D, 0x87, 0x98, 0xF1, 0x1E, 0x33, 0xC3, 0x78, 0x6F, 0x07, - 0xE0, 0x7C, 0x0E, 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, - 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, - 0x00, 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, - 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, - 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x6C, - 0x60, 0xC0, 0xFB, 0x00, 0x08, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, - 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, - 0x70, 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, - 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, - 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0xC0, 0x78, 0x0F, - 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, - 0xB0, 0x61, 0xF0, 0xC0, 0x6C, 0x0D, 0x81, 0x10, 0x63, 0x0C, 0x61, 0x04, - 0x60, 0xCC, 0x19, 0x01, 0x60, 0x3C, 0x07, 0x00, 0x60, 0xC1, 0x81, 0x30, - 0xE1, 0x98, 0x70, 0xCC, 0x28, 0x66, 0x26, 0x21, 0x13, 0x30, 0xC8, 0x98, - 0x6C, 0x4C, 0x14, 0x34, 0x0A, 0x1A, 0x07, 0x07, 0x03, 0x03, 0x80, 0x81, - 0x80, 0x60, 0x63, 0x0C, 0x30, 0xC1, 0x98, 0x0F, 0x00, 0xE0, 0x06, 0x00, - 0xF0, 0x19, 0x01, 0x98, 0x30, 0xC6, 0x0E, 0x60, 0x60, 0xC0, 0x36, 0x06, - 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, - 0x06, 0x00, 0x60, 0x06, 0x00, 0xFF, 0xC0, 0x60, 0x30, 0x0C, 0x06, 0x03, - 0x01, 0xC0, 0x60, 0x30, 0x18, 0x06, 0x03, 0x00, 0xFF, 0xC0, 0xFB, 0x6D, - 0xB6, 0xDB, 0x6D, 0xB6, 0xE0, 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x84, - 0x10, 0x80, 0xED, 0xB6, 0xDB, 0x6D, 0xB6, 0xDB, 0xE0, 0x30, 0x60, 0xA2, - 0x44, 0xD8, 0xA1, 0x80, 0xFF, 0xC0, 0xC6, 0x30, 0x7E, 0x71, 0xB0, 0xC0, - 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, 0xC0, 0x60, 0x30, 0x1B, - 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, 0x3C, - 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, 0x03, 0x03, 0x03, - 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x3C, 0x66, - 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, 0x36, 0x6F, 0x66, 0x66, - 0x66, 0x66, 0x60, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, - 0x3B, 0x03, 0x03, 0xC6, 0x7C, 0xC0, 0xC0, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, - 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, 0xC0, 0x30, 0x03, - 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, 0xC0, 0x60, 0x30, 0x18, 0x4C, - 0x46, 0x63, 0x61, 0xF0, 0xEC, 0x62, 0x31, 0x98, 0x6C, 0x30, 0xFF, 0xFF, - 0xFF, 0xC0, 0xDE, 0xF7, 0x1C, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, - 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, - 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, - 0xC3, 0x66, 0x3C, 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, - 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, - 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0x03, 0xDF, 0x31, 0x8C, 0x63, 0x18, - 0xC6, 0x00, 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, - 0x66, 0xF6, 0x66, 0x66, 0x66, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, - 0xC3, 0xC3, 0xC7, 0x7B, 0xC1, 0xA0, 0x98, 0xCC, 0x42, 0x21, 0xB0, 0xD0, - 0x28, 0x1C, 0x0C, 0x00, 0xC6, 0x1E, 0x38, 0x91, 0xC4, 0xCA, 0x66, 0xD3, - 0x16, 0xD0, 0xA6, 0x87, 0x1C, 0x38, 0xC0, 0xC6, 0x00, 0x43, 0x62, 0x36, - 0x1C, 0x18, 0x1C, 0x3C, 0x26, 0x62, 0x43, 0xC1, 0x21, 0x98, 0xCC, 0x42, - 0x61, 0xB0, 0xD0, 0x38, 0x1C, 0x0C, 0x06, 0x03, 0x01, 0x03, 0x00, 0xFE, - 0x0C, 0x30, 0xC1, 0x86, 0x18, 0x20, 0xC1, 0xFC, 0x36, 0x66, 0x66, 0x6E, - 0xCE, 0x66, 0x66, 0x66, 0x30, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xC6, 0x66, - 0x66, 0x67, 0x37, 0x66, 0x66, 0x66, 0xC0, 0x61, 0x24, 0x38}; + 0xFF, 0xFF, 0xF8, 0xC0, 0xDE, 0xF7, 0x20, 0x09, 0x86, 0x41, 0x91, 0xFF, + 0x13, 0x04, 0xC3, 0x20, 0xC8, 0xFF, 0x89, 0x82, 0x61, 0x90, 0x10, 0x1F, + 0x14, 0xDA, 0x3D, 0x1E, 0x83, 0x40, 0x78, 0x17, 0x08, 0xF4, 0x7A, 0x35, + 0x33, 0xF0, 0x40, 0x20, 0x38, 0x10, 0xEC, 0x20, 0xC6, 0x20, 0xC6, 0x40, + 0xC6, 0x40, 0x6C, 0x80, 0x39, 0x00, 0x01, 0x3C, 0x02, 0x77, 0x02, 0x63, + 0x04, 0x63, 0x04, 0x77, 0x08, 0x3C, 0x0E, 0x06, 0x60, 0xCC, 0x19, 0x81, + 0xE0, 0x18, 0x0F, 0x03, 0x36, 0xC2, 0xD8, 0x73, 0x06, 0x31, 0xE3, 0xC4, + 0xFE, 0x13, 0x26, 0x6C, 0xCC, 0xCC, 0xC4, 0x66, 0x23, 0x10, 0x8C, 0x46, + 0x63, 0x33, 0x33, 0x32, 0x66, 0x4C, 0x80, 0x25, 0x7E, 0xA5, 0x00, 0x30, + 0xC3, 0x3F, 0x30, 0xC3, 0x0C, 0xD6, 0xF0, 0xC0, 0x08, 0x44, 0x21, 0x10, + 0x84, 0x42, 0x11, 0x08, 0x00, 0x3C, 0x66, 0x42, 0xC3, 0xC3, 0xC3, 0xC3, + 0xC3, 0xC3, 0xC3, 0x42, 0x66, 0x3C, 0x11, 0x3F, 0x33, 0x33, 0x33, 0x33, + 0x30, 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x1C, 0x1C, 0x1C, 0x18, 0x18, + 0x10, 0x08, 0x07, 0xF8, 0x3C, 0x66, 0xC3, 0xC3, 0x03, 0x06, 0x1C, 0x07, + 0x03, 0xC3, 0xC3, 0x66, 0x3C, 0x0C, 0x18, 0x71, 0x62, 0xC9, 0xA3, 0x46, + 0xFE, 0x18, 0x30, 0x60, 0xC0, 0x7F, 0x20, 0x10, 0x08, 0x08, 0x07, 0xF3, + 0x8C, 0x03, 0x01, 0x80, 0xF0, 0x6C, 0x63, 0xE0, 0x1E, 0x31, 0x98, 0x78, + 0x0C, 0x06, 0xF3, 0x8D, 0x83, 0xC1, 0xE0, 0xD0, 0x6C, 0x63, 0xE0, 0xFF, + 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x18, 0x18, 0x18, 0x10, 0x30, 0x30, + 0x3E, 0x31, 0xB0, 0x78, 0x3C, 0x1B, 0x18, 0xF8, 0xC6, 0xC1, 0xE0, 0xF0, + 0x6C, 0x63, 0xE0, 0x3C, 0x66, 0xC2, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x03, + 0x03, 0xC2, 0x66, 0x3C, 0xC0, 0x00, 0x30, 0xC0, 0x00, 0x00, 0x64, 0xA0, + 0x00, 0x81, 0xC7, 0x8E, 0x0C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0x80, 0xFF, + 0x80, 0x00, 0x1F, 0xF0, 0x00, 0x70, 0x0E, 0x01, 0xC0, 0x18, 0x38, 0x71, + 0xC0, 0x80, 0x00, 0x3E, 0x31, 0xB0, 0x78, 0x30, 0x18, 0x18, 0x38, 0x18, + 0x18, 0x0C, 0x00, 0x00, 0x01, 0x80, 0x03, 0xF0, 0x06, 0x0E, 0x06, 0x01, + 0x86, 0x00, 0x66, 0x1D, 0xBB, 0x31, 0xCF, 0x18, 0xC7, 0x98, 0x63, 0xCC, + 0x31, 0xE6, 0x11, 0xB3, 0x99, 0xCC, 0xF7, 0x86, 0x00, 0x01, 0x80, 0x00, + 0x70, 0x40, 0x0F, 0xE0, 0x06, 0x00, 0xF0, 0x0F, 0x00, 0x90, 0x19, 0x81, + 0x98, 0x10, 0x83, 0x0C, 0x3F, 0xC2, 0x04, 0x60, 0x66, 0x06, 0xC0, 0x30, + 0xFF, 0x18, 0x33, 0x03, 0x60, 0x6C, 0x0D, 0x83, 0x3F, 0xC6, 0x06, 0xC0, + 0x78, 0x0F, 0x01, 0xE0, 0x6F, 0xF8, 0x1F, 0x86, 0x19, 0x81, 0xA0, 0x3C, + 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x68, 0x0D, 0x83, 0x18, 0x61, 0xF0, + 0xFF, 0x18, 0x33, 0x03, 0x60, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, + 0x78, 0x0F, 0x03, 0x60, 0xCF, 0xF0, 0xFF, 0xE0, 0x30, 0x18, 0x0C, 0x06, + 0x03, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0F, 0xF8, 0xFF, 0xC0, 0xC0, + 0xC0, 0xC0, 0xC0, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x0F, 0x83, + 0x0E, 0x60, 0x66, 0x03, 0xC0, 0x0C, 0x00, 0xC1, 0xFC, 0x03, 0xC0, 0x36, + 0x03, 0x60, 0x73, 0x0F, 0x0F, 0x10, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, + 0x07, 0x80, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x06, + 0xFF, 0xFF, 0xFF, 0xC0, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, + 0x8F, 0x1E, 0x27, 0x80, 0xC0, 0xD8, 0x33, 0x0C, 0x63, 0x0C, 0xC1, 0xB8, + 0x3F, 0x07, 0x30, 0xC3, 0x18, 0x63, 0x06, 0x60, 0x6C, 0x0C, 0xC0, 0xC0, + 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, 0xE0, + 0x3F, 0x01, 0xFC, 0x1F, 0xE0, 0xFD, 0x05, 0xEC, 0x6F, 0x63, 0x79, 0x13, + 0xCD, 0x9E, 0x6C, 0xF1, 0x47, 0x8E, 0x3C, 0x71, 0x80, 0xE0, 0x7C, 0x0F, + 0xC1, 0xE8, 0x3D, 0x87, 0x98, 0xF1, 0x1E, 0x33, 0xC3, 0x78, 0x6F, 0x07, + 0xE0, 0x7C, 0x0E, 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, 0x6C, 0x01, 0xE0, + 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x0C, 0x60, 0xC0, 0xF8, + 0x00, 0xFF, 0x30, 0x6C, 0x0F, 0x03, 0xC0, 0xF0, 0x6F, 0xF3, 0x00, 0xC0, + 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x00, 0x0F, 0x81, 0x83, 0x18, 0x0C, 0xC0, + 0x6C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1B, 0x01, 0x98, 0x6C, + 0x60, 0xC0, 0xFB, 0x00, 0x08, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, + 0x6C, 0x0C, 0xFF, 0x8C, 0x0E, 0xC0, 0x6C, 0x06, 0xC0, 0x6C, 0x06, 0xC0, + 0x70, 0x3F, 0x18, 0x6C, 0x0F, 0x03, 0xC0, 0x1E, 0x01, 0xF0, 0x0E, 0x00, + 0xF0, 0x3C, 0x0D, 0x86, 0x3F, 0x00, 0xFF, 0x86, 0x03, 0x01, 0x80, 0xC0, + 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x80, 0xC0, 0xC0, 0x78, 0x0F, + 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, + 0xB0, 0x61, 0xF0, 0xC0, 0x6C, 0x0D, 0x81, 0x10, 0x63, 0x0C, 0x61, 0x04, + 0x60, 0xCC, 0x19, 0x01, 0x60, 0x3C, 0x07, 0x00, 0x60, 0xC1, 0x81, 0x30, + 0xE1, 0x98, 0x70, 0xCC, 0x28, 0x66, 0x26, 0x21, 0x13, 0x30, 0xC8, 0x98, + 0x6C, 0x4C, 0x14, 0x34, 0x0A, 0x1A, 0x07, 0x07, 0x03, 0x03, 0x80, 0x81, + 0x80, 0x60, 0x63, 0x0C, 0x30, 0xC1, 0x98, 0x0F, 0x00, 0xE0, 0x06, 0x00, + 0xF0, 0x19, 0x01, 0x98, 0x30, 0xC6, 0x0E, 0x60, 0x60, 0xC0, 0x36, 0x06, + 0x30, 0xC3, 0x0C, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x60, 0x06, 0x00, 0x60, + 0x06, 0x00, 0x60, 0x06, 0x00, 0xFF, 0xC0, 0x60, 0x30, 0x0C, 0x06, 0x03, + 0x01, 0xC0, 0x60, 0x30, 0x18, 0x06, 0x03, 0x00, 0xFF, 0xC0, 0xFB, 0x6D, + 0xB6, 0xDB, 0x6D, 0xB6, 0xE0, 0x84, 0x10, 0x84, 0x10, 0x84, 0x10, 0x84, + 0x10, 0x80, 0xED, 0xB6, 0xDB, 0x6D, 0xB6, 0xDB, 0xE0, 0x30, 0x60, 0xA2, + 0x44, 0xD8, 0xA1, 0x80, 0xFF, 0xC0, 0xC6, 0x30, 0x7E, 0x71, 0xB0, 0xC0, + 0x60, 0xF3, 0xDB, 0x0D, 0x86, 0xC7, 0x3D, 0xC0, 0xC0, 0x60, 0x30, 0x1B, + 0xCE, 0x36, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x6D, 0xE0, 0x3C, + 0x66, 0xC3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, 0x03, 0x03, 0x03, + 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, 0x3B, 0x3C, 0x66, + 0xC3, 0xC3, 0xFF, 0xC0, 0xC0, 0xC3, 0x66, 0x3C, 0x36, 0x6F, 0x66, 0x66, + 0x66, 0x66, 0x60, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x67, + 0x3B, 0x03, 0x03, 0xC6, 0x7C, 0xC0, 0xC0, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, + 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, 0xC0, 0x30, 0x03, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xE0, 0xC0, 0x60, 0x30, 0x18, 0x4C, + 0x46, 0x63, 0x61, 0xF0, 0xEC, 0x62, 0x31, 0x98, 0x6C, 0x30, 0xFF, 0xFF, + 0xFF, 0xC0, 0xDE, 0xF7, 0x1C, 0xF0, 0xC7, 0x86, 0x3C, 0x31, 0xE1, 0x8F, + 0x0C, 0x78, 0x63, 0xC3, 0x1E, 0x18, 0xC0, 0xDE, 0xE3, 0xC3, 0xC3, 0xC3, + 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0x3C, 0x66, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, + 0xC3, 0x66, 0x3C, 0xDE, 0x71, 0xB0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, + 0xE3, 0x6F, 0x30, 0x18, 0x0C, 0x00, 0x3B, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, + 0xC3, 0xC3, 0x67, 0x3B, 0x03, 0x03, 0x03, 0xDF, 0x31, 0x8C, 0x63, 0x18, + 0xC6, 0x00, 0x3E, 0xE3, 0xC0, 0xC0, 0xE0, 0x3C, 0x07, 0xC3, 0xE3, 0x7E, + 0x66, 0xF6, 0x66, 0x66, 0x66, 0x67, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, + 0xC3, 0xC3, 0xC7, 0x7B, 0xC1, 0xA0, 0x98, 0xCC, 0x42, 0x21, 0xB0, 0xD0, + 0x28, 0x1C, 0x0C, 0x00, 0xC6, 0x1E, 0x38, 0x91, 0xC4, 0xCA, 0x66, 0xD3, + 0x16, 0xD0, 0xA6, 0x87, 0x1C, 0x38, 0xC0, 0xC6, 0x00, 0x43, 0x62, 0x36, + 0x1C, 0x18, 0x1C, 0x3C, 0x26, 0x62, 0x43, 0xC1, 0x21, 0x98, 0xCC, 0x42, + 0x61, 0xB0, 0xD0, 0x38, 0x1C, 0x0C, 0x06, 0x03, 0x01, 0x03, 0x00, 0xFE, + 0x0C, 0x30, 0xC1, 0x86, 0x18, 0x20, 0xC1, 0xFC, 0x36, 0x66, 0x66, 0x6E, + 0xCE, 0x66, 0x66, 0x66, 0x30, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xC6, 0x66, + 0x66, 0x67, 0x37, 0x66, 0x66, 0x66, 0xC0, 0x61, 0x24, 0x38 }; const GFXglyph FreeSans9pt7bGlyphs[] PROGMEM = { - {0, 0, 0, 5, 0, 1}, // 0x20 ' ' - {0, 2, 13, 6, 2, -12}, // 0x21 '!' - {4, 5, 4, 6, 1, -12}, // 0x22 '"' - {7, 10, 12, 10, 0, -11}, // 0x23 '#' - {22, 9, 16, 10, 1, -13}, // 0x24 '$' - {40, 16, 13, 16, 1, -12}, // 0x25 '%' - {66, 11, 13, 12, 1, -12}, // 0x26 '&' - {84, 2, 4, 4, 1, -12}, // 0x27 ''' - {85, 4, 17, 6, 1, -12}, // 0x28 '(' - {94, 4, 17, 6, 1, -12}, // 0x29 ')' - {103, 5, 5, 7, 1, -12}, // 0x2A '*' - {107, 6, 8, 11, 3, -7}, // 0x2B '+' - {113, 2, 4, 5, 2, 0}, // 0x2C ',' - {114, 4, 1, 6, 1, -4}, // 0x2D '-' - {115, 2, 1, 5, 1, 0}, // 0x2E '.' - {116, 5, 13, 5, 0, -12}, // 0x2F '/' - {125, 8, 13, 10, 1, -12}, // 0x30 '0' - {138, 4, 13, 10, 3, -12}, // 0x31 '1' - {145, 9, 13, 10, 1, -12}, // 0x32 '2' - {160, 8, 13, 10, 1, -12}, // 0x33 '3' - {173, 7, 13, 10, 2, -12}, // 0x34 '4' - {185, 9, 13, 10, 1, -12}, // 0x35 '5' - {200, 9, 13, 10, 1, -12}, // 0x36 '6' - {215, 8, 13, 10, 0, -12}, // 0x37 '7' - {228, 9, 13, 10, 1, -12}, // 0x38 '8' - {243, 8, 13, 10, 1, -12}, // 0x39 '9' - {256, 2, 10, 5, 1, -9}, // 0x3A ':' - {259, 3, 12, 5, 1, -8}, // 0x3B ';' - {264, 9, 9, 11, 1, -8}, // 0x3C '<' - {275, 9, 4, 11, 1, -5}, // 0x3D '=' - {280, 9, 9, 11, 1, -8}, // 0x3E '>' - {291, 9, 13, 10, 1, -12}, // 0x3F '?' - {306, 17, 16, 18, 1, -12}, // 0x40 '@' - {340, 12, 13, 12, 0, -12}, // 0x41 'A' - {360, 11, 13, 12, 1, -12}, // 0x42 'B' - {378, 11, 13, 13, 1, -12}, // 0x43 'C' - {396, 11, 13, 13, 1, -12}, // 0x44 'D' - {414, 9, 13, 11, 1, -12}, // 0x45 'E' - {429, 8, 13, 11, 1, -12}, // 0x46 'F' - {442, 12, 13, 14, 1, -12}, // 0x47 'G' - {462, 11, 13, 13, 1, -12}, // 0x48 'H' - {480, 2, 13, 5, 2, -12}, // 0x49 'I' - {484, 7, 13, 10, 1, -12}, // 0x4A 'J' - {496, 11, 13, 12, 1, -12}, // 0x4B 'K' - {514, 8, 13, 10, 1, -12}, // 0x4C 'L' - {527, 13, 13, 15, 1, -12}, // 0x4D 'M' - {549, 11, 13, 13, 1, -12}, // 0x4E 'N' - {567, 13, 13, 14, 1, -12}, // 0x4F 'O' - {589, 10, 13, 12, 1, -12}, // 0x50 'P' - {606, 13, 14, 14, 1, -12}, // 0x51 'Q' - {629, 12, 13, 13, 1, -12}, // 0x52 'R' - {649, 10, 13, 12, 1, -12}, // 0x53 'S' - {666, 9, 13, 11, 1, -12}, // 0x54 'T' - {681, 11, 13, 13, 1, -12}, // 0x55 'U' - {699, 11, 13, 12, 0, -12}, // 0x56 'V' - {717, 17, 13, 17, 0, -12}, // 0x57 'W' - {745, 12, 13, 12, 0, -12}, // 0x58 'X' - {765, 12, 13, 12, 0, -12}, // 0x59 'Y' - {785, 10, 13, 11, 1, -12}, // 0x5A 'Z' - {802, 3, 17, 5, 1, -12}, // 0x5B '[' - {809, 5, 13, 5, 0, -12}, // 0x5C '\' - {818, 3, 17, 5, 0, -12}, // 0x5D ']' - {825, 7, 7, 8, 1, -12}, // 0x5E '^' - {832, 10, 1, 10, 0, 3}, // 0x5F '_' - {834, 4, 3, 5, 0, -12}, // 0x60 '`' - {836, 9, 10, 10, 1, -9}, // 0x61 'a' - {848, 9, 13, 10, 1, -12}, // 0x62 'b' - {863, 8, 10, 9, 1, -9}, // 0x63 'c' - {873, 8, 13, 10, 1, -12}, // 0x64 'd' - {886, 8, 10, 10, 1, -9}, // 0x65 'e' - {896, 4, 13, 5, 1, -12}, // 0x66 'f' - {903, 8, 14, 10, 1, -9}, // 0x67 'g' - {917, 8, 13, 10, 1, -12}, // 0x68 'h' - {930, 2, 13, 4, 1, -12}, // 0x69 'i' - {934, 4, 17, 4, 0, -12}, // 0x6A 'j' - {943, 9, 13, 9, 1, -12}, // 0x6B 'k' - {958, 2, 13, 4, 1, -12}, // 0x6C 'l' - {962, 13, 10, 15, 1, -9}, // 0x6D 'm' - {979, 8, 10, 10, 1, -9}, // 0x6E 'n' - {989, 8, 10, 10, 1, -9}, // 0x6F 'o' - {999, 9, 13, 10, 1, -9}, // 0x70 'p' - {1014, 8, 13, 10, 1, -9}, // 0x71 'q' - {1027, 5, 10, 6, 1, -9}, // 0x72 'r' - {1034, 8, 10, 9, 1, -9}, // 0x73 's' - {1044, 4, 12, 5, 1, -11}, // 0x74 't' - {1050, 8, 10, 10, 1, -9}, // 0x75 'u' - {1060, 9, 10, 9, 0, -9}, // 0x76 'v' - {1072, 13, 10, 13, 0, -9}, // 0x77 'w' - {1089, 8, 10, 9, 0, -9}, // 0x78 'x' - {1099, 9, 14, 9, 0, -9}, // 0x79 'y' - {1115, 7, 10, 9, 1, -9}, // 0x7A 'z' - {1124, 4, 17, 6, 1, -12}, // 0x7B '{' - {1133, 2, 17, 4, 2, -12}, // 0x7C '|' - {1138, 4, 17, 6, 1, -12}, // 0x7D '}' - {1147, 7, 3, 9, 1, -7}}; // 0x7E '~' + { 0, 0, 0, 5, 0, 1 }, // 0x20 ' ' + { 0, 2, 13, 6, 2, -12 }, // 0x21 '!' + { 4, 5, 4, 6, 1, -12 }, // 0x22 '"' + { 7, 10, 12, 10, 0, -11 }, // 0x23 '#' + { 22, 9, 16, 10, 1, -13 }, // 0x24 '$' + { 40, 16, 13, 16, 1, -12 }, // 0x25 '%' + { 66, 11, 13, 12, 1, -12 }, // 0x26 '&' + { 84, 2, 4, 4, 1, -12 }, // 0x27 ''' + { 85, 4, 17, 6, 1, -12 }, // 0x28 '(' + { 94, 4, 17, 6, 1, -12 }, // 0x29 ')' + { 103, 5, 5, 7, 1, -12 }, // 0x2A '*' + { 107, 6, 8, 11, 3, -7 }, // 0x2B '+' + { 113, 2, 4, 5, 2, 0 }, // 0x2C ',' + { 114, 4, 1, 6, 1, -4 }, // 0x2D '-' + { 115, 2, 1, 5, 1, 0 }, // 0x2E '.' + { 116, 5, 13, 5, 0, -12 }, // 0x2F '/' + { 125, 8, 13, 10, 1, -12 }, // 0x30 '0' + { 138, 4, 13, 10, 3, -12 }, // 0x31 '1' + { 145, 9, 13, 10, 1, -12 }, // 0x32 '2' + { 160, 8, 13, 10, 1, -12 }, // 0x33 '3' + { 173, 7, 13, 10, 2, -12 }, // 0x34 '4' + { 185, 9, 13, 10, 1, -12 }, // 0x35 '5' + { 200, 9, 13, 10, 1, -12 }, // 0x36 '6' + { 215, 8, 13, 10, 0, -12 }, // 0x37 '7' + { 228, 9, 13, 10, 1, -12 }, // 0x38 '8' + { 243, 8, 13, 10, 1, -12 }, // 0x39 '9' + { 256, 2, 10, 5, 1, -9 }, // 0x3A ':' + { 259, 3, 12, 5, 1, -8 }, // 0x3B ';' + { 264, 9, 9, 11, 1, -8 }, // 0x3C '<' + { 275, 9, 4, 11, 1, -5 }, // 0x3D '=' + { 280, 9, 9, 11, 1, -8 }, // 0x3E '>' + { 291, 9, 13, 10, 1, -12 }, // 0x3F '?' + { 306, 17, 16, 18, 1, -12 }, // 0x40 '@' + { 340, 12, 13, 12, 0, -12 }, // 0x41 'A' + { 360, 11, 13, 12, 1, -12 }, // 0x42 'B' + { 378, 11, 13, 13, 1, -12 }, // 0x43 'C' + { 396, 11, 13, 13, 1, -12 }, // 0x44 'D' + { 414, 9, 13, 11, 1, -12 }, // 0x45 'E' + { 429, 8, 13, 11, 1, -12 }, // 0x46 'F' + { 442, 12, 13, 14, 1, -12 }, // 0x47 'G' + { 462, 11, 13, 13, 1, -12 }, // 0x48 'H' + { 480, 2, 13, 5, 2, -12 }, // 0x49 'I' + { 484, 7, 13, 10, 1, -12 }, // 0x4A 'J' + { 496, 11, 13, 12, 1, -12 }, // 0x4B 'K' + { 514, 8, 13, 10, 1, -12 }, // 0x4C 'L' + { 527, 13, 13, 15, 1, -12 }, // 0x4D 'M' + { 549, 11, 13, 13, 1, -12 }, // 0x4E 'N' + { 567, 13, 13, 14, 1, -12 }, // 0x4F 'O' + { 589, 10, 13, 12, 1, -12 }, // 0x50 'P' + { 606, 13, 14, 14, 1, -12 }, // 0x51 'Q' + { 629, 12, 13, 13, 1, -12 }, // 0x52 'R' + { 649, 10, 13, 12, 1, -12 }, // 0x53 'S' + { 666, 9, 13, 11, 1, -12 }, // 0x54 'T' + { 681, 11, 13, 13, 1, -12 }, // 0x55 'U' + { 699, 11, 13, 12, 0, -12 }, // 0x56 'V' + { 717, 17, 13, 17, 0, -12 }, // 0x57 'W' + { 745, 12, 13, 12, 0, -12 }, // 0x58 'X' + { 765, 12, 13, 12, 0, -12 }, // 0x59 'Y' + { 785, 10, 13, 11, 1, -12 }, // 0x5A 'Z' + { 802, 3, 17, 5, 1, -12 }, // 0x5B '[' + { 809, 5, 13, 5, 0, -12 }, // 0x5C '\' + { 818, 3, 17, 5, 0, -12 }, // 0x5D ']' + { 825, 7, 7, 8, 1, -12 }, // 0x5E '^' + { 832, 10, 1, 10, 0, 3 }, // 0x5F '_' + { 834, 4, 3, 5, 0, -12 }, // 0x60 '`' + { 836, 9, 10, 10, 1, -9 }, // 0x61 'a' + { 848, 9, 13, 10, 1, -12 }, // 0x62 'b' + { 863, 8, 10, 9, 1, -9 }, // 0x63 'c' + { 873, 8, 13, 10, 1, -12 }, // 0x64 'd' + { 886, 8, 10, 10, 1, -9 }, // 0x65 'e' + { 896, 4, 13, 5, 1, -12 }, // 0x66 'f' + { 903, 8, 14, 10, 1, -9 }, // 0x67 'g' + { 917, 8, 13, 10, 1, -12 }, // 0x68 'h' + { 930, 2, 13, 4, 1, -12 }, // 0x69 'i' + { 934, 4, 17, 4, 0, -12 }, // 0x6A 'j' + { 943, 9, 13, 9, 1, -12 }, // 0x6B 'k' + { 958, 2, 13, 4, 1, -12 }, // 0x6C 'l' + { 962, 13, 10, 15, 1, -9 }, // 0x6D 'm' + { 979, 8, 10, 10, 1, -9 }, // 0x6E 'n' + { 989, 8, 10, 10, 1, -9 }, // 0x6F 'o' + { 999, 9, 13, 10, 1, -9 }, // 0x70 'p' + { 1014, 8, 13, 10, 1, -9 }, // 0x71 'q' + { 1027, 5, 10, 6, 1, -9 }, // 0x72 'r' + { 1034, 8, 10, 9, 1, -9 }, // 0x73 's' + { 1044, 4, 12, 5, 1, -11 }, // 0x74 't' + { 1050, 8, 10, 10, 1, -9 }, // 0x75 'u' + { 1060, 9, 10, 9, 0, -9 }, // 0x76 'v' + { 1072, 13, 10, 13, 0, -9 }, // 0x77 'w' + { 1089, 8, 10, 9, 0, -9 }, // 0x78 'x' + { 1099, 9, 14, 9, 0, -9 }, // 0x79 'y' + { 1115, 7, 10, 9, 1, -9 }, // 0x7A 'z' + { 1124, 4, 17, 6, 1, -12 }, // 0x7B '{' + { 1133, 2, 17, 4, 2, -12 }, // 0x7C '|' + { 1138, 4, 17, 6, 1, -12 }, // 0x7D '}' + { 1147, 7, 3, 9, 1, -7 } }; // 0x7E '~' -const GFXfont FreeSans9pt7b PROGMEM = {(uint8_t *)FreeSans9pt7bBitmaps, - (GFXglyph *)FreeSans9pt7bGlyphs, 0x20, - 0x7E, 22}; +const GFXfont FreeSans9pt7b PROGMEM = { (uint8_t *)FreeSans9pt7bBitmaps, + (GFXglyph *)FreeSans9pt7bGlyphs,0x20, + 0x7E, 22 }; // Approx. 1822 bytes +#endif // ifndef FONTS_FREESANS9PT7B_H diff --git a/src/src/Static/Fonts/NovaMono12pt7b.h b/src/src/Static/Fonts/NovaMono12pt7b.h index c3c8184197..f6adbad8d3 100644 --- a/src/src/Static/Fonts/NovaMono12pt7b.h +++ b/src/src/Static/Fonts/NovaMono12pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_NOVAMONO12PT7B_H +#define FONTS_NOVAMONO12PT7B_H const uint8_t NovaMono12pt7bBitmaps[] PROGMEM = { 0x00, 0x5B, 0x6D, 0xB6, 0xDB, 0x6D, 0x06, 0xF8, 0x47, 0x3C, 0xF3, 0xCE, 0x20, 0x19, 0x81, 0x98, 0x19, 0x8F, 0xFF, 0xFF, 0xF1, 0x98, 0x19, 0x8F, @@ -159,105 +161,106 @@ const uint8_t NovaMono12pt7bBitmaps[] PROGMEM = { 0x00, 0x38, 0x2F, 0xCD, 0x9F, 0x61, 0xC0 }; const GFXglyph NovaMono12pt7bGlyphs[] PROGMEM = { - { 0, 1, 1, 13, 0, 0 }, // 0x20 ' ' - { 1, 3, 18, 13, 5, -17 }, // 0x21 '!' - { 8, 6, 6, 13, 4, -17 }, // 0x22 '"' - { 13, 12, 12, 13, 1, -14 }, // 0x23 '#' - { 31, 11, 26, 13, 1, -21 }, // 0x24 '$' - { 67, 12, 18, 13, 1, -17 }, // 0x25 '%' - { 94, 13, 18, 13, 0, -17 }, // 0x26 '&' - { 124, 2, 6, 13, 6, -17 }, // 0x27 ''' - { 126, 6, 26, 13, 5, -21 }, // 0x28 '(' - { 146, 6, 26, 13, 2, -21 }, // 0x29 ')' - { 166, 10, 10, 13, 2, -17 }, // 0x2A '*' - { 179, 10, 10, 13, 2, -13 }, // 0x2B '+' - { 192, 3, 6, 13, 5, -2 }, // 0x2C ',' - { 195, 10, 2, 13, 2, -9 }, // 0x2D '-' - { 198, 3, 3, 13, 5, -2 }, // 0x2E '.' - { 200, 12, 26, 13, 1, -21 }, // 0x2F '/' - { 239, 11, 18, 13, 1, -17 }, // 0x30 '0' - { 264, 6, 18, 13, 2, -17 }, // 0x31 '1' - { 278, 11, 18, 13, 1, -17 }, // 0x32 '2' - { 303, 11, 18, 13, 1, -17 }, // 0x33 '3' - { 328, 12, 18, 13, 1, -17 }, // 0x34 '4' - { 355, 11, 18, 13, 1, -17 }, // 0x35 '5' - { 380, 11, 18, 13, 1, -17 }, // 0x36 '6' - { 405, 11, 18, 13, 1, -17 }, // 0x37 '7' - { 430, 11, 18, 13, 1, -17 }, // 0x38 '8' - { 455, 11, 18, 13, 1, -17 }, // 0x39 '9' - { 480, 3, 10, 13, 5, -9 }, // 0x3A ':' - { 484, 3, 13, 13, 5, -9 }, // 0x3B ';' - { 489, 10, 10, 13, 2, -13 }, // 0x3C '<' - { 502, 10, 6, 13, 2, -11 }, // 0x3D '=' - { 510, 10, 10, 13, 2, -13 }, // 0x3E '>' - { 523, 11, 18, 13, 1, -17 }, // 0x3F '?' - { 548, 10, 18, 13, 2, -12 }, // 0x40 '@' - { 571, 11, 18, 13, 1, -17 }, // 0x41 'A' - { 596, 11, 18, 13, 1, -17 }, // 0x42 'B' - { 621, 11, 18, 13, 1, -17 }, // 0x43 'C' - { 646, 11, 18, 13, 1, -17 }, // 0x44 'D' - { 671, 10, 18, 13, 2, -17 }, // 0x45 'E' - { 694, 9, 18, 13, 2, -17 }, // 0x46 'F' - { 715, 11, 18, 13, 1, -17 }, // 0x47 'G' - { 740, 11, 18, 13, 1, -17 }, // 0x48 'H' - { 765, 10, 18, 13, 2, -17 }, // 0x49 'I' - { 788, 11, 18, 13, 1, -17 }, // 0x4A 'J' - { 813, 11, 18, 13, 1, -17 }, // 0x4B 'K' - { 838, 10, 18, 13, 2, -17 }, // 0x4C 'L' - { 861, 11, 18, 13, 1, -17 }, // 0x4D 'M' - { 886, 11, 18, 13, 1, -17 }, // 0x4E 'N' - { 911, 11, 18, 13, 1, -17 }, // 0x4F 'O' - { 936, 11, 18, 13, 1, -17 }, // 0x50 'P' - { 961, 11, 20, 13, 1, -17 }, // 0x51 'Q' - { 989, 11, 18, 13, 1, -17 }, // 0x52 'R' - { 1014, 11, 18, 13, 1, -17 }, // 0x53 'S' - { 1039, 12, 18, 13, 1, -17 }, // 0x54 'T' - { 1066, 11, 18, 13, 1, -17 }, // 0x55 'U' - { 1091, 12, 18, 13, 1, -17 }, // 0x56 'V' - { 1118, 11, 18, 13, 1, -17 }, // 0x57 'W' - { 1143, 11, 18, 13, 1, -17 }, // 0x58 'X' - { 1168, 11, 18, 13, 1, -17 }, // 0x59 'Y' - { 1193, 11, 18, 13, 1, -17 }, // 0x5A 'Z' - { 1218, 6, 26, 13, 5, -21 }, // 0x5B '[' - { 1238, 12, 26, 13, 1, -21 }, // 0x5C '\' - { 1277, 6, 26, 13, 2, -21 }, // 0x5D ']' - { 1297, 9, 5, 13, 2, -17 }, // 0x5E '^' - { 1303, 10, 2, 13, 2, -1 }, // 0x5F '_' - { 1306, 5, 6, 13, 4, -17 }, // 0x60 '`' - { 1310, 9, 13, 13, 2, -12 }, // 0x61 'a' - { 1325, 9, 18, 13, 2, -17 }, // 0x62 'b' - { 1346, 9, 13, 13, 2, -12 }, // 0x63 'c' - { 1361, 9, 18, 13, 2, -17 }, // 0x64 'd' - { 1382, 9, 13, 13, 2, -12 }, // 0x65 'e' - { 1397, 10, 18, 13, 1, -17 }, // 0x66 'f' - { 1420, 9, 18, 13, 2, -12 }, // 0x67 'g' - { 1441, 9, 18, 13, 2, -17 }, // 0x68 'h' - { 1462, 9, 18, 13, 2, -17 }, // 0x69 'i' - { 1483, 10, 23, 13, 2, -17 }, // 0x6A 'j' - { 1512, 9, 18, 13, 2, -17 }, // 0x6B 'k' - { 1533, 9, 18, 13, 2, -17 }, // 0x6C 'l' - { 1554, 11, 13, 13, 1, -12 }, // 0x6D 'm' - { 1572, 9, 13, 13, 2, -12 }, // 0x6E 'n' - { 1587, 9, 13, 13, 2, -12 }, // 0x6F 'o' - { 1602, 9, 18, 13, 2, -12 }, // 0x70 'p' - { 1623, 12, 18, 13, 2, -12 }, // 0x71 'q' - { 1650, 9, 13, 13, 2, -12 }, // 0x72 'r' - { 1665, 9, 13, 13, 2, -12 }, // 0x73 's' - { 1680, 10, 18, 13, 1, -17 }, // 0x74 't' - { 1703, 9, 13, 13, 2, -12 }, // 0x75 'u' - { 1718, 12, 13, 13, 1, -12 }, // 0x76 'v' - { 1738, 11, 13, 13, 1, -12 }, // 0x77 'w' - { 1756, 11, 13, 13, 1, -12 }, // 0x78 'x' - { 1774, 9, 18, 13, 2, -12 }, // 0x79 'y' - { 1795, 10, 13, 13, 2, -12 }, // 0x7A 'z' - { 1812, 10, 26, 13, 1, -21 }, // 0x7B '{' - { 1845, 2, 26, 13, 6, -21 }, // 0x7C '|' - { 1852, 10, 26, 13, 2, -21 }, // 0x7D '}' - { 1885, 11, 4, 13, 1, -10 } }; // 0x7E '~' + { 0, 1, 1, 13, 0, 0 }, // 0x20 ' ' + { 1, 3, 18, 13, 5, -17 }, // 0x21 '!' + { 8, 6, 6, 13, 4, -17 }, // 0x22 '"' + { 13, 12, 12, 13, 1, -14 }, // 0x23 '#' + { 31, 11, 26, 13, 1, -21 }, // 0x24 '$' + { 67, 12, 18, 13, 1, -17 }, // 0x25 '%' + { 94, 13, 18, 13, 0, -17 }, // 0x26 '&' + { 124, 2, 6, 13, 6, -17 }, // 0x27 ''' + { 126, 6, 26, 13, 5, -21 }, // 0x28 '(' + { 146, 6, 26, 13, 2, -21 }, // 0x29 ')' + { 166, 10, 10, 13, 2, -17 }, // 0x2A '*' + { 179, 10, 10, 13, 2, -13 }, // 0x2B '+' + { 192, 3, 6, 13, 5, -2 }, // 0x2C ',' + { 195, 10, 2, 13, 2, -9 }, // 0x2D '-' + { 198, 3, 3, 13, 5, -2 }, // 0x2E '.' + { 200, 12, 26, 13, 1, -21 }, // 0x2F '/' + { 239, 11, 18, 13, 1, -17 }, // 0x30 '0' + { 264, 6, 18, 13, 2, -17 }, // 0x31 '1' + { 278, 11, 18, 13, 1, -17 }, // 0x32 '2' + { 303, 11, 18, 13, 1, -17 }, // 0x33 '3' + { 328, 12, 18, 13, 1, -17 }, // 0x34 '4' + { 355, 11, 18, 13, 1, -17 }, // 0x35 '5' + { 380, 11, 18, 13, 1, -17 }, // 0x36 '6' + { 405, 11, 18, 13, 1, -17 }, // 0x37 '7' + { 430, 11, 18, 13, 1, -17 }, // 0x38 '8' + { 455, 11, 18, 13, 1, -17 }, // 0x39 '9' + { 480, 3, 10, 13, 5, -9 }, // 0x3A ':' + { 484, 3, 13, 13, 5, -9 }, // 0x3B ';' + { 489, 10, 10, 13, 2, -13 }, // 0x3C '<' + { 502, 10, 6, 13, 2, -11 }, // 0x3D '=' + { 510, 10, 10, 13, 2, -13 }, // 0x3E '>' + { 523, 11, 18, 13, 1, -17 }, // 0x3F '?' + { 548, 10, 18, 13, 2, -12 }, // 0x40 '@' + { 571, 11, 18, 13, 1, -17 }, // 0x41 'A' + { 596, 11, 18, 13, 1, -17 }, // 0x42 'B' + { 621, 11, 18, 13, 1, -17 }, // 0x43 'C' + { 646, 11, 18, 13, 1, -17 }, // 0x44 'D' + { 671, 10, 18, 13, 2, -17 }, // 0x45 'E' + { 694, 9, 18, 13, 2, -17 }, // 0x46 'F' + { 715, 11, 18, 13, 1, -17 }, // 0x47 'G' + { 740, 11, 18, 13, 1, -17 }, // 0x48 'H' + { 765, 10, 18, 13, 2, -17 }, // 0x49 'I' + { 788, 11, 18, 13, 1, -17 }, // 0x4A 'J' + { 813, 11, 18, 13, 1, -17 }, // 0x4B 'K' + { 838, 10, 18, 13, 2, -17 }, // 0x4C 'L' + { 861, 11, 18, 13, 1, -17 }, // 0x4D 'M' + { 886, 11, 18, 13, 1, -17 }, // 0x4E 'N' + { 911, 11, 18, 13, 1, -17 }, // 0x4F 'O' + { 936, 11, 18, 13, 1, -17 }, // 0x50 'P' + { 961, 11, 20, 13, 1, -17 }, // 0x51 'Q' + { 989, 11, 18, 13, 1, -17 }, // 0x52 'R' + { 1014, 11, 18, 13, 1, -17 }, // 0x53 'S' + { 1039, 12, 18, 13, 1, -17 }, // 0x54 'T' + { 1066, 11, 18, 13, 1, -17 }, // 0x55 'U' + { 1091, 12, 18, 13, 1, -17 }, // 0x56 'V' + { 1118, 11, 18, 13, 1, -17 }, // 0x57 'W' + { 1143, 11, 18, 13, 1, -17 }, // 0x58 'X' + { 1168, 11, 18, 13, 1, -17 }, // 0x59 'Y' + { 1193, 11, 18, 13, 1, -17 }, // 0x5A 'Z' + { 1218, 6, 26, 13, 5, -21 }, // 0x5B '[' + { 1238, 12, 26, 13, 1, -21 }, // 0x5C '\' + { 1277, 6, 26, 13, 2, -21 }, // 0x5D ']' + { 1297, 9, 5, 13, 2, -17 }, // 0x5E '^' + { 1303, 10, 2, 13, 2, -1 }, // 0x5F '_' + { 1306, 5, 6, 13, 4, -17 }, // 0x60 '`' + { 1310, 9, 13, 13, 2, -12 }, // 0x61 'a' + { 1325, 9, 18, 13, 2, -17 }, // 0x62 'b' + { 1346, 9, 13, 13, 2, -12 }, // 0x63 'c' + { 1361, 9, 18, 13, 2, -17 }, // 0x64 'd' + { 1382, 9, 13, 13, 2, -12 }, // 0x65 'e' + { 1397, 10, 18, 13, 1, -17 }, // 0x66 'f' + { 1420, 9, 18, 13, 2, -12 }, // 0x67 'g' + { 1441, 9, 18, 13, 2, -17 }, // 0x68 'h' + { 1462, 9, 18, 13, 2, -17 }, // 0x69 'i' + { 1483, 10, 23, 13, 2, -17 }, // 0x6A 'j' + { 1512, 9, 18, 13, 2, -17 }, // 0x6B 'k' + { 1533, 9, 18, 13, 2, -17 }, // 0x6C 'l' + { 1554, 11, 13, 13, 1, -12 }, // 0x6D 'm' + { 1572, 9, 13, 13, 2, -12 }, // 0x6E 'n' + { 1587, 9, 13, 13, 2, -12 }, // 0x6F 'o' + { 1602, 9, 18, 13, 2, -12 }, // 0x70 'p' + { 1623, 12, 18, 13, 2, -12 }, // 0x71 'q' + { 1650, 9, 13, 13, 2, -12 }, // 0x72 'r' + { 1665, 9, 13, 13, 2, -12 }, // 0x73 's' + { 1680, 10, 18, 13, 1, -17 }, // 0x74 't' + { 1703, 9, 13, 13, 2, -12 }, // 0x75 'u' + { 1718, 12, 13, 13, 1, -12 }, // 0x76 'v' + { 1738, 11, 13, 13, 1, -12 }, // 0x77 'w' + { 1756, 11, 13, 13, 1, -12 }, // 0x78 'x' + { 1774, 9, 18, 13, 2, -12 }, // 0x79 'y' + { 1795, 10, 13, 13, 2, -12 }, // 0x7A 'z' + { 1812, 10, 26, 13, 1, -21 }, // 0x7B '{' + { 1845, 2, 26, 13, 6, -21 }, // 0x7C '|' + { 1852, 10, 26, 13, 2, -21 }, // 0x7D '}' + { 1885, 11, 4, 13, 1, -10 } }; // 0x7E '~' const GFXfont NovaMono12pt7b PROGMEM = { (uint8_t *)NovaMono12pt7bBitmaps, (GFXglyph *)NovaMono12pt7bGlyphs, - 0x20, 0x7E, 33 }; + 0x20, 0x7E, 33 }; // Approx. 2563 bytes +#endif // ifndef FONTS_NOVAMONO12PT7B_H diff --git a/src/src/Static/Fonts/NovaMono8pt7b.h b/src/src/Static/Fonts/NovaMono8pt7b.h index aca048532b..c5598b61c1 100644 --- a/src/src/Static/Fonts/NovaMono8pt7b.h +++ b/src/src/Static/Fonts/NovaMono8pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_NOVAMONO8PT7B_H +#define FONTS_NOVAMONO8PT7B_H const uint8_t NovaMono8pt7bBitmaps[] PROGMEM = { 0x00, 0x55, 0x55, 0x4F, 0xB6, 0xD0, 0x28, 0x53, 0xF9, 0x42, 0x9F, 0xCA, 0x14, 0x00, 0x20, 0x41, 0x87, 0xD2, 0xA4, 0x68, 0x78, 0x38, 0x4C, 0x99, @@ -69,105 +71,106 @@ const uint8_t NovaMono8pt7bBitmaps[] PROGMEM = { 0x40, 0x81, 0x02, 0x04, 0x18, 0xE0, 0x73, 0x38 }; const GFXglyph NovaMono8pt7bGlyphs[] PROGMEM = { - { 0, 1, 1, 9, 0, 0 }, // 0x20 ' ' - { 1, 2, 12, 9, 3, -11 }, // 0x21 '!' - { 4, 3, 4, 9, 3, -11 }, // 0x22 '"' - { 6, 7, 8, 9, 1, -9 }, // 0x23 '#' - { 13, 7, 18, 9, 1, -14 }, // 0x24 '$' - { 29, 8, 12, 9, 0, -11 }, // 0x25 '%' - { 41, 9, 11, 9, 0, -10 }, // 0x26 '&' - { 54, 1, 4, 9, 4, -11 }, // 0x27 ''' - { 55, 3, 18, 9, 3, -14 }, // 0x28 '(' - { 62, 3, 18, 9, 2, -14 }, // 0x29 ')' - { 69, 7, 7, 9, 1, -11 }, // 0x2A '*' - { 76, 7, 7, 9, 1, -8 }, // 0x2B '+' - { 83, 2, 4, 9, 3, -1 }, // 0x2C ',' - { 84, 7, 1, 9, 1, -5 }, // 0x2D '-' - { 85, 2, 2, 9, 3, -1 }, // 0x2E '.' - { 86, 9, 18, 9, 0, -14 }, // 0x2F '/' - { 107, 7, 12, 9, 1, -11 }, // 0x30 '0' - { 118, 4, 12, 9, 1, -11 }, // 0x31 '1' - { 124, 7, 12, 9, 1, -11 }, // 0x32 '2' - { 135, 7, 11, 9, 1, -10 }, // 0x33 '3' - { 145, 7, 12, 9, 1, -11 }, // 0x34 '4' - { 156, 7, 11, 9, 1, -10 }, // 0x35 '5' - { 166, 7, 12, 9, 1, -11 }, // 0x36 '6' - { 177, 7, 11, 9, 1, -10 }, // 0x37 '7' - { 187, 7, 12, 9, 1, -11 }, // 0x38 '8' - { 198, 7, 12, 9, 1, -11 }, // 0x39 '9' - { 209, 2, 7, 9, 3, -6 }, // 0x3A ':' - { 211, 2, 9, 9, 3, -6 }, // 0x3B ';' - { 214, 7, 6, 9, 1, -8 }, // 0x3C '<' - { 220, 7, 4, 9, 1, -7 }, // 0x3D '=' - { 224, 7, 6, 9, 1, -8 }, // 0x3E '>' - { 230, 7, 12, 9, 1, -11 }, // 0x3F '?' - { 241, 6, 12, 9, 1, -8 }, // 0x40 '@' - { 250, 7, 12, 9, 1, -11 }, // 0x41 'A' - { 261, 7, 11, 9, 1, -10 }, // 0x42 'B' - { 271, 7, 12, 9, 1, -11 }, // 0x43 'C' - { 282, 7, 11, 9, 1, -10 }, // 0x44 'D' - { 292, 7, 11, 9, 2, -10 }, // 0x45 'E' - { 302, 6, 11, 9, 2, -10 }, // 0x46 'F' - { 311, 7, 12, 9, 1, -11 }, // 0x47 'G' - { 322, 7, 12, 9, 1, -11 }, // 0x48 'H' - { 333, 7, 11, 9, 1, -10 }, // 0x49 'I' - { 343, 7, 11, 9, 1, -10 }, // 0x4A 'J' - { 353, 7, 12, 9, 1, -11 }, // 0x4B 'K' - { 364, 7, 12, 9, 1, -11 }, // 0x4C 'L' - { 375, 7, 12, 9, 1, -11 }, // 0x4D 'M' - { 386, 7, 12, 9, 1, -11 }, // 0x4E 'N' - { 397, 7, 12, 9, 1, -11 }, // 0x4F 'O' - { 408, 7, 11, 9, 1, -10 }, // 0x50 'P' - { 418, 7, 13, 9, 1, -11 }, // 0x51 'Q' - { 430, 7, 11, 9, 1, -10 }, // 0x52 'R' - { 440, 7, 12, 9, 1, -11 }, // 0x53 'S' - { 451, 9, 11, 9, 0, -10 }, // 0x54 'T' - { 464, 7, 12, 9, 1, -11 }, // 0x55 'U' - { 475, 9, 12, 9, 0, -11 }, // 0x56 'V' - { 489, 7, 12, 9, 1, -11 }, // 0x57 'W' - { 500, 7, 12, 9, 1, -11 }, // 0x58 'X' - { 511, 7, 12, 9, 1, -11 }, // 0x59 'Y' - { 522, 7, 11, 9, 1, -10 }, // 0x5A 'Z' - { 532, 4, 17, 9, 3, -13 }, // 0x5B '[' - { 541, 9, 18, 9, 0, -14 }, // 0x5C '\' - { 562, 4, 17, 9, 1, -13 }, // 0x5D ']' - { 571, 7, 3, 9, 1, -11 }, // 0x5E '^' - { 574, 7, 1, 9, 1, 0 }, // 0x5F '_' - { 575, 3, 4, 9, 3, -11 }, // 0x60 '`' - { 577, 5, 9, 9, 2, -8 }, // 0x61 'a' - { 583, 5, 12, 9, 2, -11 }, // 0x62 'b' - { 591, 5, 9, 9, 2, -8 }, // 0x63 'c' - { 597, 5, 12, 9, 2, -11 }, // 0x64 'd' - { 605, 5, 9, 9, 2, -8 }, // 0x65 'e' - { 611, 6, 12, 9, 1, -11 }, // 0x66 'f' - { 620, 5, 12, 9, 2, -8 }, // 0x67 'g' - { 628, 5, 12, 9, 2, -11 }, // 0x68 'h' - { 636, 6, 12, 9, 1, -11 }, // 0x69 'i' - { 645, 5, 15, 9, 2, -11 }, // 0x6A 'j' - { 655, 5, 12, 9, 2, -11 }, // 0x6B 'k' - { 663, 5, 12, 9, 2, -11 }, // 0x6C 'l' - { 671, 7, 9, 9, 1, -8 }, // 0x6D 'm' - { 679, 5, 9, 9, 2, -8 }, // 0x6E 'n' - { 685, 5, 9, 9, 2, -8 }, // 0x6F 'o' - { 691, 5, 12, 9, 2, -8 }, // 0x70 'p' - { 699, 7, 12, 9, 2, -8 }, // 0x71 'q' - { 710, 5, 9, 9, 2, -8 }, // 0x72 'r' - { 716, 6, 9, 9, 1, -8 }, // 0x73 's' - { 723, 6, 12, 9, 1, -11 }, // 0x74 't' - { 732, 5, 9, 9, 2, -8 }, // 0x75 'u' - { 738, 7, 9, 9, 1, -8 }, // 0x76 'v' - { 746, 7, 9, 9, 1, -8 }, // 0x77 'w' - { 754, 7, 9, 9, 1, -8 }, // 0x78 'x' - { 762, 5, 12, 9, 2, -8 }, // 0x79 'y' - { 770, 7, 8, 9, 1, -7 }, // 0x7A 'z' - { 777, 7, 17, 9, 0, -13 }, // 0x7B '{' - { 792, 1, 18, 9, 4, -14 }, // 0x7C '|' - { 795, 7, 17, 9, 1, -13 }, // 0x7D '}' - { 810, 7, 2, 9, 1, -6 } }; // 0x7E '~' + { 0, 1, 1, 9, 0, 0 }, // 0x20 ' ' + { 1, 2, 12, 9, 3, -11 }, // 0x21 '!' + { 4, 3, 4, 9, 3, -11 }, // 0x22 '"' + { 6, 7, 8, 9, 1, -9 }, // 0x23 '#' + { 13, 7, 18, 9, 1, -14 }, // 0x24 '$' + { 29, 8, 12, 9, 0, -11 }, // 0x25 '%' + { 41, 9, 11, 9, 0, -10 }, // 0x26 '&' + { 54, 1, 4, 9, 4, -11 }, // 0x27 ''' + { 55, 3, 18, 9, 3, -14 }, // 0x28 '(' + { 62, 3, 18, 9, 2, -14 }, // 0x29 ')' + { 69, 7, 7, 9, 1, -11 }, // 0x2A '*' + { 76, 7, 7, 9, 1, -8 }, // 0x2B '+' + { 83, 2, 4, 9, 3, -1 }, // 0x2C ',' + { 84, 7, 1, 9, 1, -5 }, // 0x2D '-' + { 85, 2, 2, 9, 3, -1 }, // 0x2E '.' + { 86, 9, 18, 9, 0, -14 }, // 0x2F '/' + { 107, 7, 12, 9, 1, -11 }, // 0x30 '0' + { 118, 4, 12, 9, 1, -11 }, // 0x31 '1' + { 124, 7, 12, 9, 1, -11 }, // 0x32 '2' + { 135, 7, 11, 9, 1, -10 }, // 0x33 '3' + { 145, 7, 12, 9, 1, -11 }, // 0x34 '4' + { 156, 7, 11, 9, 1, -10 }, // 0x35 '5' + { 166, 7, 12, 9, 1, -11 }, // 0x36 '6' + { 177, 7, 11, 9, 1, -10 }, // 0x37 '7' + { 187, 7, 12, 9, 1, -11 }, // 0x38 '8' + { 198, 7, 12, 9, 1, -11 }, // 0x39 '9' + { 209, 2, 7, 9, 3, -6 }, // 0x3A ':' + { 211, 2, 9, 9, 3, -6 }, // 0x3B ';' + { 214, 7, 6, 9, 1, -8 }, // 0x3C '<' + { 220, 7, 4, 9, 1, -7 }, // 0x3D '=' + { 224, 7, 6, 9, 1, -8 }, // 0x3E '>' + { 230, 7, 12, 9, 1, -11 }, // 0x3F '?' + { 241, 6, 12, 9, 1, -8 }, // 0x40 '@' + { 250, 7, 12, 9, 1, -11 }, // 0x41 'A' + { 261, 7, 11, 9, 1, -10 }, // 0x42 'B' + { 271, 7, 12, 9, 1, -11 }, // 0x43 'C' + { 282, 7, 11, 9, 1, -10 }, // 0x44 'D' + { 292, 7, 11, 9, 2, -10 }, // 0x45 'E' + { 302, 6, 11, 9, 2, -10 }, // 0x46 'F' + { 311, 7, 12, 9, 1, -11 }, // 0x47 'G' + { 322, 7, 12, 9, 1, -11 }, // 0x48 'H' + { 333, 7, 11, 9, 1, -10 }, // 0x49 'I' + { 343, 7, 11, 9, 1, -10 }, // 0x4A 'J' + { 353, 7, 12, 9, 1, -11 }, // 0x4B 'K' + { 364, 7, 12, 9, 1, -11 }, // 0x4C 'L' + { 375, 7, 12, 9, 1, -11 }, // 0x4D 'M' + { 386, 7, 12, 9, 1, -11 }, // 0x4E 'N' + { 397, 7, 12, 9, 1, -11 }, // 0x4F 'O' + { 408, 7, 11, 9, 1, -10 }, // 0x50 'P' + { 418, 7, 13, 9, 1, -11 }, // 0x51 'Q' + { 430, 7, 11, 9, 1, -10 }, // 0x52 'R' + { 440, 7, 12, 9, 1, -11 }, // 0x53 'S' + { 451, 9, 11, 9, 0, -10 }, // 0x54 'T' + { 464, 7, 12, 9, 1, -11 }, // 0x55 'U' + { 475, 9, 12, 9, 0, -11 }, // 0x56 'V' + { 489, 7, 12, 9, 1, -11 }, // 0x57 'W' + { 500, 7, 12, 9, 1, -11 }, // 0x58 'X' + { 511, 7, 12, 9, 1, -11 }, // 0x59 'Y' + { 522, 7, 11, 9, 1, -10 }, // 0x5A 'Z' + { 532, 4, 17, 9, 3, -13 }, // 0x5B '[' + { 541, 9, 18, 9, 0, -14 }, // 0x5C '\' + { 562, 4, 17, 9, 1, -13 }, // 0x5D ']' + { 571, 7, 3, 9, 1, -11 }, // 0x5E '^' + { 574, 7, 1, 9, 1, 0 }, // 0x5F '_' + { 575, 3, 4, 9, 3, -11 }, // 0x60 '`' + { 577, 5, 9, 9, 2, -8 }, // 0x61 'a' + { 583, 5, 12, 9, 2, -11 }, // 0x62 'b' + { 591, 5, 9, 9, 2, -8 }, // 0x63 'c' + { 597, 5, 12, 9, 2, -11 }, // 0x64 'd' + { 605, 5, 9, 9, 2, -8 }, // 0x65 'e' + { 611, 6, 12, 9, 1, -11 }, // 0x66 'f' + { 620, 5, 12, 9, 2, -8 }, // 0x67 'g' + { 628, 5, 12, 9, 2, -11 }, // 0x68 'h' + { 636, 6, 12, 9, 1, -11 }, // 0x69 'i' + { 645, 5, 15, 9, 2, -11 }, // 0x6A 'j' + { 655, 5, 12, 9, 2, -11 }, // 0x6B 'k' + { 663, 5, 12, 9, 2, -11 }, // 0x6C 'l' + { 671, 7, 9, 9, 1, -8 }, // 0x6D 'm' + { 679, 5, 9, 9, 2, -8 }, // 0x6E 'n' + { 685, 5, 9, 9, 2, -8 }, // 0x6F 'o' + { 691, 5, 12, 9, 2, -8 }, // 0x70 'p' + { 699, 7, 12, 9, 2, -8 }, // 0x71 'q' + { 710, 5, 9, 9, 2, -8 }, // 0x72 'r' + { 716, 6, 9, 9, 1, -8 }, // 0x73 's' + { 723, 6, 12, 9, 1, -11 }, // 0x74 't' + { 732, 5, 9, 9, 2, -8 }, // 0x75 'u' + { 738, 7, 9, 9, 1, -8 }, // 0x76 'v' + { 746, 7, 9, 9, 1, -8 }, // 0x77 'w' + { 754, 7, 9, 9, 1, -8 }, // 0x78 'x' + { 762, 5, 12, 9, 2, -8 }, // 0x79 'y' + { 770, 7, 8, 9, 1, -7 }, // 0x7A 'z' + { 777, 7, 17, 9, 0, -13 }, // 0x7B '{' + { 792, 1, 18, 9, 4, -14 }, // 0x7C '|' + { 795, 7, 17, 9, 1, -13 }, // 0x7D '}' + { 810, 7, 2, 9, 1, -6 } }; // 0x7E '~' const GFXfont NovaMono8pt7b PROGMEM = { (uint8_t *)NovaMono8pt7bBitmaps, (GFXglyph *)NovaMono8pt7bGlyphs, - 0x20, 0x7E, 22 }; + 0x20, 0x7E, 22 }; // Approx. 1484 bytes +#endif // ifndef FONTS_NOVAMONO8PT7B_H diff --git a/src/src/Static/Fonts/RepetitionScrolling12pt7b.h b/src/src/Static/Fonts/RepetitionScrolling12pt7b.h index 776c23890a..58f400ef61 100644 --- a/src/src/Static/Fonts/RepetitionScrolling12pt7b.h +++ b/src/src/Static/Fonts/RepetitionScrolling12pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_REPETITIONSCROLLING12PT7B_H +#define FONTS_REPETITIONSCROLLING12PT7B_H const uint8_t RepetitionScrolling12pt7bBitmaps[] PROGMEM = { 0x00, 0xFF, 0x3F, 0xFC, 0x0F, 0xC7, 0x8F, 0x1E, 0x30, 0x31, 0x86, 0x30, 0xC6, 0x18, 0xC0, 0x01, 0xEB, 0xFD, 0x79, 0x8C, 0x31, 0x9E, 0xBF, 0xD7, @@ -134,105 +136,106 @@ const uint8_t RepetitionScrolling12pt7bBitmaps[] PROGMEM = { 0xCF, 0x1E, 0x63, 0xC0 }; const GFXglyph RepetitionScrolling12pt7bGlyphs[] PROGMEM = { - { 0, 1, 1, 13, 0, 0 }, // 0x20 ' ' - { 1, 2, 16, 13, 5, -15 }, // 0x21 '!' - { 5, 7, 4, 13, 3, -15 }, // 0x22 '"' - { 9, 11, 16, 13, 1, -15 }, // 0x23 '#' - { 31, 11, 16, 13, 1, -15 }, // 0x24 '$' - { 53, 11, 16, 13, 1, -15 }, // 0x25 '%' - { 75, 11, 16, 13, 1, -15 }, // 0x26 '&' - { 97, 2, 4, 13, 5, -15 }, // 0x27 ''' - { 98, 6, 16, 13, 3, -15 }, // 0x28 '(' - { 110, 6, 16, 13, 3, -15 }, // 0x29 ')' - { 122, 6, 7, 13, 3, -15 }, // 0x2A '*' - { 128, 11, 11, 13, 1, -10 }, // 0x2B '+' - { 144, 4, 7, 13, 4, -6 }, // 0x2C ',' - { 148, 6, 2, 13, 3, -6 }, // 0x2D '-' - { 150, 4, 4, 13, 4, -3 }, // 0x2E '.' - { 152, 11, 16, 13, 1, -15 }, // 0x2F '/' - { 174, 11, 16, 13, 1, -15 }, // 0x30 '0' - { 196, 7, 16, 13, 3, -15 }, // 0x31 '1' - { 210, 11, 16, 13, 1, -15 }, // 0x32 '2' - { 232, 11, 16, 13, 1, -15 }, // 0x33 '3' - { 254, 11, 16, 13, 1, -15 }, // 0x34 '4' - { 276, 11, 16, 13, 1, -15 }, // 0x35 '5' - { 298, 11, 16, 13, 1, -15 }, // 0x36 '6' - { 320, 11, 16, 13, 1, -15 }, // 0x37 '7' - { 342, 11, 16, 13, 1, -15 }, // 0x38 '8' - { 364, 11, 16, 13, 1, -15 }, // 0x39 '9' - { 386, 4, 11, 13, 4, -10 }, // 0x3A ':' - { 392, 4, 14, 13, 4, -13 }, // 0x3B ';' - { 399, 9, 16, 13, 2, -15 }, // 0x3C '<' - { 417, 11, 7, 13, 1, -8 }, // 0x3D '=' - { 427, 9, 16, 13, 3, -15 }, // 0x3E '>' - { 445, 11, 16, 13, 1, -15 }, // 0x3F '?' - { 467, 11, 16, 13, 1, -15 }, // 0x40 '@' - { 489, 11, 16, 13, 1, -15 }, // 0x41 'A' - { 511, 11, 16, 13, 1, -15 }, // 0x42 'B' - { 533, 11, 16, 13, 1, -15 }, // 0x43 'C' - { 555, 11, 16, 13, 1, -15 }, // 0x44 'D' - { 577, 11, 16, 13, 1, -15 }, // 0x45 'E' - { 599, 11, 16, 13, 1, -15 }, // 0x46 'F' - { 621, 11, 16, 13, 1, -15 }, // 0x47 'G' - { 643, 11, 16, 13, 1, -15 }, // 0x48 'H' - { 665, 7, 16, 13, 3, -15 }, // 0x49 'I' - { 679, 11, 16, 13, 1, -15 }, // 0x4A 'J' - { 701, 11, 16, 13, 1, -15 }, // 0x4B 'K' - { 723, 11, 16, 13, 1, -15 }, // 0x4C 'L' - { 745, 11, 16, 13, 1, -15 }, // 0x4D 'M' - { 767, 11, 16, 13, 1, -15 }, // 0x4E 'N' - { 789, 11, 16, 13, 1, -15 }, // 0x4F 'O' - { 811, 11, 16, 13, 1, -15 }, // 0x50 'P' - { 833, 11, 16, 13, 1, -15 }, // 0x51 'Q' - { 855, 11, 16, 13, 1, -15 }, // 0x52 'R' - { 877, 11, 16, 13, 1, -15 }, // 0x53 'S' - { 899, 11, 16, 13, 1, -15 }, // 0x54 'T' - { 921, 11, 16, 13, 1, -15 }, // 0x55 'U' - { 943, 11, 16, 13, 1, -15 }, // 0x56 'V' - { 965, 11, 16, 13, 1, -15 }, // 0x57 'W' - { 987, 11, 16, 13, 1, -15 }, // 0x58 'X' - { 1009, 11, 16, 13, 1, -15 }, // 0x59 'Y' - { 1031, 11, 16, 13, 1, -15 }, // 0x5A 'Z' - { 1053, 7, 16, 13, 3, -15 }, // 0x5B '[' - { 1067, 11, 16, 13, 1, -15 }, // 0x5C '\' - { 1089, 7, 16, 13, 3, -15 }, // 0x5D ']' - { 1103, 11, 7, 13, 1, -15 }, // 0x5E '^' - { 1113, 11, 2, 13, 1, -1 }, // 0x5F '_' - { 1116, 4, 4, 13, 4, -15 }, // 0x60 '`' - { 1118, 11, 11, 13, 1, -10 }, // 0x61 'a' - { 1134, 11, 16, 13, 1, -15 }, // 0x62 'b' - { 1156, 11, 11, 13, 1, -10 }, // 0x63 'c' - { 1172, 11, 16, 13, 1, -15 }, // 0x64 'd' - { 1194, 11, 11, 13, 1, -10 }, // 0x65 'e' - { 1210, 11, 16, 13, 1, -15 }, // 0x66 'f' - { 1232, 11, 11, 13, 1, -10 }, // 0x67 'g' - { 1248, 11, 16, 13, 1, -15 }, // 0x68 'h' - { 1270, 2, 16, 13, 5, -15 }, // 0x69 'i' - { 1274, 9, 16, 13, 2, -15 }, // 0x6A 'j' - { 1292, 9, 16, 13, 1, -15 }, // 0x6B 'k' - { 1310, 2, 16, 13, 5, -15 }, // 0x6C 'l' - { 1314, 11, 11, 13, 1, -10 }, // 0x6D 'm' - { 1330, 11, 11, 13, 1, -10 }, // 0x6E 'n' - { 1346, 11, 11, 13, 1, -10 }, // 0x6F 'o' - { 1362, 11, 11, 13, 1, -10 }, // 0x70 'p' - { 1378, 11, 11, 13, 1, -10 }, // 0x71 'q' - { 1394, 11, 11, 13, 1, -10 }, // 0x72 'r' - { 1410, 11, 11, 13, 1, -10 }, // 0x73 's' - { 1426, 11, 14, 13, 1, -13 }, // 0x74 't' - { 1446, 11, 11, 13, 1, -10 }, // 0x75 'u' - { 1462, 11, 11, 13, 1, -10 }, // 0x76 'v' - { 1478, 11, 11, 13, 1, -10 }, // 0x77 'w' - { 1494, 11, 11, 13, 1, -10 }, // 0x78 'x' - { 1510, 11, 11, 13, 1, -10 }, // 0x79 'y' - { 1526, 11, 11, 13, 1, -10 }, // 0x7A 'z' - { 1542, 9, 16, 13, 1, -15 }, // 0x7B '{' - { 1560, 2, 16, 13, 5, -15 }, // 0x7C '|' - { 1564, 9, 16, 13, 3, -15 }, // 0x7D '}' - { 1582, 11, 4, 13, 1, -15 } }; // 0x7E '~' + { 0, 1, 1, 13, 0, 0 }, // 0x20 ' ' + { 1, 2, 16, 13, 5, -15 }, // 0x21 '!' + { 5, 7, 4, 13, 3, -15 }, // 0x22 '"' + { 9, 11, 16, 13, 1, -15 }, // 0x23 '#' + { 31, 11, 16, 13, 1, -15 }, // 0x24 '$' + { 53, 11, 16, 13, 1, -15 }, // 0x25 '%' + { 75, 11, 16, 13, 1, -15 }, // 0x26 '&' + { 97, 2, 4, 13, 5, -15 }, // 0x27 ''' + { 98, 6, 16, 13, 3, -15 }, // 0x28 '(' + { 110, 6, 16, 13, 3, -15 }, // 0x29 ')' + { 122, 6, 7, 13, 3, -15 }, // 0x2A '*' + { 128, 11, 11, 13, 1, -10 }, // 0x2B '+' + { 144, 4, 7, 13, 4, -6 }, // 0x2C ',' + { 148, 6, 2, 13, 3, -6 }, // 0x2D '-' + { 150, 4, 4, 13, 4, -3 }, // 0x2E '.' + { 152, 11, 16, 13, 1, -15 }, // 0x2F '/' + { 174, 11, 16, 13, 1, -15 }, // 0x30 '0' + { 196, 7, 16, 13, 3, -15 }, // 0x31 '1' + { 210, 11, 16, 13, 1, -15 }, // 0x32 '2' + { 232, 11, 16, 13, 1, -15 }, // 0x33 '3' + { 254, 11, 16, 13, 1, -15 }, // 0x34 '4' + { 276, 11, 16, 13, 1, -15 }, // 0x35 '5' + { 298, 11, 16, 13, 1, -15 }, // 0x36 '6' + { 320, 11, 16, 13, 1, -15 }, // 0x37 '7' + { 342, 11, 16, 13, 1, -15 }, // 0x38 '8' + { 364, 11, 16, 13, 1, -15 }, // 0x39 '9' + { 386, 4, 11, 13, 4, -10 }, // 0x3A ':' + { 392, 4, 14, 13, 4, -13 }, // 0x3B ';' + { 399, 9, 16, 13, 2, -15 }, // 0x3C '<' + { 417, 11, 7, 13, 1, -8 }, // 0x3D '=' + { 427, 9, 16, 13, 3, -15 }, // 0x3E '>' + { 445, 11, 16, 13, 1, -15 }, // 0x3F '?' + { 467, 11, 16, 13, 1, -15 }, // 0x40 '@' + { 489, 11, 16, 13, 1, -15 }, // 0x41 'A' + { 511, 11, 16, 13, 1, -15 }, // 0x42 'B' + { 533, 11, 16, 13, 1, -15 }, // 0x43 'C' + { 555, 11, 16, 13, 1, -15 }, // 0x44 'D' + { 577, 11, 16, 13, 1, -15 }, // 0x45 'E' + { 599, 11, 16, 13, 1, -15 }, // 0x46 'F' + { 621, 11, 16, 13, 1, -15 }, // 0x47 'G' + { 643, 11, 16, 13, 1, -15 }, // 0x48 'H' + { 665, 7, 16, 13, 3, -15 }, // 0x49 'I' + { 679, 11, 16, 13, 1, -15 }, // 0x4A 'J' + { 701, 11, 16, 13, 1, -15 }, // 0x4B 'K' + { 723, 11, 16, 13, 1, -15 }, // 0x4C 'L' + { 745, 11, 16, 13, 1, -15 }, // 0x4D 'M' + { 767, 11, 16, 13, 1, -15 }, // 0x4E 'N' + { 789, 11, 16, 13, 1, -15 }, // 0x4F 'O' + { 811, 11, 16, 13, 1, -15 }, // 0x50 'P' + { 833, 11, 16, 13, 1, -15 }, // 0x51 'Q' + { 855, 11, 16, 13, 1, -15 }, // 0x52 'R' + { 877, 11, 16, 13, 1, -15 }, // 0x53 'S' + { 899, 11, 16, 13, 1, -15 }, // 0x54 'T' + { 921, 11, 16, 13, 1, -15 }, // 0x55 'U' + { 943, 11, 16, 13, 1, -15 }, // 0x56 'V' + { 965, 11, 16, 13, 1, -15 }, // 0x57 'W' + { 987, 11, 16, 13, 1, -15 }, // 0x58 'X' + { 1009, 11, 16, 13, 1, -15 }, // 0x59 'Y' + { 1031, 11, 16, 13, 1, -15 }, // 0x5A 'Z' + { 1053, 7, 16, 13, 3, -15 }, // 0x5B '[' + { 1067, 11, 16, 13, 1, -15 }, // 0x5C '\' + { 1089, 7, 16, 13, 3, -15 }, // 0x5D ']' + { 1103, 11, 7, 13, 1, -15 }, // 0x5E '^' + { 1113, 11, 2, 13, 1, -1 }, // 0x5F '_' + { 1116, 4, 4, 13, 4, -15 }, // 0x60 '`' + { 1118, 11, 11, 13, 1, -10 }, // 0x61 'a' + { 1134, 11, 16, 13, 1, -15 }, // 0x62 'b' + { 1156, 11, 11, 13, 1, -10 }, // 0x63 'c' + { 1172, 11, 16, 13, 1, -15 }, // 0x64 'd' + { 1194, 11, 11, 13, 1, -10 }, // 0x65 'e' + { 1210, 11, 16, 13, 1, -15 }, // 0x66 'f' + { 1232, 11, 11, 13, 1, -10 }, // 0x67 'g' + { 1248, 11, 16, 13, 1, -15 }, // 0x68 'h' + { 1270, 2, 16, 13, 5, -15 }, // 0x69 'i' + { 1274, 9, 16, 13, 2, -15 }, // 0x6A 'j' + { 1292, 9, 16, 13, 1, -15 }, // 0x6B 'k' + { 1310, 2, 16, 13, 5, -15 }, // 0x6C 'l' + { 1314, 11, 11, 13, 1, -10 }, // 0x6D 'm' + { 1330, 11, 11, 13, 1, -10 }, // 0x6E 'n' + { 1346, 11, 11, 13, 1, -10 }, // 0x6F 'o' + { 1362, 11, 11, 13, 1, -10 }, // 0x70 'p' + { 1378, 11, 11, 13, 1, -10 }, // 0x71 'q' + { 1394, 11, 11, 13, 1, -10 }, // 0x72 'r' + { 1410, 11, 11, 13, 1, -10 }, // 0x73 's' + { 1426, 11, 14, 13, 1, -13 }, // 0x74 't' + { 1446, 11, 11, 13, 1, -10 }, // 0x75 'u' + { 1462, 11, 11, 13, 1, -10 }, // 0x76 'v' + { 1478, 11, 11, 13, 1, -10 }, // 0x77 'w' + { 1494, 11, 11, 13, 1, -10 }, // 0x78 'x' + { 1510, 11, 11, 13, 1, -10 }, // 0x79 'y' + { 1526, 11, 11, 13, 1, -10 }, // 0x7A 'z' + { 1542, 9, 16, 13, 1, -15 }, // 0x7B '{' + { 1560, 2, 16, 13, 5, -15 }, // 0x7C '|' + { 1564, 9, 16, 13, 3, -15 }, // 0x7D '}' + { 1582, 11, 4, 13, 1, -15 } }; // 0x7E '~' const GFXfont RepetitionScrolling12pt7b PROGMEM = { (uint8_t *)RepetitionScrolling12pt7bBitmaps, (GFXglyph *)RepetitionScrolling12pt7bGlyphs, - 0x20, 0x7E, 24 }; + 0x20, 0x7E, 24 }; // Approx. 2260 bytes +#endif // ifndef FONTS_REPETITIONSCROLLING12PT7B_H diff --git a/src/src/Static/Fonts/Seven_Segment18pt7b.h b/src/src/Static/Fonts/Seven_Segment18pt7b.h index f70351a4e7..4a6d48b5bd 100644 --- a/src/src/Static/Fonts/Seven_Segment18pt7b.h +++ b/src/src/Static/Fonts/Seven_Segment18pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_SEVEN_SEGMENT18PT7B_H +#define FONTS_SEVEN_SEGMENT18PT7B_H const uint8_t Seven_Segment18pt7bBitmaps[] PROGMEM = { 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x40, 0x4B, 0x74, 0x80, 0x00, 0x08, 0x03, 0x30, 0x0E, 0x60, 0x19, 0xC0, 0x33, 0x0F, 0xFF, 0x9F, 0xFF, 0x07, @@ -297,105 +299,106 @@ const uint8_t Seven_Segment18pt7bBitmaps[] PROGMEM = { 0x0C, 0x07, 0xFE }; const GFXglyph Seven_Segment18pt7bGlyphs[] PROGMEM = { - { 0, 0, 0, 11, 0, 1 }, // 0x20 ' ' - { 0, 2, 25, 6, 2, -24 }, // 0x21 '!' - { 7, 6, 3, 10, 2, -23 }, // 0x22 '"' - { 10, 15, 16, 19, 2, -15 }, // 0x23 '#' - { 40, 13, 31, 18, 2, -27 }, // 0x24 '$' - { 91, 20, 25, 25, 2, -24 }, // 0x25 '%' - { 154, 79, 25, 83, 2, -24 }, // 0x26 '&' - { 401, 9, 23, 13, 1, -22 }, // 0x27 ''' - { 427, 9, 25, 13, 2, -24 }, // 0x28 '(' - { 456, 9, 25, 13, 2, -24 }, // 0x29 ')' - { 485, 10, 11, 14, 2, -28 }, // 0x2A '*' - { 499, 19, 18, 23, 2, -19 }, // 0x2B '+' - { 542, 4, 4, 8, 2, -3 }, // 0x2C ',' - { 544, 10, 2, 14, 2, -12 }, // 0x2D '-' - { 547, 2, 3, 6, 2, -2 }, // 0x2E '.' - { 548, 15, 24, 19, 2, -23 }, // 0x2F '/' - { 593, 13, 25, 18, 2, -24 }, // 0x30 '0' - { 634, 2, 25, 6, 2, -24 }, // 0x31 '1' - { 641, 13, 25, 18, 2, -24 }, // 0x32 '2' - { 682, 12, 25, 16, 2, -24 }, // 0x33 '3' - { 720, 13, 25, 18, 2, -24 }, // 0x34 '4' - { 761, 13, 25, 18, 2, -24 }, // 0x35 '5' - { 802, 13, 25, 18, 2, -24 }, // 0x36 '6' - { 843, 12, 25, 16, 2, -24 }, // 0x37 '7' - { 881, 13, 25, 18, 2, -24 }, // 0x38 '8' - { 922, 13, 25, 18, 2, -24 }, // 0x39 '9' - { 963, 2, 10, 6, 2, -9 }, // 0x3A ':' - { 966, 9, 23, 13, 1, -22 }, // 0x3B ';' - { 992, 6, 25, 10, 2, -24 }, // 0x3C '<' - { 1011, 15, 9, 19, 2, -13 }, // 0x3D '=' - { 1028, 6, 25, 10, 2, -24 }, // 0x3E '>' - { 1047, 13, 25, 18, 2, -24 }, // 0x3F '?' - { 1088, 13, 25, 18, 2, -24 }, // 0x40 '@' - { 1129, 13, 25, 18, 2, -24 }, // 0x41 'A' - { 1170, 13, 25, 18, 2, -24 }, // 0x42 'B' - { 1211, 12, 25, 16, 2, -24 }, // 0x43 'C' - { 1249, 13, 25, 18, 2, -24 }, // 0x44 'D' - { 1290, 12, 25, 16, 2, -24 }, // 0x45 'E' - { 1328, 12, 25, 16, 2, -24 }, // 0x46 'F' - { 1366, 13, 25, 18, 2, -24 }, // 0x47 'G' - { 1407, 13, 25, 18, 2, -24 }, // 0x48 'H' - { 1448, 2, 25, 6, 2, -24 }, // 0x49 'I' - { 1455, 13, 25, 18, 2, -24 }, // 0x4A 'J' - { 1496, 13, 25, 18, 2, -24 }, // 0x4B 'K' - { 1537, 12, 25, 16, 2, -24 }, // 0x4C 'L' - { 1575, 20, 25, 24, 2, -24 }, // 0x4D 'M' - { 1638, 13, 25, 18, 2, -24 }, // 0x4E 'N' - { 1679, 13, 25, 18, 2, -24 }, // 0x4F 'O' - { 1720, 13, 25, 18, 2, -24 }, // 0x50 'P' - { 1761, 13, 25, 18, 2, -24 }, // 0x51 'Q' - { 1802, 13, 25, 18, 2, -24 }, // 0x52 'R' - { 1843, 13, 25, 18, 2, -24 }, // 0x53 'S' - { 1884, 16, 25, 20, 2, -24 }, // 0x54 'T' - { 1934, 13, 25, 18, 2, -24 }, // 0x55 'U' - { 1975, 13, 25, 18, 2, -24 }, // 0x56 'V' - { 2016, 20, 25, 24, 2, -24 }, // 0x57 'W' - { 2079, 13, 25, 18, 2, -24 }, // 0x58 'X' - { 2120, 13, 25, 18, 2, -24 }, // 0x59 'Y' - { 2161, 13, 25, 18, 2, -24 }, // 0x5A 'Z' - { 2202, 9, 25, 13, 2, -24 }, // 0x5B '[' - { 2231, 15, 24, 19, 2, -23 }, // 0x5C '\' - { 2276, 9, 25, 13, 2, -24 }, // 0x5D ']' - { 2305, 25, 6, 29, 2, -16 }, // 0x5E '^' - { 2324, 18, 2, 22, 2, -1 }, // 0x5F '_' - { 2329, 9, 23, 13, 1, -22 }, // 0x60 '`' - { 2355, 13, 25, 18, 2, -24 }, // 0x61 'a' - { 2396, 13, 25, 18, 2, -24 }, // 0x62 'b' - { 2437, 12, 25, 16, 2, -24 }, // 0x63 'c' - { 2475, 13, 25, 18, 2, -24 }, // 0x64 'd' - { 2516, 12, 25, 16, 2, -24 }, // 0x65 'e' - { 2554, 12, 25, 16, 2, -24 }, // 0x66 'f' - { 2592, 13, 25, 18, 2, -24 }, // 0x67 'g' - { 2633, 13, 25, 18, 2, -24 }, // 0x68 'h' - { 2674, 2, 25, 6, 2, -24 }, // 0x69 'i' - { 2681, 13, 25, 18, 2, -24 }, // 0x6A 'j' - { 2722, 13, 25, 18, 2, -24 }, // 0x6B 'k' - { 2763, 12, 25, 16, 2, -24 }, // 0x6C 'l' - { 2801, 20, 25, 24, 2, -24 }, // 0x6D 'm' - { 2864, 13, 25, 18, 2, -24 }, // 0x6E 'n' - { 2905, 13, 25, 18, 2, -24 }, // 0x6F 'o' - { 2946, 13, 25, 18, 2, -24 }, // 0x70 'p' - { 2987, 13, 25, 18, 2, -24 }, // 0x71 'q' - { 3028, 13, 25, 18, 2, -24 }, // 0x72 'r' - { 3069, 13, 25, 18, 2, -24 }, // 0x73 's' - { 3110, 16, 25, 20, 2, -24 }, // 0x74 't' - { 3160, 13, 25, 18, 2, -24 }, // 0x75 'u' - { 3201, 13, 25, 18, 2, -24 }, // 0x76 'v' - { 3242, 20, 25, 24, 2, -24 }, // 0x77 'w' - { 3305, 13, 25, 18, 2, -24 }, // 0x78 'x' - { 3346, 13, 25, 18, 2, -24 }, // 0x79 'y' - { 3387, 13, 25, 18, 2, -24 }, // 0x7A 'z' - { 3428, 13, 25, 17, 2, -24 }, // 0x7B '{' - { 3469, 2, 27, 6, 2, -26 }, // 0x7C '|' - { 3476, 13, 25, 17, 2, -24 }, // 0x7D '}' - { 3517, 9, 23, 13, 1, -22 } }; // 0x7E '~' + { 0, 0, 0, 11, 0, 1 }, // 0x20 ' ' + { 0, 2, 25, 6, 2, -24 }, // 0x21 '!' + { 7, 6, 3, 10, 2, -23 }, // 0x22 '"' + { 10, 15, 16, 19, 2, -15 }, // 0x23 '#' + { 40, 13, 31, 18, 2, -27 }, // 0x24 '$' + { 91, 20, 25, 25, 2, -24 }, // 0x25 '%' + { 154, 79, 25, 83, 2, -24 }, // 0x26 '&' + { 401, 9, 23, 13, 1, -22 }, // 0x27 ''' + { 427, 9, 25, 13, 2, -24 }, // 0x28 '(' + { 456, 9, 25, 13, 2, -24 }, // 0x29 ')' + { 485, 10, 11, 14, 2, -28 }, // 0x2A '*' + { 499, 19, 18, 23, 2, -19 }, // 0x2B '+' + { 542, 4, 4, 8, 2, -3 }, // 0x2C ',' + { 544, 10, 2, 14, 2, -12 }, // 0x2D '-' + { 547, 2, 3, 6, 2, -2 }, // 0x2E '.' + { 548, 15, 24, 19, 2, -23 }, // 0x2F '/' + { 593, 13, 25, 18, 2, -24 }, // 0x30 '0' + { 634, 2, 25, 6, 2, -24 }, // 0x31 '1' + { 641, 13, 25, 18, 2, -24 }, // 0x32 '2' + { 682, 12, 25, 16, 2, -24 }, // 0x33 '3' + { 720, 13, 25, 18, 2, -24 }, // 0x34 '4' + { 761, 13, 25, 18, 2, -24 }, // 0x35 '5' + { 802, 13, 25, 18, 2, -24 }, // 0x36 '6' + { 843, 12, 25, 16, 2, -24 }, // 0x37 '7' + { 881, 13, 25, 18, 2, -24 }, // 0x38 '8' + { 922, 13, 25, 18, 2, -24 }, // 0x39 '9' + { 963, 2, 10, 6, 2, -9 }, // 0x3A ':' + { 966, 9, 23, 13, 1, -22 }, // 0x3B ';' + { 992, 6, 25, 10, 2, -24 }, // 0x3C '<' + { 1011, 15, 9, 19, 2, -13 }, // 0x3D '=' + { 1028, 6, 25, 10, 2, -24 }, // 0x3E '>' + { 1047, 13, 25, 18, 2, -24 }, // 0x3F '?' + { 1088, 13, 25, 18, 2, -24 }, // 0x40 '@' + { 1129, 13, 25, 18, 2, -24 }, // 0x41 'A' + { 1170, 13, 25, 18, 2, -24 }, // 0x42 'B' + { 1211, 12, 25, 16, 2, -24 }, // 0x43 'C' + { 1249, 13, 25, 18, 2, -24 }, // 0x44 'D' + { 1290, 12, 25, 16, 2, -24 }, // 0x45 'E' + { 1328, 12, 25, 16, 2, -24 }, // 0x46 'F' + { 1366, 13, 25, 18, 2, -24 }, // 0x47 'G' + { 1407, 13, 25, 18, 2, -24 }, // 0x48 'H' + { 1448, 2, 25, 6, 2, -24 }, // 0x49 'I' + { 1455, 13, 25, 18, 2, -24 }, // 0x4A 'J' + { 1496, 13, 25, 18, 2, -24 }, // 0x4B 'K' + { 1537, 12, 25, 16, 2, -24 }, // 0x4C 'L' + { 1575, 20, 25, 24, 2, -24 }, // 0x4D 'M' + { 1638, 13, 25, 18, 2, -24 }, // 0x4E 'N' + { 1679, 13, 25, 18, 2, -24 }, // 0x4F 'O' + { 1720, 13, 25, 18, 2, -24 }, // 0x50 'P' + { 1761, 13, 25, 18, 2, -24 }, // 0x51 'Q' + { 1802, 13, 25, 18, 2, -24 }, // 0x52 'R' + { 1843, 13, 25, 18, 2, -24 }, // 0x53 'S' + { 1884, 16, 25, 20, 2, -24 }, // 0x54 'T' + { 1934, 13, 25, 18, 2, -24 }, // 0x55 'U' + { 1975, 13, 25, 18, 2, -24 }, // 0x56 'V' + { 2016, 20, 25, 24, 2, -24 }, // 0x57 'W' + { 2079, 13, 25, 18, 2, -24 }, // 0x58 'X' + { 2120, 13, 25, 18, 2, -24 }, // 0x59 'Y' + { 2161, 13, 25, 18, 2, -24 }, // 0x5A 'Z' + { 2202, 9, 25, 13, 2, -24 }, // 0x5B '[' + { 2231, 15, 24, 19, 2, -23 }, // 0x5C '\' + { 2276, 9, 25, 13, 2, -24 }, // 0x5D ']' + { 2305, 25, 6, 29, 2, -16 }, // 0x5E '^' + { 2324, 18, 2, 22, 2, -1 }, // 0x5F '_' + { 2329, 9, 23, 13, 1, -22 }, // 0x60 '`' + { 2355, 13, 25, 18, 2, -24 }, // 0x61 'a' + { 2396, 13, 25, 18, 2, -24 }, // 0x62 'b' + { 2437, 12, 25, 16, 2, -24 }, // 0x63 'c' + { 2475, 13, 25, 18, 2, -24 }, // 0x64 'd' + { 2516, 12, 25, 16, 2, -24 }, // 0x65 'e' + { 2554, 12, 25, 16, 2, -24 }, // 0x66 'f' + { 2592, 13, 25, 18, 2, -24 }, // 0x67 'g' + { 2633, 13, 25, 18, 2, -24 }, // 0x68 'h' + { 2674, 2, 25, 6, 2, -24 }, // 0x69 'i' + { 2681, 13, 25, 18, 2, -24 }, // 0x6A 'j' + { 2722, 13, 25, 18, 2, -24 }, // 0x6B 'k' + { 2763, 12, 25, 16, 2, -24 }, // 0x6C 'l' + { 2801, 20, 25, 24, 2, -24 }, // 0x6D 'm' + { 2864, 13, 25, 18, 2, -24 }, // 0x6E 'n' + { 2905, 13, 25, 18, 2, -24 }, // 0x6F 'o' + { 2946, 13, 25, 18, 2, -24 }, // 0x70 'p' + { 2987, 13, 25, 18, 2, -24 }, // 0x71 'q' + { 3028, 13, 25, 18, 2, -24 }, // 0x72 'r' + { 3069, 13, 25, 18, 2, -24 }, // 0x73 's' + { 3110, 16, 25, 20, 2, -24 }, // 0x74 't' + { 3160, 13, 25, 18, 2, -24 }, // 0x75 'u' + { 3201, 13, 25, 18, 2, -24 }, // 0x76 'v' + { 3242, 20, 25, 24, 2, -24 }, // 0x77 'w' + { 3305, 13, 25, 18, 2, -24 }, // 0x78 'x' + { 3346, 13, 25, 18, 2, -24 }, // 0x79 'y' + { 3387, 13, 25, 18, 2, -24 }, // 0x7A 'z' + { 3428, 13, 25, 17, 2, -24 }, // 0x7B '{' + { 3469, 2, 27, 6, 2, -26 }, // 0x7C '|' + { 3476, 13, 25, 17, 2, -24 }, // 0x7D '}' + { 3517, 9, 23, 13, 1, -22 } }; // 0x7E '~' const GFXfont Seven_Segment18pt7b PROGMEM = { (uint8_t *)Seven_Segment18pt7bBitmaps, (GFXglyph *)Seven_Segment18pt7bGlyphs, - 0x20, 0x7E, 36 }; + 0x20, 0x7E, 36 }; // Approx. 4215 bytes +#endif // ifndef FONTS_SEVEN_SEGMENT18PT7B_H diff --git a/src/src/Static/Fonts/Seven_Segment24pt7b.h b/src/src/Static/Fonts/Seven_Segment24pt7b.h index 0e59a6df60..1046b4dbc7 100644 --- a/src/src/Static/Fonts/Seven_Segment24pt7b.h +++ b/src/src/Static/Fonts/Seven_Segment24pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_SEVEN_SEGMENT24PT7B_H +#define FONTS_SEVEN_SEGMENT24PT7B_H const uint8_t Seven_Segment24pt7bBitmaps[] PROGMEM = { 0x5F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0x01, 0xBF, 0x40, 0x45, 0xDF, 0xBF, 0x34, 0x40, 0x00, 0x61, 0x80, 0x07, 0x3C, 0x00, @@ -532,105 +534,106 @@ const uint8_t Seven_Segment24pt7bBitmaps[] PROGMEM = { 0x03, 0x80, 0x38, 0x03, 0xFF, 0xFF, 0xFF }; const GFXglyph Seven_Segment24pt7bGlyphs[] PROGMEM = { - { 0, 0, 0, 14, 0, 1 }, // 0x20 ' ' - { 0, 3, 33, 9, 3, -32 }, // 0x21 '!' - { 13, 7, 5, 13, 3, -32 }, // 0x22 '"' - { 18, 20, 22, 26, 3, -21 }, // 0x23 '#' - { 73, 18, 41, 24, 3, -36 }, // 0x24 '$' - { 166, 27, 33, 33, 3, -32 }, // 0x25 '%' - { 278, 105, 33, 111, 3, -32 }, // 0x26 '&' - { 712, 12, 32, 17, 2, -31 }, // 0x27 ''' - { 760, 12, 33, 18, 3, -32 }, // 0x28 '(' - { 810, 12, 33, 18, 3, -32 }, // 0x29 ')' - { 860, 14, 15, 19, 3, -38 }, // 0x2A '*' - { 887, 25, 25, 31, 3, -27 }, // 0x2B '+' - { 966, 5, 5, 11, 3, -4 }, // 0x2C ',' - { 970, 13, 3, 19, 3, -17 }, // 0x2D '-' - { 975, 3, 5, 9, 3, -4 }, // 0x2E '.' - { 977, 20, 32, 25, 3, -31 }, // 0x2F '/' - { 1057, 18, 33, 24, 3, -32 }, // 0x30 '0' - { 1132, 3, 33, 9, 3, -32 }, // 0x31 '1' - { 1145, 18, 33, 24, 3, -32 }, // 0x32 '2' - { 1220, 16, 33, 21, 3, -32 }, // 0x33 '3' - { 1286, 18, 33, 24, 3, -32 }, // 0x34 '4' - { 1361, 18, 33, 24, 3, -32 }, // 0x35 '5' - { 1436, 18, 33, 24, 3, -32 }, // 0x36 '6' - { 1511, 16, 33, 21, 3, -32 }, // 0x37 '7' - { 1577, 18, 33, 24, 3, -32 }, // 0x38 '8' - { 1652, 18, 33, 24, 3, -32 }, // 0x39 '9' - { 1727, 3, 14, 9, 3, -13 }, // 0x3A ':' - { 1733, 12, 32, 17, 2, -31 }, // 0x3B ';' - { 1781, 8, 33, 14, 3, -32 }, // 0x3C '<' - { 1814, 20, 11, 26, 3, -17 }, // 0x3D '=' - { 1842, 8, 33, 14, 3, -32 }, // 0x3E '>' - { 1875, 18, 33, 24, 3, -32 }, // 0x3F '?' - { 1950, 18, 33, 24, 3, -32 }, // 0x40 '@' - { 2025, 18, 33, 24, 3, -32 }, // 0x41 'A' - { 2100, 18, 33, 24, 3, -32 }, // 0x42 'B' - { 2175, 16, 33, 21, 3, -32 }, // 0x43 'C' - { 2241, 18, 33, 24, 3, -32 }, // 0x44 'D' - { 2316, 16, 33, 21, 3, -32 }, // 0x45 'E' - { 2382, 16, 33, 21, 3, -32 }, // 0x46 'F' - { 2448, 18, 33, 24, 3, -32 }, // 0x47 'G' - { 2523, 18, 33, 24, 3, -32 }, // 0x48 'H' - { 2598, 3, 33, 9, 3, -32 }, // 0x49 'I' - { 2611, 18, 33, 24, 3, -32 }, // 0x4A 'J' - { 2686, 18, 33, 24, 3, -32 }, // 0x4B 'K' - { 2761, 16, 33, 21, 3, -32 }, // 0x4C 'L' - { 2827, 26, 33, 32, 3, -32 }, // 0x4D 'M' - { 2935, 18, 33, 24, 3, -32 }, // 0x4E 'N' - { 3010, 18, 33, 24, 3, -32 }, // 0x4F 'O' - { 3085, 18, 33, 24, 3, -32 }, // 0x50 'P' - { 3160, 18, 33, 24, 3, -32 }, // 0x51 'Q' - { 3235, 18, 33, 24, 3, -32 }, // 0x52 'R' - { 3310, 18, 33, 24, 3, -32 }, // 0x53 'S' - { 3385, 22, 33, 28, 3, -32 }, // 0x54 'T' - { 3476, 18, 33, 24, 3, -32 }, // 0x55 'U' - { 3551, 18, 33, 24, 3, -32 }, // 0x56 'V' - { 3626, 26, 33, 32, 3, -32 }, // 0x57 'W' - { 3734, 18, 33, 24, 3, -32 }, // 0x58 'X' - { 3809, 18, 33, 24, 3, -32 }, // 0x59 'Y' - { 3884, 18, 33, 24, 3, -32 }, // 0x5A 'Z' - { 3959, 12, 33, 18, 3, -32 }, // 0x5B '[' - { 4009, 20, 32, 25, 3, -31 }, // 0x5C '\' - { 4089, 12, 33, 18, 3, -32 }, // 0x5D ']' - { 4139, 33, 8, 39, 3, -21 }, // 0x5E '^' - { 4172, 23, 3, 29, 3, -2 }, // 0x5F '_' - { 4181, 12, 32, 17, 2, -31 }, // 0x60 '`' - { 4229, 18, 33, 24, 3, -32 }, // 0x61 'a' - { 4304, 18, 33, 24, 3, -32 }, // 0x62 'b' - { 4379, 16, 33, 21, 3, -32 }, // 0x63 'c' - { 4445, 18, 33, 24, 3, -32 }, // 0x64 'd' - { 4520, 16, 33, 21, 3, -32 }, // 0x65 'e' - { 4586, 16, 33, 21, 3, -32 }, // 0x66 'f' - { 4652, 18, 33, 24, 3, -32 }, // 0x67 'g' - { 4727, 18, 33, 24, 3, -32 }, // 0x68 'h' - { 4802, 3, 33, 9, 3, -32 }, // 0x69 'i' - { 4815, 18, 33, 24, 3, -32 }, // 0x6A 'j' - { 4890, 18, 33, 24, 3, -32 }, // 0x6B 'k' - { 4965, 16, 33, 21, 3, -32 }, // 0x6C 'l' - { 5031, 26, 33, 32, 3, -32 }, // 0x6D 'm' - { 5139, 18, 33, 24, 3, -32 }, // 0x6E 'n' - { 5214, 18, 33, 24, 3, -32 }, // 0x6F 'o' - { 5289, 18, 33, 24, 3, -32 }, // 0x70 'p' - { 5364, 18, 33, 24, 3, -32 }, // 0x71 'q' - { 5439, 18, 33, 24, 3, -32 }, // 0x72 'r' - { 5514, 18, 33, 24, 3, -32 }, // 0x73 's' - { 5589, 22, 33, 28, 3, -32 }, // 0x74 't' - { 5680, 18, 33, 24, 3, -32 }, // 0x75 'u' - { 5755, 18, 33, 24, 3, -32 }, // 0x76 'v' - { 5830, 26, 33, 32, 3, -32 }, // 0x77 'w' - { 5938, 18, 33, 24, 3, -32 }, // 0x78 'x' - { 6013, 18, 33, 24, 3, -32 }, // 0x79 'y' - { 6088, 18, 33, 24, 3, -32 }, // 0x7A 'z' - { 6163, 17, 33, 23, 3, -32 }, // 0x7B '{' - { 6234, 3, 36, 9, 3, -35 }, // 0x7C '|' - { 6248, 17, 33, 23, 3, -32 }, // 0x7D '}' - { 6319, 12, 32, 17, 2, -31 } }; // 0x7E '~' + { 0, 0, 0, 14, 0, 1 }, // 0x20 ' ' + { 0, 3, 33, 9, 3, -32 }, // 0x21 '!' + { 13, 7, 5, 13, 3, -32 }, // 0x22 '"' + { 18, 20, 22, 26, 3, -21 }, // 0x23 '#' + { 73, 18, 41, 24, 3, -36 }, // 0x24 '$' + { 166, 27, 33, 33, 3, -32 }, // 0x25 '%' + { 278, 105, 33, 111, 3, -32 }, // 0x26 '&' + { 712, 12, 32, 17, 2, -31 }, // 0x27 ''' + { 760, 12, 33, 18, 3, -32 }, // 0x28 '(' + { 810, 12, 33, 18, 3, -32 }, // 0x29 ')' + { 860, 14, 15, 19, 3, -38 }, // 0x2A '*' + { 887, 25, 25, 31, 3, -27 }, // 0x2B '+' + { 966, 5, 5, 11, 3, -4 }, // 0x2C ',' + { 970, 13, 3, 19, 3, -17 }, // 0x2D '-' + { 975, 3, 5, 9, 3, -4 }, // 0x2E '.' + { 977, 20, 32, 25, 3, -31 }, // 0x2F '/' + { 1057, 18, 33, 24, 3, -32 }, // 0x30 '0' + { 1132, 3, 33, 9, 3, -32 }, // 0x31 '1' + { 1145, 18, 33, 24, 3, -32 }, // 0x32 '2' + { 1220, 16, 33, 21, 3, -32 }, // 0x33 '3' + { 1286, 18, 33, 24, 3, -32 }, // 0x34 '4' + { 1361, 18, 33, 24, 3, -32 }, // 0x35 '5' + { 1436, 18, 33, 24, 3, -32 }, // 0x36 '6' + { 1511, 16, 33, 21, 3, -32 }, // 0x37 '7' + { 1577, 18, 33, 24, 3, -32 }, // 0x38 '8' + { 1652, 18, 33, 24, 3, -32 }, // 0x39 '9' + { 1727, 3, 14, 9, 3, -13 }, // 0x3A ':' + { 1733, 12, 32, 17, 2, -31 }, // 0x3B ';' + { 1781, 8, 33, 14, 3, -32 }, // 0x3C '<' + { 1814, 20, 11, 26, 3, -17 }, // 0x3D '=' + { 1842, 8, 33, 14, 3, -32 }, // 0x3E '>' + { 1875, 18, 33, 24, 3, -32 }, // 0x3F '?' + { 1950, 18, 33, 24, 3, -32 }, // 0x40 '@' + { 2025, 18, 33, 24, 3, -32 }, // 0x41 'A' + { 2100, 18, 33, 24, 3, -32 }, // 0x42 'B' + { 2175, 16, 33, 21, 3, -32 }, // 0x43 'C' + { 2241, 18, 33, 24, 3, -32 }, // 0x44 'D' + { 2316, 16, 33, 21, 3, -32 }, // 0x45 'E' + { 2382, 16, 33, 21, 3, -32 }, // 0x46 'F' + { 2448, 18, 33, 24, 3, -32 }, // 0x47 'G' + { 2523, 18, 33, 24, 3, -32 }, // 0x48 'H' + { 2598, 3, 33, 9, 3, -32 }, // 0x49 'I' + { 2611, 18, 33, 24, 3, -32 }, // 0x4A 'J' + { 2686, 18, 33, 24, 3, -32 }, // 0x4B 'K' + { 2761, 16, 33, 21, 3, -32 }, // 0x4C 'L' + { 2827, 26, 33, 32, 3, -32 }, // 0x4D 'M' + { 2935, 18, 33, 24, 3, -32 }, // 0x4E 'N' + { 3010, 18, 33, 24, 3, -32 }, // 0x4F 'O' + { 3085, 18, 33, 24, 3, -32 }, // 0x50 'P' + { 3160, 18, 33, 24, 3, -32 }, // 0x51 'Q' + { 3235, 18, 33, 24, 3, -32 }, // 0x52 'R' + { 3310, 18, 33, 24, 3, -32 }, // 0x53 'S' + { 3385, 22, 33, 28, 3, -32 }, // 0x54 'T' + { 3476, 18, 33, 24, 3, -32 }, // 0x55 'U' + { 3551, 18, 33, 24, 3, -32 }, // 0x56 'V' + { 3626, 26, 33, 32, 3, -32 }, // 0x57 'W' + { 3734, 18, 33, 24, 3, -32 }, // 0x58 'X' + { 3809, 18, 33, 24, 3, -32 }, // 0x59 'Y' + { 3884, 18, 33, 24, 3, -32 }, // 0x5A 'Z' + { 3959, 12, 33, 18, 3, -32 }, // 0x5B '[' + { 4009, 20, 32, 25, 3, -31 }, // 0x5C '\' + { 4089, 12, 33, 18, 3, -32 }, // 0x5D ']' + { 4139, 33, 8, 39, 3, -21 }, // 0x5E '^' + { 4172, 23, 3, 29, 3, -2 }, // 0x5F '_' + { 4181, 12, 32, 17, 2, -31 }, // 0x60 '`' + { 4229, 18, 33, 24, 3, -32 }, // 0x61 'a' + { 4304, 18, 33, 24, 3, -32 }, // 0x62 'b' + { 4379, 16, 33, 21, 3, -32 }, // 0x63 'c' + { 4445, 18, 33, 24, 3, -32 }, // 0x64 'd' + { 4520, 16, 33, 21, 3, -32 }, // 0x65 'e' + { 4586, 16, 33, 21, 3, -32 }, // 0x66 'f' + { 4652, 18, 33, 24, 3, -32 }, // 0x67 'g' + { 4727, 18, 33, 24, 3, -32 }, // 0x68 'h' + { 4802, 3, 33, 9, 3, -32 }, // 0x69 'i' + { 4815, 18, 33, 24, 3, -32 }, // 0x6A 'j' + { 4890, 18, 33, 24, 3, -32 }, // 0x6B 'k' + { 4965, 16, 33, 21, 3, -32 }, // 0x6C 'l' + { 5031, 26, 33, 32, 3, -32 }, // 0x6D 'm' + { 5139, 18, 33, 24, 3, -32 }, // 0x6E 'n' + { 5214, 18, 33, 24, 3, -32 }, // 0x6F 'o' + { 5289, 18, 33, 24, 3, -32 }, // 0x70 'p' + { 5364, 18, 33, 24, 3, -32 }, // 0x71 'q' + { 5439, 18, 33, 24, 3, -32 }, // 0x72 'r' + { 5514, 18, 33, 24, 3, -32 }, // 0x73 's' + { 5589, 22, 33, 28, 3, -32 }, // 0x74 't' + { 5680, 18, 33, 24, 3, -32 }, // 0x75 'u' + { 5755, 18, 33, 24, 3, -32 }, // 0x76 'v' + { 5830, 26, 33, 32, 3, -32 }, // 0x77 'w' + { 5938, 18, 33, 24, 3, -32 }, // 0x78 'x' + { 6013, 18, 33, 24, 3, -32 }, // 0x79 'y' + { 6088, 18, 33, 24, 3, -32 }, // 0x7A 'z' + { 6163, 17, 33, 23, 3, -32 }, // 0x7B '{' + { 6234, 3, 36, 9, 3, -35 }, // 0x7C '|' + { 6248, 17, 33, 23, 3, -32 }, // 0x7D '}' + { 6319, 12, 32, 17, 2, -31 } }; // 0x7E '~' const GFXfont Seven_Segment24pt7b PROGMEM = { (uint8_t *)Seven_Segment24pt7bBitmaps, (GFXglyph *)Seven_Segment24pt7bGlyphs, - 0x20, 0x7E, 49 }; + 0x20, 0x7E, 49 }; // Approx. 7039 bytes +#endif // ifndef FONTS_SEVEN_SEGMENT24PT7B_H diff --git a/src/src/Static/Fonts/angelina12pt7b.h b/src/src/Static/Fonts/angelina12pt7b.h index 08ca969644..96fd126809 100644 --- a/src/src/Static/Fonts/angelina12pt7b.h +++ b/src/src/Static/Fonts/angelina12pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_ANGELINA12PT7B_H +#define FONTS_ANGELINA12PT7B_H const uint8_t angelina12pt7bBitmaps[] PROGMEM = { 0x00, 0x5A, 0xAA, 0xA4, 0x10, 0x6F, 0x00, 0x00, 0x08, 0x01, 0x20, 0x04, 0x40, 0x3F, 0xE7, 0xA2, 0x10, 0x88, 0x01, 0x10, 0x07, 0xFF, 0xF8, 0x80, @@ -115,105 +117,106 @@ const uint8_t angelina12pt7bBitmaps[] PROGMEM = { 0xC0 }; const GFXglyph angelina12pt7bGlyphs[] PROGMEM = { - { 0, 1, 1, 6, 0, 0 }, // 0x20 ' ' - { 1, 2, 14, 5, 1, -11 }, // 0x21 '!' - { 5, 3, 3, 5, 1, -9 }, // 0x22 '"' - { 7, 15, 14, 16, 0, -13 }, // 0x23 '#' - { 34, 6, 13, 6, 0, -14 }, // 0x24 '$' - { 44, 12, 14, 10, -1, -11 }, // 0x25 '%' - { 65, 10, 13, 12, 1, -10 }, // 0x26 '&' - { 82, 2, 3, 3, 0, -9 }, // 0x27 ''' - { 83, 5, 15, 5, 0, -12 }, // 0x28 '(' - { 93, 5, 15, 5, 0, -12 }, // 0x29 ')' - { 103, 1, 1, 29, 0, 0 }, // 0x2A '*' - { 104, 1, 1, 29, 0, 0 }, // 0x2B '+' - { 105, 3, 3, 5, 1, 0 }, // 0x2C ',' - { 107, 7, 2, 8, 1, -5 }, // 0x2D '-' - { 109, 1, 1, 3, 1, 0 }, // 0x2E '.' - { 110, 12, 14, 10, -1, -11 }, // 0x2F '/' - { 131, 9, 14, 10, 0, -11 }, // 0x30 '0' - { 147, 6, 14, 6, 0, -11 }, // 0x31 '1' - { 158, 10, 13, 10, 0, -10 }, // 0x32 '2' - { 175, 13, 14, 11, -1, -11 }, // 0x33 '3' - { 198, 11, 15, 10, -1, -11 }, // 0x34 '4' - { 219, 10, 14, 10, 0, -11 }, // 0x35 '5' - { 237, 9, 14, 9, 0, -11 }, // 0x36 '6' - { 253, 9, 14, 8, 0, -11 }, // 0x37 '7' - { 269, 11, 14, 10, -1, -11 }, // 0x38 '8' - { 289, 8, 15, 9, 0, -11 }, // 0x39 '9' - { 304, 2, 7, 3, 1, -5 }, // 0x3A ':' - { 306, 4, 8, 5, 0, -5 }, // 0x3B ';' - { 310, 1, 1, 29, 0, 0 }, // 0x3C '<' - { 311, 1, 1, 29, 0, 0 }, // 0x3D '=' - { 312, 1, 1, 29, 0, 0 }, // 0x3E '>' - { 313, 10, 14, 10, 0, -11 }, // 0x3F '?' - { 331, 10, 10, 12, 0, -9 }, // 0x40 '@' - { 344, 13, 16, 13, 0, -12 }, // 0x41 'A' - { 370, 14, 14, 13, -1, -11 }, // 0x42 'B' - { 395, 10, 15, 10, 0, -12 }, // 0x43 'C' - { 414, 14, 14, 14, -1, -11 }, // 0x44 'D' - { 439, 11, 14, 11, 0, -11 }, // 0x45 'E' - { 459, 11, 15, 7, -1, -12 }, // 0x46 'F' - { 480, 12, 16, 12, 0, -12 }, // 0x47 'G' - { 504, 15, 15, 15, -1, -12 }, // 0x48 'H' - { 533, 9, 15, 8, -1, -12 }, // 0x49 'I' - { 550, 13, 20, 8, -4, -13 }, // 0x4A 'J' - { 583, 11, 14, 9, -1, -11 }, // 0x4B 'K' - { 603, 12, 15, 10, -1, -12 }, // 0x4C 'L' - { 626, 20, 15, 19, -1, -12 }, // 0x4D 'M' - { 664, 13, 14, 13, 0, -11 }, // 0x4E 'N' - { 687, 10, 15, 11, 0, -12 }, // 0x4F 'O' - { 706, 14, 16, 13, -1, -13 }, // 0x50 'P' - { 734, 10, 15, 11, 0, -12 }, // 0x51 'Q' - { 753, 14, 16, 13, -1, -13 }, // 0x52 'R' - { 781, 10, 15, 10, -1, -12 }, // 0x53 'S' - { 800, 13, 15, 10, -1, -12 }, // 0x54 'T' - { 825, 12, 15, 12, 0, -12 }, // 0x55 'U' - { 848, 14, 15, 12, -1, -12 }, // 0x56 'V' - { 875, 18, 16, 18, -1, -13 }, // 0x57 'W' - { 911, 12, 16, 12, -1, -13 }, // 0x58 'X' - { 935, 13, 23, 10, -2, -13 }, // 0x59 'Y' - { 973, 14, 15, 11, -1, -12 }, // 0x5A 'Z' - { 1000, 7, 17, 8, 1, -14 }, // 0x5B '[' - { 1015, 1, 1, 29, 0, 0 }, // 0x5C '\' - { 1016, 7, 17, 8, 0, -14 }, // 0x5D ']' - { 1031, 1, 1, 29, 0, 0 }, // 0x5E '^' - { 1032, 1, 1, 29, 0, 0 }, // 0x5F '_' - { 1033, 4, 2, 7, 1, -10 }, // 0x60 '`' - { 1034, 9, 11, 8, 0, -8 }, // 0x61 'a' - { 1047, 10, 15, 10, 0, -12 }, // 0x62 'b' - { 1066, 7, 9, 8, 0, -7 }, // 0x63 'c' - { 1074, 10, 15, 10, 0, -12 }, // 0x64 'd' - { 1093, 7, 9, 7, 0, -7 }, // 0x65 'e' - { 1101, 7, 15, 6, 0, -12 }, // 0x66 'f' - { 1115, 8, 17, 9, 0, -8 }, // 0x67 'g' - { 1132, 8, 15, 8, 0, -12 }, // 0x68 'h' - { 1147, 3, 13, 4, 0, -10 }, // 0x69 'i' - { 1152, 9, 18, 4, -4, -9 }, // 0x6A 'j' - { 1173, 8, 15, 8, 0, -12 }, // 0x6B 'k' - { 1188, 3, 15, 4, 0, -12 }, // 0x6C 'l' - { 1194, 14, 8, 14, -1, -6 }, // 0x6D 'm' - { 1208, 9, 8, 8, 0, -6 }, // 0x6E 'n' - { 1217, 6, 9, 7, 0, -7 }, // 0x6F 'o' - { 1224, 7, 15, 8, 0, -7 }, // 0x70 'p' - { 1238, 9, 15, 8, -1, -7 }, // 0x71 'q' - { 1255, 5, 9, 6, 0, -7 }, // 0x72 'r' - { 1261, 6, 9, 6, 0, -7 }, // 0x73 's' - { 1268, 6, 11, 6, 0, -9 }, // 0x74 't' - { 1277, 8, 9, 8, 0, -7 }, // 0x75 'u' - { 1286, 9, 9, 7, 0, -7 }, // 0x76 'v' - { 1297, 11, 10, 12, 0, -8 }, // 0x77 'w' - { 1311, 9, 12, 9, 0, -9 }, // 0x78 'x' - { 1325, 9, 16, 8, -1, -8 }, // 0x79 'y' - { 1343, 9, 8, 9, 0, -6 }, // 0x7A 'z' - { 1352, 1, 1, 29, 0, 0 }, // 0x7B '{' - { 1353, 1, 1, 29, 0, 0 }, // 0x7C '|' - { 1354, 1, 1, 29, 0, 0 }, // 0x7D '}' - { 1355, 5, 2, 7, 1, -9 } }; // 0x7E '~' + { 0, 1, 1, 6, 0, 0 }, // 0x20 ' ' + { 1, 2, 14, 5, 1, -11 }, // 0x21 '!' + { 5, 3, 3, 5, 1, -9 }, // 0x22 '"' + { 7, 15, 14, 16, 0, -13 }, // 0x23 '#' + { 34, 6, 13, 6, 0, -14 }, // 0x24 '$' + { 44, 12, 14, 10, -1, -11 }, // 0x25 '%' + { 65, 10, 13, 12, 1, -10 }, // 0x26 '&' + { 82, 2, 3, 3, 0, -9 }, // 0x27 ''' + { 83, 5, 15, 5, 0, -12 }, // 0x28 '(' + { 93, 5, 15, 5, 0, -12 }, // 0x29 ')' + { 103, 1, 1, 29, 0, 0 }, // 0x2A '*' + { 104, 1, 1, 29, 0, 0 }, // 0x2B '+' + { 105, 3, 3, 5, 1, 0 }, // 0x2C ',' + { 107, 7, 2, 8, 1, -5 }, // 0x2D '-' + { 109, 1, 1, 3, 1, 0 }, // 0x2E '.' + { 110, 12, 14, 10, -1, -11 }, // 0x2F '/' + { 131, 9, 14, 10, 0, -11 }, // 0x30 '0' + { 147, 6, 14, 6, 0, -11 }, // 0x31 '1' + { 158, 10, 13, 10, 0, -10 }, // 0x32 '2' + { 175, 13, 14, 11, -1, -11 }, // 0x33 '3' + { 198, 11, 15, 10, -1, -11 }, // 0x34 '4' + { 219, 10, 14, 10, 0, -11 }, // 0x35 '5' + { 237, 9, 14, 9, 0, -11 }, // 0x36 '6' + { 253, 9, 14, 8, 0, -11 }, // 0x37 '7' + { 269, 11, 14, 10, -1, -11 }, // 0x38 '8' + { 289, 8, 15, 9, 0, -11 }, // 0x39 '9' + { 304, 2, 7, 3, 1, -5 }, // 0x3A ':' + { 306, 4, 8, 5, 0, -5 }, // 0x3B ';' + { 310, 1, 1, 29, 0, 0 }, // 0x3C '<' + { 311, 1, 1, 29, 0, 0 }, // 0x3D '=' + { 312, 1, 1, 29, 0, 0 }, // 0x3E '>' + { 313, 10, 14, 10, 0, -11 }, // 0x3F '?' + { 331, 10, 10, 12, 0, -9 }, // 0x40 '@' + { 344, 13, 16, 13, 0, -12 }, // 0x41 'A' + { 370, 14, 14, 13, -1, -11 }, // 0x42 'B' + { 395, 10, 15, 10, 0, -12 }, // 0x43 'C' + { 414, 14, 14, 14, -1, -11 }, // 0x44 'D' + { 439, 11, 14, 11, 0, -11 }, // 0x45 'E' + { 459, 11, 15, 7, -1, -12 }, // 0x46 'F' + { 480, 12, 16, 12, 0, -12 }, // 0x47 'G' + { 504, 15, 15, 15, -1, -12 }, // 0x48 'H' + { 533, 9, 15, 8, -1, -12 }, // 0x49 'I' + { 550, 13, 20, 8, -4, -13 }, // 0x4A 'J' + { 583, 11, 14, 9, -1, -11 }, // 0x4B 'K' + { 603, 12, 15, 10, -1, -12 }, // 0x4C 'L' + { 626, 20, 15, 19, -1, -12 }, // 0x4D 'M' + { 664, 13, 14, 13, 0, -11 }, // 0x4E 'N' + { 687, 10, 15, 11, 0, -12 }, // 0x4F 'O' + { 706, 14, 16, 13, -1, -13 }, // 0x50 'P' + { 734, 10, 15, 11, 0, -12 }, // 0x51 'Q' + { 753, 14, 16, 13, -1, -13 }, // 0x52 'R' + { 781, 10, 15, 10, -1, -12 }, // 0x53 'S' + { 800, 13, 15, 10, -1, -12 }, // 0x54 'T' + { 825, 12, 15, 12, 0, -12 }, // 0x55 'U' + { 848, 14, 15, 12, -1, -12 }, // 0x56 'V' + { 875, 18, 16, 18, -1, -13 }, // 0x57 'W' + { 911, 12, 16, 12, -1, -13 }, // 0x58 'X' + { 935, 13, 23, 10, -2, -13 }, // 0x59 'Y' + { 973, 14, 15, 11, -1, -12 }, // 0x5A 'Z' + { 1000, 7, 17, 8, 1, -14 }, // 0x5B '[' + { 1015, 1, 1, 29, 0, 0 }, // 0x5C '\' + { 1016, 7, 17, 8, 0, -14 }, // 0x5D ']' + { 1031, 1, 1, 29, 0, 0 }, // 0x5E '^' + { 1032, 1, 1, 29, 0, 0 }, // 0x5F '_' + { 1033, 4, 2, 7, 1, -10 }, // 0x60 '`' + { 1034, 9, 11, 8, 0, -8 }, // 0x61 'a' + { 1047, 10, 15, 10, 0, -12 }, // 0x62 'b' + { 1066, 7, 9, 8, 0, -7 }, // 0x63 'c' + { 1074, 10, 15, 10, 0, -12 }, // 0x64 'd' + { 1093, 7, 9, 7, 0, -7 }, // 0x65 'e' + { 1101, 7, 15, 6, 0, -12 }, // 0x66 'f' + { 1115, 8, 17, 9, 0, -8 }, // 0x67 'g' + { 1132, 8, 15, 8, 0, -12 }, // 0x68 'h' + { 1147, 3, 13, 4, 0, -10 }, // 0x69 'i' + { 1152, 9, 18, 4, -4, -9 }, // 0x6A 'j' + { 1173, 8, 15, 8, 0, -12 }, // 0x6B 'k' + { 1188, 3, 15, 4, 0, -12 }, // 0x6C 'l' + { 1194, 14, 8, 14, -1, -6 }, // 0x6D 'm' + { 1208, 9, 8, 8, 0, -6 }, // 0x6E 'n' + { 1217, 6, 9, 7, 0, -7 }, // 0x6F 'o' + { 1224, 7, 15, 8, 0, -7 }, // 0x70 'p' + { 1238, 9, 15, 8, -1, -7 }, // 0x71 'q' + { 1255, 5, 9, 6, 0, -7 }, // 0x72 'r' + { 1261, 6, 9, 6, 0, -7 }, // 0x73 's' + { 1268, 6, 11, 6, 0, -9 }, // 0x74 't' + { 1277, 8, 9, 8, 0, -7 }, // 0x75 'u' + { 1286, 9, 9, 7, 0, -7 }, // 0x76 'v' + { 1297, 11, 10, 12, 0, -8 }, // 0x77 'w' + { 1311, 9, 12, 9, 0, -9 }, // 0x78 'x' + { 1325, 9, 16, 8, -1, -8 }, // 0x79 'y' + { 1343, 9, 8, 9, 0, -6 }, // 0x7A 'z' + { 1352, 1, 1, 29, 0, 0 }, // 0x7B '{' + { 1353, 1, 1, 29, 0, 0 }, // 0x7C '|' + { 1354, 1, 1, 29, 0, 0 }, // 0x7D '}' + { 1355, 5, 2, 7, 1, -9 } }; // 0x7E '~' const GFXfont angelina12pt7b PROGMEM = { (uint8_t *)angelina12pt7bBitmaps, (GFXglyph *)angelina12pt7bGlyphs, - 0x20, 0x7E, 25 }; + 0x20, 0x7E, 25 }; // Approx. 2029 bytes +#endif // ifndef FONTS_ANGELINA12PT7B_H diff --git a/src/src/Static/Fonts/angelina8pt7b.h b/src/src/Static/Fonts/angelina8pt7b.h index eff391dac1..732c596798 100644 --- a/src/src/Static/Fonts/angelina8pt7b.h +++ b/src/src/Static/Fonts/angelina8pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_ANGELINA8PT7B_H +#define FONTS_ANGELINA8PT7B_H const uint8_t angelina8pt7bBitmaps[] PROGMEM = { 0x00, 0xAA, 0xA8, 0x80, 0x5C, 0x00, 0x81, 0x40, 0xA0, 0xFF, 0x54, 0x07, 0xFE, 0x40, 0xA0, 0x40, 0x00, 0x00, 0x23, 0x4C, 0xF3, 0xAE, 0x11, 0x1E, @@ -53,105 +55,106 @@ const uint8_t angelina8pt7bBitmaps[] PROGMEM = { 0x27, 0x00, 0x7C, 0x21, 0x08, 0xDF, 0x90, 0x00, 0x00, 0x00, 0xE0 }; const GFXglyph angelina8pt7bGlyphs[] PROGMEM = { - { 0, 1, 1, 4, 0, 0 }, // 0x20 ' ' - { 1, 2, 9, 3, 1, -7 }, // 0x21 '!' - { 4, 2, 3, 3, 0, -6 }, // 0x22 '"' - { 5, 10, 10, 10, 0, -9 }, // 0x23 '#' - { 18, 4, 8, 4, 0, -9 }, // 0x24 '$' - { 22, 8, 9, 7, 0, -7 }, // 0x25 '%' - { 31, 7, 8, 8, 0, -6 }, // 0x26 '&' - { 38, 1, 3, 2, 0, -6 }, // 0x27 ''' - { 39, 3, 9, 4, 0, -7 }, // 0x28 '(' - { 43, 3, 9, 4, 0, -7 }, // 0x29 ')' - { 47, 1, 1, 20, 0, 0 }, // 0x2A '*' - { 48, 1, 1, 20, 0, 0 }, // 0x2B '+' - { 49, 2, 2, 3, 0, 0 }, // 0x2C ',' - { 50, 4, 1, 5, 0, -3 }, // 0x2D '-' - { 51, 1, 1, 2, 1, 0 }, // 0x2E '.' - { 52, 8, 9, 7, 0, -7 }, // 0x2F '/' - { 61, 6, 9, 6, 0, -7 }, // 0x30 '0' - { 68, 4, 9, 4, 0, -7 }, // 0x31 '1' - { 73, 7, 9, 7, 0, -7 }, // 0x32 '2' - { 81, 8, 9, 7, -1, -7 }, // 0x33 '3' - { 90, 7, 10, 7, 0, -7 }, // 0x34 '4' - { 99, 7, 9, 7, 0, -7 }, // 0x35 '5' - { 107, 6, 9, 6, 0, -7 }, // 0x36 '6' - { 114, 6, 9, 6, 0, -7 }, // 0x37 '7' - { 121, 7, 9, 6, 0, -7 }, // 0x38 '8' - { 129, 5, 10, 6, 0, -7 }, // 0x39 '9' - { 136, 1, 4, 2, 1, -3 }, // 0x3A ':' - { 137, 3, 5, 3, 0, -3 }, // 0x3B ';' - { 139, 1, 1, 20, 0, 0 }, // 0x3C '<' - { 140, 1, 1, 20, 0, 0 }, // 0x3D '=' - { 141, 1, 1, 20, 0, 0 }, // 0x3E '>' - { 142, 7, 9, 7, 0, -7 }, // 0x3F '?' - { 150, 7, 7, 8, 0, -6 }, // 0x40 '@' - { 157, 9, 10, 9, 0, -7 }, // 0x41 'A' - { 169, 9, 9, 9, 0, -7 }, // 0x42 'B' - { 180, 7, 10, 6, 0, -8 }, // 0x43 'C' - { 189, 9, 9, 9, 0, -7 }, // 0x44 'D' - { 200, 8, 9, 7, 0, -7 }, // 0x45 'E' - { 209, 7, 9, 5, 0, -7 }, // 0x46 'F' - { 217, 8, 11, 8, 0, -8 }, // 0x47 'G' - { 228, 10, 10, 10, -1, -8 }, // 0x48 'H' - { 241, 6, 10, 6, 0, -8 }, // 0x49 'I' - { 249, 9, 13, 5, -2, -8 }, // 0x4A 'J' - { 264, 7, 9, 6, 0, -7 }, // 0x4B 'K' - { 272, 8, 10, 7, 0, -8 }, // 0x4C 'L' - { 282, 14, 10, 13, -1, -8 }, // 0x4D 'M' - { 300, 8, 9, 9, 0, -7 }, // 0x4E 'N' - { 309, 7, 10, 7, 0, -8 }, // 0x4F 'O' - { 318, 9, 11, 9, 0, -9 }, // 0x50 'P' - { 331, 7, 10, 7, 0, -8 }, // 0x51 'Q' - { 340, 9, 10, 8, -1, -8 }, // 0x52 'R' - { 352, 7, 10, 6, 0, -8 }, // 0x53 'S' - { 361, 9, 10, 7, -1, -8 }, // 0x54 'T' - { 373, 8, 9, 8, 0, -7 }, // 0x55 'U' - { 382, 9, 9, 8, -1, -7 }, // 0x56 'V' - { 393, 12, 11, 12, 0, -9 }, // 0x57 'W' - { 410, 8, 10, 8, 0, -8 }, // 0x58 'X' - { 420, 8, 16, 7, 0, -9 }, // 0x59 'Y' - { 436, 9, 10, 7, 0, -8 }, // 0x5A 'Z' - { 448, 4, 11, 6, 1, -9 }, // 0x5B '[' - { 454, 1, 1, 20, 0, 0 }, // 0x5C '\' - { 455, 5, 11, 6, 1, -9 }, // 0x5D ']' - { 462, 1, 1, 20, 0, 0 }, // 0x5E '^' - { 463, 1, 1, 20, 0, 0 }, // 0x5F '_' - { 464, 3, 1, 5, 1, -6 }, // 0x60 '`' - { 465, 6, 7, 6, 0, -5 }, // 0x61 'a' - { 471, 6, 9, 7, 0, -7 }, // 0x62 'b' - { 478, 5, 7, 5, 0, -5 }, // 0x63 'c' - { 483, 6, 10, 7, 0, -8 }, // 0x64 'd' - { 491, 5, 7, 5, 0, -5 }, // 0x65 'e' - { 496, 4, 10, 4, 1, -8 }, // 0x66 'f' - { 501, 6, 11, 6, 0, -5 }, // 0x67 'g' - { 510, 5, 10, 5, 0, -8 }, // 0x68 'h' - { 517, 2, 8, 2, 0, -6 }, // 0x69 'i' - { 519, 6, 12, 3, -3, -6 }, // 0x6A 'j' - { 528, 5, 10, 5, 0, -8 }, // 0x6B 'k' - { 535, 2, 10, 3, 0, -8 }, // 0x6C 'l' - { 538, 9, 6, 9, 0, -4 }, // 0x6D 'm' - { 545, 6, 6, 5, 0, -4 }, // 0x6E 'n' - { 550, 4, 7, 5, 0, -5 }, // 0x6F 'o' - { 554, 5, 11, 5, 0, -5 }, // 0x70 'p' - { 561, 6, 11, 5, 0, -5 }, // 0x71 'q' - { 570, 4, 7, 4, 0, -5 }, // 0x72 'r' - { 574, 4, 7, 4, 0, -5 }, // 0x73 's' - { 578, 4, 8, 4, 0, -6 }, // 0x74 't' - { 582, 6, 6, 5, 0, -4 }, // 0x75 'u' - { 587, 6, 6, 5, 0, -4 }, // 0x76 'v' - { 592, 7, 7, 8, 0, -5 }, // 0x77 'w' - { 599, 6, 7, 6, 0, -5 }, // 0x78 'x' - { 605, 6, 11, 5, -1, -5 }, // 0x79 'y' - { 614, 6, 6, 6, 0, -4 }, // 0x7A 'z' - { 619, 1, 1, 20, 0, 0 }, // 0x7B '{' - { 620, 1, 1, 20, 0, 0 }, // 0x7C '|' - { 621, 1, 1, 20, 0, 0 }, // 0x7D '}' - { 622, 3, 1, 5, 1, -5 } }; // 0x7E '~' + { 0, 1, 1, 4, 0, 0 }, // 0x20 ' ' + { 1, 2, 9, 3, 1, -7 }, // 0x21 '!' + { 4, 2, 3, 3, 0, -6 }, // 0x22 '"' + { 5, 10, 10, 10, 0, -9 }, // 0x23 '#' + { 18, 4, 8, 4, 0, -9 }, // 0x24 '$' + { 22, 8, 9, 7, 0, -7 }, // 0x25 '%' + { 31, 7, 8, 8, 0, -6 }, // 0x26 '&' + { 38, 1, 3, 2, 0, -6 }, // 0x27 ''' + { 39, 3, 9, 4, 0, -7 }, // 0x28 '(' + { 43, 3, 9, 4, 0, -7 }, // 0x29 ')' + { 47, 1, 1, 20, 0, 0 }, // 0x2A '*' + { 48, 1, 1, 20, 0, 0 }, // 0x2B '+' + { 49, 2, 2, 3, 0, 0 }, // 0x2C ',' + { 50, 4, 1, 5, 0, -3 }, // 0x2D '-' + { 51, 1, 1, 2, 1, 0 }, // 0x2E '.' + { 52, 8, 9, 7, 0, -7 }, // 0x2F '/' + { 61, 6, 9, 6, 0, -7 }, // 0x30 '0' + { 68, 4, 9, 4, 0, -7 }, // 0x31 '1' + { 73, 7, 9, 7, 0, -7 }, // 0x32 '2' + { 81, 8, 9, 7, -1, -7 }, // 0x33 '3' + { 90, 7, 10, 7, 0, -7 }, // 0x34 '4' + { 99, 7, 9, 7, 0, -7 }, // 0x35 '5' + { 107, 6, 9, 6, 0, -7 }, // 0x36 '6' + { 114, 6, 9, 6, 0, -7 }, // 0x37 '7' + { 121, 7, 9, 6, 0, -7 }, // 0x38 '8' + { 129, 5, 10, 6, 0, -7 }, // 0x39 '9' + { 136, 1, 4, 2, 1, -3 }, // 0x3A ':' + { 137, 3, 5, 3, 0, -3 }, // 0x3B ';' + { 139, 1, 1, 20, 0, 0 }, // 0x3C '<' + { 140, 1, 1, 20, 0, 0 }, // 0x3D '=' + { 141, 1, 1, 20, 0, 0 }, // 0x3E '>' + { 142, 7, 9, 7, 0, -7 }, // 0x3F '?' + { 150, 7, 7, 8, 0, -6 }, // 0x40 '@' + { 157, 9, 10, 9, 0, -7 }, // 0x41 'A' + { 169, 9, 9, 9, 0, -7 }, // 0x42 'B' + { 180, 7, 10, 6, 0, -8 }, // 0x43 'C' + { 189, 9, 9, 9, 0, -7 }, // 0x44 'D' + { 200, 8, 9, 7, 0, -7 }, // 0x45 'E' + { 209, 7, 9, 5, 0, -7 }, // 0x46 'F' + { 217, 8, 11, 8, 0, -8 }, // 0x47 'G' + { 228, 10, 10, 10, -1, -8 }, // 0x48 'H' + { 241, 6, 10, 6, 0, -8 }, // 0x49 'I' + { 249, 9, 13, 5, -2, -8 }, // 0x4A 'J' + { 264, 7, 9, 6, 0, -7 }, // 0x4B 'K' + { 272, 8, 10, 7, 0, -8 }, // 0x4C 'L' + { 282, 14, 10, 13, -1, -8 }, // 0x4D 'M' + { 300, 8, 9, 9, 0, -7 }, // 0x4E 'N' + { 309, 7, 10, 7, 0, -8 }, // 0x4F 'O' + { 318, 9, 11, 9, 0, -9 }, // 0x50 'P' + { 331, 7, 10, 7, 0, -8 }, // 0x51 'Q' + { 340, 9, 10, 8, -1, -8 }, // 0x52 'R' + { 352, 7, 10, 6, 0, -8 }, // 0x53 'S' + { 361, 9, 10, 7, -1, -8 }, // 0x54 'T' + { 373, 8, 9, 8, 0, -7 }, // 0x55 'U' + { 382, 9, 9, 8, -1, -7 }, // 0x56 'V' + { 393, 12, 11, 12, 0, -9 }, // 0x57 'W' + { 410, 8, 10, 8, 0, -8 }, // 0x58 'X' + { 420, 8, 16, 7, 0, -9 }, // 0x59 'Y' + { 436, 9, 10, 7, 0, -8 }, // 0x5A 'Z' + { 448, 4, 11, 6, 1, -9 }, // 0x5B '[' + { 454, 1, 1, 20, 0, 0 }, // 0x5C '\' + { 455, 5, 11, 6, 1, -9 }, // 0x5D ']' + { 462, 1, 1, 20, 0, 0 }, // 0x5E '^' + { 463, 1, 1, 20, 0, 0 }, // 0x5F '_' + { 464, 3, 1, 5, 1, -6 }, // 0x60 '`' + { 465, 6, 7, 6, 0, -5 }, // 0x61 'a' + { 471, 6, 9, 7, 0, -7 }, // 0x62 'b' + { 478, 5, 7, 5, 0, -5 }, // 0x63 'c' + { 483, 6, 10, 7, 0, -8 }, // 0x64 'd' + { 491, 5, 7, 5, 0, -5 }, // 0x65 'e' + { 496, 4, 10, 4, 1, -8 }, // 0x66 'f' + { 501, 6, 11, 6, 0, -5 }, // 0x67 'g' + { 510, 5, 10, 5, 0, -8 }, // 0x68 'h' + { 517, 2, 8, 2, 0, -6 }, // 0x69 'i' + { 519, 6, 12, 3, -3, -6 }, // 0x6A 'j' + { 528, 5, 10, 5, 0, -8 }, // 0x6B 'k' + { 535, 2, 10, 3, 0, -8 }, // 0x6C 'l' + { 538, 9, 6, 9, 0, -4 }, // 0x6D 'm' + { 545, 6, 6, 5, 0, -4 }, // 0x6E 'n' + { 550, 4, 7, 5, 0, -5 }, // 0x6F 'o' + { 554, 5, 11, 5, 0, -5 }, // 0x70 'p' + { 561, 6, 11, 5, 0, -5 }, // 0x71 'q' + { 570, 4, 7, 4, 0, -5 }, // 0x72 'r' + { 574, 4, 7, 4, 0, -5 }, // 0x73 's' + { 578, 4, 8, 4, 0, -6 }, // 0x74 't' + { 582, 6, 6, 5, 0, -4 }, // 0x75 'u' + { 587, 6, 6, 5, 0, -4 }, // 0x76 'v' + { 592, 7, 7, 8, 0, -5 }, // 0x77 'w' + { 599, 6, 7, 6, 0, -5 }, // 0x78 'x' + { 605, 6, 11, 5, -1, -5 }, // 0x79 'y' + { 614, 6, 6, 6, 0, -4 }, // 0x7A 'z' + { 619, 1, 1, 20, 0, 0 }, // 0x7B '{' + { 620, 1, 1, 20, 0, 0 }, // 0x7C '|' + { 621, 1, 1, 20, 0, 0 }, // 0x7D '}' + { 622, 3, 1, 5, 1, -5 } }; // 0x7E '~' const GFXfont angelina8pt7b PROGMEM = { (uint8_t *)angelina8pt7bBitmaps, (GFXglyph *)angelina8pt7bGlyphs, - 0x20, 0x7E, 16 }; + 0x20, 0x7E, 16 }; // Approx. 1295 bytes +#endif // ifndef FONTS_ANGELINA8PT7B_H diff --git a/src/src/Static/Fonts/unispace12pt7b.h b/src/src/Static/Fonts/unispace12pt7b.h index 738d830d5e..f5b4256d7b 100644 --- a/src/src/Static/Fonts/unispace12pt7b.h +++ b/src/src/Static/Fonts/unispace12pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_UNISPACE12PT7B_H +#define FONTS_UNISPACE12PT7B_H const uint8_t unispace12pt7bBitmaps[] PROGMEM = { 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x0F, 0xFF, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0x1C, 0x70, 0x71, 0xC1, @@ -314,105 +316,106 @@ const uint8_t unispace12pt7bBitmaps[] PROGMEM = { 0xEE, 0x1E, 0x00 }; const GFXglyph unispace12pt7bGlyphs[] PROGMEM = { - { 0, 1, 1, 18, 0, 0 }, // 0x20 ' ' - { 1, 4, 24, 18, 7, -23 }, // 0x21 '!' - { 13, 8, 7, 18, 5, -24 }, // 0x22 '"' - { 20, 14, 24, 18, 2, -23 }, // 0x23 '#' - { 62, 17, 24, 19, 1, -23 }, // 0x24 '$' - { 113, 16, 24, 18, 1, -23 }, // 0x25 '%' - { 161, 17, 24, 18, 1, -23 }, // 0x26 '&' - { 212, 3, 7, 18, 8, -24 }, // 0x27 ''' - { 215, 9, 35, 18, 4, -27 }, // 0x28 '(' - { 255, 9, 35, 18, 5, -27 }, // 0x29 ')' - { 295, 13, 13, 18, 3, -23 }, // 0x2A '*' - { 317, 15, 12, 18, 2, -18 }, // 0x2B '+' - { 340, 4, 7, 18, 7, -1 }, // 0x2C ',' - { 344, 16, 2, 18, 1, -11 }, // 0x2D '-' - { 348, 4, 3, 18, 7, -2 }, // 0x2E '.' - { 350, 16, 24, 18, 1, -23 }, // 0x2F '/' - { 398, 17, 24, 19, 1, -23 }, // 0x30 '0' - { 449, 16, 24, 18, 1, -23 }, // 0x31 '1' - { 497, 16, 24, 18, 1, -23 }, // 0x32 '2' - { 545, 16, 24, 18, 1, -23 }, // 0x33 '3' - { 593, 17, 24, 19, 1, -23 }, // 0x34 '4' - { 644, 16, 24, 18, 1, -23 }, // 0x35 '5' - { 692, 16, 24, 18, 1, -23 }, // 0x36 '6' - { 740, 16, 24, 18, 1, -23 }, // 0x37 '7' - { 788, 17, 24, 19, 1, -23 }, // 0x38 '8' - { 839, 16, 24, 18, 1, -23 }, // 0x39 '9' - { 887, 4, 14, 18, 7, -13 }, // 0x3A ':' - { 894, 4, 19, 18, 7, -13 }, // 0x3B ';' - { 904, 10, 18, 18, 4, -21 }, // 0x3C '<' - { 927, 12, 8, 18, 3, -16 }, // 0x3D '=' - { 939, 10, 18, 18, 4, -21 }, // 0x3E '>' - { 962, 16, 24, 18, 1, -23 }, // 0x3F '?' - { 1010, 16, 24, 19, 1, -23 }, // 0x40 '@' - { 1058, 16, 24, 18, 1, -23 }, // 0x41 'A' - { 1106, 16, 24, 18, 1, -23 }, // 0x42 'B' - { 1154, 15, 24, 18, 2, -23 }, // 0x43 'C' - { 1199, 16, 24, 18, 1, -23 }, // 0x44 'D' - { 1247, 15, 24, 18, 2, -23 }, // 0x45 'E' - { 1292, 15, 24, 18, 1, -23 }, // 0x46 'F' - { 1337, 16, 24, 18, 1, -23 }, // 0x47 'G' - { 1385, 16, 24, 18, 1, -23 }, // 0x48 'H' - { 1433, 16, 24, 18, 1, -23 }, // 0x49 'I' - { 1481, 15, 24, 19, 2, -23 }, // 0x4A 'J' - { 1526, 16, 24, 18, 1, -23 }, // 0x4B 'K' - { 1574, 16, 24, 18, 1, -23 }, // 0x4C 'L' - { 1622, 16, 24, 18, 1, -23 }, // 0x4D 'M' - { 1670, 16, 24, 18, 1, -23 }, // 0x4E 'N' - { 1718, 16, 24, 18, 1, -23 }, // 0x4F 'O' - { 1766, 16, 24, 18, 1, -23 }, // 0x50 'P' - { 1814, 16, 28, 18, 1, -23 }, // 0x51 'Q' - { 1870, 16, 24, 18, 1, -23 }, // 0x52 'R' - { 1918, 16, 24, 18, 1, -23 }, // 0x53 'S' - { 1966, 15, 24, 18, 1, -23 }, // 0x54 'T' - { 2011, 16, 24, 18, 1, -23 }, // 0x55 'U' - { 2059, 16, 24, 18, 1, -23 }, // 0x56 'V' - { 2107, 18, 24, 18, 0, -23 }, // 0x57 'W' - { 2161, 16, 24, 18, 1, -23 }, // 0x58 'X' - { 2209, 16, 24, 18, 1, -23 }, // 0x59 'Y' - { 2257, 16, 24, 18, 1, -23 }, // 0x5A 'Z' - { 2305, 9, 35, 18, 4, -27 }, // 0x5B '[' - { 2345, 16, 24, 18, 1, -23 }, // 0x5C '\' - { 2393, 9, 35, 18, 5, -27 }, // 0x5D ']' - { 2433, 18, 11, 18, 0, -23 }, // 0x5E '^' - { 2458, 18, 2, 18, 0, 6 }, // 0x5F '_' - { 2463, 7, 3, 18, 6, -22 }, // 0x60 '`' - { 2466, 16, 19, 18, 1, -18 }, // 0x61 'a' - { 2504, 16, 25, 18, 1, -24 }, // 0x62 'b' - { 2554, 15, 19, 18, 1, -18 }, // 0x63 'c' - { 2590, 16, 25, 18, 1, -24 }, // 0x64 'd' - { 2640, 17, 19, 19, 1, -18 }, // 0x65 'e' - { 2681, 16, 25, 18, 1, -24 }, // 0x66 'f' - { 2731, 16, 26, 18, 1, -18 }, // 0x67 'g' - { 2783, 16, 25, 18, 1, -24 }, // 0x68 'h' - { 2833, 16, 25, 18, 1, -24 }, // 0x69 'i' - { 2883, 11, 32, 18, 4, -24 }, // 0x6A 'j' - { 2927, 16, 25, 18, 1, -24 }, // 0x6B 'k' - { 2977, 16, 25, 18, 1, -24 }, // 0x6C 'l' - { 3027, 16, 19, 18, 1, -18 }, // 0x6D 'm' - { 3065, 16, 19, 18, 1, -18 }, // 0x6E 'n' - { 3103, 16, 19, 18, 1, -18 }, // 0x6F 'o' - { 3141, 16, 26, 18, 1, -18 }, // 0x70 'p' - { 3193, 16, 26, 18, 1, -18 }, // 0x71 'q' - { 3245, 15, 19, 18, 2, -18 }, // 0x72 'r' - { 3281, 16, 19, 18, 1, -18 }, // 0x73 's' - { 3319, 16, 24, 18, 1, -23 }, // 0x74 't' - { 3367, 16, 19, 18, 1, -18 }, // 0x75 'u' - { 3405, 17, 19, 18, 1, -18 }, // 0x76 'v' - { 3446, 18, 19, 18, 0, -18 }, // 0x77 'w' - { 3489, 16, 19, 18, 1, -18 }, // 0x78 'x' - { 3527, 18, 26, 18, 0, -18 }, // 0x79 'y' - { 3586, 15, 19, 18, 2, -18 }, // 0x7A 'z' - { 3622, 12, 35, 18, 3, -27 }, // 0x7B '{' - { 3675, 3, 35, 18, 8, -27 }, // 0x7C '|' - { 3689, 11, 35, 18, 3, -27 }, // 0x7D '}' - { 3738, 13, 5, 19, 3, -14 } }; // 0x7E '~' + { 0, 1, 1, 18, 0, 0 }, // 0x20 ' ' + { 1, 4, 24, 18, 7, -23 }, // 0x21 '!' + { 13, 8, 7, 18, 5, -24 }, // 0x22 '"' + { 20, 14, 24, 18, 2, -23 }, // 0x23 '#' + { 62, 17, 24, 19, 1, -23 }, // 0x24 '$' + { 113, 16, 24, 18, 1, -23 }, // 0x25 '%' + { 161, 17, 24, 18, 1, -23 }, // 0x26 '&' + { 212, 3, 7, 18, 8, -24 }, // 0x27 ''' + { 215, 9, 35, 18, 4, -27 }, // 0x28 '(' + { 255, 9, 35, 18, 5, -27 }, // 0x29 ')' + { 295, 13, 13, 18, 3, -23 }, // 0x2A '*' + { 317, 15, 12, 18, 2, -18 }, // 0x2B '+' + { 340, 4, 7, 18, 7, -1 }, // 0x2C ',' + { 344, 16, 2, 18, 1, -11 }, // 0x2D '-' + { 348, 4, 3, 18, 7, -2 }, // 0x2E '.' + { 350, 16, 24, 18, 1, -23 }, // 0x2F '/' + { 398, 17, 24, 19, 1, -23 }, // 0x30 '0' + { 449, 16, 24, 18, 1, -23 }, // 0x31 '1' + { 497, 16, 24, 18, 1, -23 }, // 0x32 '2' + { 545, 16, 24, 18, 1, -23 }, // 0x33 '3' + { 593, 17, 24, 19, 1, -23 }, // 0x34 '4' + { 644, 16, 24, 18, 1, -23 }, // 0x35 '5' + { 692, 16, 24, 18, 1, -23 }, // 0x36 '6' + { 740, 16, 24, 18, 1, -23 }, // 0x37 '7' + { 788, 17, 24, 19, 1, -23 }, // 0x38 '8' + { 839, 16, 24, 18, 1, -23 }, // 0x39 '9' + { 887, 4, 14, 18, 7, -13 }, // 0x3A ':' + { 894, 4, 19, 18, 7, -13 }, // 0x3B ';' + { 904, 10, 18, 18, 4, -21 }, // 0x3C '<' + { 927, 12, 8, 18, 3, -16 }, // 0x3D '=' + { 939, 10, 18, 18, 4, -21 }, // 0x3E '>' + { 962, 16, 24, 18, 1, -23 }, // 0x3F '?' + { 1010, 16, 24, 19, 1, -23 }, // 0x40 '@' + { 1058, 16, 24, 18, 1, -23 }, // 0x41 'A' + { 1106, 16, 24, 18, 1, -23 }, // 0x42 'B' + { 1154, 15, 24, 18, 2, -23 }, // 0x43 'C' + { 1199, 16, 24, 18, 1, -23 }, // 0x44 'D' + { 1247, 15, 24, 18, 2, -23 }, // 0x45 'E' + { 1292, 15, 24, 18, 1, -23 }, // 0x46 'F' + { 1337, 16, 24, 18, 1, -23 }, // 0x47 'G' + { 1385, 16, 24, 18, 1, -23 }, // 0x48 'H' + { 1433, 16, 24, 18, 1, -23 }, // 0x49 'I' + { 1481, 15, 24, 19, 2, -23 }, // 0x4A 'J' + { 1526, 16, 24, 18, 1, -23 }, // 0x4B 'K' + { 1574, 16, 24, 18, 1, -23 }, // 0x4C 'L' + { 1622, 16, 24, 18, 1, -23 }, // 0x4D 'M' + { 1670, 16, 24, 18, 1, -23 }, // 0x4E 'N' + { 1718, 16, 24, 18, 1, -23 }, // 0x4F 'O' + { 1766, 16, 24, 18, 1, -23 }, // 0x50 'P' + { 1814, 16, 28, 18, 1, -23 }, // 0x51 'Q' + { 1870, 16, 24, 18, 1, -23 }, // 0x52 'R' + { 1918, 16, 24, 18, 1, -23 }, // 0x53 'S' + { 1966, 15, 24, 18, 1, -23 }, // 0x54 'T' + { 2011, 16, 24, 18, 1, -23 }, // 0x55 'U' + { 2059, 16, 24, 18, 1, -23 }, // 0x56 'V' + { 2107, 18, 24, 18, 0, -23 }, // 0x57 'W' + { 2161, 16, 24, 18, 1, -23 }, // 0x58 'X' + { 2209, 16, 24, 18, 1, -23 }, // 0x59 'Y' + { 2257, 16, 24, 18, 1, -23 }, // 0x5A 'Z' + { 2305, 9, 35, 18, 4, -27 }, // 0x5B '[' + { 2345, 16, 24, 18, 1, -23 }, // 0x5C '\' + { 2393, 9, 35, 18, 5, -27 }, // 0x5D ']' + { 2433, 18, 11, 18, 0, -23 }, // 0x5E '^' + { 2458, 18, 2, 18, 0, 6 }, // 0x5F '_' + { 2463, 7, 3, 18, 6, -22 }, // 0x60 '`' + { 2466, 16, 19, 18, 1, -18 }, // 0x61 'a' + { 2504, 16, 25, 18, 1, -24 }, // 0x62 'b' + { 2554, 15, 19, 18, 1, -18 }, // 0x63 'c' + { 2590, 16, 25, 18, 1, -24 }, // 0x64 'd' + { 2640, 17, 19, 19, 1, -18 }, // 0x65 'e' + { 2681, 16, 25, 18, 1, -24 }, // 0x66 'f' + { 2731, 16, 26, 18, 1, -18 }, // 0x67 'g' + { 2783, 16, 25, 18, 1, -24 }, // 0x68 'h' + { 2833, 16, 25, 18, 1, -24 }, // 0x69 'i' + { 2883, 11, 32, 18, 4, -24 }, // 0x6A 'j' + { 2927, 16, 25, 18, 1, -24 }, // 0x6B 'k' + { 2977, 16, 25, 18, 1, -24 }, // 0x6C 'l' + { 3027, 16, 19, 18, 1, -18 }, // 0x6D 'm' + { 3065, 16, 19, 18, 1, -18 }, // 0x6E 'n' + { 3103, 16, 19, 18, 1, -18 }, // 0x6F 'o' + { 3141, 16, 26, 18, 1, -18 }, // 0x70 'p' + { 3193, 16, 26, 18, 1, -18 }, // 0x71 'q' + { 3245, 15, 19, 18, 2, -18 }, // 0x72 'r' + { 3281, 16, 19, 18, 1, -18 }, // 0x73 's' + { 3319, 16, 24, 18, 1, -23 }, // 0x74 't' + { 3367, 16, 19, 18, 1, -18 }, // 0x75 'u' + { 3405, 17, 19, 18, 1, -18 }, // 0x76 'v' + { 3446, 18, 19, 18, 0, -18 }, // 0x77 'w' + { 3489, 16, 19, 18, 1, -18 }, // 0x78 'x' + { 3527, 18, 26, 18, 0, -18 }, // 0x79 'y' + { 3586, 15, 19, 18, 2, -18 }, // 0x7A 'z' + { 3622, 12, 35, 18, 3, -27 }, // 0x7B '{' + { 3675, 3, 35, 18, 8, -27 }, // 0x7C '|' + { 3689, 11, 35, 18, 3, -27 }, // 0x7D '}' + { 3738, 13, 5, 19, 3, -14 } }; // 0x7E '~' const GFXfont unispace12pt7b PROGMEM = { (uint8_t *)unispace12pt7bBitmaps, (GFXglyph *)unispace12pt7bGlyphs, - 0x20, 0x7E, 36 }; + 0x20, 0x7E, 36 }; // Approx. 4419 bytes +#endif // ifndef FONTS_UNISPACE12PT7B_H diff --git a/src/src/Static/Fonts/unispace8pt7b.h b/src/src/Static/Fonts/unispace8pt7b.h index f7257567d2..e12aff7b7a 100644 --- a/src/src/Static/Fonts/unispace8pt7b.h +++ b/src/src/Static/Fonts/unispace8pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_UNISPACE8PT7B_H +#define FONTS_UNISPACE8PT7B_H const uint8_t unispace8pt7bBitmaps[] PROGMEM = { 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0x8C, 0x63, 0x18, 0x80, 0x21, 0x10, 0x88, 0x44, 0x22, 0x17, 0xFC, 0x84, 0x42, 0x21, 0x10, 0x88, 0x5F, @@ -143,105 +145,106 @@ const uint8_t unispace8pt7bBitmaps[] PROGMEM = { 0x18, 0x30, 0x60, 0xC1, 0x83, 0x0C, 0x70, 0x71, 0x99, 0x8E }; const GFXglyph unispace8pt7bGlyphs[] PROGMEM = { - { 0, 1, 1, 12, 0, 0 }, // 0x20 ' ' - { 1, 3, 16, 12, 5, -15 }, // 0x21 '!' - { 7, 5, 5, 12, 3, -16 }, // 0x22 '"' - { 11, 9, 16, 12, 1, -15 }, // 0x23 '#' - { 29, 12, 16, 13, 0, -15 }, // 0x24 '$' - { 53, 10, 16, 12, 1, -15 }, // 0x25 '%' - { 73, 9, 16, 12, 1, -15 }, // 0x26 '&' - { 91, 1, 5, 12, 6, -16 }, // 0x27 ''' - { 92, 6, 24, 12, 3, -18 }, // 0x28 '(' - { 110, 6, 24, 12, 3, -18 }, // 0x29 ')' - { 128, 8, 9, 12, 2, -15 }, // 0x2A '*' - { 137, 9, 8, 12, 2, -12 }, // 0x2B '+' - { 146, 2, 5, 12, 5, 0 }, // 0x2C ',' - { 148, 10, 1, 12, 1, -7 }, // 0x2D '-' - { 150, 3, 1, 12, 5, 0 }, // 0x2E '.' - { 151, 10, 16, 12, 1, -15 }, // 0x2F '/' - { 171, 11, 16, 13, 1, -15 }, // 0x30 '0' - { 193, 11, 16, 12, 1, -15 }, // 0x31 '1' - { 215, 11, 16, 13, 1, -15 }, // 0x32 '2' - { 237, 11, 16, 12, 0, -15 }, // 0x33 '3' - { 259, 12, 16, 12, 0, -15 }, // 0x34 '4' - { 283, 11, 16, 13, 1, -15 }, // 0x35 '5' - { 305, 11, 16, 13, 1, -15 }, // 0x36 '6' - { 327, 11, 16, 12, 1, -15 }, // 0x37 '7' - { 349, 11, 16, 12, 1, -15 }, // 0x38 '8' - { 371, 11, 16, 13, 1, -15 }, // 0x39 '9' - { 393, 3, 9, 12, 5, -8 }, // 0x3A ':' - { 397, 3, 13, 12, 5, -8 }, // 0x3B ';' - { 402, 7, 12, 12, 3, -13 }, // 0x3C '<' - { 413, 8, 5, 12, 2, -10 }, // 0x3D '=' - { 418, 8, 12, 12, 2, -13 }, // 0x3E '>' - { 430, 11, 16, 12, 0, -15 }, // 0x3F '?' - { 452, 10, 16, 12, 1, -15 }, // 0x40 '@' - { 472, 10, 16, 12, 1, -15 }, // 0x41 'A' - { 492, 11, 16, 12, 0, -15 }, // 0x42 'B' - { 514, 10, 16, 12, 1, -15 }, // 0x43 'C' - { 534, 11, 16, 13, 1, -15 }, // 0x44 'D' - { 556, 10, 16, 12, 1, -15 }, // 0x45 'E' - { 576, 11, 16, 12, 1, -15 }, // 0x46 'F' - { 598, 11, 16, 13, 1, -15 }, // 0x47 'G' - { 620, 11, 16, 13, 1, -15 }, // 0x48 'H' - { 642, 11, 16, 12, 1, -15 }, // 0x49 'I' - { 664, 11, 16, 12, 0, -15 }, // 0x4A 'J' - { 686, 11, 16, 12, 1, -15 }, // 0x4B 'K' - { 708, 11, 16, 12, 1, -15 }, // 0x4C 'L' - { 730, 9, 16, 11, 1, -15 }, // 0x4D 'M' - { 748, 10, 16, 12, 1, -15 }, // 0x4E 'N' - { 768, 11, 16, 13, 1, -15 }, // 0x4F 'O' - { 790, 11, 16, 13, 1, -15 }, // 0x50 'P' - { 812, 11, 18, 13, 1, -15 }, // 0x51 'Q' - { 837, 11, 16, 13, 1, -15 }, // 0x52 'R' - { 859, 11, 16, 13, 1, -15 }, // 0x53 'S' - { 881, 11, 16, 12, 1, -15 }, // 0x54 'T' - { 903, 11, 16, 13, 1, -15 }, // 0x55 'U' - { 925, 10, 16, 12, 1, -15 }, // 0x56 'V' - { 945, 12, 16, 12, 0, -15 }, // 0x57 'W' - { 969, 10, 16, 12, 1, -15 }, // 0x58 'X' - { 989, 11, 16, 12, 0, -15 }, // 0x59 'Y' - { 1011, 10, 16, 12, 1, -15 }, // 0x5A 'Z' - { 1031, 6, 24, 12, 3, -18 }, // 0x5B '[' - { 1049, 10, 16, 12, 1, -15 }, // 0x5C '\' - { 1069, 6, 24, 12, 3, -18 }, // 0x5D ']' - { 1087, 12, 7, 12, 0, -15 }, // 0x5E '^' - { 1098, 12, 1, 12, 0, 4 }, // 0x5F '_' - { 1100, 4, 3, 12, 4, -15 }, // 0x60 '`' - { 1102, 11, 13, 13, 1, -12 }, // 0x61 'a' - { 1120, 11, 17, 13, 1, -16 }, // 0x62 'b' - { 1144, 10, 13, 12, 1, -12 }, // 0x63 'c' - { 1161, 11, 17, 13, 1, -16 }, // 0x64 'd' - { 1185, 11, 13, 13, 1, -12 }, // 0x65 'e' - { 1203, 11, 17, 12, 1, -16 }, // 0x66 'f' - { 1227, 11, 18, 13, 1, -12 }, // 0x67 'g' - { 1252, 11, 17, 13, 1, -16 }, // 0x68 'h' - { 1276, 11, 17, 12, 1, -16 }, // 0x69 'i' - { 1300, 8, 22, 12, 2, -16 }, // 0x6A 'j' - { 1322, 11, 17, 12, 1, -16 }, // 0x6B 'k' - { 1346, 11, 17, 12, 0, -16 }, // 0x6C 'l' - { 1370, 12, 13, 13, 0, -12 }, // 0x6D 'm' - { 1390, 11, 13, 13, 1, -12 }, // 0x6E 'n' - { 1408, 11, 13, 13, 1, -12 }, // 0x6F 'o' - { 1426, 11, 18, 13, 1, -12 }, // 0x70 'p' - { 1451, 11, 18, 13, 1, -12 }, // 0x71 'q' - { 1476, 10, 13, 12, 1, -12 }, // 0x72 'r' - { 1493, 11, 13, 12, 1, -12 }, // 0x73 's' - { 1511, 10, 16, 12, 1, -15 }, // 0x74 't' - { 1531, 11, 13, 13, 1, -12 }, // 0x75 'u' - { 1549, 12, 13, 12, 0, -12 }, // 0x76 'v' - { 1569, 12, 13, 12, 0, -12 }, // 0x77 'w' - { 1589, 11, 13, 12, 1, -12 }, // 0x78 'x' - { 1607, 12, 18, 12, 0, -12 }, // 0x79 'y' - { 1634, 10, 13, 12, 1, -12 }, // 0x7A 'z' - { 1651, 8, 24, 12, 2, -18 }, // 0x7B '{' - { 1675, 1, 24, 12, 6, -18 }, // 0x7C '|' - { 1678, 7, 24, 12, 3, -18 }, // 0x7D '}' - { 1699, 8, 3, 12, 2, -9 } }; // 0x7E '~' + { 0, 1, 1, 12, 0, 0 }, // 0x20 ' ' + { 1, 3, 16, 12, 5, -15 }, // 0x21 '!' + { 7, 5, 5, 12, 3, -16 }, // 0x22 '"' + { 11, 9, 16, 12, 1, -15 }, // 0x23 '#' + { 29, 12, 16, 13, 0, -15 }, // 0x24 '$' + { 53, 10, 16, 12, 1, -15 }, // 0x25 '%' + { 73, 9, 16, 12, 1, -15 }, // 0x26 '&' + { 91, 1, 5, 12, 6, -16 }, // 0x27 ''' + { 92, 6, 24, 12, 3, -18 }, // 0x28 '(' + { 110, 6, 24, 12, 3, -18 }, // 0x29 ')' + { 128, 8, 9, 12, 2, -15 }, // 0x2A '*' + { 137, 9, 8, 12, 2, -12 }, // 0x2B '+' + { 146, 2, 5, 12, 5, 0 }, // 0x2C ',' + { 148, 10, 1, 12, 1, -7 }, // 0x2D '-' + { 150, 3, 1, 12, 5, 0 }, // 0x2E '.' + { 151, 10, 16, 12, 1, -15 }, // 0x2F '/' + { 171, 11, 16, 13, 1, -15 }, // 0x30 '0' + { 193, 11, 16, 12, 1, -15 }, // 0x31 '1' + { 215, 11, 16, 13, 1, -15 }, // 0x32 '2' + { 237, 11, 16, 12, 0, -15 }, // 0x33 '3' + { 259, 12, 16, 12, 0, -15 }, // 0x34 '4' + { 283, 11, 16, 13, 1, -15 }, // 0x35 '5' + { 305, 11, 16, 13, 1, -15 }, // 0x36 '6' + { 327, 11, 16, 12, 1, -15 }, // 0x37 '7' + { 349, 11, 16, 12, 1, -15 }, // 0x38 '8' + { 371, 11, 16, 13, 1, -15 }, // 0x39 '9' + { 393, 3, 9, 12, 5, -8 }, // 0x3A ':' + { 397, 3, 13, 12, 5, -8 }, // 0x3B ';' + { 402, 7, 12, 12, 3, -13 }, // 0x3C '<' + { 413, 8, 5, 12, 2, -10 }, // 0x3D '=' + { 418, 8, 12, 12, 2, -13 }, // 0x3E '>' + { 430, 11, 16, 12, 0, -15 }, // 0x3F '?' + { 452, 10, 16, 12, 1, -15 }, // 0x40 '@' + { 472, 10, 16, 12, 1, -15 }, // 0x41 'A' + { 492, 11, 16, 12, 0, -15 }, // 0x42 'B' + { 514, 10, 16, 12, 1, -15 }, // 0x43 'C' + { 534, 11, 16, 13, 1, -15 }, // 0x44 'D' + { 556, 10, 16, 12, 1, -15 }, // 0x45 'E' + { 576, 11, 16, 12, 1, -15 }, // 0x46 'F' + { 598, 11, 16, 13, 1, -15 }, // 0x47 'G' + { 620, 11, 16, 13, 1, -15 }, // 0x48 'H' + { 642, 11, 16, 12, 1, -15 }, // 0x49 'I' + { 664, 11, 16, 12, 0, -15 }, // 0x4A 'J' + { 686, 11, 16, 12, 1, -15 }, // 0x4B 'K' + { 708, 11, 16, 12, 1, -15 }, // 0x4C 'L' + { 730, 9, 16, 11, 1, -15 }, // 0x4D 'M' + { 748, 10, 16, 12, 1, -15 }, // 0x4E 'N' + { 768, 11, 16, 13, 1, -15 }, // 0x4F 'O' + { 790, 11, 16, 13, 1, -15 }, // 0x50 'P' + { 812, 11, 18, 13, 1, -15 }, // 0x51 'Q' + { 837, 11, 16, 13, 1, -15 }, // 0x52 'R' + { 859, 11, 16, 13, 1, -15 }, // 0x53 'S' + { 881, 11, 16, 12, 1, -15 }, // 0x54 'T' + { 903, 11, 16, 13, 1, -15 }, // 0x55 'U' + { 925, 10, 16, 12, 1, -15 }, // 0x56 'V' + { 945, 12, 16, 12, 0, -15 }, // 0x57 'W' + { 969, 10, 16, 12, 1, -15 }, // 0x58 'X' + { 989, 11, 16, 12, 0, -15 }, // 0x59 'Y' + { 1011, 10, 16, 12, 1, -15 }, // 0x5A 'Z' + { 1031, 6, 24, 12, 3, -18 }, // 0x5B '[' + { 1049, 10, 16, 12, 1, -15 }, // 0x5C '\' + { 1069, 6, 24, 12, 3, -18 }, // 0x5D ']' + { 1087, 12, 7, 12, 0, -15 }, // 0x5E '^' + { 1098, 12, 1, 12, 0, 4 }, // 0x5F '_' + { 1100, 4, 3, 12, 4, -15 }, // 0x60 '`' + { 1102, 11, 13, 13, 1, -12 }, // 0x61 'a' + { 1120, 11, 17, 13, 1, -16 }, // 0x62 'b' + { 1144, 10, 13, 12, 1, -12 }, // 0x63 'c' + { 1161, 11, 17, 13, 1, -16 }, // 0x64 'd' + { 1185, 11, 13, 13, 1, -12 }, // 0x65 'e' + { 1203, 11, 17, 12, 1, -16 }, // 0x66 'f' + { 1227, 11, 18, 13, 1, -12 }, // 0x67 'g' + { 1252, 11, 17, 13, 1, -16 }, // 0x68 'h' + { 1276, 11, 17, 12, 1, -16 }, // 0x69 'i' + { 1300, 8, 22, 12, 2, -16 }, // 0x6A 'j' + { 1322, 11, 17, 12, 1, -16 }, // 0x6B 'k' + { 1346, 11, 17, 12, 0, -16 }, // 0x6C 'l' + { 1370, 12, 13, 13, 0, -12 }, // 0x6D 'm' + { 1390, 11, 13, 13, 1, -12 }, // 0x6E 'n' + { 1408, 11, 13, 13, 1, -12 }, // 0x6F 'o' + { 1426, 11, 18, 13, 1, -12 }, // 0x70 'p' + { 1451, 11, 18, 13, 1, -12 }, // 0x71 'q' + { 1476, 10, 13, 12, 1, -12 }, // 0x72 'r' + { 1493, 11, 13, 12, 1, -12 }, // 0x73 's' + { 1511, 10, 16, 12, 1, -15 }, // 0x74 't' + { 1531, 11, 13, 13, 1, -12 }, // 0x75 'u' + { 1549, 12, 13, 12, 0, -12 }, // 0x76 'v' + { 1569, 12, 13, 12, 0, -12 }, // 0x77 'w' + { 1589, 11, 13, 12, 1, -12 }, // 0x78 'x' + { 1607, 12, 18, 12, 0, -12 }, // 0x79 'y' + { 1634, 10, 13, 12, 1, -12 }, // 0x7A 'z' + { 1651, 8, 24, 12, 2, -18 }, // 0x7B '{' + { 1675, 1, 24, 12, 6, -18 }, // 0x7C '|' + { 1678, 7, 24, 12, 3, -18 }, // 0x7D '}' + { 1699, 8, 3, 12, 2, -9 } }; // 0x7E '~' const GFXfont unispace8pt7b PROGMEM = { (uint8_t *)unispace8pt7bBitmaps, (GFXglyph *)unispace8pt7bGlyphs, - 0x20, 0x7E, 24 }; + 0x20, 0x7E, 24 }; // Approx. 2374 bytes +#endif // ifndef FONTS_UNISPACE8PT7B_H diff --git a/src/src/Static/Fonts/unispace_italic12pt7b.h b/src/src/Static/Fonts/unispace_italic12pt7b.h index f46046a5ea..62c4f6cdc6 100644 --- a/src/src/Static/Fonts/unispace_italic12pt7b.h +++ b/src/src/Static/Fonts/unispace_italic12pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_UNISPACE_ITALIC12PT7B_H +#define FONTS_UNISPACE_ITALIC12PT7B_H const uint8_t unispace_italic12pt7bBitmaps[] PROGMEM = { 0x00, 0x07, 0x83, 0xC1, 0xE1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE1, 0xE0, 0xF0, 0x78, 0x3C, 0x20, 0x00, 0x00, 0x00, @@ -381,105 +383,106 @@ const uint8_t unispace_italic12pt7bBitmaps[] PROGMEM = { 0xB0, 0xFC, 0xC1, 0xE0 }; const GFXglyph unispace_italic12pt7bGlyphs[] PROGMEM = { - { 0, 1, 1, 18, 0, 0 }, // 0x20 ' ' - { 1, 9, 24, 18, 7, -23 }, // 0x21 '!' - { 28, 10, 7, 18, 9, -24 }, // 0x22 '"' - { 37, 17, 24, 18, 3, -23 }, // 0x23 '#' - { 88, 19, 24, 19, 2, -23 }, // 0x24 '$' - { 145, 15, 24, 18, 4, -23 }, // 0x25 '%' - { 190, 17, 24, 18, 2, -23 }, // 0x26 '&' - { 241, 4, 7, 18, 12, -24 }, // 0x27 ''' - { 245, 17, 35, 18, 3, -27 }, // 0x28 '(' - { 320, 17, 35, 18, 2, -27 }, // 0x29 ')' - { 395, 13, 13, 18, 6, -23 }, // 0x2A '*' - { 417, 15, 12, 18, 5, -18 }, // 0x2B '+' - { 440, 5, 7, 18, 6, -1 }, // 0x2C ',' - { 445, 15, 2, 18, 4, -11 }, // 0x2D '-' - { 449, 5, 3, 18, 7, -2 }, // 0x2E '.' - { 451, 21, 24, 18, 1, -23 }, // 0x2F '/' - { 514, 19, 24, 18, 2, -23 }, // 0x30 '0' - { 571, 16, 24, 18, 1, -23 }, // 0x31 '1' - { 619, 20, 24, 18, 1, -23 }, // 0x32 '2' - { 679, 20, 24, 18, 1, -23 }, // 0x33 '3' - { 739, 18, 24, 18, 2, -23 }, // 0x34 '4' - { 793, 19, 24, 18, 1, -23 }, // 0x35 '5' - { 850, 17, 24, 18, 2, -23 }, // 0x36 '6' - { 901, 19, 24, 18, 3, -23 }, // 0x37 '7' - { 958, 19, 24, 19, 2, -23 }, // 0x38 '8' - { 1015, 18, 24, 18, 3, -23 }, // 0x39 '9' - { 1069, 7, 14, 18, 7, -13 }, // 0x3A ':' - { 1082, 8, 19, 18, 6, -13 }, // 0x3B ';' - { 1101, 10, 18, 18, 7, -21 }, // 0x3C '<' - { 1124, 12, 8, 18, 6, -16 }, // 0x3D '=' - { 1136, 10, 18, 18, 7, -21 }, // 0x3E '>' - { 1159, 17, 24, 18, 4, -23 }, // 0x3F '?' - { 1210, 19, 24, 18, 2, -23 }, // 0x40 '@' - { 1267, 17, 24, 18, 0, -23 }, // 0x41 'A' - { 1318, 20, 24, 18, 1, -23 }, // 0x42 'B' - { 1378, 19, 24, 18, 3, -23 }, // 0x43 'C' - { 1435, 20, 24, 18, 1, -23 }, // 0x44 'D' - { 1495, 20, 24, 18, 2, -23 }, // 0x45 'E' - { 1555, 21, 24, 18, 1, -23 }, // 0x46 'F' - { 1618, 19, 24, 18, 2, -23 }, // 0x47 'G' - { 1675, 21, 24, 18, 1, -23 }, // 0x48 'H' - { 1738, 20, 24, 18, 2, -23 }, // 0x49 'I' - { 1798, 20, 24, 18, 2, -23 }, // 0x4A 'J' - { 1858, 21, 24, 18, 1, -23 }, // 0x4B 'K' - { 1921, 16, 24, 18, 1, -23 }, // 0x4C 'L' - { 1969, 21, 24, 18, 1, -23 }, // 0x4D 'M' - { 2032, 21, 24, 18, 1, -23 }, // 0x4E 'N' - { 2095, 19, 24, 18, 2, -23 }, // 0x4F 'O' - { 2152, 20, 24, 18, 1, -23 }, // 0x50 'P' - { 2212, 19, 28, 18, 2, -23 }, // 0x51 'Q' - { 2279, 19, 24, 17, 1, -23 }, // 0x52 'R' - { 2336, 19, 24, 18, 2, -23 }, // 0x53 'S' - { 2393, 16, 24, 18, 6, -23 }, // 0x54 'T' - { 2441, 20, 24, 18, 2, -23 }, // 0x55 'U' - { 2501, 17, 24, 18, 6, -23 }, // 0x56 'V' - { 2552, 20, 24, 18, 3, -23 }, // 0x57 'W' - { 2612, 21, 24, 18, 1, -23 }, // 0x58 'X' - { 2675, 16, 24, 18, 6, -23 }, // 0x59 'Y' - { 2723, 21, 24, 18, 1, -23 }, // 0x5A 'Z' - { 2786, 17, 35, 18, 3, -27 }, // 0x5B '[' - { 2861, 11, 24, 18, 6, -23 }, // 0x5C '\' - { 2894, 17, 35, 18, 3, -27 }, // 0x5D ']' - { 2969, 18, 11, 18, 3, -23 }, // 0x5E '^' - { 2994, 19, 2, 18, -2, 6 }, // 0x5F '_' - { 2999, 7, 3, 18, 10, -22 }, // 0x60 '`' - { 3002, 18, 19, 18, 2, -18 }, // 0x61 'a' - { 3045, 19, 25, 18, 1, -24 }, // 0x62 'b' - { 3105, 18, 19, 18, 2, -18 }, // 0x63 'c' - { 3148, 20, 25, 18, 2, -24 }, // 0x64 'd' - { 3211, 19, 19, 19, 2, -18 }, // 0x65 'e' - { 3257, 17, 25, 18, 5, -24 }, // 0x66 'f' - { 3311, 19, 26, 18, 2, -18 }, // 0x67 'g' - { 3373, 19, 25, 18, 1, -24 }, // 0x68 'h' - { 3433, 16, 25, 18, 1, -24 }, // 0x69 'i' - { 3483, 18, 32, 18, 2, -24 }, // 0x6A 'j' - { 3555, 20, 25, 18, 1, -24 }, // 0x6B 'k' - { 3618, 16, 25, 18, 1, -24 }, // 0x6C 'l' - { 3668, 19, 19, 18, 1, -18 }, // 0x6D 'm' - { 3714, 19, 19, 18, 1, -18 }, // 0x6E 'n' - { 3760, 18, 19, 18, 2, -18 }, // 0x6F 'o' - { 3803, 21, 26, 18, -1, -18 }, // 0x70 'p' - { 3872, 19, 26, 18, 2, -18 }, // 0x71 'q' - { 3934, 18, 19, 18, 2, -18 }, // 0x72 'r' - { 3977, 20, 19, 18, 1, -18 }, // 0x73 's' - { 4025, 16, 24, 18, 5, -23 }, // 0x74 't' - { 4073, 19, 19, 18, 2, -18 }, // 0x75 'u' - { 4119, 17, 19, 18, 5, -18 }, // 0x76 'v' - { 4160, 19, 19, 18, 3, -18 }, // 0x77 'w' - { 4206, 20, 19, 18, 1, -18 }, // 0x78 'x' - { 4254, 19, 26, 18, 3, -18 }, // 0x79 'y' - { 4316, 19, 19, 18, 2, -18 }, // 0x7A 'z' - { 4362, 15, 35, 18, 6, -27 }, // 0x7B '{' - { 4428, 11, 35, 18, 6, -27 }, // 0x7C '|' - { 4477, 15, 35, 19, 2, -27 }, // 0x7D '}' - { 4543, 14, 5, 18, 5, -14 } }; // 0x7E '~' + { 0, 1, 1, 18, 0, 0 }, // 0x20 ' ' + { 1, 9, 24, 18, 7, -23 }, // 0x21 '!' + { 28, 10, 7, 18, 9, -24 }, // 0x22 '"' + { 37, 17, 24, 18, 3, -23 }, // 0x23 '#' + { 88, 19, 24, 19, 2, -23 }, // 0x24 '$' + { 145, 15, 24, 18, 4, -23 }, // 0x25 '%' + { 190, 17, 24, 18, 2, -23 }, // 0x26 '&' + { 241, 4, 7, 18, 12, -24 }, // 0x27 ''' + { 245, 17, 35, 18, 3, -27 }, // 0x28 '(' + { 320, 17, 35, 18, 2, -27 }, // 0x29 ')' + { 395, 13, 13, 18, 6, -23 }, // 0x2A '*' + { 417, 15, 12, 18, 5, -18 }, // 0x2B '+' + { 440, 5, 7, 18, 6, -1 }, // 0x2C ',' + { 445, 15, 2, 18, 4, -11 }, // 0x2D '-' + { 449, 5, 3, 18, 7, -2 }, // 0x2E '.' + { 451, 21, 24, 18, 1, -23 }, // 0x2F '/' + { 514, 19, 24, 18, 2, -23 }, // 0x30 '0' + { 571, 16, 24, 18, 1, -23 }, // 0x31 '1' + { 619, 20, 24, 18, 1, -23 }, // 0x32 '2' + { 679, 20, 24, 18, 1, -23 }, // 0x33 '3' + { 739, 18, 24, 18, 2, -23 }, // 0x34 '4' + { 793, 19, 24, 18, 1, -23 }, // 0x35 '5' + { 850, 17, 24, 18, 2, -23 }, // 0x36 '6' + { 901, 19, 24, 18, 3, -23 }, // 0x37 '7' + { 958, 19, 24, 19, 2, -23 }, // 0x38 '8' + { 1015, 18, 24, 18, 3, -23 }, // 0x39 '9' + { 1069, 7, 14, 18, 7, -13 }, // 0x3A ':' + { 1082, 8, 19, 18, 6, -13 }, // 0x3B ';' + { 1101, 10, 18, 18, 7, -21 }, // 0x3C '<' + { 1124, 12, 8, 18, 6, -16 }, // 0x3D '=' + { 1136, 10, 18, 18, 7, -21 }, // 0x3E '>' + { 1159, 17, 24, 18, 4, -23 }, // 0x3F '?' + { 1210, 19, 24, 18, 2, -23 }, // 0x40 '@' + { 1267, 17, 24, 18, 0, -23 }, // 0x41 'A' + { 1318, 20, 24, 18, 1, -23 }, // 0x42 'B' + { 1378, 19, 24, 18, 3, -23 }, // 0x43 'C' + { 1435, 20, 24, 18, 1, -23 }, // 0x44 'D' + { 1495, 20, 24, 18, 2, -23 }, // 0x45 'E' + { 1555, 21, 24, 18, 1, -23 }, // 0x46 'F' + { 1618, 19, 24, 18, 2, -23 }, // 0x47 'G' + { 1675, 21, 24, 18, 1, -23 }, // 0x48 'H' + { 1738, 20, 24, 18, 2, -23 }, // 0x49 'I' + { 1798, 20, 24, 18, 2, -23 }, // 0x4A 'J' + { 1858, 21, 24, 18, 1, -23 }, // 0x4B 'K' + { 1921, 16, 24, 18, 1, -23 }, // 0x4C 'L' + { 1969, 21, 24, 18, 1, -23 }, // 0x4D 'M' + { 2032, 21, 24, 18, 1, -23 }, // 0x4E 'N' + { 2095, 19, 24, 18, 2, -23 }, // 0x4F 'O' + { 2152, 20, 24, 18, 1, -23 }, // 0x50 'P' + { 2212, 19, 28, 18, 2, -23 }, // 0x51 'Q' + { 2279, 19, 24, 17, 1, -23 }, // 0x52 'R' + { 2336, 19, 24, 18, 2, -23 }, // 0x53 'S' + { 2393, 16, 24, 18, 6, -23 }, // 0x54 'T' + { 2441, 20, 24, 18, 2, -23 }, // 0x55 'U' + { 2501, 17, 24, 18, 6, -23 }, // 0x56 'V' + { 2552, 20, 24, 18, 3, -23 }, // 0x57 'W' + { 2612, 21, 24, 18, 1, -23 }, // 0x58 'X' + { 2675, 16, 24, 18, 6, -23 }, // 0x59 'Y' + { 2723, 21, 24, 18, 1, -23 }, // 0x5A 'Z' + { 2786, 17, 35, 18, 3, -27 }, // 0x5B '[' + { 2861, 11, 24, 18, 6, -23 }, // 0x5C '\' + { 2894, 17, 35, 18, 3, -27 }, // 0x5D ']' + { 2969, 18, 11, 18, 3, -23 }, // 0x5E '^' + { 2994, 19, 2, 18, -2, 6 }, // 0x5F '_' + { 2999, 7, 3, 18, 10, -22 }, // 0x60 '`' + { 3002, 18, 19, 18, 2, -18 }, // 0x61 'a' + { 3045, 19, 25, 18, 1, -24 }, // 0x62 'b' + { 3105, 18, 19, 18, 2, -18 }, // 0x63 'c' + { 3148, 20, 25, 18, 2, -24 }, // 0x64 'd' + { 3211, 19, 19, 19, 2, -18 }, // 0x65 'e' + { 3257, 17, 25, 18, 5, -24 }, // 0x66 'f' + { 3311, 19, 26, 18, 2, -18 }, // 0x67 'g' + { 3373, 19, 25, 18, 1, -24 }, // 0x68 'h' + { 3433, 16, 25, 18, 1, -24 }, // 0x69 'i' + { 3483, 18, 32, 18, 2, -24 }, // 0x6A 'j' + { 3555, 20, 25, 18, 1, -24 }, // 0x6B 'k' + { 3618, 16, 25, 18, 1, -24 }, // 0x6C 'l' + { 3668, 19, 19, 18, 1, -18 }, // 0x6D 'm' + { 3714, 19, 19, 18, 1, -18 }, // 0x6E 'n' + { 3760, 18, 19, 18, 2, -18 }, // 0x6F 'o' + { 3803, 21, 26, 18, -1, -18 }, // 0x70 'p' + { 3872, 19, 26, 18, 2, -18 }, // 0x71 'q' + { 3934, 18, 19, 18, 2, -18 }, // 0x72 'r' + { 3977, 20, 19, 18, 1, -18 }, // 0x73 's' + { 4025, 16, 24, 18, 5, -23 }, // 0x74 't' + { 4073, 19, 19, 18, 2, -18 }, // 0x75 'u' + { 4119, 17, 19, 18, 5, -18 }, // 0x76 'v' + { 4160, 19, 19, 18, 3, -18 }, // 0x77 'w' + { 4206, 20, 19, 18, 1, -18 }, // 0x78 'x' + { 4254, 19, 26, 18, 3, -18 }, // 0x79 'y' + { 4316, 19, 19, 18, 2, -18 }, // 0x7A 'z' + { 4362, 15, 35, 18, 6, -27 }, // 0x7B '{' + { 4428, 11, 35, 18, 6, -27 }, // 0x7C '|' + { 4477, 15, 35, 19, 2, -27 }, // 0x7D '}' + { 4543, 14, 5, 18, 5, -14 } }; // 0x7E '~' const GFXfont unispace_italic12pt7b PROGMEM = { (uint8_t *)unispace_italic12pt7bBitmaps, (GFXglyph *)unispace_italic12pt7bGlyphs, - 0x20, 0x7E, 36 }; + 0x20, 0x7E, 36 }; // Approx. 5224 bytes +#endif // ifndef FONTS_UNISPACE_ITALIC12PT7B_H diff --git a/src/src/Static/Fonts/unispace_italic8pt7b.h b/src/src/Static/Fonts/unispace_italic8pt7b.h index e23bf56ca9..cc1fe79aa9 100644 --- a/src/src/Static/Fonts/unispace_italic8pt7b.h +++ b/src/src/Static/Fonts/unispace_italic8pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_UNISPACE_ITALIC8PT7B_H +#define FONTS_UNISPACE_ITALIC8PT7B_H const uint8_t unispace_italic8pt7bBitmaps[] PROGMEM = { 0x00, 0x1C, 0x61, 0x8E, 0x38, 0xE3, 0x0C, 0x71, 0xC7, 0x18, 0x00, 0x00, 0x38, 0x6D, 0xBC, 0xF3, 0xD8, 0x0C, 0x61, 0x88, 0x31, 0x04, 0x60, 0x8C, @@ -176,105 +178,106 @@ const uint8_t unispace_italic8pt7bBitmaps[] PROGMEM = { 0x78, 0xA6, 0xF1, 0xC0 }; const GFXglyph unispace_italic8pt7bGlyphs[] PROGMEM = { - { 0, 1, 1, 12, 0, 0 }, // 0x20 ' ' - { 1, 6, 16, 12, 5, -15 }, // 0x21 '!' - { 13, 6, 5, 12, 6, -16 }, // 0x22 '"' - { 17, 11, 16, 12, 2, -15 }, // 0x23 '#' - { 39, 12, 16, 12, 2, -15 }, // 0x24 '$' - { 63, 10, 16, 11, 3, -15 }, // 0x25 '%' - { 83, 13, 16, 12, 1, -15 }, // 0x26 '&' - { 109, 3, 5, 12, 8, -16 }, // 0x27 ''' - { 111, 12, 24, 12, 2, -18 }, // 0x28 '(' - { 147, 12, 24, 12, 1, -18 }, // 0x29 ')' - { 183, 9, 9, 12, 4, -15 }, // 0x2A '*' - { 194, 9, 8, 12, 3, -12 }, // 0x2B '+' - { 203, 3, 5, 12, 4, 0 }, // 0x2C ',' - { 205, 11, 1, 12, 2, -7 }, // 0x2D '-' - { 207, 3, 1, 12, 5, 0 }, // 0x2E '.' - { 208, 14, 16, 12, 1, -15 }, // 0x2F '/' - { 236, 13, 16, 12, 1, -15 }, // 0x30 '0' - { 262, 11, 16, 12, 1, -15 }, // 0x31 '1' - { 284, 14, 16, 12, 0, -15 }, // 0x32 '2' - { 312, 14, 16, 13, 1, -15 }, // 0x33 '3' - { 340, 12, 16, 12, 1, -15 }, // 0x34 '4' - { 364, 14, 16, 12, 0, -15 }, // 0x35 '5' - { 392, 12, 16, 12, 1, -15 }, // 0x36 '6' - { 416, 13, 16, 12, 2, -15 }, // 0x37 '7' - { 442, 14, 16, 13, 1, -15 }, // 0x38 '8' - { 470, 13, 16, 12, 1, -15 }, // 0x39 '9' - { 496, 4, 9, 12, 5, -8 }, // 0x3A ':' - { 501, 5, 13, 12, 4, -8 }, // 0x3B ';' - { 510, 7, 12, 12, 4, -13 }, // 0x3C '<' - { 521, 8, 5, 12, 4, -10 }, // 0x3D '=' - { 526, 7, 12, 12, 4, -13 }, // 0x3E '>' - { 537, 11, 16, 12, 3, -15 }, // 0x3F '?' - { 559, 12, 16, 12, 2, -15 }, // 0x40 '@' - { 583, 11, 16, 13, 1, -15 }, // 0x41 'A' - { 605, 14, 16, 12, 1, -15 }, // 0x42 'B' - { 633, 13, 16, 12, 2, -15 }, // 0x43 'C' - { 659, 13, 16, 12, 1, -15 }, // 0x44 'D' - { 685, 13, 16, 12, 1, -15 }, // 0x45 'E' - { 711, 14, 16, 12, 1, -15 }, // 0x46 'F' - { 739, 13, 16, 12, 1, -15 }, // 0x47 'G' - { 765, 14, 16, 12, 1, -15 }, // 0x48 'H' - { 793, 13, 16, 12, 1, -15 }, // 0x49 'I' - { 819, 13, 16, 12, 1, -15 }, // 0x4A 'J' - { 845, 14, 16, 12, 1, -15 }, // 0x4B 'K' - { 873, 11, 16, 12, 1, -15 }, // 0x4C 'L' - { 895, 14, 16, 12, 1, -15 }, // 0x4D 'M' - { 923, 14, 16, 12, 1, -15 }, // 0x4E 'N' - { 951, 14, 16, 13, 1, -15 }, // 0x4F 'O' - { 979, 13, 16, 12, 1, -15 }, // 0x50 'P' - { 1005, 14, 18, 13, 1, -15 }, // 0x51 'Q' - { 1037, 14, 16, 11, 0, -15 }, // 0x52 'R' - { 1065, 13, 16, 12, 1, -15 }, // 0x53 'S' - { 1091, 10, 16, 12, 4, -15 }, // 0x54 'T' - { 1111, 14, 16, 12, 1, -15 }, // 0x55 'U' - { 1139, 11, 16, 12, 4, -15 }, // 0x56 'V' - { 1161, 13, 16, 12, 2, -15 }, // 0x57 'W' - { 1187, 14, 16, 12, 1, -15 }, // 0x58 'X' - { 1215, 11, 16, 12, 4, -15 }, // 0x59 'Y' - { 1237, 14, 16, 12, 1, -15 }, // 0x5A 'Z' - { 1265, 11, 24, 12, 2, -18 }, // 0x5B '[' - { 1298, 7, 16, 12, 4, -15 }, // 0x5C '\' - { 1312, 11, 24, 12, 2, -18 }, // 0x5D ']' - { 1345, 12, 7, 12, 2, -15 }, // 0x5E '^' - { 1356, 12, 1, 12, -1, 4 }, // 0x5F '_' - { 1358, 5, 3, 12, 7, -15 }, // 0x60 '`' - { 1360, 13, 13, 12, 1, -12 }, // 0x61 'a' - { 1382, 13, 17, 13, 1, -16 }, // 0x62 'b' - { 1410, 13, 13, 12, 1, -12 }, // 0x63 'c' - { 1432, 13, 17, 12, 1, -16 }, // 0x64 'd' - { 1460, 13, 13, 13, 1, -12 }, // 0x65 'e' - { 1482, 12, 17, 12, 3, -16 }, // 0x66 'f' - { 1508, 13, 18, 12, 1, -12 }, // 0x67 'g' - { 1538, 13, 17, 13, 1, -16 }, // 0x68 'h' - { 1566, 10, 17, 12, 1, -16 }, // 0x69 'i' - { 1588, 12, 22, 12, 1, -16 }, // 0x6A 'j' - { 1621, 13, 17, 12, 1, -16 }, // 0x6B 'k' - { 1649, 10, 17, 12, 1, -16 }, // 0x6C 'l' - { 1671, 13, 13, 12, 1, -12 }, // 0x6D 'm' - { 1693, 13, 13, 13, 1, -12 }, // 0x6E 'n' - { 1715, 13, 13, 12, 1, -12 }, // 0x6F 'o' - { 1737, 14, 18, 13, 0, -12 }, // 0x70 'p' - { 1769, 13, 18, 12, 1, -12 }, // 0x71 'q' - { 1799, 13, 13, 12, 1, -12 }, // 0x72 'r' - { 1821, 13, 13, 12, 1, -12 }, // 0x73 's' - { 1843, 11, 16, 12, 4, -15 }, // 0x74 't' - { 1865, 13, 13, 12, 1, -12 }, // 0x75 'u' - { 1887, 11, 13, 12, 3, -12 }, // 0x76 'v' - { 1905, 13, 13, 12, 2, -12 }, // 0x77 'w' - { 1927, 13, 13, 12, 1, -12 }, // 0x78 'x' - { 1949, 13, 18, 12, 2, -12 }, // 0x79 'y' - { 1979, 13, 13, 12, 1, -12 }, // 0x7A 'z' - { 2001, 11, 24, 12, 3, -18 }, // 0x7B '{' - { 2034, 7, 24, 12, 4, -18 }, // 0x7C '|' - { 2055, 11, 24, 13, 1, -18 }, // 0x7D '}' - { 2088, 9, 3, 12, 3, -9 } }; // 0x7E '~' + { 0, 1, 1, 12, 0, 0 }, // 0x20 ' ' + { 1, 6, 16, 12, 5, -15 }, // 0x21 '!' + { 13, 6, 5, 12, 6, -16 }, // 0x22 '"' + { 17, 11, 16, 12, 2, -15 }, // 0x23 '#' + { 39, 12, 16, 12, 2, -15 }, // 0x24 '$' + { 63, 10, 16, 11, 3, -15 }, // 0x25 '%' + { 83, 13, 16, 12, 1, -15 }, // 0x26 '&' + { 109, 3, 5, 12, 8, -16 }, // 0x27 ''' + { 111, 12, 24, 12, 2, -18 }, // 0x28 '(' + { 147, 12, 24, 12, 1, -18 }, // 0x29 ')' + { 183, 9, 9, 12, 4, -15 }, // 0x2A '*' + { 194, 9, 8, 12, 3, -12 }, // 0x2B '+' + { 203, 3, 5, 12, 4, 0 }, // 0x2C ',' + { 205, 11, 1, 12, 2, -7 }, // 0x2D '-' + { 207, 3, 1, 12, 5, 0 }, // 0x2E '.' + { 208, 14, 16, 12, 1, -15 }, // 0x2F '/' + { 236, 13, 16, 12, 1, -15 }, // 0x30 '0' + { 262, 11, 16, 12, 1, -15 }, // 0x31 '1' + { 284, 14, 16, 12, 0, -15 }, // 0x32 '2' + { 312, 14, 16, 13, 1, -15 }, // 0x33 '3' + { 340, 12, 16, 12, 1, -15 }, // 0x34 '4' + { 364, 14, 16, 12, 0, -15 }, // 0x35 '5' + { 392, 12, 16, 12, 1, -15 }, // 0x36 '6' + { 416, 13, 16, 12, 2, -15 }, // 0x37 '7' + { 442, 14, 16, 13, 1, -15 }, // 0x38 '8' + { 470, 13, 16, 12, 1, -15 }, // 0x39 '9' + { 496, 4, 9, 12, 5, -8 }, // 0x3A ':' + { 501, 5, 13, 12, 4, -8 }, // 0x3B ';' + { 510, 7, 12, 12, 4, -13 }, // 0x3C '<' + { 521, 8, 5, 12, 4, -10 }, // 0x3D '=' + { 526, 7, 12, 12, 4, -13 }, // 0x3E '>' + { 537, 11, 16, 12, 3, -15 }, // 0x3F '?' + { 559, 12, 16, 12, 2, -15 }, // 0x40 '@' + { 583, 11, 16, 13, 1, -15 }, // 0x41 'A' + { 605, 14, 16, 12, 1, -15 }, // 0x42 'B' + { 633, 13, 16, 12, 2, -15 }, // 0x43 'C' + { 659, 13, 16, 12, 1, -15 }, // 0x44 'D' + { 685, 13, 16, 12, 1, -15 }, // 0x45 'E' + { 711, 14, 16, 12, 1, -15 }, // 0x46 'F' + { 739, 13, 16, 12, 1, -15 }, // 0x47 'G' + { 765, 14, 16, 12, 1, -15 }, // 0x48 'H' + { 793, 13, 16, 12, 1, -15 }, // 0x49 'I' + { 819, 13, 16, 12, 1, -15 }, // 0x4A 'J' + { 845, 14, 16, 12, 1, -15 }, // 0x4B 'K' + { 873, 11, 16, 12, 1, -15 }, // 0x4C 'L' + { 895, 14, 16, 12, 1, -15 }, // 0x4D 'M' + { 923, 14, 16, 12, 1, -15 }, // 0x4E 'N' + { 951, 14, 16, 13, 1, -15 }, // 0x4F 'O' + { 979, 13, 16, 12, 1, -15 }, // 0x50 'P' + { 1005, 14, 18, 13, 1, -15 }, // 0x51 'Q' + { 1037, 14, 16, 11, 0, -15 }, // 0x52 'R' + { 1065, 13, 16, 12, 1, -15 }, // 0x53 'S' + { 1091, 10, 16, 12, 4, -15 }, // 0x54 'T' + { 1111, 14, 16, 12, 1, -15 }, // 0x55 'U' + { 1139, 11, 16, 12, 4, -15 }, // 0x56 'V' + { 1161, 13, 16, 12, 2, -15 }, // 0x57 'W' + { 1187, 14, 16, 12, 1, -15 }, // 0x58 'X' + { 1215, 11, 16, 12, 4, -15 }, // 0x59 'Y' + { 1237, 14, 16, 12, 1, -15 }, // 0x5A 'Z' + { 1265, 11, 24, 12, 2, -18 }, // 0x5B '[' + { 1298, 7, 16, 12, 4, -15 }, // 0x5C '\' + { 1312, 11, 24, 12, 2, -18 }, // 0x5D ']' + { 1345, 12, 7, 12, 2, -15 }, // 0x5E '^' + { 1356, 12, 1, 12, -1, 4 }, // 0x5F '_' + { 1358, 5, 3, 12, 7, -15 }, // 0x60 '`' + { 1360, 13, 13, 12, 1, -12 }, // 0x61 'a' + { 1382, 13, 17, 13, 1, -16 }, // 0x62 'b' + { 1410, 13, 13, 12, 1, -12 }, // 0x63 'c' + { 1432, 13, 17, 12, 1, -16 }, // 0x64 'd' + { 1460, 13, 13, 13, 1, -12 }, // 0x65 'e' + { 1482, 12, 17, 12, 3, -16 }, // 0x66 'f' + { 1508, 13, 18, 12, 1, -12 }, // 0x67 'g' + { 1538, 13, 17, 13, 1, -16 }, // 0x68 'h' + { 1566, 10, 17, 12, 1, -16 }, // 0x69 'i' + { 1588, 12, 22, 12, 1, -16 }, // 0x6A 'j' + { 1621, 13, 17, 12, 1, -16 }, // 0x6B 'k' + { 1649, 10, 17, 12, 1, -16 }, // 0x6C 'l' + { 1671, 13, 13, 12, 1, -12 }, // 0x6D 'm' + { 1693, 13, 13, 13, 1, -12 }, // 0x6E 'n' + { 1715, 13, 13, 12, 1, -12 }, // 0x6F 'o' + { 1737, 14, 18, 13, 0, -12 }, // 0x70 'p' + { 1769, 13, 18, 12, 1, -12 }, // 0x71 'q' + { 1799, 13, 13, 12, 1, -12 }, // 0x72 'r' + { 1821, 13, 13, 12, 1, -12 }, // 0x73 's' + { 1843, 11, 16, 12, 4, -15 }, // 0x74 't' + { 1865, 13, 13, 12, 1, -12 }, // 0x75 'u' + { 1887, 11, 13, 12, 3, -12 }, // 0x76 'v' + { 1905, 13, 13, 12, 2, -12 }, // 0x77 'w' + { 1927, 13, 13, 12, 1, -12 }, // 0x78 'x' + { 1949, 13, 18, 12, 2, -12 }, // 0x79 'y' + { 1979, 13, 13, 12, 1, -12 }, // 0x7A 'z' + { 2001, 11, 24, 12, 3, -18 }, // 0x7B '{' + { 2034, 7, 24, 12, 4, -18 }, // 0x7C '|' + { 2055, 11, 24, 13, 1, -18 }, // 0x7D '}' + { 2088, 9, 3, 12, 3, -9 } }; // 0x7E '~' const GFXfont unispace_italic8pt7b PROGMEM = { (uint8_t *)unispace_italic8pt7bBitmaps, (GFXglyph *)unispace_italic8pt7bGlyphs, - 0x20, 0x7E, 24 }; + 0x20, 0x7E, 24 }; // Approx. 2764 bytes +#endif // ifndef FONTS_UNISPACE_ITALIC8PT7B_H diff --git a/src/src/Static/Fonts/whitrabt12pt7b.h b/src/src/Static/Fonts/whitrabt12pt7b.h index 3515c6aaf7..d9ab4b5fc4 100644 --- a/src/src/Static/Fonts/whitrabt12pt7b.h +++ b/src/src/Static/Fonts/whitrabt12pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_WHITRABT12PT7B_H +#define FONTS_WHITRABT12PT7B_H const uint8_t whitrabt12pt7bBitmaps[] PROGMEM = { 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x0F, 0xFF, 0xF0, 0xC7, 0x8F, 0x1E, 0x30, 0x33, 0x06, 0x60, 0xCC, 0x19, 0x8F, 0xFF, 0xFF, 0xCC, 0xC1, 0x98, @@ -132,105 +134,106 @@ const uint8_t whitrabt12pt7bBitmaps[] PROGMEM = { 0x3C, 0x63, 0xC6, 0x30, 0x3E, 0x01, 0xC0 }; const GFXglyph whitrabt12pt7bGlyphs[] PROGMEM = { - { 0, 1, 1, 13, 0, 0 }, // 0x20 ' ' - { 1, 4, 15, 13, 3, -14 }, // 0x21 '!' - { 9, 7, 4, 13, 3, -14 }, // 0x22 '"' - { 13, 11, 15, 13, 1, -14 }, // 0x23 '#' - { 34, 12, 15, 14, 1, -14 }, // 0x24 '$' - { 57, 10, 15, 12, 1, -14 }, // 0x25 '%' - { 76, 11, 15, 13, 1, -14 }, // 0x26 '&' - { 97, 2, 4, 14, 6, -14 }, // 0x27 ''' - { 98, 5, 15, 14, 6, -14 }, // 0x28 '(' - { 108, 4, 15, 13, 3, -14 }, // 0x29 ')' - { 116, 11, 11, 13, 1, -14 }, // 0x2A '*' - { 132, 11, 11, 13, 1, -12 }, // 0x2B '+' - { 148, 4, 6, 13, 3, -3 }, // 0x2C ',' - { 151, 11, 2, 13, 1, -8 }, // 0x2D '-' - { 154, 4, 4, 13, 3, -3 }, // 0x2E '.' - { 156, 11, 15, 13, 1, -14 }, // 0x2F '/' - { 177, 11, 15, 13, 1, -14 }, // 0x30 '0' - { 198, 7, 15, 13, 3, -14 }, // 0x31 '1' - { 212, 11, 15, 13, 1, -14 }, // 0x32 '2' - { 233, 11, 15, 13, 1, -14 }, // 0x33 '3' - { 254, 11, 15, 13, 1, -14 }, // 0x34 '4' - { 275, 11, 15, 13, 1, -14 }, // 0x35 '5' - { 296, 11, 15, 13, 1, -14 }, // 0x36 '6' - { 317, 11, 15, 13, 1, -14 }, // 0x37 '7' - { 338, 11, 15, 13, 1, -14 }, // 0x38 '8' - { 359, 11, 15, 13, 1, -14 }, // 0x39 '9' - { 380, 4, 13, 13, 3, -12 }, // 0x3A ':' - { 387, 4, 15, 13, 3, -12 }, // 0x3B ';' - { 395, 9, 15, 13, 1, -14 }, // 0x3C '<' - { 412, 11, 6, 13, 1, -10 }, // 0x3D '=' - { 421, 9, 15, 13, 3, -14 }, // 0x3E '>' - { 438, 12, 15, 14, 1, -14 }, // 0x3F '?' - { 461, 12, 15, 14, 1, -14 }, // 0x40 '@' - { 484, 11, 15, 13, 1, -14 }, // 0x41 'A' - { 505, 11, 15, 13, 1, -14 }, // 0x42 'B' - { 526, 11, 15, 13, 1, -14 }, // 0x43 'C' - { 547, 11, 15, 13, 1, -14 }, // 0x44 'D' - { 568, 11, 15, 13, 1, -14 }, // 0x45 'E' - { 589, 11, 15, 13, 1, -14 }, // 0x46 'F' - { 610, 11, 15, 13, 1, -14 }, // 0x47 'G' - { 631, 11, 15, 13, 1, -14 }, // 0x48 'H' - { 652, 7, 15, 13, 3, -14 }, // 0x49 'I' - { 666, 11, 15, 13, 1, -14 }, // 0x4A 'J' - { 687, 11, 15, 13, 1, -14 }, // 0x4B 'K' - { 708, 11, 15, 13, 1, -14 }, // 0x4C 'L' - { 729, 11, 15, 13, 1, -14 }, // 0x4D 'M' - { 750, 11, 15, 13, 1, -14 }, // 0x4E 'N' - { 771, 11, 15, 13, 1, -14 }, // 0x4F 'O' - { 792, 11, 15, 13, 1, -14 }, // 0x50 'P' - { 813, 11, 15, 13, 1, -14 }, // 0x51 'Q' - { 834, 11, 15, 13, 1, -14 }, // 0x52 'R' - { 855, 11, 15, 13, 1, -14 }, // 0x53 'S' - { 876, 11, 15, 13, 1, -14 }, // 0x54 'T' - { 897, 11, 15, 13, 1, -14 }, // 0x55 'U' - { 918, 11, 15, 13, 1, -14 }, // 0x56 'V' - { 939, 12, 15, 14, 1, -14 }, // 0x57 'W' - { 962, 11, 15, 13, 1, -14 }, // 0x58 'X' - { 983, 12, 15, 14, 1, -14 }, // 0x59 'Y' - { 1006, 11, 15, 13, 1, -14 }, // 0x5A 'Z' - { 1027, 5, 15, 14, 6, -14 }, // 0x5B '[' - { 1037, 11, 15, 13, 1, -14 }, // 0x5C '\' - { 1058, 4, 15, 13, 3, -14 }, // 0x5D ']' - { 1066, 11, 7, 13, 1, -15 }, // 0x5E '^' - { 1076, 11, 2, 13, 1, 1 }, // 0x5F '_' - { 1079, 5, 5, 14, 6, -15 }, // 0x60 '`' - { 1083, 11, 11, 13, 1, -10 }, // 0x61 'a' - { 1099, 11, 15, 13, 1, -14 }, // 0x62 'b' - { 1120, 11, 11, 13, 1, -10 }, // 0x63 'c' - { 1136, 11, 15, 13, 1, -14 }, // 0x64 'd' - { 1157, 11, 11, 13, 1, -10 }, // 0x65 'e' - { 1173, 9, 15, 13, 3, -14 }, // 0x66 'f' - { 1190, 11, 13, 13, 1, -10 }, // 0x67 'g' - { 1208, 11, 15, 13, 1, -14 }, // 0x68 'h' - { 1229, 7, 15, 13, 3, -14 }, // 0x69 'i' - { 1243, 9, 17, 13, 1, -14 }, // 0x6A 'j' - { 1263, 9, 15, 13, 3, -14 }, // 0x6B 'k' - { 1280, 7, 15, 13, 3, -14 }, // 0x6C 'l' - { 1294, 12, 11, 14, 1, -10 }, // 0x6D 'm' - { 1311, 11, 11, 13, 1, -10 }, // 0x6E 'n' - { 1327, 11, 11, 13, 1, -10 }, // 0x6F 'o' - { 1343, 11, 13, 13, 1, -10 }, // 0x70 'p' - { 1361, 11, 13, 13, 1, -10 }, // 0x71 'q' - { 1379, 9, 11, 13, 3, -10 }, // 0x72 'r' - { 1392, 11, 11, 13, 1, -10 }, // 0x73 's' - { 1408, 9, 15, 13, 3, -14 }, // 0x74 't' - { 1425, 11, 11, 13, 1, -10 }, // 0x75 'u' - { 1441, 11, 11, 13, 1, -10 }, // 0x76 'v' - { 1457, 12, 11, 14, 1, -10 }, // 0x77 'w' - { 1474, 11, 11, 13, 1, -10 }, // 0x78 'x' - { 1490, 11, 13, 13, 1, -10 }, // 0x79 'y' - { 1508, 11, 11, 13, 1, -10 }, // 0x7A 'z' - { 1524, 7, 15, 13, 3, -14 }, // 0x7B '{' - { 1538, 2, 15, 14, 6, -14 }, // 0x7C '|' - { 1542, 7, 15, 13, 3, -14 }, // 0x7D '}' - { 1556, 12, 7, 14, 1, -15 } }; // 0x7E '~' + { 0, 1, 1, 13, 0, 0 }, // 0x20 ' ' + { 1, 4, 15, 13, 3, -14 }, // 0x21 '!' + { 9, 7, 4, 13, 3, -14 }, // 0x22 '"' + { 13, 11, 15, 13, 1, -14 }, // 0x23 '#' + { 34, 12, 15, 14, 1, -14 }, // 0x24 '$' + { 57, 10, 15, 12, 1, -14 }, // 0x25 '%' + { 76, 11, 15, 13, 1, -14 }, // 0x26 '&' + { 97, 2, 4, 14, 6, -14 }, // 0x27 ''' + { 98, 5, 15, 14, 6, -14 }, // 0x28 '(' + { 108, 4, 15, 13, 3, -14 }, // 0x29 ')' + { 116, 11, 11, 13, 1, -14 }, // 0x2A '*' + { 132, 11, 11, 13, 1, -12 }, // 0x2B '+' + { 148, 4, 6, 13, 3, -3 }, // 0x2C ',' + { 151, 11, 2, 13, 1, -8 }, // 0x2D '-' + { 154, 4, 4, 13, 3, -3 }, // 0x2E '.' + { 156, 11, 15, 13, 1, -14 }, // 0x2F '/' + { 177, 11, 15, 13, 1, -14 }, // 0x30 '0' + { 198, 7, 15, 13, 3, -14 }, // 0x31 '1' + { 212, 11, 15, 13, 1, -14 }, // 0x32 '2' + { 233, 11, 15, 13, 1, -14 }, // 0x33 '3' + { 254, 11, 15, 13, 1, -14 }, // 0x34 '4' + { 275, 11, 15, 13, 1, -14 }, // 0x35 '5' + { 296, 11, 15, 13, 1, -14 }, // 0x36 '6' + { 317, 11, 15, 13, 1, -14 }, // 0x37 '7' + { 338, 11, 15, 13, 1, -14 }, // 0x38 '8' + { 359, 11, 15, 13, 1, -14 }, // 0x39 '9' + { 380, 4, 13, 13, 3, -12 }, // 0x3A ':' + { 387, 4, 15, 13, 3, -12 }, // 0x3B ';' + { 395, 9, 15, 13, 1, -14 }, // 0x3C '<' + { 412, 11, 6, 13, 1, -10 }, // 0x3D '=' + { 421, 9, 15, 13, 3, -14 }, // 0x3E '>' + { 438, 12, 15, 14, 1, -14 }, // 0x3F '?' + { 461, 12, 15, 14, 1, -14 }, // 0x40 '@' + { 484, 11, 15, 13, 1, -14 }, // 0x41 'A' + { 505, 11, 15, 13, 1, -14 }, // 0x42 'B' + { 526, 11, 15, 13, 1, -14 }, // 0x43 'C' + { 547, 11, 15, 13, 1, -14 }, // 0x44 'D' + { 568, 11, 15, 13, 1, -14 }, // 0x45 'E' + { 589, 11, 15, 13, 1, -14 }, // 0x46 'F' + { 610, 11, 15, 13, 1, -14 }, // 0x47 'G' + { 631, 11, 15, 13, 1, -14 }, // 0x48 'H' + { 652, 7, 15, 13, 3, -14 }, // 0x49 'I' + { 666, 11, 15, 13, 1, -14 }, // 0x4A 'J' + { 687, 11, 15, 13, 1, -14 }, // 0x4B 'K' + { 708, 11, 15, 13, 1, -14 }, // 0x4C 'L' + { 729, 11, 15, 13, 1, -14 }, // 0x4D 'M' + { 750, 11, 15, 13, 1, -14 }, // 0x4E 'N' + { 771, 11, 15, 13, 1, -14 }, // 0x4F 'O' + { 792, 11, 15, 13, 1, -14 }, // 0x50 'P' + { 813, 11, 15, 13, 1, -14 }, // 0x51 'Q' + { 834, 11, 15, 13, 1, -14 }, // 0x52 'R' + { 855, 11, 15, 13, 1, -14 }, // 0x53 'S' + { 876, 11, 15, 13, 1, -14 }, // 0x54 'T' + { 897, 11, 15, 13, 1, -14 }, // 0x55 'U' + { 918, 11, 15, 13, 1, -14 }, // 0x56 'V' + { 939, 12, 15, 14, 1, -14 }, // 0x57 'W' + { 962, 11, 15, 13, 1, -14 }, // 0x58 'X' + { 983, 12, 15, 14, 1, -14 }, // 0x59 'Y' + { 1006, 11, 15, 13, 1, -14 }, // 0x5A 'Z' + { 1027, 5, 15, 14, 6, -14 }, // 0x5B '[' + { 1037, 11, 15, 13, 1, -14 }, // 0x5C '\' + { 1058, 4, 15, 13, 3, -14 }, // 0x5D ']' + { 1066, 11, 7, 13, 1, -15 }, // 0x5E '^' + { 1076, 11, 2, 13, 1, 1 }, // 0x5F '_' + { 1079, 5, 5, 14, 6, -15 }, // 0x60 '`' + { 1083, 11, 11, 13, 1, -10 }, // 0x61 'a' + { 1099, 11, 15, 13, 1, -14 }, // 0x62 'b' + { 1120, 11, 11, 13, 1, -10 }, // 0x63 'c' + { 1136, 11, 15, 13, 1, -14 }, // 0x64 'd' + { 1157, 11, 11, 13, 1, -10 }, // 0x65 'e' + { 1173, 9, 15, 13, 3, -14 }, // 0x66 'f' + { 1190, 11, 13, 13, 1, -10 }, // 0x67 'g' + { 1208, 11, 15, 13, 1, -14 }, // 0x68 'h' + { 1229, 7, 15, 13, 3, -14 }, // 0x69 'i' + { 1243, 9, 17, 13, 1, -14 }, // 0x6A 'j' + { 1263, 9, 15, 13, 3, -14 }, // 0x6B 'k' + { 1280, 7, 15, 13, 3, -14 }, // 0x6C 'l' + { 1294, 12, 11, 14, 1, -10 }, // 0x6D 'm' + { 1311, 11, 11, 13, 1, -10 }, // 0x6E 'n' + { 1327, 11, 11, 13, 1, -10 }, // 0x6F 'o' + { 1343, 11, 13, 13, 1, -10 }, // 0x70 'p' + { 1361, 11, 13, 13, 1, -10 }, // 0x71 'q' + { 1379, 9, 11, 13, 3, -10 }, // 0x72 'r' + { 1392, 11, 11, 13, 1, -10 }, // 0x73 's' + { 1408, 9, 15, 13, 3, -14 }, // 0x74 't' + { 1425, 11, 11, 13, 1, -10 }, // 0x75 'u' + { 1441, 11, 11, 13, 1, -10 }, // 0x76 'v' + { 1457, 12, 11, 14, 1, -10 }, // 0x77 'w' + { 1474, 11, 11, 13, 1, -10 }, // 0x78 'x' + { 1490, 11, 13, 13, 1, -10 }, // 0x79 'y' + { 1508, 11, 11, 13, 1, -10 }, // 0x7A 'z' + { 1524, 7, 15, 13, 3, -14 }, // 0x7B '{' + { 1538, 2, 15, 14, 6, -14 }, // 0x7C '|' + { 1542, 7, 15, 13, 3, -14 }, // 0x7D '}' + { 1556, 12, 7, 14, 1, -15 } }; // 0x7E '~' const GFXfont whitrabt12pt7b PROGMEM = { (uint8_t *)whitrabt12pt7bBitmaps, (GFXglyph *)whitrabt12pt7bGlyphs, - 0x20, 0x7E, 18 }; + 0x20, 0x7E, 18 }; // Approx. 2239 bytes +#endif // ifndef FONTS_WHITRABT12PT7B_H diff --git a/src/src/Static/Fonts/whitrabt16pt7b.h b/src/src/Static/Fonts/whitrabt16pt7b.h index ee11bf99a9..ced93d8613 100644 --- a/src/src/Static/Fonts/whitrabt16pt7b.h +++ b/src/src/Static/Fonts/whitrabt16pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_WHITRABT16PT7B_H +#define FONTS_WHITRABT16PT7B_H const uint8_t whitrabt16pt7bBitmaps[] PROGMEM = { 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFC, 0xE3, 0xF1, 0xF8, 0xFC, 0x7E, 0x3F, 0x1C, @@ -244,105 +246,106 @@ const uint8_t whitrabt16pt7bBitmaps[] PROGMEM = { 0x7E, 0x38, 0xFC, 0x71, 0xC0, 0xFF, 0x80, 0xFE, 0x00, 0x70 }; const GFXglyph whitrabt16pt7bGlyphs[] PROGMEM = { - { 0, 1, 1, 18, 0, 0 }, // 0x20 ' ' - { 1, 6, 21, 17, 4, -20 }, // 0x21 '!' - { 17, 9, 6, 17, 4, -20 }, // 0x22 '"' - { 24, 15, 21, 18, 1, -20 }, // 0x23 '#' - { 64, 15, 21, 18, 1, -20 }, // 0x24 '$' - { 104, 15, 21, 18, 1, -20 }, // 0x25 '%' - { 144, 15, 21, 18, 1, -20 }, // 0x26 '&' - { 184, 3, 6, 17, 7, -20 }, // 0x27 ''' - { 187, 6, 21, 17, 7, -20 }, // 0x28 '(' - { 203, 6, 21, 17, 4, -20 }, // 0x29 ')' - { 219, 15, 15, 18, 1, -20 }, // 0x2A '*' - { 248, 15, 15, 18, 1, -17 }, // 0x2B '+' - { 277, 6, 9, 17, 4, -5 }, // 0x2C ',' - { 284, 15, 3, 18, 1, -11 }, // 0x2D '-' - { 290, 6, 6, 17, 4, -5 }, // 0x2E '.' - { 295, 15, 21, 18, 1, -20 }, // 0x2F '/' - { 335, 15, 21, 18, 1, -20 }, // 0x30 '0' - { 375, 9, 21, 18, 4, -20 }, // 0x31 '1' - { 399, 15, 21, 18, 1, -20 }, // 0x32 '2' - { 439, 15, 21, 18, 1, -20 }, // 0x33 '3' - { 479, 15, 21, 18, 1, -20 }, // 0x34 '4' - { 519, 15, 21, 18, 1, -20 }, // 0x35 '5' - { 559, 15, 21, 18, 1, -20 }, // 0x36 '6' - { 599, 15, 21, 18, 1, -20 }, // 0x37 '7' - { 639, 15, 21, 18, 1, -20 }, // 0x38 '8' - { 679, 15, 21, 18, 1, -20 }, // 0x39 '9' - { 719, 6, 18, 17, 4, -17 }, // 0x3A ':' - { 733, 6, 21, 17, 4, -17 }, // 0x3B ';' - { 749, 12, 21, 17, 1, -20 }, // 0x3C '<' - { 781, 15, 9, 18, 1, -14 }, // 0x3D '=' - { 798, 12, 21, 18, 4, -20 }, // 0x3E '>' - { 830, 15, 21, 18, 1, -20 }, // 0x3F '?' - { 870, 15, 21, 18, 1, -20 }, // 0x40 '@' - { 910, 15, 21, 18, 1, -20 }, // 0x41 'A' - { 950, 15, 21, 18, 1, -20 }, // 0x42 'B' - { 990, 15, 21, 18, 1, -20 }, // 0x43 'C' - { 1030, 15, 21, 18, 1, -20 }, // 0x44 'D' - { 1070, 15, 21, 18, 1, -20 }, // 0x45 'E' - { 1110, 15, 21, 18, 1, -20 }, // 0x46 'F' - { 1150, 15, 21, 18, 1, -20 }, // 0x47 'G' - { 1190, 15, 21, 18, 1, -20 }, // 0x48 'H' - { 1230, 9, 21, 17, 4, -20 }, // 0x49 'I' - { 1254, 15, 21, 18, 1, -20 }, // 0x4A 'J' - { 1294, 15, 21, 18, 1, -20 }, // 0x4B 'K' - { 1334, 15, 21, 18, 1, -20 }, // 0x4C 'L' - { 1374, 15, 21, 18, 1, -20 }, // 0x4D 'M' - { 1414, 15, 21, 18, 1, -20 }, // 0x4E 'N' - { 1454, 15, 21, 18, 1, -20 }, // 0x4F 'O' - { 1494, 15, 21, 18, 1, -20 }, // 0x50 'P' - { 1534, 15, 21, 18, 1, -20 }, // 0x51 'Q' - { 1574, 15, 21, 18, 1, -20 }, // 0x52 'R' - { 1614, 15, 21, 18, 1, -20 }, // 0x53 'S' - { 1654, 15, 21, 18, 1, -20 }, // 0x54 'T' - { 1694, 15, 21, 18, 1, -20 }, // 0x55 'U' - { 1734, 15, 21, 18, 1, -20 }, // 0x56 'V' - { 1774, 15, 21, 18, 1, -20 }, // 0x57 'W' - { 1814, 15, 21, 18, 1, -20 }, // 0x58 'X' - { 1854, 15, 21, 18, 1, -20 }, // 0x59 'Y' - { 1894, 15, 21, 18, 1, -20 }, // 0x5A 'Z' - { 1934, 6, 21, 17, 7, -20 }, // 0x5B '[' - { 1950, 15, 21, 18, 1, -20 }, // 0x5C '\' - { 1990, 6, 21, 17, 4, -20 }, // 0x5D ']' - { 2006, 15, 9, 18, 1, -20 }, // 0x5E '^' - { 2023, 15, 3, 18, 1, 1 }, // 0x5F '_' - { 2029, 6, 6, 17, 7, -20 }, // 0x60 '`' - { 2034, 15, 15, 18, 1, -14 }, // 0x61 'a' - { 2063, 15, 21, 18, 1, -20 }, // 0x62 'b' - { 2103, 15, 15, 18, 1, -14 }, // 0x63 'c' - { 2132, 15, 21, 18, 1, -20 }, // 0x64 'd' - { 2172, 15, 15, 18, 1, -14 }, // 0x65 'e' - { 2201, 12, 21, 18, 4, -20 }, // 0x66 'f' - { 2233, 15, 18, 18, 1, -14 }, // 0x67 'g' - { 2267, 15, 21, 18, 1, -20 }, // 0x68 'h' - { 2307, 9, 21, 17, 4, -20 }, // 0x69 'i' - { 2331, 12, 24, 17, 1, -20 }, // 0x6A 'j' - { 2367, 12, 21, 18, 4, -20 }, // 0x6B 'k' - { 2399, 9, 21, 17, 4, -20 }, // 0x6C 'l' - { 2423, 15, 15, 18, 1, -14 }, // 0x6D 'm' - { 2452, 15, 15, 18, 1, -14 }, // 0x6E 'n' - { 2481, 15, 15, 18, 1, -14 }, // 0x6F 'o' - { 2510, 15, 18, 18, 1, -14 }, // 0x70 'p' - { 2544, 15, 18, 18, 1, -14 }, // 0x71 'q' - { 2578, 12, 15, 18, 4, -14 }, // 0x72 'r' - { 2601, 15, 15, 18, 1, -14 }, // 0x73 's' - { 2630, 12, 21, 18, 4, -20 }, // 0x74 't' - { 2662, 15, 15, 18, 1, -14 }, // 0x75 'u' - { 2691, 15, 15, 18, 1, -14 }, // 0x76 'v' - { 2720, 15, 15, 18, 1, -14 }, // 0x77 'w' - { 2749, 15, 15, 18, 1, -14 }, // 0x78 'x' - { 2778, 15, 18, 18, 1, -14 }, // 0x79 'y' - { 2812, 15, 15, 18, 1, -14 }, // 0x7A 'z' - { 2841, 9, 21, 17, 4, -20 }, // 0x7B '{' - { 2865, 3, 21, 17, 7, -20 }, // 0x7C '|' - { 2873, 9, 21, 17, 4, -20 }, // 0x7D '}' - { 2897, 15, 9, 18, 1, -20 } }; // 0x7E '~' + { 0, 1, 1, 18, 0, 0 }, // 0x20 ' ' + { 1, 6, 21, 17, 4, -20 }, // 0x21 '!' + { 17, 9, 6, 17, 4, -20 }, // 0x22 '"' + { 24, 15, 21, 18, 1, -20 }, // 0x23 '#' + { 64, 15, 21, 18, 1, -20 }, // 0x24 '$' + { 104, 15, 21, 18, 1, -20 }, // 0x25 '%' + { 144, 15, 21, 18, 1, -20 }, // 0x26 '&' + { 184, 3, 6, 17, 7, -20 }, // 0x27 ''' + { 187, 6, 21, 17, 7, -20 }, // 0x28 '(' + { 203, 6, 21, 17, 4, -20 }, // 0x29 ')' + { 219, 15, 15, 18, 1, -20 }, // 0x2A '*' + { 248, 15, 15, 18, 1, -17 }, // 0x2B '+' + { 277, 6, 9, 17, 4, -5 }, // 0x2C ',' + { 284, 15, 3, 18, 1, -11 }, // 0x2D '-' + { 290, 6, 6, 17, 4, -5 }, // 0x2E '.' + { 295, 15, 21, 18, 1, -20 }, // 0x2F '/' + { 335, 15, 21, 18, 1, -20 }, // 0x30 '0' + { 375, 9, 21, 18, 4, -20 }, // 0x31 '1' + { 399, 15, 21, 18, 1, -20 }, // 0x32 '2' + { 439, 15, 21, 18, 1, -20 }, // 0x33 '3' + { 479, 15, 21, 18, 1, -20 }, // 0x34 '4' + { 519, 15, 21, 18, 1, -20 }, // 0x35 '5' + { 559, 15, 21, 18, 1, -20 }, // 0x36 '6' + { 599, 15, 21, 18, 1, -20 }, // 0x37 '7' + { 639, 15, 21, 18, 1, -20 }, // 0x38 '8' + { 679, 15, 21, 18, 1, -20 }, // 0x39 '9' + { 719, 6, 18, 17, 4, -17 }, // 0x3A ':' + { 733, 6, 21, 17, 4, -17 }, // 0x3B ';' + { 749, 12, 21, 17, 1, -20 }, // 0x3C '<' + { 781, 15, 9, 18, 1, -14 }, // 0x3D '=' + { 798, 12, 21, 18, 4, -20 }, // 0x3E '>' + { 830, 15, 21, 18, 1, -20 }, // 0x3F '?' + { 870, 15, 21, 18, 1, -20 }, // 0x40 '@' + { 910, 15, 21, 18, 1, -20 }, // 0x41 'A' + { 950, 15, 21, 18, 1, -20 }, // 0x42 'B' + { 990, 15, 21, 18, 1, -20 }, // 0x43 'C' + { 1030, 15, 21, 18, 1, -20 }, // 0x44 'D' + { 1070, 15, 21, 18, 1, -20 }, // 0x45 'E' + { 1110, 15, 21, 18, 1, -20 }, // 0x46 'F' + { 1150, 15, 21, 18, 1, -20 }, // 0x47 'G' + { 1190, 15, 21, 18, 1, -20 }, // 0x48 'H' + { 1230, 9, 21, 17, 4, -20 }, // 0x49 'I' + { 1254, 15, 21, 18, 1, -20 }, // 0x4A 'J' + { 1294, 15, 21, 18, 1, -20 }, // 0x4B 'K' + { 1334, 15, 21, 18, 1, -20 }, // 0x4C 'L' + { 1374, 15, 21, 18, 1, -20 }, // 0x4D 'M' + { 1414, 15, 21, 18, 1, -20 }, // 0x4E 'N' + { 1454, 15, 21, 18, 1, -20 }, // 0x4F 'O' + { 1494, 15, 21, 18, 1, -20 }, // 0x50 'P' + { 1534, 15, 21, 18, 1, -20 }, // 0x51 'Q' + { 1574, 15, 21, 18, 1, -20 }, // 0x52 'R' + { 1614, 15, 21, 18, 1, -20 }, // 0x53 'S' + { 1654, 15, 21, 18, 1, -20 }, // 0x54 'T' + { 1694, 15, 21, 18, 1, -20 }, // 0x55 'U' + { 1734, 15, 21, 18, 1, -20 }, // 0x56 'V' + { 1774, 15, 21, 18, 1, -20 }, // 0x57 'W' + { 1814, 15, 21, 18, 1, -20 }, // 0x58 'X' + { 1854, 15, 21, 18, 1, -20 }, // 0x59 'Y' + { 1894, 15, 21, 18, 1, -20 }, // 0x5A 'Z' + { 1934, 6, 21, 17, 7, -20 }, // 0x5B '[' + { 1950, 15, 21, 18, 1, -20 }, // 0x5C '\' + { 1990, 6, 21, 17, 4, -20 }, // 0x5D ']' + { 2006, 15, 9, 18, 1, -20 }, // 0x5E '^' + { 2023, 15, 3, 18, 1, 1 }, // 0x5F '_' + { 2029, 6, 6, 17, 7, -20 }, // 0x60 '`' + { 2034, 15, 15, 18, 1, -14 }, // 0x61 'a' + { 2063, 15, 21, 18, 1, -20 }, // 0x62 'b' + { 2103, 15, 15, 18, 1, -14 }, // 0x63 'c' + { 2132, 15, 21, 18, 1, -20 }, // 0x64 'd' + { 2172, 15, 15, 18, 1, -14 }, // 0x65 'e' + { 2201, 12, 21, 18, 4, -20 }, // 0x66 'f' + { 2233, 15, 18, 18, 1, -14 }, // 0x67 'g' + { 2267, 15, 21, 18, 1, -20 }, // 0x68 'h' + { 2307, 9, 21, 17, 4, -20 }, // 0x69 'i' + { 2331, 12, 24, 17, 1, -20 }, // 0x6A 'j' + { 2367, 12, 21, 18, 4, -20 }, // 0x6B 'k' + { 2399, 9, 21, 17, 4, -20 }, // 0x6C 'l' + { 2423, 15, 15, 18, 1, -14 }, // 0x6D 'm' + { 2452, 15, 15, 18, 1, -14 }, // 0x6E 'n' + { 2481, 15, 15, 18, 1, -14 }, // 0x6F 'o' + { 2510, 15, 18, 18, 1, -14 }, // 0x70 'p' + { 2544, 15, 18, 18, 1, -14 }, // 0x71 'q' + { 2578, 12, 15, 18, 4, -14 }, // 0x72 'r' + { 2601, 15, 15, 18, 1, -14 }, // 0x73 's' + { 2630, 12, 21, 18, 4, -20 }, // 0x74 't' + { 2662, 15, 15, 18, 1, -14 }, // 0x75 'u' + { 2691, 15, 15, 18, 1, -14 }, // 0x76 'v' + { 2720, 15, 15, 18, 1, -14 }, // 0x77 'w' + { 2749, 15, 15, 18, 1, -14 }, // 0x78 'x' + { 2778, 15, 18, 18, 1, -14 }, // 0x79 'y' + { 2812, 15, 15, 18, 1, -14 }, // 0x7A 'z' + { 2841, 9, 21, 17, 4, -20 }, // 0x7B '{' + { 2865, 3, 21, 17, 7, -20 }, // 0x7C '|' + { 2873, 9, 21, 17, 4, -20 }, // 0x7D '}' + { 2897, 15, 9, 18, 1, -20 } }; // 0x7E '~' const GFXfont whitrabt16pt7b PROGMEM = { (uint8_t *)whitrabt16pt7bBitmaps, (GFXglyph *)whitrabt16pt7bGlyphs, - 0x20, 0x7E, 24 }; + 0x20, 0x7E, 24 }; // Approx. 3586 bytes +#endif // ifndef FONTS_WHITRABT16PT7B_H diff --git a/src/src/Static/Fonts/whitrabt18pt7b.h b/src/src/Static/Fonts/whitrabt18pt7b.h index e61363dd90..a76d52dc0c 100644 --- a/src/src/Static/Fonts/whitrabt18pt7b.h +++ b/src/src/Static/Fonts/whitrabt18pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_WHITRABT18PT7B_H +#define FONTS_WHITRABT18PT7B_H const uint8_t whitrabt18pt7bBitmaps[] PROGMEM = { 0x00, 0x7D, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xC0, 0x00, 0x00, 0x00, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xBE, 0xE1, 0xF8, @@ -311,105 +313,106 @@ const uint8_t whitrabt18pt7bBitmaps[] PROGMEM = { 0x0E, 0x1F, 0x07, 0x9E, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x1E, 0x00 }; const GFXglyph whitrabt18pt7bGlyphs[] PROGMEM = { - { 0, 1, 1, 20, 0, 0 }, // 0x20 ' ' - { 1, 7, 24, 20, 5, -23 }, // 0x21 '!' - { 22, 10, 7, 20, 5, -23 }, // 0x22 '"' - { 31, 17, 24, 21, 2, -23 }, // 0x23 '#' - { 82, 17, 24, 21, 2, -23 }, // 0x24 '$' - { 133, 17, 24, 21, 2, -23 }, // 0x25 '%' - { 184, 17, 24, 21, 2, -23 }, // 0x26 '&' - { 235, 3, 7, 19, 8, -23 }, // 0x27 ''' - { 238, 7, 24, 20, 8, -23 }, // 0x28 '(' - { 259, 7, 24, 20, 5, -23 }, // 0x29 ')' - { 280, 17, 17, 21, 2, -23 }, // 0x2A '*' - { 317, 17, 17, 21, 2, -19 }, // 0x2B '+' - { 354, 7, 10, 20, 5, -6 }, // 0x2C ',' - { 363, 17, 3, 21, 2, -12 }, // 0x2D '-' - { 370, 7, 7, 20, 5, -6 }, // 0x2E '.' - { 377, 17, 24, 21, 2, -23 }, // 0x2F '/' - { 428, 17, 24, 20, 2, -23 }, // 0x30 '0' - { 479, 10, 24, 20, 5, -23 }, // 0x31 '1' - { 509, 17, 24, 20, 2, -23 }, // 0x32 '2' - { 560, 17, 24, 20, 2, -23 }, // 0x33 '3' - { 611, 17, 24, 20, 2, -23 }, // 0x34 '4' - { 662, 17, 24, 20, 2, -23 }, // 0x35 '5' - { 713, 17, 24, 20, 2, -23 }, // 0x36 '6' - { 764, 17, 24, 20, 1, -23 }, // 0x37 '7' - { 815, 17, 24, 20, 2, -23 }, // 0x38 '8' - { 866, 17, 24, 20, 2, -23 }, // 0x39 '9' - { 917, 7, 20, 20, 5, -19 }, // 0x3A ':' - { 935, 7, 24, 20, 5, -20 }, // 0x3B ';' - { 956, 14, 24, 21, 2, -23 }, // 0x3C '<' - { 998, 17, 10, 21, 2, -16 }, // 0x3D '=' - { 1020, 14, 24, 20, 5, -23 }, // 0x3E '>' - { 1062, 17, 24, 21, 2, -23 }, // 0x3F '?' - { 1113, 17, 24, 21, 2, -23 }, // 0x40 '@' - { 1164, 17, 24, 21, 2, -23 }, // 0x41 'A' - { 1215, 17, 24, 21, 2, -23 }, // 0x42 'B' - { 1266, 17, 24, 21, 2, -23 }, // 0x43 'C' - { 1317, 17, 24, 21, 2, -23 }, // 0x44 'D' - { 1368, 17, 24, 21, 2, -23 }, // 0x45 'E' - { 1419, 17, 24, 21, 2, -23 }, // 0x46 'F' - { 1470, 17, 24, 21, 2, -23 }, // 0x47 'G' - { 1521, 17, 24, 21, 2, -23 }, // 0x48 'H' - { 1572, 10, 24, 20, 5, -23 }, // 0x49 'I' - { 1602, 17, 24, 21, 2, -23 }, // 0x4A 'J' - { 1653, 17, 24, 21, 2, -23 }, // 0x4B 'K' - { 1704, 17, 24, 21, 2, -23 }, // 0x4C 'L' - { 1755, 17, 24, 21, 2, -23 }, // 0x4D 'M' - { 1806, 17, 24, 21, 2, -23 }, // 0x4E 'N' - { 1857, 17, 24, 21, 2, -23 }, // 0x4F 'O' - { 1908, 17, 24, 21, 2, -23 }, // 0x50 'P' - { 1959, 17, 24, 21, 2, -23 }, // 0x51 'Q' - { 2010, 17, 24, 21, 2, -23 }, // 0x52 'R' - { 2061, 17, 24, 21, 2, -23 }, // 0x53 'S' - { 2112, 17, 24, 21, 2, -23 }, // 0x54 'T' - { 2163, 17, 24, 21, 2, -23 }, // 0x55 'U' - { 2214, 17, 24, 21, 2, -23 }, // 0x56 'V' - { 2265, 17, 24, 21, 2, -23 }, // 0x57 'W' - { 2316, 17, 24, 21, 2, -23 }, // 0x58 'X' - { 2367, 17, 24, 21, 2, -23 }, // 0x59 'Y' - { 2418, 17, 24, 21, 2, -23 }, // 0x5A 'Z' - { 2469, 7, 24, 20, 8, -23 }, // 0x5B '[' - { 2490, 17, 24, 21, 2, -23 }, // 0x5C '\' - { 2541, 7, 24, 20, 5, -23 }, // 0x5D ']' - { 2562, 17, 10, 21, 2, -23 }, // 0x5E '^' - { 2584, 17, 3, 21, 2, 1 }, // 0x5F '_' - { 2591, 7, 7, 20, 8, -23 }, // 0x60 '`' - { 2598, 17, 17, 21, 2, -16 }, // 0x61 'a' - { 2635, 17, 24, 21, 2, -23 }, // 0x62 'b' - { 2686, 17, 17, 21, 2, -16 }, // 0x63 'c' - { 2723, 17, 24, 21, 2, -23 }, // 0x64 'd' - { 2774, 17, 17, 21, 2, -16 }, // 0x65 'e' - { 2811, 14, 24, 20, 5, -23 }, // 0x66 'f' - { 2853, 17, 20, 21, 2, -16 }, // 0x67 'g' - { 2896, 17, 24, 21, 2, -23 }, // 0x68 'h' - { 2947, 10, 24, 20, 5, -23 }, // 0x69 'i' - { 2977, 13, 27, 20, 2, -23 }, // 0x6A 'j' - { 3021, 14, 24, 20, 5, -23 }, // 0x6B 'k' - { 3063, 10, 24, 20, 5, -23 }, // 0x6C 'l' - { 3093, 17, 17, 21, 2, -16 }, // 0x6D 'm' - { 3130, 17, 17, 21, 2, -16 }, // 0x6E 'n' - { 3167, 17, 17, 21, 2, -16 }, // 0x6F 'o' - { 3204, 17, 20, 21, 2, -16 }, // 0x70 'p' - { 3247, 17, 20, 21, 2, -16 }, // 0x71 'q' - { 3290, 13, 17, 20, 5, -16 }, // 0x72 'r' - { 3318, 17, 17, 21, 2, -16 }, // 0x73 's' - { 3355, 13, 24, 20, 5, -23 }, // 0x74 't' - { 3394, 17, 17, 21, 2, -16 }, // 0x75 'u' - { 3431, 17, 17, 21, 2, -16 }, // 0x76 'v' - { 3468, 17, 17, 21, 2, -16 }, // 0x77 'w' - { 3505, 17, 17, 21, 2, -16 }, // 0x78 'x' - { 3542, 17, 20, 21, 2, -16 }, // 0x79 'y' - { 3585, 17, 17, 21, 2, -16 }, // 0x7A 'z' - { 3622, 11, 24, 21, 5, -23 }, // 0x7B '{' - { 3655, 3, 24, 19, 8, -23 }, // 0x7C '|' - { 3664, 11, 24, 21, 5, -23 }, // 0x7D '}' - { 3697, 17, 10, 21, 2, -23 } }; // 0x7E '~' + { 0, 1, 1, 20, 0, 0 }, // 0x20 ' ' + { 1, 7, 24, 20, 5, -23 }, // 0x21 '!' + { 22, 10, 7, 20, 5, -23 }, // 0x22 '"' + { 31, 17, 24, 21, 2, -23 }, // 0x23 '#' + { 82, 17, 24, 21, 2, -23 }, // 0x24 '$' + { 133, 17, 24, 21, 2, -23 }, // 0x25 '%' + { 184, 17, 24, 21, 2, -23 }, // 0x26 '&' + { 235, 3, 7, 19, 8, -23 }, // 0x27 ''' + { 238, 7, 24, 20, 8, -23 }, // 0x28 '(' + { 259, 7, 24, 20, 5, -23 }, // 0x29 ')' + { 280, 17, 17, 21, 2, -23 }, // 0x2A '*' + { 317, 17, 17, 21, 2, -19 }, // 0x2B '+' + { 354, 7, 10, 20, 5, -6 }, // 0x2C ',' + { 363, 17, 3, 21, 2, -12 }, // 0x2D '-' + { 370, 7, 7, 20, 5, -6 }, // 0x2E '.' + { 377, 17, 24, 21, 2, -23 }, // 0x2F '/' + { 428, 17, 24, 20, 2, -23 }, // 0x30 '0' + { 479, 10, 24, 20, 5, -23 }, // 0x31 '1' + { 509, 17, 24, 20, 2, -23 }, // 0x32 '2' + { 560, 17, 24, 20, 2, -23 }, // 0x33 '3' + { 611, 17, 24, 20, 2, -23 }, // 0x34 '4' + { 662, 17, 24, 20, 2, -23 }, // 0x35 '5' + { 713, 17, 24, 20, 2, -23 }, // 0x36 '6' + { 764, 17, 24, 20, 1, -23 }, // 0x37 '7' + { 815, 17, 24, 20, 2, -23 }, // 0x38 '8' + { 866, 17, 24, 20, 2, -23 }, // 0x39 '9' + { 917, 7, 20, 20, 5, -19 }, // 0x3A ':' + { 935, 7, 24, 20, 5, -20 }, // 0x3B ';' + { 956, 14, 24, 21, 2, -23 }, // 0x3C '<' + { 998, 17, 10, 21, 2, -16 }, // 0x3D '=' + { 1020, 14, 24, 20, 5, -23 }, // 0x3E '>' + { 1062, 17, 24, 21, 2, -23 }, // 0x3F '?' + { 1113, 17, 24, 21, 2, -23 }, // 0x40 '@' + { 1164, 17, 24, 21, 2, -23 }, // 0x41 'A' + { 1215, 17, 24, 21, 2, -23 }, // 0x42 'B' + { 1266, 17, 24, 21, 2, -23 }, // 0x43 'C' + { 1317, 17, 24, 21, 2, -23 }, // 0x44 'D' + { 1368, 17, 24, 21, 2, -23 }, // 0x45 'E' + { 1419, 17, 24, 21, 2, -23 }, // 0x46 'F' + { 1470, 17, 24, 21, 2, -23 }, // 0x47 'G' + { 1521, 17, 24, 21, 2, -23 }, // 0x48 'H' + { 1572, 10, 24, 20, 5, -23 }, // 0x49 'I' + { 1602, 17, 24, 21, 2, -23 }, // 0x4A 'J' + { 1653, 17, 24, 21, 2, -23 }, // 0x4B 'K' + { 1704, 17, 24, 21, 2, -23 }, // 0x4C 'L' + { 1755, 17, 24, 21, 2, -23 }, // 0x4D 'M' + { 1806, 17, 24, 21, 2, -23 }, // 0x4E 'N' + { 1857, 17, 24, 21, 2, -23 }, // 0x4F 'O' + { 1908, 17, 24, 21, 2, -23 }, // 0x50 'P' + { 1959, 17, 24, 21, 2, -23 }, // 0x51 'Q' + { 2010, 17, 24, 21, 2, -23 }, // 0x52 'R' + { 2061, 17, 24, 21, 2, -23 }, // 0x53 'S' + { 2112, 17, 24, 21, 2, -23 }, // 0x54 'T' + { 2163, 17, 24, 21, 2, -23 }, // 0x55 'U' + { 2214, 17, 24, 21, 2, -23 }, // 0x56 'V' + { 2265, 17, 24, 21, 2, -23 }, // 0x57 'W' + { 2316, 17, 24, 21, 2, -23 }, // 0x58 'X' + { 2367, 17, 24, 21, 2, -23 }, // 0x59 'Y' + { 2418, 17, 24, 21, 2, -23 }, // 0x5A 'Z' + { 2469, 7, 24, 20, 8, -23 }, // 0x5B '[' + { 2490, 17, 24, 21, 2, -23 }, // 0x5C '\' + { 2541, 7, 24, 20, 5, -23 }, // 0x5D ']' + { 2562, 17, 10, 21, 2, -23 }, // 0x5E '^' + { 2584, 17, 3, 21, 2, 1 }, // 0x5F '_' + { 2591, 7, 7, 20, 8, -23 }, // 0x60 '`' + { 2598, 17, 17, 21, 2, -16 }, // 0x61 'a' + { 2635, 17, 24, 21, 2, -23 }, // 0x62 'b' + { 2686, 17, 17, 21, 2, -16 }, // 0x63 'c' + { 2723, 17, 24, 21, 2, -23 }, // 0x64 'd' + { 2774, 17, 17, 21, 2, -16 }, // 0x65 'e' + { 2811, 14, 24, 20, 5, -23 }, // 0x66 'f' + { 2853, 17, 20, 21, 2, -16 }, // 0x67 'g' + { 2896, 17, 24, 21, 2, -23 }, // 0x68 'h' + { 2947, 10, 24, 20, 5, -23 }, // 0x69 'i' + { 2977, 13, 27, 20, 2, -23 }, // 0x6A 'j' + { 3021, 14, 24, 20, 5, -23 }, // 0x6B 'k' + { 3063, 10, 24, 20, 5, -23 }, // 0x6C 'l' + { 3093, 17, 17, 21, 2, -16 }, // 0x6D 'm' + { 3130, 17, 17, 21, 2, -16 }, // 0x6E 'n' + { 3167, 17, 17, 21, 2, -16 }, // 0x6F 'o' + { 3204, 17, 20, 21, 2, -16 }, // 0x70 'p' + { 3247, 17, 20, 21, 2, -16 }, // 0x71 'q' + { 3290, 13, 17, 20, 5, -16 }, // 0x72 'r' + { 3318, 17, 17, 21, 2, -16 }, // 0x73 's' + { 3355, 13, 24, 20, 5, -23 }, // 0x74 't' + { 3394, 17, 17, 21, 2, -16 }, // 0x75 'u' + { 3431, 17, 17, 21, 2, -16 }, // 0x76 'v' + { 3468, 17, 17, 21, 2, -16 }, // 0x77 'w' + { 3505, 17, 17, 21, 2, -16 }, // 0x78 'x' + { 3542, 17, 20, 21, 2, -16 }, // 0x79 'y' + { 3585, 17, 17, 21, 2, -16 }, // 0x7A 'z' + { 3622, 11, 24, 21, 5, -23 }, // 0x7B '{' + { 3655, 3, 24, 19, 8, -23 }, // 0x7C '|' + { 3664, 11, 24, 21, 5, -23 }, // 0x7D '}' + { 3697, 17, 10, 21, 2, -23 } }; // 0x7E '~' const GFXfont whitrabt18pt7b PROGMEM = { (uint8_t *)whitrabt18pt7bBitmaps, (GFXglyph *)whitrabt18pt7bGlyphs, - 0x20, 0x7E, 27 }; + 0x20, 0x7E, 27 }; // Approx. 4391 bytes +#endif // ifndef FONTS_WHITRABT18PT7B_H diff --git a/src/src/Static/Fonts/whitrabt20pt7b.h b/src/src/Static/Fonts/whitrabt20pt7b.h index 83f96984ca..311b7f8b07 100644 --- a/src/src/Static/Fonts/whitrabt20pt7b.h +++ b/src/src/Static/Fonts/whitrabt20pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_WHITRABT20PT7B_H +#define FONTS_WHITRABT20PT7B_H const uint8_t whitrabt20pt7bBitmaps[] PROGMEM = { 0x00, 0x7D, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0x00, 0x00, 0x00, 0x00, 0x07, 0xDF, 0xFF, 0xFF, 0xFF, 0xFD, @@ -391,105 +393,106 @@ const uint8_t whitrabt20pt7bBitmaps[] PROGMEM = { 0x7C, 0x07, 0xFF, 0x00, 0xFF, 0x80, 0x1F, 0xC0, 0x03, 0xE0 }; const GFXglyph whitrabt20pt7bGlyphs[] PROGMEM = { - { 0, 1, 1, 22, 0, 0 }, // 0x20 ' ' - { 1, 7, 27, 22, 6, -26 }, // 0x21 '!' - { 25, 11, 7, 23, 6, -26 }, // 0x22 '"' - { 35, 19, 27, 23, 2, -26 }, // 0x23 '#' - { 100, 18, 27, 22, 2, -26 }, // 0x24 '$' - { 161, 18, 27, 22, 2, -26 }, // 0x25 '%' - { 222, 19, 27, 22, 2, -26 }, // 0x26 '&' - { 287, 4, 7, 22, 9, -26 }, // 0x27 ''' - { 291, 8, 27, 22, 9, -26 }, // 0x28 '(' - { 318, 8, 27, 22, 5, -26 }, // 0x29 ')' - { 345, 19, 20, 23, 2, -26 }, // 0x2A '*' - { 393, 19, 19, 23, 2, -22 }, // 0x2B '+' - { 439, 7, 11, 22, 6, -6 }, // 0x2C ',' - { 449, 19, 4, 23, 2, -14 }, // 0x2D '-' - { 459, 7, 7, 22, 6, -6 }, // 0x2E '.' - { 466, 19, 27, 23, 2, -26 }, // 0x2F '/' - { 531, 19, 27, 22, 2, -26 }, // 0x30 '0' - { 596, 11, 27, 22, 6, -26 }, // 0x31 '1' - { 634, 19, 27, 22, 2, -26 }, // 0x32 '2' - { 699, 19, 27, 22, 2, -26 }, // 0x33 '3' - { 764, 19, 27, 22, 2, -26 }, // 0x34 '4' - { 829, 19, 27, 22, 2, -26 }, // 0x35 '5' - { 894, 19, 27, 22, 2, -26 }, // 0x36 '6' - { 959, 19, 27, 22, 2, -26 }, // 0x37 '7' - { 1024, 19, 27, 22, 2, -26 }, // 0x38 '8' - { 1089, 19, 27, 22, 2, -26 }, // 0x39 '9' - { 1154, 7, 23, 22, 6, -22 }, // 0x3A ':' - { 1175, 7, 26, 22, 6, -21 }, // 0x3B ';' - { 1198, 15, 27, 23, 2, -26 }, // 0x3C '<' - { 1249, 19, 12, 23, 2, -18 }, // 0x3D '=' - { 1278, 15, 27, 23, 6, -26 }, // 0x3E '>' - { 1329, 18, 27, 22, 2, -26 }, // 0x3F '?' - { 1390, 18, 27, 22, 2, -26 }, // 0x40 '@' - { 1451, 19, 27, 23, 2, -26 }, // 0x41 'A' - { 1516, 19, 27, 23, 2, -26 }, // 0x42 'B' - { 1581, 19, 27, 23, 2, -26 }, // 0x43 'C' - { 1646, 19, 27, 23, 2, -26 }, // 0x44 'D' - { 1711, 19, 27, 22, 2, -26 }, // 0x45 'E' - { 1776, 19, 27, 22, 2, -26 }, // 0x46 'F' - { 1841, 19, 27, 23, 2, -26 }, // 0x47 'G' - { 1906, 19, 27, 23, 2, -26 }, // 0x48 'H' - { 1971, 11, 27, 23, 6, -26 }, // 0x49 'I' - { 2009, 19, 27, 23, 2, -26 }, // 0x4A 'J' - { 2074, 19, 27, 22, 2, -26 }, // 0x4B 'K' - { 2139, 19, 27, 22, 2, -26 }, // 0x4C 'L' - { 2204, 19, 27, 23, 2, -26 }, // 0x4D 'M' - { 2269, 19, 27, 23, 2, -26 }, // 0x4E 'N' - { 2334, 19, 27, 23, 2, -26 }, // 0x4F 'O' - { 2399, 19, 27, 23, 2, -26 }, // 0x50 'P' - { 2464, 19, 27, 23, 2, -26 }, // 0x51 'Q' - { 2529, 19, 27, 23, 2, -26 }, // 0x52 'R' - { 2594, 19, 27, 23, 2, -26 }, // 0x53 'S' - { 2659, 19, 27, 23, 2, -26 }, // 0x54 'T' - { 2724, 19, 27, 23, 2, -26 }, // 0x55 'U' - { 2789, 19, 27, 23, 2, -26 }, // 0x56 'V' - { 2854, 18, 27, 22, 2, -26 }, // 0x57 'W' - { 2915, 19, 27, 23, 2, -26 }, // 0x58 'X' - { 2980, 18, 27, 22, 2, -26 }, // 0x59 'Y' - { 3041, 19, 27, 23, 2, -26 }, // 0x5A 'Z' - { 3106, 8, 27, 22, 9, -26 }, // 0x5B '[' - { 3133, 19, 27, 23, 2, -26 }, // 0x5C '\' - { 3198, 8, 27, 22, 5, -26 }, // 0x5D ']' - { 3225, 19, 12, 23, 2, -26 }, // 0x5E '^' - { 3254, 19, 4, 23, 2, 1 }, // 0x5F '_' - { 3264, 8, 8, 22, 9, -26 }, // 0x60 '`' - { 3272, 19, 19, 23, 2, -18 }, // 0x61 'a' - { 3318, 19, 27, 23, 2, -26 }, // 0x62 'b' - { 3383, 19, 19, 23, 2, -18 }, // 0x63 'c' - { 3429, 19, 27, 23, 2, -26 }, // 0x64 'd' - { 3494, 19, 19, 23, 2, -18 }, // 0x65 'e' - { 3540, 15, 27, 23, 6, -26 }, // 0x66 'f' - { 3591, 19, 23, 23, 2, -18 }, // 0x67 'g' - { 3646, 19, 27, 23, 2, -26 }, // 0x68 'h' - { 3711, 11, 27, 23, 6, -26 }, // 0x69 'i' - { 3749, 15, 31, 23, 2, -26 }, // 0x6A 'j' - { 3808, 15, 27, 23, 6, -26 }, // 0x6B 'k' - { 3859, 11, 27, 23, 6, -26 }, // 0x6C 'l' - { 3897, 18, 19, 22, 2, -18 }, // 0x6D 'm' - { 3940, 19, 19, 23, 2, -18 }, // 0x6E 'n' - { 3986, 19, 19, 23, 2, -18 }, // 0x6F 'o' - { 4032, 19, 23, 23, 2, -18 }, // 0x70 'p' - { 4087, 19, 23, 23, 2, -18 }, // 0x71 'q' - { 4142, 15, 19, 23, 6, -18 }, // 0x72 'r' - { 4178, 19, 19, 23, 2, -18 }, // 0x73 's' - { 4224, 15, 27, 23, 6, -26 }, // 0x74 't' - { 4275, 19, 19, 23, 2, -18 }, // 0x75 'u' - { 4321, 19, 19, 23, 2, -18 }, // 0x76 'v' - { 4367, 18, 19, 22, 2, -18 }, // 0x77 'w' - { 4410, 19, 19, 23, 2, -18 }, // 0x78 'x' - { 4456, 19, 23, 23, 2, -18 }, // 0x79 'y' - { 4511, 19, 19, 23, 2, -18 }, // 0x7A 'z' - { 4557, 12, 27, 22, 5, -26 }, // 0x7B '{' - { 4598, 4, 27, 22, 9, -26 }, // 0x7C '|' - { 4612, 12, 27, 22, 5, -26 }, // 0x7D '}' - { 4653, 18, 11, 22, 2, -25 } }; // 0x7E '~' + { 0, 1, 1, 22, 0, 0 }, // 0x20 ' ' + { 1, 7, 27, 22, 6, -26 }, // 0x21 '!' + { 25, 11, 7, 23, 6, -26 }, // 0x22 '"' + { 35, 19, 27, 23, 2, -26 }, // 0x23 '#' + { 100, 18, 27, 22, 2, -26 }, // 0x24 '$' + { 161, 18, 27, 22, 2, -26 }, // 0x25 '%' + { 222, 19, 27, 22, 2, -26 }, // 0x26 '&' + { 287, 4, 7, 22, 9, -26 }, // 0x27 ''' + { 291, 8, 27, 22, 9, -26 }, // 0x28 '(' + { 318, 8, 27, 22, 5, -26 }, // 0x29 ')' + { 345, 19, 20, 23, 2, -26 }, // 0x2A '*' + { 393, 19, 19, 23, 2, -22 }, // 0x2B '+' + { 439, 7, 11, 22, 6, -6 }, // 0x2C ',' + { 449, 19, 4, 23, 2, -14 }, // 0x2D '-' + { 459, 7, 7, 22, 6, -6 }, // 0x2E '.' + { 466, 19, 27, 23, 2, -26 }, // 0x2F '/' + { 531, 19, 27, 22, 2, -26 }, // 0x30 '0' + { 596, 11, 27, 22, 6, -26 }, // 0x31 '1' + { 634, 19, 27, 22, 2, -26 }, // 0x32 '2' + { 699, 19, 27, 22, 2, -26 }, // 0x33 '3' + { 764, 19, 27, 22, 2, -26 }, // 0x34 '4' + { 829, 19, 27, 22, 2, -26 }, // 0x35 '5' + { 894, 19, 27, 22, 2, -26 }, // 0x36 '6' + { 959, 19, 27, 22, 2, -26 }, // 0x37 '7' + { 1024, 19, 27, 22, 2, -26 }, // 0x38 '8' + { 1089, 19, 27, 22, 2, -26 }, // 0x39 '9' + { 1154, 7, 23, 22, 6, -22 }, // 0x3A ':' + { 1175, 7, 26, 22, 6, -21 }, // 0x3B ';' + { 1198, 15, 27, 23, 2, -26 }, // 0x3C '<' + { 1249, 19, 12, 23, 2, -18 }, // 0x3D '=' + { 1278, 15, 27, 23, 6, -26 }, // 0x3E '>' + { 1329, 18, 27, 22, 2, -26 }, // 0x3F '?' + { 1390, 18, 27, 22, 2, -26 }, // 0x40 '@' + { 1451, 19, 27, 23, 2, -26 }, // 0x41 'A' + { 1516, 19, 27, 23, 2, -26 }, // 0x42 'B' + { 1581, 19, 27, 23, 2, -26 }, // 0x43 'C' + { 1646, 19, 27, 23, 2, -26 }, // 0x44 'D' + { 1711, 19, 27, 22, 2, -26 }, // 0x45 'E' + { 1776, 19, 27, 22, 2, -26 }, // 0x46 'F' + { 1841, 19, 27, 23, 2, -26 }, // 0x47 'G' + { 1906, 19, 27, 23, 2, -26 }, // 0x48 'H' + { 1971, 11, 27, 23, 6, -26 }, // 0x49 'I' + { 2009, 19, 27, 23, 2, -26 }, // 0x4A 'J' + { 2074, 19, 27, 22, 2, -26 }, // 0x4B 'K' + { 2139, 19, 27, 22, 2, -26 }, // 0x4C 'L' + { 2204, 19, 27, 23, 2, -26 }, // 0x4D 'M' + { 2269, 19, 27, 23, 2, -26 }, // 0x4E 'N' + { 2334, 19, 27, 23, 2, -26 }, // 0x4F 'O' + { 2399, 19, 27, 23, 2, -26 }, // 0x50 'P' + { 2464, 19, 27, 23, 2, -26 }, // 0x51 'Q' + { 2529, 19, 27, 23, 2, -26 }, // 0x52 'R' + { 2594, 19, 27, 23, 2, -26 }, // 0x53 'S' + { 2659, 19, 27, 23, 2, -26 }, // 0x54 'T' + { 2724, 19, 27, 23, 2, -26 }, // 0x55 'U' + { 2789, 19, 27, 23, 2, -26 }, // 0x56 'V' + { 2854, 18, 27, 22, 2, -26 }, // 0x57 'W' + { 2915, 19, 27, 23, 2, -26 }, // 0x58 'X' + { 2980, 18, 27, 22, 2, -26 }, // 0x59 'Y' + { 3041, 19, 27, 23, 2, -26 }, // 0x5A 'Z' + { 3106, 8, 27, 22, 9, -26 }, // 0x5B '[' + { 3133, 19, 27, 23, 2, -26 }, // 0x5C '\' + { 3198, 8, 27, 22, 5, -26 }, // 0x5D ']' + { 3225, 19, 12, 23, 2, -26 }, // 0x5E '^' + { 3254, 19, 4, 23, 2, 1 }, // 0x5F '_' + { 3264, 8, 8, 22, 9, -26 }, // 0x60 '`' + { 3272, 19, 19, 23, 2, -18 }, // 0x61 'a' + { 3318, 19, 27, 23, 2, -26 }, // 0x62 'b' + { 3383, 19, 19, 23, 2, -18 }, // 0x63 'c' + { 3429, 19, 27, 23, 2, -26 }, // 0x64 'd' + { 3494, 19, 19, 23, 2, -18 }, // 0x65 'e' + { 3540, 15, 27, 23, 6, -26 }, // 0x66 'f' + { 3591, 19, 23, 23, 2, -18 }, // 0x67 'g' + { 3646, 19, 27, 23, 2, -26 }, // 0x68 'h' + { 3711, 11, 27, 23, 6, -26 }, // 0x69 'i' + { 3749, 15, 31, 23, 2, -26 }, // 0x6A 'j' + { 3808, 15, 27, 23, 6, -26 }, // 0x6B 'k' + { 3859, 11, 27, 23, 6, -26 }, // 0x6C 'l' + { 3897, 18, 19, 22, 2, -18 }, // 0x6D 'm' + { 3940, 19, 19, 23, 2, -18 }, // 0x6E 'n' + { 3986, 19, 19, 23, 2, -18 }, // 0x6F 'o' + { 4032, 19, 23, 23, 2, -18 }, // 0x70 'p' + { 4087, 19, 23, 23, 2, -18 }, // 0x71 'q' + { 4142, 15, 19, 23, 6, -18 }, // 0x72 'r' + { 4178, 19, 19, 23, 2, -18 }, // 0x73 's' + { 4224, 15, 27, 23, 6, -26 }, // 0x74 't' + { 4275, 19, 19, 23, 2, -18 }, // 0x75 'u' + { 4321, 19, 19, 23, 2, -18 }, // 0x76 'v' + { 4367, 18, 19, 22, 2, -18 }, // 0x77 'w' + { 4410, 19, 19, 23, 2, -18 }, // 0x78 'x' + { 4456, 19, 23, 23, 2, -18 }, // 0x79 'y' + { 4511, 19, 19, 23, 2, -18 }, // 0x7A 'z' + { 4557, 12, 27, 22, 5, -26 }, // 0x7B '{' + { 4598, 4, 27, 22, 9, -26 }, // 0x7C '|' + { 4612, 12, 27, 22, 5, -26 }, // 0x7D '}' + { 4653, 18, 11, 22, 2, -25 } }; // 0x7E '~' const GFXfont whitrabt20pt7b PROGMEM = { (uint8_t *)whitrabt20pt7bBitmaps, (GFXglyph *)whitrabt20pt7bGlyphs, - 0x20, 0x7E, 30 }; + 0x20, 0x7E, 30 }; // Approx. 5350 bytes +#endif // ifndef FONTS_WHITRABT20PT7B_H diff --git a/src/src/Static/Fonts/whitrabt8pt7b.h b/src/src/Static/Fonts/whitrabt8pt7b.h index 3cc51e8ac9..85748d59fe 100644 --- a/src/src/Static/Fonts/whitrabt8pt7b.h +++ b/src/src/Static/Fonts/whitrabt8pt7b.h @@ -1,3 +1,5 @@ +#ifndef FONTS_WHITRABT8PT7B_H +#define FONTS_WHITRABT8PT7B_H const uint8_t whitrabt8pt7bBitmaps[] PROGMEM = { 0x00, 0xFF, 0xFF, 0xC0, 0xFF, 0x80, 0xDE, 0xF6, 0x6C, 0xD9, 0xB7, 0xF6, 0xCD, 0xBF, 0xB6, 0x6C, 0xD9, 0xB0, 0x18, 0x18, 0x7F, 0xD8, 0xD8, 0x7E, @@ -69,105 +71,106 @@ const uint8_t whitrabt8pt7bBitmaps[] PROGMEM = { 0xDB, 0xDB, 0xDB, 0x0E }; const GFXglyph whitrabt8pt7bGlyphs[] PROGMEM = { - { 0, 1, 1, 9, 0, 0 }, // 0x20 ' ' - { 1, 3, 11, 9, 2, -10 }, // 0x21 '!' - { 6, 5, 3, 9, 2, -10 }, // 0x22 '"' - { 8, 7, 11, 9, 1, -10 }, // 0x23 '#' - { 18, 8, 11, 10, 1, -10 }, // 0x24 '$' - { 29, 8, 11, 10, 1, -10 }, // 0x25 '%' - { 40, 8, 11, 9, 1, -10 }, // 0x26 '&' - { 51, 2, 3, 10, 4, -10 }, // 0x27 ''' - { 52, 3, 11, 9, 4, -10 }, // 0x28 '(' - { 57, 4, 11, 10, 2, -10 }, // 0x29 ')' - { 63, 7, 7, 9, 1, -10 }, // 0x2A '*' - { 70, 7, 8, 9, 1, -9 }, // 0x2B '+' - { 77, 3, 5, 9, 2, -2 }, // 0x2C ',' - { 79, 7, 1, 9, 1, -5 }, // 0x2D '-' - { 80, 3, 3, 9, 2, -2 }, // 0x2E '.' - { 82, 8, 11, 10, 1, -10 }, // 0x2F '/' - { 93, 8, 11, 9, 1, -10 }, // 0x30 '0' - { 104, 5, 11, 9, 1, -10 }, // 0x31 '1' - { 111, 8, 11, 9, 1, -10 }, // 0x32 '2' - { 122, 8, 11, 9, 1, -10 }, // 0x33 '3' - { 133, 7, 11, 9, 1, -10 }, // 0x34 '4' - { 143, 8, 11, 9, 1, -10 }, // 0x35 '5' - { 154, 8, 11, 9, 1, -10 }, // 0x36 '6' - { 165, 8, 11, 9, 1, -10 }, // 0x37 '7' - { 176, 8, 11, 9, 1, -10 }, // 0x38 '8' - { 187, 8, 11, 9, 1, -10 }, // 0x39 '9' - { 198, 3, 9, 9, 2, -8 }, // 0x3A ':' - { 202, 3, 11, 9, 2, -8 }, // 0x3B ';' - { 207, 6, 11, 9, 1, -10 }, // 0x3C '<' - { 216, 7, 4, 9, 1, -7 }, // 0x3D '=' - { 220, 6, 11, 9, 2, -10 }, // 0x3E '>' - { 229, 8, 11, 10, 1, -10 }, // 0x3F '?' - { 240, 8, 11, 10, 1, -10 }, // 0x40 '@' - { 251, 8, 11, 10, 1, -10 }, // 0x41 'A' - { 262, 8, 11, 10, 1, -10 }, // 0x42 'B' - { 273, 8, 11, 10, 1, -10 }, // 0x43 'C' - { 284, 8, 11, 10, 1, -10 }, // 0x44 'D' - { 295, 8, 11, 10, 1, -10 }, // 0x45 'E' - { 306, 8, 11, 10, 1, -10 }, // 0x46 'F' - { 317, 8, 11, 10, 1, -10 }, // 0x47 'G' - { 328, 8, 11, 10, 1, -10 }, // 0x48 'H' - { 339, 4, 11, 8, 2, -10 }, // 0x49 'I' - { 345, 8, 11, 10, 1, -10 }, // 0x4A 'J' - { 356, 8, 11, 10, 1, -10 }, // 0x4B 'K' - { 367, 8, 11, 10, 1, -10 }, // 0x4C 'L' - { 378, 8, 11, 10, 1, -10 }, // 0x4D 'M' - { 389, 8, 11, 10, 1, -10 }, // 0x4E 'N' - { 400, 8, 11, 10, 1, -10 }, // 0x4F 'O' - { 411, 8, 11, 10, 1, -10 }, // 0x50 'P' - { 422, 8, 11, 10, 1, -10 }, // 0x51 'Q' - { 433, 8, 11, 10, 1, -10 }, // 0x52 'R' - { 444, 8, 11, 10, 1, -10 }, // 0x53 'S' - { 455, 7, 11, 9, 1, -10 }, // 0x54 'T' - { 465, 8, 11, 10, 1, -10 }, // 0x55 'U' - { 476, 8, 11, 10, 1, -10 }, // 0x56 'V' - { 487, 8, 11, 10, 1, -10 }, // 0x57 'W' - { 498, 8, 11, 10, 1, -10 }, // 0x58 'X' - { 509, 8, 11, 10, 1, -10 }, // 0x59 'Y' - { 520, 8, 11, 10, 1, -10 }, // 0x5A 'Z' - { 531, 4, 11, 10, 4, -10 }, // 0x5B '[' - { 537, 8, 11, 10, 1, -10 }, // 0x5C '\' - { 548, 4, 11, 10, 2, -10 }, // 0x5D ']' - { 554, 7, 5, 9, 1, -10 }, // 0x5E '^' - { 559, 7, 1, 9, 1, 1 }, // 0x5F '_' - { 560, 3, 3, 9, 4, -10 }, // 0x60 '`' - { 562, 8, 8, 10, 1, -7 }, // 0x61 'a' - { 570, 8, 11, 10, 1, -10 }, // 0x62 'b' - { 581, 8, 8, 10, 1, -7 }, // 0x63 'c' - { 589, 8, 11, 10, 1, -10 }, // 0x64 'd' - { 600, 8, 8, 10, 1, -7 }, // 0x65 'e' - { 608, 6, 11, 9, 2, -10 }, // 0x66 'f' - { 617, 8, 10, 10, 1, -7 }, // 0x67 'g' - { 627, 8, 11, 10, 1, -10 }, // 0x68 'h' - { 638, 5, 11, 8, 1, -10 }, // 0x69 'i' - { 645, 6, 13, 9, 1, -10 }, // 0x6A 'j' - { 655, 6, 11, 9, 2, -10 }, // 0x6B 'k' - { 664, 5, 11, 8, 1, -10 }, // 0x6C 'l' - { 671, 8, 8, 10, 1, -7 }, // 0x6D 'm' - { 679, 8, 8, 10, 1, -7 }, // 0x6E 'n' - { 687, 8, 8, 10, 1, -7 }, // 0x6F 'o' - { 695, 8, 10, 10, 1, -7 }, // 0x70 'p' - { 705, 8, 10, 10, 1, -7 }, // 0x71 'q' - { 715, 6, 8, 9, 2, -7 }, // 0x72 'r' - { 721, 8, 8, 10, 1, -7 }, // 0x73 's' - { 729, 6, 11, 9, 2, -10 }, // 0x74 't' - { 738, 8, 8, 10, 1, -7 }, // 0x75 'u' - { 746, 8, 8, 10, 1, -7 }, // 0x76 'v' - { 754, 8, 8, 10, 1, -7 }, // 0x77 'w' - { 762, 7, 8, 9, 1, -7 }, // 0x78 'x' - { 769, 8, 10, 10, 1, -7 }, // 0x79 'y' - { 779, 7, 8, 9, 1, -7 }, // 0x7A 'z' - { 786, 5, 11, 9, 2, -10 }, // 0x7B '{' - { 793, 2, 11, 10, 4, -10 }, // 0x7C '|' - { 796, 5, 11, 9, 2, -10 }, // 0x7D '}' - { 803, 8, 5, 10, 1, -11 } }; // 0x7E '~' + { 0, 1, 1, 9, 0, 0 }, // 0x20 ' ' + { 1, 3, 11, 9, 2, -10 }, // 0x21 '!' + { 6, 5, 3, 9, 2, -10 }, // 0x22 '"' + { 8, 7, 11, 9, 1, -10 }, // 0x23 '#' + { 18, 8, 11, 10, 1, -10 }, // 0x24 '$' + { 29, 8, 11, 10, 1, -10 }, // 0x25 '%' + { 40, 8, 11, 9, 1, -10 }, // 0x26 '&' + { 51, 2, 3, 10, 4, -10 }, // 0x27 ''' + { 52, 3, 11, 9, 4, -10 }, // 0x28 '(' + { 57, 4, 11, 10, 2, -10 }, // 0x29 ')' + { 63, 7, 7, 9, 1, -10 }, // 0x2A '*' + { 70, 7, 8, 9, 1, -9 }, // 0x2B '+' + { 77, 3, 5, 9, 2, -2 }, // 0x2C ',' + { 79, 7, 1, 9, 1, -5 }, // 0x2D '-' + { 80, 3, 3, 9, 2, -2 }, // 0x2E '.' + { 82, 8, 11, 10, 1, -10 }, // 0x2F '/' + { 93, 8, 11, 9, 1, -10 }, // 0x30 '0' + { 104, 5, 11, 9, 1, -10 }, // 0x31 '1' + { 111, 8, 11, 9, 1, -10 }, // 0x32 '2' + { 122, 8, 11, 9, 1, -10 }, // 0x33 '3' + { 133, 7, 11, 9, 1, -10 }, // 0x34 '4' + { 143, 8, 11, 9, 1, -10 }, // 0x35 '5' + { 154, 8, 11, 9, 1, -10 }, // 0x36 '6' + { 165, 8, 11, 9, 1, -10 }, // 0x37 '7' + { 176, 8, 11, 9, 1, -10 }, // 0x38 '8' + { 187, 8, 11, 9, 1, -10 }, // 0x39 '9' + { 198, 3, 9, 9, 2, -8 }, // 0x3A ':' + { 202, 3, 11, 9, 2, -8 }, // 0x3B ';' + { 207, 6, 11, 9, 1, -10 }, // 0x3C '<' + { 216, 7, 4, 9, 1, -7 }, // 0x3D '=' + { 220, 6, 11, 9, 2, -10 }, // 0x3E '>' + { 229, 8, 11, 10, 1, -10 }, // 0x3F '?' + { 240, 8, 11, 10, 1, -10 }, // 0x40 '@' + { 251, 8, 11, 10, 1, -10 }, // 0x41 'A' + { 262, 8, 11, 10, 1, -10 }, // 0x42 'B' + { 273, 8, 11, 10, 1, -10 }, // 0x43 'C' + { 284, 8, 11, 10, 1, -10 }, // 0x44 'D' + { 295, 8, 11, 10, 1, -10 }, // 0x45 'E' + { 306, 8, 11, 10, 1, -10 }, // 0x46 'F' + { 317, 8, 11, 10, 1, -10 }, // 0x47 'G' + { 328, 8, 11, 10, 1, -10 }, // 0x48 'H' + { 339, 4, 11, 8, 2, -10 }, // 0x49 'I' + { 345, 8, 11, 10, 1, -10 }, // 0x4A 'J' + { 356, 8, 11, 10, 1, -10 }, // 0x4B 'K' + { 367, 8, 11, 10, 1, -10 }, // 0x4C 'L' + { 378, 8, 11, 10, 1, -10 }, // 0x4D 'M' + { 389, 8, 11, 10, 1, -10 }, // 0x4E 'N' + { 400, 8, 11, 10, 1, -10 }, // 0x4F 'O' + { 411, 8, 11, 10, 1, -10 }, // 0x50 'P' + { 422, 8, 11, 10, 1, -10 }, // 0x51 'Q' + { 433, 8, 11, 10, 1, -10 }, // 0x52 'R' + { 444, 8, 11, 10, 1, -10 }, // 0x53 'S' + { 455, 7, 11, 9, 1, -10 }, // 0x54 'T' + { 465, 8, 11, 10, 1, -10 }, // 0x55 'U' + { 476, 8, 11, 10, 1, -10 }, // 0x56 'V' + { 487, 8, 11, 10, 1, -10 }, // 0x57 'W' + { 498, 8, 11, 10, 1, -10 }, // 0x58 'X' + { 509, 8, 11, 10, 1, -10 }, // 0x59 'Y' + { 520, 8, 11, 10, 1, -10 }, // 0x5A 'Z' + { 531, 4, 11, 10, 4, -10 }, // 0x5B '[' + { 537, 8, 11, 10, 1, -10 }, // 0x5C '\' + { 548, 4, 11, 10, 2, -10 }, // 0x5D ']' + { 554, 7, 5, 9, 1, -10 }, // 0x5E '^' + { 559, 7, 1, 9, 1, 1 }, // 0x5F '_' + { 560, 3, 3, 9, 4, -10 }, // 0x60 '`' + { 562, 8, 8, 10, 1, -7 }, // 0x61 'a' + { 570, 8, 11, 10, 1, -10 }, // 0x62 'b' + { 581, 8, 8, 10, 1, -7 }, // 0x63 'c' + { 589, 8, 11, 10, 1, -10 }, // 0x64 'd' + { 600, 8, 8, 10, 1, -7 }, // 0x65 'e' + { 608, 6, 11, 9, 2, -10 }, // 0x66 'f' + { 617, 8, 10, 10, 1, -7 }, // 0x67 'g' + { 627, 8, 11, 10, 1, -10 }, // 0x68 'h' + { 638, 5, 11, 8, 1, -10 }, // 0x69 'i' + { 645, 6, 13, 9, 1, -10 }, // 0x6A 'j' + { 655, 6, 11, 9, 2, -10 }, // 0x6B 'k' + { 664, 5, 11, 8, 1, -10 }, // 0x6C 'l' + { 671, 8, 8, 10, 1, -7 }, // 0x6D 'm' + { 679, 8, 8, 10, 1, -7 }, // 0x6E 'n' + { 687, 8, 8, 10, 1, -7 }, // 0x6F 'o' + { 695, 8, 10, 10, 1, -7 }, // 0x70 'p' + { 705, 8, 10, 10, 1, -7 }, // 0x71 'q' + { 715, 6, 8, 9, 2, -7 }, // 0x72 'r' + { 721, 8, 8, 10, 1, -7 }, // 0x73 's' + { 729, 6, 11, 9, 2, -10 }, // 0x74 't' + { 738, 8, 8, 10, 1, -7 }, // 0x75 'u' + { 746, 8, 8, 10, 1, -7 }, // 0x76 'v' + { 754, 8, 8, 10, 1, -7 }, // 0x77 'w' + { 762, 7, 8, 9, 1, -7 }, // 0x78 'x' + { 769, 8, 10, 10, 1, -7 }, // 0x79 'y' + { 779, 7, 8, 9, 1, -7 }, // 0x7A 'z' + { 786, 5, 11, 9, 2, -10 }, // 0x7B '{' + { 793, 2, 11, 10, 4, -10 }, // 0x7C '|' + { 796, 5, 11, 9, 2, -10 }, // 0x7D '}' + { 803, 8, 5, 10, 1, -11 } }; // 0x7E '~' const GFXfont whitrabt8pt7b PROGMEM = { (uint8_t *)whitrabt8pt7bBitmaps, (GFXglyph *)whitrabt8pt7bGlyphs, - 0x20, 0x7E, 12 }; + 0x20, 0x7E, 12 }; // Approx. 1480 bytes +#endif // ifndef FONTS_WHITRABT8PT7B_H From 08a50497b51e2fa21c098f580bd6a10efbc7e551 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 9 Jun 2022 21:15:38 +0200 Subject: [PATCH 024/113] [AdaGFX] Regression: Background-fill for text should fill entire line height --- src/src/Helpers/AdafruitGFX_helper.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index 00b0e68fe7..292807b4b1 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -1891,10 +1891,11 @@ void AdafruitGFX_helper::printText(const char *string, if (bkcolor == color) { bkcolor = _bgcolor; } // To get at least the text readable - if (_textPrintMode == AdaGFXTextPrintMode::ClearThenTruncate) { - _display->fillRect(_x + oTop, yText, res_x - (_x - xOffset), hText + oBottom, bkcolor); // Clear text area to right edge of screen + if (_textPrintMode == AdaGFXTextPrintMode::ClearThenTruncate) { // oTop is negative so subtract to add... + _display->fillRect(_x + oTop, yText, res_x - (_x - xOffset), hText + oBottom - oTop, bkcolor); // Clear text area to right edge of + // screen } else { - _display->fillRect(_x + oTop, yText, _w, hText + oBottom, bkcolor); // Clear text area + _display->fillRect(_x + oTop, yText, _w, hText + oBottom - oTop, bkcolor); // Clear text area } delay(0); From 6f1c1588052c37932d20f68b55216a7d95bdcffa Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 9 Jun 2022 21:22:19 +0200 Subject: [PATCH 025/113] [TouchHandler] Change method arguments to const-by-reference where possible --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 64 +++++++++++------------ src/src/Helpers/ESPEasy_TouchHandler.h | 65 ++++++++++++------------ 2 files changed, 65 insertions(+), 64 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 6aa0c370c8..d3730cdcff 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -26,8 +26,8 @@ const __FlashStringHelper* toString(Touch_action_e action) { */ ESPEasy_TouchHandler::ESPEasy_TouchHandler() {} -ESPEasy_TouchHandler::ESPEasy_TouchHandler(uint16_t displayTask, - AdaGFXColorDepth colorDepth) +ESPEasy_TouchHandler::ESPEasy_TouchHandler(const uint16_t & displayTask, + const AdaGFXColorDepth& colorDepth) : _displayTask(displayTask), _colorDepth(colorDepth) {} /** @@ -193,10 +193,10 @@ void ESPEasy_TouchHandler::init(struct EventStruct *event) { /** * helper function: use parseString() to read an argument, and convert that to an int value */ -int ESPEasy_TouchHandler::parseStringToInt(const String& string, - uint8_t indexFind, - char separator, - int defaultValue) { +int ESPEasy_TouchHandler::parseStringToInt(const String & string, + const uint8_t& indexFind, + const char & separator, + const int & defaultValue) { String parsed = parseStringKeepCase(string, indexFind, separator); int result = defaultValue; @@ -224,10 +224,10 @@ bool ESPEasy_TouchHandler::isCalibrationActive() { * Returns state, sets selectedObjectName to the best matching object name * and selectedObjectIndex to the index into the TouchObjects vector. */ -bool ESPEasy_TouchHandler::isValidAndTouchedTouchObject(int16_t x, - int16_t y, - String& selectedObjectName, - int8_t& selectedObjectIndex) { +bool ESPEasy_TouchHandler::isValidAndTouchedTouchObject(const int16_t& x, + const int16_t& y, + String & selectedObjectName, + int8_t & selectedObjectIndex) { uint32_t lastObjectArea = 0u; bool selected = false; @@ -292,7 +292,7 @@ bool ESPEasy_TouchHandler::isValidAndTouchedTouchObject(int16_t x, */ int8_t ESPEasy_TouchHandler::getTouchObjectIndex(struct EventStruct *event, const String & touchObject, - bool isButton) { + const bool & isButton) { if (touchObject.isEmpty()) { return -1; } int index = -1; @@ -319,7 +319,7 @@ int8_t ESPEasy_TouchHandler::getTouchObjectIndex(struct EventStruct *event, */ bool ESPEasy_TouchHandler::setTouchObjectState(struct EventStruct *event, const String & touchObject, - bool state) { + const bool & state) { if (touchObject.isEmpty()) { return false; } bool success = false; @@ -381,7 +381,7 @@ int8_t ESPEasy_TouchHandler::getTouchObjectState(struct EventStruct *event, */ bool ESPEasy_TouchHandler::setTouchButtonOnOff(struct EventStruct *event, const String & touchObject, - bool state) { + const bool & state) { if (touchObject.isEmpty()) { return false; } bool success = false; @@ -439,8 +439,8 @@ int8_t ESPEasy_TouchHandler::getTouchButtonOnOff(struct EventStruct *event, * mode: -2 = clear buttons in group, -3 = clear all buttongroups, -1 = draw buttons in group, 0 = initialize buttons */ void ESPEasy_TouchHandler::displayButtonGroup(struct EventStruct *event, - int16_t buttonGroup, - int8_t mode) { + const int16_t & buttonGroup, + const int8_t & mode) { for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { displayButton(event, objectNr, buttonGroup, mode); @@ -468,8 +468,8 @@ void ESPEasy_TouchHandler::displayButtonGroup(struct EventStruct *event, * Display a single button, using mode from displayButtonGroup */ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, - int8_t buttonNr, - int16_t buttonGroup, + const int8_t & buttonNr, + const int16_t & buttonGroup, int8_t mode) { if ((buttonNr < 0) || (buttonNr >= static_cast(TouchObjects.size()))) { return false; } // sanity check int8_t state = 99; @@ -557,8 +557,8 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, * When ignoreZero = true will return false for group 0 if the number of groups > 1. * NB: Group 0 is always available, even without button definitions! */ -bool ESPEasy_TouchHandler::validButtonGroup(int16_t group, - bool ignoreZero) { +bool ESPEasy_TouchHandler::validButtonGroup(const int16_t& group, + const bool & ignoreZero) { return _buttonGroups.find(group) != _buttonGroups.end() && (!ignoreZero || group > 0 || (group == 0 && _buttonGroups.size() == 1)); } @@ -567,7 +567,7 @@ bool ESPEasy_TouchHandler::validButtonGroup(int16_t group, * Set the desired button group, must be a known group, previous group will be erased and new group drawn */ bool ESPEasy_TouchHandler::setButtonGroup(struct EventStruct *event, - int16_t buttonGroup) { + const int16_t & buttonGroup) { if (validButtonGroup(buttonGroup)) { if (buttonGroup != _buttonGroup) { displayButtonGroup(event, _buttonGroup, -2); @@ -1381,13 +1381,13 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { * Every 10th second we check if the screen is touched */ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, - int16_t x, - int16_t y, - int16_t ox, - int16_t oy, - int16_t rx, - int16_t ry, - int16_t z) { + const int16_t & x, + const int16_t & y, + const int16_t & ox, + const int16_t & oy, + const int16_t & rx, + const int16_t & ry, + const int16_t & z) { bool success = false; // Avoid event-storms by deduplicating coordinates @@ -1635,11 +1635,11 @@ bool ESPEasy_TouchHandler::plugin_get_config_value(struct EventStruct *event, * When a display is configured add x,y coordinate, width,height of the object, objectIndex, and TaskIndex of display **************************************************************************/ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, - const int8_t objectIndex, - const int8_t onOffState, - const int8_t mode, - const bool groupSwitch, - const int8_t factor) { + const int8_t & objectIndex, + const int8_t & onOffState, + const int8_t & mode, + const bool & groupSwitch, + const int8_t & factor) { if ((objectIndex < 0) || // Range check (objectIndex >= static_cast(TouchObjects.size()))) { return; diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index 3bdb027df0..14c4fa79d7 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -11,6 +11,7 @@ /***** * Changelog: + * 2022-06-09 tonhuisman: Change method arguments to const-by-reference where possible for improved compile-time checks * 2022-06-06 tonhuisman: Move PLUGIN_WRITE handling from P123 * Move PLUGIN_GET_CONFIG_VALUE handling from P123. * Add getters for on/off (state) and (enabled), and matching GET_CONFIG_VALUE commands @@ -206,40 +207,40 @@ class ESPEasy_TouchHandler { public: ESPEasy_TouchHandler(); - ESPEasy_TouchHandler(uint16_t displayTask, - AdaGFXColorDepth colorDepth); + ESPEasy_TouchHandler(const uint16_t & displayTask, + const AdaGFXColorDepth& colorDepth); virtual ~ESPEasy_TouchHandler() {} void loadTouchObjects(struct EventStruct *event); void init(struct EventStruct *event); bool isCalibrationActive(); - bool isValidAndTouchedTouchObject(int16_t x, - int16_t y, - String& selectedObjectName, - int8_t& selectedObjectIndex); + bool isValidAndTouchedTouchObject(const int16_t& x, + const int16_t& y, + String & selectedObjectName, + int8_t & selectedObjectIndex); int8_t getTouchObjectIndex(struct EventStruct *event, const String & touchObject, - bool isButton = false); + const bool & isButton = false); bool setTouchObjectState(struct EventStruct *event, const String & touchObject, - bool state); + const bool & state); int8_t getTouchObjectState(struct EventStruct *event, const String & touchObject); bool setTouchButtonOnOff(struct EventStruct *event, const String & touchObject, - bool state); + const bool & state); int8_t getTouchButtonOnOff(struct EventStruct *event, const String & touchObject); bool plugin_webform_load(struct EventStruct *event); bool plugin_webform_save(struct EventStruct *event); bool plugin_fifty_per_second(struct EventStruct *event, - int16_t x, - int16_t y, - int16_t ox, - int16_t oy, - int16_t rx, - int16_t ry, - int16_t z); + const int16_t & x, + const int16_t & y, + const int16_t & ox, + const int16_t & oy, + const int16_t & rx, + const int16_t & ry, + const int16_t & z); bool plugin_write(struct EventStruct *event, const String & string); bool plugin_get_config_value(struct EventStruct *event, @@ -248,34 +249,34 @@ class ESPEasy_TouchHandler { return _buttonGroup; } - bool validButtonGroup(int16_t group, - bool ignoreZero = false); + bool validButtonGroup(const int16_t& group, + const bool & ignoreZero = false); bool setButtonGroup(struct EventStruct *event, - int16_t buttonGroup); + const int16_t & buttonGroup); bool incrementButtonGroup(struct EventStruct *event); bool decrementButtonGroup(struct EventStruct *event); bool incrementButtonPage(struct EventStruct *event); bool decrementButtonPage(struct EventStruct *event); void displayButtonGroup(struct EventStruct *event, - int16_t buttonGroup, - int8_t mode = 0); + const int16_t & buttonGroup, + const int8_t & mode = 0); bool displayButton(struct EventStruct *event, - int8_t buttonNr, - int16_t buttonGroup = -1, + const int8_t & buttonNr, + const int16_t & buttonGroup = -1, int8_t mode = 0); private: - int parseStringToInt(const String& string, - uint8_t indexFind, - char separator = ',', - int defaultValue = 0); + int parseStringToInt(const String & string, + const uint8_t& indexFind, + const char & separator = ',', + const int & defaultValue = 0); void generateObjectEvent(struct EventStruct *event, - const int8_t objectIndex, - const int8_t onOffState, - const int8_t mode = 0, - const bool groupSwitch = false, - const int8_t factor = 1); + const int8_t & objectIndex, + const int8_t & onOffState, + const int8_t & mode = 0, + const bool & groupSwitch = false, + const int8_t & factor = 1); bool _deduplicate = false; uint16_t _displayTask = 0u; From 2e753273dfb5df761c151a8c95167d4565927b41 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Fri, 10 Jun 2022 21:56:50 +0200 Subject: [PATCH 026/113] [TouchHandler] Strip unneeded prefix from page variables --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 64 ++++++++++++------------ 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index d3730cdcff..3bd7aa74f4 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -629,7 +629,7 @@ bool ESPEasy_TouchHandler::decrementButtonPage(struct EventStruct *event) { bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { addFormSubHeader(F("Touch configuration")); - addFormCheckBox(F("Flip rotation 180°"), F("tch_rotation_flipped"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_ROTATION_FLIPPED)); + addFormCheckBox(F("Flip rotation 180°"), F("rotation_flipped"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_ROTATION_FLIPPED)); # ifndef LIMIT_BUILD_SIZE addFormNote(F("Some touchscreens are mounted 180° rotated on the display.")); # endif // ifndef LIMIT_BUILD_SIZE @@ -656,13 +656,13 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { # endif // ifdef TOUCH_USE_EXTENDED_TOUCH }; int optionValues3[TOUCH_EVENTS_OPTIONS] = { 0, 1, 3, 4, 5, 7 }; // Already used as a bitmap! - addFormSelector(F("Events"), F("tch_events"), TOUCH_EVENTS_OPTIONS, options3, optionValues3, choice3); + addFormSelector(F("Events"), F("events"), TOUCH_EVENTS_OPTIONS, options3, optionValues3, choice3); - addFormCheckBox(F("Draw buttons when started"), F("tch_init_objectevent"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)); + addFormCheckBox(F("Draw buttons when started"), F("init_objectevent"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)); addFormNote(F("Needs Objectnames 'Events' to be enabled.")); } - addFormCheckBox(F("Prevent duplicate events"), F("tch_deduplicate"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_DEDUPLICATE)); + addFormCheckBox(F("Prevent duplicate events"), F("deduplicate"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_DEDUPLICATE)); # ifndef LIMIT_BUILD_SIZE @@ -677,7 +677,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { const __FlashStringHelper *noYesOptions[2] = { F("No"), F("Yes") }; int noYesOptionValues[2] = { 0, 1 }; addFormSelector(F("Calibrate to screen resolution"), - F("tch_use_calibration"), + F("use_calibration"), 2, noYesOptions, noYesOptionValues, @@ -698,24 +698,24 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { html_TR_TD(); addHtml(F("Top-left")); html_TD(); - addNumericBox(F("tch_cal_tl_x"), + addNumericBox(F("cal_tl_x"), Touch_Settings.top_left.x, 0, 65535); html_TD(); - addNumericBox(F("tch_cal_tl_y"), + addNumericBox(F("cal_tl_y"), Touch_Settings.top_left.y, 0, 65535); html_TD(); addHtml(F("Bottom-right")); html_TD(); - addNumericBox(F("tch_cal_br_x"), + addNumericBox(F("cal_br_x"), Touch_Settings.bottom_right.x, 0, 65535); html_TD(); - addNumericBox(F("tch_cal_br_y"), + addNumericBox(F("cal_br_y"), Touch_Settings.bottom_right.y, 0, 65535); @@ -723,7 +723,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { html_end_table(); } - addFormCheckBox(F("Enable logging for calibration"), F("tch_log_calibration"), + addFormCheckBox(F("Enable logging for calibration"), F("log_calibration"), Touch_Settings.logEnabled); addFormSubHeader(F("Touch objects")); @@ -800,15 +800,15 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { html_end_table(); } { - addFormNumericBox(F("Initial button group"), F("tch_initial_group"), + addFormNumericBox(F("Initial button group"), F("initial_group"), get8BitFromUL(Touch_Settings.flags, TOUCH_FLAGS_INITIAL_GROUP), 0, TOUCH_MAX_BUTTON_GROUPS # ifdef TOUCH_USE_TOOLTIPS , F("Initial group") # endif // ifdef TOUCH_USE_TOOLTIPS ); - addFormCheckBox(F("Draw buttons via Rules"), F("tch_via_rules"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_DRAWBTN_VIA_RULES)); - addFormCheckBox(F("Enable/Disable page buttons"), F("tch_page_buttons"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_AUTO_PAGE_ARROWS)); - addFormCheckBox(F("PageUp/PageDown reversed"), F("tch_page_below"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU)); + addFormCheckBox(F("Draw buttons via Rules"), F("via_rules"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_DRAWBTN_VIA_RULES)); + addFormCheckBox(F("Enable/Disable page buttons"), F("page_buttons"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_AUTO_PAGE_ARROWS)); + addFormCheckBox(F("PageUp/PageDown reversed"), F("page_below"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU)); } # endif // ifdef TOUCH_USE_EXTENDED_TOUCH { @@ -1146,7 +1146,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { } html_end_table(); - addFormNumericBox(F("Debounce delay for On/Off buttons"), F("tch_debounce"), + addFormNumericBox(F("Debounce delay for On/Off buttons"), F("debounce"), Touch_Settings.debounceMs, 0, 255); addUnit(F("0-255 msec.")); } @@ -1181,32 +1181,32 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { uint32_t lSettings = 0u; - bitWrite(lSettings, TOUCH_FLAGS_SEND_XY, bitRead(getFormItemInt(F("tch_events")), TOUCH_FLAGS_SEND_XY)); - bitWrite(lSettings, TOUCH_FLAGS_SEND_Z, bitRead(getFormItemInt(F("tch_events")), TOUCH_FLAGS_SEND_Z)); - bitWrite(lSettings, TOUCH_FLAGS_SEND_OBJECTNAME, bitRead(getFormItemInt(F("tch_events")), TOUCH_FLAGS_SEND_OBJECTNAME)); - bitWrite(lSettings, TOUCH_FLAGS_ROTATION_FLIPPED, isFormItemChecked(F("tch_rotation_flipped"))); - bitWrite(lSettings, TOUCH_FLAGS_DEDUPLICATE, isFormItemChecked(F("tch_deduplicate"))); - bitWrite(lSettings, TOUCH_FLAGS_INIT_OBJECTEVENT, isFormItemChecked(F("tch_init_objectevent"))); + bitWrite(lSettings, TOUCH_FLAGS_SEND_XY, bitRead(getFormItemInt(F("events")), TOUCH_FLAGS_SEND_XY)); + bitWrite(lSettings, TOUCH_FLAGS_SEND_Z, bitRead(getFormItemInt(F("events")), TOUCH_FLAGS_SEND_Z)); + bitWrite(lSettings, TOUCH_FLAGS_SEND_OBJECTNAME, bitRead(getFormItemInt(F("events")), TOUCH_FLAGS_SEND_OBJECTNAME)); + bitWrite(lSettings, TOUCH_FLAGS_ROTATION_FLIPPED, isFormItemChecked(F("rotation_flipped"))); + bitWrite(lSettings, TOUCH_FLAGS_DEDUPLICATE, isFormItemChecked(F("deduplicate"))); + bitWrite(lSettings, TOUCH_FLAGS_INIT_OBJECTEVENT, isFormItemChecked(F("init_objectevent"))); # ifdef TOUCH_USE_EXTENDED_TOUCH - set8BitToUL(lSettings, TOUCH_FLAGS_INITIAL_GROUP, getFormItemInt(F("tch_initial_group"))); // Button group - bitWrite(lSettings, TOUCH_FLAGS_DRAWBTN_VIA_RULES, isFormItemChecked(F("tch_via_rules"))); - bitWrite(lSettings, TOUCH_FLAGS_AUTO_PAGE_ARROWS, isFormItemChecked(F("tch_page_buttons"))); - bitWrite(lSettings, TOUCH_FLAGS_PGUP_BELOW_MENU, isFormItemChecked(F("tch_page_below"))); + set8BitToUL(lSettings, TOUCH_FLAGS_INITIAL_GROUP, getFormItemInt(F("initial_group"))); // Button group + bitWrite(lSettings, TOUCH_FLAGS_DRAWBTN_VIA_RULES, isFormItemChecked(F("via_rules"))); + bitWrite(lSettings, TOUCH_FLAGS_AUTO_PAGE_ARROWS, isFormItemChecked(F("page_buttons"))); + bitWrite(lSettings, TOUCH_FLAGS_PGUP_BELOW_MENU, isFormItemChecked(F("page_below"))); # endif // ifdef TOUCH_USE_EXTENDED_TOUCH - config += getFormItemInt(F("tch_use_calibration")); // First value should NEVER be empty, or parseString() wil get confused + config += getFormItemInt(F("use_calibration")); // First value should NEVER be empty, or parseString() wil get confused config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(isFormItemChecked(F("tch_log_calibration")) ? 1 : 0); + config += toStringNoZero(isFormItemChecked(F("log_calibration")) ? 1 : 0); config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemInt(F("tch_cal_tl_x"))); + config += toStringNoZero(getFormItemInt(F("cal_tl_x"))); config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemInt(F("tch_cal_tl_y"))); + config += toStringNoZero(getFormItemInt(F("cal_tl_y"))); config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemInt(F("tch_cal_br_x"))); + config += toStringNoZero(getFormItemInt(F("cal_br_x"))); config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemInt(F("tch_cal_br_y"))); + config += toStringNoZero(getFormItemInt(F("cal_br_y"))); config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemInt(F("tch_debounce"))); + config += toStringNoZero(getFormItemInt(F("debounce"))); config += TOUCH_SETTINGS_SEPARATOR; config += ull2String(lSettings); # ifdef TOUCH_USE_EXTENDED_TOUCH From 46787dde30c15398701c8aaf30605b88273426f5 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Fri, 10 Jun 2022 22:13:34 +0200 Subject: [PATCH 027/113] [P123] Strip unneeded prefix from page variables --- src/_P123_FT62x6Touch.ino | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index b5b093d3ad..2b76c2d3cf 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -6,6 +6,7 @@ /** * Changelog: + * 2022-06-10 tonhuisman: Remove p123_ prefixes on Settings variables * 2022-06-06 tonhuisman: Move PLUGIN_WRITE handling mostly to ESPEasy_TouchHandler (only rot and flip subcommands remain) * Move PLUGIN_GET_CONFIG_VALUE handling to ESPEasy_TouchHandler * 2022-05-29 tonhuisman: Extend enable,disable subcommands to support a list of objects @@ -105,7 +106,7 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) { addRowLabel(F("Display task")); - addTaskSelect(F("p123_task"), P123_CONFIG_DISPLAY_TASK); + addTaskSelect(F("task"), P123_CONFIG_DISPLAY_TASK); addFormNote(F("Screen Width, Heigth, Rotation & Color-depth will be fetched from the Display task if possible.")); } @@ -122,19 +123,19 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) if (width_ == 0) { width_ = P123_TS_X_RES; // default value } - addFormNumericBox(F("Screen Width (px) (x)"), F("p123_width"), width_, 1, 65535); + addFormNumericBox(F("Screen Width (px) (x)"), F("width"), width_, 1, 65535); if (height_ == 0) { height_ = P123_TS_Y_RES; // default value } - addFormNumericBox(F("Screen Height (px) (y)"), F("p123_height"), height_, 1, 65535); + addFormNumericBox(F("Screen Height (px) (y)"), F("height"), height_, 1, 65535); - AdaGFXFormRotation(F("p123_rotate"), rotation_); + AdaGFXFormRotation(F("rotate"), rotation_); - AdaGFXFormColorDepth(F("p123_colordepth"), P123_COLOR_DEPTH, (colorDepth_ == 0)); + AdaGFXFormColorDepth(F("colordepth"), P123_COLOR_DEPTH, (colorDepth_ == 0)); - addFormNumericBox(F("Touch minimum pressure"), F("p123_treshold"), P123_CONFIG_TRESHOLD, 0, 255); + addFormNumericBox(F("Touch minimum pressure"), F("treshold"), P123_CONFIG_TRESHOLD, 0, 255); { P123_data_struct *P123_data = new (std::nothrow) P123_data_struct(); @@ -155,13 +156,13 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SAVE: { P123_CONFIG_DISPLAY_PREV = P123_CONFIG_DISPLAY_TASK; - P123_CONFIG_TRESHOLD = getFormItemInt(F("p123_treshold")); - P123_CONFIG_DISPLAY_TASK = getFormItemInt(F("p123_task")); - P123_CONFIG_ROTATION = getFormItemInt(F("p123_rotate")); - P123_CONFIG_X_RES = getFormItemInt(F("p123_width")); - P123_CONFIG_Y_RES = getFormItemInt(F("p123_height")); + P123_CONFIG_TRESHOLD = getFormItemInt(F("treshold")); + P123_CONFIG_DISPLAY_TASK = getFormItemInt(F("task")); + P123_CONFIG_ROTATION = getFormItemInt(F("rotate")); + P123_CONFIG_X_RES = getFormItemInt(F("width")); + P123_CONFIG_Y_RES = getFormItemInt(F("height")); - int colorDepth = getFormItemInt(F("p123_colordepth"), -1); + int colorDepth = getFormItemInt(F("colordepth"), -1); if (colorDepth != -1) { P123_COLOR_DEPTH = colorDepth; From dfcfd585dc85b310cf3324e947177116e79cbbb5 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 11 Jun 2022 12:48:20 +0200 Subject: [PATCH 028/113] [TouchHandler] Optimizations (as suggested and more) --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 51 ++++++++++++------------ 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 3bd7aa74f4..6ac406e269 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -655,11 +655,13 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { F("Objectnames, X, Y and Z") # endif // ifdef TOUCH_USE_EXTENDED_TOUCH }; - int optionValues3[TOUCH_EVENTS_OPTIONS] = { 0, 1, 3, 4, 5, 7 }; // Already used as a bitmap! + const int optionValues3[TOUCH_EVENTS_OPTIONS] = { 0, 1, 3, 4, 5, 7 }; // Already used as a bitmap! addFormSelector(F("Events"), F("events"), TOUCH_EVENTS_OPTIONS, options3, optionValues3, choice3); addFormCheckBox(F("Draw buttons when started"), F("init_objectevent"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)); + # ifndef LIMIT_BUILD_SIZE addFormNote(F("Needs Objectnames 'Events' to be enabled.")); + # endif // ifndef LIMIT_BUILD_SIZE } addFormCheckBox(F("Prevent duplicate events"), F("deduplicate"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_DEDUPLICATE)); @@ -675,7 +677,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { { const __FlashStringHelper *noYesOptions[2] = { F("No"), F("Yes") }; - int noYesOptionValues[2] = { 0, 1 }; + const int noYesOptionValues[2] = { 0, 1 }; addFormSelector(F("Calibrate to screen resolution"), F("use_calibration"), 2, @@ -1179,11 +1181,12 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { # endif // ifdef TOUCH_USE_EXTENDED_TOUCH config.reserve(80); - uint32_t lSettings = 0u; + uint32_t lSettings = 0u; + const int eventsValue = getFormItemInt(F("events")); - bitWrite(lSettings, TOUCH_FLAGS_SEND_XY, bitRead(getFormItemInt(F("events")), TOUCH_FLAGS_SEND_XY)); - bitWrite(lSettings, TOUCH_FLAGS_SEND_Z, bitRead(getFormItemInt(F("events")), TOUCH_FLAGS_SEND_Z)); - bitWrite(lSettings, TOUCH_FLAGS_SEND_OBJECTNAME, bitRead(getFormItemInt(F("events")), TOUCH_FLAGS_SEND_OBJECTNAME)); + bitWrite(lSettings, TOUCH_FLAGS_SEND_XY, bitRead(eventsValue, TOUCH_FLAGS_SEND_XY)); + bitWrite(lSettings, TOUCH_FLAGS_SEND_Z, bitRead(eventsValue, TOUCH_FLAGS_SEND_Z)); + bitWrite(lSettings, TOUCH_FLAGS_SEND_OBJECTNAME, bitRead(eventsValue, TOUCH_FLAGS_SEND_OBJECTNAME)); bitWrite(lSettings, TOUCH_FLAGS_ROTATION_FLIPPED, isFormItemChecked(F("rotation_flipped"))); bitWrite(lSettings, TOUCH_FLAGS_DEDUPLICATE, isFormItemChecked(F("deduplicate"))); bitWrite(lSettings, TOUCH_FLAGS_INIT_OBJECTEVENT, isFormItemChecked(F("init_objectevent"))); @@ -1265,32 +1268,28 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { bitWrite(flags, TOUCH_OBJECT_FLAG_ENABLED, isFormItemChecked(getPluginCustomArgName(objectNr + 0))); // Enabled bitWrite(flags, TOUCH_OBJECT_FLAG_INVERTED, isFormItemChecked(getPluginCustomArgName(objectNr + 700))); // Inverted # ifdef TOUCH_USE_EXTENDED_TOUCH - uint32_t groupFlags = 0u; - uint8_t buttonType = getFormItemInt(getPluginCustomArgName(objectNr + 800)); - set4BitToUL(flags, TOUCH_OBJECT_FLAG_BUTTONTYPE, buttonType); // Buttontype - set4BitToUL(flags, TOUCH_OBJECT_FLAG_BUTTONALIGN, getFormItemInt(getPluginCustomArgName(objectNr + 900)) >> 4); // Button layout - bitWrite(flags, TOUCH_OBJECT_FLAG_BUTTON, (static_cast(buttonType) != Button_type_e::None)); // On/Off button - // uint8_t buttonAction = getFormItemInt(getPluginCustomArgName(objectNr + 2000)); - // uint8_t buttonSelectGroup = ); - set4BitToUL(groupFlags, TOUCH_OBJECT_GROUP_ACTION, getFormItemInt(getPluginCustomArgName(objectNr + 2000))); // ButtonAction - set8BitToUL(groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP, getFormItemInt(getPluginCustomArgName(objectNr + 2100))); // ActionGroup - // uint8_t fontScale = getFormItemInt(getPluginCustomArgName(objectNr + 1200)); - set4BitToUL(flags, TOUCH_OBJECT_FLAG_FONTSCALE, getFormItemInt(getPluginCustomArgName(objectNr + 1200))); // Font scaling - uint8_t buttonGroup = getFormItemInt(getPluginCustomArgName(objectNr + 1600)); - set8BitToUL(flags, TOUCH_OBJECT_FLAG_GROUP, buttonGroup); // Button group + uint32_t groupFlags = 0u; + const uint8_t buttonType = getFormItemIntCustomArgName(objectNr + 800); + set4BitToUL(flags, TOUCH_OBJECT_FLAG_BUTTONTYPE, buttonType); // Buttontype + set4BitToUL(flags, TOUCH_OBJECT_FLAG_BUTTONALIGN, getFormItemIntCustomArgName(objectNr + 900) >> 4); // Button layout + bitWrite(flags, TOUCH_OBJECT_FLAG_BUTTON, (static_cast(buttonType) != Button_type_e::None)); // On/Off button + set4BitToUL(groupFlags, TOUCH_OBJECT_GROUP_ACTION, getFormItemIntCustomArgName(objectNr + 2000)); // ButtonAction + set8BitToUL(groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP, getFormItemIntCustomArgName(objectNr + 2100)); // ActionGroup + set4BitToUL(flags, TOUCH_OBJECT_FLAG_FONTSCALE, getFormItemIntCustomArgName(objectNr + 1200)); // Font scaling + set8BitToUL(flags, TOUCH_OBJECT_FLAG_GROUP, getFormItemIntCustomArgName(objectNr + 1600)); // Button group # else // ifdef TOUCH_USE_EXTENDED_TOUCH - bitWrite(flags, TOUCH_OBJECT_FLAG_BUTTON, isFormItemChecked(getPluginCustomArgName(objectNr + 600))); // On/Off button + bitWrite(flags, TOUCH_OBJECT_FLAG_BUTTON, isFormItemChecked(getPluginCustomArgName(objectNr + 600))); // On/Off button # endif // ifdef TOUCH_USE_EXTENDED_TOUCH - config += ull2String(flags); // Flags + config += ull2String(flags); // Flags config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemInt(getPluginCustomArgName(objectNr + 200))); // Top x + config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 200)); // Top x config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemInt(getPluginCustomArgName(objectNr + 300))); // Top y + config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 300)); // Top y config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemInt(getPluginCustomArgName(objectNr + 400))); // Bottom x + config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 400)); // Bottom x config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemInt(getPluginCustomArgName(objectNr + 500))); // Bottom y + config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 500)); // Bottom y # ifdef TOUCH_USE_EXTENDED_TOUCH config += TOUCH_SETTINGS_SEPARATOR; @@ -1324,7 +1323,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { String endZero; // Trim off and 0 from the end endZero += TOUCH_SETTINGS_SEPARATOR; endZero += '0'; - uint8_t endZeroLen = endZero.length(); + const uint8_t endZeroLen = endZero.length(); while (!config.isEmpty() && (config.endsWith(endZero) || config[config.length() - 1] == TOUCH_SETTINGS_SEPARATOR)) { if (config[config.length() - 1] == TOUCH_SETTINGS_SEPARATOR) { From 52c589f2922cd8364272eacb9a2223a9e3f6319e Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 14 Jun 2022 22:24:12 +0200 Subject: [PATCH 029/113] [P123] Fix some spelling issues, code optimizations --- src/_P123_FT62x6Touch.ino | 75 +++++++++++++--------- src/src/PluginStructs/P123_data_struct.cpp | 2 +- src/src/PluginStructs/P123_data_struct.h | 12 ++-- 3 files changed, 53 insertions(+), 36 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index 2b76c2d3cf..58a9582cfd 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -30,7 +30,7 @@ * ------------------- * touch,rot,<0..3> : Set rotation to 0(0), 90(1), 180(2), 270(3) degrees * touch,flip,<0|1> : Set rotation normal(0) or flipped by 180 degrees(1) - * + * * Other commands: see ESPEasy_TouchHandler.h */ @@ -93,6 +93,7 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_SET_DEFAULTS: { P123_CONFIG_DISPLAY_TASK = event->TaskIndex; // Preselect current task to avoid pointing to Task 1 by default + P123_CONFIG_THRESHOLD = P123_TS_THRESHOLD; P123_CONFIG_ROTATION = P123_TS_ROTATION; P123_CONFIG_X_RES = P123_TS_X_RES; P123_CONFIG_Y_RES = P123_TS_Y_RES; @@ -106,7 +107,7 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) { addRowLabel(F("Display task")); - addTaskSelect(F("task"), P123_CONFIG_DISPLAY_TASK); + addTaskSelect(F("dsptask"), P123_CONFIG_DISPLAY_TASK); addFormNote(F("Screen Width, Heigth, Rotation & Color-depth will be fetched from the Display task if possible.")); } @@ -123,31 +124,40 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) if (width_ == 0) { width_ = P123_TS_X_RES; // default value } - addFormNumericBox(F("Screen Width (px) (x)"), F("width"), width_, 1, 65535); + addFormNumericBox(F("Screen Width (px) (x)"), F("xres"), width_, 1, 65535); if (height_ == 0) { height_ = P123_TS_Y_RES; // default value } - addFormNumericBox(F("Screen Height (px) (y)"), F("height"), height_, 1, 65535); + addFormNumericBox(F("Screen Height (px) (y)"), F("yres"), height_, 1, 65535); AdaGFXFormRotation(F("rotate"), rotation_); AdaGFXFormColorDepth(F("colordepth"), P123_COLOR_DEPTH, (colorDepth_ == 0)); - addFormNumericBox(F("Touch minimum pressure"), F("treshold"), P123_CONFIG_TRESHOLD, 0, 255); + addFormNumericBox(F("Touch minimum pressure"), F("threshold"), P123_CONFIG_THRESHOLD, 0, 255); { - P123_data_struct *P123_data = new (std::nothrow) P123_data_struct(); + P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); + bool deleteP123_data = false; if (nullptr == P123_data) { - return success; + P123_data = new (std::nothrow) P123_data_struct(); + deleteP123_data = true; } - P123_data->loadTouchObjects(event); - P123_data->plugin_webform_load(event); + if (nullptr != P123_data) { + if (deleteP123_data) { + P123_data->loadTouchObjects(event); + } - delete P123_data; + P123_data->plugin_webform_load(event); + + if (deleteP123_data) { + delete P123_data; + } + } } success = true; break; @@ -156,27 +166,35 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SAVE: { P123_CONFIG_DISPLAY_PREV = P123_CONFIG_DISPLAY_TASK; - P123_CONFIG_TRESHOLD = getFormItemInt(F("treshold")); - P123_CONFIG_DISPLAY_TASK = getFormItemInt(F("task")); + P123_CONFIG_THRESHOLD = getFormItemInt(F("threshold")); + P123_CONFIG_DISPLAY_TASK = getFormItemInt(F("dsptask")); P123_CONFIG_ROTATION = getFormItemInt(F("rotate")); - P123_CONFIG_X_RES = getFormItemInt(F("width")); - P123_CONFIG_Y_RES = getFormItemInt(F("height")); + P123_CONFIG_X_RES = getFormItemInt(F("xres")); + P123_CONFIG_Y_RES = getFormItemInt(F("yres")); - int colorDepth = getFormItemInt(F("colordepth"), -1); + const int colorDepth = getFormItemInt(F("colordepth"), -1); if (colorDepth != -1) { P123_COLOR_DEPTH = colorDepth; } - P123_data_struct *P123_data = new (std::nothrow) P123_data_struct(); + { + P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); + bool deleteP123_data = false; - if (nullptr == P123_data) { - return success; // Save other settings even though this didn't initialize properly - } + if (nullptr == P123_data) { + P123_data = new (std::nothrow) P123_data_struct(); + deleteP123_data = true; + } - success = P123_data->plugin_webform_save(event); + if (nullptr != P123_data) { + success = P123_data->plugin_webform_save(event); - delete P123_data; + if (deleteP123_data) { + delete P123_data; + } + } + } break; } @@ -205,10 +223,10 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) { P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); - if (nullptr == P123_data) { - return success; + if (nullptr != P123_data) { + success = P123_data->plugin_write(event, string); } - success = P123_data->plugin_write(event, string); + break; } @@ -216,20 +234,19 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) { P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); - if (nullptr == P123_data) { - return success; + if (nullptr != P123_data) { + success = P123_data->plugin_fifty_per_second(event); } - success = P123_data->plugin_fifty_per_second(event); - break; } + case PLUGIN_GET_CONFIG_VALUE: { P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); if (nullptr != P123_data) { - return P123_data->plugin_get_config_value(event, string); + success = P123_data->plugin_get_config_value(event, string); } break; } diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index e98201342d..d9cd609ec7 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -58,7 +58,7 @@ bool P123_data_struct::init(struct EventStruct *event) { } if (isInitialized()) { - touchscreen->begin(P123_CONFIG_TRESHOLD); + touchscreen->begin(P123_CONFIG_THRESHOLD); touchHandler->init(event); diff --git a/src/src/PluginStructs/P123_data_struct.h b/src/src/PluginStructs/P123_data_struct.h index 590657f5fb..607e705380 100644 --- a/src/src/PluginStructs/P123_data_struct.h +++ b/src/src/PluginStructs/P123_data_struct.h @@ -17,7 +17,7 @@ # define P123_CONFIG_DISPLAY_TASK PCONFIG(0) # define P123_COLOR_DEPTH PCONFIG_LONG(1) -# define P123_CONFIG_TRESHOLD PCONFIG(1) +# define P123_CONFIG_THRESHOLD PCONFIG(1) # define P123_CONFIG_ROTATION PCONFIG(2) # define P123_CONFIG_X_RES PCONFIG(3) # define P123_CONFIG_Y_RES PCONFIG(4) @@ -25,13 +25,13 @@ # define P123_CONFIG_DISPLAY_PREV PCONFIG(7) // Default settings values -# define P123_TS_TRESHOLD 40 // Treshold before the value is registered as a proper touch -# define P123_TS_ROTATION 0 // Rotation 0-3 = 0/90/180/270 degrees -# define P123_TS_X_RES 320 // Pixels, should match with the screen it is mounted on +# define P123_TS_THRESHOLD 40 // Threshold before the value is registered as a proper touch +# define P123_TS_ROTATION 0 // Rotation 0-3 = 0/90/180/270 degrees +# define P123_TS_X_RES 320 // Pixels, should match with the screen it is mounted on # define P123_TS_Y_RES 480 -# define P123_TOUCH_X_NATIVE 320 // Native touchscreen resolution -# define P123_TOUCH_Y_NATIVE 480 +# define P123_TOUCH_X_NATIVE P123_TS_X_RES // Native touchscreen resolution, same as display resolution +# define P123_TOUCH_Y_NATIVE P123_TS_Y_RES # define P123_ROTATION_0 0 # define P123_ROTATION_90 1 From b10f6326ba17e4e11e20c51c441eed711e3ba7ed Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 16 Jun 2022 19:38:48 +0200 Subject: [PATCH 030/113] [P123] Minor optimizations --- src/_P123_FT62x6Touch.ino | 4 ++-- src/src/PluginStructs/P123_data_struct.cpp | 10 +++++----- src/src/PluginStructs/P123_data_struct.h | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index 58a9582cfd..eaaf9e1548 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -28,8 +28,8 @@ /** * Commands supported: * ------------------- - * touch,rot,<0..3> : Set rotation to 0(0), 90(1), 180(2), 270(3) degrees - * touch,flip,<0|1> : Set rotation normal(0) or flipped by 180 degrees(1) + * touch,rot,<0..3> : Set rotation to 0(0), 90(1), 180(2), 270(3) degrees + * touch,flip,<0|1> : Set rotation normal(0) or flipped by 180 degrees(1) * * Other commands: see ESPEasy_TouchHandler.h */ diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index d9cd609ec7..5cde60b113 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -84,7 +84,7 @@ void P123_data_struct::displayButtonGroup(struct EventStruct *event, * (Re)Display a button */ bool P123_data_struct::displayButton(struct EventStruct *event, - int8_t buttonNr, + const int8_t & buttonNr, int16_t buttonGroup, int8_t mode) { return touchHandler->displayButton(event, buttonNr, buttonGroup, mode); @@ -295,10 +295,10 @@ void P123_data_struct::setRotationFlipped(bool flipped) { * The smallest matching surface is selected if multiple objects overlap. * Returns state, and sets selectedObjectName to the best matching object */ -bool P123_data_struct::isValidAndTouchedTouchObject(int16_t x, - int16_t y, - String& selectedObjectName, - int8_t& selectedObjectIndex) { +bool P123_data_struct::isValidAndTouchedTouchObject(const int16_t& x, + const int16_t& y, + String & selectedObjectName, + int8_t & selectedObjectIndex) { return touchHandler->isValidAndTouchedTouchObject(x, y, selectedObjectName, selectedObjectIndex); } diff --git a/src/src/PluginStructs/P123_data_struct.h b/src/src/PluginStructs/P123_data_struct.h index 607e705380..0540a981fb 100644 --- a/src/src/PluginStructs/P123_data_struct.h +++ b/src/src/PluginStructs/P123_data_struct.h @@ -66,10 +66,10 @@ struct P123_data_struct : public PluginTaskData_base void setRotation(uint8_t n); void setRotationFlipped(bool _flipped); - bool isValidAndTouchedTouchObject(int16_t x, - int16_t y, - String& selectedObjectName, - int8_t& selectedObjectIndex); + bool isValidAndTouchedTouchObject(const int16_t& x, + const int16_t& y, + String & selectedObjectName, + int8_t & selectedObjectIndex); int8_t getTouchObjectIndex(struct EventStruct *event, const String & touchObject, bool isButton = false); @@ -95,7 +95,7 @@ struct P123_data_struct : public PluginTaskData_base int16_t buttonGroup, int8_t mode = 0); bool displayButton(struct EventStruct *event, - int8_t buttonNr, + const int8_t & buttonNr, int16_t buttonGroup = -1, int8_t mode = 0); From b981ca9720b7a68775933bea5cfe570d8fd7050e Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Fri, 5 Aug 2022 20:50:54 +0200 Subject: [PATCH 031/113] [AdaGFX] Reduce build some more for LIMIT_BUILD_SIZE --- src/src/Helpers/AdafruitGFX_helper.cpp | 43 +++++++++++++++++++------- src/src/Helpers/AdafruitGFX_helper.h | 6 ++-- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index 41af543c3d..9ba25fd7e5 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -242,9 +242,24 @@ void AdaGFXFormForeAndBackColors(const __FlashStringHelper *foregroundId, AdaGFXColorDepth colorDepth) { String color = AdaGFXcolorToString(foregroundColor, colorDepth); - addFormTextBox(F("Foreground color"), foregroundId, color, 11); + AdaGFXHtmlColorDepthDataList(F("adagfxFGBGcolors"), colorDepth); + addRowLabel(F("Foreground color")); + addTextBox(foregroundId, color, 11, false, false, + EMPTY_STRING, EMPTY_STRING + # ifdef TOUCH_USE_TOOLTIPS + , F("Foreground color") + # endif // ifdef TOUCH_USE_TOOLTIPS + , F("adagfxFGBGcolors") + ); color = AdaGFXcolorToString(backgroundColor, colorDepth); - addFormTextBox(F("Background color"), backgroundId, color, 11); + addRowLabel(F("Background color")); + addTextBox(backgroundId, color, 11, false, false, + EMPTY_STRING, EMPTY_STRING + # ifdef TOUCH_USE_TOOLTIPS + , F("Background color") + # endif // ifdef TOUCH_USE_TOOLTIPS + , F("adagfxFGBGcolors") + ); # ifndef LIMIT_BUILD_SIZE addFormNote(F("Use Color name, '#RGB565' (# + 1..4 hex nibbles) or '#RRGGBB' (# + 6 hex nibbles RGB color).")); addFormNote(F("NB: Colors stored as RGB565 value!")); @@ -656,12 +671,15 @@ bool AdafruitGFX_helper::processCommand(const String& string) { if ((nullptr == _display) || _trigger.isEmpty()) { return success; } - String cmd = parseString(string, 1); // lower case - String subcommand = parseString(string, 2); - uint16_t res_x = _res_x; - uint16_t res_y = _res_y; - uint16_t _xo = 0; - uint16_t _yo = 0; + String cmd = parseString(string, 1); // lower case + String subcommand = parseString(string, 2); + + # if ADAGFX_ENABLE_FRAMED_WINDOW || ADAGFX_ARGUMENT_VALIDATION + uint16_t res_x = _res_x; + uint16_t res_y = _res_y; + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW || ADAGFX_ARGUMENT_VALIDATION + uint16_t _xo = 0; + uint16_t _yo = 0; # if ADAGFX_ENABLE_FRAMED_WINDOW getWindowLimits(res_x, res_y); @@ -1806,11 +1824,14 @@ void AdafruitGFX_helper::printText(const char *string, int16_t oTop = 0; int16_t oBottom = 0; int16_t oLeft = 0; - uint16_t res_x = _res_x; - uint16_t res_y = _res_y; uint16_t xOffset = 0; - uint16_t yOffset = 0; String newString = string; + uint16_t res_x = _res_x; + + # if ADAGFX_ENABLE_FRAMED_WINDOW + uint16_t res_y = _res_y; + uint16_t yOffset = 0; + # endif // if ADAGFX_ENABLE_FRAMED_WINDOW # if ADAGFX_ENABLE_FRAMED_WINDOW getWindowLimits(res_x, res_y); diff --git a/src/src/Helpers/AdafruitGFX_helper.h b/src/src/Helpers/AdafruitGFX_helper.h index b46f2a84c0..83a6d84f47 100644 --- a/src/src/Helpers/AdafruitGFX_helper.h +++ b/src/src/Helpers/AdafruitGFX_helper.h @@ -127,9 +127,9 @@ // # ifdef ADAGFX_ENABLE_BUTTON_DRAW // # undef ADAGFX_ENABLE_BUTTON_DRAW // # endif // ifdef ADAGFX_ENABLE_BUTTON_DRAW -// # ifdef ADAGFX_ENABLE_FRAMED_WINDOW -// # undef ADAGFX_ENABLE_FRAMED_WINDOW -// # endif // ifdef ADAGFX_ENABLE_FRAMED_WINDOW +# ifdef ADAGFX_ENABLE_FRAMED_WINDOW +# undef ADAGFX_ENABLE_FRAMED_WINDOW +# endif // ifdef ADAGFX_ENABLE_FRAMED_WINDOW // # ifdef ADAGFX_ENABLE_GET_CONFIG_VALUE // # undef ADAGFX_ENABLE_GET_CONFIG_VALUE // # endif // ifdef ADAGFX_ENABLE_GET_CONFIG_VALUE From 305b3fa6341d67970c849debb11110e4e2e76792 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 13 Aug 2022 16:41:08 +0200 Subject: [PATCH 032/113] [P123] Small size improvement --- src/_P123_FT62x6Touch.ino | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index eaaf9e1548..d405e17c4a 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -108,7 +108,9 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) { addRowLabel(F("Display task")); addTaskSelect(F("dsptask"), P123_CONFIG_DISPLAY_TASK); + #ifndef LIMIT_BUILD_SIZE addFormNote(F("Screen Width, Heigth, Rotation & Color-depth will be fetched from the Display task if possible.")); + #endif // ifndef LIMIT_BUILD_SIZE } uint16_t width_ = P123_CONFIG_X_RES; From 84b50a2700d706236fd8f658b32782e26fb8a75b Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 13 Aug 2022 23:01:42 +0200 Subject: [PATCH 033/113] [TouchHandler] Replace _ in captions and object names by a space, and reverse, might save some settings size --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 43 +++++++++++++++++------- src/src/Helpers/ESPEasy_TouchHandler.h | 3 ++ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 6ac406e269..b0381b922a 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -1005,8 +1005,10 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { , F("adagfx65kcolors") ); html_TD(); // ON Caption + parsed = TouchObjects[objectNr].captionOn; + parsed.replace('_', ' '); addTextBox(getPluginCustomArgName(objectNr + 1300), - TouchObjects[objectNr].captionOn, + parsed, TOUCH_MaxCaptionNameLength, false, false, @@ -1107,8 +1109,10 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { , F("adagfx65kcolors") ); html_TD(); // OFF Caption + parsed = TouchObjects[objectNr].captionOff; + parsed.replace('_', ' '); addTextBox(getPluginCustomArgName(objectNr + 1400), - TouchObjects[objectNr].captionOff, + parsed, TOUCH_MaxCaptionNameLength, false, false, @@ -1302,9 +1306,13 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { colorInput = webArg(getPluginCustomArgName(objectNr + 1500)); // Color caption config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true)); config += TOUCH_SETTINGS_SEPARATOR; // Caption ON - config += wrapWithQuotesIfContainsParameterSeparatorChar(webArg(getPluginCustomArgName(objectNr + 1300))); + colorInput = webArg(getPluginCustomArgName(objectNr + 1300)); + colorInput.replace(' ', '_'); // Replace spaces by '_', often cheaper than 2 quotes... + config += wrapWithQuotesIfContainsParameterSeparatorChar(colorInput); config += TOUCH_SETTINGS_SEPARATOR; // Caption OFF - config += wrapWithQuotesIfContainsParameterSeparatorChar(webArg(getPluginCustomArgName(objectNr + 1400))); + colorInput = webArg(getPluginCustomArgName(objectNr + 1400)); + colorInput.replace(' ', '_'); // Replace spaces by '_', often cheaper than 2 quotes... + config += wrapWithQuotesIfContainsParameterSeparatorChar(colorInput); config += TOUCH_SETTINGS_SEPARATOR; colorInput = webArg(getPluginCustomArgName(objectNr + 1700)); // Color Border config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true)); @@ -1717,14 +1725,25 @@ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, eventCommand += ','; // (12 = Font scaling) eventCommand += get4BitFromUL(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_FONTSCALE); eventCommand += ','; // (13 = ON caption, default=object name) - eventCommand += wrapWithQuotesIfContainsParameterSeparatorChar(TouchObjects[objectIndex].captionOn.isEmpty() ? - TouchObjects[objectIndex].objectName : - TouchObjects[objectIndex].captionOn); - eventCommand += ','; // (14 = OFF caption) - eventCommand += wrapWithQuotesIfContainsParameterSeparatorChar(TouchObjects[objectIndex].captionOff.isEmpty() ? - TouchObjects[objectIndex].objectName : - TouchObjects[objectIndex].captionOff); - eventCommand += ','; // (15 = Border color) + String _capt; + + if (TouchObjects[objectIndex].captionOn.isEmpty()) { + _capt = TouchObjects[objectIndex].objectName; + } else { + _capt = TouchObjects[objectIndex].captionOn; + } + _capt.replace('_', ' '); // Replace all '_' by space + eventCommand += wrapWithQuotesIfContainsParameterSeparatorChar(_capt); + eventCommand += ','; // (14 = OFF caption) + + if (TouchObjects[objectIndex].captionOff.isEmpty()) { + _capt = TouchObjects[objectIndex].objectName; + } else { + _capt = TouchObjects[objectIndex].captionOff; + } + _capt.replace('_', ' '); // Replace all '_' by space + eventCommand += wrapWithQuotesIfContainsParameterSeparatorChar(_capt); + eventCommand += ','; // (15 = Border color) eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorBorder == 0 ? Touch_Settings.colorBorder : TouchObjects[objectIndex].colorBorder, diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index 14c4fa79d7..c4c531fca2 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -11,6 +11,9 @@ /***** * Changelog: + * 2022-08-13 tonhuisman: Replace _ om object name and on/off captions by space, to ease the use of object name as caption + * On save, any spaces in captions are replaced by _ to avoid using 2 quotes around the value. + * This implies that no underscores wil be shown in captions! * 2022-06-09 tonhuisman: Change method arguments to const-by-reference where possible for improved compile-time checks * 2022-06-06 tonhuisman: Move PLUGIN_WRITE handling from P123 * Move PLUGIN_GET_CONFIG_VALUE handling from P123. From a422da9b7f123355bbf41c4c3eb6aae476dd8a40 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 15 Aug 2022 11:45:20 +0200 Subject: [PATCH 034/113] [UI] Add class table.multi2row for alternating color per 2 rows --- src/src/Static/WebStaticData.h | 2 +- static/espeasy_default.css | 14 +++++++++++++- static/espeasy_default.min.css | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/src/Static/WebStaticData.h b/src/src/Static/WebStaticData.h index a2900f83a4..452782d45c 100644 --- a/src/src/Static/WebStaticData.h +++ b/src/src/Static/WebStaticData.h @@ -85,7 +85,7 @@ static const char DATA_GITHUB_CLIPBOARD_JS[] PROGMEM = {0x66,0x75,0x6e,0x63,0x74 #endif #if defined(WEBSERVER_CSS) && !defined(WEBSERVER_EMBED_CUSTOM_CSS) -static const char DATA_ESPEASY_DEFAULT_MIN_CSS[] PROGMEM = {0x2e,0x63,0x6c,0x6f,0x73,0x65,0x62,0x74,0x6e,0x2c,0x68,0x31,0x2c,0x68,0x32,0x2c,0x68,0x33,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x77,0x65,0x69,0x67,0x68,0x74,0x3a,0x37,0x30,0x30,0x7d,0x68,0x31,0x2c,0x68,0x36,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x37,0x44,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x63,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x3a,0x27,0x27,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2c,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x74,0x65,0x78,0x74,0x2d,0x64,0x65,0x63,0x6f,0x72,0x61,0x74,0x69,0x6f,0x6e,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x64,0x69,0x76,0x5f,0x6c,0x2c,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x66,0x6c,0x6f,0x61,0x74,0x3a,0x6c,0x65,0x66,0x74,0x7d,0x2e,0x63,0x6c,0x6f,0x73,0x65,0x62,0x74,0x6e,0x2c,0x2e,0x64,0x69,0x76,0x5f,0x72,0x7b,0x66,0x6c,0x6f,0x61,0x74,0x3a,0x72,0x69,0x67,0x68,0x74,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x7d,0x2a,0x7b,0x62,0x6f,0x78,0x2d,0x73,0x69,0x7a,0x69,0x6e,0x67,0x3a,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x62,0x6f,0x78,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x66,0x61,0x6d,0x69,0x6c,0x79,0x3a,0x73,0x61,0x6e,0x73,0x2d,0x73,0x65,0x72,0x69,0x66,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x32,0x70,0x74,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x30,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x30,0x7d,0x68,0x31,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x36,0x70,0x74,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x38,0x70,0x78,0x20,0x30,0x7d,0x68,0x32,0x2c,0x68,0x33,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x32,0x70,0x74,0x7d,0x68,0x32,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x46,0x46,0x46,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x30,0x20,0x2d,0x34,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x36,0x70,0x78,0x7d,0x68,0x33,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x45,0x45,0x45,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x31,0x36,0x70,0x78,0x20,0x2d,0x34,0x70,0x78,0x20,0x30,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x7d,0x68,0x36,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x30,0x70,0x74,0x7d,0x63,0x6f,0x64,0x65,0x2c,0x6b,0x62,0x64,0x2c,0x70,0x72,0x65,0x2c,0x73,0x61,0x6d,0x70,0x2c,0x74,0x74,0x2c,0x78,0x6d,0x70,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x66,0x61,0x6d,0x69,0x6c,0x79,0x3a,0x6d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x2c,0x6d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x65,0x6d,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x37,0x44,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x34,0x70,0x78,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x46,0x46,0x46,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x34,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x20,0x31,0x36,0x70,0x78,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x68,0x65,0x6c,0x70,0x2c,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2c,0x69,0x6e,0x70,0x75,0x74,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x67,0x72,0x61,0x79,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x73,0x74,0x79,0x6c,0x65,0x3a,0x73,0x6f,0x6c,0x69,0x64,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x70,0x78,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x6c,0x69,0x6e,0x6b,0x2e,0x77,0x69,0x64,0x65,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x69,0x6e,0x6c,0x69,0x6e,0x65,0x2d,0x62,0x6c,0x6f,0x63,0x6b,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x25,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x6c,0x69,0x6e,0x6b,0x2e,0x72,0x65,0x64,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x72,0x65,0x64,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x68,0x65,0x6c,0x70,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x30,0x25,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x32,0x70,0x78,0x20,0x34,0x70,0x78,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2c,0x69,0x6e,0x70,0x75,0x74,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x34,0x70,0x78,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x3a,0x68,0x6f,0x76,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x3a,0x23,0x33,0x36,0x39,0x7d,0x69,0x6e,0x70,0x75,0x74,0x3a,0x68,0x6f,0x76,0x65,0x72,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x68,0x6f,0x76,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x63,0x63,0x63,0x7d,0x69,0x6e,0x70,0x75,0x74,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x65,0x65,0x65,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x34,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x20,0x38,0x70,0x78,0x7d,0x69,0x6e,0x70,0x75,0x74,0x2e,0x77,0x69,0x64,0x65,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2e,0x77,0x69,0x64,0x65,0x7b,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x35,0x30,0x30,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x38,0x30,0x25,0x7d,0x69,0x6e,0x70,0x75,0x74,0x2e,0x78,0x77,0x69,0x64,0x65,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2e,0x78,0x77,0x69,0x64,0x65,0x7b,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x35,0x30,0x30,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x25,0x7d,0x2e,0x77,0x69,0x64,0x65,0x6e,0x75,0x6d,0x62,0x65,0x72,0x7b,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x35,0x30,0x30,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x37,0x65,0x6d,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x32,0x70,0x74,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x33,0x35,0x70,0x78,0x3b,0x63,0x75,0x72,0x73,0x6f,0x72,0x3a,0x70,0x6f,0x69,0x6e,0x74,0x65,0x72,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x7b,0x2d,0x6d,0x6f,0x7a,0x2d,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x2d,0x6d,0x73,0x2d,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x2d,0x77,0x65,0x62,0x6b,0x69,0x74,0x2d,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x62,0x6c,0x6f,0x63,0x6b,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x34,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x72,0x65,0x6c,0x61,0x74,0x69,0x76,0x65,0x3b,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x7b,0x63,0x75,0x72,0x73,0x6f,0x72,0x3a,0x70,0x6f,0x69,0x6e,0x74,0x65,0x72,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2e,0x64,0x69,0x73,0x61,0x62,0x6c,0x65,0x64,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x67,0x72,0x65,0x79,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x65,0x65,0x65,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x32,0x35,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x3b,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x32,0x35,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x3a,0x68,0x6f,0x76,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x7e,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x63,0x63,0x63,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x37,0x44,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x62,0x6c,0x6f,0x63,0x6b,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x2d,0x6d,0x73,0x2d,0x74,0x72,0x61,0x6e,0x73,0x66,0x6f,0x72,0x6d,0x3a,0x72,0x6f,0x74,0x61,0x74,0x65,0x28,0x34,0x35,0x64,0x65,0x67,0x29,0x3b,0x2d,0x77,0x65,0x62,0x6b,0x69,0x74,0x2d,0x74,0x72,0x61,0x6e,0x73,0x66,0x6f,0x72,0x6d,0x3a,0x72,0x6f,0x74,0x61,0x74,0x65,0x28,0x34,0x35,0x64,0x65,0x67,0x29,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x3a,0x73,0x6f,0x6c,0x69,0x64,0x20,0x23,0x66,0x66,0x66,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x30,0x20,0x33,0x70,0x78,0x20,0x33,0x70,0x78,0x20,0x30,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x31,0x30,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x37,0x70,0x78,0x3b,0x74,0x6f,0x70,0x3a,0x33,0x70,0x78,0x3b,0x74,0x72,0x61,0x6e,0x73,0x66,0x6f,0x72,0x6d,0x3a,0x72,0x6f,0x74,0x61,0x74,0x65,0x28,0x34,0x35,0x64,0x65,0x67,0x29,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x35,0x70,0x78,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x2c,0x2e,0x6c,0x6f,0x67,0x76,0x69,0x65,0x77,0x65,0x72,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x67,0x72,0x61,0x79,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x73,0x74,0x79,0x6c,0x65,0x3a,0x73,0x6f,0x6c,0x69,0x64,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x7b,0x2d,0x6d,0x6f,0x7a,0x2d,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x2d,0x6d,0x73,0x2d,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x2d,0x77,0x65,0x62,0x6b,0x69,0x74,0x2d,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x32,0x30,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x39,0x70,0x78,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x72,0x65,0x6c,0x61,0x74,0x69,0x76,0x65,0x3b,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x69,0x6e,0x70,0x75,0x74,0x7b,0x63,0x75,0x72,0x73,0x6f,0x72,0x3a,0x70,0x6f,0x69,0x6e,0x74,0x65,0x72,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x7d,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x65,0x65,0x65,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x30,0x25,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x32,0x36,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x3b,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x32,0x36,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x3a,0x68,0x6f,0x76,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x7e,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x63,0x63,0x63,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x37,0x44,0x7d,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x62,0x6c,0x6f,0x63,0x6b,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x3a,0x23,0x66,0x66,0x66,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x30,0x25,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x38,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x38,0x70,0x78,0x3b,0x74,0x6f,0x70,0x3a,0x38,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x38,0x70,0x78,0x7d,0x2e,0x6c,0x6f,0x67,0x76,0x69,0x65,0x77,0x65,0x72,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x66,0x61,0x6d,0x69,0x6c,0x79,0x3a,0x27,0x4c,0x75,0x63,0x69,0x64,0x61,0x20,0x43,0x6f,0x6e,0x73,0x6f,0x6c,0x65,0x27,0x2c,0x4d,0x6f,0x6e,0x61,0x63,0x6f,0x2c,0x6d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x3b,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x30,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x20,0x38,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x38,0x30,0x25,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x37,0x44,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x34,0x70,0x78,0x3b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x37,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x32,0x38,0x32,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x2d,0x31,0x32,0x35,0x70,0x78,0x3b,0x6d,0x69,0x6e,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x32,0x35,0x30,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x31,0x36,0x70,0x78,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x66,0x69,0x78,0x65,0x64,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x76,0x69,0x73,0x69,0x62,0x69,0x6c,0x69,0x74,0x79,0x3a,0x68,0x69,0x64,0x64,0x65,0x6e,0x3b,0x7a,0x2d,0x69,0x6e,0x64,0x65,0x78,0x3a,0x31,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x2e,0x73,0x68,0x6f,0x77,0x7b,0x2d,0x77,0x65,0x62,0x6b,0x69,0x74,0x2d,0x61,0x6e,0x69,0x6d,0x61,0x74,0x69,0x6f,0x6e,0x3a,0x66,0x61,0x64,0x65,0x69,0x6e,0x20,0x2e,0x35,0x73,0x2c,0x66,0x61,0x64,0x65,0x6f,0x75,0x74,0x20,0x2e,0x35,0x73,0x20,0x32,0x2e,0x35,0x73,0x3b,0x61,0x6e,0x69,0x6d,0x61,0x74,0x69,0x6f,0x6e,0x3a,0x66,0x61,0x64,0x65,0x69,0x6e,0x20,0x2e,0x35,0x73,0x2c,0x66,0x61,0x64,0x65,0x6f,0x75,0x74,0x20,0x2e,0x35,0x73,0x20,0x32,0x2e,0x35,0x73,0x3b,0x76,0x69,0x73,0x69,0x62,0x69,0x6c,0x69,0x74,0x79,0x3a,0x76,0x69,0x73,0x69,0x62,0x6c,0x65,0x7d,0x40,0x2d,0x77,0x65,0x62,0x6b,0x69,0x74,0x2d,0x6b,0x65,0x79,0x66,0x72,0x61,0x6d,0x65,0x73,0x20,0x66,0x61,0x64,0x65,0x69,0x6e,0x7b,0x66,0x72,0x6f,0x6d,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x32,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x7d,0x74,0x6f,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x2e,0x39,0x7d,0x7d,0x40,0x6b,0x65,0x79,0x66,0x72,0x61,0x6d,0x65,0x73,0x20,0x66,0x61,0x64,0x65,0x69,0x6e,0x7b,0x66,0x72,0x6f,0x6d,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x32,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x7d,0x74,0x6f,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x2e,0x39,0x7d,0x7d,0x40,0x2d,0x77,0x65,0x62,0x6b,0x69,0x74,0x2d,0x6b,0x65,0x79,0x66,0x72,0x61,0x6d,0x65,0x73,0x20,0x66,0x61,0x64,0x65,0x6f,0x75,0x74,0x7b,0x66,0x72,0x6f,0x6d,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x2e,0x39,0x7d,0x74,0x6f,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x30,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x7d,0x7d,0x40,0x6b,0x65,0x79,0x66,0x72,0x61,0x6d,0x65,0x73,0x20,0x66,0x61,0x64,0x65,0x6f,0x75,0x74,0x7b,0x66,0x72,0x6f,0x6d,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x2e,0x39,0x7d,0x74,0x6f,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x30,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x7d,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x30,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x46,0x31,0x46,0x31,0x46,0x31,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x31,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x46,0x43,0x46,0x46,0x39,0x35,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x32,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x39,0x44,0x43,0x45,0x46,0x45,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x33,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x41,0x34,0x46,0x43,0x37,0x39,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x34,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x46,0x32,0x41,0x42,0x33,0x39,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x39,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x46,0x35,0x30,0x7d,0x2e,0x6c,0x6f,0x67,0x76,0x69,0x65,0x77,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x32,0x37,0x32,0x37,0x32,0x37,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x46,0x31,0x46,0x31,0x46,0x31,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x35,0x33,0x30,0x70,0x78,0x3b,0x6f,0x76,0x65,0x72,0x66,0x6c,0x6f,0x77,0x3a,0x61,0x75,0x74,0x6f,0x7d,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x3a,0x68,0x6f,0x76,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x63,0x63,0x63,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x68,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x68,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x38,0x38,0x38,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x46,0x46,0x46,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x77,0x65,0x69,0x67,0x68,0x74,0x3a,0x37,0x30,0x30,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x36,0x70,0x78,0x3b,0x61,0x6c,0x69,0x67,0x6e,0x2d,0x63,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6c,0x61,0x70,0x73,0x65,0x3a,0x63,0x6f,0x6c,0x6c,0x61,0x70,0x73,0x65,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x30,0x30,0x3b,0x6d,0x69,0x6e,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x34,0x32,0x30,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x25,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x72,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x64,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x72,0x7b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x64,0x7b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x33,0x30,0x70,0x78,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x64,0x7b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x33,0x30,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x72,0x3a,0x6e,0x74,0x68,0x2d,0x63,0x68,0x69,0x6c,0x64,0x28,0x65,0x76,0x65,0x6e,0x29,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x44,0x45,0x45,0x36,0x46,0x46,0x7d,0x2e,0x68,0x69,0x67,0x68,0x6c,0x69,0x67,0x68,0x74,0x20,0x74,0x64,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x64,0x62,0x66,0x66,0x30,0x30,0x37,0x35,0x7d,0x2e,0x61,0x70,0x68,0x65,0x61,0x64,0x65,0x72,0x2c,0x2e,0x68,0x65,0x61,0x64,0x65,0x72,0x6d,0x65,0x6e,0x75,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x46,0x38,0x46,0x38,0x46,0x38,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x38,0x70,0x78,0x20,0x31,0x32,0x70,0x78,0x7d,0x2e,0x6e,0x6f,0x74,0x65,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x74,0x79,0x6c,0x65,0x3a,0x69,0x74,0x61,0x6c,0x69,0x63,0x7d,0x2e,0x68,0x65,0x61,0x64,0x65,0x72,0x6d,0x65,0x6e,0x75,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x31,0x70,0x78,0x20,0x73,0x6f,0x6c,0x69,0x64,0x20,0x23,0x44,0x44,0x44,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x39,0x30,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x66,0x69,0x78,0x65,0x64,0x3b,0x72,0x69,0x67,0x68,0x74,0x3a,0x30,0x3b,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x7a,0x2d,0x69,0x6e,0x64,0x65,0x78,0x3a,0x31,0x7d,0x2e,0x62,0x6f,0x64,0x79,0x6d,0x65,0x6e,0x75,0x7b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x74,0x6f,0x70,0x3a,0x39,0x36,0x70,0x78,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x62,0x61,0x72,0x7b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x69,0x6e,0x68,0x65,0x72,0x69,0x74,0x3b,0x74,0x6f,0x70,0x3a,0x35,0x35,0x70,0x78,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x3a,0x73,0x6f,0x6c,0x69,0x64,0x20,0x74,0x72,0x61,0x6e,0x73,0x70,0x61,0x72,0x65,0x6e,0x74,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x34,0x70,0x78,0x20,0x34,0x70,0x78,0x20,0x30,0x20,0x30,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x34,0x70,0x78,0x20,0x31,0x70,0x78,0x20,0x31,0x70,0x78,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x20,0x31,0x36,0x70,0x78,0x20,0x38,0x70,0x78,0x3b,0x77,0x68,0x69,0x74,0x65,0x2d,0x73,0x70,0x61,0x63,0x65,0x3a,0x6e,0x6f,0x77,0x72,0x61,0x70,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x2e,0x61,0x63,0x74,0x69,0x76,0x65,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x46,0x46,0x46,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x37,0x44,0x20,0x23,0x44,0x44,0x44,0x20,0x23,0x46,0x46,0x46,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x30,0x30,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x3a,0x68,0x6f,0x76,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x3a,0x23,0x44,0x45,0x46,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x30,0x30,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x5f,0x62,0x75,0x74,0x74,0x6f,0x6e,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x6f,0x6e,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x67,0x72,0x65,0x65,0x6e,0x7d,0x2e,0x6f,0x66,0x66,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x72,0x65,0x64,0x7d,0x2e,0x64,0x69,0x76,0x5f,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x38,0x30,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x34,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x32,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x31,0x70,0x78,0x20,0x31,0x30,0x70,0x78,0x7d,0x2e,0x61,0x6c,0x65,0x72,0x74,0x2c,0x2e,0x77,0x61,0x72,0x6e,0x69,0x6e,0x67,0x7b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x31,0x35,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x32,0x30,0x70,0x78,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x7d,0x2e,0x64,0x69,0x76,0x5f,0x62,0x72,0x7b,0x63,0x6c,0x65,0x61,0x72,0x3a,0x62,0x6f,0x74,0x68,0x7d,0x2e,0x61,0x6c,0x65,0x72,0x74,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x34,0x34,0x33,0x33,0x36,0x7d,0x2e,0x77,0x61,0x72,0x6e,0x69,0x6e,0x67,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x63,0x61,0x31,0x37,0x7d,0x2e,0x63,0x6c,0x6f,0x73,0x65,0x62,0x74,0x6e,0x7b,0x63,0x75,0x72,0x73,0x6f,0x72,0x3a,0x70,0x6f,0x69,0x6e,0x74,0x65,0x72,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x32,0x32,0x70,0x78,0x3b,0x6c,0x69,0x6e,0x65,0x2d,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x32,0x30,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x31,0x35,0x70,0x78,0x3b,0x74,0x72,0x61,0x6e,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x2e,0x33,0x73,0x7d,0x2e,0x63,0x6c,0x6f,0x73,0x65,0x62,0x74,0x6e,0x3a,0x68,0x6f,0x76,0x65,0x72,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x30,0x30,0x7d,0x73,0x65,0x63,0x74,0x69,0x6f,0x6e,0x7b,0x6f,0x76,0x65,0x72,0x66,0x6c,0x6f,0x77,0x2d,0x78,0x3a,0x61,0x75,0x74,0x6f,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x25,0x7d,0x40,0x6d,0x65,0x64,0x69,0x61,0x20,0x73,0x63,0x72,0x65,0x65,0x6e,0x20,0x61,0x6e,0x64,0x20,0x28,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x39,0x36,0x30,0x70,0x78,0x29,0x7b,0x2e,0x73,0x68,0x6f,0x77,0x6d,0x65,0x6e,0x75,0x6c,0x61,0x62,0x65,0x6c,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x31,0x76,0x77,0x3b,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x34,0x38,0x70,0x78,0x7d,0x7d,0x0a,0}; +static const char DATA_ESPEASY_DEFAULT_MIN_CSS[] PROGMEM = {0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x62,0x6c,0x6f,0x63,0x6b,0x7d,0x2e,0x63,0x6c,0x6f,0x73,0x65,0x62,0x74,0x6e,0x2c,0x68,0x31,0x2c,0x68,0x32,0x2c,0x68,0x33,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x77,0x65,0x69,0x67,0x68,0x74,0x3a,0x37,0x30,0x30,0x7d,0x68,0x31,0x2c,0x68,0x36,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x37,0x64,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x63,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x3a,0x27,0x27,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2c,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x74,0x65,0x78,0x74,0x2d,0x64,0x65,0x63,0x6f,0x72,0x61,0x74,0x69,0x6f,0x6e,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x64,0x69,0x76,0x5f,0x6c,0x2c,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x66,0x6c,0x6f,0x61,0x74,0x3a,0x6c,0x65,0x66,0x74,0x7d,0x2e,0x63,0x6c,0x6f,0x73,0x65,0x62,0x74,0x6e,0x2c,0x2e,0x64,0x69,0x76,0x5f,0x72,0x7b,0x66,0x6c,0x6f,0x61,0x74,0x3a,0x72,0x69,0x67,0x68,0x74,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x7d,0x2a,0x7b,0x62,0x6f,0x78,0x2d,0x73,0x69,0x7a,0x69,0x6e,0x67,0x3a,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x62,0x6f,0x78,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x66,0x61,0x6d,0x69,0x6c,0x79,0x3a,0x73,0x61,0x6e,0x73,0x2d,0x73,0x65,0x72,0x69,0x66,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x32,0x70,0x74,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x30,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x30,0x7d,0x68,0x31,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x36,0x70,0x74,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x38,0x70,0x78,0x20,0x30,0x7d,0x68,0x32,0x2c,0x68,0x33,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x32,0x70,0x74,0x7d,0x68,0x32,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x30,0x20,0x2d,0x34,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x36,0x70,0x78,0x7d,0x68,0x33,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x65,0x65,0x65,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x31,0x36,0x70,0x78,0x20,0x2d,0x34,0x70,0x78,0x20,0x30,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x7d,0x68,0x36,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x30,0x70,0x74,0x7d,0x63,0x6f,0x64,0x65,0x2c,0x6b,0x62,0x64,0x2c,0x70,0x72,0x65,0x2c,0x73,0x61,0x6d,0x70,0x2c,0x74,0x74,0x2c,0x78,0x6d,0x70,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x66,0x61,0x6d,0x69,0x6c,0x79,0x3a,0x6d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x2c,0x6d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x65,0x6d,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x37,0x64,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x34,0x70,0x78,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x34,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x20,0x31,0x36,0x70,0x78,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x2c,0x69,0x6e,0x70,0x75,0x74,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x65,0x65,0x65,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x68,0x65,0x6c,0x70,0x2c,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2c,0x69,0x6e,0x70,0x75,0x74,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x3a,0x31,0x70,0x78,0x20,0x73,0x6f,0x6c,0x69,0x64,0x20,0x67,0x72,0x61,0x79,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x6c,0x69,0x6e,0x6b,0x2e,0x77,0x69,0x64,0x65,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x69,0x6e,0x6c,0x69,0x6e,0x65,0x2d,0x62,0x6c,0x6f,0x63,0x6b,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x25,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x6c,0x69,0x6e,0x6b,0x2e,0x72,0x65,0x64,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x72,0x65,0x64,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x68,0x65,0x6c,0x70,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x30,0x25,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x32,0x70,0x78,0x20,0x34,0x70,0x78,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2c,0x69,0x6e,0x70,0x75,0x74,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x34,0x70,0x78,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x3a,0x68,0x6f,0x76,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x3a,0x23,0x33,0x36,0x39,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x3a,0x68,0x6f,0x76,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x7e,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x3a,0x68,0x6f,0x76,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x7e,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2c,0x69,0x6e,0x70,0x75,0x74,0x3a,0x68,0x6f,0x76,0x65,0x72,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x68,0x6f,0x76,0x65,0x72,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x3a,0x68,0x6f,0x76,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x63,0x63,0x63,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x37,0x64,0x7d,0x69,0x6e,0x70,0x75,0x74,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x34,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x20,0x38,0x70,0x78,0x7d,0x69,0x6e,0x70,0x75,0x74,0x2e,0x77,0x69,0x64,0x65,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2e,0x77,0x69,0x64,0x65,0x7b,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x35,0x30,0x30,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x38,0x30,0x25,0x7d,0x69,0x6e,0x70,0x75,0x74,0x2e,0x78,0x77,0x69,0x64,0x65,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2e,0x78,0x77,0x69,0x64,0x65,0x7b,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x35,0x30,0x30,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x25,0x7d,0x2e,0x77,0x69,0x64,0x65,0x6e,0x75,0x6d,0x62,0x65,0x72,0x7b,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x35,0x30,0x30,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x32,0x70,0x74,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x33,0x35,0x70,0x78,0x3b,0x63,0x75,0x72,0x73,0x6f,0x72,0x3a,0x70,0x6f,0x69,0x6e,0x74,0x65,0x72,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x7b,0x2d,0x6d,0x6f,0x7a,0x2d,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x2d,0x6d,0x73,0x2d,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x2d,0x77,0x65,0x62,0x6b,0x69,0x74,0x2d,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x34,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x72,0x65,0x6c,0x61,0x74,0x69,0x76,0x65,0x3b,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x69,0x6e,0x70,0x75,0x74,0x7b,0x63,0x75,0x72,0x73,0x6f,0x72,0x3a,0x70,0x6f,0x69,0x6e,0x74,0x65,0x72,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2e,0x64,0x69,0x73,0x61,0x62,0x6c,0x65,0x64,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x67,0x72,0x65,0x79,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x7b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x32,0x35,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x3b,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x32,0x35,0x70,0x78,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x2d,0x6d,0x73,0x2d,0x74,0x72,0x61,0x6e,0x73,0x66,0x6f,0x72,0x6d,0x3a,0x72,0x6f,0x74,0x61,0x74,0x65,0x28,0x34,0x35,0x64,0x65,0x67,0x29,0x3b,0x2d,0x77,0x65,0x62,0x6b,0x69,0x74,0x2d,0x74,0x72,0x61,0x6e,0x73,0x66,0x6f,0x72,0x6d,0x3a,0x72,0x6f,0x74,0x61,0x74,0x65,0x28,0x34,0x35,0x64,0x65,0x67,0x29,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x3a,0x73,0x6f,0x6c,0x69,0x64,0x20,0x23,0x66,0x66,0x66,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x30,0x20,0x33,0x70,0x78,0x20,0x33,0x70,0x78,0x20,0x30,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x31,0x30,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x37,0x70,0x78,0x3b,0x74,0x6f,0x70,0x3a,0x33,0x70,0x78,0x3b,0x74,0x72,0x61,0x6e,0x73,0x66,0x6f,0x72,0x6d,0x3a,0x72,0x6f,0x74,0x61,0x74,0x65,0x28,0x34,0x35,0x64,0x65,0x67,0x29,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x35,0x70,0x78,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x2c,0x2e,0x6c,0x6f,0x67,0x76,0x69,0x65,0x77,0x65,0x72,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x67,0x72,0x61,0x79,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x73,0x74,0x79,0x6c,0x65,0x3a,0x73,0x6f,0x6c,0x69,0x64,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x7b,0x2d,0x6d,0x6f,0x7a,0x2d,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x2d,0x6d,0x73,0x2d,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x2d,0x77,0x65,0x62,0x6b,0x69,0x74,0x2d,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x32,0x30,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x39,0x70,0x78,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x72,0x65,0x6c,0x61,0x74,0x69,0x76,0x65,0x3b,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x30,0x25,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x32,0x36,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x3b,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x32,0x36,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x3a,0x23,0x66,0x66,0x66,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x30,0x25,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x38,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x38,0x70,0x78,0x3b,0x74,0x6f,0x70,0x3a,0x38,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x38,0x70,0x78,0x7d,0x2e,0x6c,0x6f,0x67,0x76,0x69,0x65,0x77,0x65,0x72,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x66,0x61,0x6d,0x69,0x6c,0x79,0x3a,0x27,0x4c,0x75,0x63,0x69,0x64,0x61,0x20,0x43,0x6f,0x6e,0x73,0x6f,0x6c,0x65,0x27,0x2c,0x4d,0x6f,0x6e,0x61,0x63,0x6f,0x2c,0x6d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x3b,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x30,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x20,0x38,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x38,0x30,0x25,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x34,0x70,0x78,0x3b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x37,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x32,0x38,0x32,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x2d,0x31,0x32,0x35,0x70,0x78,0x3b,0x6d,0x69,0x6e,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x32,0x35,0x30,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x31,0x36,0x70,0x78,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x66,0x69,0x78,0x65,0x64,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x76,0x69,0x73,0x69,0x62,0x69,0x6c,0x69,0x74,0x79,0x3a,0x68,0x69,0x64,0x64,0x65,0x6e,0x3b,0x7a,0x2d,0x69,0x6e,0x64,0x65,0x78,0x3a,0x31,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x2e,0x73,0x68,0x6f,0x77,0x7b,0x2d,0x77,0x65,0x62,0x6b,0x69,0x74,0x2d,0x61,0x6e,0x69,0x6d,0x61,0x74,0x69,0x6f,0x6e,0x3a,0x2e,0x35,0x73,0x20,0x66,0x61,0x64,0x65,0x69,0x6e,0x2c,0x2e,0x35,0x73,0x20,0x32,0x2e,0x35,0x73,0x20,0x66,0x61,0x64,0x65,0x6f,0x75,0x74,0x3b,0x61,0x6e,0x69,0x6d,0x61,0x74,0x69,0x6f,0x6e,0x3a,0x2e,0x35,0x73,0x20,0x66,0x61,0x64,0x65,0x69,0x6e,0x2c,0x2e,0x35,0x73,0x20,0x32,0x2e,0x35,0x73,0x20,0x66,0x61,0x64,0x65,0x6f,0x75,0x74,0x3b,0x76,0x69,0x73,0x69,0x62,0x69,0x6c,0x69,0x74,0x79,0x3a,0x76,0x69,0x73,0x69,0x62,0x6c,0x65,0x7d,0x40,0x2d,0x77,0x65,0x62,0x6b,0x69,0x74,0x2d,0x6b,0x65,0x79,0x66,0x72,0x61,0x6d,0x65,0x73,0x20,0x66,0x61,0x64,0x65,0x69,0x6e,0x7b,0x66,0x72,0x6f,0x6d,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x32,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x7d,0x74,0x6f,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x2e,0x39,0x7d,0x7d,0x40,0x6b,0x65,0x79,0x66,0x72,0x61,0x6d,0x65,0x73,0x20,0x66,0x61,0x64,0x65,0x69,0x6e,0x7b,0x66,0x72,0x6f,0x6d,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x32,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x7d,0x74,0x6f,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x2e,0x39,0x7d,0x7d,0x40,0x2d,0x77,0x65,0x62,0x6b,0x69,0x74,0x2d,0x6b,0x65,0x79,0x66,0x72,0x61,0x6d,0x65,0x73,0x20,0x66,0x61,0x64,0x65,0x6f,0x75,0x74,0x7b,0x66,0x72,0x6f,0x6d,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x2e,0x39,0x7d,0x74,0x6f,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x30,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x7d,0x7d,0x40,0x6b,0x65,0x79,0x66,0x72,0x61,0x6d,0x65,0x73,0x20,0x66,0x61,0x64,0x65,0x6f,0x75,0x74,0x7b,0x66,0x72,0x6f,0x6d,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x2e,0x39,0x7d,0x74,0x6f,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x30,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x7d,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x30,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x31,0x66,0x31,0x66,0x31,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x31,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x63,0x66,0x66,0x39,0x35,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x32,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x39,0x64,0x63,0x65,0x66,0x65,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x33,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x61,0x34,0x66,0x63,0x37,0x39,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x34,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x32,0x61,0x62,0x33,0x39,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x39,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x35,0x30,0x7d,0x2e,0x6c,0x6f,0x67,0x76,0x69,0x65,0x77,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x32,0x37,0x32,0x37,0x32,0x37,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x31,0x66,0x31,0x66,0x31,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x35,0x33,0x30,0x70,0x78,0x3b,0x6f,0x76,0x65,0x72,0x66,0x6c,0x6f,0x77,0x3a,0x61,0x75,0x74,0x6f,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x68,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x68,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x68,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x38,0x38,0x38,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x77,0x65,0x69,0x67,0x68,0x74,0x3a,0x37,0x30,0x30,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x36,0x70,0x78,0x3b,0x61,0x6c,0x69,0x67,0x6e,0x2d,0x63,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6c,0x61,0x70,0x73,0x65,0x3a,0x63,0x6f,0x6c,0x6c,0x61,0x70,0x73,0x65,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x30,0x30,0x3b,0x6d,0x69,0x6e,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x34,0x32,0x30,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x25,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x72,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x72,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x64,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x72,0x7b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x64,0x7b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x33,0x30,0x70,0x78,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x64,0x7b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x33,0x30,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x64,0x7b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x33,0x30,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x3b,0x61,0x6c,0x69,0x67,0x6e,0x2d,0x63,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x3a,0x66,0x6c,0x65,0x78,0x2d,0x73,0x74,0x61,0x72,0x74,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x6c,0x65,0x66,0x74,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x72,0x3a,0x6e,0x74,0x68,0x2d,0x63,0x68,0x69,0x6c,0x64,0x28,0x34,0x6e,0x2b,0x31,0x29,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x72,0x3a,0x6e,0x74,0x68,0x2d,0x63,0x68,0x69,0x6c,0x64,0x28,0x34,0x6e,0x2b,0x32,0x29,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x72,0x3a,0x6e,0x74,0x68,0x2d,0x63,0x68,0x69,0x6c,0x64,0x28,0x32,0x6e,0x29,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x64,0x65,0x65,0x36,0x66,0x66,0x7d,0x2e,0x68,0x69,0x67,0x68,0x6c,0x69,0x67,0x68,0x74,0x20,0x74,0x64,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x64,0x62,0x66,0x66,0x30,0x30,0x37,0x35,0x7d,0x2e,0x61,0x70,0x68,0x65,0x61,0x64,0x65,0x72,0x2c,0x2e,0x68,0x65,0x61,0x64,0x65,0x72,0x6d,0x65,0x6e,0x75,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x38,0x66,0x38,0x66,0x38,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x38,0x70,0x78,0x20,0x31,0x32,0x70,0x78,0x7d,0x2e,0x6e,0x6f,0x74,0x65,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x74,0x79,0x6c,0x65,0x3a,0x69,0x74,0x61,0x6c,0x69,0x63,0x7d,0x2e,0x68,0x65,0x61,0x64,0x65,0x72,0x6d,0x65,0x6e,0x75,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x31,0x70,0x78,0x20,0x73,0x6f,0x6c,0x69,0x64,0x20,0x23,0x64,0x64,0x64,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x39,0x30,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x66,0x69,0x78,0x65,0x64,0x3b,0x72,0x69,0x67,0x68,0x74,0x3a,0x30,0x3b,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x7a,0x2d,0x69,0x6e,0x64,0x65,0x78,0x3a,0x31,0x7d,0x2e,0x62,0x6f,0x64,0x79,0x6d,0x65,0x6e,0x75,0x7b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x74,0x6f,0x70,0x3a,0x39,0x36,0x70,0x78,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x62,0x61,0x72,0x7b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x69,0x6e,0x68,0x65,0x72,0x69,0x74,0x3b,0x74,0x6f,0x70,0x3a,0x35,0x35,0x70,0x78,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x3a,0x73,0x6f,0x6c,0x69,0x64,0x20,0x74,0x72,0x61,0x6e,0x73,0x70,0x61,0x72,0x65,0x6e,0x74,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x34,0x70,0x78,0x20,0x34,0x70,0x78,0x20,0x30,0x20,0x30,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x34,0x70,0x78,0x20,0x31,0x70,0x78,0x20,0x31,0x70,0x78,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x20,0x31,0x36,0x70,0x78,0x20,0x38,0x70,0x78,0x3b,0x77,0x68,0x69,0x74,0x65,0x2d,0x73,0x70,0x61,0x63,0x65,0x3a,0x6e,0x6f,0x77,0x72,0x61,0x70,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x2e,0x61,0x63,0x74,0x69,0x76,0x65,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x37,0x64,0x20,0x23,0x64,0x64,0x64,0x20,0x23,0x66,0x66,0x66,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x30,0x30,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x3a,0x68,0x6f,0x76,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x3a,0x23,0x64,0x65,0x66,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x30,0x30,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x5f,0x62,0x75,0x74,0x74,0x6f,0x6e,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x6f,0x6e,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x67,0x72,0x65,0x65,0x6e,0x7d,0x2e,0x6f,0x66,0x66,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x72,0x65,0x64,0x7d,0x2e,0x64,0x69,0x76,0x5f,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x38,0x30,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x34,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x32,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x31,0x70,0x78,0x20,0x31,0x30,0x70,0x78,0x7d,0x2e,0x61,0x6c,0x65,0x72,0x74,0x2c,0x2e,0x77,0x61,0x72,0x6e,0x69,0x6e,0x67,0x7b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x31,0x35,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x32,0x30,0x70,0x78,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x7d,0x2e,0x64,0x69,0x76,0x5f,0x62,0x72,0x7b,0x63,0x6c,0x65,0x61,0x72,0x3a,0x62,0x6f,0x74,0x68,0x7d,0x2e,0x61,0x6c,0x65,0x72,0x74,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x34,0x34,0x33,0x33,0x36,0x7d,0x2e,0x77,0x61,0x72,0x6e,0x69,0x6e,0x67,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x63,0x61,0x31,0x37,0x7d,0x2e,0x63,0x6c,0x6f,0x73,0x65,0x62,0x74,0x6e,0x7b,0x63,0x75,0x72,0x73,0x6f,0x72,0x3a,0x70,0x6f,0x69,0x6e,0x74,0x65,0x72,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x32,0x32,0x70,0x78,0x3b,0x6c,0x69,0x6e,0x65,0x2d,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x32,0x30,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x31,0x35,0x70,0x78,0x3b,0x74,0x72,0x61,0x6e,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x2e,0x33,0x73,0x7d,0x2e,0x63,0x6c,0x6f,0x73,0x65,0x62,0x74,0x6e,0x3a,0x68,0x6f,0x76,0x65,0x72,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x30,0x30,0x7d,0x73,0x65,0x63,0x74,0x69,0x6f,0x6e,0x7b,0x6f,0x76,0x65,0x72,0x66,0x6c,0x6f,0x77,0x2d,0x78,0x3a,0x61,0x75,0x74,0x6f,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x25,0x7d,0x40,0x6d,0x65,0x64,0x69,0x61,0x20,0x73,0x63,0x72,0x65,0x65,0x6e,0x20,0x61,0x6e,0x64,0x20,0x28,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x39,0x36,0x30,0x70,0x78,0x29,0x7b,0x2e,0x73,0x68,0x6f,0x77,0x6d,0x65,0x6e,0x75,0x6c,0x61,0x62,0x65,0x6c,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x34,0x38,0x70,0x78,0x7d,0x7d,0}; #endif // WEBSERVER_CSS // JavaScript blobs diff --git a/static/espeasy_default.css b/static/espeasy_default.css index 5f0ff50bf0..d1ced2a222 100644 --- a/static/espeasy_default.css +++ b/static/espeasy_default.css @@ -393,6 +393,7 @@ textarea:hover { } table.multirow th, +table.multi2row th, table.normal th { background-color: #444; border-color: #888; @@ -404,6 +405,7 @@ table.normal th { } table.multirow, +table.multi2row, table.normal { border-collapse: collapse; color: #000; @@ -412,6 +414,7 @@ table.normal { } table.multirow tr, +table.multi2row tr, table.normal td, table.normal tr { padding: 4px @@ -427,7 +430,16 @@ table.multirow td { text-align: center } -table.multirow tr:nth-child(even) { +table.multi2row td { + height: 30px; + padding: 4px; + align-content: flex-start; + text-align: left +} + +table.multirow tr:nth-child(even), +table.multi2row tr:nth-child(4n+1), +table.multi2row tr:nth-child(4n+2) { background-color: #DEE6FF } diff --git a/static/espeasy_default.min.css b/static/espeasy_default.min.css index cbc62e3298..4b01b7ca0a 100644 --- a/static/espeasy_default.min.css +++ b/static/espeasy_default.min.css @@ -1 +1 @@ -.closebtn,h1,h2,h3{font-weight:700}h1,h6{color:#07D}.checkmark:after,.dotmark:after{content:''}.button,.menu{text-decoration:none}.div_l,.menu{float:left}.closebtn,.div_r{float:right;color:#fff}*{box-sizing:border-box;font-family:sans-serif;font-size:12pt;margin:0;padding:0}h1{font-size:16pt;margin:8px 0}h2,h3{font-size:12pt}h2{background-color:#444;color:#FFF;margin:0 -4px;padding:6px}h3{background-color:#EEE;color:#444;margin:16px -4px 0;padding:4px}h6{font-size:10pt}code,kbd,pre,samp,tt,xmp{font-family:monospace,monospace;font-size:1em}.button{background-color:#07D;border:none;border-radius:4px;color:#FFF;margin:4px;padding:4px 16px}.button.help,.checkmark,input,select,textarea{border-color:gray;border-style:solid;border-width:1px}.button.link.wide{display:inline-block;text-align:center;width:100%}.button.link.red{background-color:red}.button.help{border-radius:50%;padding:2px 4px}.checkmark,input,select,textarea{border-radius:4px}.button:hover{background:#369}input:hover,select:hover{background-color:#ccc}input,select,textarea{background-color:#eee;margin:4px;padding:4px 8px}input.wide,select.wide{max-width:500px;width:80%}input.xwide,select.xwide{max-width:500px;width:100%}.widenumber{max-width:500px;width:7em}.container,.container2{font-size:12pt;padding-left:35px;cursor:pointer}.container{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;display:block;margin-left:4px;margin-top:0;position:relative;user-select:none}.container input{cursor:pointer;opacity:0;position:absolute}.checkmark.disabled{background-color:grey}.checkmark{background-color:#eee;height:25px;left:0;position:absolute;top:0;width:25px}.container:hover input~.checkmark{background-color:#ccc}.container input:checked~.checkmark{background-color:#07D}.checkmark:after{display:none;position:absolute}.container input:checked~.checkmark:after,.container2{display:block}.container .checkmark:after{-ms-transform:rotate(45deg);-webkit-transform:rotate(45deg);border:solid #fff;border-width:0 3px 3px 0;height:10px;left:7px;top:3px;transform:rotate(45deg);width:5px}#toastmessage,.dotmark,.logviewer{border-color:gray;border-style:solid}#toastmessage,.dotmark{border-width:1px}.container2{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;margin-bottom:20px;margin-left:9px;position:relative;user-select:none}.container2 input{cursor:pointer;opacity:0;position:absolute}.dotmark{background-color:#eee;border-radius:50%;height:26px;left:0;position:absolute;top:0;width:26px}.container2:hover input~.dotmark{background-color:#ccc}.container2 input:checked~.dotmark{background-color:#07D}.dotmark:after{display:none;position:absolute}.container2 input:checked~.dotmark:after{display:block}.container2 .dotmark:after{background:#fff;border-radius:50%;height:8px;left:8px;top:8px;width:8px}.logviewer,textarea{font-family:'Lucida Console',Monaco,monospace;max-width:1000px;padding:4px 8px;width:80%}#toastmessage{background-color:#07D;border-radius:4px;bottom:30%;color:#fff;font-size:17px;left:282px;margin-left:-125px;min-width:250px;padding:16px;position:fixed;text-align:center;visibility:hidden;z-index:1}#toastmessage.show{-webkit-animation:fadein .5s,fadeout .5s 2.5s;animation:fadein .5s,fadeout .5s 2.5s;visibility:visible}@-webkit-keyframes fadein{from{bottom:20%;opacity:0}to{bottom:30%;opacity:.9}}@keyframes fadein{from{bottom:20%;opacity:0}to{bottom:30%;opacity:.9}}@-webkit-keyframes fadeout{from{bottom:30%;opacity:.9}to{bottom:0;opacity:0}}@keyframes fadeout{from{bottom:30%;opacity:.9}to{bottom:0;opacity:0}}.level_0{color:#F1F1F1}.level_1{color:#FCFF95}.level_2{color:#9DCEFE}.level_3{color:#A4FC79}.level_4{color:#F2AB39}.level_9{color:#F50}.logviewer{background-color:#272727;color:#F1F1F1;height:530px;overflow:auto}textarea:hover{background-color:#ccc}table.multirow th,table.normal th{background-color:#444;border-color:#888;color:#FFF;font-weight:700;padding:6px;align-content:center;text-align:center}table.multirow,table.normal{border-collapse:collapse;color:#000;min-width:420px;width:100%}table.multirow tr,table.normal td,table.normal tr{padding:4px}table.normal td{height:30px}table.multirow td{height:30px;padding:4px;text-align:center}table.multirow tr:nth-child(even){background-color:#DEE6FF}.highlight td{background-color:#dbff0075}.apheader,.headermenu{background-color:#F8F8F8;padding:8px 12px}.note{color:#444;font-style:italic}.headermenu{border-bottom:1px solid #DDD;height:90px;left:0;position:fixed;right:0;top:0;z-index:1}.bodymenu{margin-top:96px}.menubar{position:inherit;top:55px}.menu{border:solid transparent;border-radius:4px 4px 0 0;border-width:4px 1px 1px;color:#444;padding:4px 16px 8px;white-space:nowrap}.menu.active{background-color:#FFF;border-color:#07D #DDD #FFF;color:#000}.menu:hover{background:#DEF;color:#000}.menu_button{display:none}.on{color:green}.off{color:red}.div_r{background-color:#080;border-radius:4px;margin:2px;padding:1px 10px}.alert,.warning{margin-bottom:15px;padding:20px;color:#fff}.div_br{clear:both}.alert{background-color:#f44336}.warning{background-color:#ffca17}.closebtn{cursor:pointer;font-size:22px;line-height:20px;margin-left:15px;transition:.3s}.closebtn:hover{color:#000}section{overflow-x:auto;width:100%}@media screen and (max-width:960px){.showmenulabel{display:none}.menu{max-width:11vw;max-width:48px}} +.container,.container input:checked~.checkmark:after,.container2,.container2 input:checked~.dotmark:after{display:block}.closebtn,h1,h2,h3{font-weight:700}h1,h6{color:#07d}.checkmark:after,.dotmark:after{content:''}.button,.menu{text-decoration:none}.div_l,.menu{float:left}.closebtn,.div_r{float:right;color:#fff}*{box-sizing:border-box;font-family:sans-serif;font-size:12pt;margin:0;padding:0}h1{font-size:16pt;margin:8px 0}h2,h3{font-size:12pt}h2{background-color:#444;color:#fff;margin:0 -4px;padding:6px}h3{background-color:#eee;color:#444;margin:16px -4px 0;padding:4px}h6{font-size:10pt}code,kbd,pre,samp,tt,xmp{font-family:monospace,monospace;font-size:1em}.button{background-color:#07d;border:none;border-radius:4px;color:#fff;margin:4px;padding:4px 16px}.checkmark,.dotmark,input,select,textarea{background-color:#eee}.button.help,.checkmark,input,select,textarea{border:1px solid gray}.button.link.wide{display:inline-block;text-align:center;width:100%}.button.link.red{background-color:red}.button.help{border-radius:50%;padding:2px 4px}.checkmark,input,select,textarea{border-radius:4px}.button:hover{background:#369}.container2:hover input~.dotmark,.container:hover input~.checkmark,input:hover,select:hover,textarea:hover{background-color:#ccc}#toastmessage,.container input:checked~.checkmark,.container2 input:checked~.dotmark{background-color:#07d}input,select,textarea{margin:4px;padding:4px 8px}input.wide,select.wide{max-width:500px;width:80%}input.xwide,select.xwide{max-width:500px;width:100%}.widenumber{max-width:500px;width:100px}.container,.container2{font-size:12pt;padding-left:35px;cursor:pointer}.container{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;margin-left:4px;margin-top:0;position:relative;user-select:none}.container input,.container2 input{cursor:pointer;opacity:0;position:absolute}.checkmark.disabled{background-color:grey}.checkmark{height:25px;left:0;position:absolute;top:0;width:25px}.checkmark:after,.dotmark:after{display:none;position:absolute}.container .checkmark:after{-ms-transform:rotate(45deg);-webkit-transform:rotate(45deg);border:solid #fff;border-width:0 3px 3px 0;height:10px;left:7px;top:3px;transform:rotate(45deg);width:5px}#toastmessage,.dotmark,.logviewer{border-color:gray;border-style:solid}#toastmessage,.dotmark{border-width:1px}.container2{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;margin-bottom:20px;margin-left:9px;position:relative;user-select:none}.dotmark{border-radius:50%;height:26px;left:0;position:absolute;top:0;width:26px}.container2 .dotmark:after{background:#fff;border-radius:50%;height:8px;left:8px;top:8px;width:8px}.logviewer,textarea{font-family:'Lucida Console',Monaco,monospace;max-width:1000px;padding:4px 8px;width:80%}#toastmessage{border-radius:4px;bottom:30%;color:#fff;font-size:17px;left:282px;margin-left:-125px;min-width:250px;padding:16px;position:fixed;text-align:center;visibility:hidden;z-index:1}#toastmessage.show{-webkit-animation:.5s fadein,.5s 2.5s fadeout;animation:.5s fadein,.5s 2.5s fadeout;visibility:visible}@-webkit-keyframes fadein{from{bottom:20%;opacity:0}to{bottom:30%;opacity:.9}}@keyframes fadein{from{bottom:20%;opacity:0}to{bottom:30%;opacity:.9}}@-webkit-keyframes fadeout{from{bottom:30%;opacity:.9}to{bottom:0;opacity:0}}@keyframes fadeout{from{bottom:30%;opacity:.9}to{bottom:0;opacity:0}}.level_0{color:#f1f1f1}.level_1{color:#fcff95}.level_2{color:#9dcefe}.level_3{color:#a4fc79}.level_4{color:#f2ab39}.level_9{color:#f50}.logviewer{background-color:#272727;color:#f1f1f1;height:530px;overflow:auto}table.multi2row th,table.multirow th,table.normal th{background-color:#444;border-color:#888;color:#fff;font-weight:700;padding:6px;align-content:center;text-align:center}table.multi2row,table.multirow,table.normal{border-collapse:collapse;color:#000;min-width:420px;width:100%}table.multi2row tr,table.multirow tr,table.normal td,table.normal tr{padding:4px}table.normal td{height:30px}table.multirow td{height:30px;padding:4px;text-align:center}table.multi2row td{height:30px;padding:4px;align-content:flex-start;text-align:left}table.multi2row tr:nth-child(4n+1),table.multi2row tr:nth-child(4n+2),table.multirow tr:nth-child(2n){background-color:#dee6ff}.highlight td{background-color:#dbff0075}.apheader,.headermenu{background-color:#f8f8f8;padding:8px 12px}.note{color:#444;font-style:italic}.headermenu{border-bottom:1px solid #ddd;height:90px;left:0;position:fixed;right:0;top:0;z-index:1}.bodymenu{margin-top:96px}.menubar{position:inherit;top:55px}.menu{border:solid transparent;border-radius:4px 4px 0 0;border-width:4px 1px 1px;color:#444;padding:4px 16px 8px;white-space:nowrap}.menu.active{background-color:#fff;border-color:#07d #ddd #fff;color:#000}.menu:hover{background:#def;color:#000}.menu_button{display:none}.on{color:green}.off{color:red}.div_r{background-color:#080;border-radius:4px;margin:2px;padding:1px 10px}.alert,.warning{margin-bottom:15px;padding:20px;color:#fff}.div_br{clear:both}.alert{background-color:#f44336}.warning{background-color:#ffca17}.closebtn{cursor:pointer;font-size:22px;line-height:20px;margin-left:15px;transition:.3s}.closebtn:hover{color:#000}section{overflow-x:auto;width:100%}@media screen and (max-width:960px){.showmenulabel{display:none}.menu{max-width:48px}} \ No newline at end of file From c9d39d3cb9b3e22661f585b0e43a1be28b432b9e Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 15 Aug 2022 11:49:32 +0200 Subject: [PATCH 035/113] [TouchHandler] Use 2-row alternating color, improvements --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 19 +++++++------------ src/src/Helpers/ESPEasy_TouchHandler.h | 4 ++-- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index b0381b922a..42577801e0 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -817,7 +817,11 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { addRowLabel(F("Object")); { - html_table(EMPTY_STRING, false); // Sub-table + # ifdef TOUCH_USE_EXTENDED_TOUCH + html_table(F("multi2row"), false); // Sub-table with alternating highlight per 2 rows + # else // ifdef TOUCH_USE_EXTENDED_TOUCH + html_table(EMPTY_STRING, false); // Sub-table + # endif // ifdef TOUCH_USE_EXTENDED_TOUCH html_table_header(F(" # ")); html_table_header(F("On")); html_table_header(F("Objectname")); @@ -1176,9 +1180,7 @@ String toStringNoZero(int64_t value) { bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { String config; - # ifdef TOUCH_DEBUG uint16_t saveSize = 0; - # endif // ifdef TOUCH_DEBUG # ifdef TOUCH_USE_EXTENDED_TOUCH String colorInput; @@ -1238,9 +1240,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { # endif // ifdef TOUCH_USE_EXTENDED_TOUCH settingsArray[TOUCH_CALIBRATION_START] = config; - # ifdef TOUCH_DEBUG - saveSize += config.length() + 1; - # endif // ifdef TOUCH_DEBUG + saveSize += config.length() + 1; # ifdef TOUCH_DEBUG @@ -1342,9 +1342,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { } settingsArray[objectNr + TOUCH_OBJECT_INDEX_START] = config; - # ifdef TOUCH_DEBUG - saveSize += config.length() + 1; - # endif // ifdef TOUCH_DEBUG + saveSize += config.length() + 1; # ifdef TOUCH_DEBUG @@ -1367,14 +1365,11 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { error = SaveCustomTaskSettings(event->TaskIndex, settingsArray, TOUCH_ARRAY_SIZE, 0); - # ifdef TOUCH_DEBUG - if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("TOUCH Save settings size: "); log += saveSize; addLogMove(LOG_LEVEL_INFO, log); } - # endif // ifdef TOUCH_DEBUG if (!error.isEmpty()) { addLog(LOG_LEVEL_ERROR, error); diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index c4c531fca2..3f8616a95c 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -63,9 +63,9 @@ # undef TOUCH_USE_EXTENDED_TOUCH # endif // ifdef TOUCH_USE_EXTENDED_TOUCH # endif // ifdef LIMIT_BUILD_SIZE -# if defined(TOUCH_USE_TOOLTIPS) && !defined(ENABLE_TOOLTIPS) +# if defined(TOUCH_USE_TOOLTIPS) && !FEATURE_TOOLTIPS # undef TOUCH_USE_TOOLTIPS -# endif // if defined(TOUCH_USE_TOOLTIPS) && !defined(ENABLE_TOOLTIPS) +# endif // if defined(TOUCH_USE_TOOLTIPS) && !FEATURE_TOOLTIPS // Global Settings flags # define TOUCH_FLAGS_SEND_XY 0 // Send X and Y coordinate events From ec2f62edb178836ffd1c08192ced7739e93eba8d Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 15 Aug 2022 11:51:01 +0200 Subject: [PATCH 036/113] [P123] Code improvements --- src/_P123_FT62x6Touch.ino | 5 +-- src/src/PluginStructs/P123_data_struct.cpp | 27 ++++++++-------- src/src/PluginStructs/P123_data_struct.h | 37 ++++++++++++---------- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index d405e17c4a..05e49790d7 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -6,6 +6,7 @@ /** * Changelog: + * 2022-08-15 tonhuisman: UI improvement, settings table uses alternate color per 2 rows, code improvements * 2022-06-10 tonhuisman: Remove p123_ prefixes on Settings variables * 2022-06-06 tonhuisman: Move PLUGIN_WRITE handling mostly to ESPEasy_TouchHandler (only rot and flip subcommands remain) * Move PLUGIN_GET_CONFIG_VALUE handling to ESPEasy_TouchHandler @@ -108,9 +109,9 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) { addRowLabel(F("Display task")); addTaskSelect(F("dsptask"), P123_CONFIG_DISPLAY_TASK); - #ifndef LIMIT_BUILD_SIZE + #ifndef P123_LIMIT_BUILD_SIZE addFormNote(F("Screen Width, Heigth, Rotation & Color-depth will be fetched from the Display task if possible.")); - #endif // ifndef LIMIT_BUILD_SIZE + #endif // ifndef P123_LIMIT_BUILD_SIZE } uint16_t width_ = P123_CONFIG_X_RES; diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index 5cde60b113..873b2c4186 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -65,7 +65,7 @@ bool P123_data_struct::init(struct EventStruct *event) { # ifdef PLUGIN_123_DEBUG addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Plugin & touchscreen initialized.")); } else { - addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen initialisation FAILED.")); + addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen initialization FAILED.")); # endif // PLUGIN_123_DEBUG } return isInitialized(); @@ -145,7 +145,7 @@ bool P123_data_struct::plugin_write(struct EventStruct *event, } else if (subcommand.equals(F("flip"))) { // touch,flip,<0|1> : Flip rotation by 0 or 180 degrees setRotationFlipped(event->Par2 > 0); success = true; - } else { // Rest of the commands handled by + } else { // Rest of the commands handled by ESPEasy_TouchHandler success = touchHandler->plugin_write(event, string); } } @@ -156,26 +156,27 @@ bool P123_data_struct::plugin_write(struct EventStruct *event, * Every 1/50th second we check if the screen is touched */ bool P123_data_struct::plugin_fifty_per_second(struct EventStruct *event) { - bool success = false; - if (isInitialized()) { if (touched()) { - int16_t x = 0, y = 0, ox = 0, oy = 0, rx, ry; - int16_t z = 0; + int16_t x = 0; + int16_t y = 0; + int16_t z = 0; + int16_t ox = 0; + int16_t oy = 0; readData(x, y, z, ox, oy); - rx = x; - ry = y; + int16_t rx = x; // Keep raw values + int16_t ry = y; scaleRawToCalibrated(x, y); // Map to screen coordinates if so configured - success = touchHandler->plugin_fifty_per_second(event, x, y, ox, oy, rx, ry, z); + return touchHandler->plugin_fifty_per_second(event, x, y, ox, oy, rx, ry, z); } } - return success; + return false; } /** - * Handle getting config values, mostly delegated to ESPEasy_TouchHandler + * Handle getting config values, delegated to ESPEasy_TouchHandler */ bool P123_data_struct::plugin_get_config_value(struct EventStruct *event, String & string) { @@ -345,7 +346,7 @@ void P123_data_struct::scaleRawToCalibrated(int16_t& x, } float x_fact = static_cast(touchHandler->Touch_Settings.bottom_right.x - touchHandler->Touch_Settings.top_left.x) / static_cast(_ts_x_res); - x = static_cast(round(lx / x_fact)); + x = static_cast(round(lx / x_fact)); } int16_t ly = y - touchHandler->Touch_Settings.top_left.y; @@ -356,7 +357,7 @@ void P123_data_struct::scaleRawToCalibrated(int16_t& x, ly = touchHandler->Touch_Settings.bottom_right.y; } float y_fact = (touchHandler->Touch_Settings.bottom_right.y - touchHandler->Touch_Settings.top_left.y) / _ts_y_res; - y = static_cast(round(ly / y_fact)); + y = static_cast(round(ly / y_fact)); } } } diff --git a/src/src/PluginStructs/P123_data_struct.h b/src/src/PluginStructs/P123_data_struct.h index 0540a981fb..855e15e99a 100644 --- a/src/src/PluginStructs/P123_data_struct.h +++ b/src/src/PluginStructs/P123_data_struct.h @@ -2,27 +2,32 @@ #define PLUGINSTRUCTS_P123_DATA_STRUCT_H #include "../../_Plugin_Helper.h" -#include "../../ESPEasy_common.h" -#include "../Helpers/AdafruitGFX_helper.h" -#include "../Helpers/ESPEasy_TouchHandler.h" #ifdef USES_P123 +# include "../../ESPEasy_common.h" +# include "../Helpers/AdafruitGFX_helper.h" +# include "../Helpers/ESPEasy_TouchHandler.h" + # include # ifndef LIMIT_BUILD_SIZE -# define PLUGIN_123_DEBUG // Additional debugging information +# define PLUGIN_123_DEBUG // Additional debugging information +# else // ifndef LIMIT_BUILD_SIZE +# ifndef P123_LIMIT_BUILD_SIZE // Can be set from elsewhere +# define P123_LIMIT_BUILD_SIZE +# endif // ifndef P123_LIMIT_BUILD_SIZE # endif // ifndef LIMIT_BUILD_SIZE -# define P123_CONFIG_DISPLAY_TASK PCONFIG(0) +# define P123_CONFIG_DISPLAY_TASK PCONFIG(0) -# define P123_COLOR_DEPTH PCONFIG_LONG(1) -# define P123_CONFIG_THRESHOLD PCONFIG(1) -# define P123_CONFIG_ROTATION PCONFIG(2) -# define P123_CONFIG_X_RES PCONFIG(3) -# define P123_CONFIG_Y_RES PCONFIG(4) +# define P123_COLOR_DEPTH PCONFIG_LONG(1) +# define P123_CONFIG_THRESHOLD PCONFIG(1) +# define P123_CONFIG_ROTATION PCONFIG(2) +# define P123_CONFIG_X_RES PCONFIG(3) +# define P123_CONFIG_Y_RES PCONFIG(4) -# define P123_CONFIG_DISPLAY_PREV PCONFIG(7) +# define P123_CONFIG_DISPLAY_PREV PCONFIG(7) // Default settings values # define P123_TS_THRESHOLD 40 // Threshold before the value is registered as a proper touch @@ -30,13 +35,13 @@ # define P123_TS_X_RES 320 // Pixels, should match with the screen it is mounted on # define P123_TS_Y_RES 480 -# define P123_TOUCH_X_NATIVE P123_TS_X_RES // Native touchscreen resolution, same as display resolution +# define P123_TOUCH_X_NATIVE P123_TS_X_RES // Native touchscreen resolution, same as default display resolution # define P123_TOUCH_Y_NATIVE P123_TS_Y_RES -# define P123_ROTATION_0 0 -# define P123_ROTATION_90 1 -# define P123_ROTATION_180 2 -# define P123_ROTATION_270 3 +# define P123_ROTATION_0 0 +# define P123_ROTATION_90 1 +# define P123_ROTATION_180 2 +# define P123_ROTATION_270 3 // Data structure struct P123_data_struct : public PluginTaskData_base From 4539e63cdca644c302567e4d2f1d4172088c5c75 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 17 Aug 2022 23:03:21 +0200 Subject: [PATCH 037/113] [AdaGFX] Add Slider/Gauge support (via btn subcommand), fixes --- src/src/Helpers/AdafruitGFX_helper.cpp | 189 ++++++++++++++++++++++--- src/src/Helpers/AdafruitGFX_helper.h | 61 +++++--- 2 files changed, 212 insertions(+), 38 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index 4c8a855677..74f6fb893c 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -110,6 +110,9 @@ const __FlashStringHelper* toString(const Button_layout_e layout) { case Button_layout_e::LeftBottomAligned: return F("Left-Bottom-aligned"); case Button_layout_e::NoCaption: return F("No Caption"); case Button_layout_e::Bitmap: return F("Bitmap image"); + # if ADAGFX_ENABLE_BUTTON_SLIDER + case Button_layout_e::Slider: return F("Slide control"); + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER case Button_layout_e::Alignment_MAX: break; } return F("Unsupported!"); @@ -246,18 +249,18 @@ void AdaGFXFormForeAndBackColors(const __FlashStringHelper *foregroundId, addRowLabel(F("Foreground color")); addTextBox(foregroundId, color, 11, false, false, EMPTY_STRING, EMPTY_STRING - # ifdef TOUCH_USE_TOOLTIPS + # if FEATURE_TOOLTIPS , F("Foreground color") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if FEATURE_TOOLTIPS , F("adagfxFGBGcolors") ); color = AdaGFXcolorToString(backgroundColor, colorDepth); addRowLabel(F("Background color")); addTextBox(backgroundId, color, 11, false, false, EMPTY_STRING, EMPTY_STRING - # ifdef TOUCH_USE_TOOLTIPS + # if FEATURE_TOOLTIPS , F("Background color") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if FEATURE_TOOLTIPS , F("adagfxFGBGcolors") ); # ifndef LIMIT_BUILD_SIZE @@ -632,6 +635,9 @@ String AdafruitGFX_helper::getFeatures() { # if (defined(ADAGFX_ENABLE_BUTTON_DRAW) && ADAGFX_ENABLE_BUTTON_DRAW) log += F(" btn,"); # endif // if (defined(ADAGFX_ENABLE_BUTTON_DRAW) && ADAGFX_ENABLE_BUTTON_DRAW)` + # if (defined(ADAGFX_ENABLE_BUTTON_SLIDER) && ADAGFX_ENABLE_BUTTON_SLIDER) + log += F(" slider/gauge,"); + # endif // if (defined(ADAGFX_ENABLE_BUTTON_SLIDER) && ADAGFX_ENABLE_BUTTON_SLIDER)` # if (defined(ADAGFX_ENABLE_FRAMED_WINDOW) && ADAGFX_ENABLE_FRAMED_WINDOW) log += F(" win,"); # endif // if (defined(ADAGFX_ENABLE_FRAMED_WINDOW) && ADAGFX_ENABLE_FRAMED_WINDOW) @@ -1417,7 +1423,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { // id: < 0 = clear area // type & 0x0F: 0 = none, 1 = rectangle, 2 = rounded rect., 3 = circle, // type & 0xF0 = CenterAligned, LeftAligned, TopAligned, RightAligned, BottomAligned, LeftTopAligned, RightTopAligned, - // RightBottomAligned, LeftBottomAligned, NoCaption + // RightBottomAligned, LeftBottomAligned, NoCaption, Bitmap, Slider (not a button) // (*clr = color, TaskIndex, Group and SelGrp are ignored) # if ADAGFX_ARGUMENT_VALIDATION @@ -1460,22 +1466,36 @@ bool AdafruitGFX_helper::processCommand(const String& string) { Button_layout_e buttonLayout = static_cast(nParams[7] & 0xF0); // Check mode & state: -2, -1, 0, 1 to select used colors - if (nParams[0] == 0) { - fillColor = offColor; - } + # if ADAGFX_ENABLE_BUTTON_SLIDER + + if (buttonLayout == Button_layout_e::Slider) { + if (nParams[1] == -2) { + fillColor = disabledColor; + textColor = disabledCaptionColor; + } else if (clearArea) { + fillColor = _bgcolor; // + borderColor = _bgcolor; + } + } else + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER + { + if (nParams[0] == 0) { + fillColor = offColor; + } - if ((nParams[1] == -2) || (nParams[0] < 0)) { - fillColor = disabledColor; - textColor = disabledCaptionColor; - } else if (clearArea) { - fillColor = _bgcolor; // - borderColor = _bgcolor; + if ((nParams[1] == -2) || (nParams[0] < 0)) { + fillColor = disabledColor; + textColor = disabledCaptionColor; + } else if (clearArea) { + fillColor = _bgcolor; // + borderColor = _bgcolor; + } } // Clear the area? if ((buttonType != Button_type_e::None) || clearArea) { - drawButtonShape(buttonType, + drawButtonShape(buttonLayout == Button_layout_e::Slider ? Button_type_e::Square : buttonType, // Clear full square for slider nParams[2] + _xo, nParams[3] + _yo, nParams[4], nParams[5], _bgcolor, _bgcolor); } @@ -1495,7 +1515,11 @@ bool AdafruitGFX_helper::processCommand(const String& string) { String newString; // Determine alignment parameters - if ((nParams[0] == 1) || (nParams[0] == -1)) { // 1 = on+enabled, -1 = on+disabled + if ((nParams[0] == 1) || (nParams[0] == -1) // 1 = on+enabled, -1 = on+disabled + # if ADAGFX_ENABLE_BUTTON_SLIDER + || (buttonLayout == Button_layout_e::Slider) + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER + ) { newString = sParams[12].isEmpty() ? sParams[6] : sParams[12]; } else { newString = sParams[13].isEmpty() ? sParams[6] : sParams[13]; @@ -1574,12 +1598,18 @@ bool AdafruitGFX_helper::processCommand(const String& string) { break; } case Button_layout_e::NoCaption: + # if ADAGFX_ENABLE_BUTTON_SLIDER + case Button_layout_e::Slider: // Nothing to do here (yet) + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER case Button_layout_e::Alignment_MAX: break; } - if ((buttonLayout != Button_layout_e::NoCaption) && - (buttonLayout != Button_layout_e::Bitmap)) { + if ((buttonLayout != Button_layout_e::NoCaption) + # if ADAGFX_ENABLE_BUTTON_SLIDER + && (buttonLayout != Button_layout_e::Slider) + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER + && (buttonLayout != Button_layout_e::Bitmap)) { // Set position and colors, then print _display->setCursor(nParams[2] + _xo, nParams[3] + _yo); _display->setTextColor(textColor, textColor); // transparent bg results in button color @@ -1588,6 +1618,129 @@ bool AdafruitGFX_helper::processCommand(const String& string) { // restore colors _display->setTextColor(_fgcolor, _bgcolor); } + # if ADAGFX_ENABLE_BUTTON_SLIDER + + if (buttonLayout == Button_layout_e::Slider) { + // 1) Determine direction from w/h + const bool isVertical = nParams[4] < nParams[5]; // width < height + const bool showAsCircle = borderColor == fillColor; + + // determine value and range + int16_t offI2 = 5; // half of indicator width + int16_t offG2 = 3; // half of Gauge width + int16_t offP = 0; // Offset for indicator + int16_t zeroLine = -1; // Draw a range zero-line at this offset? only when >= 0 + int percentage = 0; + float gaugeValue = 0.0f; + int16_t lowRange = 0; + int16_t highRange = 100; + float rangeFrom = 0.0f; + float rangeTo = 0.0f; + float range = 100.0f; // For percentage the range is 100 + bool useRange = false; + + if (!validFloatFromString(newString, gaugeValue)) { + percentage = nParams[0]; // Value as provided + } + + // Have a range? + if (!sParams[13].isEmpty()) { // Off caption can hold range: , + String tmp = parseString(sParams[13], 1); + const bool validFrom = validFloatFromString(tmp, rangeFrom); + tmp = parseString(sParams[13], 2); + + if (validFrom && validFloatFromString(tmp, rangeTo) && + !essentiallyEqual(rangeFrom, 0.0f) && !essentiallyEqual(rangeTo, 0.0f)) { + useRange = true; + lowRange = static_cast(min(rangeFrom, rangeTo)); + highRange = static_cast(max(rangeTo, rangeFrom)); + } + } + + // 2) Draw center-line from 0 to 100% + // 3) Draw gauge for used/filled part + // 4) Draw indicator at correct percentage index + if (showAsCircle) { // Circle indicator or full width bar indicator + offI2 = (isVertical ? nParams[4] : nParams[5]) / 4; + } + + if (useRange) { // Calculate range-boundaries + range = abs(max(rangeTo, rangeFrom) - min(rangeFrom, rangeTo)); + + if (gaugeValue > max(rangeTo, rangeFrom)) { + gaugeValue = max(rangeTo, rangeFrom); + } else if (gaugeValue < min(rangeFrom, rangeTo)) { + gaugeValue = min(rangeFrom, rangeTo); + } else { + gaugeValue -= min(rangeFrom, rangeTo); // Give it the correct Offset + } + + if ((lowRange < 0) && + (highRange > 0)) { + zeroLine = map(0, lowRange, highRange, 0, isVertical ? nParams[5] : nParams[4]); + } + } + percentage = static_cast(gaugeValue); + offP = ((((isVertical ? nParams[5] : nParams[4]) - (2 * offI2)) / range) * percentage) - 1; // keep within button borders + + if (isVertical) { + // centerline + _display->drawLine(nParams[2] + _xo + nParams[4] / 2, nParams[3] + _yo + (nParams[5] - offI2 - 1), + nParams[2] + _xo + nParams[4] / 2, nParams[3] + _yo + offI2, textColor); + + if (zeroLine > -1) { + _display->drawLine(nParams[2] + _xo, nParams[3] + _yo + (nParams[5] - zeroLine - 1), + nParams[2] + _xo + nParams[4], nParams[3] + _yo + (nParams[5] - zeroLine - 1), textColor); + } + + // Gauge + _display->fillRoundRect(nParams[2] + _xo + (nParams[4] / 2) - offG2, nParams[3] + _yo + (nParams[5] - offI2 - offP - 1), + 2 * offG2, offP, offG2, textColor); + + // Indicator/drag-handle + if (showAsCircle) { // Circle indicator + _display->fillCircle(nParams[2] + _xo + nParams[4] / 2, + nParams[3] + _yo + (nParams[5] - offI2 - offP - 2) + (percentage == 100 ? 1 : 0), + trunc(nParams[4] / 4), textColor); + } else { + _display->fillRoundRect(nParams[2] + _xo + 1, nParams[3] + _yo + (nParams[5] - (2 * offI2) - offP - 1), + nParams[4] - 2, 2 * offI2, offI2, textColor); + } + } else { // : if !isVertical + // centerline + _display->drawLine(nParams[2] + _xo + offI2 + 1, nParams[3] + _yo + nParams[5] / 2, + nParams[2] + _xo + nParams[4] - offI2, nParams[3] + _yo + nParams[5] / 2, textColor); + + if (zeroLine > -1) { + _display->drawLine(nParams[2] + _xo + zeroLine + 1, nParams[3] + _yo, + nParams[2] + _xo + zeroLine + 1, nParams[3] + _yo + nParams[5], textColor); + } + + // Gauge + _display->fillRoundRect(nParams[2] + _xo + offI2 + 1, nParams[3] + _yo + (nParams[5] / 2) - offG2, + offP, offG2 * 2, offG2, textColor); + + // Indicator/drag-handle + if (showAsCircle) { // Circle indicator + _display->fillCircle(nParams[2] + _xo + offP + offI2 + 1 - (percentage == 100 ? 1 : 0), + nParams[3] + _yo + (nParams[5] / 2), + trunc(nParams[5] / 4), textColor); + } else { + _display->fillRoundRect(nParams[2] + _xo + offP + 1, nParams[3] + _yo + 1, + 2 * offI2, nParams[5] - 2, offI2, textColor); + } + } + + // 5) Draw percentage in center if Fontsize > 0 + if (fontScale > 0) { + nParams[2] += (nParams[4] / 2 - w1 / 2); // center horizontically + nParams[3] += (nParams[5] / 2 - h1 / 2); // center vertically + _display->setCursor(nParams[2] + _xo, nParams[3] + _yo); + _display->setTextColor(textColor, fillColor); // regular bg color for readability + _display->print(newString); + } + } + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER // restore font scaling _display->setTextSize(_fontscaling); diff --git a/src/src/Helpers/AdafruitGFX_helper.h b/src/src/Helpers/AdafruitGFX_helper.h index b75cb6db7b..d6abc232ab 100644 --- a/src/src/Helpers/AdafruitGFX_helper.h +++ b/src/src/Helpers/AdafruitGFX_helper.h @@ -12,6 +12,8 @@ ***************************************************************************/ /************ * Changelog: + * 2022-08-16 tonhuisman: Add drawing of Slide/Gauge controls via btn subcommand, horizontal or vertical depending on width/height ratio + * 2022-08-15 tonhuisman: Add initial support for slide/gauge controls * 2022-06-07 tonhuisman: Code improvements in initialization, move offset calculation to printText() function * 2022-06-06 tonhuisman: Process any special characters for lenght and textheight values for correct sizing * 2022-06-05 tonhuisman: Add support for getting config values: win (current window id), iswin (exists?), width & height (current window), @@ -59,17 +61,20 @@ # define ADAGFX_FONTS_INCLUDED 1 // 3 extra fonts, also controls enable/disable of below 8pt/12pt fonts # endif // ifndef ADAGFX_FONTS_INCLUDED # ifndef ADAGFX_PARSE_SUBCOMMAND -# define ADAGFX_PARSE_SUBCOMMAND 1 // Enable parsing of subcommands (pre/postfix below) to be executed by the helper +# define ADAGFX_PARSE_SUBCOMMAND 1 // Enable/disable parsing of subcommands (pre/postfix below) to be executed by the helper # endif // ifndef ADAGFX_PARSE_SUBCOMMAND # ifndef ADAGFX_ENABLE_EXTRA_CMDS -# define ADAGFX_ENABLE_EXTRA_CMDS 1 // Enable extra subcommands like lm (line-multi) and lmr (line-multi, relative) +# define ADAGFX_ENABLE_EXTRA_CMDS 1 // Enable/disable extra subcommands like lm (line-multi) and lmr (line-multi, relative) # endif // ifndef ADAGFX_ENABLE_EXTRA_CMDS # ifndef ADAGFX_ENABLE_BMP_DISPLAY -# define ADAGFX_ENABLE_BMP_DISPLAY 1 // Enable subcommands for displaying .bmp files on supported displays (color) +# define ADAGFX_ENABLE_BMP_DISPLAY 1 // Enable/disable subcommands for displaying .bmp files on supported displays (color) # endif // ifndef ADAGFX_ENABLE_BMP_DISPLAY # ifndef ADAGFX_ENABLE_BUTTON_DRAW -# define ADAGFX_ENABLE_BUTTON_DRAW 1 // Enable subcommands for displaying button-like shapes +# define ADAGFX_ENABLE_BUTTON_DRAW 1 // Enable/disable subcommands for displaying button-like shapes # endif // ifndef ADAGFX_ENABLE_BUTTON_DRAW +# ifndef ADAGFX_ENABLE_BUTTON_SLIDER +# define ADAGFX_ENABLE_BUTTON_SLIDER 1 // Enable/disable displaying button-shape with slider-actions +# endif // ifndef ADAGFX_ENABLE_BUTTON_SLIDER # ifndef ADAGFX_ENABLE_FRAMED_WINDOW # define ADAGFX_ENABLE_FRAMED_WINDOW 1 // Enable framed window features # endif // ifndef ADAGFX_ENABLE_BUTTON_DRAW @@ -109,30 +114,41 @@ # define ADAGFX_FONTS_EXTRA_20PT_WHITERABBiT # ifdef LIMIT_BUILD_SIZE -# ifdef ADAGFX_FONTS_INCLUDED +# if ADAGFX_FONTS_INCLUDED # undef ADAGFX_FONTS_INCLUDED -# endif // ifdef ADAGFX_FONTS_INCLUDED -# ifdef ADAGFX_ARGUMENT_VALIDATION +# define ADAGFX_FONTS_INCLUDED 0 +# endif // if ADAGFX_FONTS_INCLUDED +# if ADAGFX_ARGUMENT_VALIDATION # undef ADAGFX_ARGUMENT_VALIDATION -# endif // ifdef ADAGFX_ARGUMENT_VALIDATION -# ifdef ADAGFX_USE_ASCIITABLE +# define ADAGFX_ARGUMENT_VALIDATION 0 +# endif // if ADAGFX_ARGUMENT_VALIDATION +# if ADAGFX_USE_ASCIITABLE # undef ADAGFX_USE_ASCIITABLE -# endif // ifdef ADAGFX_USE_ASCIITABLE -# ifdef ADAGFX_SUPPORT_8and16COLOR +# define ADAGFX_USE_ASCIITABLE 0 +# endif // if ADAGFX_USE_ASCIITABLE +# if ADAGFX_SUPPORT_8and16COLOR # undef ADAGFX_SUPPORT_8and16COLOR -# endif // ifdef ADAGFX_SUPPORT_8and16COLOR -// # ifdef ADAGFX_ENABLE_BMP_DISPLAY +# define ADAGFX_SUPPORT_8and16COLOR 0 +# endif // if ADAGFX_SUPPORT_8and16COLOR +// # if ADAGFX_ENABLE_BMP_DISPLAY // # undef ADAGFX_ENABLE_BMP_DISPLAY -// # endif // ifdef ADAGFX_ENABLE_BMP_DISPLAY -// # ifdef ADAGFX_ENABLE_BUTTON_DRAW +// # define ADAGFX_ENABLE_BMP_DISPLAY 0 +// # endif // if ADAGFX_ENABLE_BMP_DISPLAY +// # if ADAGFX_ENABLE_BUTTON_DRAW // # undef ADAGFX_ENABLE_BUTTON_DRAW -// # endif // ifdef ADAGFX_ENABLE_BUTTON_DRAW -# ifdef ADAGFX_ENABLE_FRAMED_WINDOW +// # define ADAGFX_ENABLE_BUTTON_DRAW 0 +// # endif // if ADAGFX_ENABLE_BUTTON_DRAW +# if ADAGFX_ENABLE_FRAMED_WINDOW # undef ADAGFX_ENABLE_FRAMED_WINDOW -# endif // ifdef ADAGFX_ENABLE_FRAMED_WINDOW +# define ADAGFX_ENABLE_FRAMED_WINDOW 0 +# endif // if ADAGFX_ENABLE_FRAMED_WINDOW // # ifdef ADAGFX_ENABLE_GET_CONFIG_VALUE // # undef ADAGFX_ENABLE_GET_CONFIG_VALUE // # endif // ifdef ADAGFX_ENABLE_GET_CONFIG_VALUE +# if ADAGFX_ENABLE_BUTTON_SLIDER +# undef ADAGFX_ENABLE_BUTTON_SLIDER +# define ADAGFX_ENABLE_BUTTON_SLIDER 0 // Disable displaying button-shape with slider-actions +# endif // if ADAGFX_ENABLE_BUTTON_SLIDER # endif // ifdef LIMIT_BUILD_SIZE # ifdef PLUGIN_SET_MAX // Include all fonts in MAX builds @@ -279,7 +295,12 @@ enum class Button_layout_e : uint8_t { LeftBottomAligned = 0x80, NoCaption = 0x90, Bitmap = 0xA0, - Alignment_MAX = 11u // options-count, max possible values: 16 + # if ADAGFX_ENABLE_BUTTON_SLIDER + Slider = 0xB0, + Alignment_MAX = 12u // options-count, max possible values: 16 + # else // if ADAGFX_ENABLE_BUTTON_SLIDER + Alignment_MAX = 11u // options-count, max possible values: 16 + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER }; const __FlashStringHelper* toString(const Button_type_e button); @@ -455,7 +476,7 @@ class AdafruitGFX_helper { const int16_t& h, int16_t windowId = -1, const int8_t & rotation = -1); - bool deleteWindow(const uint8_t& windowId); + bool deleteWindow(const uint8_t& windowId); # endif // if ADAGFX_ENABLE_FRAMED_WINDOW uint16_t getTextSize(const String& text, From 4e3d4d4200ac919dc4a66f2520c0982af0f52a54 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 17 Aug 2022 23:08:36 +0200 Subject: [PATCH 038/113] [AdaGFX] Missed a conditional compile option --- src/src/Helpers/AdafruitGFX_helper.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index 74f6fb893c..d9a8da82df 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -1495,9 +1495,13 @@ bool AdafruitGFX_helper::processCommand(const String& string) { // Clear the area? if ((buttonType != Button_type_e::None) || clearArea) { - drawButtonShape(buttonLayout == Button_layout_e::Slider ? Button_type_e::Square : buttonType, // Clear full square for slider - nParams[2] + _xo, nParams[3] + _yo, nParams[4], nParams[5], - _bgcolor, _bgcolor); + drawButtonShape( + # if ADAGFX_ENABLE_BUTTON_SLIDER + buttonLayout == Button_layout_e::Slider ? Button_type_e::Square : + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER + buttonType, // Clear full square for slider + nParams[2] + _xo, nParams[3] + _yo, nParams[4], nParams[5], + _bgcolor, _bgcolor); } // Check button-type bits (mask: 0x0F) to draw correct shape @@ -1757,7 +1761,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # if ADAGFX_ARGUMENT_VALIDATION const int16_t curWin = getWindow(); - if (curWin != 0) { selectWindow(0); } // Validate against raw window coordinates + if (curWin != 0) { selectWindow(0); } // Validate against raw window coordinates if (argCount == 6) { setRotation(nParams[5]); } // Use requested rotation From 4b81a693904029dd37b5a383cc18e0e3fb6b6801 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 17 Aug 2022 23:12:37 +0200 Subject: [PATCH 039/113] [TouchHandler] Add support for sliders and swiping, many other improvements --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 809 +++++++++++++++++------ src/src/Helpers/ESPEasy_TouchHandler.h | 182 +++-- 2 files changed, 725 insertions(+), 266 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 42577801e0..783fd0e077 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -5,7 +5,7 @@ /**************************************************************************** * toString: Display-value for the touch action ***************************************************************************/ -# ifdef TOUCH_USE_EXTENDED_TOUCH +# if TOUCH_FEATURE_EXTENDED_TOUCH const __FlashStringHelper* toString(Touch_action_e action) { switch (action) { case Touch_action_e::Default: return F("Default"); @@ -19,7 +19,28 @@ const __FlashStringHelper* toString(Touch_action_e action) { return F("Unsupported!"); } -# endif // ifdef TOUCH_USE_EXTENDED_TOUCH +# endif // if TOUCH_FEATURE_EXTENDED_TOUCH + +/**************************************************************************** + * toString: Display-value for the swipe action + ***************************************************************************/ +# if TOUCH_FEATURE_SWIPE +const __FlashStringHelper* toString(Swipe_action_e action) { + switch (action) { + case Swipe_action_e::Up: return F("Up"); + case Swipe_action_e::UpRight: return F("Up-Right"); + case Swipe_action_e::Right: return F("Right"); + case Swipe_action_e::RightDown: return F("Right-Down"); + case Swipe_action_e::Down: return F("Down"); + case Swipe_action_e::DownLeft: return F("Down-Left"); + case Swipe_action_e::Left: return F("Left"); + case Swipe_action_e::LeftUp: return F("Left-Up"); + case Swipe_action_e::None: return F("None"); + } + return F("Unknown"); +} + +# endif // if TOUCH_FEATURE_SWIPE /** * Constructors @@ -31,7 +52,7 @@ ESPEasy_TouchHandler::ESPEasy_TouchHandler(const uint16_t & displayTask, : _displayTask(displayTask), _colorDepth(colorDepth) {} /** - * Load the touch objects from the settings, and initialize then properly where needed. + * Load the touch objects from the settings, and initialize them properly where needed. */ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { # ifdef TOUCH_DEBUG @@ -74,7 +95,7 @@ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { TOUCH_SETTINGS_SEPARATOR); Touch_Settings.debounceMs = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], TOUCH_COMMON_DEBOUNCE_MS, TOUCH_SETTINGS_SEPARATOR, TOUCH_DEBOUNCE_MILLIS); - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH Touch_Settings.colorOn = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], TOUCH_COMMON_DEF_COLOR_ON, TOUCH_SETTINGS_SEPARATOR); Touch_Settings.colorOff = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], @@ -88,7 +109,7 @@ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { Touch_Settings.colorDisabledCaption = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], TOUCH_COMMON_DEF_COLOR_DISABCAPT, TOUCH_SETTINGS_SEPARATOR); - if ((Touch_Settings.colorOn == 0u) && + if ((Touch_Settings.colorOn == 0u) && // Validate and set defaults (Touch_Settings.colorOff == 0u) && (Touch_Settings.colorCaption == 0u) && (Touch_Settings.colorBorder == 0u) && @@ -101,7 +122,13 @@ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { Touch_Settings.colorDisabled = TOUCH_DEFAULT_COLOR_DISABLED; Touch_Settings.colorDisabledCaption = TOUCH_DEFAULT_COLOR_DISABLED_CAPTION; } - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + # if TOUCH_FEATURE_SWIPE + Touch_Settings.swipeMinimal = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], + TOUCH_COMMON_SWIPE_MINIMAL, TOUCH_SETTINGS_SEPARATOR, TOUCH_DEF_SWIPE_MINIMAL); + Touch_Settings.swipeMargin = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], + TOUCH_COMMON_SWIPE_MARGIN, TOUCH_SETTINGS_SEPARATOR, TOUCH_DEF_SWIPE_MARGIN); + # endif // if TOUCH_FEATURE_SWIPE settingsArray[TOUCH_CALIBRATION_START].clear(); // Free a little memory @@ -124,7 +151,7 @@ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { TouchObjects[t].top_left.y = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COORD_TOP_Y, TOUCH_SETTINGS_SEPARATOR); TouchObjects[t].width_height.x = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COORD_WIDTH, TOUCH_SETTINGS_SEPARATOR); TouchObjects[t].width_height.y = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COORD_HEIGHT, TOUCH_SETTINGS_SEPARATOR); - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH TouchObjects[t].colorOn = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_ON, TOUCH_SETTINGS_SEPARATOR); TouchObjects[t].colorOff = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_OFF, TOUCH_SETTINGS_SEPARATOR); TouchObjects[t].colorCaption = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_CAPTION, TOUCH_SETTINGS_SEPARATOR); @@ -138,11 +165,11 @@ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { if (!validButtonGroup(get8BitFromUL(TouchObjects[t].flags, TOUCH_OBJECT_FLAG_GROUP))) { _buttonGroups.insert(get8BitFromUL(TouchObjects[t].flags, TOUCH_OBJECT_FLAG_GROUP)); } - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH - TouchObjects[t].SurfaceAreas = 0; // Reset runtime stuff - TouchObjects[t].TouchTimers = 0; - TouchObjects[t].TouchStates = false; + TouchObjects[t].SurfaceAreas = 0u; // Reset runtime stuff + TouchObjects[t].TouchTimers = 0u; + TouchObjects[t].TouchStates = 0; t++; @@ -150,10 +177,6 @@ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { } } } - - // if (_maxButtonGroup > 0) { - // _minButtonGroup = 1; - // } } /** @@ -165,13 +188,15 @@ void ESPEasy_TouchHandler::init(struct EventStruct *event) { _settingsLoaded = true; } + # if TOUCH_FEATURE_EXTENDED_TOUCH + if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME) && bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)) { if (_buttonGroups.size() > 1) { // Multiple groups? displayButtonGroup(event, _buttonGroup, -3); // Clear all displayed groups } _buttonGroup = get8BitFromUL(Touch_Settings.flags, TOUCH_FLAGS_INITIAL_GROUP); - # ifdef TOUCH_DEBUG + # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { String log = F("TOUCH DEBUG group: "); @@ -180,14 +205,15 @@ void ESPEasy_TouchHandler::init(struct EventStruct *event) { log += *_buttonGroups.crbegin(); addLogMove(LOG_LEVEL_INFO, log); } - # endif // ifdef TOUCH_DEBUG + # endif // ifdef TOUCH_DEBUG displayButtonGroup(event, _buttonGroup); // Initialize selected group and group 0 - # ifdef TOUCH_DEBUG + # ifdef TOUCH_DEBUG addLogMove(LOG_LEVEL_INFO, F("TOUCH DEBUG group done.")); - # endif // ifdef TOUCH_DEBUG + # endif // ifdef TOUCH_DEBUG } + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } /** @@ -230,9 +256,11 @@ bool ESPEasy_TouchHandler::isValidAndTouchedTouchObject(const int16_t& x, int8_t & selectedObjectIndex) { uint32_t lastObjectArea = 0u; bool selected = false; + uint16_t _x = static_cast(x); + uint16_t _y = static_cast(y); - for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { - uint8_t group = get8BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_GROUP); + for (size_t objectNr = 0; objectNr < TouchObjects.size(); objectNr++) { + const uint8_t group = get8BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_GROUP); if (!TouchObjects[objectNr].objectName.isEmpty() && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED) @@ -243,10 +271,10 @@ bool ESPEasy_TouchHandler::isValidAndTouchedTouchObject(const int16_t& x, TouchObjects[objectNr].SurfaceAreas = TouchObjects[objectNr].width_height.x * TouchObjects[objectNr].width_height.y; } - if ((TouchObjects[objectNr].top_left.x <= x) - && (TouchObjects[objectNr].top_left.y <= y) - && ((TouchObjects[objectNr].width_height.x + TouchObjects[objectNr].top_left.x) >= x) - && ((TouchObjects[objectNr].width_height.y + TouchObjects[objectNr].top_left.y) >= y) + if ((TouchObjects[objectNr].top_left.x <= _x) + && (TouchObjects[objectNr].top_left.y <= _y) + && ((TouchObjects[objectNr].width_height.x + TouchObjects[objectNr].top_left.x) >= _x) + && ((TouchObjects[objectNr].width_height.y + TouchObjects[objectNr].top_left.y) >= _y) && ((lastObjectArea == 0) || (TouchObjects[objectNr].SurfaceAreas < lastObjectArea))) { // Select smallest area that fits the coordinates selectedObjectName = TouchObjects[objectNr].objectName; @@ -334,7 +362,11 @@ bool ESPEasy_TouchHandler::setTouchObjectState(struct EventStruct *event, // Event when enabling/disabling if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME) && bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)) { - generateObjectEvent(event, objectNr, TouchObjects[objectNr].TouchStates ? 1 : 0, state ? -1 : -2); // Redraw only, no activation + generateObjectEvent(event, objectNr, + bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_SLIDER) ? + TouchObjects[objectNr].TouchStates : + (TouchObjects[objectNr].TouchStates > 0 ? 1 : 0), + state ? -1 : -2); // Redraw only, no activation } } # ifdef TOUCH_DEBUG @@ -420,24 +452,69 @@ bool ESPEasy_TouchHandler::setTouchButtonOnOff(struct EventStruct *event, /** * Get the on/off state of an enabled touch-button object. */ -int8_t ESPEasy_TouchHandler::getTouchButtonOnOff(struct EventStruct *event, - const String & touchObject) { - if (touchObject.isEmpty()) { return false; } - int8_t result = -1; // invalid button +int16_t ESPEasy_TouchHandler::getTouchObjectValue(struct EventStruct *event, + const String & touchObject) { + if (touchObject.isEmpty()) { return -1; } + int16_t result = -1; // invalid object - int8_t objectNr = getTouchObjectIndex(event, touchObject, true); + int8_t objectNr = getTouchObjectIndex(event, touchObject); if ((objectNr > -1) - && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED) - && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTON)) { - result = TouchObjects[objectNr].TouchStates && !bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_INVERTED) ? 1 : 0; + && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED)) { + if (bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTON)) { + result = TouchObjects[objectNr].TouchStates > 0 && + !bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_INVERTED) ? 1 : 0; + } else { + result = TouchObjects[objectNr].TouchStates; + } } return result; } +/** + * Set the value of any enabled touch-object. Will generate an event if so configured. + */ +bool ESPEasy_TouchHandler::setTouchObjectValue(struct EventStruct *event, + const String & touchObject, + const uint16_t & value) { + if (touchObject.isEmpty()) { return false; } + bool success = false; + + int8_t objectNr = getTouchObjectIndex(event, touchObject, false); + + if ((objectNr > -1) + && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED)) { + success = true; // Always success if matched object + + if (value != TouchObjects[objectNr].TouchStates) { + TouchObjects[objectNr].TouchStates = value; + + // Send event like it was pressed + if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME) && + bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)) { + generateObjectEvent(event, objectNr, value); + } + } + # ifdef TOUCH_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("TOUCH setTouchObjectValue: obj: "); + log += touchObject; + log += '/'; + log += objectNr; + log += F(", new value: "); + log += value; + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // ifdef TOUCH_DEBUG + } + return success; +} + /** * mode: -2 = clear buttons in group, -3 = clear all buttongroups, -1 = draw buttons in group, 0 = initialize buttons */ +# if TOUCH_FEATURE_EXTENDED_TOUCH void ESPEasy_TouchHandler::displayButtonGroup(struct EventStruct *event, const int16_t & buttonGroup, const int8_t & mode) { @@ -475,12 +552,12 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, int8_t state = 99; int16_t group = get8BitFromUL(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_GROUP); - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH Touch_action_e action = static_cast(get4BitFromUL(TouchObjects[buttonNr].groupFlags, TOUCH_OBJECT_GROUP_ACTION)); - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH bool isArrow = false; - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH if ((mode > -2) && // Not on clear (-2 and -3) bitRead(Touch_Settings.flags, TOUCH_FLAGS_AUTO_PAGE_ARROWS) && @@ -490,18 +567,19 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, (action == Touch_action_e::IncrementPage))) { isArrow = true; } - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH if (!TouchObjects[buttonNr].objectName.isEmpty() && ((bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED) && (group == 0)) || (group > 0) || isArrow) && - bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_BUTTON) && + (bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_BUTTON) || + bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_SLIDER)) && (((group == buttonGroup) || (buttonGroup < 0)) || ((mode != -2) && (group == 0)) || (mode == -3))) { // Act like a button, 1 = On, 0 = Off, inversion is handled in generateObjectEvent() - state = TouchObjects[buttonNr].TouchStates ? 1 : 0; + state = TouchObjects[buttonNr].TouchStates; - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH if (isArrow) { // Auto-Enable/Disable the arrow buttons bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); @@ -520,7 +598,7 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, bitWrite(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED, validButtonGroup(buttonGroup + (pgupInvert ? -10 : 10), true)); } } - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH if (bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED)) { if (mode == 0) { @@ -531,7 +609,7 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, } generateObjectEvent(event, buttonNr, state, mode, mode < 0, mode <= -2 ? -1 : 1); } - # ifdef TOUCH_DEBUG + # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log = F("TOUCH: button init, state: "); @@ -548,27 +626,73 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, log += buttonNr; addLog(LOG_LEVEL_DEBUG, log); } - # endif // ifdef TOUCH_DEBUG + # endif // ifdef TOUCH_DEBUG return true; } /** - * Check if this is a valid button group + * Check if this is a valid button group, 2022-08-16: default changed to IGNORE group 0! * When ignoreZero = true will return false for group 0 if the number of groups > 1. + * When ignoreZero = false will return true for group 0 also if the number of groups > 1. * NB: Group 0 is always available, even without button definitions! */ bool ESPEasy_TouchHandler::validButtonGroup(const int16_t& group, - const bool & ignoreZero) { + const bool & ignoreZero /* = true*/) { return _buttonGroups.find(group) != _buttonGroups.end() && (!ignoreZero || group > 0 || (group == 0 && _buttonGroups.size() == 1)); } +# if TOUCH_FEATURE_SWIPE + +/** + * set button group page via the Swipe event + */ +bool ESPEasy_TouchHandler::handleButtonSwipe(struct EventStruct *event, + const int16_t & swipeValue) { + bool success = false; + Swipe_action_e swipe = static_cast(swipeValue); + bool swapped = bitRead(Touch_Settings.flags, TOUCH_FLAGS_SWAP_LEFT_RIGHT); + + if (swipe == Swipe_action_e::Up) { + if (swapped) { + decrementButtonPage(event); + } else { + incrementButtonPage(event); + } + success = true; + } else if (swipe == Swipe_action_e::Right) { + if (swapped) { + decrementButtonGroup(event); + } else { + incrementButtonGroup(event); + } + success = true; + } else if (swipe == Swipe_action_e::Down) { + if (swapped) { + incrementButtonPage(event); + } else { + decrementButtonPage(event); + } + success = true; + } else if ((swipe == Swipe_action_e::Left)) { + if (swapped) { + incrementButtonGroup(event); + } else { + decrementButtonGroup(event); + } + success = true; + } + return success; +} + +# endif // if TOUCH_FEATURE_SWIPE + /** * Set the desired button group, must be a known group, previous group will be erased and new group drawn */ bool ESPEasy_TouchHandler::setButtonGroup(struct EventStruct *event, const int16_t & buttonGroup) { - if (validButtonGroup(buttonGroup)) { + if (validButtonGroup(buttonGroup, false)) { // We want to be able to select group 0 if (buttonGroup != _buttonGroup) { displayButtonGroup(event, _buttonGroup, -2); _buttonGroup = buttonGroup; @@ -623,6 +747,8 @@ bool ESPEasy_TouchHandler::decrementButtonPage(struct EventStruct *event) { return false; } +# endif // if TOUCH_FEATURE_EXTENDED_TOUCH + /** * Load the settings onto the webpage */ @@ -645,15 +771,15 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { { F("None"), F("X and Y"), F("X, Y and Z"), - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH F("Objectnames and Button groups"), F("Objectnames, Button groups, X and Y"), F("Objectnames, Button groups, X, Y and Z") - # else // ifdef TOUCH_USE_EXTENDED_TOUCH + # else // if TOUCH_FEATURE_EXTENDED_TOUCH F("Objectnames only"), F("Objectnames, X and Y"), F("Objectnames, X, Y and Z") - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH }; const int optionValues3[TOUCH_EVENTS_OPTIONS] = { 0, 1, 3, 4, 5, 7 }; // Already used as a bitmap! addFormSelector(F("Events"), F("events"), TOUCH_EVENTS_OPTIONS, options3, optionValues3, choice3); @@ -730,7 +856,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { addFormSubHeader(F("Touch objects")); - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH AdaGFXHtmlColorDepthDataList(F("adagfx65kcolors"), _colorDepth); @@ -749,54 +875,54 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { parsed = AdaGFXcolorToString(Touch_Settings.colorOn, _colorDepth, true); addTextBox(getPluginCustomArgName(3000), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, EMPTY_STRING, F("widenumber") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("ON color") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS , F("adagfx65kcolors") ); html_TD(); // OFF color parsed = AdaGFXcolorToString(Touch_Settings.colorOff, _colorDepth, true); addTextBox(getPluginCustomArgName(3001), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, EMPTY_STRING, F("widenumber") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("OFF color") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS , F("adagfx65kcolors") ); html_TD(); // Border color parsed = AdaGFXcolorToString(Touch_Settings.colorBorder, _colorDepth, true); addTextBox(getPluginCustomArgName(3002), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, EMPTY_STRING, F("widenumber") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("Border color") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS , F("adagfx65kcolors") ); html_TD(); // Caption color parsed = AdaGFXcolorToString(Touch_Settings.colorCaption, _colorDepth, true); addTextBox(getPluginCustomArgName(3003), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, EMPTY_STRING, F("widenumber") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("Caption color") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS , F("adagfx65kcolors") ); html_TD(); // Disabled color parsed = AdaGFXcolorToString(Touch_Settings.colorDisabled, _colorDepth, true); addTextBox(getPluginCustomArgName(3004), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, EMPTY_STRING, F("widenumber") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("Disabled color") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS , F("adagfx65kcolors") ); html_TD(); // Disabled caption color parsed = AdaGFXcolorToString(Touch_Settings.colorDisabledCaption, _colorDepth, true); addTextBox(getPluginCustomArgName(3005), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, EMPTY_STRING, F("widenumber") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("Disabled caption color") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS , F("adagfx65kcolors") ); html_end_table(); @@ -804,30 +930,35 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { { addFormNumericBox(F("Initial button group"), F("initial_group"), get8BitFromUL(Touch_Settings.flags, TOUCH_FLAGS_INITIAL_GROUP), 0, TOUCH_MAX_BUTTON_GROUPS - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("Initial group") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); - addFormCheckBox(F("Draw buttons via Rules"), F("via_rules"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_DRAWBTN_VIA_RULES)); - addFormCheckBox(F("Enable/Disable page buttons"), F("page_buttons"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_AUTO_PAGE_ARROWS)); - addFormCheckBox(F("PageUp/PageDown reversed"), F("page_below"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU)); + addFormCheckBox(F("Draw buttons via Rules"), F("via_rules"), + bitRead(Touch_Settings.flags, TOUCH_FLAGS_DRAWBTN_VIA_RULES)); + addFormCheckBox(F("Enable/Disable page buttons"), F("page_buttons"), + bitRead(Touch_Settings.flags, TOUCH_FLAGS_AUTO_PAGE_ARROWS)); + addFormCheckBox(F("PageUp/PageDown reversed"), F("page_below"), + bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU)); + addFormCheckBox(F("Swipe Left/Right/Up/Down menu reversed"), F("swipeswap"), + bitRead(Touch_Settings.flags, TOUCH_FLAGS_SWAP_LEFT_RIGHT)); } - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH { addRowLabel(F("Object")); { - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH html_table(F("multi2row"), false); // Sub-table with alternating highlight per 2 rows - # else // ifdef TOUCH_USE_EXTENDED_TOUCH + # else // if TOUCH_FEATURE_EXTENDED_TOUCH html_table(EMPTY_STRING, false); // Sub-table - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH html_table_header(F(" # ")); html_table_header(F("On")); html_table_header(F("Objectname")); html_table_header(F("Top-left x")); html_table_header(F("Top-left y")); - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH html_table_header(F("Button")); html_table_header(F("Layout")); html_table_header(F("ON color")); @@ -835,30 +966,30 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { html_table_header(F("Border color")); html_table_header(F("Disab. cap. clr")); html_table_header(F("Touch action")); - # else // ifdef TOUCH_USE_EXTENDED_TOUCH + # else // if TOUCH_FEATURE_EXTENDED_TOUCH html_table_header(F("On/Off button")); - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH html_TR(); // New row html_table_header(EMPTY_STRING); html_table_header(EMPTY_STRING); - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH html_table_header(F("Button-group")); - # else // ifdef TOUCH_USE_EXTENDED_TOUCH + # else // if TOUCH_FEATURE_EXTENDED_TOUCH html_table_header(EMPTY_STRING); - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH html_table_header(F("Width")); html_table_header(F("Height")); html_table_header(F("Inverted")); - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH html_table_header(F("Font scale")); html_table_header(F("OFF color")); html_table_header(F("OFF caption")); html_table_header(F("Caption color")); html_table_header(F("Disabled clr")); html_table_header(F("Action group")); - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH const __FlashStringHelper *buttonTypeOptions[] = { toString(Button_type_e::None), toString(Button_type_e::Square), @@ -893,6 +1024,9 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { toString(Button_layout_e::RightBottomAligned), toString(Button_layout_e::NoCaption), toString(Button_layout_e::Bitmap), + # if ADAGFX_ENABLE_BUTTON_SLIDER + toString(Button_layout_e::Slider), + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER }; const int buttonLayoutValues[] = { @@ -907,6 +1041,9 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { static_cast(Button_layout_e::RightBottomAligned), static_cast(Button_layout_e::NoCaption), static_cast(Button_layout_e::Bitmap), + # if ADAGFX_ENABLE_BUTTON_SLIDER + static_cast(Button_layout_e::Slider), + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER }; const __FlashStringHelper *touchActionOptions[] = { @@ -927,7 +1064,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { static_cast(Touch_action_e::DecrementPage), }; - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH uint8_t maxIdx = std::min(static_cast(TouchObjects.size() + TOUCH_EXTRA_OBJECT_COUNT), TOUCH_MAX_OBJECT_COUNT); String parsed; @@ -944,9 +1081,9 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { bool enabled = bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED) || TouchObjects[objectNr].objectName.isEmpty(); addCheckBox(getPluginCustomArgName(objectNr + 0), enabled, false - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("Enabled") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); html_TD(); // Name addTextBox(getPluginCustomArgName(objectNr + 100), @@ -956,28 +1093,28 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { html_TD(); // top-x addNumericBox(getPluginCustomArgName(objectNr + 200), TouchObjects[objectNr].top_left.x, 0, 65535 - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("widenumber"), F("Top-left x") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); html_TD(); // top-y addNumericBox(getPluginCustomArgName(objectNr + 300), TouchObjects[objectNr].top_left.y, 0, 65535 - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("widenumber"), F("Top-left y") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); html_TD(); // (on/off) button (type) - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH addSelector(getPluginCustomArgName(objectNr + 800), static_cast(Button_type_e::Button_MAX), buttonTypeOptions, buttonTypeValues, nullptr, get4BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTONTYPE), false, true, F("widenumber") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("Buttontype") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); html_TD(); // button alignment addSelector(getPluginCustomArgName(objectNr + 900), @@ -986,26 +1123,26 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { buttonLayoutValues, nullptr, get4BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTONALIGN) << 4, false, true, F("widenumber") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("Button alignment") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); - # else // ifdef TOUCH_USE_EXTENDED_TOUCH + # else // if TOUCH_FEATURE_EXTENDED_TOUCH addCheckBox(getPluginCustomArgName(objectNr + 600), bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTON), false - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("On/Off button") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH - # ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH html_TD(); // ON color parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorOn, _colorDepth, true); addTextBox(getPluginCustomArgName(objectNr + 1000), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, EMPTY_STRING, F("widenumber") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("ON color") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS , F("adagfx65kcolors") ); html_TD(); // ON Caption @@ -1018,26 +1155,26 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { false, EMPTY_STRING, F("wide") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("ON caption") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); html_TD(); // Border color parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorBorder, _colorDepth, true); addTextBox(getPluginCustomArgName(objectNr + 1700), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, EMPTY_STRING, F("widenumber") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("Border color") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS , F("adagfx65kcolors") ); html_TD(); // Disabled caption color parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorDisabledCaption, _colorDepth, true); addTextBox(getPluginCustomArgName(objectNr + 1900), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, EMPTY_STRING, F("widenumber") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("Disabled caption color") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS , F("adagfx65kcolors") ); html_TD(); // button action @@ -1050,66 +1187,66 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { false, true, F("widenumber") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("Touch action") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH html_TR_TD(); // Start new row html_TD(2); // Start with some blank columns - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH { - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS String buttonGroupToolTip = F("Button-group [0.."); buttonGroupToolTip += TOUCH_MAX_BUTTON_GROUPS; buttonGroupToolTip += ']'; - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS addNumericBox(getPluginCustomArgName(objectNr + 1600), get8BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_GROUP), 0, TOUCH_MAX_BUTTON_GROUPS - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("widenumber"), buttonGroupToolTip - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); } - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH html_TD(); // Width addNumericBox(getPluginCustomArgName(objectNr + 400), TouchObjects[objectNr].width_height.x, 0, 65535 - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("widenumber"), F("Width") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); html_TD(); // Height addNumericBox(getPluginCustomArgName(objectNr + 500), TouchObjects[objectNr].width_height.y, 0, 65535 - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("widenumber"), F("Height") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); html_TD(); // inverted addCheckBox(getPluginCustomArgName(objectNr + 700), bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_INVERTED), false - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("Inverted") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH html_TD(); // font scale addNumericBox(getPluginCustomArgName(objectNr + 1200), - get4BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_FONTSCALE), 1, 10 - # ifdef TOUCH_USE_TOOLTIPS + get4BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_FONTSCALE), 0, 10 + # if TOUCH_FEATURE_TOOLTIPS , F("widenumber"), F("Font scaling [1x..10x]") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); html_TD(); // OFF color parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorOff, _colorDepth, true); addTextBox(getPluginCustomArgName(objectNr + 1100), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, EMPTY_STRING, F("widenumber") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("OFF color") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS , F("adagfx65kcolors") ); html_TD(); // OFF Caption @@ -1122,43 +1259,54 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { false, EMPTY_STRING, F("wide") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("OFF caption") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); html_TD(); // Caption color parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorCaption, _colorDepth, true); addTextBox(getPluginCustomArgName(objectNr + 1500), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, EMPTY_STRING, F("widenumber") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("Caption color") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS , F("adagfx65kcolors") ); html_TD(); // Disabled color parsed = AdaGFXcolorToString(TouchObjects[objectNr].colorDisabled, _colorDepth, true); addTextBox(getPluginCustomArgName(objectNr + 1800), parsed, TOUCH_MAX_COLOR_INPUTLENGTH, false, false, EMPTY_STRING, F("widenumber") - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("Disabled color") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS , F("adagfx65kcolors") ); html_TD(); // Action Group addNumericBox(getPluginCustomArgName(objectNr + 2100), get8BitFromUL(TouchObjects[objectNr].groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP), 0, TOUCH_MAX_BUTTON_GROUPS - # ifdef TOUCH_USE_TOOLTIPS + # if TOUCH_FEATURE_TOOLTIPS , F("widenumber") , F("Action group") - # endif // ifdef TOUCH_USE_TOOLTIPS + # endif // if TOUCH_FEATURE_TOOLTIPS ); - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } html_end_table(); - + } + { addFormNumericBox(F("Debounce delay for On/Off buttons"), F("debounce"), Touch_Settings.debounceMs, 0, 255); - addUnit(F("0-255 msec.")); + addUnit(F("0..255 msec.")); + + # if TOUCH_FEATURE_SWIPE + addFormNumericBox(F("Minimal swipe movement"), F("swipemin"), + Touch_Settings.swipeMinimal, 1, 25); + addUnit(F("1..25px")); + + addFormNumericBox(F("Maximum swipe margin"), F("swipemax"), + Touch_Settings.swipeMargin, 5, 100); + addUnit(F("5..100px")); + # endif // if TOUCH_FEATURE_SWIPE } return false; } @@ -1182,9 +1330,9 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { uint16_t saveSize = 0; - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH String colorInput; - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH config.reserve(80); uint32_t lSettings = 0u; @@ -1196,12 +1344,13 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { bitWrite(lSettings, TOUCH_FLAGS_ROTATION_FLIPPED, isFormItemChecked(F("rotation_flipped"))); bitWrite(lSettings, TOUCH_FLAGS_DEDUPLICATE, isFormItemChecked(F("deduplicate"))); bitWrite(lSettings, TOUCH_FLAGS_INIT_OBJECTEVENT, isFormItemChecked(F("init_objectevent"))); - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH set8BitToUL(lSettings, TOUCH_FLAGS_INITIAL_GROUP, getFormItemInt(F("initial_group"))); // Button group bitWrite(lSettings, TOUCH_FLAGS_DRAWBTN_VIA_RULES, isFormItemChecked(F("via_rules"))); bitWrite(lSettings, TOUCH_FLAGS_AUTO_PAGE_ARROWS, isFormItemChecked(F("page_buttons"))); bitWrite(lSettings, TOUCH_FLAGS_PGUP_BELOW_MENU, isFormItemChecked(F("page_below"))); - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + bitWrite(lSettings, TOUCH_FLAGS_SWAP_LEFT_RIGHT, isFormItemChecked(F("swipeswap"))); + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH config += getFormItemInt(F("use_calibration")); // First value should NEVER be empty, or parseString() wil get confused config += TOUCH_SETTINGS_SEPARATOR; @@ -1218,7 +1367,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { config += toStringNoZero(getFormItemInt(F("debounce"))); config += TOUCH_SETTINGS_SEPARATOR; config += ull2String(lSettings); - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH config += TOUCH_SETTINGS_SEPARATOR; colorInput = webArg(getPluginCustomArgName(3000)); // Default Color ON config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth)); @@ -1237,7 +1386,13 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { config += TOUCH_SETTINGS_SEPARATOR; colorInput = webArg(getPluginCustomArgName(3005)); // Default Disabled Caption Color config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, false)); - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + # if TOUCH_FEATURE_SWIPE + config += TOUCH_SETTINGS_SEPARATOR; + config += toStringNoZero(getFormItemInt(F("swipemin"))); + config += TOUCH_SETTINGS_SEPARATOR; + config += toStringNoZero(getFormItemInt(F("swipemax"))); + # endif // if TOUCH_FEATURE_SWIPE settingsArray[TOUCH_CALIBRATION_START] = config; saveSize += config.length() + 1; @@ -1257,6 +1412,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { for (int objectNr = 0; objectNr < TOUCH_MAX_OBJECT_COUNT; objectNr++) { config.clear(); config += webArg(getPluginCustomArgName(objectNr + 100)); // Name + config.trim(); // Remove leading/trailing whitespace from name if (!config.isEmpty()) { // Empty name => skip entry bool numStart = (config[0] >= '0' && config[0] <= '9'); // Numeric start? @@ -1265,37 +1421,46 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { numStart) { // Check for invalid characters in objectname error += F("Invalid character in objectname #"); error += objectNr + 1; - error += numStart ? F(". Should not start with a digit.\n") : F(". Do not use ',-+/*=^%!#[]{}()' or space.\n"); + error += F(". "); + error += numStart ? F("Should not start with a digit.\n") : F("Do not use ',-+/*=^%!#[]{}()' or space.\n"); } config += TOUCH_SETTINGS_SEPARATOR; uint32_t flags = 0u; bitWrite(flags, TOUCH_OBJECT_FLAG_ENABLED, isFormItemChecked(getPluginCustomArgName(objectNr + 0))); // Enabled bitWrite(flags, TOUCH_OBJECT_FLAG_INVERTED, isFormItemChecked(getPluginCustomArgName(objectNr + 700))); // Inverted - # ifdef TOUCH_USE_EXTENDED_TOUCH - uint32_t groupFlags = 0u; - const uint8_t buttonType = getFormItemIntCustomArgName(objectNr + 800); - set4BitToUL(flags, TOUCH_OBJECT_FLAG_BUTTONTYPE, buttonType); // Buttontype - set4BitToUL(flags, TOUCH_OBJECT_FLAG_BUTTONALIGN, getFormItemIntCustomArgName(objectNr + 900) >> 4); // Button layout - bitWrite(flags, TOUCH_OBJECT_FLAG_BUTTON, (static_cast(buttonType) != Button_type_e::None)); // On/Off button - set4BitToUL(groupFlags, TOUCH_OBJECT_GROUP_ACTION, getFormItemIntCustomArgName(objectNr + 2000)); // ButtonAction - set8BitToUL(groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP, getFormItemIntCustomArgName(objectNr + 2100)); // ActionGroup - set4BitToUL(flags, TOUCH_OBJECT_FLAG_FONTSCALE, getFormItemIntCustomArgName(objectNr + 1200)); // Font scaling - set8BitToUL(flags, TOUCH_OBJECT_FLAG_GROUP, getFormItemIntCustomArgName(objectNr + 1600)); // Button group - # else // ifdef TOUCH_USE_EXTENDED_TOUCH - bitWrite(flags, TOUCH_OBJECT_FLAG_BUTTON, isFormItemChecked(getPluginCustomArgName(objectNr + 600))); // On/Off button - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH - - config += ull2String(flags); // Flags + # if TOUCH_FEATURE_EXTENDED_TOUCH + uint32_t groupFlags = 0u; + const uint8_t buttonType = getFormItemIntCustomArgName(objectNr + 800); + const uint8_t buttonLayout = getFormItemIntCustomArgName(objectNr + 900) >> 4; + set4BitToUL(flags, TOUCH_OBJECT_FLAG_BUTTONTYPE, buttonType); // Buttontype + set4BitToUL(flags, TOUCH_OBJECT_FLAG_BUTTONALIGN, buttonLayout); // Button layout + # if ADAGFX_ENABLE_BUTTON_SLIDER + const bool isSlider = (static_cast(buttonLayout << 4) == Button_layout_e::Slider); + bitWrite(flags, TOUCH_OBJECT_FLAG_SLIDER, isSlider); // Slider + # else // if ADAGFX_ENABLE_BUTTON_SLIDER + const bool isSlider = false; + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER + const bool isButton = (static_cast(buttonType) != Button_type_e::None) && !isSlider; + bitWrite(flags, TOUCH_OBJECT_FLAG_BUTTON, isButton); // On/Off button + set4BitToUL(groupFlags, TOUCH_OBJECT_GROUP_ACTION, getFormItemIntCustomArgName(objectNr + 2000)); // ButtonAction + set8BitToUL(groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP, getFormItemIntCustomArgName(objectNr + 2100)); // ActionGroup + set4BitToUL(flags, TOUCH_OBJECT_FLAG_FONTSCALE, getFormItemIntCustomArgName(objectNr + 1200)); // Font scaling + set8BitToUL(flags, TOUCH_OBJECT_FLAG_GROUP, getFormItemIntCustomArgName(objectNr + 1600)); // Button group + # else // if TOUCH_FEATURE_EXTENDED_TOUCH + bitWrite(flags, TOUCH_OBJECT_FLAG_BUTTON, isFormItemChecked(getPluginCustomArgName(objectNr + 600))); // On/Off button + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + + config += ull2String(flags); // Flags config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 200)); // Top x + config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 200)); // Top x config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 300)); // Top y + config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 300)); // Top y config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 400)); // Bottom x + config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 400)); // Bottom x config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 500)); // Bottom y + config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 500)); // Bottom y - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH config += TOUCH_SETTINGS_SEPARATOR; colorInput = webArg(getPluginCustomArgName(objectNr + 1000)); // Color ON config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true)); @@ -1324,7 +1489,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, true)); config += TOUCH_SETTINGS_SEPARATOR; config += ull2String(groupFlags); // Group Flags - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } config.trim(); @@ -1380,7 +1545,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { } /** - * Every 10th second we check if the screen is touched + * Every 20 milliseconds we check if the screen is touched */ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, const int16_t & x, @@ -1441,47 +1606,223 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, } } - if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME)) { // Send events for objectname if within reach + if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME)) { // Send events for objectname if within reach, and swipes String selectedObjectName; int8_t selectedObjectIndex = -1; if (isValidAndTouchedTouchObject(x, y, selectedObjectName, selectedObjectIndex)) { + # if TOUCH_FEATURE_SWIPE + int16_t delta_x = x - _last_point.x; + int16_t delta_y = y - _last_point.y; + + Swipe_action_e swipe = Swipe_action_e::None; + + if ((std::abs(delta_x) >= Touch_Settings.swipeMargin) || (std::abs(delta_x) <= Touch_Settings.swipeMinimal)) { + delta_x = 0; // Ignore + } + + if ((std::abs(delta_y) >= Touch_Settings.swipeMargin) || (std::abs(delta_y) <= Touch_Settings.swipeMinimal)) { + delta_y = 0; // Ignore + } + + if ((delta_x != 0) || (delta_y != 0)) { + _lastObjectIndex = -2; + + // Swipe, determine direction (from 12 o'clock, clock-wise) + if ((delta_x == 0) && (delta_y < 0)) { // Up + swipe = Swipe_action_e::Up; + } else if ((delta_x > 0) && (delta_y < 0)) { // Up-Right + swipe = Swipe_action_e::UpRight; + } else if ((delta_x > 0) && (delta_y == 0)) { // Right + swipe = Swipe_action_e::Right; + } else if ((delta_x > 0) && (delta_y > 0)) { // Right-Down + swipe = Swipe_action_e::RightDown; + } else if ((delta_x == 0) && (delta_y > 0)) { // Down + swipe = Swipe_action_e::Down; + } else if ((delta_x < 0) && (delta_y > 0)) { // Down-Left + swipe = Swipe_action_e::DownLeft; + } else if ((delta_x < 0) && (delta_y == 0)) { // Left + swipe = Swipe_action_e::Left; + } else if ((delta_x < 0) && (delta_y < 0)) { // Left-Up + swipe = Swipe_action_e::LeftUp; + } + + # ifdef TOUCH_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + String log = F("Touch Swiped, direction: "); + log += toString(swipe); + log += F(", dx: "); + log += delta_x; + log += F(", dy: "); + log += delta_y; + addLogMove(LOG_LEVEL_DEBUG, log); + } + # endif // ifdef TOUCH_DEBUG + } + # endif // if TOUCH_FEATURE_SWIPE + // Not touched yet or too long ago - if ((TouchObjects[selectedObjectIndex].TouchTimers == 0) || - (TouchObjects[selectedObjectIndex].TouchTimers < (millis() - (1.5 * Touch_Settings.debounceMs)))) { + if ( + # if TOUCH_FEATURE_SWIPE + (swipe == Swipe_action_e::None) && + # endif // if TOUCH_FEATURE_SWIPE + ((TouchObjects[selectedObjectIndex].TouchTimers == 0) + || (TouchObjects[selectedObjectIndex].TouchTimers < (millis() - (1.5 * Touch_Settings.debounceMs))) + )) { // From now wait the debounce time TouchObjects[selectedObjectIndex].TouchTimers = millis() + Touch_Settings.debounceMs; } else { - // Debouncing time elapsed? - if (TouchObjects[selectedObjectIndex].TouchTimers <= millis()) { + // Debouncing time elapsed? Swiping/sliding passes through without debounce + + if ( + # if TOUCH_FEATURE_SWIPE + (swipe != Swipe_action_e::None) || + # endif // if TOUCH_FEATURE_SWIPE + (TouchObjects[selectedObjectIndex].TouchTimers <= millis())) { TouchObjects[selectedObjectIndex].TouchTimers = 0; - if ((selectedObjectIndex > -1) && bitRead(TouchObjects[selectedObjectIndex].flags, TOUCH_OBJECT_FLAG_BUTTON)) { - TouchObjects[selectedObjectIndex].TouchStates = !TouchObjects[selectedObjectIndex].TouchStates; - generateObjectEvent(event, selectedObjectIndex, TouchObjects[selectedObjectIndex].TouchStates ? 1 : 0); - } else { - // Matching object is found, send # event with x, y and z as %eventvalue1/2/3% - String eventCommand; - eventCommand.reserve(48); - eventCommand = getTaskDeviceName(event->TaskIndex); - eventCommand += '#'; - eventCommand += selectedObjectName; - eventCommand += '='; // Add arguments - eventCommand += x; - eventCommand += ','; - eventCommand += y; - eventCommand += ','; - eventCommand += z; - eventQueue.addMove(std::move(eventCommand)); + if ( + # if TOUCH_FEATURE_SWIPE + (swipe == Swipe_action_e::None) && + # endif // if TOUCH_FEATURE_SWIPE + (selectedObjectIndex > -1) && bitRead(TouchObjects[selectedObjectIndex].flags, TOUCH_OBJECT_FLAG_BUTTON)) { + // Button touched + _lastObjectIndex = selectedObjectIndex; // Handle on release + # if TOUCH_FEATURE_SWIPE + } else if ((selectedObjectIndex > -1) && bitRead(TouchObjects[selectedObjectIndex].flags, TOUCH_OBJECT_FLAG_SLIDER)) { + // Handle slider immediately to move/set absolute position + _lastObjectIndex = -1; // Handled + const bool isVertical = TouchObjects[selectedObjectIndex].width_height.x < TouchObjects[selectedObjectIndex].width_height.y; + int16_t position = 0; + int16_t lowRange = 0; + int16_t highRange = 100; + float rangeFrom = 0.0f; + float rangeTo = 0.0f; + bool useRange = false; + + if (!TouchObjects[selectedObjectIndex].captionOff.isEmpty()) { // Off caption can hold range: , + String tmp = parseString(TouchObjects[selectedObjectIndex].captionOff, 1); + const bool validFrom = validFloatFromString(tmp, rangeFrom); + tmp = parseString(TouchObjects[selectedObjectIndex].captionOff, 2); + + if (validFrom && validFloatFromString(tmp, rangeTo) && + !essentiallyEqual(rangeFrom, 0.0f) && !essentiallyEqual(rangeTo, 0.0f)) { + useRange = true; + } + } + + if (isVertical) { + position = (TouchObjects[selectedObjectIndex].top_left.y + TouchObjects[selectedObjectIndex].width_height.y) - y; + position = ceil(position / (TouchObjects[selectedObjectIndex].width_height.y / 100.0)); + } else { + position = x - TouchObjects[selectedObjectIndex].top_left.x; + position = ceil(position / (TouchObjects[selectedObjectIndex].width_height.x / 100.0)); + } + + if (useRange) { // Calculate range-boundaries + lowRange = static_cast(min(rangeFrom, rangeTo)); + highRange = static_cast(max(rangeTo, rangeFrom)); + position = map(position, 0, 100, lowRange, highRange); + TouchObjects[selectedObjectIndex].TouchStates = position; + } else if (position < lowRange) { + TouchObjects[selectedObjectIndex].TouchStates = lowRange; + } else if (position > highRange) { + TouchObjects[selectedObjectIndex].TouchStates = highRange; + } else { + TouchObjects[selectedObjectIndex].TouchStates = position; + } + + // Reduce the number of events during sliding + if ((TouchObjects[selectedObjectIndex].TouchTimers == 0) || + (TouchObjects[selectedObjectIndex].TouchTimers < millis())) { + generateObjectEvent(event, selectedObjectIndex, TouchObjects[selectedObjectIndex].TouchStates); + TouchObjects[selectedObjectIndex].TouchTimers = millis() + (2 * Touch_Settings.debounceMs); + } else { + _lastObjectIndex = selectedObjectIndex; // Update on touch-release + } + # endif // if TOUCH_FEATURE_SWIPE + } else { // Generic touch event + _lastObjectIndex = -2; // Update on touch-release + _lastObjectName = selectedObjectName; + + String log = F("Swiped/touched, object: "); + log += _lastObjectName; + log += ':'; + log += toString(swipe); + + # if TOUCH_FEATURE_SWIPE + + if (swipe != Swipe_action_e::None) { + _lastSwipe = swipe; + } + _last_delta_x = delta_x; + _last_delta_y = delta_y; + # endif // if TOUCH_FEATURE_SWIPE + addLogMove(LOG_LEVEL_INFO, log); } } + _last_point.x = x; // Save last touchpoint + _last_point.y = y; + _last_point_z.x = z; // Don't want to extend the struct for 1 use } } } } + return success; } +/** + * Release touch + */ +void ESPEasy_TouchHandler::releaseTouch(struct EventStruct *event) { + if ((_lastObjectIndex > -1) && bitRead(TouchObjects[_lastObjectIndex].flags, TOUCH_OBJECT_FLAG_BUTTON)) { + TouchObjects[_lastObjectIndex].TouchStates = (TouchObjects[_lastObjectIndex].TouchStates > 0 ? 0 : 1); // Flip state + generateObjectEvent(event, _lastObjectIndex, TouchObjects[_lastObjectIndex].TouchStates > 0 ? 1 : 0); + _lastObjectIndex = -1; // Handle only once + } else if ((_lastObjectIndex > -1) && bitRead(TouchObjects[_lastObjectIndex].flags, TOUCH_OBJECT_FLAG_SLIDER)) { + generateObjectEvent(event, _lastObjectIndex, TouchObjects[_lastObjectIndex].TouchStates); + TouchObjects[_lastObjectIndex].TouchTimers = 0; + _lastObjectIndex = -1; // Handle only once + } else if (_lastObjectIndex != -1) { + // Matching object is found, send # event with x, y and z as %eventvalue1/2/3% + String eventCommand; + eventCommand.reserve(48); + eventCommand = getTaskDeviceName(event->TaskIndex); + eventCommand += '#'; + + # if TOUCH_FEATURE_SWIPE + + if (_lastSwipe == Swipe_action_e::None) + # endif // if TOUCH_FEATURE_SWIPE + { + eventCommand += _lastObjectName; + eventCommand += '='; // Add arguments + eventCommand += _last_point.x; + eventCommand += ','; + eventCommand += _last_point.y; + eventCommand += ','; + eventCommand += _last_point_z.x; + } + # if TOUCH_FEATURE_SWIPE + else { + eventCommand += F("Swiped"); + eventCommand += '='; // Add arguments + eventCommand += static_cast(_lastSwipe); + eventCommand += ','; + eventCommand += _last_delta_x; + eventCommand += ','; + eventCommand += _last_delta_y; + _lastSwipe = Swipe_action_e::None; + } + # endif // if TOUCH_FEATURE_SWIPE + eventQueue.addMove(std::move(eventCommand)); + _lastObjectIndex = -1; // Handle only once + } + _stillTouching = false; +} + /** * Parse and execute the plugin commands */ @@ -1551,7 +1892,7 @@ bool ESPEasy_TouchHandler::plugin_write(struct EventStruct *event, arguments = parseString(string, arg); while (!arguments.isEmpty()) { - int8_t state = getTouchButtonOnOff(event, arguments); + int16_t state = getTouchObjectValue(event, arguments); if (state > -1) { success |= setTouchButtonOnOff(event, arguments, state == 0); @@ -1559,6 +1900,14 @@ bool ESPEasy_TouchHandler::plugin_write(struct EventStruct *event, arg++; arguments = parseString(string, arg); } + } else if (subcommand.equals(F("set"))) { // touch,set,, : Set TouchObject value + arguments = parseString(string, arg); + success = setTouchObjectValue(event, arguments, event->Par3); + # if TOUCH_FEATURE_EXTENDED_TOUCH + # if TOUCH_FEATURE_SWIPE + } else if (subcommand.equals(F("swipe"))) { // touch,swipe, : Switch button group via swipe value + success = handleButtonSwipe(event, event->Par2); + # endif // if TOUCH_FEATURE_SWIPE } else if (subcommand.equals(F("setgrp"))) { // touch,setgrp, : Activate button group success = setButtonGroup(event, event->Par2); } else if (subcommand.equals(F("incgrp"))) { // touch,incgrp : increment group and Activate @@ -1587,6 +1936,7 @@ bool ESPEasy_TouchHandler::plugin_write(struct EventStruct *event, success = displayButton(event, index); // Use default argument values } } + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } } return success; @@ -1597,12 +1947,13 @@ bool ESPEasy_TouchHandler::plugin_write(struct EventStruct *event, */ bool ESPEasy_TouchHandler::plugin_get_config_value(struct EventStruct *event, String & string) { - bool success = false; - String command = parseString(string, 1); + bool success = false; + const String command = parseString(string, 1); if (command == F("buttongroup")) { string = getButtonGroup(); success = true; + # if TOUCH_FEATURE_EXTENDED_TOUCH } else if (command == F("hasgroup")) { int group; // We'll be ignoring group 0 if there are multiple button groups @@ -1612,22 +1963,37 @@ bool ESPEasy_TouchHandler::plugin_get_config_value(struct EventStruct *event, } else { string = '0'; // invalid number = false } + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } else if (command == F("enabled")) { - String arguments = parseStringKeepCase(string, 2); - int8_t enabled = getTouchObjectState(event, arguments); + const String arguments = parseStringKeepCase(string, 2); + int8_t enabled = getTouchObjectState(event, arguments); if (enabled > -1) { string = enabled; success = true; } } else if (command == F("state")) { - String arguments = parseStringKeepCase(string, 2); - int8_t state = getTouchButtonOnOff(event, arguments); + const String arguments = parseStringKeepCase(string, 2); + int8_t state = getTouchObjectValue(event, arguments); if (state > -1) { string = state; success = true; } + # if TOUCH_FEATURE_EXTENDED_TOUCH + } else if (command == F("pagemode")) { + string = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); + success = true; + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + # if TOUCH_FEATURE_SWIPE + } else if (command == F("swipedir")) { + int state; + + if (validIntFromString(parseString(string, 2), state)) { + string = toString(static_cast(state)); + success = true; + } + # endif // if TOUCH_FEATURE_SWIPE } return success; } @@ -1638,7 +2004,7 @@ bool ESPEasy_TouchHandler::plugin_get_config_value(struct EventStruct *event, **************************************************************************/ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, const int8_t & objectIndex, - const int8_t & onOffState, + const int16_t & onOffState, const int8_t & mode, const bool & groupSwitch, const int8_t & factor) { @@ -1671,16 +2037,21 @@ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, } } - if (onOffState < 0) { // Negative value: pass on unaltered (1 = state) - eventCommand += onOffState; - extraCommand += onOffState; // duplicate - } else { // Check for inverted output (1 = state) - if (bitRead(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_INVERTED)) { - eventCommand += onOffState == 1 ? '0' : '1'; // Act like an inverted button, 0 = On, 1 = Off - extraCommand += onOffState == 1 ? '0' : '1'; // Act like an inverted button, 0 = On, 1 = Off // duplicate - } else { - eventCommand += onOffState == 1 ? '1' : '0'; // Act like a button, 1 = On, 0 = Off - extraCommand += onOffState == 1 ? '1' : '0'; // Act like a button, 1 = On, 0 = Off // duplicate + if (bitRead(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_SLIDER)) { + eventCommand += onOffState; // Slider control: pass value as state (1 = state) + extraCommand += onOffState; // duplicate + } else { + if (onOffState < 0) { // Negative value: pass on unaltered (1 = state) + eventCommand += onOffState; + extraCommand += onOffState; // duplicate + } else { // Check for inverted output (1 = state) + if (bitRead(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_INVERTED)) { + eventCommand += onOffState == 1 ? '0' : '1'; // Act like an inverted button, 0 = On, 1 = Off + extraCommand += onOffState == 1 ? '0' : '1'; // Act like an inverted button, 0 = On, 1 = Off // duplicate + } else { + eventCommand += onOffState == 1 ? '1' : '0'; // Act like a button, 1 = On, 0 = Off + extraCommand += onOffState == 1 ? '1' : '0'; // Act like a button, 1 = On, 0 = Off // duplicate + } } } eventCommand += ','; @@ -1701,7 +2072,7 @@ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, eventCommand += objectIndex + 1; // Adjust to displayed index (7 = id) eventCommand += ','; // (8 = type + layout, 4+4 bit, side by side) eventCommand += get8BitFromUL(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_BUTTONTYPE) * factor; - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH eventCommand += ','; // (9 = ON color) eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorOn == 0 ? Touch_Settings.colorOn @@ -1723,7 +2094,11 @@ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, String _capt; if (TouchObjects[objectIndex].captionOn.isEmpty()) { - _capt = TouchObjects[objectIndex].objectName; + if (bitRead(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_SLIDER)) { + _capt = onOffState; // Override caption if not set + } else { + _capt = TouchObjects[objectIndex].objectName; + } } else { _capt = TouchObjects[objectIndex].captionOn; } @@ -1732,7 +2107,11 @@ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, eventCommand += ','; // (14 = OFF caption) if (TouchObjects[objectIndex].captionOff.isEmpty()) { - _capt = TouchObjects[objectIndex].objectName; + if (bitRead(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_SLIDER)) { + _capt = onOffState; // override caption if not set + } else { + _capt = TouchObjects[objectIndex].objectName; + } } else { _capt = TouchObjects[objectIndex].captionOff; } @@ -1753,12 +2132,12 @@ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, ? Touch_Settings.colorDisabledCaption : TouchObjects[objectIndex].colorDisabledCaption, _colorDepth); - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH eventCommand += ','; eventCommand += _displayTask + 1; // What TaskIndex? (18) or (9) eventCommand += ','; // Group (19) or (10) eventCommand += get8BitFromUL(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_GROUP); - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH eventCommand += ','; // Group mode (20) uint8_t action = get4BitFromUL(TouchObjects[objectIndex].groupFlags, TOUCH_OBJECT_GROUP_ACTION); @@ -1787,10 +2166,10 @@ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, } else { eventCommand += -1; // No group to activate } - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_DRAWBTN_VIA_RULES)) { eventQueue.addMove(std::move(eventCommand)); @@ -1834,7 +2213,7 @@ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, } } } - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH delay(0); } diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index 3f8616a95c..04b383f786 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -11,7 +11,13 @@ /***** * Changelog: - * 2022-08-13 tonhuisman: Replace _ om object name and on/off captions by space, to ease the use of object name as caption + * 2022-08-17 tonhuisman: Add support for range (x..y) for sliders + * 2022-08-16 tonhuisman: Changed validButtonGroup() to ignore group 0 by default, and setButtonGroup(), and setgrp subcommand, + * to allow group 0 + * Add setting for swapping (reversing) menu-swipe direction + * 2022-08-15 tonhuisman: Add optional swipe/slider support, add swipe subcommand, add GetConfigValue options and docs + * Replace ifdef *_USE_* defines by if *_FEATURE_* + * 2022-08-13 tonhuisman: Replace _ in object name and on/off captions by space, to ease the use of object name as caption * On save, any spaces in captions are replaced by _ to avoid using 2 quotes around the value. * This implies that no underscores wil be shown in captions! * 2022-06-09 tonhuisman: Change method arguments to const-by-reference where possible for improved compile-time checks @@ -37,7 +43,9 @@ * touch,disable,[,...] : Disable enabled objectname(s) * touch,on,[,...] : Switch TouchButton(s) on (must be enabled) * touch,off,[,...] : Switch TouchButton(s) off (must be enabled) + * touch,set,, : Set TouchObject to value (slider) or 0=off >0=on (must be enabled) * touch,toggle,[,...] : Switch TouchButton(s) to the other state (must be enabled) + * touch,swipe, : Switch button group according to swipe direction * touch,setgrp, : Switch to button group * touch,incgrp : Switch to next button group * touch,decgrp : Switch to previous button group @@ -45,27 +53,45 @@ * touch,decpage : Switch to previous button group page (-10) * touch,updatebutton,[,[,]] : Update a button by name or number */ +/** + * Get Config Variables supported: [#{,arguments}] + * {,arguments} : Description + * buttongroup : Get current buttongroup + * hasgroup,groupNr : Check if group exists, ignores group 0 + * enabled,objectName|objectNr : Check if object is enabled + * state,objectName|objectNr : Get current object state (buttons: on = 1, off = 0, sliders: value 0..100 (=percentage)) + * pagemode : Get the PageUp/PageDown mode, 0 = up=pgup, 1 = up=pgdown + * swipedir,directionId : Get the name for the direction provided in numeric form + */ -# define TOUCH_DEBUG // Additional debugging information - -# define TOUCH_USE_TOOLTIPS // Enable tooltips in UI +# define TOUCH_DEBUG // Additional debugging information -# define TOUCH_USE_EXTENDED_TOUCH // Enable extended touch settings +# define TOUCH_FEATURE_TOOLTIPS 1 // Enable/disable tooltips in UI +# define TOUCH_FEATURE_EXTENDED_TOUCH 1 // Enable/disable extended touch settings +# define TOUCH_FEATURE_SWIPE 1 // Enable/disable Swipe support # ifdef LIMIT_BUILD_SIZE -# ifdef TOUCH_USE_TOOLTIPS -# undef TOUCH_USE_TOOLTIPS -# endif // ifdef TOUCH_USE_TOOLTIPS +# if TOUCH_FEATURE_TOOLTIPS +# undef TOUCH_FEATURE_TOOLTIPS +# define TOUCH_FEATURE_TOOLTIPS 0 +# endif // if TOUCH_FEATURE_TOOLTIPS # ifdef TOUCH_DEBUG # undef TOUCH_DEBUG # endif // ifdef TOUCH_DEBUG -# ifdef TOUCH_USE_EXTENDED_TOUCH -# undef TOUCH_USE_EXTENDED_TOUCH -# endif // ifdef TOUCH_USE_EXTENDED_TOUCH +# if TOUCH_FEATURE_EXTENDED_TOUCH +# undef TOUCH_FEATURE_EXTENDED_TOUCH +# define TOUCH_FEATURE_EXTENDED_TOUCH 0 +# endif // if TOUCH_FEATURE_EXTENDED_TOUCH +// # if TOUCH_FEATURE_SWIPE +// # undef TOUCH_FEATURE_SWIPE +// # define TOUCH_FEATURE_SWIPE 0 +// # endif // if TOUCH_FEATURE_SWIPE # endif // ifdef LIMIT_BUILD_SIZE -# if defined(TOUCH_USE_TOOLTIPS) && !FEATURE_TOOLTIPS -# undef TOUCH_USE_TOOLTIPS -# endif // if defined(TOUCH_USE_TOOLTIPS) && !FEATURE_TOOLTIPS + +# if TOUCH_FEATURE_TOOLTIPS && !FEATURE_TOOLTIPS +# undef TOUCH_FEATURE_TOOLTIPS +# define TOUCH_FEATURE_TOOLTIPS 0 +# endif // if TOUCH_FEATURE_TOOLTIPS && !FEATURE_TOOLTIPS // Global Settings flags # define TOUCH_FLAGS_SEND_XY 0 // Send X and Y coordinate events @@ -80,6 +106,7 @@ # define TOUCH_FLAGS_DRAWBTN_VIA_RULES 16 // Draw buttons using rule # define TOUCH_FLAGS_AUTO_PAGE_ARROWS 17 // Automatically enable/disable paging buttons # define TOUCH_FLAGS_PGUP_BELOW_MENU 18 // Group-page below current menu (reverts Up/Down buttons) +# define TOUCH_FLAGS_SWAP_LEFT_RIGHT 19 // Swaps Left and Right, Up and Down swipe directions for menu actions # define TOUCH_VALUE_X UserVar[event->BaseVarIndex + 0] # define TOUCH_VALUE_Y UserVar[event->BaseVarIndex + 1] @@ -95,6 +122,8 @@ # define TOUCH_TS_X_RES 320 // Pixels, should match with the screen it is mounted on # define TOUCH_TS_Y_RES 480 # define TOUCH_DEBOUNCE_MILLIS 100 // Debounce delay for On/Off button function +# define TOUCH_DEF_SWIPE_MINIMAL 3 // Minimal swipe pixels +# define TOUCH_DEF_SWIPE_MARGIN 10 // Default swipe margin # define TOUCH_MAX_COLOR_INPUTLENGTH 11 // 11 Characters is enough to type in all recognized color names and values # define TOUCH_MaxObjectNameLength 15 // 15 character objectnames @@ -119,14 +148,19 @@ # define TOUCH_CALIBRATION_BOTTOM_Y 6 // Bottom Y # define TOUCH_COMMON_DEBOUNCE_MS 7 // Debounce milliseconds # define TOUCH_COMMON_FLAGS 8 // Common flags -# ifdef TOUCH_USE_EXTENDED_TOUCH +# if TOUCH_FEATURE_EXTENDED_TOUCH # define TOUCH_COMMON_DEF_COLOR_ON 9 // Default Color ON (rgb565, uint16_t) # define TOUCH_COMMON_DEF_COLOR_OFF 10 // Default Color OFF # define TOUCH_COMMON_DEF_COLOR_BORDER 11 // Default Color Border # define TOUCH_COMMON_DEF_COLOR_CAPTION 12 // Default Color Caption # define TOUCH_COMMON_DEF_COLOR_DISABLED 13 // Default Disabled Color # define TOUCH_COMMON_DEF_COLOR_DISABCAPT 14 // Default Disabled Caption Color -# endif // ifdef TOUCH_USE_EXTENDED_TOUCH +# define TOUCH_COMMON_SWIPE_MINIMAL 15 // Minimal swipe pixels +# define TOUCH_COMMON_SWIPE_MARGIN 16 // Swipe margin +# else // if TOUCH_FEATURE_EXTENDED_TOUCH +# define TOUCH_COMMON_SWIPE_MINIMAL 9 // Minimal swipe pixels +# define TOUCH_COMMON_SWIPE_MARGIN 10 // Swipe margin +# endif // if TOUCH_FEATURE_EXTENDED_TOUCH // Settings array field offsets: Touch objects # define TOUCH_OBJECT_INDEX_START (TOUCH_CALIBRATION_START + 1) @@ -137,7 +171,7 @@ # define TOUCH_OBJECT_COORD_TOP_Y 4 // Top Y # define TOUCH_OBJECT_COORD_WIDTH 5 // Width # define TOUCH_OBJECT_COORD_HEIGHT 6 // Height -# ifdef TOUCH_USE_EXTENDED_TOUCH +# if TOUCH_FEATURE_EXTENDED_TOUCH # define TOUCH_OBJECT_COLOR_ON 7 // Color ON (rgb565, uint16_t) # define TOUCH_OBJECT_COLOR_OFF 8 // Color OFF # define TOUCH_OBJECT_COLOR_CAPTION 9 // Color Caption @@ -147,7 +181,7 @@ # define TOUCH_OBJECT_COLOR_DISABLED 13 // Disabled Color # define TOUCH_OBJECT_COLOR_DISABCAPT 14 // Disabled Caption Color # define TOUCH_OBJECT_GROUPFLAGS 15 // Group flags -# endif // ifdef TOUCH_USE_EXTENDED_TOUCH +# endif // if TOUCH_FEATURE_EXTENDED_TOUCH # define TOUCH_OBJECT_FLAG_ENABLED 0 // Enabled # define TOUCH_OBJECT_FLAG_BUTTON 1 // Button behavior @@ -156,6 +190,7 @@ # define TOUCH_OBJECT_FLAG_BUTTONTYPE 7 // 4 bits used as button type (low 4 bits) # define TOUCH_OBJECT_FLAG_BUTTONALIGN 11 // 4 bits used as button caption layout (high 4 bits) # define TOUCH_OBJECT_FLAG_GROUP 16 // 8 bits used as button group +# define TOUCH_OBJECT_FLAG_SLIDER 24 // Slider object # define TOUCH_OBJECT_GROUP_ACTIONGROUP 8 // 8 bits used as action group # define TOUCH_OBJECT_GROUP_ACTION 16 // 4 bits used as action option @@ -173,15 +208,13 @@ struct tTouch_Point // For touch objects we store a name, 2 coordinates, flags and other options struct tTouchObjects { - String objectName; - String captionOn; - String captionOff; uint32_t flags = 0u; uint32_t SurfaceAreas = 0u; uint32_t TouchTimers = 0u; tTouch_Point top_left; tTouch_Point width_height; - # ifdef TOUCH_USE_EXTENDED_TOUCH + int16_t TouchStates = 0; + # if TOUCH_FEATURE_EXTENDED_TOUCH uint32_t groupFlags = 0u; uint16_t colorOn = 0u; uint16_t colorOff = 0u; @@ -189,8 +222,10 @@ struct tTouchObjects uint16_t colorBorder = 0u; uint16_t colorDisabled = 0u; uint16_t colorDisabledCaption = 0u; - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH - bool TouchStates = false; + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + String objectName; + String captionOn; + String captionOff; }; // Touch actions @@ -204,8 +239,29 @@ enum class Touch_action_e : uint8_t { TouchAction_MAX = 6u // Last item is count, max 16! }; +# if TOUCH_FEATURE_SWIPE + +// Swipe actions, start at 12 o'çlock, clock-wise +enum class Swipe_action_e : uint8_t { + None = 0u, + Up = 1u, + UpRight = 2u, + Right = 3u, + RightDown = 4u, + Down = 5u, + DownLeft = 6u, + Left = 7u, + LeftUp = 8u, + SwipeAction_MAX = 9u // Last item is count +}; +# endif // if TOUCH_FEATURE_SWIPE + const __FlashStringHelper* toString(Touch_action_e action); +# if TOUCH_FEATURE_SWIPE +const __FlashStringHelper* toString(Swipe_action_e action); +# endif // if TOUCH_FEATURE_SWIPE + class ESPEasy_TouchHandler { public: @@ -221,39 +277,48 @@ class ESPEasy_TouchHandler { const int16_t& y, String & selectedObjectName, int8_t & selectedObjectIndex); - int8_t getTouchObjectIndex(struct EventStruct *event, - const String & touchObject, - const bool & isButton = false); - bool setTouchObjectState(struct EventStruct *event, - const String & touchObject, - const bool & state); - int8_t getTouchObjectState(struct EventStruct *event, - const String & touchObject); - bool setTouchButtonOnOff(struct EventStruct *event, - const String & touchObject, - const bool & state); - int8_t getTouchButtonOnOff(struct EventStruct *event, - const String & touchObject); - bool plugin_webform_load(struct EventStruct *event); - bool plugin_webform_save(struct EventStruct *event); - bool plugin_fifty_per_second(struct EventStruct *event, - const int16_t & x, - const int16_t & y, - const int16_t & ox, - const int16_t & oy, - const int16_t & rx, - const int16_t & ry, - const int16_t & z); + int8_t getTouchObjectIndex(struct EventStruct *event, + const String & touchObject, + const bool & isButton = false); + bool setTouchObjectState(struct EventStruct *event, + const String & touchObject, + const bool & state); + int8_t getTouchObjectState(struct EventStruct *event, + const String & touchObject); + bool setTouchButtonOnOff(struct EventStruct *event, + const String & touchObject, + const bool & state); + int16_t getTouchObjectValue(struct EventStruct *event, + const String & touchObject); + bool setTouchObjectValue(struct EventStruct *event, + const String & touchObject, + const uint16_t & value); + bool plugin_webform_load(struct EventStruct *event); + bool plugin_webform_save(struct EventStruct *event); + bool plugin_fifty_per_second(struct EventStruct *event, + const int16_t & x, + const int16_t & y, + const int16_t & ox, + const int16_t & oy, + const int16_t & rx, + const int16_t & ry, + const int16_t & z); bool plugin_write(struct EventStruct *event, const String & string); bool plugin_get_config_value(struct EventStruct *event, String & string); + void releaseTouch(struct EventStruct *event); int16_t getButtonGroup() { return _buttonGroup; } + # if TOUCH_FEATURE_EXTENDED_TOUCH bool validButtonGroup(const int16_t& group, - const bool & ignoreZero = false); + const bool & ignoreZero = true); + # if TOUCH_FEATURE_SWIPE + bool handleButtonSwipe(struct EventStruct *event, + const int16_t & swipeValue); + # endif // if TOUCH_FEATURE_SWIPE bool setButtonGroup(struct EventStruct *event, const int16_t & buttonGroup); bool incrementButtonGroup(struct EventStruct *event); @@ -267,6 +332,7 @@ class ESPEasy_TouchHandler { const int8_t & buttonNr, const int16_t & buttonGroup = -1, int8_t mode = 0); + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH private: @@ -276,7 +342,7 @@ class ESPEasy_TouchHandler { const int & defaultValue = 0); void generateObjectEvent(struct EventStruct *event, const int8_t & objectIndex, - const int8_t & onOffState, + const int16_t & onOffState, const int8_t & mode = 0, const bool & groupSwitch = false, const int8_t & factor = 1); @@ -289,21 +355,35 @@ class ESPEasy_TouchHandler { std::set_buttonGroups; bool _settingsLoaded = false; + bool _stillTouching = false; + + // Used to generate events on touch-release + int8_t _lastObjectIndex = -1; + String _lastObjectName; + tTouch_Point _last_point; + tTouch_Point _last_point_z; // Only used to store z in the x member + # if TOUCH_FEATURE_SWIPE + Swipe_action_e _lastSwipe = Swipe_action_e::None; + int16_t _last_delta_x; + int16_t _last_delta_y; + # endif // if TOUCH_FEATURE_SWIPE struct tTouch_Globals { uint32_t flags = 0u; tTouch_Point top_left; tTouch_Point bottom_right; - # ifdef TOUCH_USE_EXTENDED_TOUCH + # if TOUCH_FEATURE_EXTENDED_TOUCH uint16_t colorOn = 0u; uint16_t colorOff = 0u; uint16_t colorCaption = 0u; uint16_t colorBorder = 0u; uint16_t colorDisabled = 0u; uint16_t colorDisabledCaption = 0u; - # endif // ifdef TOUCH_USE_EXTENDED_TOUCH + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH uint8_t debounceMs = 0u; + uint8_t swipeMargin = 0u; + uint8_t swipeMinimal = 0u; bool calibrationEnabled = false; bool logEnabled = false; }; From 2bb80319bbaaf7254f72f770e5569383d17cf014 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 17 Aug 2022 23:14:50 +0200 Subject: [PATCH 040/113] [P123] Implement Slider/Gauge support, other (size) improvements --- src/_P123_FT62x6Touch.ino | 1 + src/src/PluginStructs/P123_data_struct.cpp | 32 ++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index 05e49790d7..a2a1e094da 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -6,6 +6,7 @@ /** * Changelog: + * 2022-08-15 tonhuisman: Add Swipe and Slider support (to TouchHandler) * 2022-08-15 tonhuisman: UI improvement, settings table uses alternate color per 2 rows, code improvements * 2022-06-10 tonhuisman: Remove p123_ prefixes on Settings variables * 2022-06-06 tonhuisman: Move PLUGIN_WRITE handling mostly to ESPEasy_TouchHandler (only rot and flip subcommands remain) diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index 873b2c4186..3d6645ba96 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -77,7 +77,9 @@ bool P123_data_struct::init(struct EventStruct *event) { void P123_data_struct::displayButtonGroup(struct EventStruct *event, int16_t buttonGroup, int8_t mode) { + # if TOUCH_FEATURE_EXTENDED_TOUCH touchHandler->displayButtonGroup(event, buttonGroup, mode); + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } /** @@ -87,7 +89,11 @@ bool P123_data_struct::displayButton(struct EventStruct *event, const int8_t & buttonNr, int16_t buttonGroup, int8_t mode) { + # if TOUCH_FEATURE_EXTENDED_TOUCH return touchHandler->displayButton(event, buttonNr, buttonGroup, mode); + # else // if TOUCH_FEATURE_EXTENDED_TOUCH + return false; + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } /** @@ -170,6 +176,8 @@ bool P123_data_struct::plugin_fifty_per_second(struct EventStruct *event) { scaleRawToCalibrated(x, y); // Map to screen coordinates if so configured return touchHandler->plugin_fifty_per_second(event, x, y, ox, oy, rx, ry, z); + } else { + touchHandler->releaseTouch(event); } } return false; @@ -374,7 +382,11 @@ int16_t P123_data_struct::getButtonGroup() { */ bool P123_data_struct::validButtonGroup(int16_t buttonGroup, bool ignoreZero) { + # if TOUCH_FEATURE_EXTENDED_TOUCH return touchHandler->validButtonGroup(buttonGroup, ignoreZero); + # else // if TOUCH_FEATURE_EXTENDED_TOUCH + return false; + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } /** @@ -382,35 +394,55 @@ bool P123_data_struct::validButtonGroup(int16_t buttonGroup, */ bool P123_data_struct::setButtonGroup(struct EventStruct *event, int16_t buttonGroup) { + # if TOUCH_FEATURE_EXTENDED_TOUCH return touchHandler->setButtonGroup(event, buttonGroup); + # else // if TOUCH_FEATURE_EXTENDED_TOUCH + return false; + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } /** * Increment button group, if max. group > 0 then min. group = 1 */ bool P123_data_struct::incrementButtonGroup(struct EventStruct *event) { + # if TOUCH_FEATURE_EXTENDED_TOUCH return touchHandler->incrementButtonGroup(event); + # else // if TOUCH_FEATURE_EXTENDED_TOUCH + return false; + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } /** * Decrement button group, if max. group > 0 then min. group = 1 */ bool P123_data_struct::decrementButtonGroup(struct EventStruct *event) { + # if TOUCH_FEATURE_EXTENDED_TOUCH return touchHandler->decrementButtonGroup(event); + # else // if TOUCH_FEATURE_EXTENDED_TOUCH + return false; + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } /** * Increment button group page (+10), if max. group > 0 then min. group page (+10) = 1 */ bool P123_data_struct::incrementButtonPage(struct EventStruct *event) { + # if TOUCH_FEATURE_EXTENDED_TOUCH return touchHandler->incrementButtonPage(event); + # else // if TOUCH_FEATURE_EXTENDED_TOUCH + return false; + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } /** * Decrement button group page (-10), if max. group > 0 then min. group = 1 */ bool P123_data_struct::decrementButtonPage(struct EventStruct *event) { + # if TOUCH_FEATURE_EXTENDED_TOUCH return touchHandler->decrementButtonPage(event); + # else // if TOUCH_FEATURE_EXTENDED_TOUCH + return false; + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } #endif // ifdef USES_P123 From dbe118e56ab2b3bf813794d0e247c59b4e966e94 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 22 Aug 2022 19:54:31 +0200 Subject: [PATCH 041/113] [AdaGFX] Improved drawing of Slider/Gauge control (also reverted) --- src/src/Helpers/AdafruitGFX_helper.cpp | 71 ++++++++++++++++---------- src/src/Helpers/AdafruitGFX_helper.h | 1 + 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index e352a8fd39..4db978a2be 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -1702,21 +1702,22 @@ bool AdafruitGFX_helper::processCommand(const String& string) { const bool showAsCircle = borderColor == fillColor; // determine value and range - int16_t offI2 = 5; // half of indicator width - int16_t offG2 = 3; // half of Gauge width - int16_t offP = 0; // Offset for indicator - int16_t zeroLine = -1; // Draw a range zero-line at this offset? only when >= 0 - int percentage = 0; - float gaugeValue = 0.0f; - int16_t lowRange = 0; - int16_t highRange = 100; - float rangeFrom = 0.0f; - float rangeTo = 0.0f; - float range = 100.0f; // For percentage the range is 100 - bool useRange = false; + int16_t offI2 = 5; // half of indicator width + int16_t offG2 = 3; // half of Gauge width + int16_t offP = 0; // Offset for indicator + int16_t zeroLine = -1; // Draw a range zero-line at this offset? only when >= 0 + int percentage = 0; + float gaugeValue = 0.0f; + int16_t lowRange = 0; + int16_t highRange = 100; + float rangeFrom = 0.0f; + float rangeTo = 0.0f; + float range = 100.0f; // For percentage the range is 100 + bool useRange = false; + bool hasRangeReversed = false; // Range low value left or top, high value right or bottom if (!validFloatFromString(newString, gaugeValue)) { - percentage = nParams[0]; // Value as provided + percentage = nParams[0]; // Value as provided } // Have a range? @@ -1727,9 +1728,10 @@ bool AdafruitGFX_helper::processCommand(const String& string) { if (validFrom && validFloatFromString(tmp, rangeTo) && !essentiallyEqual(rangeFrom, 0.0f) && !essentiallyEqual(rangeTo, 0.0f)) { - useRange = true; - lowRange = static_cast(min(rangeFrom, rangeTo)); - highRange = static_cast(max(rangeTo, rangeFrom)); + useRange = true; + lowRange = static_cast(rangeFrom); + highRange = static_cast(rangeTo); + hasRangeReversed = lowRange > highRange; // Range high value left or top, low value right or bottom? } } @@ -1751,13 +1753,18 @@ bool AdafruitGFX_helper::processCommand(const String& string) { gaugeValue -= min(rangeFrom, rangeTo); // Give it the correct Offset } - if ((lowRange < 0) && - (highRange > 0)) { + if (((lowRange < 0) && (highRange > 0)) || + ((lowRange > 0) && (highRange < 0))) { zeroLine = map(0, lowRange, highRange, 0, isVertical ? nParams[5] : nParams[4]); } } percentage = static_cast(gaugeValue); - offP = ((((isVertical ? nParams[5] : nParams[4]) - (2 * offI2)) / range) * percentage) - 1; // keep within button borders + + offP = ((((isVertical ? nParams[5] : nParams[4]) - (2 * offI2)) / range) * percentage) - 1; // keep within button borders + + if (hasRangeReversed) { + offP = (isVertical ? nParams[5] : nParams[4]) - (2 * offI2) - offP - 1; // flip + } if (isVertical) { // centerline @@ -1770,8 +1777,14 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } // Gauge - _display->fillRoundRect(nParams[2] + _xo + (nParams[4] / 2) - offG2, nParams[3] + _yo + (nParams[5] - offI2 - offP - 1), - 2 * offG2, offP, offG2, textColor); + if (hasRangeReversed) { + int16_t bar = nParams[5] - (2 * offI2); + _display->fillRoundRect(nParams[2] + _xo + (nParams[4] / 2) - offG2, nParams[3] + _yo + offI2 + 0, + 2 * offG2, bar - offP, offG2, textColor); + } else { + _display->fillRoundRect(nParams[2] + _xo + (nParams[4] / 2) - offG2, nParams[3] + _yo + (nParams[5] - offI2 - offP - 1), + 2 * offG2, offP, offG2, textColor); + } // Indicator/drag-handle if (showAsCircle) { // Circle indicator @@ -1779,10 +1792,10 @@ bool AdafruitGFX_helper::processCommand(const String& string) { nParams[3] + _yo + (nParams[5] - offI2 - offP - 2) + (percentage == 100 ? 1 : 0), trunc(nParams[4] / 4), textColor); } else { - _display->fillRoundRect(nParams[2] + _xo + 1, nParams[3] + _yo + (nParams[5] - (2 * offI2) - offP - 1), + _display->fillRoundRect(nParams[2] + _xo + 1, nParams[3] + _yo + (nParams[5] - (2 * offI2) - offP - (hasRangeReversed ? 0 : 1)), nParams[4] - 2, 2 * offI2, offI2, textColor); } - } else { // : if !isVertical + } else { // : if !isVertical -> isHorizontal // centerline _display->drawLine(nParams[2] + _xo + offI2 + 1, nParams[3] + _yo + nParams[5] / 2, nParams[2] + _xo + nParams[4] - offI2, nParams[3] + _yo + nParams[5] / 2, textColor); @@ -1793,8 +1806,14 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } // Gauge - _display->fillRoundRect(nParams[2] + _xo + offI2 + 1, nParams[3] + _yo + (nParams[5] / 2) - offG2, - offP, offG2 * 2, offG2, textColor); + if (hasRangeReversed) { + int16_t bar = nParams[4] - (2 * offI2); + _display->fillRoundRect(nParams[2] + _xo + offI2 + offP + 2, nParams[3] + _yo + (nParams[5] / 2) - offG2, + bar - offP, offG2 * 2, offG2, textColor); + } else { + _display->fillRoundRect(nParams[2] + _xo + offI2 + 1, nParams[3] + _yo + (nParams[5] / 2) - offG2, + offP, offG2 * 2, offG2, textColor); + } // Indicator/drag-handle if (showAsCircle) { // Circle indicator @@ -1802,7 +1821,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { nParams[3] + _yo + (nParams[5] / 2), trunc(nParams[5] / 4), textColor); } else { - _display->fillRoundRect(nParams[2] + _xo + offP + 1, nParams[3] + _yo + 1, + _display->fillRoundRect(nParams[2] + _xo + offP + (hasRangeReversed ? 0 : 1), nParams[3] + _yo + 1, 2 * offI2, nParams[5] - 2, offI2, textColor); } } diff --git a/src/src/Helpers/AdafruitGFX_helper.h b/src/src/Helpers/AdafruitGFX_helper.h index e999bc05ae..fe6a19feae 100644 --- a/src/src/Helpers/AdafruitGFX_helper.h +++ b/src/src/Helpers/AdafruitGFX_helper.h @@ -12,6 +12,7 @@ ***************************************************************************/ /************ * Changelog: + * 2022-08-22 tonhuisman: Improve drawing of slider when using a range, so a reverse range (40,-10) is displayed 'flipped' * 2022-08-16 tonhuisman: Add drawing of Slide/Gauge controls via btn subcommand, horizontal or vertical depending on width/height ratio * 2022-08-15 tonhuisman: Add initial support for slide/gauge controls * 2022-06-07 tonhuisman: Code improvements in initialization, move offset calculation to printText() function From eeed82a26ef658d93b07395a8687e0d113d8d6c0 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 22 Aug 2022 19:58:43 +0200 Subject: [PATCH 042/113] [TouchHandler] Enable use of reverted sliders/gauges, and improved handling of ranges that don't have 0 included --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 59 +++++++++++++++++++----- src/src/Helpers/ESPEasy_TouchHandler.h | 1 + 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 783fd0e077..c8753633a5 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -36,6 +36,7 @@ const __FlashStringHelper* toString(Swipe_action_e action) { case Swipe_action_e::Left: return F("Left"); case Swipe_action_e::LeftUp: return F("Left-Up"); case Swipe_action_e::None: return F("None"); + case Swipe_action_e::SwipeAction_MAX: break; } return F("Unknown"); } @@ -171,6 +172,35 @@ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { TouchObjects[t].TouchTimers = 0u; TouchObjects[t].TouchStates = 0; + # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE + + // Check if a slider/gauge with range not including 0 is used, then set starting value closest to 0 + if (bitRead(TouchObjects[t].flags, TOUCH_OBJECT_FLAG_SLIDER) && !TouchObjects[t].captionOff.isEmpty()) { + float rangeFrom = 0.0f; + float rangeTo = 0.0f; + String tmp = parseString(TouchObjects[t].captionOff, 1); + const bool validFrom = validFloatFromString(tmp, rangeFrom); + tmp = parseString(TouchObjects[t].captionOff, 2); + + if (validFrom && validFloatFromString(tmp, rangeTo) && + !essentiallyEqual(rangeFrom, 0.0f) && !essentiallyEqual(rangeTo, 0.0f)) { + if (definitelyGreaterThan(rangeFrom, 0.0f) && definitelyGreaterThan(rangeTo, 0.0f)) { + if (definitelyGreaterThan(rangeFrom, rangeTo)) { + TouchObjects[t].TouchStates = rangeTo; + } else { + TouchObjects[t].TouchStates = rangeFrom; + } + } else if (definitelyLessThan(rangeFrom, 0.0f) && definitelyLessThan(rangeTo, 0.0f)) { + if (definitelyGreaterThan(rangeFrom, rangeTo)) { + TouchObjects[t].TouchStates = rangeFrom; + } else { + TouchObjects[t].TouchStates = rangeTo; + } + } + } + } + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE + t++; settingsArray[i].clear(); // Free a little memory @@ -753,6 +783,11 @@ bool ESPEasy_TouchHandler::decrementButtonPage(struct EventStruct *event) { * Load the settings onto the webpage */ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { + if (!_settingsLoaded) { + loadTouchObjects(event); + _settingsLoaded = true; + } + addFormSubHeader(F("Touch configuration")); addFormCheckBox(F("Flip rotation 180°"), F("rotation_flipped"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_ROTATION_FLIPPED)); @@ -940,7 +975,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { bitRead(Touch_Settings.flags, TOUCH_FLAGS_AUTO_PAGE_ARROWS)); addFormCheckBox(F("PageUp/PageDown reversed"), F("page_below"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU)); - addFormCheckBox(F("Swipe Left/Right/Up/Down menu reversed"), F("swipeswap"), + addFormCheckBox(F("Swipe Left/Right/Up/Down menu reversed"), F("swipe_swap"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_SWAP_LEFT_RIGHT)); } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH @@ -1304,8 +1339,8 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { addUnit(F("1..25px")); addFormNumericBox(F("Maximum swipe margin"), F("swipemax"), - Touch_Settings.swipeMargin, 5, 100); - addUnit(F("5..100px")); + Touch_Settings.swipeMargin, 5, 250); + addUnit(F("5..250px")); # endif // if TOUCH_FEATURE_SWIPE } return false; @@ -1349,7 +1384,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { bitWrite(lSettings, TOUCH_FLAGS_DRAWBTN_VIA_RULES, isFormItemChecked(F("via_rules"))); bitWrite(lSettings, TOUCH_FLAGS_AUTO_PAGE_ARROWS, isFormItemChecked(F("page_buttons"))); bitWrite(lSettings, TOUCH_FLAGS_PGUP_BELOW_MENU, isFormItemChecked(F("page_below"))); - bitWrite(lSettings, TOUCH_FLAGS_SWAP_LEFT_RIGHT, isFormItemChecked(F("swipeswap"))); + bitWrite(lSettings, TOUCH_FLAGS_SWAP_LEFT_RIGHT, isFormItemChecked(F("swipe_swap"))); # endif // if TOUCH_FEATURE_EXTENDED_TOUCH config += getFormItemInt(F("use_calibration")); // First value should NEVER be empty, or parseString() wil get confused @@ -1545,7 +1580,8 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { } /** - * Every 20 milliseconds we check if the screen is touched + * Every 20 milliseconds we check if the screen is touched, + * handles button switching, swiping and slider-sliding */ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, const int16_t & x, @@ -1571,7 +1607,7 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, loglevelActiveFor(LOG_LEVEL_INFO)) { // REQUIRED for calibration and setting up objects, so do not make this optional! String log; log.reserve(72); - log = F("Touch calibration rx= "); // Space before the logged values added for readability + log = F("Touch calibration rx= "); // Space before the logged values for readability log += rx; log += F(", ry= "); log += ry; @@ -1667,8 +1703,8 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, # if TOUCH_FEATURE_SWIPE (swipe == Swipe_action_e::None) && # endif // if TOUCH_FEATURE_SWIPE - ((TouchObjects[selectedObjectIndex].TouchTimers == 0) - || (TouchObjects[selectedObjectIndex].TouchTimers < (millis() - (1.5 * Touch_Settings.debounceMs))) + ((TouchObjects[selectedObjectIndex].TouchTimers == 0) || + (TouchObjects[selectedObjectIndex].TouchTimers < (millis() - (1.5 * Touch_Settings.debounceMs))) )) { // From now wait the debounce time TouchObjects[selectedObjectIndex].TouchTimers = millis() + Touch_Settings.debounceMs; @@ -1690,7 +1726,8 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, // Button touched _lastObjectIndex = selectedObjectIndex; // Handle on release # if TOUCH_FEATURE_SWIPE - } else if ((selectedObjectIndex > -1) && bitRead(TouchObjects[selectedObjectIndex].flags, TOUCH_OBJECT_FLAG_SLIDER)) { + } else if ((swipe != Swipe_action_e::None) && + (selectedObjectIndex > -1) && bitRead(TouchObjects[selectedObjectIndex].flags, TOUCH_OBJECT_FLAG_SLIDER)) { // Handle slider immediately to move/set absolute position _lastObjectIndex = -1; // Handled const bool isVertical = TouchObjects[selectedObjectIndex].width_height.x < TouchObjects[selectedObjectIndex].width_height.y; @@ -1721,8 +1758,8 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, } if (useRange) { // Calculate range-boundaries - lowRange = static_cast(min(rangeFrom, rangeTo)); - highRange = static_cast(max(rangeTo, rangeFrom)); + lowRange = static_cast(rangeFrom); + highRange = static_cast(rangeTo); position = map(position, 0, 100, lowRange, highRange); TouchObjects[selectedObjectIndex].TouchStates = position; } else if (position < lowRange) { diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index 04b383f786..3c11acc88b 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -11,6 +11,7 @@ /***** * Changelog: + * 2022-08-22 tonhuisman: Improve Slider range handling so a reverse range (40,-10) can also be used. * 2022-08-17 tonhuisman: Add support for range (x..y) for sliders * 2022-08-16 tonhuisman: Changed validButtonGroup() to ignore group 0 by default, and setButtonGroup(), and setgrp subcommand, * to allow group 0 From 3f24e3683ddd6f0e987952310bf5bc24ff3d2b9a Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 22 Aug 2022 19:59:33 +0200 Subject: [PATCH 043/113] [P123] Minor code improvements --- src/_P123_FT62x6Touch.ino | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index a2a1e094da..648cf89610 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -105,8 +105,6 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) } case PLUGIN_WEBFORM_LOAD: { - addFormSubHeader(F("Screen")); - { addRowLabel(F("Display task")); addTaskSelect(F("dsptask"), P123_CONFIG_DISPLAY_TASK); @@ -115,7 +113,7 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) #endif // ifndef P123_LIMIT_BUILD_SIZE } - uint16_t width_ = P123_CONFIG_X_RES; + uint16_t width_ = P123_CONFIG_X_RES; uint16_t height_ = P123_CONFIG_Y_RES; uint16_t rotation_ = P123_CONFIG_ROTATION; uint16_t colorDepth_ = P123_COLOR_DEPTH; @@ -143,8 +141,8 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) addFormNumericBox(F("Touch minimum pressure"), F("threshold"), P123_CONFIG_THRESHOLD, 0, 255); { - P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); bool deleteP123_data = false; + P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); if (nullptr == P123_data) { P123_data = new (std::nothrow) P123_data_struct(); @@ -152,10 +150,6 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) } if (nullptr != P123_data) { - if (deleteP123_data) { - P123_data->loadTouchObjects(event); - } - P123_data->plugin_webform_load(event); if (deleteP123_data) { From ed41e72e816435df022da7ff2fec969e29970538 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 27 Aug 2022 21:32:51 +0200 Subject: [PATCH 044/113] [TouchHandler] Add optional touchscreen disabling, code fixes and improvements --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 190 ++++++++++++++++------- src/src/Helpers/ESPEasy_TouchHandler.h | 17 +- 2 files changed, 146 insertions(+), 61 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index c8753633a5..a8a5045142 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -48,7 +48,7 @@ const __FlashStringHelper* toString(Swipe_action_e action) { */ ESPEasy_TouchHandler::ESPEasy_TouchHandler() {} -ESPEasy_TouchHandler::ESPEasy_TouchHandler(const uint16_t & displayTask, +ESPEasy_TouchHandler::ESPEasy_TouchHandler(const taskIndex_t & displayTask, const AdaGFXColorDepth& colorDepth) : _displayTask(displayTask), _colorDepth(colorDepth) {} @@ -176,27 +176,25 @@ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { // Check if a slider/gauge with range not including 0 is used, then set starting value closest to 0 if (bitRead(TouchObjects[t].flags, TOUCH_OBJECT_FLAG_SLIDER) && !TouchObjects[t].captionOff.isEmpty()) { - float rangeFrom = 0.0f; - float rangeTo = 0.0f; - String tmp = parseString(TouchObjects[t].captionOff, 1); - const bool validFrom = validFloatFromString(tmp, rangeFrom); - tmp = parseString(TouchObjects[t].captionOff, 2); - - if (validFrom && validFloatFromString(tmp, rangeTo) && - !essentiallyEqual(rangeFrom, 0.0f) && !essentiallyEqual(rangeTo, 0.0f)) { - if (definitelyGreaterThan(rangeFrom, 0.0f) && definitelyGreaterThan(rangeTo, 0.0f)) { - if (definitelyGreaterThan(rangeFrom, rangeTo)) { - TouchObjects[t].TouchStates = rangeTo; - } else { - TouchObjects[t].TouchStates = rangeFrom; + int16_t _value = 0; + int16_t lowRange = 0; + int16_t highRange = 100; + + if (parseRangeToInt16(TouchObjects[t].captionOff, lowRange, highRange)) { + if (lowRange > highRange) { + if (_value < highRange) { + _value = highRange; + } else if (_value > lowRange) { + _value = lowRange; } - } else if (definitelyLessThan(rangeFrom, 0.0f) && definitelyLessThan(rangeTo, 0.0f)) { - if (definitelyGreaterThan(rangeFrom, rangeTo)) { - TouchObjects[t].TouchStates = rangeFrom; - } else { - TouchObjects[t].TouchStates = rangeTo; + } else { + if (_value < lowRange) { + _value = lowRange; + } else if (_value > highRange) { + _value = highRange; } } + TouchObjects[t].TouchStates = _value; } } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE @@ -219,6 +217,7 @@ void ESPEasy_TouchHandler::init(struct EventStruct *event) { } # if TOUCH_FEATURE_EXTENDED_TOUCH + _touchEnabled = bitRead(TOUCH_COMMON_FLAGS, TOUCH_FLAGS_IGNORE_TOUCH); if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME) && bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)) { @@ -355,6 +354,38 @@ int8_t ESPEasy_TouchHandler::getTouchObjectIndex(struct EventStruct *event, int index = -1; + int16_t idx = -1; + + if ((idx = touchObject.indexOf('.')) > -1) { + String part = touchObject.substring(0, idx); + int grp = -1; + + if (validIntFromString(part, grp) && validButtonGroup(static_cast(grp), false)) { + part = touchObject.substring(idx + 1); + int btn = -1; + + if (validIntFromString(part, btn)) { + idx = 0; + + for (size_t objectNr = 0; objectNr < TouchObjects.size(); objectNr++) { + if (!TouchObjects[objectNr].objectName.isEmpty() + && (get8BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_GROUP) == grp) + && (!isButton || bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTON))) { + idx++; + + if (idx == btn) { + return static_cast(objectNr); + } + } + } + } else { + return -1; // Invalid button name + } + } else { + return -1; // Invalid group number + } + } + // ATTENTION: Any externally provided objectNumber is 1-based, result is 0-based if (validIntFromString(touchObject, index) && (index > 0) && @@ -401,7 +432,7 @@ bool ESPEasy_TouchHandler::setTouchObjectState(struct EventStruct *event, } # ifdef TOUCH_DEBUG - if (loglevelActiveFor(LOG_LEVEL_INFO)) { + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log = F("TOUCH setTouchObjectState: obj: "); log += touchObject; log += '/'; @@ -414,7 +445,7 @@ bool ESPEasy_TouchHandler::setTouchObjectState(struct EventStruct *event, } else { log += F(" failed!"); } - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG } @@ -465,14 +496,14 @@ bool ESPEasy_TouchHandler::setTouchButtonOnOff(struct EventStruct *event, } # ifdef TOUCH_DEBUG - if (loglevelActiveFor(LOG_LEVEL_INFO)) { + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log = F("TOUCH setTouchButtonOnOff: obj: "); log += touchObject; log += '/'; log += objectNr; log += F(", new state: "); log += (state ? F("on") : F("off")); - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG } @@ -506,7 +537,7 @@ int16_t ESPEasy_TouchHandler::getTouchObjectValue(struct EventStruct *event, */ bool ESPEasy_TouchHandler::setTouchObjectValue(struct EventStruct *event, const String & touchObject, - const uint16_t & value) { + const int16_t & value) { if (touchObject.isEmpty()) { return false; } bool success = false; @@ -517,30 +548,76 @@ bool ESPEasy_TouchHandler::setTouchObjectValue(struct EventStruct *event, success = true; // Always success if matched object if (value != TouchObjects[objectNr].TouchStates) { - TouchObjects[objectNr].TouchStates = value; + int16_t _value = value; + + if (bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_SLIDER)) { + int16_t lowRange = 0; + int16_t highRange = 100; + + if (!TouchObjects[objectNr].captionOff.isEmpty()) { // Off caption can hold range: , + parseRangeToInt16(TouchObjects[objectNr].captionOff, lowRange, highRange); + + if (lowRange > highRange) { + if (_value < highRange) { + _value = highRange; + } else if (_value > lowRange) { + _value = lowRange; + } + } else { + if (_value < lowRange) { + _value = lowRange; + } else if (_value > highRange) { + _value = highRange; + } + } + } + } + TouchObjects[objectNr].TouchStates = _value; // Send event like it was pressed if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME) && bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)) { - generateObjectEvent(event, objectNr, value); + generateObjectEvent(event, objectNr, _value); } } # ifdef TOUCH_DEBUG - if (loglevelActiveFor(LOG_LEVEL_INFO)) { + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log = F("TOUCH setTouchObjectValue: obj: "); log += touchObject; log += '/'; log += objectNr; log += F(", new value: "); log += value; - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG } return success; } +/** + * parseRangeToInt16: get the low and high values of a range and convert to int16_t + */ +bool ESPEasy_TouchHandler::parseRangeToInt16(const String& range, + int16_t & lowRange, + int16_t & highRange) { + float rangeFrom = 0.0f; + float rangeTo = 0.0f; + String tmp = parseString(range, 1); + const bool validFrom = validFloatFromString(tmp, rangeFrom); + + tmp = parseString(range, 2); + + if (validFrom && validFloatFromString(tmp, rangeTo) && + !essentiallyEqual(rangeFrom, 0.0f) && !essentiallyEqual(rangeTo, 0.0f)) { + lowRange = static_cast(rangeFrom); + highRange = static_cast(rangeTo); + return true; + } + return false; +} + /** * mode: -2 = clear buttons in group, -3 = clear all buttongroups, -1 = draw buttons in group, 0 = initialize buttons */ @@ -827,6 +904,13 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { addFormCheckBox(F("Prevent duplicate events"), F("deduplicate"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_DEDUPLICATE)); + # if TOUCH_FEATURE_EXTENDED_TOUCH + addFormCheckBox(F("Ignore touch-screen"), F("ignoretouch"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_IGNORE_TOUCH)); + # ifndef LIMIT_BUILD_SIZE + addFormNote(F("To enable the use of touch-object display-functions only.")); + # endif // ifndef LIMIT_BUILD_SIZE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + # ifndef LIMIT_BUILD_SIZE if (!Settings.UseRules) { @@ -851,10 +935,10 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { if (Touch_Settings.calibrationEnabled) { addRowLabel(F("Calibration")); html_table(EMPTY_STRING, false); // Sub-table - html_table_header(F("")); + html_table_header(EMPTY_STRING); html_table_header(F("x")); html_table_header(F("y")); - html_table_header(F("")); + html_table_header(EMPTY_STRING); html_table_header(F("x")); html_table_header(F("y")); @@ -1385,6 +1469,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { bitWrite(lSettings, TOUCH_FLAGS_AUTO_PAGE_ARROWS, isFormItemChecked(F("page_buttons"))); bitWrite(lSettings, TOUCH_FLAGS_PGUP_BELOW_MENU, isFormItemChecked(F("page_below"))); bitWrite(lSettings, TOUCH_FLAGS_SWAP_LEFT_RIGHT, isFormItemChecked(F("swipe_swap"))); + bitWrite(lSettings, TOUCH_FLAGS_IGNORE_TOUCH, isFormItemChecked(F("ignoretouch"))); # endif // if TOUCH_FEATURE_EXTENDED_TOUCH config += getFormItemInt(F("use_calibration")); // First value should NEVER be empty, or parseString() wil get confused @@ -1734,19 +1819,10 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, int16_t position = 0; int16_t lowRange = 0; int16_t highRange = 100; - float rangeFrom = 0.0f; - float rangeTo = 0.0f; - bool useRange = false; + bool useRange = false; if (!TouchObjects[selectedObjectIndex].captionOff.isEmpty()) { // Off caption can hold range: , - String tmp = parseString(TouchObjects[selectedObjectIndex].captionOff, 1); - const bool validFrom = validFloatFromString(tmp, rangeFrom); - tmp = parseString(TouchObjects[selectedObjectIndex].captionOff, 2); - - if (validFrom && validFloatFromString(tmp, rangeTo) && - !essentiallyEqual(rangeFrom, 0.0f) && !essentiallyEqual(rangeTo, 0.0f)) { - useRange = true; - } + useRange = parseRangeToInt16(TouchObjects[selectedObjectIndex].captionOff, lowRange, highRange); } if (isVertical) { @@ -1758,8 +1834,6 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, } if (useRange) { // Calculate range-boundaries - lowRange = static_cast(rangeFrom); - highRange = static_cast(rangeTo); position = map(position, 0, 100, lowRange, highRange); TouchObjects[selectedObjectIndex].TouchStates = position; } else if (position < lowRange) { @@ -1871,14 +1945,14 @@ bool ESPEasy_TouchHandler::plugin_write(struct EventStruct *event, String arguments; uint8_t arg = 3; - arguments.reserve(24); - command = parseString(string, 1); - subcommand = parseString(string, 2); + command = parseString(string, 1); if (command.equals(F("touch"))) { + arguments.reserve(24); + subcommand = parseString(string, 2); # ifdef TOUCH_DEBUG - if (loglevelActiveFor(LOG_LEVEL_INFO)) { + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log = F("TOUCH PLUGIN_WRITE arguments Par1:"); log += event->Par1; log += F(", 2: "); @@ -1889,7 +1963,7 @@ bool ESPEasy_TouchHandler::plugin_write(struct EventStruct *event, log += event->Par4; log += F(", string: "); log += string; - addLog(LOG_LEVEL_INFO, log); + addLog(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG @@ -1987,11 +2061,11 @@ bool ESPEasy_TouchHandler::plugin_get_config_value(struct EventStruct *event, bool success = false; const String command = parseString(string, 1); - if (command == F("buttongroup")) { + if (command.equals(F("buttongroup"))) { string = getButtonGroup(); success = true; # if TOUCH_FEATURE_EXTENDED_TOUCH - } else if (command == F("hasgroup")) { + } else if (command.equals(F("hasgroup"))) { int group; // We'll be ignoring group 0 if there are multiple button groups if (validIntFromString(parseString(string, 2), group)) { @@ -2001,7 +2075,7 @@ bool ESPEasy_TouchHandler::plugin_get_config_value(struct EventStruct *event, string = '0'; // invalid number = false } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH - } else if (command == F("enabled")) { + } else if (command.equals(F("enabled"))) { const String arguments = parseStringKeepCase(string, 2); int8_t enabled = getTouchObjectState(event, arguments); @@ -2009,21 +2083,19 @@ bool ESPEasy_TouchHandler::plugin_get_config_value(struct EventStruct *event, string = enabled; success = true; } - } else if (command == F("state")) { + } else if (command.equals(F("state"))) { const String arguments = parseStringKeepCase(string, 2); - int8_t state = getTouchObjectValue(event, arguments); + int16_t state = getTouchObjectValue(event, arguments); - if (state > -1) { - string = state; - success = true; - } + string = state; + success = true; # if TOUCH_FEATURE_EXTENDED_TOUCH - } else if (command == F("pagemode")) { + } else if (command.equals(F("pagemode"))) { string = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); success = true; # endif // if TOUCH_FEATURE_EXTENDED_TOUCH # if TOUCH_FEATURE_SWIPE - } else if (command == F("swipedir")) { + } else if (command.equals(F("swipedir"))) { int state; if (validIntFromString(parseString(string, 2), state)) { diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index 3c11acc88b..e6de390c68 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -11,6 +11,10 @@ /***** * Changelog: + * 2022-08-27 tonhuisman: Enable identifying an object using the . notation, where groupnr = 0..255, and objectnr + * range starts at 1, in sequential order for the objects in that group + * Add option to disable polling the touch-screen, to be able to only use the object/button draw features + * f.e. when using a device like an M5Stack Core that has buttons below the screen * 2022-08-22 tonhuisman: Improve Slider range handling so a reverse range (40,-10) can also be used. * 2022-08-17 tonhuisman: Add support for range (x..y) for sliders * 2022-08-16 tonhuisman: Changed validButtonGroup() to ignore group 0 by default, and setButtonGroup(), and setgrp subcommand, @@ -108,6 +112,7 @@ # define TOUCH_FLAGS_AUTO_PAGE_ARROWS 17 // Automatically enable/disable paging buttons # define TOUCH_FLAGS_PGUP_BELOW_MENU 18 // Group-page below current menu (reverts Up/Down buttons) # define TOUCH_FLAGS_SWAP_LEFT_RIGHT 19 // Swaps Left and Right, Up and Down swipe directions for menu actions +# define TOUCH_FLAGS_IGNORE_TOUCH 20 // Disable touch, use for object/button features only # define TOUCH_VALUE_X UserVar[event->BaseVarIndex + 0] # define TOUCH_VALUE_Y UserVar[event->BaseVarIndex + 1] @@ -267,7 +272,7 @@ class ESPEasy_TouchHandler { public: ESPEasy_TouchHandler(); - ESPEasy_TouchHandler(const uint16_t & displayTask, + ESPEasy_TouchHandler(const taskIndex_t & displayTask, const AdaGFXColorDepth& colorDepth); virtual ~ESPEasy_TouchHandler() {} @@ -293,7 +298,7 @@ class ESPEasy_TouchHandler { const String & touchObject); bool setTouchObjectValue(struct EventStruct *event, const String & touchObject, - const uint16_t & value); + const int16_t & value); bool plugin_webform_load(struct EventStruct *event); bool plugin_webform_save(struct EventStruct *event); bool plugin_fifty_per_second(struct EventStruct *event, @@ -335,6 +340,10 @@ class ESPEasy_TouchHandler { int8_t mode = 0); # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + bool touchEnabled() { + return _touchEnabled; + } + private: int parseStringToInt(const String & string, @@ -347,6 +356,9 @@ class ESPEasy_TouchHandler { const int8_t & mode = 0, const bool & groupSwitch = false, const int8_t & factor = 1); + bool parseRangeToInt16(const String& range, + int16_t & lowRange, + int16_t & highRange); bool _deduplicate = false; uint16_t _displayTask = 0u; @@ -357,6 +369,7 @@ class ESPEasy_TouchHandler { bool _settingsLoaded = false; bool _stillTouching = false; + bool _touchEnabled = true; // Used to generate events on touch-release int8_t _lastObjectIndex = -1; From 32bea53526467c6acfafb3df30af9ac6a855052c Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 27 Aug 2022 21:33:56 +0200 Subject: [PATCH 045/113] [P123] Implement disabled-touchscreen feature --- src/src/PluginStructs/P123_data_struct.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index 3d6645ba96..cc6d727c46 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -203,7 +203,7 @@ void P123_data_struct::loadTouchObjects(struct EventStruct *event) { */ bool P123_data_struct::touched() { if (isInitialized()) { - return touchscreen->touched(); + return touchHandler->touchEnabled() && touchscreen->touched(); } return false; } From d14fd1d9003dc3619b57ee9f1d19b65b71a64511 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 27 Aug 2022 22:26:03 +0200 Subject: [PATCH 046/113] [TouchHandler] Fix typo and conditional compilation issue --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index a8a5045142..9baf75916e 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -360,7 +360,9 @@ int8_t ESPEasy_TouchHandler::getTouchObjectIndex(struct EventStruct *event, String part = touchObject.substring(0, idx); int grp = -1; - if (validIntFromString(part, grp) && validButtonGroup(static_cast(grp), false)) { + # if TOUCH_FEATURE_EXTENDED_TOUCH + + if (validIntFromString(part, grp) && validButtonGroup(static_cast(grp), false)) { part = touchObject.substring(idx + 1); int btn = -1; @@ -384,6 +386,7 @@ int8_t ESPEasy_TouchHandler::getTouchObjectIndex(struct EventStruct *event, } else { return -1; // Invalid group number } + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } // ATTENTION: Any externally provided objectNumber is 1-based, result is 0-based From 3d9bba3f03a23aea46bafc19576fb15204840bf3 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 29 Aug 2022 23:22:07 +0200 Subject: [PATCH 047/113] [UI] Improvements for `multi2row` style --- src/src/Static/WebStaticData.h | 2 +- static/espeasy_default.css | 19 +++++++++++++------ static/espeasy_default.min.css | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/src/Static/WebStaticData.h b/src/src/Static/WebStaticData.h index 9c11aa6eba..22417c0865 100644 --- a/src/src/Static/WebStaticData.h +++ b/src/src/Static/WebStaticData.h @@ -85,7 +85,7 @@ static const char DATA_GITHUB_CLIPBOARD_JS[] PROGMEM = {0x66,0x75,0x6e,0x63,0x74 #endif #if defined(WEBSERVER_CSS) && !defined(WEBSERVER_EMBED_CUSTOM_CSS) -const char DATA_ESPEASY_DEFAULT_MIN_CSS[] PROGMEM = {0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x2c,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x37,0x64,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x62,0x6c,0x6f,0x63,0x6b,0x7d,0x68,0x32,0x2c,0x68,0x33,0x7b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x36,0x70,0x78,0x7d,0x68,0x31,0x2c,0x68,0x36,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x37,0x64,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x3b,0x63,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x3a,0x27,0x27,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2c,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x74,0x65,0x78,0x74,0x2d,0x64,0x65,0x63,0x6f,0x72,0x61,0x74,0x69,0x6f,0x6e,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x64,0x69,0x76,0x5f,0x6c,0x2c,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x66,0x6c,0x6f,0x61,0x74,0x3a,0x6c,0x65,0x66,0x74,0x7d,0x2e,0x63,0x6c,0x6f,0x73,0x65,0x62,0x74,0x6e,0x2c,0x2e,0x64,0x69,0x76,0x5f,0x72,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x66,0x6c,0x6f,0x61,0x74,0x3a,0x72,0x69,0x67,0x68,0x74,0x7d,0x2a,0x7b,0x62,0x6f,0x78,0x2d,0x73,0x69,0x7a,0x69,0x6e,0x67,0x3a,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x62,0x6f,0x78,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x66,0x61,0x6d,0x69,0x6c,0x79,0x3a,0x73,0x61,0x6e,0x73,0x2d,0x73,0x65,0x72,0x69,0x66,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x32,0x70,0x74,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x30,0x7d,0x68,0x31,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x36,0x70,0x74,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x38,0x70,0x78,0x20,0x30,0x7d,0x68,0x32,0x2c,0x68,0x33,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x32,0x70,0x74,0x7d,0x68,0x32,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x30,0x20,0x2d,0x34,0x70,0x78,0x7d,0x68,0x33,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x63,0x61,0x63,0x61,0x63,0x61,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x2d,0x34,0x70,0x78,0x7d,0x68,0x36,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x30,0x70,0x74,0x7d,0x63,0x6f,0x64,0x65,0x2c,0x6b,0x62,0x64,0x2c,0x70,0x72,0x65,0x2c,0x73,0x61,0x6d,0x70,0x2c,0x74,0x74,0x2c,0x78,0x6d,0x70,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x66,0x61,0x6d,0x69,0x6c,0x79,0x3a,0x6d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x2c,0x6d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x65,0x6d,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x3a,0x30,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x70,0x78,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x34,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x20,0x31,0x36,0x70,0x78,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x2c,0x69,0x6e,0x70,0x75,0x74,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x65,0x65,0x65,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x68,0x65,0x6c,0x70,0x2c,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2c,0x69,0x6e,0x70,0x75,0x74,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x3a,0x31,0x70,0x78,0x20,0x73,0x6f,0x6c,0x69,0x64,0x20,0x67,0x72,0x61,0x79,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x6c,0x69,0x6e,0x6b,0x2e,0x77,0x69,0x64,0x65,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x69,0x6e,0x6c,0x69,0x6e,0x65,0x2d,0x62,0x6c,0x6f,0x63,0x6b,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x25,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x6c,0x69,0x6e,0x6b,0x2e,0x72,0x65,0x64,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x72,0x65,0x64,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x68,0x65,0x6c,0x70,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x30,0x25,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x66,0x61,0x6d,0x69,0x6c,0x79,0x3a,0x6d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x32,0x70,0x78,0x20,0x37,0x70,0x78,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2c,0x69,0x6e,0x70,0x75,0x74,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x70,0x78,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x3a,0x68,0x6f,0x76,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x3a,0x23,0x33,0x36,0x39,0x7d,0x69,0x6e,0x70,0x75,0x74,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x20,0x38,0x70,0x78,0x7d,0x69,0x6e,0x70,0x75,0x74,0x2e,0x77,0x69,0x64,0x65,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2e,0x77,0x69,0x64,0x65,0x7b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x35,0x70,0x78,0x3b,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x33,0x35,0x30,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x39,0x35,0x25,0x7d,0x2e,0x77,0x69,0x64,0x65,0x6e,0x75,0x6d,0x62,0x65,0x72,0x7b,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x35,0x30,0x30,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x7b,0x63,0x75,0x72,0x73,0x6f,0x72,0x3a,0x70,0x6f,0x69,0x6e,0x74,0x65,0x72,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x32,0x70,0x74,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x33,0x35,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x7b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x33,0x30,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x34,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x72,0x65,0x6c,0x61,0x74,0x69,0x76,0x65,0x3b,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2e,0x64,0x69,0x73,0x61,0x62,0x6c,0x65,0x64,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x67,0x72,0x65,0x79,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x7b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x32,0x35,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x3b,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x32,0x35,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x3a,0x73,0x6f,0x6c,0x69,0x64,0x20,0x23,0x66,0x66,0x66,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x30,0x20,0x33,0x70,0x78,0x20,0x33,0x70,0x78,0x20,0x30,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x31,0x30,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x37,0x70,0x78,0x3b,0x74,0x6f,0x70,0x3a,0x33,0x70,0x78,0x3b,0x74,0x72,0x61,0x6e,0x73,0x66,0x6f,0x72,0x6d,0x3a,0x72,0x6f,0x74,0x61,0x74,0x65,0x28,0x34,0x35,0x64,0x65,0x67,0x29,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x35,0x70,0x78,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x2c,0x2e,0x6c,0x6f,0x67,0x76,0x69,0x65,0x77,0x65,0x72,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x67,0x72,0x61,0x79,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x73,0x74,0x79,0x6c,0x65,0x3a,0x73,0x6f,0x6c,0x69,0x64,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x7b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x32,0x30,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x39,0x70,0x78,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x72,0x65,0x6c,0x61,0x74,0x69,0x76,0x65,0x3b,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x30,0x25,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x32,0x36,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x3b,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x32,0x36,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x3a,0x23,0x66,0x66,0x66,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x30,0x25,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x38,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x38,0x70,0x78,0x3b,0x74,0x6f,0x70,0x3a,0x38,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x38,0x70,0x78,0x7d,0x2e,0x6c,0x6f,0x67,0x76,0x69,0x65,0x77,0x65,0x72,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x66,0x61,0x6d,0x69,0x6c,0x79,0x3a,0x27,0x4c,0x75,0x63,0x69,0x64,0x61,0x20,0x43,0x6f,0x6e,0x73,0x6f,0x6c,0x65,0x27,0x2c,0x4d,0x6f,0x6e,0x61,0x63,0x6f,0x2c,0x6d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x36,0x30,0x76,0x68,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x25,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x70,0x78,0x3b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x37,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x32,0x38,0x32,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x2d,0x31,0x32,0x35,0x70,0x78,0x3b,0x6d,0x69,0x6e,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x32,0x35,0x30,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x31,0x36,0x70,0x78,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x66,0x69,0x78,0x65,0x64,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x76,0x69,0x73,0x69,0x62,0x69,0x6c,0x69,0x74,0x79,0x3a,0x68,0x69,0x64,0x64,0x65,0x6e,0x3b,0x7a,0x2d,0x69,0x6e,0x64,0x65,0x78,0x3a,0x31,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x2e,0x73,0x68,0x6f,0x77,0x7b,0x61,0x6e,0x69,0x6d,0x61,0x74,0x69,0x6f,0x6e,0x3a,0x2e,0x35,0x73,0x20,0x66,0x61,0x64,0x65,0x69,0x6e,0x2c,0x2e,0x35,0x73,0x20,0x32,0x2e,0x35,0x73,0x20,0x66,0x61,0x64,0x65,0x6f,0x75,0x74,0x3b,0x76,0x69,0x73,0x69,0x62,0x69,0x6c,0x69,0x74,0x79,0x3a,0x76,0x69,0x73,0x69,0x62,0x6c,0x65,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x69,0x6e,0x70,0x75,0x74,0x7b,0x63,0x75,0x72,0x73,0x6f,0x72,0x3a,0x70,0x6f,0x69,0x6e,0x74,0x65,0x72,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x7d,0x40,0x6b,0x65,0x79,0x66,0x72,0x61,0x6d,0x65,0x73,0x20,0x66,0x61,0x64,0x65,0x69,0x6e,0x7b,0x66,0x72,0x6f,0x6d,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x32,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x7d,0x74,0x6f,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x2e,0x39,0x7d,0x7d,0x40,0x6b,0x65,0x79,0x66,0x72,0x61,0x6d,0x65,0x73,0x20,0x66,0x61,0x64,0x65,0x6f,0x75,0x74,0x7b,0x66,0x72,0x6f,0x6d,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x2e,0x39,0x7d,0x74,0x6f,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x30,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x7d,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x30,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x31,0x66,0x31,0x66,0x31,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x31,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x63,0x66,0x66,0x39,0x35,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x32,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x39,0x64,0x63,0x65,0x66,0x65,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x33,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x61,0x34,0x66,0x63,0x37,0x39,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x34,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x32,0x61,0x62,0x33,0x39,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x39,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x35,0x30,0x7d,0x2e,0x6c,0x6f,0x67,0x76,0x69,0x65,0x77,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x32,0x37,0x32,0x37,0x32,0x37,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x31,0x66,0x31,0x66,0x31,0x3b,0x6f,0x76,0x65,0x72,0x66,0x6c,0x6f,0x77,0x3a,0x61,0x75,0x74,0x6f,0x7d,0x74,0x62,0x6f,0x64,0x79,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x69,0x6e,0x6c,0x69,0x6e,0x65,0x2d,0x62,0x6c,0x6f,0x63,0x6b,0x3b,0x6f,0x76,0x65,0x72,0x66,0x6c,0x6f,0x77,0x3a,0x61,0x75,0x74,0x6f,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x68,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x68,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x68,0x7b,0x61,0x6c,0x69,0x67,0x6e,0x2d,0x63,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x73,0x69,0x6c,0x76,0x65,0x72,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x74,0x68,0x69,0x6e,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x36,0x70,0x78,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x76,0x77,0x7d,0x74,0x72,0x20,0x74,0x64,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x74,0x68,0x69,0x6e,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x73,0x70,0x61,0x63,0x69,0x6e,0x67,0x3a,0x30,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x73,0x74,0x79,0x6c,0x65,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x6d,0x69,0x6e,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x34,0x32,0x30,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x35,0x70,0x78,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x72,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x72,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x64,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x72,0x7b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x64,0x7b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x33,0x30,0x70,0x78,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x64,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x73,0x69,0x6c,0x76,0x65,0x72,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x33,0x30,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x64,0x7b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x33,0x30,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x3b,0x61,0x6c,0x69,0x67,0x6e,0x2d,0x63,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x3a,0x66,0x6c,0x65,0x78,0x2d,0x73,0x74,0x61,0x72,0x74,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x6c,0x65,0x66,0x74,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x72,0x3a,0x6e,0x74,0x68,0x2d,0x63,0x68,0x69,0x6c,0x64,0x28,0x34,0x6e,0x2b,0x33,0x29,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x72,0x3a,0x6e,0x74,0x68,0x2d,0x63,0x68,0x69,0x6c,0x64,0x28,0x34,0x6e,0x2b,0x34,0x29,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x72,0x3a,0x6e,0x74,0x68,0x2d,0x63,0x68,0x69,0x6c,0x64,0x28,0x6f,0x64,0x64,0x29,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x65,0x38,0x65,0x38,0x65,0x38,0x7d,0x2e,0x68,0x69,0x67,0x68,0x6c,0x69,0x67,0x68,0x74,0x20,0x74,0x64,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x64,0x62,0x66,0x66,0x30,0x30,0x37,0x35,0x7d,0x2e,0x61,0x70,0x68,0x65,0x61,0x64,0x65,0x72,0x2c,0x2e,0x68,0x65,0x61,0x64,0x65,0x72,0x6d,0x65,0x6e,0x75,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x38,0x66,0x38,0x66,0x38,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x38,0x70,0x78,0x20,0x31,0x32,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x76,0x77,0x7d,0x2e,0x6e,0x6f,0x74,0x65,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x74,0x79,0x6c,0x65,0x3a,0x69,0x74,0x61,0x6c,0x69,0x63,0x7d,0x2e,0x68,0x65,0x61,0x64,0x65,0x72,0x6d,0x65,0x6e,0x75,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x2e,0x31,0x70,0x78,0x20,0x73,0x6f,0x6c,0x69,0x64,0x20,0x23,0x34,0x34,0x34,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x31,0x30,0x30,0x70,0x78,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x66,0x69,0x78,0x65,0x64,0x3b,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x7a,0x2d,0x69,0x6e,0x64,0x65,0x78,0x3a,0x31,0x7d,0x2e,0x62,0x6f,0x64,0x79,0x6d,0x65,0x6e,0x75,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x31,0x30,0x30,0x76,0x68,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x2d,0x74,0x6f,0x70,0x3a,0x31,0x30,0x30,0x70,0x78,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x62,0x61,0x72,0x7b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x69,0x6e,0x68,0x65,0x72,0x69,0x74,0x3b,0x74,0x6f,0x70,0x3a,0x35,0x34,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x25,0x7d,0x61,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x61,0x6c,0x69,0x67,0x6e,0x2d,0x69,0x74,0x65,0x6d,0x73,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x66,0x6c,0x65,0x78,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x34,0x36,0x70,0x78,0x3b,0x6a,0x75,0x73,0x74,0x69,0x66,0x79,0x2d,0x63,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x3a,0x31,0x70,0x78,0x20,0x73,0x6f,0x6c,0x69,0x64,0x20,0x74,0x72,0x61,0x6e,0x73,0x70,0x61,0x72,0x65,0x6e,0x74,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x70,0x78,0x20,0x35,0x70,0x78,0x20,0x30,0x20,0x30,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x31,0x30,0x70,0x78,0x20,0x31,0x30,0x70,0x78,0x20,0x38,0x70,0x78,0x3b,0x77,0x68,0x69,0x74,0x65,0x2d,0x73,0x70,0x61,0x63,0x65,0x3a,0x6e,0x6f,0x77,0x72,0x61,0x70,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x2e,0x61,0x63,0x74,0x69,0x76,0x65,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x20,0x23,0x34,0x34,0x34,0x20,0x23,0x66,0x66,0x66,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x2e,0x31,0x70,0x78,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x30,0x30,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x3a,0x68,0x6f,0x76,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x3a,0x23,0x62,0x64,0x62,0x64,0x62,0x64,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x30,0x30,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x5f,0x62,0x75,0x74,0x74,0x6f,0x6e,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x6f,0x6e,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x32,0x39,0x62,0x33,0x34,0x7d,0x2e,0x6f,0x66,0x66,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x72,0x65,0x64,0x7d,0x2e,0x64,0x69,0x76,0x5f,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x32,0x39,0x62,0x33,0x34,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x34,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x32,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x31,0x70,0x78,0x20,0x31,0x30,0x70,0x78,0x7d,0x2e,0x61,0x6c,0x65,0x72,0x74,0x2c,0x2e,0x77,0x61,0x72,0x6e,0x69,0x6e,0x67,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x31,0x35,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x32,0x30,0x70,0x78,0x7d,0x2e,0x64,0x69,0x76,0x5f,0x62,0x72,0x7b,0x63,0x6c,0x65,0x61,0x72,0x3a,0x62,0x6f,0x74,0x68,0x7d,0x2e,0x61,0x6c,0x65,0x72,0x74,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x34,0x34,0x33,0x33,0x36,0x7d,0x2e,0x77,0x61,0x72,0x6e,0x69,0x6e,0x67,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x63,0x61,0x31,0x37,0x7d,0x2e,0x63,0x6c,0x6f,0x73,0x65,0x62,0x74,0x6e,0x7b,0x63,0x75,0x72,0x73,0x6f,0x72,0x3a,0x70,0x6f,0x69,0x6e,0x74,0x65,0x72,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x32,0x32,0x70,0x78,0x3b,0x6c,0x69,0x6e,0x65,0x2d,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x32,0x30,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x31,0x35,0x70,0x78,0x3b,0x74,0x72,0x61,0x6e,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x2e,0x33,0x73,0x7d,0x2e,0x63,0x6c,0x6f,0x73,0x65,0x62,0x74,0x6e,0x3a,0x68,0x6f,0x76,0x65,0x72,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x30,0x30,0x7d,0x73,0x65,0x63,0x74,0x69,0x6f,0x6e,0x7b,0x6f,0x76,0x65,0x72,0x66,0x6c,0x6f,0x77,0x2d,0x78,0x3a,0x61,0x75,0x74,0x6f,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x31,0x30,0x70,0x78,0x20,0x30,0x7d,0x66,0x6f,0x6f,0x74,0x65,0x72,0x7b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x30,0x20,0x35,0x70,0x78,0x3b,0x77,0x6f,0x72,0x64,0x2d,0x62,0x72,0x65,0x61,0x6b,0x3a,0x62,0x72,0x65,0x61,0x6b,0x2d,0x77,0x6f,0x72,0x64,0x7d,0x40,0x6d,0x65,0x64,0x69,0x61,0x20,0x73,0x63,0x72,0x65,0x65,0x6e,0x20,0x61,0x6e,0x64,0x20,0x28,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x37,0x38,0x30,0x70,0x78,0x29,0x7b,0x2e,0x73,0x68,0x6f,0x77,0x6d,0x65,0x6e,0x75,0x6c,0x61,0x62,0x65,0x6c,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x61,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x32,0x25,0x7d,0x7d,0x40,0x6d,0x65,0x64,0x69,0x61,0x20,0x73,0x63,0x72,0x65,0x65,0x6e,0x20,0x61,0x6e,0x64,0x20,0x28,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x34,0x35,0x30,0x70,0x78,0x29,0x7b,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x7b,0x6d,0x69,0x6e,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x33,0x30,0x30,0x70,0x78,0x7d,0x69,0x6e,0x70,0x75,0x74,0x2e,0x77,0x69,0x64,0x65,0x3a,0x66,0x6f,0x63,0x75,0x73,0x7b,0x6c,0x65,0x66,0x74,0x3a,0x34,0x70,0x78,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x3b,0x7a,0x2d,0x69,0x6e,0x64,0x65,0x78,0x3a,0x31,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x69,0x6e,0x6c,0x69,0x6e,0x65,0x3b,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x7d,0}; +const char DATA_ESPEASY_DEFAULT_MIN_CSS[] PROGMEM = {0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x2c,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x37,0x64,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x69,0x6e,0x70,0x75,0x74,0x3a,0x63,0x68,0x65,0x63,0x6b,0x65,0x64,0x7e,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x62,0x6c,0x6f,0x63,0x6b,0x7d,0x68,0x32,0x2c,0x68,0x33,0x7b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x36,0x70,0x78,0x7d,0x68,0x31,0x2c,0x68,0x36,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x37,0x64,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x3b,0x63,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x3a,0x27,0x27,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2c,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x74,0x65,0x78,0x74,0x2d,0x64,0x65,0x63,0x6f,0x72,0x61,0x74,0x69,0x6f,0x6e,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x64,0x69,0x76,0x5f,0x6c,0x2c,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x66,0x6c,0x6f,0x61,0x74,0x3a,0x6c,0x65,0x66,0x74,0x7d,0x2e,0x63,0x6c,0x6f,0x73,0x65,0x62,0x74,0x6e,0x2c,0x2e,0x64,0x69,0x76,0x5f,0x72,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x66,0x6c,0x6f,0x61,0x74,0x3a,0x72,0x69,0x67,0x68,0x74,0x7d,0x2a,0x7b,0x62,0x6f,0x78,0x2d,0x73,0x69,0x7a,0x69,0x6e,0x67,0x3a,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x62,0x6f,0x78,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x66,0x61,0x6d,0x69,0x6c,0x79,0x3a,0x73,0x61,0x6e,0x73,0x2d,0x73,0x65,0x72,0x69,0x66,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x32,0x70,0x74,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x30,0x7d,0x68,0x31,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x36,0x70,0x74,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x38,0x70,0x78,0x20,0x30,0x7d,0x68,0x32,0x2c,0x68,0x33,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x32,0x70,0x74,0x7d,0x68,0x32,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x30,0x20,0x2d,0x34,0x70,0x78,0x7d,0x68,0x33,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x63,0x61,0x63,0x61,0x63,0x61,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x2d,0x34,0x70,0x78,0x7d,0x68,0x36,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x30,0x70,0x74,0x7d,0x63,0x6f,0x64,0x65,0x2c,0x6b,0x62,0x64,0x2c,0x70,0x72,0x65,0x2c,0x73,0x61,0x6d,0x70,0x2c,0x74,0x74,0x2c,0x78,0x6d,0x70,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x66,0x61,0x6d,0x69,0x6c,0x79,0x3a,0x6d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x2c,0x6d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x65,0x6d,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x3a,0x30,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x70,0x78,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x34,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x20,0x31,0x36,0x70,0x78,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x2c,0x69,0x6e,0x70,0x75,0x74,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x65,0x65,0x65,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x68,0x65,0x6c,0x70,0x2c,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2c,0x69,0x6e,0x70,0x75,0x74,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x3a,0x31,0x70,0x78,0x20,0x73,0x6f,0x6c,0x69,0x64,0x20,0x67,0x72,0x61,0x79,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x6c,0x69,0x6e,0x6b,0x2e,0x77,0x69,0x64,0x65,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x69,0x6e,0x6c,0x69,0x6e,0x65,0x2d,0x62,0x6c,0x6f,0x63,0x6b,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x25,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x6c,0x69,0x6e,0x6b,0x2e,0x72,0x65,0x64,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x72,0x65,0x64,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x2e,0x68,0x65,0x6c,0x70,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x30,0x25,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x66,0x61,0x6d,0x69,0x6c,0x79,0x3a,0x6d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x32,0x70,0x78,0x20,0x37,0x70,0x78,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2c,0x69,0x6e,0x70,0x75,0x74,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x70,0x78,0x7d,0x2e,0x62,0x75,0x74,0x74,0x6f,0x6e,0x3a,0x68,0x6f,0x76,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x3a,0x23,0x33,0x36,0x39,0x7d,0x69,0x6e,0x70,0x75,0x74,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x20,0x38,0x70,0x78,0x7d,0x69,0x6e,0x70,0x75,0x74,0x2e,0x77,0x69,0x64,0x65,0x2c,0x73,0x65,0x6c,0x65,0x63,0x74,0x2e,0x77,0x69,0x64,0x65,0x7b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x35,0x70,0x78,0x3b,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x33,0x35,0x30,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x39,0x35,0x25,0x7d,0x2e,0x77,0x69,0x64,0x65,0x6e,0x75,0x6d,0x62,0x65,0x72,0x7b,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x35,0x30,0x30,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x7b,0x63,0x75,0x72,0x73,0x6f,0x72,0x3a,0x70,0x6f,0x69,0x6e,0x74,0x65,0x72,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x32,0x70,0x74,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x33,0x35,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x7b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x33,0x30,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x34,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x72,0x65,0x6c,0x61,0x74,0x69,0x76,0x65,0x3b,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x2e,0x64,0x69,0x73,0x61,0x62,0x6c,0x65,0x64,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x67,0x72,0x65,0x79,0x7d,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x7b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x32,0x35,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x3b,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x32,0x35,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x2e,0x63,0x68,0x65,0x63,0x6b,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x3a,0x73,0x6f,0x6c,0x69,0x64,0x20,0x23,0x66,0x66,0x66,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x30,0x20,0x33,0x70,0x78,0x20,0x33,0x70,0x78,0x20,0x30,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x31,0x30,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x37,0x70,0x78,0x3b,0x74,0x6f,0x70,0x3a,0x33,0x70,0x78,0x3b,0x74,0x72,0x61,0x6e,0x73,0x66,0x6f,0x72,0x6d,0x3a,0x72,0x6f,0x74,0x61,0x74,0x65,0x28,0x34,0x35,0x64,0x65,0x67,0x29,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x35,0x70,0x78,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x2c,0x2e,0x6c,0x6f,0x67,0x76,0x69,0x65,0x77,0x65,0x72,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x67,0x72,0x61,0x79,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x73,0x74,0x79,0x6c,0x65,0x3a,0x73,0x6f,0x6c,0x69,0x64,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x2c,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x7b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x32,0x30,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x39,0x70,0x78,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x72,0x65,0x6c,0x61,0x74,0x69,0x76,0x65,0x3b,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x30,0x25,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x32,0x36,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x3b,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x32,0x36,0x70,0x78,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x2e,0x64,0x6f,0x74,0x6d,0x61,0x72,0x6b,0x3a,0x61,0x66,0x74,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x3a,0x23,0x66,0x66,0x66,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x30,0x25,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x38,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x38,0x70,0x78,0x3b,0x74,0x6f,0x70,0x3a,0x38,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x38,0x70,0x78,0x7d,0x2e,0x6c,0x6f,0x67,0x76,0x69,0x65,0x77,0x65,0x72,0x2c,0x74,0x65,0x78,0x74,0x61,0x72,0x65,0x61,0x7b,0x66,0x6f,0x6e,0x74,0x2d,0x66,0x61,0x6d,0x69,0x6c,0x79,0x3a,0x27,0x4c,0x75,0x63,0x69,0x64,0x61,0x20,0x43,0x6f,0x6e,0x73,0x6f,0x6c,0x65,0x27,0x2c,0x4d,0x6f,0x6e,0x61,0x63,0x6f,0x2c,0x6d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x36,0x30,0x76,0x68,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x25,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x70,0x78,0x3b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x31,0x37,0x70,0x78,0x3b,0x6c,0x65,0x66,0x74,0x3a,0x32,0x38,0x32,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x2d,0x31,0x32,0x35,0x70,0x78,0x3b,0x6d,0x69,0x6e,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x32,0x35,0x30,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x31,0x36,0x70,0x78,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x66,0x69,0x78,0x65,0x64,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x76,0x69,0x73,0x69,0x62,0x69,0x6c,0x69,0x74,0x79,0x3a,0x68,0x69,0x64,0x64,0x65,0x6e,0x3b,0x7a,0x2d,0x69,0x6e,0x64,0x65,0x78,0x3a,0x31,0x7d,0x23,0x74,0x6f,0x61,0x73,0x74,0x6d,0x65,0x73,0x73,0x61,0x67,0x65,0x2e,0x73,0x68,0x6f,0x77,0x7b,0x61,0x6e,0x69,0x6d,0x61,0x74,0x69,0x6f,0x6e,0x3a,0x2e,0x35,0x73,0x20,0x66,0x61,0x64,0x65,0x69,0x6e,0x2c,0x2e,0x35,0x73,0x20,0x32,0x2e,0x35,0x73,0x20,0x66,0x61,0x64,0x65,0x6f,0x75,0x74,0x3b,0x76,0x69,0x73,0x69,0x62,0x69,0x6c,0x69,0x74,0x79,0x3a,0x76,0x69,0x73,0x69,0x62,0x6c,0x65,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x20,0x69,0x6e,0x70,0x75,0x74,0x2c,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x32,0x20,0x69,0x6e,0x70,0x75,0x74,0x7b,0x63,0x75,0x72,0x73,0x6f,0x72,0x3a,0x70,0x6f,0x69,0x6e,0x74,0x65,0x72,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x7d,0x40,0x6b,0x65,0x79,0x66,0x72,0x61,0x6d,0x65,0x73,0x20,0x66,0x61,0x64,0x65,0x69,0x6e,0x7b,0x66,0x72,0x6f,0x6d,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x32,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x7d,0x74,0x6f,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x2e,0x39,0x7d,0x7d,0x40,0x6b,0x65,0x79,0x66,0x72,0x61,0x6d,0x65,0x73,0x20,0x66,0x61,0x64,0x65,0x6f,0x75,0x74,0x7b,0x66,0x72,0x6f,0x6d,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x33,0x30,0x25,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x2e,0x39,0x7d,0x74,0x6f,0x7b,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x30,0x3b,0x6f,0x70,0x61,0x63,0x69,0x74,0x79,0x3a,0x30,0x7d,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x30,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x31,0x66,0x31,0x66,0x31,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x31,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x63,0x66,0x66,0x39,0x35,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x32,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x39,0x64,0x63,0x65,0x66,0x65,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x33,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x61,0x34,0x66,0x63,0x37,0x39,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x34,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x32,0x61,0x62,0x33,0x39,0x7d,0x2e,0x6c,0x65,0x76,0x65,0x6c,0x5f,0x39,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x35,0x30,0x7d,0x2e,0x6c,0x6f,0x67,0x76,0x69,0x65,0x77,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x32,0x37,0x32,0x37,0x32,0x37,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x31,0x66,0x31,0x66,0x31,0x3b,0x6f,0x76,0x65,0x72,0x66,0x6c,0x6f,0x77,0x3a,0x61,0x75,0x74,0x6f,0x7d,0x74,0x62,0x6f,0x64,0x79,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x69,0x6e,0x6c,0x69,0x6e,0x65,0x2d,0x62,0x6c,0x6f,0x63,0x6b,0x3b,0x6f,0x76,0x65,0x72,0x66,0x6c,0x6f,0x77,0x3a,0x61,0x75,0x74,0x6f,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x68,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x68,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x68,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x73,0x69,0x6c,0x76,0x65,0x72,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x74,0x68,0x69,0x6e,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x36,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x76,0x77,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x68,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x68,0x7b,0x61,0x6c,0x69,0x67,0x6e,0x2d,0x63,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x68,0x7b,0x61,0x6c,0x69,0x67,0x6e,0x2d,0x63,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x3a,0x66,0x6c,0x65,0x78,0x2d,0x73,0x74,0x61,0x72,0x74,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x6c,0x65,0x66,0x74,0x7d,0x74,0x72,0x20,0x74,0x64,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x74,0x68,0x69,0x6e,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x73,0x70,0x61,0x63,0x69,0x6e,0x67,0x3a,0x30,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x73,0x74,0x79,0x6c,0x65,0x3a,0x6e,0x6f,0x6e,0x65,0x3b,0x6d,0x69,0x6e,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x34,0x32,0x30,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x35,0x70,0x78,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x72,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x72,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x64,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x72,0x7b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x20,0x74,0x64,0x7b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x33,0x30,0x70,0x78,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x64,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x73,0x69,0x6c,0x76,0x65,0x72,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x33,0x30,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x64,0x7b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x33,0x30,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x34,0x70,0x78,0x3b,0x61,0x6c,0x69,0x67,0x6e,0x2d,0x63,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x3a,0x66,0x6c,0x65,0x78,0x2d,0x73,0x74,0x61,0x72,0x74,0x3b,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,0x6e,0x3a,0x6c,0x65,0x66,0x74,0x7d,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x72,0x3a,0x6e,0x74,0x68,0x2d,0x63,0x68,0x69,0x6c,0x64,0x28,0x34,0x6e,0x2b,0x33,0x29,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x32,0x72,0x6f,0x77,0x20,0x74,0x72,0x3a,0x6e,0x74,0x68,0x2d,0x63,0x68,0x69,0x6c,0x64,0x28,0x34,0x6e,0x2b,0x34,0x29,0x2c,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6d,0x75,0x6c,0x74,0x69,0x72,0x6f,0x77,0x20,0x74,0x72,0x3a,0x6e,0x74,0x68,0x2d,0x63,0x68,0x69,0x6c,0x64,0x28,0x6f,0x64,0x64,0x29,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x65,0x38,0x65,0x38,0x65,0x38,0x7d,0x2e,0x68,0x69,0x67,0x68,0x6c,0x69,0x67,0x68,0x74,0x20,0x74,0x64,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x64,0x62,0x66,0x66,0x30,0x30,0x37,0x35,0x7d,0x2e,0x61,0x70,0x68,0x65,0x61,0x64,0x65,0x72,0x2c,0x2e,0x68,0x65,0x61,0x64,0x65,0x72,0x6d,0x65,0x6e,0x75,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x38,0x66,0x38,0x66,0x38,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x38,0x70,0x78,0x20,0x31,0x32,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x76,0x77,0x7d,0x2e,0x6e,0x6f,0x74,0x65,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x74,0x79,0x6c,0x65,0x3a,0x69,0x74,0x61,0x6c,0x69,0x63,0x7d,0x2e,0x68,0x65,0x61,0x64,0x65,0x72,0x6d,0x65,0x6e,0x75,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x2e,0x31,0x70,0x78,0x20,0x73,0x6f,0x6c,0x69,0x64,0x20,0x23,0x34,0x34,0x34,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x31,0x30,0x30,0x70,0x78,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x66,0x69,0x78,0x65,0x64,0x3b,0x74,0x6f,0x70,0x3a,0x30,0x3b,0x7a,0x2d,0x69,0x6e,0x64,0x65,0x78,0x3a,0x31,0x7d,0x2e,0x62,0x6f,0x64,0x79,0x6d,0x65,0x6e,0x75,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x31,0x30,0x30,0x76,0x68,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x2d,0x74,0x6f,0x70,0x3a,0x31,0x30,0x30,0x70,0x78,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x62,0x61,0x72,0x7b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x69,0x6e,0x68,0x65,0x72,0x69,0x74,0x3b,0x74,0x6f,0x70,0x3a,0x35,0x34,0x70,0x78,0x3b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x30,0x30,0x25,0x7d,0x61,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x61,0x6c,0x69,0x67,0x6e,0x2d,0x69,0x74,0x65,0x6d,0x73,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x66,0x6c,0x65,0x78,0x3b,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x34,0x36,0x70,0x78,0x3b,0x6a,0x75,0x73,0x74,0x69,0x66,0x79,0x2d,0x63,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x3a,0x63,0x65,0x6e,0x74,0x65,0x72,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x62,0x6f,0x72,0x64,0x65,0x72,0x3a,0x31,0x70,0x78,0x20,0x73,0x6f,0x6c,0x69,0x64,0x20,0x74,0x72,0x61,0x6e,0x73,0x70,0x61,0x72,0x65,0x6e,0x74,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x35,0x70,0x78,0x20,0x35,0x70,0x78,0x20,0x30,0x20,0x30,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x31,0x30,0x70,0x78,0x20,0x31,0x30,0x70,0x78,0x20,0x38,0x70,0x78,0x3b,0x77,0x68,0x69,0x74,0x65,0x2d,0x73,0x70,0x61,0x63,0x65,0x3a,0x6e,0x6f,0x77,0x72,0x61,0x70,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x2e,0x61,0x63,0x74,0x69,0x76,0x65,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x34,0x34,0x34,0x20,0x23,0x34,0x34,0x34,0x20,0x23,0x66,0x66,0x66,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x2e,0x31,0x70,0x78,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x30,0x30,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x3a,0x68,0x6f,0x76,0x65,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x3a,0x23,0x62,0x64,0x62,0x64,0x62,0x64,0x3b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x30,0x30,0x7d,0x2e,0x6d,0x65,0x6e,0x75,0x5f,0x62,0x75,0x74,0x74,0x6f,0x6e,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x2e,0x6f,0x6e,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x32,0x39,0x62,0x33,0x34,0x7d,0x2e,0x6f,0x66,0x66,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x72,0x65,0x64,0x7d,0x2e,0x64,0x69,0x76,0x5f,0x72,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x32,0x39,0x62,0x33,0x34,0x3b,0x62,0x6f,0x72,0x64,0x65,0x72,0x2d,0x72,0x61,0x64,0x69,0x75,0x73,0x3a,0x34,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x3a,0x32,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x31,0x70,0x78,0x20,0x31,0x30,0x70,0x78,0x7d,0x2e,0x61,0x6c,0x65,0x72,0x74,0x2c,0x2e,0x77,0x61,0x72,0x6e,0x69,0x6e,0x67,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x66,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x62,0x6f,0x74,0x74,0x6f,0x6d,0x3a,0x31,0x35,0x70,0x78,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x32,0x30,0x70,0x78,0x7d,0x2e,0x64,0x69,0x76,0x5f,0x62,0x72,0x7b,0x63,0x6c,0x65,0x61,0x72,0x3a,0x62,0x6f,0x74,0x68,0x7d,0x2e,0x61,0x6c,0x65,0x72,0x74,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x34,0x34,0x33,0x33,0x36,0x7d,0x2e,0x77,0x61,0x72,0x6e,0x69,0x6e,0x67,0x7b,0x62,0x61,0x63,0x6b,0x67,0x72,0x6f,0x75,0x6e,0x64,0x2d,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x66,0x66,0x63,0x61,0x31,0x37,0x7d,0x2e,0x63,0x6c,0x6f,0x73,0x65,0x62,0x74,0x6e,0x7b,0x63,0x75,0x72,0x73,0x6f,0x72,0x3a,0x70,0x6f,0x69,0x6e,0x74,0x65,0x72,0x3b,0x66,0x6f,0x6e,0x74,0x2d,0x73,0x69,0x7a,0x65,0x3a,0x32,0x32,0x70,0x78,0x3b,0x6c,0x69,0x6e,0x65,0x2d,0x68,0x65,0x69,0x67,0x68,0x74,0x3a,0x32,0x30,0x70,0x78,0x3b,0x6d,0x61,0x72,0x67,0x69,0x6e,0x2d,0x6c,0x65,0x66,0x74,0x3a,0x31,0x35,0x70,0x78,0x3b,0x74,0x72,0x61,0x6e,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x2e,0x33,0x73,0x7d,0x2e,0x63,0x6c,0x6f,0x73,0x65,0x62,0x74,0x6e,0x3a,0x68,0x6f,0x76,0x65,0x72,0x7b,0x63,0x6f,0x6c,0x6f,0x72,0x3a,0x23,0x30,0x30,0x30,0x7d,0x73,0x65,0x63,0x74,0x69,0x6f,0x6e,0x7b,0x6f,0x76,0x65,0x72,0x66,0x6c,0x6f,0x77,0x2d,0x78,0x3a,0x61,0x75,0x74,0x6f,0x3b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x31,0x30,0x70,0x78,0x20,0x30,0x7d,0x66,0x6f,0x6f,0x74,0x65,0x72,0x7b,0x70,0x61,0x64,0x64,0x69,0x6e,0x67,0x3a,0x30,0x20,0x35,0x70,0x78,0x3b,0x77,0x6f,0x72,0x64,0x2d,0x62,0x72,0x65,0x61,0x6b,0x3a,0x62,0x72,0x65,0x61,0x6b,0x2d,0x77,0x6f,0x72,0x64,0x7d,0x40,0x6d,0x65,0x64,0x69,0x61,0x20,0x73,0x63,0x72,0x65,0x65,0x6e,0x20,0x61,0x6e,0x64,0x20,0x28,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x37,0x38,0x30,0x70,0x78,0x29,0x7b,0x2e,0x73,0x68,0x6f,0x77,0x6d,0x65,0x6e,0x75,0x6c,0x61,0x62,0x65,0x6c,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x61,0x2e,0x6d,0x65,0x6e,0x75,0x7b,0x77,0x69,0x64,0x74,0x68,0x3a,0x31,0x32,0x25,0x7d,0x7d,0x40,0x6d,0x65,0x64,0x69,0x61,0x20,0x73,0x63,0x72,0x65,0x65,0x6e,0x20,0x61,0x6e,0x64,0x20,0x28,0x6d,0x61,0x78,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x34,0x35,0x30,0x70,0x78,0x29,0x7b,0x74,0x61,0x62,0x6c,0x65,0x2e,0x6e,0x6f,0x72,0x6d,0x61,0x6c,0x7b,0x6d,0x69,0x6e,0x2d,0x77,0x69,0x64,0x74,0x68,0x3a,0x33,0x30,0x30,0x70,0x78,0x7d,0x69,0x6e,0x70,0x75,0x74,0x2e,0x77,0x69,0x64,0x65,0x3a,0x66,0x6f,0x63,0x75,0x73,0x7b,0x6c,0x65,0x66,0x74,0x3a,0x34,0x70,0x78,0x3b,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3a,0x61,0x62,0x73,0x6f,0x6c,0x75,0x74,0x65,0x3b,0x7a,0x2d,0x69,0x6e,0x64,0x65,0x78,0x3a,0x31,0x7d,0x2e,0x63,0x6f,0x6e,0x74,0x61,0x69,0x6e,0x65,0x72,0x7b,0x64,0x69,0x73,0x70,0x6c,0x61,0x79,0x3a,0x69,0x6e,0x6c,0x69,0x6e,0x65,0x3b,0x75,0x73,0x65,0x72,0x2d,0x73,0x65,0x6c,0x65,0x63,0x74,0x3a,0x6e,0x6f,0x6e,0x65,0x7d,0x7d,0}; #endif // WEBSERVER_CSS // JavaScript blobs diff --git a/static/espeasy_default.css b/static/espeasy_default.css index f1db8065bf..05daa6f18f 100644 --- a/static/espeasy_default.css +++ b/static/espeasy_default.css @@ -241,15 +241,22 @@ tbody { table.multirow th, table.multi2row th, table.normal th { - align-content:center; background-color:#444; border-color:silver; border-width:thin; color:#FFF; padding:6px; - text-align:center; width:100vw } +table.multirow th, +table.normal th { + align-content:center; + text-align:center; +} +table.multi2row th{ + align-content:flex-start; + text-align:left +} tr td { border-width:thin } @@ -274,10 +281,10 @@ table.multirow td { text-align:center } table.multi2row td { - height: 30px; - padding: 4px; - align-content: flex-start; - text-align: left + height:30px; + padding:4px; + align-content:flex-start; + text-align:left } table.multirow tr:nth-child(odd), diff --git a/static/espeasy_default.min.css b/static/espeasy_default.min.css index 7a4b18de1e..e92a2366fb 100644 --- a/static/espeasy_default.min.css +++ b/static/espeasy_default.min.css @@ -1 +1 @@ -#toastmessage,.button,.container input:checked~.checkmark,.container2 input:checked~.dotmark{background-color:#07d}.container,.container input:checked~.checkmark:after,.container2,.container2 input:checked~.dotmark:after{display:block}h2,h3{padding:6px}h1,h6{color:#07d}.checkmark:after,.dotmark:after{display:none;position:absolute;content:''}.button,.menu{text-decoration:none}.div_l,.menu{float:left}.closebtn,.div_r{color:#fff;float:right}*{box-sizing:border-box;font-family:sans-serif;font-size:12pt;margin:0}h1{font-size:16pt;margin:8px 0}h2,h3{font-size:12pt}h2{background-color:#444;color:#fff;margin:0 -4px}h3{background-color:#cacaca;color:#444;margin:-4px}h6{font-size:10pt}code,kbd,pre,samp,tt,xmp{font-family:monospace,monospace;font-size:1em}.button{border:0;border-radius:5px;color:#fff;margin:4px;padding:4px 16px}.checkmark,.dotmark,input,select,textarea{background-color:#eee}.button.help,.checkmark,input,select,textarea{border:1px solid gray}.button.link.wide{display:inline-block;text-align:center;width:100%}.button.link.red{background-color:red}.button.help{border-radius:50%;font-family:monospace;padding:2px 7px}.checkmark,input,select,textarea{border-radius:5px}.button:hover{background:#369}input,select,textarea{padding:4px 8px}input.wide,select.wide{margin-bottom:5px;max-width:350px;width:95%}.widenumber{max-width:500px;width:100px}.container,.container2{cursor:pointer;font-size:12pt;padding-left:35px}.container{height:30px;margin-left:4px;margin-top:0;position:relative;user-select:none}.checkmark.disabled{background-color:grey}.checkmark{height:25px;left:0;position:absolute;top:0;width:25px}.container .checkmark:after{border:solid #fff;border-width:0 3px 3px 0;height:10px;left:7px;top:3px;transform:rotate(45deg);width:5px}#toastmessage,.dotmark,.logviewer{border-color:gray;border-style:solid}#toastmessage,.dotmark{border-width:1px}.container2{margin-bottom:20px;margin-left:9px;position:relative;user-select:none}.dotmark{border-radius:50%;height:26px;left:0;position:absolute;top:0;width:26px}.container2 .dotmark:after{background:#fff;border-radius:50%;height:8px;left:8px;top:8px;width:8px}.logviewer,textarea{font-family:'Lucida Console',Monaco,monospace;height:60vh;width:100%}#toastmessage{border-radius:5px;bottom:30%;color:#fff;font-size:17px;left:282px;margin-left:-125px;min-width:250px;padding:16px;position:fixed;text-align:center;visibility:hidden;z-index:1}#toastmessage.show{animation:.5s fadein,.5s 2.5s fadeout;visibility:visible}.container input,.container2 input{cursor:pointer;opacity:0;position:absolute}@keyframes fadein{from{bottom:20%;opacity:0}to{bottom:30%;opacity:.9}}@keyframes fadeout{from{bottom:30%;opacity:.9}to{bottom:0;opacity:0}}.level_0{color:#f1f1f1}.level_1{color:#fcff95}.level_2{color:#9dcefe}.level_3{color:#a4fc79}.level_4{color:#f2ab39}.level_9{color:#f50}.logviewer{background-color:#272727;color:#f1f1f1;overflow:auto}tbody{display:inline-block;overflow:auto}table.multi2row th,table.multirow th,table.normal th{align-content:center;background-color:#444;border-color:silver;border-width:thin;color:#fff;padding:6px;text-align:center;width:100vw}tr td{border-width:thin}table.multi2row,table.multirow,table.normal{border-spacing:0;border-style:none;min-width:420px;padding-bottom:5px}table.multi2row tr,table.multirow tr,table.normal td,table.normal tr{padding:4px}table.normal td{height:30px}table.multirow td{border-color:silver;height:30px;padding:4px;text-align:center}table.multi2row td{height:30px;padding:4px;align-content:flex-start;text-align:left}table.multi2row tr:nth-child(4n+3),table.multi2row tr:nth-child(4n+4),table.multirow tr:nth-child(odd){background-color:#e8e8e8}.highlight td{background-color:#dbff0075}.apheader,.headermenu{background-color:#f8f8f8;padding:8px 12px;width:100vw}.note{color:#444;font-style:italic}.headermenu{border-bottom:.1px solid #444;height:100px;position:fixed;top:0;z-index:1}.bodymenu{background-color:#fff;height:100vh;padding-top:100px}.menubar{position:inherit;top:54px;width:100%}a.menu{align-items:center;display:flex;height:46px;justify-content:center}.menu{border:1px solid transparent;border-radius:5px 5px 0 0;color:#444;padding:10px 10px 8px;white-space:nowrap}.menu.active{background-color:#fff;border-color:#444 #444 #fff;border-width:.1px;color:#000}.menu:hover{background:#bdbdbd;color:#000}.menu_button{display:none}.on{color:#029b34}.off{color:red}.div_r{background-color:#029b34;border-radius:4px;margin:2px;padding:1px 10px}.alert,.warning{color:#fff;margin-bottom:15px;padding:20px}.div_br{clear:both}.alert{background-color:#f44336}.warning{background-color:#ffca17}.closebtn{cursor:pointer;font-size:22px;line-height:20px;margin-left:15px;transition:.3s}.closebtn:hover{color:#000}section{overflow-x:auto;padding:10px 0}footer{padding:0 5px;word-break:break-word}@media screen and (max-width:780px){.showmenulabel{display:none}a.menu{width:12%}}@media screen and (max-width:450px){table.normal{min-width:300px}input.wide:focus{left:4px;position:absolute;z-index:1}.container{display:inline;user-select:none}} \ No newline at end of file +#toastmessage,.button,.container input:checked~.checkmark,.container2 input:checked~.dotmark{background-color:#07d}.container,.container input:checked~.checkmark:after,.container2,.container2 input:checked~.dotmark:after{display:block}h2,h3{padding:6px}h1,h6{color:#07d}.checkmark:after,.dotmark:after{display:none;position:absolute;content:''}.button,.menu{text-decoration:none}.div_l,.menu{float:left}.closebtn,.div_r{color:#fff;float:right}*{box-sizing:border-box;font-family:sans-serif;font-size:12pt;margin:0}h1{font-size:16pt;margin:8px 0}h2,h3{font-size:12pt}h2{background-color:#444;color:#fff;margin:0 -4px}h3{background-color:#cacaca;color:#444;margin:-4px}h6{font-size:10pt}code,kbd,pre,samp,tt,xmp{font-family:monospace,monospace;font-size:1em}.button{border:0;border-radius:5px;color:#fff;margin:4px;padding:4px 16px}.checkmark,.dotmark,input,select,textarea{background-color:#eee}.button.help,.checkmark,input,select,textarea{border:1px solid gray}.button.link.wide{display:inline-block;text-align:center;width:100%}.button.link.red{background-color:red}.button.help{border-radius:50%;font-family:monospace;padding:2px 7px}.checkmark,input,select,textarea{border-radius:5px}.button:hover{background:#369}input,select,textarea{padding:4px 8px}input.wide,select.wide{margin-bottom:5px;max-width:350px;width:95%}.widenumber{max-width:500px;width:100px}.container,.container2{cursor:pointer;font-size:12pt;padding-left:35px}.container{height:30px;margin-left:4px;margin-top:0;position:relative;user-select:none}.checkmark.disabled{background-color:grey}.checkmark{height:25px;left:0;position:absolute;top:0;width:25px}.container .checkmark:after{border:solid #fff;border-width:0 3px 3px 0;height:10px;left:7px;top:3px;transform:rotate(45deg);width:5px}#toastmessage,.dotmark,.logviewer{border-color:gray;border-style:solid}#toastmessage,.dotmark{border-width:1px}.container2{margin-bottom:20px;margin-left:9px;position:relative;user-select:none}.dotmark{border-radius:50%;height:26px;left:0;position:absolute;top:0;width:26px}.container2 .dotmark:after{background:#fff;border-radius:50%;height:8px;left:8px;top:8px;width:8px}.logviewer,textarea{font-family:'Lucida Console',Monaco,monospace;height:60vh;width:100%}#toastmessage{border-radius:5px;bottom:30%;color:#fff;font-size:17px;left:282px;margin-left:-125px;min-width:250px;padding:16px;position:fixed;text-align:center;visibility:hidden;z-index:1}#toastmessage.show{animation:.5s fadein,.5s 2.5s fadeout;visibility:visible}.container input,.container2 input{cursor:pointer;opacity:0;position:absolute}@keyframes fadein{from{bottom:20%;opacity:0}to{bottom:30%;opacity:.9}}@keyframes fadeout{from{bottom:30%;opacity:.9}to{bottom:0;opacity:0}}.level_0{color:#f1f1f1}.level_1{color:#fcff95}.level_2{color:#9dcefe}.level_3{color:#a4fc79}.level_4{color:#f2ab39}.level_9{color:#f50}.logviewer{background-color:#272727;color:#f1f1f1;overflow:auto}tbody{display:inline-block;overflow:auto}table.multi2row th,table.multirow th,table.normal th{background-color:#444;border-color:silver;border-width:thin;color:#fff;padding:6px;width:100vw}table.multirow th,table.normal th{align-content:center;text-align:center}table.multi2row th{align-content:flex-start;text-align:left}tr td{border-width:thin}table.multi2row,table.multirow,table.normal{border-spacing:0;border-style:none;min-width:420px;padding-bottom:5px}table.multi2row tr,table.multirow tr,table.normal td,table.normal tr{padding:4px}table.normal td{height:30px}table.multirow td{border-color:silver;height:30px;padding:4px;text-align:center}table.multi2row td{height:30px;padding:4px;align-content:flex-start;text-align:left}table.multi2row tr:nth-child(4n+3),table.multi2row tr:nth-child(4n+4),table.multirow tr:nth-child(odd){background-color:#e8e8e8}.highlight td{background-color:#dbff0075}.apheader,.headermenu{background-color:#f8f8f8;padding:8px 12px;width:100vw}.note{color:#444;font-style:italic}.headermenu{border-bottom:.1px solid #444;height:100px;position:fixed;top:0;z-index:1}.bodymenu{background-color:#fff;height:100vh;padding-top:100px}.menubar{position:inherit;top:54px;width:100%}a.menu{align-items:center;display:flex;height:46px;justify-content:center}.menu{border:1px solid transparent;border-radius:5px 5px 0 0;color:#444;padding:10px 10px 8px;white-space:nowrap}.menu.active{background-color:#fff;border-color:#444 #444 #fff;border-width:.1px;color:#000}.menu:hover{background:#bdbdbd;color:#000}.menu_button{display:none}.on{color:#029b34}.off{color:red}.div_r{background-color:#029b34;border-radius:4px;margin:2px;padding:1px 10px}.alert,.warning{color:#fff;margin-bottom:15px;padding:20px}.div_br{clear:both}.alert{background-color:#f44336}.warning{background-color:#ffca17}.closebtn{cursor:pointer;font-size:22px;line-height:20px;margin-left:15px;transition:.3s}.closebtn:hover{color:#000}section{overflow-x:auto;padding:10px 0}footer{padding:0 5px;word-break:break-word}@media screen and (max-width:780px){.showmenulabel{display:none}a.menu{width:12%}}@media screen and (max-width:450px){table.normal{min-width:300px}input.wide:focus{left:4px;position:absolute;z-index:1}.container{display:inline;user-select:none}} \ No newline at end of file From cce15a98e12b986e00a40494d9e8a2a67b4ec45a Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 29 Aug 2022 23:23:54 +0200 Subject: [PATCH 048/113] [TouchHandler] Settings page layout adjusted, and some minor improvements --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 50 ++++++++++++------------ 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 9baf75916e..1b5af8e72c 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -544,15 +544,14 @@ bool ESPEasy_TouchHandler::setTouchObjectValue(struct EventStruct *event, if (touchObject.isEmpty()) { return false; } bool success = false; - int8_t objectNr = getTouchObjectIndex(event, touchObject, false); + int8_t objectNr = getTouchObjectIndex(event, touchObject, false); + int16_t _value = value; if ((objectNr > -1) && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED)) { success = true; // Always success if matched object - if (value != TouchObjects[objectNr].TouchStates) { - int16_t _value = value; - + if (_value != TouchObjects[objectNr].TouchStates) { if (bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_SLIDER)) { int16_t lowRange = 0; int16_t highRange = 100; @@ -591,7 +590,7 @@ bool ESPEasy_TouchHandler::setTouchObjectValue(struct EventStruct *event, log += '/'; log += objectNr; log += F(", new value: "); - log += value; + log += _value; addLogMove(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG @@ -976,7 +975,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { addFormCheckBox(F("Enable logging for calibration"), F("log_calibration"), Touch_Settings.logEnabled); - addFormSubHeader(F("Touch objects")); + addFormSubHeader(F("Object settings")); # if TOUCH_FEATURE_EXTENDED_TOUCH @@ -1067,14 +1066,28 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH { - addRowLabel(F("Object")); + addFormNumericBox(F("Debounce delay for On/Off buttons"), F("debounce"), + Touch_Settings.debounceMs, 0, 255); + addUnit(F("0..255 msec.")); + + # if TOUCH_FEATURE_SWIPE + addFormNumericBox(F("Minimal swipe movement"), F("swipemin"), + Touch_Settings.swipeMinimal, 1, 25); + addUnit(F("1..25px")); + + addFormNumericBox(F("Maximum swipe margin"), F("swipemax"), + Touch_Settings.swipeMargin, 5, 250); + addUnit(F("5..250px")); + # endif // if TOUCH_FEATURE_SWIPE + } + { + addFormSubHeader(F("Touch objects")); { - # if TOUCH_FEATURE_EXTENDED_TOUCH + # if !TOUCH_FEATURE_EXTENDED_TOUCH + addRowLabel(F("Object")); + # endif // if !TOUCH_FEATURE_EXTENDED_TOUCH html_table(F("multi2row"), false); // Sub-table with alternating highlight per 2 rows - # else // if TOUCH_FEATURE_EXTENDED_TOUCH - html_table(EMPTY_STRING, false); // Sub-table - # endif // if TOUCH_FEATURE_EXTENDED_TOUCH html_table_header(F(" # ")); html_table_header(F("On")); html_table_header(F("Objectname")); @@ -1415,21 +1428,6 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { } html_end_table(); } - { - addFormNumericBox(F("Debounce delay for On/Off buttons"), F("debounce"), - Touch_Settings.debounceMs, 0, 255); - addUnit(F("0..255 msec.")); - - # if TOUCH_FEATURE_SWIPE - addFormNumericBox(F("Minimal swipe movement"), F("swipemin"), - Touch_Settings.swipeMinimal, 1, 25); - addUnit(F("1..25px")); - - addFormNumericBox(F("Maximum swipe margin"), F("swipemax"), - Touch_Settings.swipeMargin, 5, 250); - addUnit(F("5..250px")); - # endif // if TOUCH_FEATURE_SWIPE - } return false; } From a289e0bf3f4b565bfd35e84b2dcfa086c8d4bc41 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 30 Aug 2022 20:37:30 +0200 Subject: [PATCH 049/113] [TouchHandler] Logging check improvements --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 4 ++-- src/src/Helpers/ESPEasy_TouchHandler.h | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 1b5af8e72c..199ac29cd0 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -311,7 +311,7 @@ bool ESPEasy_TouchHandler::isValidAndTouchedTouchObject(const int16_t& x, lastObjectArea = TouchObjects[objectNr].SurfaceAreas; selected = true; } - # if defined(TOUCH_DEBUG) && !defined(BUILD_NO_DEBUG) + # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log = F("TOUCH DEBUG Touched: obj: "); @@ -338,7 +338,7 @@ bool ESPEasy_TouchHandler::isValidAndTouchedTouchObject(const int16_t& x, log += selected ? 'T' : 'f'; addLogMove(LOG_LEVEL_DEBUG, log); } - # endif // if defined(TOUCH_DEBUG) && !defined(BUILD_NO_DEBUG) + # endif // ifdef TOUCH_DEBUG } } return selected; diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index e6de390c68..56bc1ea97d 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -80,9 +80,6 @@ # undef TOUCH_FEATURE_TOOLTIPS # define TOUCH_FEATURE_TOOLTIPS 0 # endif // if TOUCH_FEATURE_TOOLTIPS -# ifdef TOUCH_DEBUG -# undef TOUCH_DEBUG -# endif // ifdef TOUCH_DEBUG # if TOUCH_FEATURE_EXTENDED_TOUCH # undef TOUCH_FEATURE_EXTENDED_TOUCH # define TOUCH_FEATURE_EXTENDED_TOUCH 0 @@ -92,6 +89,11 @@ // # define TOUCH_FEATURE_SWIPE 0 // # endif // if TOUCH_FEATURE_SWIPE # endif // ifdef LIMIT_BUILD_SIZE +# ifdef BUILD_NO_DEBUG +# ifdef TOUCH_DEBUG +# undef TOUCH_DEBUG +# endif // ifdef TOUCH_DEBUG +# endif // ifdef BUILD_NO_DEBUG # if TOUCH_FEATURE_TOOLTIPS && !FEATURE_TOOLTIPS # undef TOUCH_FEATURE_TOOLTIPS From 92e7ed066da461a99c382698cb100f5cc6cfb5b5 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 30 Aug 2022 20:37:55 +0200 Subject: [PATCH 050/113] [P123] Logging check improvements --- src/src/PluginStructs/P123_data_struct.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/src/PluginStructs/P123_data_struct.h b/src/src/PluginStructs/P123_data_struct.h index 855e15e99a..31e316cba5 100644 --- a/src/src/PluginStructs/P123_data_struct.h +++ b/src/src/PluginStructs/P123_data_struct.h @@ -19,6 +19,10 @@ # endif // ifndef P123_LIMIT_BUILD_SIZE # endif // ifndef LIMIT_BUILD_SIZE +# if defined(BUILD_NO_DEBUG) && defined(PLUGIN_123_DEBUG) +# undef PLUGIN_123_DEBUG +# endif // if defined(BUILD_NO_DEBUG) && defined(PLUGIN_123_DEBUG) + # define P123_CONFIG_DISPLAY_TASK PCONFIG(0) # define P123_COLOR_DEPTH PCONFIG_LONG(1) From 1305728cee03b3727f54ad0c06c8e772bb058e0f Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 14 Sep 2022 22:12:58 +0200 Subject: [PATCH 051/113] [TouchHandler] Remove unneeded enum MAX values --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 20 ++++++++++---------- src/src/Helpers/ESPEasy_TouchHandler.h | 3 +-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 199ac29cd0..0be65b8b4a 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -14,7 +14,6 @@ const __FlashStringHelper* toString(Touch_action_e action) { case Touch_action_e::DecrementGroup: return F("Previous Group"); case Touch_action_e::IncrementPage: return F("Next Page (+10)"); case Touch_action_e::DecrementPage: return F("Previous Page (-10)"); - case Touch_action_e::TouchAction_MAX: break; } return F("Unsupported!"); } @@ -984,7 +983,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { { String parsed; addRowLabel(F("Default On/Off button colors")); - html_table(EMPTY_STRING, false); // Sub-table + html_table(F("sub"), false); // Sub-table html_table_header(F("ON color")); html_table_header(F("OFF color")); html_table_header(F("Border color")); @@ -1084,10 +1083,9 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { addFormSubHeader(F("Touch objects")); { - # if !TOUCH_FEATURE_EXTENDED_TOUCH addRowLabel(F("Object")); - # endif // if !TOUCH_FEATURE_EXTENDED_TOUCH - html_table(F("multi2row"), false); // Sub-table with alternating highlight per 2 rows + + html_table(F("multirow tworow"), false); // Sub-table with alternating highlight per 2 rows html_table_header(F(" # ")); html_table_header(F("On")); html_table_header(F("Objectname")); @@ -1158,7 +1156,9 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { toString(Button_layout_e::LeftBottomAligned), toString(Button_layout_e::RightBottomAligned), toString(Button_layout_e::NoCaption), + # if ADAGFX_ENABLE_BMP_DISPLAY toString(Button_layout_e::Bitmap), + # endif // if ADAGFX_ENABLE_BMP_DISPLAY # if ADAGFX_ENABLE_BUTTON_SLIDER toString(Button_layout_e::Slider), # endif // if ADAGFX_ENABLE_BUTTON_SLIDER @@ -1175,7 +1175,9 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { static_cast(Button_layout_e::LeftBottomAligned), static_cast(Button_layout_e::RightBottomAligned), static_cast(Button_layout_e::NoCaption), + # if ADAGFX_ENABLE_BMP_DISPLAY static_cast(Button_layout_e::Bitmap), + # endif // if ADAGFX_ENABLE_BMP_DISPLAY # if ADAGFX_ENABLE_BUTTON_SLIDER static_cast(Button_layout_e::Slider), # endif // if ADAGFX_ENABLE_BUTTON_SLIDER @@ -1242,7 +1244,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { html_TD(); // (on/off) button (type) # if TOUCH_FEATURE_EXTENDED_TOUCH addSelector(getPluginCustomArgName(objectNr + 800), - static_cast(Button_type_e::Button_MAX), + sizeof(buttonTypeValues) / sizeof(int), buttonTypeOptions, buttonTypeValues, nullptr, @@ -1253,7 +1255,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { ); html_TD(); // button alignment addSelector(getPluginCustomArgName(objectNr + 900), - static_cast(Button_layout_e::Alignment_MAX), + sizeof(buttonLayoutValues) / sizeof(int), buttonLayoutOptions, buttonLayoutValues, nullptr, @@ -1314,7 +1316,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { ); html_TD(); // button action addSelector(getPluginCustomArgName(objectNr + 2000), - static_cast(Touch_action_e::TouchAction_MAX), + sizeof(touchActionValues) / sizeof(int), touchActionOptions, touchActionValues, nullptr, @@ -2269,7 +2271,6 @@ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, eventCommand += -5; break; case Touch_action_e::Default: - case Touch_action_e::TouchAction_MAX: eventCommand += -1; // Ignore break; } @@ -2314,7 +2315,6 @@ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, decrementButtonPage(event); break; case Touch_action_e::Default: - case Touch_action_e::TouchAction_MAX: // no action break; } String log = F("TOUCH event: "); diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index 56bc1ea97d..3f7718afe7 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -236,7 +236,7 @@ struct tTouchObjects String captionOff; }; -// Touch actions +// Touch actions, max 16! enum class Touch_action_e : uint8_t { Default = 0u, ActivateGroup = 1u, @@ -244,7 +244,6 @@ enum class Touch_action_e : uint8_t { DecrementGroup = 3u, IncrementPage = 4u, DecrementPage = 5u, - TouchAction_MAX = 6u // Last item is count, max 16! }; # if TOUCH_FEATURE_SWIPE From 168d6954be99f8f72513b68e3ea3002d90f43cdc Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 14 Sep 2022 22:13:54 +0200 Subject: [PATCH 052/113] [AdaGFX] Remove unneeded enum MAX values, fix BMP conditional compilation issue --- src/src/Helpers/AdafruitGFX_helper.cpp | 40 ++++++++++++-------------- src/src/Helpers/AdafruitGFX_helper.h | 40 ++++++++++++-------------- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index c9d08b29a7..189f304f61 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -5,9 +5,9 @@ # include "../Helpers/StringConverter.h" # include "../WebServer/Markup_Forms.h" -# if FEATURE_SD && defined(ADAGFX_ENABLE_BMP_DISPLAY) +# if FEATURE_SD && ADAGFX_ENABLE_BMP_DISPLAY # include -# endif // if FEATURE_SD && defined(ADAGFX_ENABLE_BMP_DISPLAY) +# endif // if FEATURE_SD && ADAGFX_ENABLE_BMP_DISPLAY # if ADAGFX_FONTS_INCLUDED # include "../Static/Fonts/Seven_Segment24pt7b.h" @@ -116,7 +116,6 @@ const __FlashStringHelper* toString(const Button_type_e button) { case Button_type_e::ArrowUp: return F("Arrow, up"); case Button_type_e::ArrowRight: return F("Arrow, right"); case Button_type_e::ArrowDown: return F("Arrow, down"); - case Button_type_e::Button_MAX: break; } return F("Unsupported!"); } @@ -136,11 +135,12 @@ const __FlashStringHelper* toString(const Button_layout_e layout) { case Button_layout_e::RightBottomAligned: return F("Right-Bottom-aligned"); case Button_layout_e::LeftBottomAligned: return F("Left-Bottom-aligned"); case Button_layout_e::NoCaption: return F("No Caption"); + # if ADAGFX_ENABLE_BMP_DISPLAY case Button_layout_e::Bitmap: return F("Bitmap image"); + # endif // if ADAGFX_ENABLE_BMP_DISPLAY # if ADAGFX_ENABLE_BUTTON_SLIDER case Button_layout_e::Slider: return F("Slide control"); # endif // if ADAGFX_ENABLE_BUTTON_SLIDER - case Button_layout_e::Alignment_MAX: break; } return F("Unsupported!"); } @@ -152,21 +152,20 @@ const __FlashStringHelper* toString(const Button_layout_e layout) { ****************************************************************************************/ void AdaGFXFormTextPrintMode(const __FlashStringHelper *id, uint8_t selectedIndex) { - const int textModeCount = static_cast(AdaGFXTextPrintMode::MAX); - const __FlashStringHelper *textModes[textModeCount] = { // Be sure to use all available modes from enum! + const __FlashStringHelper *textModes[] = { // Be sure to use all available modes from enum! toString(AdaGFXTextPrintMode::ContinueToNextLine), toString(AdaGFXTextPrintMode::TruncateExceedingMessage), toString(AdaGFXTextPrintMode::ClearThenTruncate), toString(AdaGFXTextPrintMode::TruncateExceedingCentered), }; - const int textModeOptions[textModeCount] = { + const int textModeOptions[] = { static_cast(AdaGFXTextPrintMode::ContinueToNextLine), static_cast(AdaGFXTextPrintMode::TruncateExceedingMessage), static_cast(AdaGFXTextPrintMode::ClearThenTruncate), static_cast(AdaGFXTextPrintMode::TruncateExceedingCentered), }; - addFormSelector(F("Text print Mode"), id, textModeCount, textModes, textModeOptions, selectedIndex); + addFormSelector(F("Text print Mode"), id, sizeof(textModeOptions) / sizeof(int), textModes, textModeOptions, selectedIndex); } void AdaGFXFormColorDepth(const __FlashStringHelper *id, @@ -1646,10 +1645,9 @@ bool AdafruitGFX_helper::processCommand(const String& string) { nParams[2] += w2 / 2; // A little margin from left nParams[3] += (nParams[5] - h1 * 1.5); // bottom align + a little margin break; + # if ADAGFX_ENABLE_BMP_DISPLAY case Button_layout_e::Bitmap: - { // Use ON/OFF caption to specify (full) bitmap filename - # if ADAGFX_ENABLE_BMP_DISPLAY - + { // Use ON/OFF caption to specify (full) bitmap filename if (!newString.isEmpty()) { int offX = 0; // Allow optional arguments for x and y offset values, usage: int offY = 0; // [x,[y,]]filename.bmp @@ -1666,18 +1664,16 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } } success = showBmp(newString, nParams[2] + _xo + offX, nParams[3] + _yo + offY); - } else - # endif // if ADAGFX_ENABLE_BMP_DISPLAY - { + } else { success = false; } break; } + # endif // if ADAGFX_ENABLE_BMP_DISPLAY case Button_layout_e::NoCaption: # if ADAGFX_ENABLE_BUTTON_SLIDER case Button_layout_e::Slider: // Nothing to do here (yet) # endif // if ADAGFX_ENABLE_BUTTON_SLIDER - case Button_layout_e::Alignment_MAX: break; } @@ -1685,7 +1681,10 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # if ADAGFX_ENABLE_BUTTON_SLIDER && (buttonLayout != Button_layout_e::Slider) # endif // if ADAGFX_ENABLE_BUTTON_SLIDER - && (buttonLayout != Button_layout_e::Bitmap)) { + # if ADAGFX_ENABLE_BMP_DISPLAY + && (buttonLayout != Button_layout_e::Bitmap) + # endif // if ADAGFX_ENABLE_BMP_DISPLAY + ) { // Set position and colors, then print _display->setCursor(nParams[2] + _xo, nParams[3] + _yo); _display->setTextColor(textColor, textColor); // transparent bg results in button color @@ -1921,13 +1920,13 @@ bool AdafruitGFX_helper::pluginGetConfigValue(String& string) { bool success = false; String command = parseString(string, 1); - if (command.equals(F("win"))) { // win: get current window id + if (command.equals(F("win"))) { // win: get current window id # if ADAGFX_ENABLE_FRAMED_WINDOW // if feature enabled string = getWindow(); success = true; # endif // if ADAGFX_ENABLE_FRAMED_WINDOW } else if (command.equals(F("iswin"))) { // iswin: check if windows exists - # if ADAGFX_ENABLE_FRAMED_WINDOW // if feature enabled + # if ADAGFX_ENABLE_FRAMED_WINDOW // if feature enabled command = parseString(string, 2); int win = 0; @@ -1936,11 +1935,11 @@ bool AdafruitGFX_helper::pluginGetConfigValue(String& string) { } else { string = '0'; } - success = true; // Always correct, just return 'false' if wrong + success = true; // Always correct, just return 'false' if wrong # endif // if ADAGFX_ENABLE_FRAMED_WINDOW } else if ((command.equals(F("width"))) || // width/height: get window width or height (command.equals(F("height")))) { - # if ADAGFX_ENABLE_FRAMED_WINDOW // if feature enabled + # if ADAGFX_ENABLE_FRAMED_WINDOW // if feature enabled uint16_t w = 0, h = 0; getWindowLimits(w, h); @@ -2044,7 +2043,6 @@ void AdafruitGFX_helper::drawButtonShape(const Button_type_e& buttonType, break; } case Button_type_e::None: - case Button_type_e::Button_MAX: break; } } diff --git a/src/src/Helpers/AdafruitGFX_helper.h b/src/src/Helpers/AdafruitGFX_helper.h index fe6a19feae..f928903bf6 100644 --- a/src/src/Helpers/AdafruitGFX_helper.h +++ b/src/src/Helpers/AdafruitGFX_helper.h @@ -43,20 +43,18 @@ # include "../Helpers/ESPEasy_Storage.h" # include "../ESPEasyCore/ESPEasy_Log.h" -# define ADAGFX_PARSE_MAX_ARGS 7 // Maximum number of arguments needed and supported (corrected) +# define ADAGFX_PARSE_MAX_ARGS 7 // Maximum number of arguments needed and supported (corrected) # ifndef ADAGFX_ARGUMENT_VALIDATION -# define ADAGFX_ARGUMENT_VALIDATION 1 // Validate command arguments +# define ADAGFX_ARGUMENT_VALIDATION 1 // Validate command arguments # endif // ifndef ADAGFX_ARGUMENT_VALIDATION # ifndef ADAGFX_USE_ASCIITABLE -# define ADAGFX_USE_ASCIITABLE 1 // Enable 'asciitable' command (useful for debugging/development) +# define ADAGFX_USE_ASCIITABLE 1 // Enable 'asciitable' command (useful for debugging/development) # endif // ifndef ADAGFX_USE_ASCIITABLE # ifndef ADAGFX_SUPPORT_7COLOR - -// # define ADAGFX_SUPPORT_7COLOR 1 // Do we support 7-Color displays? +# define ADAGFX_SUPPORT_7COLOR 0 // Do we support 7-Color displays? # endif // ifndef ADAGFX_SUPPORT_7COLOR # ifndef ADAGFX_SUPPORT_8and16COLOR - -// # define ADAGFX_SUPPORT_8and16COLOR 1 // Do we support 8 and 16-Color displays? +# define ADAGFX_SUPPORT_8and16COLOR 0 // Do we support 8 and 16-Color displays? # endif // ifndef ADAGFX_SUPPORT_8and16COLOR # ifndef ADAGFX_FONTS_INCLUDED # define ADAGFX_FONTS_INCLUDED 1 // 3 extra fonts, also controls enable/disable of below 8pt/12pt fonts @@ -180,12 +178,14 @@ # ifndef ADAGFX_FONTS_EXTRA_20PT_INCLUDED # define ADAGFX_FONTS_EXTRA_20PT_INCLUDED # endif // ifndef ADAGFX_FONTS_EXTRA_20PT_INCLUDED -# ifndef ADAGFX_SUPPORT_7COLOR +# if !ADAGFX_SUPPORT_7COLOR +# undef ADAGFX_SUPPORT_7COLOR # define ADAGFX_SUPPORT_7COLOR 1 -# endif // ifndef ADAGFX_SUPPORT_7COLOR -# ifndef ADAGFX_SUPPORT_8and16COLOR +# endif // if !ADAGFX_SUPPORT_7COLOR +# if !ADAGFX_SUPPORT_8and16COLOR +# undef ADAGFX_SUPPORT_8and16COLOR # define ADAGFX_SUPPORT_8and16COLOR 1 -# endif // ifndef ADAGFX_SUPPORT_8and16COLOR +# endif // if !ADAGFX_SUPPORT_8and16COLOR # endif // ifdef PLUGIN_SET_MAX # define ADAGFX_PARSE_PREFIX F("~") // Subcommand-trigger prefix and postfix strings @@ -275,12 +275,12 @@ enum class AdaGFXColorDepth : uint16_t { EightColor = 8u, // 8 regular colors SixteenColor = 16u, // 16 colors # endif // if ADAGFX_SUPPORT_8and16COLOR - FullColor = 65535u // 65535 colors (max. supported by RGB565) + FullColor = 65535u // 65535 colors (max. supported by RGB565) }; # if ADAGFX_ENABLE_BUTTON_DRAW -// Only bits 0..3 can be used, masked with: 0x0F +// Only bits 0..3 can be used, masked with: 0x0F, max possible values: 16 // stored combined with Button_layout_e value enum class Button_type_e : uint8_t { None = 0x00, @@ -291,10 +291,9 @@ enum class Button_type_e : uint8_t { ArrowUp = 0x05, ArrowRight = 0x06, ArrowDown = 0x07, - Button_MAX = 8u // must be last value in enum, max possible values: 16 }; -// Only bits 4..7 can be used, masked with: 0xF0 +// Only bits 4..7 can be used, masked with: 0xF0, max possible values: 16 // stored combined with Button_type_e value enum class Button_layout_e : uint8_t { CenterAligned = 0x00, @@ -307,12 +306,11 @@ enum class Button_layout_e : uint8_t { RightBottomAligned = 0x70, LeftBottomAligned = 0x80, NoCaption = 0x90, - Bitmap = 0xA0, + # if ADAGFX_ENABLE_BMP_DISPLAY + Bitmap = 0xA0, + # endif // if ADAGFX_ENABLE_BMP_DISPLAY # if ADAGFX_ENABLE_BUTTON_SLIDER - Slider = 0xB0, - Alignment_MAX = 12u // options-count, max possible values: 16 - # else // if ADAGFX_ENABLE_BUTTON_SLIDER - Alignment_MAX = 11u // options-count, max possible values: 16 + Slider = 0xB0, # endif // if ADAGFX_ENABLE_BUTTON_SLIDER }; @@ -489,7 +487,7 @@ class AdafruitGFX_helper { const int16_t& h, int16_t windowId = -1, const int8_t & rotation = -1); - bool deleteWindow(const uint8_t& windowId); + bool deleteWindow(const uint8_t& windowId); # endif // if ADAGFX_ENABLE_FRAMED_WINDOW uint16_t getTextSize(const String& text, From 2f5ecb3855dcd41ada68f65c4fc68b23254ac6a6 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 24 Sep 2022 20:31:00 +0200 Subject: [PATCH 053/113] [AdaGFX] Fix compilation error after merge from mega --- src/src/Helpers/AdafruitGFX_helper.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index 814a8a79d5..9b2d21e944 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -274,7 +274,7 @@ void AdaGFXFormForeAndBackColors(const __FlashStringHelper *foregroundId, AdaGFXHtmlColorDepthDataList(F("adagfxFGBGcolors"), colorDepth); addRowLabel(F("Foreground color")); addTextBox(foregroundId, color, 11, false, false, - EMPTY_STRING, EMPTY_STRING + EMPTY_STRING, F("") # if FEATURE_TOOLTIPS , F("Foreground color") # endif // if FEATURE_TOOLTIPS @@ -283,7 +283,7 @@ void AdaGFXFormForeAndBackColors(const __FlashStringHelper *foregroundId, color = AdaGFXcolorToString(backgroundColor, colorDepth); addRowLabel(F("Background color")); addTextBox(backgroundId, color, 11, false, false, - EMPTY_STRING, EMPTY_STRING + EMPTY_STRING, F("") # if FEATURE_TOOLTIPS , F("Background color") # endif // if FEATURE_TOOLTIPS From b417410e50ae175241f92ff153445d60324314bf Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 24 Sep 2022 20:31:59 +0200 Subject: [PATCH 054/113] [TouchHandler] Fix compilation error after merge from mega, table improvements --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 0be65b8b4a..fb2c3956d9 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -1226,7 +1226,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { addTextBox(getPluginCustomArgName(objectNr + 100), TouchObjects[objectNr].objectName, TOUCH_MaxObjectNameLength, - false, false, EMPTY_STRING, EMPTY_STRING); + false, false, EMPTY_STRING, F("wide")); html_TD(); // top-x addNumericBox(getPluginCustomArgName(objectNr + 200), TouchObjects[objectNr].top_left.x, 0, 65535 @@ -1291,7 +1291,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { false, false, EMPTY_STRING, - F("wide") + F("xwide") # if TOUCH_FEATURE_TOOLTIPS , F("ON caption") # endif // if TOUCH_FEATURE_TOOLTIPS @@ -1395,7 +1395,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { false, false, EMPTY_STRING, - F("wide") + F("xwide") # if TOUCH_FEATURE_TOOLTIPS , F("OFF caption") # endif // if TOUCH_FEATURE_TOOLTIPS From a03dcb2d8cd8c078f91e2a09ff9aa541ecafcd5a Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 24 Sep 2022 21:14:04 +0200 Subject: [PATCH 055/113] [P116] Add GetConfig feature, code improvements --- docs/source/Plugin/P116.rst | 5 ++ src/_P116_ST77xx.ino | 89 +++++++++++++--------- src/src/PluginStructs/P116_data_struct.cpp | 31 ++++++-- src/src/PluginStructs/P116_data_struct.h | 24 +++--- 4 files changed, 96 insertions(+), 53 deletions(-) diff --git a/docs/source/Plugin/P116.rst b/docs/source/Plugin/P116.rst index f0e8060125..6e2a42d139 100644 --- a/docs/source/Plugin/P116.rst +++ b/docs/source/Plugin/P116.rst @@ -242,6 +242,11 @@ Commands .. include:: P116_commands.repl .. include:: AdaGFX_commands.repl +Values +------ + +.. include:: AdaGFX_values.repl + .. Events .. ~~~~~~ diff --git a/src/_P116_ST77xx.ino b/src/_P116_ST77xx.ino index e2af9db700..b377af1233 100644 --- a/src/_P116_ST77xx.ino +++ b/src/_P116_ST77xx.ino @@ -8,6 +8,7 @@ // History: +// 2022-06-05 tonhuisman: Implement support for getting config values, see AdafruitGFX_Helper.h changelog for details // 2022-07-06 tonhuisman: Add support for ST7735sv M5Stack StickC (Inverted colors) // 2021-11-16 tonhuisman: P116: Change state from Development to Testing // 2021-11-08 tonhuisman: Add support for function PLUGIN_GET_DISPLAY_PARAMETERS for retrieving the display parameters @@ -110,12 +111,12 @@ boolean Plugin_116(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_LOAD: { - AdaGFXFormBacklight(F("p116_backlight"), P116_CONFIG_BACKLIGHT_PIN, - F("p116_backpercentage"), P116_CONFIG_BACKLIGHT_PERCENT); + AdaGFXFormBacklight(F("backlight"), P116_CONFIG_BACKLIGHT_PIN, + F("backpercentage"), P116_CONFIG_BACKLIGHT_PERCENT); - AdaGFXFormDisplayButton(F("p116_button"), P116_CONFIG_BUTTON_PIN, - F("p116_buttonInverse"), bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_INVERT_BUTTON), - F("p116_timer"), P116_CONFIG_DISPLAY_TIMEOUT); + AdaGFXFormDisplayButton(F("button"), P116_CONFIG_BUTTON_PIN, + F("buttonInverse"), bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_INVERT_BUTTON), + F("timer"), P116_CONFIG_DISPLAY_TIMEOUT); { const __FlashStringHelper *options4[] = { @@ -141,7 +142,7 @@ boolean Plugin_116(uint8_t function, struct EventStruct *event, String& string) static_cast(ST77xx_type_e::ST7796s_320x480) }; addFormSelector(F("TFT display model"), - F("p116_type"), + F("type"), static_cast(ST77xx_type_e::ST77xx_MAX), options4, optionValues4, @@ -150,13 +151,13 @@ boolean Plugin_116(uint8_t function, struct EventStruct *event, String& string) addFormSubHeader(F("Layout")); - AdaGFXFormRotation(F("p116_rotate"), P116_CONFIG_FLAG_GET_ROTATION); + AdaGFXFormRotation(F("rotate"), P116_CONFIG_FLAG_GET_ROTATION); - AdaGFXFormTextPrintMode(F("p116_mode"), P116_CONFIG_FLAG_GET_MODE); + AdaGFXFormTextPrintMode(F("mode"), P116_CONFIG_FLAG_GET_MODE); - AdaGFXFormFontScaling(F("p116_fontscale"), P116_CONFIG_FLAG_GET_FONTSCALE); + AdaGFXFormFontScaling(F("fontscale"), P116_CONFIG_FLAG_GET_FONTSCALE); - addFormCheckBox(F("Clear display on exit"), F("p116_clearOnExit"), bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_CLEAR_ON_EXIT)); + addFormCheckBox(F("Clear display on exit"), F("clearOnExit"), bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_CLEAR_ON_EXIT)); { const __FlashStringHelper *commandTriggers[] = { // Be sure to use all options available in the enum (except MAX)! @@ -174,7 +175,7 @@ boolean Plugin_116(uint8_t function, struct EventStruct *event, String& string) static_cast(P116_CommandTrigger::st7796) }; addFormSelector(F("Write Command trigger"), - F("p116_commandtrigger"), + F("commandtrigger"), static_cast(P116_CommandTrigger::MAX), commandTriggers, commandTriggerOptions, @@ -183,18 +184,18 @@ boolean Plugin_116(uint8_t function, struct EventStruct *event, String& string) } // Inverted state! - addFormCheckBox(F("Wake display on receiving text"), F("p116_NoDisplay"), !bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_NO_WAKE)); + addFormCheckBox(F("Wake display on receiving text"), F("NoDisplay"), !bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_NO_WAKE)); addFormNote(F("When checked, the display wakes up at receiving remote updates.")); - AdaGFXFormTextColRowMode(F("p116_colrow"), bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_USE_COL_ROW) == 1); + AdaGFXFormTextColRowMode(F("colrow"), bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_USE_COL_ROW) == 1); - AdaGFXFormTextBackgroundFill(F("p116_backfill"), bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_BACK_FILL) == 0); // Inverse + AdaGFXFormTextBackgroundFill(F("backfill"), bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_BACK_FILL) == 0); // Inverse addFormSubHeader(F("Content")); - AdaGFXFormForeAndBackColors(F("p116_foregroundcolor"), + AdaGFXFormForeAndBackColors(F("foregroundcolor"), P116_CONFIG_GET_COLOR_FOREGROUND, - F("p116_backgroundcolor"), + F("backgroundcolor"), P116_CONFIG_GET_COLOR_BACKGROUND); String strings[P116_Nlines]; @@ -221,46 +222,45 @@ boolean Plugin_116(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SAVE: { - P116_CONFIG_BUTTON_PIN = getFormItemInt(F("p116_button")); - P116_CONFIG_DISPLAY_TIMEOUT = getFormItemInt(F("p116_timer")); - P116_CONFIG_BACKLIGHT_PIN = getFormItemInt(F("p116_backlight")); - P116_CONFIG_BACKLIGHT_PERCENT = getFormItemInt(F("p116_backpercentage")); + P116_CONFIG_BUTTON_PIN = getFormItemInt(F("button")); + P116_CONFIG_DISPLAY_TIMEOUT = getFormItemInt(F("timer")); + P116_CONFIG_BACKLIGHT_PIN = getFormItemInt(F("backlight")); + P116_CONFIG_BACKLIGHT_PERCENT = getFormItemInt(F("backpercentage")); uint32_t lSettings = 0; - bitWrite(lSettings, P116_CONFIG_FLAG_NO_WAKE, !isFormItemChecked(F("p116_NoDisplay"))); // Bit 0 NoDisplayOnReceivingText, - // reverse logic, default=checked! - bitWrite(lSettings, P116_CONFIG_FLAG_INVERT_BUTTON, isFormItemChecked(F("p116_buttonInverse"))); // Bit 1 buttonInverse - bitWrite(lSettings, P116_CONFIG_FLAG_CLEAR_ON_EXIT, isFormItemChecked(F("p116_clearOnExit"))); // Bit 2 ClearOnExit - bitWrite(lSettings, P116_CONFIG_FLAG_USE_COL_ROW, isFormItemChecked(F("p116_colrow"))); // Bit 3 Col/Row addressing - - set4BitToUL(lSettings, P116_CONFIG_FLAG_MODE, getFormItemInt(F("p116_mode"))); // Bit 4..7 Text print mode - set4BitToUL(lSettings, P116_CONFIG_FLAG_ROTATION, getFormItemInt(F("p116_rotate"))); // Bit 8..11 Rotation - set4BitToUL(lSettings, P116_CONFIG_FLAG_FONTSCALE, getFormItemInt(F("p116_fontscale"))); // Bit 12..15 Font scale - set4BitToUL(lSettings, P116_CONFIG_FLAG_TYPE, getFormItemInt(F("p116_type"))); // Bit 16..19 Hardwaretype - set4BitToUL(lSettings, P116_CONFIG_FLAG_CMD_TRIGGER, getFormItemInt(F("p116_commandtrigger"))); // Bit 20..23 Command trigger - - bitWrite(lSettings, P116_CONFIG_FLAG_BACK_FILL, !isFormItemChecked(F("p116_backfill"))); // Bit 28 Back fill text (inv) + bitWrite(lSettings, P116_CONFIG_FLAG_NO_WAKE, !isFormItemChecked(F("NoDisplay"))); // Bit 0 NoDisplayOnReceivingText, + // reverse logic, default=checked! + bitWrite(lSettings, P116_CONFIG_FLAG_INVERT_BUTTON, isFormItemChecked(F("buttonInverse"))); // Bit 1 buttonInverse + bitWrite(lSettings, P116_CONFIG_FLAG_CLEAR_ON_EXIT, isFormItemChecked(F("clearOnExit"))); // Bit 2 ClearOnExit + bitWrite(lSettings, P116_CONFIG_FLAG_USE_COL_ROW, isFormItemChecked(F("colrow"))); // Bit 3 Col/Row addressing + + set4BitToUL(lSettings, P116_CONFIG_FLAG_MODE, getFormItemInt(F("mode"))); // Bit 4..7 Text print mode + set4BitToUL(lSettings, P116_CONFIG_FLAG_ROTATION, getFormItemInt(F("rotate"))); // Bit 8..11 Rotation + set4BitToUL(lSettings, P116_CONFIG_FLAG_FONTSCALE, getFormItemInt(F("fontscale"))); // Bit 12..15 Font scale + set4BitToUL(lSettings, P116_CONFIG_FLAG_TYPE, getFormItemInt(F("type"))); // Bit 16..19 Hardwaretype + set4BitToUL(lSettings, P116_CONFIG_FLAG_CMD_TRIGGER, getFormItemInt(F("commandtrigger"))); // Bit 20..23 Command trigger + + bitWrite(lSettings, P116_CONFIG_FLAG_BACK_FILL, !isFormItemChecked(F("backfill"))); // Bit 28 Back fill text (inv) P116_CONFIG_FLAGS = lSettings; - String color = web_server.arg(F("p116_foregroundcolor")); + String color = web_server.arg(F("foregroundcolor")); uint16_t fgcolor = ADAGFX_WHITE; // Default to white when empty if (!color.isEmpty()) { fgcolor = AdaGFXparseColor(color); // Reduce to rgb565 } - color = web_server.arg(F("p116_backgroundcolor")); - uint16_t bgcolor = AdaGFXparseColor(color); + color = web_server.arg(F("backgroundcolor")); + const uint16_t bgcolor = AdaGFXparseColor(color); P116_CONFIG_COLORS = fgcolor | (bgcolor << 16); // Store as a single setting String strings[P116_Nlines]; - String error; for (uint8_t varNr = 0; varNr < P116_Nlines; varNr++) { strings[varNr] = web_server.arg(getPluginCustomArgName(varNr)); } - error = SaveCustomTaskSettings(event->TaskIndex, strings, P116_Nlines, 0); + const String error = SaveCustomTaskSettings(event->TaskIndex, strings, P116_Nlines, 0); if (error.length() > 0) { addHtmlError(error); @@ -376,6 +376,19 @@ boolean Plugin_116(uint8_t function, struct EventStruct *event, String& string) } break; } + + # if ADAGFX_ENABLE_GET_CONFIG_VALUE + case PLUGIN_GET_CONFIG_VALUE: + { + P116_data_struct *P116_data = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr != P116_data) { + success = P116_data->plugin_get_config_value(event, string); // GetConfig operation, handle variables, fully delegated to + // AdafruitGFX_helper + } + break; + } + # endif // if ADAGFX_ENABLE_GET_CONFIG_VALUE } return success; } diff --git a/src/src/PluginStructs/P116_data_struct.cpp b/src/src/PluginStructs/P116_data_struct.cpp index d8e0c15002..bdc3776eff 100644 --- a/src/src/PluginStructs/P116_data_struct.cpp +++ b/src/src/PluginStructs/P116_data_struct.cpp @@ -7,7 +7,7 @@ /**************************************************************************** * ST77xx_type_toString: Display-value for the device selected ***************************************************************************/ -const __FlashStringHelper* ST77xx_type_toString(ST77xx_type_e device) { +const __FlashStringHelper* ST77xx_type_toString(const ST77xx_type_e& device) { switch (device) { case ST77xx_type_e::ST7735s_128x128: return F("ST7735 128 x 128px"); case ST77xx_type_e::ST7735s_128x160: return F("ST7735 128 x 160px"); @@ -26,7 +26,9 @@ const __FlashStringHelper* ST77xx_type_toString(ST77xx_type_e device) { /**************************************************************************** * ST77xx_type_toResolution: X and Y resolution for the selected type ***************************************************************************/ -void ST77xx_type_toResolution(ST77xx_type_e device, uint16_t& x, uint16_t& y) { +void ST77xx_type_toResolution(const ST77xx_type_e& device, + uint16_t & x, + uint16_t & y) { switch (device) { case ST77xx_type_e::ST7735s_128x128: x = 128; @@ -69,7 +71,7 @@ void ST77xx_type_toResolution(ST77xx_type_e device, uint16_t& x, uint16_t& y) { /**************************************************************************** * P116_CommandTrigger_toString: return the command string selected ***************************************************************************/ -const __FlashStringHelper* P116_CommandTrigger_toString(P116_CommandTrigger cmd) { +const __FlashStringHelper* P116_CommandTrigger_toString(const P116_CommandTrigger& cmd) { switch (cmd) { case P116_CommandTrigger::tft: return F("tft"); case P116_CommandTrigger::st7735: return F("st7735"); @@ -366,7 +368,8 @@ bool P116_data_struct::plugin_once_a_second(struct EventStruct *event) { /**************************************************************************** * plugin_write: Handle commands ***************************************************************************/ -bool P116_data_struct::plugin_write(struct EventStruct *event, const String& string) { +bool P116_data_struct::plugin_write(struct EventStruct *event, + const String & string) { bool success = false; String cmd = parseString(string, 1); @@ -427,6 +430,23 @@ bool P116_data_struct::plugin_write(struct EventStruct *event, const String& str return success; } +# if ADAGFX_ENABLE_GET_CONFIG_VALUE + +/**************************************************************************** + * plugin_get_config_value: Retrieve values like [#] + ***************************************************************************/ +bool P116_data_struct::plugin_get_config_value(struct EventStruct *event, + String & string) { + bool success = false; + + if (gfxHelper != nullptr) { + success = gfxHelper->pluginGetConfigValue(string); + } + return success; +} + +# endif // if ADAGFX_ENABLE_GET_CONFIG_VALUE + /**************************************************************************** * displayOnOff: Turn display on or off ***************************************************************************/ @@ -446,7 +466,8 @@ void P116_data_struct::displayOnOff(bool state) { /**************************************************************************** * registerButtonState: the button has been pressed, apply some debouncing ***************************************************************************/ -void P116_data_struct::registerButtonState(uint8_t newButtonState, bool bPin3Invers) { +void P116_data_struct::registerButtonState(uint8_t newButtonState, + bool bPin3Invers) { if ((ButtonLastState == 0xFF) || (bPin3Invers != (!!newButtonState))) { ButtonLastState = newButtonState; DebounceCounter++; diff --git a/src/src/PluginStructs/P116_data_struct.h b/src/src/PluginStructs/P116_data_struct.h index 758663f406..12a33ce391 100644 --- a/src/src/PluginStructs/P116_data_struct.h +++ b/src/src/PluginStructs/P116_data_struct.h @@ -78,19 +78,19 @@ enum class ST77xx_type_e : uint8_t { }; enum class P116_CommandTrigger : uint8_t { - tft = 0u, - st77xx, - st7735, - st7789, - st7796, + tft = 0u, + st77xx = 1u, + st7735 = 2u, + st7789 = 3u, + st7796 = 4u, MAX // Keep as last item! }; -const __FlashStringHelper* ST77xx_type_toString(ST77xx_type_e device); -const __FlashStringHelper* P116_CommandTrigger_toString(P116_CommandTrigger cmd); -void ST77xx_type_toResolution(ST77xx_type_e device, - uint16_t & x, - uint16_t & y); +const __FlashStringHelper* ST77xx_type_toString(const ST77xx_type_e& device); +const __FlashStringHelper* P116_CommandTrigger_toString(const P116_CommandTrigger& cmd); +void ST77xx_type_toResolution(const ST77xx_type_e& device, + uint16_t & x, + uint16_t & y); struct P116_data_struct : public PluginTaskData_base { public: @@ -113,6 +113,10 @@ struct P116_data_struct : public PluginTaskData_base { bool plugin_read(struct EventStruct *event); bool plugin_write(struct EventStruct *event, const String & string); + # if ADAGFX_ENABLE_GET_CONFIG_VALUE + bool plugin_get_config_value(struct EventStruct *event, + String & string); + # endif // if ADAGFX_ENABLE_GET_CONFIG_VALUE bool plugin_ten_per_second(struct EventStruct *event); bool plugin_once_a_second(struct EventStruct *event); From 4c488aff92838a4c12bb276f11e78b064310acd6 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 26 Sep 2022 21:59:22 +0200 Subject: [PATCH 056/113] [I2C scanner] Enable plugin-scan for ESP32, updated sensors --- src/src/CustomBuild/define_plugin_sets.h | 85 +++++++++++++----------- src/src/WebServer/I2C_Scanner.cpp | 12 ++-- 2 files changed, 52 insertions(+), 45 deletions(-) diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index 8d785c2d9b..7b4b27c8f9 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -529,11 +529,11 @@ To create/register a plugin, you have to : #if !defined(PLUGIN_DESCR) && !defined(PLUGIN_BUILD_MAX_ESP32) #define PLUGIN_DESCR "IR" #endif - #ifndef USES_P016 + #ifndef USES_P016 #define USES_P016 // IR #endif #define P016_SEND_IR_TO_CONTROLLER false //IF true then the JSON replay solution is transmited back to the condroller. - #ifndef USES_P035 + #ifndef USES_P035 #define USES_P035 // IRTX #endif #define P016_P035_USE_RAW_RAW2 //Use the RAW and RAW2 encodings, disabling it saves 3.7Kb @@ -543,11 +543,11 @@ To create/register a plugin, you have to : #if !defined(PLUGIN_DESCR) && !defined(PLUGIN_BUILD_MAX_ESP32) #define PLUGIN_DESCR "IR Extended" #endif // PLUGIN_DESCR - #ifndef USES_P016 + #ifndef USES_P016 #define USES_P016 // IR #endif #define P016_SEND_IR_TO_CONTROLLER false //IF true then the JSON replay solution is transmited back to the condroller. - #ifndef USES_P035 + #ifndef USES_P035 #define USES_P035 // IRTX #endif // The following define is needed for extended decoding of A/C Messages and or using standardised common arguments for controlling all deeply supported A/C units @@ -566,7 +566,7 @@ To create/register a plugin, you have to : #if !defined(PLUGIN_DESCR) && !defined(PLUGIN_BUILD_MAX_ESP32) #define PLUGIN_DESCR "IR Extended, no IR RX" #endif // PLUGIN_DESCR - #ifndef USES_P035 + #ifndef USES_P035 #define USES_P035 // IRTX #endif // The following define is needed for extended decoding of A/C Messages and or using standardised common arguments for controlling all deeply supported A/C units @@ -695,7 +695,7 @@ To create/register a plugin, you have to : #ifdef PLUGIN_SET_MAGICHOME_IR #define PLUGIN_SET_ONLY_LEDSTRIP - #ifndef USES_P016 + #ifndef USES_P016 #define USES_P016 // IR #endif @@ -1235,7 +1235,7 @@ To create/register a plugin, you have to : #ifndef NOTIFIER_SET_NONE #define NOTIFIER_SET_NONE #endif - + // Do not include large blobs but fetch them from CDN #ifndef WEBSERVER_USE_CDN_JS_CSS #define WEBSERVER_USE_CDN_JS_CSS @@ -1346,7 +1346,7 @@ To create/register a plugin, you have to : #ifdef PLUGIN_SET_COLLECTION_E #define USES_P119 // ITG3205 Gyro #define USES_P120 // ADXL345 I2C - #define USES_P121 // HMC5883L + #define USES_P121 // HMC5883L #define USES_P125 // ADXL345 SPI #define USES_P126 // 74HC595 Shift register #define USES_P129 // 74HC165 Input shiftregisters @@ -1634,6 +1634,9 @@ To create/register a plugin, you have to : #ifndef SHOW_SYSINFO_JSON #define SHOW_SYSINFO_JSON 1 #endif + #ifndef FEATURE_I2C_DEVICE_SCAN + #define FEATURE_I2C_DEVICE_SCAN 1 + #endif // Plugins #ifndef USES_P016 @@ -1725,7 +1728,7 @@ To create/register a plugin, you have to : #define USES_P120 // ADXL345 I2C Acceleration / Gravity #endif #ifndef USES_P121 - #define USES_P121 // HMC5883L + #define USES_P121 // HMC5883L #endif #ifndef USES_P122 // #define USES_P122 // @@ -2189,11 +2192,11 @@ To create/register a plugin, you have to : // This should be done at the end of this file. // Keep them alfabetically sorted so it is easier to add new ones -#ifndef FEATURE_BLYNK +#ifndef FEATURE_BLYNK #define FEATURE_BLYNK 0 #endif -#ifndef FEATURE_CHART_JS +#ifndef FEATURE_CHART_JS #define FEATURE_CHART_JS 0 #endif @@ -2202,111 +2205,115 @@ To create/register a plugin, you have to : #endif -#ifndef FEATURE_CUSTOM_PROVISIONING +#ifndef FEATURE_CUSTOM_PROVISIONING #define FEATURE_CUSTOM_PROVISIONING 0 #endif -#ifndef FEATURE_DNS_SERVER +#ifndef FEATURE_DNS_SERVER #define FEATURE_DNS_SERVER 0 #endif -#ifndef FEATURE_DOMOTICZ +#ifndef FEATURE_DOMOTICZ #define FEATURE_DOMOTICZ 0 #endif -#ifndef FEATURE_DOWNLOAD +#ifndef FEATURE_DOWNLOAD #define FEATURE_DOWNLOAD 0 #endif -#ifndef FEATURE_ESPEASY_P2P +#ifndef FEATURE_ESPEASY_P2P #define FEATURE_ESPEASY_P2P 0 #endif -#ifndef FEATURE_ETHERNET +#ifndef FEATURE_ETHERNET #define FEATURE_ETHERNET 0 #endif -#ifndef FEATURE_EXT_RTC +#ifndef FEATURE_EXT_RTC #define FEATURE_EXT_RTC 0 #endif -#ifndef FEATURE_FHEM +#ifndef FEATURE_FHEM #define FEATURE_FHEM 0 #endif -#ifndef FEATURE_HOMEASSISTANT_OPENHAB +#ifndef FEATURE_HOMEASSISTANT_OPENHAB #define FEATURE_HOMEASSISTANT_OPENHAB 0 #endif -#ifndef FEATURE_I2CMULTIPLEXER +#ifndef FEATURE_I2CMULTIPLEXER #define FEATURE_I2CMULTIPLEXER 0 #endif -#ifndef FEATURE_I2C_DEVICE_SCAN -#define FEATURE_I2C_DEVICE_SCAN 0 +#ifndef FEATURE_I2C_DEVICE_SCAN + #ifdef ESP32 + #define FEATURE_I2C_DEVICE_SCAN 1 + #else + #define FEATURE_I2C_DEVICE_SCAN 0 + #endif #endif -#ifndef FEATURE_MDNS +#ifndef FEATURE_MDNS #define FEATURE_MDNS 0 #endif -#ifndef FEATURE_MODBUS +#ifndef FEATURE_MODBUS #define FEATURE_MODBUS 0 #endif -#ifndef FEATURE_MQTT +#ifndef FEATURE_MQTT #define FEATURE_MQTT 0 #endif -#ifndef FEATURE_NON_STANDARD_24_TASKS +#ifndef FEATURE_NON_STANDARD_24_TASKS #define FEATURE_NON_STANDARD_24_TASKS 0 #endif -#ifndef FEATURE_NOTIFIER +#ifndef FEATURE_NOTIFIER #define FEATURE_NOTIFIER 0 #endif -#ifndef FEATURE_PACKED_RAW_DATA +#ifndef FEATURE_PACKED_RAW_DATA #define FEATURE_PACKED_RAW_DATA 0 #endif -#ifndef FEATURE_PLUGIN_STATS +#ifndef FEATURE_PLUGIN_STATS #define FEATURE_PLUGIN_STATS 0 #endif -#ifndef FEATURE_REPORTING +#ifndef FEATURE_REPORTING #define FEATURE_REPORTING 0 #endif -#ifndef FEATURE_RTTTL +#ifndef FEATURE_RTTTL #define FEATURE_RTTTL 0 #endif -#ifndef FEATURE_SD +#ifndef FEATURE_SD #define FEATURE_SD 0 #endif -#ifndef FEATURE_SERVO +#ifndef FEATURE_SERVO #define FEATURE_SERVO 0 #endif -#ifndef FEATURE_SETTINGS_ARCHIVE +#ifndef FEATURE_SETTINGS_ARCHIVE #define FEATURE_SETTINGS_ARCHIVE 0 #endif -#ifndef FEATURE_SSDP +#ifndef FEATURE_SSDP #define FEATURE_SSDP 0 #endif -#ifndef FEATURE_TIMING_STATS +#ifndef FEATURE_TIMING_STATS #define FEATURE_TIMING_STATS 0 #endif -#ifndef FEATURE_TOOLTIPS +#ifndef FEATURE_TOOLTIPS #define FEATURE_TOOLTIPS 0 #endif -#ifndef FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES +#ifndef FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES #define FEATURE_TRIGONOMETRIC_FUNCTIONS_RULES 0 #endif diff --git a/src/src/WebServer/I2C_Scanner.cpp b/src/src/WebServer/I2C_Scanner.cpp index 089f420247..0f50fc1bd7 100644 --- a/src/src/WebServer/I2C_Scanner.cpp +++ b/src/src/WebServer/I2C_Scanner.cpp @@ -219,20 +219,20 @@ String getKnownI2Cdevice(uint8_t address) { result += F("MAX1704x"); break; case 0x38: - result += F("PCF8574A,AHT10/20/21"); + result += F("LCD,PCF8574A,VEML6070,AHT10/20/21,FT62x6"); + break; + case 0x39: + result += F("LCD,PCF8574A,TSL2561,APDS9960,AHT10"); break; case 0x3A: case 0x3B: case 0x3E: case 0x3F: - result += F("PCF8574A"); - break; - case 0x39: - result += F("PCF8574A,TSL2561,APDS9960,AHT10"); + result += F("LCD,PCF8574A"); break; case 0x3C: case 0x3D: - result += F("PCF8574A,OLED"); + result += F("LCD,PCF8574A,OLED"); break; case 0x40: result += F("SI7021,HTU21D,INA219,PCA9685,HDC1080"); From bcbe80077b6eff6ddcaa2209e47ae78e924d8abf Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 26 Sep 2022 22:00:02 +0200 Subject: [PATCH 057/113] [TouchHandler] Fix touch-disabled option, code improvements --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 147 ++++++++++------------- src/src/Helpers/ESPEasy_TouchHandler.h | 36 +++--- 2 files changed, 81 insertions(+), 102 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index fb2c3956d9..8fd3ca1661 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -23,7 +23,7 @@ const __FlashStringHelper* toString(Touch_action_e action) { /**************************************************************************** * toString: Display-value for the swipe action ***************************************************************************/ -# if TOUCH_FEATURE_SWIPE +# if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE const __FlashStringHelper* toString(Swipe_action_e action) { switch (action) { case Swipe_action_e::Up: return F("Up"); @@ -40,7 +40,7 @@ const __FlashStringHelper* toString(Swipe_action_e action) { return F("Unknown"); } -# endif // if TOUCH_FEATURE_SWIPE +# endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE /** * Constructors @@ -122,13 +122,13 @@ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { Touch_Settings.colorDisabled = TOUCH_DEFAULT_COLOR_DISABLED; Touch_Settings.colorDisabledCaption = TOUCH_DEFAULT_COLOR_DISABLED_CAPTION; } - # endif // if TOUCH_FEATURE_EXTENDED_TOUCH - # if TOUCH_FEATURE_SWIPE + # if TOUCH_FEATURE_SWIPE Touch_Settings.swipeMinimal = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], TOUCH_COMMON_SWIPE_MINIMAL, TOUCH_SETTINGS_SEPARATOR, TOUCH_DEF_SWIPE_MINIMAL); Touch_Settings.swipeMargin = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], TOUCH_COMMON_SWIPE_MARGIN, TOUCH_SETTINGS_SEPARATOR, TOUCH_DEF_SWIPE_MARGIN); - # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH settingsArray[TOUCH_CALIBRATION_START].clear(); // Free a little memory @@ -216,7 +216,7 @@ void ESPEasy_TouchHandler::init(struct EventStruct *event) { } # if TOUCH_FEATURE_EXTENDED_TOUCH - _touchEnabled = bitRead(TOUCH_COMMON_FLAGS, TOUCH_FLAGS_IGNORE_TOUCH); + _touchIgnored = bitRead(Touch_Settings.flags, TOUCH_FLAGS_IGNORE_TOUCH); if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME) && bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)) { @@ -750,7 +750,7 @@ bool ESPEasy_TouchHandler::validButtonGroup(const int16_t& group, (!ignoreZero || group > 0 || (group == 0 && _buttonGroups.size() == 1)); } -# if TOUCH_FEATURE_SWIPE +# if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE /** * set button group page via the Swipe event @@ -793,7 +793,7 @@ bool ESPEasy_TouchHandler::handleButtonSwipe(struct EventStruct *event, return success; } -# endif // if TOUCH_FEATURE_SWIPE +# endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE /** * Set the desired button group, must be a known group, previous group will be erased and new group drawn @@ -1069,7 +1069,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { Touch_Settings.debounceMs, 0, 255); addUnit(F("0..255 msec.")); - # if TOUCH_FEATURE_SWIPE + # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE addFormNumericBox(F("Minimal swipe movement"), F("swipemin"), Touch_Settings.swipeMinimal, 1, 25); addUnit(F("1..25px")); @@ -1077,7 +1077,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { addFormNumericBox(F("Maximum swipe margin"), F("swipemax"), Touch_Settings.swipeMargin, 5, 250); addUnit(F("5..250px")); - # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE } { addFormSubHeader(F("Touch objects")); @@ -1250,7 +1250,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { nullptr, get4BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTONTYPE), false, true, F("widenumber") # if TOUCH_FEATURE_TOOLTIPS - , F("Buttontype") + , F("Button") # endif // if TOUCH_FEATURE_TOOLTIPS ); html_TD(); // button alignment @@ -1261,7 +1261,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { nullptr, get4BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTONALIGN) << 4, false, true, F("widenumber") # if TOUCH_FEATURE_TOOLTIPS - , F("Button alignment") + , F("Layout") # endif // if TOUCH_FEATURE_TOOLTIPS ); # else // if TOUCH_FEATURE_EXTENDED_TOUCH @@ -1336,8 +1336,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { # if TOUCH_FEATURE_EXTENDED_TOUCH { # if TOUCH_FEATURE_TOOLTIPS - String buttonGroupToolTip = F("Button-group [0.."); - buttonGroupToolTip += TOUCH_MAX_BUTTON_GROUPS; + String buttonGroupToolTip(concat(F("Button-group [0.."), TOUCH_MAX_BUTTON_GROUPS)); buttonGroupToolTip += ']'; # endif // if TOUCH_FEATURE_TOOLTIPS addNumericBox(getPluginCustomArgName(objectNr + 1600), @@ -1509,13 +1508,13 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { config += TOUCH_SETTINGS_SEPARATOR; colorInput = webArg(getPluginCustomArgName(3005)); // Default Disabled Caption Color config += toStringNoZero(AdaGFXparseColor(colorInput, _colorDepth, false)); - # endif // if TOUCH_FEATURE_EXTENDED_TOUCH - # if TOUCH_FEATURE_SWIPE + # if TOUCH_FEATURE_SWIPE config += TOUCH_SETTINGS_SEPARATOR; config += toStringNoZero(getFormItemInt(F("swipemin"))); config += TOUCH_SETTINGS_SEPARATOR; config += toStringNoZero(getFormItemInt(F("swipemax"))); - # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH settingsArray[TOUCH_CALIBRATION_START] = config; saveSize += config.length() + 1; @@ -1636,8 +1635,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { if (loglevelActiveFor(LOG_LEVEL_INFO) && !config.isEmpty()) { - String log = F("Save object #"); - log += objectNr; + String log(concat(F("Save object #"), objectNr)); log += F(" settings: "); config.replace(TOUCH_SETTINGS_SEPARATOR, ','); log += config; @@ -1654,9 +1652,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { error = SaveCustomTaskSettings(event->TaskIndex, settingsArray, TOUCH_ARRAY_SIZE, 0); if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("TOUCH Save settings size: "); - log += saveSize; - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, concat(F("TOUCH Save settings size: "), saveSize)); } if (!error.isEmpty()) { @@ -1692,23 +1688,16 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, if (success && Touch_Settings.logEnabled && - loglevelActiveFor(LOG_LEVEL_INFO)) { // REQUIRED for calibration and setting up objects, so do not make this optional! + loglevelActiveFor(LOG_LEVEL_INFO)) { // REQUIRED for calibration and setting up objects, so do not make this optional! String log; log.reserve(72); - log = F("Touch calibration rx= "); // Space before the logged values for readability - log += rx; - log += F(", ry= "); - log += ry; - log += F("; z= "); // Always log the z value even if not used. - log += z; - log += F(", x= "); - log += x; - log += F(", y= "); - log += y; - log += F("; ox= "); - log += ox; - log += F(", oy= "); - log += oy; + log = concat(F("Touch calibration rx= "), rx); // Space before the logged values for readability + log += concat(F(", ry= "), ry); + log += concat(F("; z= "), z); // Always log the z value even if not used. + log += concat(F(", x= "), x); + log += concat(F(", y= "), y); + log += concat(F("; ox= "), ox); + log += concat(F(", oy= "), oy); addLogMove(LOG_LEVEL_INFO, log); } @@ -1735,7 +1724,7 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, int8_t selectedObjectIndex = -1; if (isValidAndTouchedTouchObject(x, y, selectedObjectName, selectedObjectIndex)) { - # if TOUCH_FEATURE_SWIPE + # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE int16_t delta_x = x - _last_point.x; int16_t delta_y = y - _last_point.y; @@ -1774,23 +1763,20 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("Touch Swiped, direction: "); - log += toString(swipe); - log += F(", dx: "); - log += delta_x; - log += F(", dy: "); - log += delta_y; + String log = concat(F("Touch Swiped, direction: "), toString(swipe)); + log += concat(F(", dx: "), delta_x); + log += concat(F(", dy: "), delta_y); addLogMove(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG } - # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE // Not touched yet or too long ago if ( - # if TOUCH_FEATURE_SWIPE + # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE (swipe == Swipe_action_e::None) && - # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE ((TouchObjects[selectedObjectIndex].TouchTimers == 0) || (TouchObjects[selectedObjectIndex].TouchTimers < (millis() - (1.5 * Touch_Settings.debounceMs))) )) { @@ -1800,20 +1786,20 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, // Debouncing time elapsed? Swiping/sliding passes through without debounce if ( - # if TOUCH_FEATURE_SWIPE + # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE (swipe != Swipe_action_e::None) || - # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE (TouchObjects[selectedObjectIndex].TouchTimers <= millis())) { TouchObjects[selectedObjectIndex].TouchTimers = 0; if ( - # if TOUCH_FEATURE_SWIPE + # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE (swipe == Swipe_action_e::None) && - # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE (selectedObjectIndex > -1) && bitRead(TouchObjects[selectedObjectIndex].flags, TOUCH_OBJECT_FLAG_BUTTON)) { // Button touched _lastObjectIndex = selectedObjectIndex; // Handle on release - # if TOUCH_FEATURE_SWIPE + # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE } else if ((swipe != Swipe_action_e::None) && (selectedObjectIndex > -1) && bitRead(TouchObjects[selectedObjectIndex].flags, TOUCH_OBJECT_FLAG_SLIDER)) { // Handle slider immediately to move/set absolute position @@ -1855,24 +1841,23 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, } else { _lastObjectIndex = selectedObjectIndex; // Update on touch-release } - # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE } else { // Generic touch event _lastObjectIndex = -2; // Update on touch-release _lastObjectName = selectedObjectName; - String log = F("Swiped/touched, object: "); - log += _lastObjectName; + String log(concat(F("Swiped/touched, object: "), _lastObjectName)); log += ':'; log += toString(swipe); - # if TOUCH_FEATURE_SWIPE + # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE if (swipe != Swipe_action_e::None) { _lastSwipe = swipe; } _last_delta_x = delta_x; _last_delta_y = delta_y; - # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE addLogMove(LOG_LEVEL_INFO, log); } } @@ -1906,10 +1891,10 @@ void ESPEasy_TouchHandler::releaseTouch(struct EventStruct *event) { eventCommand = getTaskDeviceName(event->TaskIndex); eventCommand += '#'; - # if TOUCH_FEATURE_SWIPE + # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE if (_lastSwipe == Swipe_action_e::None) - # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE { eventCommand += _lastObjectName; eventCommand += '='; // Add arguments @@ -1919,10 +1904,9 @@ void ESPEasy_TouchHandler::releaseTouch(struct EventStruct *event) { eventCommand += ','; eventCommand += _last_point_z.x; } - # if TOUCH_FEATURE_SWIPE + # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE else { - eventCommand += F("Swiped"); - eventCommand += '='; // Add arguments + eventCommand += F("Swiped="); // Add arguments eventCommand += static_cast(_lastSwipe); eventCommand += ','; eventCommand += _last_delta_x; @@ -1930,7 +1914,7 @@ void ESPEasy_TouchHandler::releaseTouch(struct EventStruct *event) { eventCommand += _last_delta_y; _lastSwipe = Swipe_action_e::None; } - # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE eventQueue.addMove(std::move(eventCommand)); _lastObjectIndex = -1; // Handle only once } @@ -1974,33 +1958,29 @@ bool ESPEasy_TouchHandler::plugin_write(struct EventStruct *event, arguments = parseString(string, arg); while (!arguments.isEmpty()) { - success |= setTouchObjectState(event, arguments, true); - arg++; - arguments = parseString(string, arg); + success |= setTouchObjectState(event, arguments, true); + arguments = parseString(string, ++arg); } } else if (subcommand.equals(F("disable"))) { // touch,disable,[,...] : Disable enabled objectname(s) arguments = parseString(string, arg); while (!arguments.isEmpty()) { - success |= setTouchObjectState(event, arguments, false); - arg++; - arguments = parseString(string, arg); + success |= setTouchObjectState(event, arguments, false); + arguments = parseString(string, ++arg); } } else if (subcommand.equals(F("on"))) { // touch,on,[,...] : Switch TouchButton(s) on arguments = parseString(string, arg); while (!arguments.isEmpty()) { - success |= setTouchButtonOnOff(event, arguments, true); - arg++; - arguments = parseString(string, arg); + success |= setTouchButtonOnOff(event, arguments, true); + arguments = parseString(string, ++arg); } } else if (subcommand.equals(F("off"))) { // touch,off,[,...] : Switch TouchButton(s) off arguments = parseString(string, arg); while (!arguments.isEmpty()) { - success |= setTouchButtonOnOff(event, arguments, false); - arg++; - arguments = parseString(string, arg); + success |= setTouchButtonOnOff(event, arguments, false); + arguments = parseString(string, ++arg); } } else if (subcommand.equals(F("toggle"))) { // touch,toggle,[,...] : Switch TouchButton(s) to the other state arguments = parseString(string, arg); @@ -2011,17 +1991,16 @@ bool ESPEasy_TouchHandler::plugin_write(struct EventStruct *event, if (state > -1) { success |= setTouchButtonOnOff(event, arguments, state == 0); } - arg++; - arguments = parseString(string, arg); + arguments = parseString(string, ++arg); } } else if (subcommand.equals(F("set"))) { // touch,set,, : Set TouchObject value arguments = parseString(string, arg); success = setTouchObjectValue(event, arguments, event->Par3); # if TOUCH_FEATURE_EXTENDED_TOUCH - # if TOUCH_FEATURE_SWIPE + # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE } else if (subcommand.equals(F("swipe"))) { // touch,swipe, : Switch button group via swipe value success = handleButtonSwipe(event, event->Par2); - # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE } else if (subcommand.equals(F("setgrp"))) { // touch,setgrp, : Activate button group success = setButtonGroup(event, event->Par2); } else if (subcommand.equals(F("incgrp"))) { // touch,incgrp : increment group and Activate @@ -2096,8 +2075,7 @@ bool ESPEasy_TouchHandler::plugin_get_config_value(struct EventStruct *event, } else if (command.equals(F("pagemode"))) { string = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); success = true; - # endif // if TOUCH_FEATURE_EXTENDED_TOUCH - # if TOUCH_FEATURE_SWIPE + # if TOUCH_FEATURE_SWIPE } else if (command.equals(F("swipedir"))) { int state; @@ -2105,7 +2083,8 @@ bool ESPEasy_TouchHandler::plugin_get_config_value(struct EventStruct *event, string = toString(static_cast(state)); success = true; } - # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } return success; } @@ -2317,9 +2296,7 @@ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, case Touch_action_e::Default: break; } - String log = F("TOUCH event: "); - log += toString(action); - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, concat(F("TOUCH event: "), toString(action))); } } } diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index 3f7718afe7..8361b3171c 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -11,6 +11,8 @@ /***** * Changelog: + * 2022-09-26 tonhuisman: Fix issue with touch-disable option. Code optimizations, improved log/string handling + * Make Swipe feature part of Extended Touch feature * 2022-08-27 tonhuisman: Enable identifying an object using the . notation, where groupnr = 0..255, and objectnr * range starts at 1, in sequential order for the objects in that group * Add option to disable polling the touch-screen, to be able to only use the object/button draw features @@ -64,7 +66,7 @@ * buttongroup : Get current buttongroup * hasgroup,groupNr : Check if group exists, ignores group 0 * enabled,objectName|objectNr : Check if object is enabled - * state,objectName|objectNr : Get current object state (buttons: on = 1, off = 0, sliders: value 0..100 (=percentage)) + * state,objectName|objectNr : Get current object state (buttons: on = 1, off = 0, sliders: value 0..100 (=percentage) or explicit value) * pagemode : Get the PageUp/PageDown mode, 0 = up=pgup, 1 = up=pgdown * swipedir,directionId : Get the name for the direction provided in numeric form */ @@ -238,15 +240,15 @@ struct tTouchObjects // Touch actions, max 16! enum class Touch_action_e : uint8_t { - Default = 0u, - ActivateGroup = 1u, - IncrementGroup = 2u, - DecrementGroup = 3u, - IncrementPage = 4u, - DecrementPage = 5u, + Default = 0u, + ActivateGroup = 1u, + IncrementGroup = 2u, + DecrementGroup = 3u, + IncrementPage = 4u, + DecrementPage = 5u, }; -# if TOUCH_FEATURE_SWIPE +# if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE // Swipe actions, start at 12 o'çlock, clock-wise enum class Swipe_action_e : uint8_t { @@ -261,13 +263,13 @@ enum class Swipe_action_e : uint8_t { LeftUp = 8u, SwipeAction_MAX = 9u // Last item is count }; -# endif // if TOUCH_FEATURE_SWIPE +# endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE const __FlashStringHelper* toString(Touch_action_e action); -# if TOUCH_FEATURE_SWIPE +# if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE const __FlashStringHelper* toString(Swipe_action_e action); -# endif // if TOUCH_FEATURE_SWIPE +# endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE class ESPEasy_TouchHandler { public: @@ -322,10 +324,10 @@ class ESPEasy_TouchHandler { # if TOUCH_FEATURE_EXTENDED_TOUCH bool validButtonGroup(const int16_t& group, const bool & ignoreZero = true); - # if TOUCH_FEATURE_SWIPE + # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE bool handleButtonSwipe(struct EventStruct *event, const int16_t & swipeValue); - # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE bool setButtonGroup(struct EventStruct *event, const int16_t & buttonGroup); bool incrementButtonGroup(struct EventStruct *event); @@ -342,7 +344,7 @@ class ESPEasy_TouchHandler { # endif // if TOUCH_FEATURE_EXTENDED_TOUCH bool touchEnabled() { - return _touchEnabled; + return !_touchIgnored; } private: @@ -370,18 +372,18 @@ class ESPEasy_TouchHandler { bool _settingsLoaded = false; bool _stillTouching = false; - bool _touchEnabled = true; + bool _touchIgnored = false; // Used to generate events on touch-release int8_t _lastObjectIndex = -1; String _lastObjectName; tTouch_Point _last_point; tTouch_Point _last_point_z; // Only used to store z in the x member - # if TOUCH_FEATURE_SWIPE + # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE Swipe_action_e _lastSwipe = Swipe_action_e::None; int16_t _last_delta_x; int16_t _last_delta_y; - # endif // if TOUCH_FEATURE_SWIPE + # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE struct tTouch_Globals { From 633f3fc4c9669cce89a8a06d85be6457b81a9365 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 26 Sep 2022 22:01:12 +0200 Subject: [PATCH 058/113] [P123] Code improvements --- src/_P123_FT62x6Touch.ino | 18 ++- src/src/PluginStructs/P123_data_struct.cpp | 145 +++++++++++++-------- 2 files changed, 105 insertions(+), 58 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index 648cf89610..86e3558963 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -6,6 +6,7 @@ /** * Changelog: + * 2022-09-26 tonhuisman: Add nullptr checks, improved log/string handling * 2022-08-15 tonhuisman: Add Swipe and Slider support (to TouchHandler) * 2022-08-15 tonhuisman: UI improvement, settings table uses alternate color per 2 rows, code improvements * 2022-06-10 tonhuisman: Remove p123_ prefixes on Settings variables @@ -105,6 +106,9 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) } case PLUGIN_WEBFORM_LOAD: { + #ifdef PLUGIN_123_DEBUG + addLogMove(LOG_LEVEL_INFO, F("P123 PLUGIN_WEBFORM_LOAD")); + #endif // ifdef PLUGIN_123_DEBUG { addRowLabel(F("Display task")); addTaskSelect(F("dsptask"), P123_CONFIG_DISPLAY_TASK); @@ -113,7 +117,7 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) #endif // ifndef P123_LIMIT_BUILD_SIZE } - uint16_t width_ = P123_CONFIG_X_RES; + uint16_t width_ = P123_CONFIG_X_RES; uint16_t height_ = P123_CONFIG_Y_RES; uint16_t rotation_ = P123_CONFIG_ROTATION; uint16_t colorDepth_ = P123_COLOR_DEPTH; @@ -141,8 +145,8 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) addFormNumericBox(F("Touch minimum pressure"), F("threshold"), P123_CONFIG_THRESHOLD, 0, 255); { + P123_data_struct *P123_data = nullptr; // static_cast(getPluginTaskData(event->TaskIndex)); bool deleteP123_data = false; - P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); if (nullptr == P123_data) { P123_data = new (std::nothrow) P123_data_struct(); @@ -163,6 +167,9 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SAVE: { + #ifdef PLUGIN_123_DEBUG + addLogMove(LOG_LEVEL_INFO, F("P123 PLUGIN_WEBFORM_SAVE")); + #endif // ifdef PLUGIN_123_DEBUG P123_CONFIG_DISPLAY_PREV = P123_CONFIG_DISPLAY_TASK; P123_CONFIG_THRESHOLD = getFormItemInt(F("threshold")); P123_CONFIG_DISPLAY_TASK = getFormItemInt(F("dsptask")); @@ -177,7 +184,7 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) } { - P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); + P123_data_struct *P123_data = nullptr; // static_cast(getPluginTaskData(event->TaskIndex)); bool deleteP123_data = false; if (nullptr == P123_data) { @@ -199,6 +206,9 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_INIT: { + #ifdef PLUGIN_123_DEBUG + addLogMove(LOG_LEVEL_INFO, F("P123 PLUGIN_INIT")); + #endif // ifdef PLUGIN_123_DEBUG initPluginTaskData(event->TaskIndex, new (std::nothrow) P123_data_struct()); P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); @@ -208,7 +218,7 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) success = true; - if (!(P123_data->init(event))) { + if (!P123_data->init(event)) { delete P123_data; success = false; } diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index cc6d727c46..d686982b46 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -34,9 +34,10 @@ void P123_data_struct::reset() { addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen reset.")); # endif // PLUGIN_123_DEBUG - delete touchscreen; + if (nullptr != touchscreen) { delete touchscreen; } touchscreen = nullptr; - delete touchHandler; + + if (nullptr != touchHandler) { delete touchHandler; } touchHandler = nullptr; } @@ -52,7 +53,8 @@ bool P123_data_struct::init(struct EventStruct *event) { touchscreen = new (std::nothrow) Adafruit_FT6206(); - if (touchscreen != nullptr) { + if (nullptr != touchscreen) { + if (nullptr != touchHandler) { delete touchHandler; } touchHandler = new (std::nothrow) ESPEasy_TouchHandler(P123_CONFIG_DISPLAY_TASK, static_cast(P123_COLOR_DEPTH)); } @@ -62,11 +64,11 @@ bool P123_data_struct::init(struct EventStruct *event) { touchHandler->init(event); - # ifdef PLUGIN_123_DEBUG + # ifdef PLUGIN_123_DEBUG addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Plugin & touchscreen initialized.")); } else { addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen initialization FAILED.")); - # endif // PLUGIN_123_DEBUG + # endif // PLUGIN_123_DEBUG } return isInitialized(); } @@ -78,7 +80,10 @@ void P123_data_struct::displayButtonGroup(struct EventStruct *event, int16_t buttonGroup, int8_t mode) { # if TOUCH_FEATURE_EXTENDED_TOUCH - touchHandler->displayButtonGroup(event, buttonGroup, mode); + + if (nullptr != touchHandler) { + touchHandler->displayButtonGroup(event, buttonGroup, mode); + } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } @@ -90,10 +95,12 @@ bool P123_data_struct::displayButton(struct EventStruct *event, int16_t buttonGroup, int8_t mode) { # if TOUCH_FEATURE_EXTENDED_TOUCH - return touchHandler->displayButton(event, buttonNr, buttonGroup, mode); - # else // if TOUCH_FEATURE_EXTENDED_TOUCH - return false; + + if (nullptr != touchHandler) { + return touchHandler->displayButton(event, buttonNr, buttonGroup, mode); + } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + return false; } /** @@ -107,14 +114,20 @@ bool P123_data_struct::isInitialized() const { * Load the settings onto the webpage */ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { - return touchHandler->plugin_webform_load(event); + if (nullptr != touchHandler) { + return touchHandler->plugin_webform_load(event); + } + return false; } /** * Save the settings from the web page to flash */ bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { - return touchHandler->plugin_webform_save(event); + if (nullptr != touchHandler) { + return touchHandler->plugin_webform_save(event); + } + return false; } /** @@ -129,18 +142,14 @@ bool P123_data_struct::plugin_write(struct EventStruct *event, command = parseString(string, 1); subcommand = parseString(string, 2); - if (command.equals(F("touch"))) { + if (isInitialized() && command.equals(F("touch"))) { # ifdef PLUGIN_123_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("P123 WRITE arguments Par1:"); - log += event->Par1; - log += F(", 2: "); - log += event->Par2; - log += F(", 3: "); - log += event->Par3; - log += F(", 4: "); - log += event->Par4; + String log(concat(F("P123 WRITE arguments Par1:"), event->Par1)); + log += concat(F(", 2: "), event->Par2); + log += concat(F(", 3: "), event->Par3); + log += concat(F(", 4: "), event->Par4); addLog(LOG_LEVEL_INFO, log); } # endif // ifdef PLUGIN_123_DEBUG @@ -162,7 +171,7 @@ bool P123_data_struct::plugin_write(struct EventStruct *event, * Every 1/50th second we check if the screen is touched */ bool P123_data_struct::plugin_fifty_per_second(struct EventStruct *event) { - if (isInitialized()) { + if (isInitialized() && touchHandler->touchEnabled()) { if (touched()) { int16_t x = 0; int16_t y = 0; @@ -188,14 +197,19 @@ bool P123_data_struct::plugin_fifty_per_second(struct EventStruct *event) { */ bool P123_data_struct::plugin_get_config_value(struct EventStruct *event, String & string) { - return touchHandler->plugin_get_config_value(event, string); + if (nullptr != touchHandler) { + return touchHandler->plugin_get_config_value(event, string); + } + return false; } /** * Load the touch objects from the settings, and initialize then properly where needed. */ void P123_data_struct::loadTouchObjects(struct EventStruct *event) { - touchHandler->loadTouchObjects(event); + if (nullptr != touchHandler) { + touchHandler->loadTouchObjects(event); + } } /** @@ -203,7 +217,7 @@ void P123_data_struct::loadTouchObjects(struct EventStruct *event) { */ bool P123_data_struct::touched() { if (isInitialized()) { - return touchHandler->touchEnabled() && touchscreen->touched(); + return touchscreen->touched(); } return false; } @@ -277,9 +291,7 @@ void P123_data_struct::setRotation(uint8_t n) { # ifdef PLUGIN_123_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("P123 DEBUG Rotation set: "); - log += n; - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, concat(F("P123 DEBUG Rotation set: "), n)); } # endif // PLUGIN_123_DEBUG } @@ -292,9 +304,7 @@ void P123_data_struct::setRotationFlipped(bool flipped) { # ifdef PLUGIN_123_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("P123 DEBUG RotationFlipped set: "); - log += flipped; - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, concat(F("P123 DEBUG RotationFlipped set: "), flipped)); } # endif // PLUGIN_123_DEBUG } @@ -308,7 +318,10 @@ bool P123_data_struct::isValidAndTouchedTouchObject(const int16_t& x, const int16_t& y, String & selectedObjectName, int8_t & selectedObjectIndex) { - return touchHandler->isValidAndTouchedTouchObject(x, y, selectedObjectName, selectedObjectIndex); + if (nullptr != touchHandler) { + return touchHandler->isValidAndTouchedTouchObject(x, y, selectedObjectName, selectedObjectIndex); + } + return false; } /** @@ -317,7 +330,10 @@ bool P123_data_struct::isValidAndTouchedTouchObject(const int16_t& x, int8_t P123_data_struct::getTouchObjectIndex(struct EventStruct *event, const String & touchObject, bool isButton) { - return touchHandler->getTouchObjectIndex(event, touchObject, isButton); + if (nullptr != touchHandler) { + return touchHandler->getTouchObjectIndex(event, touchObject, isButton); + } + return false; } /** @@ -326,7 +342,10 @@ int8_t P123_data_struct::getTouchObjectIndex(struct EventStruct *event, bool P123_data_struct::setTouchObjectState(struct EventStruct *event, const String & touchObject, bool state) { - return touchHandler->setTouchObjectState(event, touchObject, state); + if (nullptr != touchHandler) { + return touchHandler->setTouchObjectState(event, touchObject, state); + } + return false; } /** @@ -335,7 +354,10 @@ bool P123_data_struct::setTouchObjectState(struct EventStruct *event, bool P123_data_struct::setTouchButtonOnOff(struct EventStruct *event, const String & touchObject, bool state) { - return touchHandler->setTouchButtonOnOff(event, touchObject, state); + if (nullptr != touchHandler) { + return touchHandler->setTouchButtonOnOff(event, touchObject, state); + } + return false; } /** @@ -343,7 +365,7 @@ bool P123_data_struct::setTouchButtonOnOff(struct EventStruct *event, */ void P123_data_struct::scaleRawToCalibrated(int16_t& x, int16_t& y) { - if (touchHandler->isCalibrationActive()) { + if ((nullptr != touchHandler) && touchHandler->isCalibrationActive()) { int16_t lx = x - touchHandler->Touch_Settings.top_left.x; if (lx <= 0) { @@ -374,7 +396,10 @@ void P123_data_struct::scaleRawToCalibrated(int16_t& x, * Get the current button group */ int16_t P123_data_struct::getButtonGroup() { - return touchHandler->getButtonGroup(); + if (nullptr != touchHandler) { + return touchHandler->getButtonGroup(); + } + return 0; } /** @@ -383,10 +408,12 @@ int16_t P123_data_struct::getButtonGroup() { bool P123_data_struct::validButtonGroup(int16_t buttonGroup, bool ignoreZero) { # if TOUCH_FEATURE_EXTENDED_TOUCH - return touchHandler->validButtonGroup(buttonGroup, ignoreZero); - # else // if TOUCH_FEATURE_EXTENDED_TOUCH - return false; + + if (nullptr != touchHandler) { + return touchHandler->validButtonGroup(buttonGroup, ignoreZero); + } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + return false; } /** @@ -395,10 +422,12 @@ bool P123_data_struct::validButtonGroup(int16_t buttonGroup, bool P123_data_struct::setButtonGroup(struct EventStruct *event, int16_t buttonGroup) { # if TOUCH_FEATURE_EXTENDED_TOUCH - return touchHandler->setButtonGroup(event, buttonGroup); - # else // if TOUCH_FEATURE_EXTENDED_TOUCH - return false; + + if (nullptr != touchHandler) { + return touchHandler->setButtonGroup(event, buttonGroup); + } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + return false; } /** @@ -406,10 +435,12 @@ bool P123_data_struct::setButtonGroup(struct EventStruct *event, */ bool P123_data_struct::incrementButtonGroup(struct EventStruct *event) { # if TOUCH_FEATURE_EXTENDED_TOUCH - return touchHandler->incrementButtonGroup(event); - # else // if TOUCH_FEATURE_EXTENDED_TOUCH - return false; + + if (nullptr != touchHandler) { + return touchHandler->incrementButtonGroup(event); + } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + return false; } /** @@ -417,10 +448,12 @@ bool P123_data_struct::incrementButtonGroup(struct EventStruct *event) { */ bool P123_data_struct::decrementButtonGroup(struct EventStruct *event) { # if TOUCH_FEATURE_EXTENDED_TOUCH - return touchHandler->decrementButtonGroup(event); - # else // if TOUCH_FEATURE_EXTENDED_TOUCH - return false; + + if (nullptr != touchHandler) { + return touchHandler->decrementButtonGroup(event); + } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + return false; } /** @@ -428,10 +461,12 @@ bool P123_data_struct::decrementButtonGroup(struct EventStruct *event) { */ bool P123_data_struct::incrementButtonPage(struct EventStruct *event) { # if TOUCH_FEATURE_EXTENDED_TOUCH - return touchHandler->incrementButtonPage(event); - # else // if TOUCH_FEATURE_EXTENDED_TOUCH - return false; + + if (nullptr != touchHandler) { + return touchHandler->incrementButtonPage(event); + } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + return false; } /** @@ -439,10 +474,12 @@ bool P123_data_struct::incrementButtonPage(struct EventStruct *event) { */ bool P123_data_struct::decrementButtonPage(struct EventStruct *event) { # if TOUCH_FEATURE_EXTENDED_TOUCH - return touchHandler->decrementButtonPage(event); - # else // if TOUCH_FEATURE_EXTENDED_TOUCH - return false; + + if (nullptr != touchHandler) { + return touchHandler->decrementButtonPage(event); + } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + return false; } #endif // ifdef USES_P123 From 7e22143db4c493902c25a09279f93358e33cd234 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 26 Sep 2022 22:23:33 +0200 Subject: [PATCH 059/113] [TouchHandler] Conditional compilation fix --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 8fd3ca1661..87e21dfa41 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -1846,11 +1846,13 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, _lastObjectIndex = -2; // Update on touch-release _lastObjectName = selectedObjectName; + # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE + # if TOUCH_DEBUG String log(concat(F("Swiped/touched, object: "), _lastObjectName)); log += ':'; log += toString(swipe); - - # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE + addLogMove(LOG_LEVEL_INFO, log); + # endif // if TOUCH_DEBUG if (swipe != Swipe_action_e::None) { _lastSwipe = swipe; @@ -1858,7 +1860,6 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, _last_delta_x = delta_x; _last_delta_y = delta_y; # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE - addLogMove(LOG_LEVEL_INFO, log); } } _last_point.x = x; // Save last touchpoint From 41a1f95b88c981b6157d490b6bbde01c0fc4b410 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 27 Sep 2022 20:21:45 +0200 Subject: [PATCH 060/113] [TouchHandler] Conditional compilation fix --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 87e21dfa41..4aff05970d 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -1847,12 +1847,12 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, _lastObjectName = selectedObjectName; # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE - # if TOUCH_DEBUG + # ifdef TOUCH_DEBUG String log(concat(F("Swiped/touched, object: "), _lastObjectName)); log += ':'; log += toString(swipe); addLogMove(LOG_LEVEL_INFO, log); - # endif // if TOUCH_DEBUG + # endif // ifdef TOUCH_DEBUG if (swipe != Swipe_action_e::None) { _lastSwipe = swipe; From 25dbfa0f4ab752b1b5f826ddd4bd9c2e9824797d Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 2 Oct 2022 19:40:04 +0200 Subject: [PATCH 061/113] [Boards] Add BOARD_HAS_PSRAM flag for 16M8M config (avoids crashing when saving P123 settings) --- boards/esp32_16M8M.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boards/esp32_16M8M.json b/boards/esp32_16M8M.json index 0189738f45..27394967cc 100644 --- a/boards/esp32_16M8M.json +++ b/boards/esp32_16M8M.json @@ -5,7 +5,7 @@ "memory_type": "dio_qspi" }, "core": "esp32", - "extra_flags": "-DARDUINO_ESP32_DEV -DARDUINO_USB_CDC_ON_BOOT=0 -DESP32_16M", + "extra_flags": "-DARDUINO_ESP32_DEV -DBOARD_HAS_PSRAM -DARDUINO_USB_CDC_ON_BOOT=0 -DESP32_16M", "f_cpu": "240000000L", "f_flash": "40000000L", "flash_mode": "dio", From aded83c00e7c4912ceabed49781683b7afd010c7 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 2 Oct 2022 19:41:40 +0200 Subject: [PATCH 062/113] [TouchHandler] Logging and code optimizations --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 119 +++++++++++++---------- src/src/Helpers/ESPEasy_TouchHandler.h | 4 +- 2 files changed, 68 insertions(+), 55 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 4aff05970d..05b1e29930 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -51,6 +51,11 @@ ESPEasy_TouchHandler::ESPEasy_TouchHandler(const taskIndex_t & displayTask, const AdaGFXColorDepth& colorDepth) : _displayTask(displayTask), _colorDepth(colorDepth) {} +/** + * Destructor + */ +ESPEasy_TouchHandler::~ESPEasy_TouchHandler() {} + /** * Load the touch objects from the settings, and initialize them properly where needed. */ @@ -227,10 +232,8 @@ void ESPEasy_TouchHandler::init(struct EventStruct *event) { # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("TOUCH DEBUG group: "); - log += _buttonGroup; - log += F(", max group: "); - log += *_buttonGroups.crbegin(); + String log = concat(F("TOUCH DEBUG group: "), static_cast(_buttonGroup)); + log += concat(F(", max group: "), static_cast(*_buttonGroups.crbegin())); addLogMove(LOG_LEVEL_INFO, log); } # endif // ifdef TOUCH_DEBUG @@ -313,7 +316,9 @@ bool ESPEasy_TouchHandler::isValidAndTouchedTouchObject(const int16_t& x, # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("TOUCH DEBUG Touched: obj: "); + String log; + log.reserve(72); + log = F("TOUCH DEBUG Touched: obj: "); log += TouchObjects[objectNr].objectName; log += ','; log += TouchObjects[objectNr].top_left.x; @@ -435,7 +440,9 @@ bool ESPEasy_TouchHandler::setTouchObjectState(struct EventStruct *event, # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("TOUCH setTouchObjectState: obj: "); + String log; + log.reserve(72); + log += F("TOUCH setTouchObjectState: obj: "); log += touchObject; log += '/'; log += objectNr; @@ -499,7 +506,9 @@ bool ESPEasy_TouchHandler::setTouchButtonOnOff(struct EventStruct *event, # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("TOUCH setTouchButtonOnOff: obj: "); + String log; + log.reserve(72); + log += F("TOUCH setTouchButtonOnOff: obj: "); log += touchObject; log += '/'; log += objectNr; @@ -584,12 +593,13 @@ bool ESPEasy_TouchHandler::setTouchObjectValue(struct EventStruct *event, # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("TOUCH setTouchObjectValue: obj: "); + String log; + log.reserve(72); + log += F("TOUCH setTouchObjectValue: obj: "); log += touchObject; log += '/'; log += objectNr; - log += F(", new value: "); - log += _value; + log += concat(F(", new value: "), static_cast(_value)); addLogMove(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG @@ -720,18 +730,15 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("TOUCH: button init, state: "); - log += state; - log += F(", group: "); - log += buttonGroup; - log += F(", mode: "); - log += mode; - log += F(", group: "); - log += get8BitFromUL(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_GROUP); + String log; + log.reserve(90); + log = concat(F("TOUCH: button init, state: "), static_cast(state)); + log += concat(F(", group: "), static_cast(buttonGroup)); + log += concat(F(", mode: "), static_cast(mode)); + log += concat(F(", group: "), static_cast(get8BitFromUL(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_GROUP))); log += F(", en: "); log += bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_BUTTON); - log += F(", object: "); - log += buttonNr; + log += concat(F(", object: "), static_cast(buttonNr)); addLog(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG @@ -1336,7 +1343,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { # if TOUCH_FEATURE_EXTENDED_TOUCH { # if TOUCH_FEATURE_TOOLTIPS - String buttonGroupToolTip(concat(F("Button-group [0.."), TOUCH_MAX_BUTTON_GROUPS)); + String buttonGroupToolTip(concat(F("Button-group [0.."), static_cast(TOUCH_MAX_BUTTON_GROUPS))); buttonGroupToolTip += ']'; # endif // if TOUCH_FEATURE_TOOLTIPS addNumericBox(getPluginCustomArgName(objectNr + 1600), @@ -1447,6 +1454,9 @@ String toStringNoZero(int64_t value) { * Save the settings from the web page to flash */ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { + # ifdef TOUCH_DEBUG + addLogMove(LOG_LEVEL_INFO, F("TOUCH DEBUG webform_save start.")); + # endif // ifdef TOUCH_DEBUG String config; uint16_t saveSize = 0; @@ -1522,10 +1532,8 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("Save settings: "); config.replace(TOUCH_SETTINGS_SEPARATOR, ','); - log += config; - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, concat(F("Save settings: "), config)); } # endif // ifdef TOUCH_DEBUG @@ -1541,8 +1549,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { if (!ExtraTaskSettings.checkInvalidCharInNames(config.c_str()) || numStart) { // Check for invalid characters in objectname - error += F("Invalid character in objectname #"); - error += objectNr + 1; + error += concat(F("Invalid character in objectname #"), objectNr + 1); error += F(". "); error += numStart ? F("Should not start with a digit.\n") : F("Do not use ',-+/*=^%!#[]{}()' or space.\n"); } @@ -1612,8 +1619,8 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { config += TOUCH_SETTINGS_SEPARATOR; config += ull2String(groupFlags); // Group Flags # endif // if TOUCH_FEATURE_EXTENDED_TOUCH + config.trim(); } - config.trim(); String endZero; // Trim off and 0 from the end endZero += TOUCH_SETTINGS_SEPARATOR; @@ -1635,10 +1642,11 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { if (loglevelActiveFor(LOG_LEVEL_INFO) && !config.isEmpty()) { - String log(concat(F("Save object #"), objectNr)); - log += F(" settings: "); config.replace(TOUCH_SETTINGS_SEPARATOR, ','); - log += config; + String log; + log.reserve(config.length() + 32); + log = concat(F("Save object #"), objectNr); + log += concat(F(" settings: "), config); addLogMove(LOG_LEVEL_INFO, log); } # endif // ifdef TOUCH_DEBUG @@ -1652,7 +1660,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { error = SaveCustomTaskSettings(event->TaskIndex, settingsArray, TOUCH_ARRAY_SIZE, 0); if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("TOUCH Save settings size: "), saveSize)); + addLogMove(LOG_LEVEL_INFO, concat(F("TOUCH Save settings size: "), static_cast(saveSize))); } if (!error.isEmpty()) { @@ -1688,16 +1696,18 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, if (success && Touch_Settings.logEnabled && - loglevelActiveFor(LOG_LEVEL_INFO)) { // REQUIRED for calibration and setting up objects, so do not make this optional! + + // REQUIRED for calibration and setting up objects, so do not make this optional! + loglevelActiveFor(LOG_LEVEL_INFO)) { String log; log.reserve(72); - log = concat(F("Touch calibration rx= "), rx); // Space before the logged values for readability - log += concat(F(", ry= "), ry); - log += concat(F("; z= "), z); // Always log the z value even if not used. - log += concat(F(", x= "), x); - log += concat(F(", y= "), y); - log += concat(F("; ox= "), ox); - log += concat(F(", oy= "), oy); + log = concat(F("Touch calibration rx= "), static_cast(rx)); // Space before the logged values for readability + log += concat(F(", ry= "), static_cast(ry)); + log += concat(F("; z= "), static_cast(z)); // Always log the z value even if not used. + log += concat(F(", x= "), static_cast(x)); + log += concat(F(", y= "), static_cast(y)); + log += concat(F("; ox= "), static_cast(ox)); + log += concat(F(", oy= "), static_cast(oy)); addLogMove(LOG_LEVEL_INFO, log); } @@ -1763,9 +1773,12 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = concat(F("Touch Swiped, direction: "), toString(swipe)); - log += concat(F(", dx: "), delta_x); - log += concat(F(", dy: "), delta_y); + String log; + log.reserve(72); + log += F("Touch Swiped, direction: "); + log += toString(swipe); + log += concat(F(", dx: "), static_cast(delta_x)); + log += concat(F(", dy: "), static_cast(delta_y)); addLogMove(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG @@ -1848,7 +1861,10 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE # ifdef TOUCH_DEBUG - String log(concat(F("Swiped/touched, object: "), _lastObjectName)); + String log; + log.reserve(72); + log += F("Swiped/touched, object: "); + log += _lastObjectName; log += ':'; log += toString(swipe); addLogMove(LOG_LEVEL_INFO, log); @@ -1941,16 +1957,13 @@ bool ESPEasy_TouchHandler::plugin_write(struct EventStruct *event, # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("TOUCH PLUGIN_WRITE arguments Par1:"); - log += event->Par1; - log += F(", 2: "); - log += event->Par2; - log += F(", 3: "); - log += event->Par3; - log += F(", 4: "); - log += event->Par4; - log += F(", string: "); - log += string; + String log; + log.reserve(90); + log = concat(F("TOUCH PLUGIN_WRITE arguments Par1:"), event->Par1); + log += concat(F(", 2: "), event->Par2); + log += concat(F(", 3: "), event->Par3); + log += concat(F(", 4: "), event->Par4); + log += concat(F(", string: "), string); addLog(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index 8361b3171c..7dab81e8bd 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -277,7 +277,7 @@ class ESPEasy_TouchHandler { ESPEasy_TouchHandler(); ESPEasy_TouchHandler(const taskIndex_t & displayTask, const AdaGFXColorDepth& colorDepth); - virtual ~ESPEasy_TouchHandler() {} + virtual ~ESPEasy_TouchHandler(); void loadTouchObjects(struct EventStruct *event); void init(struct EventStruct *event); @@ -364,7 +364,7 @@ class ESPEasy_TouchHandler { int16_t & highRange); bool _deduplicate = false; - uint16_t _displayTask = 0u; + taskIndex_t _displayTask = INVALID_TASK_INDEX; AdaGFXColorDepth _colorDepth = AdaGFXColorDepth::FullColor; int16_t _buttonGroup = 0; From 58f77850935aaaaf04c82549ffab9ca10721715f Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 2 Oct 2022 19:42:46 +0200 Subject: [PATCH 063/113] [P123] Bugfixes and logging improvements --- src/_P123_FT62x6Touch.ino | 2 +- src/src/PluginStructs/P123_data_struct.cpp | 28 ++++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index 86e3558963..1aa2afc514 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -145,7 +145,7 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) addFormNumericBox(F("Touch minimum pressure"), F("threshold"), P123_CONFIG_THRESHOLD, 0, 255); { - P123_data_struct *P123_data = nullptr; // static_cast(getPluginTaskData(event->TaskIndex)); + P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); bool deleteP123_data = false; if (nullptr == P123_data) { diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index d686982b46..465d86b692 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -51,18 +51,22 @@ bool P123_data_struct::init(struct EventStruct *event) { reset(); - touchscreen = new (std::nothrow) Adafruit_FT6206(); + touchHandler = new (std::nothrow) ESPEasy_TouchHandler(static_cast(P123_CONFIG_DISPLAY_TASK), + static_cast(P123_COLOR_DEPTH)); - if (nullptr != touchscreen) { - if (nullptr != touchHandler) { delete touchHandler; } - touchHandler = new (std::nothrow) ESPEasy_TouchHandler(P123_CONFIG_DISPLAY_TASK, - static_cast(P123_COLOR_DEPTH)); - } + if (nullptr != touchHandler) { + touchHandler->init(event); - if (isInitialized()) { - touchscreen->begin(P123_CONFIG_THRESHOLD); + if (touchHandler->touchEnabled()) { + touchscreen = new (std::nothrow) Adafruit_FT6206(); - touchHandler->init(event); + if (nullptr != touchscreen) { + if (!touchscreen->begin(P123_CONFIG_THRESHOLD)) { + delete touchscreen; + touchscreen = nullptr; + } + } + } # ifdef PLUGIN_123_DEBUG addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Plugin & touchscreen initialized.")); @@ -70,7 +74,7 @@ bool P123_data_struct::init(struct EventStruct *event) { addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen initialization FAILED.")); # endif // PLUGIN_123_DEBUG } - return isInitialized(); + return nullptr != touchHandler; } /** @@ -291,7 +295,7 @@ void P123_data_struct::setRotation(uint8_t n) { # ifdef PLUGIN_123_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("P123 DEBUG Rotation set: "), n)); + addLogMove(LOG_LEVEL_INFO, concat(F("P123 DEBUG Rotation set: "), static_cast(n))); } # endif // PLUGIN_123_DEBUG } @@ -304,7 +308,7 @@ void P123_data_struct::setRotationFlipped(bool flipped) { # ifdef PLUGIN_123_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("P123 DEBUG RotationFlipped set: "), flipped)); + addLogMove(LOG_LEVEL_INFO, concat(F("P123 DEBUG RotationFlipped set: "), boolToString(flipped))); } # endif // PLUGIN_123_DEBUG } From ab8bf908b3d7683e74854b1805c1e062d4a45817 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 3 Oct 2022 21:15:56 +0200 Subject: [PATCH 064/113] [P123] Fix for disabled touchpanel setting --- src/src/PluginStructs/P123_data_struct.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index 465d86b692..b7e907227a 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -69,12 +69,12 @@ bool P123_data_struct::init(struct EventStruct *event) { } # ifdef PLUGIN_123_DEBUG - addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Plugin & touchscreen initialized.")); + addLogMove(LOG_LEVEL_INFO, concat(F("P123 DEBUG Plugin"), nullptr != touchscreen ? F(" & touchscreen") : F("")) + F(" initialized.")); } else { addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen initialization FAILED.")); # endif // PLUGIN_123_DEBUG } - return nullptr != touchHandler; + return isInitialized(); } /** @@ -111,7 +111,7 @@ bool P123_data_struct::displayButton(struct EventStruct *event, * Properly initialized? then true */ bool P123_data_struct::isInitialized() const { - return touchscreen != nullptr && touchHandler != nullptr; + return touchHandler != nullptr && (!touchHandler->touchEnabled() || touchscreen != nullptr); } /** From 9729c4a3312edf8ef0306bf0cca4bfc8c2bf204a Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 26 Oct 2022 21:25:34 +0200 Subject: [PATCH 065/113] [Lib FT6206] Add support for FT6336 touch panel (as used in M5Stack Core2) --- lib/Adafruit_FT6206_Library/Adafruit_FT6206.cpp | 15 ++++++++++++--- lib/Adafruit_FT6206_Library/Adafruit_FT6206.h | 4 +++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/Adafruit_FT6206_Library/Adafruit_FT6206.cpp b/lib/Adafruit_FT6206_Library/Adafruit_FT6206.cpp index 15cd463e74..0b02ac897b 100644 --- a/lib/Adafruit_FT6206_Library/Adafruit_FT6206.cpp +++ b/lib/Adafruit_FT6206_Library/Adafruit_FT6206.cpp @@ -85,9 +85,9 @@ boolean Adafruit_FT6206::begin(uint8_t thresh) { if (readRegister8(FT62XX_REG_VENDID) != FT62XX_VENDID) { return false; } - uint8_t id = readRegister8(FT62XX_REG_CHIPID); - if ((id != FT6206_CHIPID) && (id != FT6236_CHIPID) && - (id != FT6236U_CHIPID)) { + _id = readRegister8(FT62XX_REG_CHIPID); + if ((_id != FT6206_CHIPID) && (_id != FT6236_CHIPID) && + (_id != FT6236U_CHIPID) && (_id != FT5316_CHIPID)) { return false; } @@ -101,6 +101,15 @@ boolean Adafruit_FT6206::begin(uint8_t thresh) { */ /**************************************************************************/ uint8_t Adafruit_FT6206::touched(void) { + if (_id == FT5316_CHIPID) { + uint8_t val = readRegister8(FT62XX_REG_MODE); + if (val) { + // wrong mode + val = 0; + writeRegister8(FT62XX_REG_MODE, val); + } + } + uint8_t n = readRegister8(FT62XX_REG_NUMTOUCHES); if (n > 2) { n = 0; diff --git a/lib/Adafruit_FT6206_Library/Adafruit_FT6206.h b/lib/Adafruit_FT6206_Library/Adafruit_FT6206.h index 7ad9de077a..e66f9a3937 100644 --- a/lib/Adafruit_FT6206_Library/Adafruit_FT6206.h +++ b/lib/Adafruit_FT6206_Library/Adafruit_FT6206.h @@ -25,10 +25,11 @@ #define FT62XX_REG_CHIPID 0xA3 //!< Chip selecting #define FT62XX_REG_VENDID 0xA8 //!< FocalTech's panel ID -#define FT62XX_VENDID 0x11 //!< FocalTech's panel ID +#define FT62XX_VENDID 0x11 //!< FocalTech's vendor ID #define FT6206_CHIPID 0x06 //!< Chip selecting #define FT6236_CHIPID 0x36 //!< Chip selecting #define FT6236U_CHIPID 0x64 //!< Chip selecting +#define FT5316_CHIPID 0x0A //!< Chip selecting // calibrated for Adafruit 2.8" ctp screen #define FT62XX_DEFAULT_THRESHOLD 128 //!< Default threshold for touch detection @@ -73,6 +74,7 @@ class Adafruit_FT6206 { void readData(void); uint8_t touches; + uint8_t _id = 0; uint16_t touchX[2], touchY[2], touchID[2]; }; From 4ce8b7f4ceacf60f7b73b31092b08be71c14a83a Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 26 Oct 2022 21:38:11 +0200 Subject: [PATCH 066/113] [P123] Correct merge error --- platformio_core_defs.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio_core_defs.ini b/platformio_core_defs.ini index ec4926d797..f92445a571 100644 --- a/platformio_core_defs.ini +++ b/platformio_core_defs.ini @@ -245,7 +245,7 @@ build_flags = -DESP32_STAGE -Wswitch -include "ESPEasy_config.h" [core_esp32_stage] -platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.4.1/platform-espressif32-2.0.4.1.zip +platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.5.1/platform-espressif32-2.0.5.1.zip platform_packages = build_flags = -DESP32_STAGE -Wswitch -DESP_IDF_VERSION_MAJOR=4 From 20740c1b9a6c0e8ce178afc13d5c50b3e5831fd8 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 4 Dec 2022 16:41:22 +0100 Subject: [PATCH 067/113] [P123] De-initialize on error during PLUGIN_INIT fixed --- src/_P123_FT62x6Touch.ino | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index 1aa2afc514..176fade211 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -6,6 +6,7 @@ /** * Changelog: + * 2022-12-04 tonhuisman: Remove [Testing] tag from plugin name * 2022-09-26 tonhuisman: Add nullptr checks, improved log/string handling * 2022-08-15 tonhuisman: Add Swipe and Slider support (to TouchHandler) * 2022-08-15 tonhuisman: UI improvement, settings table uses alternate color per 2 rows, code improvements @@ -39,7 +40,7 @@ #define PLUGIN_123 #define PLUGIN_ID_123 123 -#define PLUGIN_NAME_123 "Touch - FT62x6 touchscreen [TESTING]" +#define PLUGIN_NAME_123 "Touch - FT62x6 touchscreen" #define PLUGIN_VALUENAME1_123 "X" #define PLUGIN_VALUENAME2_123 "Y" #define PLUGIN_VALUENAME3_123 "Z" @@ -219,7 +220,7 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) success = true; if (!P123_data->init(event)) { - delete P123_data; + clearPluginTaskData(event->TaskIndex); success = false; } break; From b70f2c3e10c6ac0f1f22f938c175511a6617da1c Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 15 Aug 2023 21:03:50 +0200 Subject: [PATCH 068/113] [TouchHandler][P123] Implement Extended CustomTaskSettings feature --- src/_P123_FT62x6Touch.ino | 33 +++++----- src/src/Helpers/ESPEasy_TouchHandler.cpp | 82 ++++++++++-------------- src/src/Helpers/ESPEasy_TouchHandler.h | 23 ++++--- 3 files changed, 67 insertions(+), 71 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index 176fade211..24359ba62d 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -1,3 +1,4 @@ +#include "_Plugin_Helper.h" #ifdef USES_P123 // ####################################################################################################### @@ -6,6 +7,7 @@ /** * Changelog: + * 2023-08-15 tonhuisman: Implement Extended CustomTaskSettings * 2022-12-04 tonhuisman: Remove [Testing] tag from plugin name * 2022-09-26 tonhuisman: Add nullptr checks, improved log/string handling * 2022-08-15 tonhuisman: Add Swipe and Slider support (to TouchHandler) @@ -38,15 +40,14 @@ * Other commands: see ESPEasy_TouchHandler.h */ -#define PLUGIN_123 -#define PLUGIN_ID_123 123 -#define PLUGIN_NAME_123 "Touch - FT62x6 touchscreen" -#define PLUGIN_VALUENAME1_123 "X" -#define PLUGIN_VALUENAME2_123 "Y" -#define PLUGIN_VALUENAME3_123 "Z" +# define PLUGIN_123 +# define PLUGIN_ID_123 123 +# define PLUGIN_NAME_123 "Touch - FT62x6 touchscreen" +# define PLUGIN_VALUENAME1_123 "X" +# define PLUGIN_VALUENAME2_123 "Y" +# define PLUGIN_VALUENAME3_123 "Z" -#include "_Plugin_Helper.h" -#include "src/PluginStructs/P123_data_struct.h" +# include "src/PluginStructs/P123_data_struct.h" boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) @@ -107,15 +108,15 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) } case PLUGIN_WEBFORM_LOAD: { - #ifdef PLUGIN_123_DEBUG + # ifdef PLUGIN_123_DEBUG addLogMove(LOG_LEVEL_INFO, F("P123 PLUGIN_WEBFORM_LOAD")); - #endif // ifdef PLUGIN_123_DEBUG + # endif // ifdef PLUGIN_123_DEBUG { addRowLabel(F("Display task")); addTaskSelect(F("dsptask"), P123_CONFIG_DISPLAY_TASK); - #ifndef P123_LIMIT_BUILD_SIZE + # ifndef P123_LIMIT_BUILD_SIZE addFormNote(F("Screen Width, Heigth, Rotation & Color-depth will be fetched from the Display task if possible.")); - #endif // ifndef P123_LIMIT_BUILD_SIZE + # endif // ifndef P123_LIMIT_BUILD_SIZE } uint16_t width_ = P123_CONFIG_X_RES; @@ -168,9 +169,9 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SAVE: { - #ifdef PLUGIN_123_DEBUG + # ifdef PLUGIN_123_DEBUG addLogMove(LOG_LEVEL_INFO, F("P123 PLUGIN_WEBFORM_SAVE")); - #endif // ifdef PLUGIN_123_DEBUG + # endif // ifdef PLUGIN_123_DEBUG P123_CONFIG_DISPLAY_PREV = P123_CONFIG_DISPLAY_TASK; P123_CONFIG_THRESHOLD = getFormItemInt(F("threshold")); P123_CONFIG_DISPLAY_TASK = getFormItemInt(F("dsptask")); @@ -207,9 +208,9 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_INIT: { - #ifdef PLUGIN_123_DEBUG + # ifdef PLUGIN_123_DEBUG addLogMove(LOG_LEVEL_INFO, F("P123 PLUGIN_INIT")); - #endif // ifdef PLUGIN_123_DEBUG + # endif // ifdef PLUGIN_123_DEBUG initPluginTaskData(event->TaskIndex, new (std::nothrow) P123_data_struct()); P123_data_struct *P123_data = static_cast(getPluginTaskData(event->TaskIndex)); diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 05b1e29930..1ee0826842 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -85,9 +85,11 @@ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { TOUCH_CALIBRATION_LOG_ENABLED, TOUCH_SETTINGS_SEPARATOR) == 1; int lSettings = 0; - bitWrite(lSettings, TOUCH_FLAGS_SEND_XY, TOUCH_TS_SEND_XY); - bitWrite(lSettings, TOUCH_FLAGS_SEND_Z, TOUCH_TS_SEND_Z); - bitWrite(lSettings, TOUCH_FLAGS_SEND_OBJECTNAME, TOUCH_TS_SEND_OBJECTNAME); + bitWrite(lSettings, TOUCH_FLAGS_SEND_XY, TOUCH_TS_SEND_XY); // Defaults initialized + bitWrite(lSettings, TOUCH_FLAGS_SEND_Z, TOUCH_TS_SEND_Z); + bitWrite(lSettings, TOUCH_FLAGS_SEND_OBJECTNAME, TOUCH_TS_SEND_OBJECTNAME); + bitWrite(lSettings, TOUCH_FLAGS_DEDUPLICATE, TOUCH_TS_DEDUPLICATE); + bitWrite(lSettings, TOUCH_FLAGS_INIT_OBJECTEVENT, TOUCH_TS_INIT_OBJECTEVENT); Touch_Settings.flags = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], TOUCH_COMMON_FLAGS, TOUCH_SETTINGS_SEPARATOR, lSettings); Touch_Settings.top_left.x = parseStringToInt(settingsArray[TOUCH_CALIBRATION_START], TOUCH_CALIBRATION_TOP_X, TOUCH_SETTINGS_SEPARATOR); @@ -120,10 +122,10 @@ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { (Touch_Settings.colorBorder == 0u) && (Touch_Settings.colorDisabled == 0u) && (Touch_Settings.colorDisabledCaption == 0u)) { - Touch_Settings.colorOn = ADAGFX_BLUE; - Touch_Settings.colorOff = ADAGFX_RED; - Touch_Settings.colorCaption = ADAGFX_WHITE; - Touch_Settings.colorBorder = ADAGFX_WHITE; + Touch_Settings.colorOn = TOUCH_DEFAULT_COLOR_ON; + Touch_Settings.colorOff = TOUCH_DEFAULT_COLOR_OFF; + Touch_Settings.colorCaption = TOUCH_DEFAULT_COLOR_CAPTION; + Touch_Settings.colorBorder = TOUCH_DEFAULT_COLOR_BORDER; Touch_Settings.colorDisabled = TOUCH_DEFAULT_COLOR_DISABLED; Touch_Settings.colorDisabledCaption = TOUCH_DEFAULT_COLOR_DISABLED_CAPTION; } @@ -362,10 +364,11 @@ int8_t ESPEasy_TouchHandler::getTouchObjectIndex(struct EventStruct *event, if ((idx = touchObject.indexOf('.')) > -1) { String part = touchObject.substring(0, idx); - int grp = -1; # if TOUCH_FEATURE_EXTENDED_TOUCH + int grp = -1; + if (validIntFromString(part, grp) && validButtonGroup(static_cast(grp), false)) { part = touchObject.substring(idx + 1); int btn = -1; @@ -449,7 +452,7 @@ bool ESPEasy_TouchHandler::setTouchObjectState(struct EventStruct *event, if (success) { log += F(", new state: "); - log += (state ? F("en") : F("dis")); + log += state ? F("en") : F("dis"); log += F("abled."); } else { log += F(" failed!"); @@ -513,7 +516,7 @@ bool ESPEasy_TouchHandler::setTouchButtonOnOff(struct EventStruct *event, log += '/'; log += objectNr; log += F(", new state: "); - log += (state ? F("on") : F("off")); + log += state ? F("on") : F("off"); addLogMove(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG @@ -875,7 +878,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { addFormSubHeader(F("Touch configuration")); - addFormCheckBox(F("Flip rotation 180°"), F("rotation_flipped"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_ROTATION_FLIPPED)); + addFormCheckBox(F("Flip rotation 180°"), F("rot_flip"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_ROTATION_FLIPPED)); # ifndef LIMIT_BUILD_SIZE addFormNote(F("Some touchscreens are mounted 180° rotated on the display.")); # endif // ifndef LIMIT_BUILD_SIZE @@ -904,16 +907,16 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { const int optionValues3[TOUCH_EVENTS_OPTIONS] = { 0, 1, 3, 4, 5, 7 }; // Already used as a bitmap! addFormSelector(F("Events"), F("events"), TOUCH_EVENTS_OPTIONS, options3, optionValues3, choice3); - addFormCheckBox(F("Draw buttons when started"), F("init_objectevent"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)); + addFormCheckBox(F("Draw buttons when started"), F("initobj"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)); # ifndef LIMIT_BUILD_SIZE addFormNote(F("Needs Objectnames 'Events' to be enabled.")); # endif // ifndef LIMIT_BUILD_SIZE } - addFormCheckBox(F("Prevent duplicate events"), F("deduplicate"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_DEDUPLICATE)); + addFormCheckBox(F("Prevent duplicate events"), F("dedupe"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_DEDUPLICATE)); # if TOUCH_FEATURE_EXTENDED_TOUCH - addFormCheckBox(F("Ignore touch-screen"), F("ignoretouch"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_IGNORE_TOUCH)); + addFormCheckBox(F("Ignore touch-screen"), F("igntouch"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_IGNORE_TOUCH)); # ifndef LIMIT_BUILD_SIZE addFormNote(F("To enable the use of touch-object display-functions only.")); # endif // ifndef LIMIT_BUILD_SIZE @@ -932,7 +935,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { const __FlashStringHelper *noYesOptions[2] = { F("No"), F("Yes") }; const int noYesOptionValues[2] = { 0, 1 }; addFormSelector(F("Calibrate to screen resolution"), - F("use_calibration"), + F("usecalib"), 2, noYesOptions, noYesOptionValues, @@ -978,7 +981,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { html_end_table(); } - addFormCheckBox(F("Enable logging for calibration"), F("log_calibration"), + addFormCheckBox(F("Enable logging for calibration"), F("logcalib"), Touch_Settings.logEnabled); addFormSubHeader(F("Object settings")); @@ -1055,7 +1058,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { html_end_table(); } { - addFormNumericBox(F("Initial button group"), F("initial_group"), + addFormNumericBox(F("Initial button group"), F("initgrp"), get8BitFromUL(Touch_Settings.flags, TOUCH_FLAGS_INITIAL_GROUP), 0, TOUCH_MAX_BUTTON_GROUPS # if TOUCH_FEATURE_TOOLTIPS , F("Initial group") @@ -1063,11 +1066,11 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { ); addFormCheckBox(F("Draw buttons via Rules"), F("via_rules"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_DRAWBTN_VIA_RULES)); - addFormCheckBox(F("Enable/Disable page buttons"), F("page_buttons"), + addFormCheckBox(F("Enable/Disable page buttons"), F("pagebtns"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_AUTO_PAGE_ARROWS)); - addFormCheckBox(F("PageUp/PageDown reversed"), F("page_below"), + addFormCheckBox(F("PageUp/PageDown reversed"), F("pageblw"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU)); - addFormCheckBox(F("Swipe Left/Right/Up/Down menu reversed"), F("swipe_swap"), + addFormCheckBox(F("Swipe Left/Right/Up/Down menu reversed"), F("swipeswap"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_SWAP_LEFT_RIGHT)); } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH @@ -1439,17 +1442,6 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { return false; } -/** - * Helper: Convert an integer to string, but return an empty string for 0, to save a little space in settings - */ -String toStringNoZero(int64_t value) { - if (value != 0) { - return toString(value, 0); - } else { - return EMPTY_STRING; - } -} - /** * Save the settings from the web page to flash */ @@ -1472,21 +1464,21 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { bitWrite(lSettings, TOUCH_FLAGS_SEND_XY, bitRead(eventsValue, TOUCH_FLAGS_SEND_XY)); bitWrite(lSettings, TOUCH_FLAGS_SEND_Z, bitRead(eventsValue, TOUCH_FLAGS_SEND_Z)); bitWrite(lSettings, TOUCH_FLAGS_SEND_OBJECTNAME, bitRead(eventsValue, TOUCH_FLAGS_SEND_OBJECTNAME)); - bitWrite(lSettings, TOUCH_FLAGS_ROTATION_FLIPPED, isFormItemChecked(F("rotation_flipped"))); - bitWrite(lSettings, TOUCH_FLAGS_DEDUPLICATE, isFormItemChecked(F("deduplicate"))); - bitWrite(lSettings, TOUCH_FLAGS_INIT_OBJECTEVENT, isFormItemChecked(F("init_objectevent"))); + bitWrite(lSettings, TOUCH_FLAGS_ROTATION_FLIPPED, isFormItemChecked(F("rot_flip"))); + bitWrite(lSettings, TOUCH_FLAGS_DEDUPLICATE, isFormItemChecked(F("dedupe"))); + bitWrite(lSettings, TOUCH_FLAGS_INIT_OBJECTEVENT, isFormItemChecked(F("initobj"))); # if TOUCH_FEATURE_EXTENDED_TOUCH - set8BitToUL(lSettings, TOUCH_FLAGS_INITIAL_GROUP, getFormItemInt(F("initial_group"))); // Button group + set8BitToUL(lSettings, TOUCH_FLAGS_INITIAL_GROUP, getFormItemInt(F("initgrp"))); // Button group bitWrite(lSettings, TOUCH_FLAGS_DRAWBTN_VIA_RULES, isFormItemChecked(F("via_rules"))); - bitWrite(lSettings, TOUCH_FLAGS_AUTO_PAGE_ARROWS, isFormItemChecked(F("page_buttons"))); - bitWrite(lSettings, TOUCH_FLAGS_PGUP_BELOW_MENU, isFormItemChecked(F("page_below"))); - bitWrite(lSettings, TOUCH_FLAGS_SWAP_LEFT_RIGHT, isFormItemChecked(F("swipe_swap"))); - bitWrite(lSettings, TOUCH_FLAGS_IGNORE_TOUCH, isFormItemChecked(F("ignoretouch"))); + bitWrite(lSettings, TOUCH_FLAGS_AUTO_PAGE_ARROWS, isFormItemChecked(F("pagebtns"))); + bitWrite(lSettings, TOUCH_FLAGS_PGUP_BELOW_MENU, isFormItemChecked(F("pageblw"))); + bitWrite(lSettings, TOUCH_FLAGS_SWAP_LEFT_RIGHT, isFormItemChecked(F("swipeswap"))); + bitWrite(lSettings, TOUCH_FLAGS_IGNORE_TOUCH, isFormItemChecked(F("igntouch"))); # endif // if TOUCH_FEATURE_EXTENDED_TOUCH - config += getFormItemInt(F("use_calibration")); // First value should NEVER be empty, or parseString() wil get confused + config += getFormItemInt(F("usecalib")); // First value should NEVER be empty, or parseString() wil get confused config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(isFormItemChecked(F("log_calibration")) ? 1 : 0); + config += toStringNoZero(isFormItemChecked(F("logcalib")) ? 1 : 0); config += TOUCH_SETTINGS_SEPARATOR; config += toStringNoZero(getFormItemInt(F("cal_tl_x"))); config += TOUCH_SETTINGS_SEPARATOR; @@ -1643,11 +1635,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { if (loglevelActiveFor(LOG_LEVEL_INFO) && !config.isEmpty()) { config.replace(TOUCH_SETTINGS_SEPARATOR, ','); - String log; - log.reserve(config.length() + 32); - log = concat(F("Save object #"), objectNr); - log += concat(F(" settings: "), config); - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, strformat(F("Save touch object #%d settings: %s"), objectNr, config)); } # endif // ifdef TOUCH_DEBUG } @@ -1660,7 +1648,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { error = SaveCustomTaskSettings(event->TaskIndex, settingsArray, TOUCH_ARRAY_SIZE, 0); if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("TOUCH Save settings size: "), static_cast(saveSize))); + addLogMove(LOG_LEVEL_INFO, strformat(F("TOUCH: Save settings, size: %d"), saveSize)); } if (!error.isEmpty()) { diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index 7dab81e8bd..5f55fbf607 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -11,6 +11,7 @@ /***** * Changelog: + * 2023-08-15 tonhuisman: Implement Extended CustomTaskSettings, minor improvements * 2022-09-26 tonhuisman: Fix issue with touch-disable option. Code optimizations, improved log/string handling * Make Swipe feature part of Extended Touch feature * 2022-08-27 tonhuisman: Enable identifying an object using the . notation, where groupnr = 0..255, and objectnr @@ -122,26 +123,28 @@ # define TOUCH_VALUE_Y UserVar[event->BaseVarIndex + 1] # define TOUCH_VALUE_Z UserVar[event->BaseVarIndex + 2] -# define TOUCH_TS_ROTATION 0 // Rotation 0-3 = 0/90/180/270 degrees +# define TOUCH_TS_ROTATION 0 // Rotation 0-3 = 0/90/180/270 degrees # define TOUCH_TS_SEND_XY true // Enable/Disable X/Y events # define TOUCH_TS_SEND_Z false // Disable/Enable Z events # define TOUCH_TS_SEND_OBJECTNAME true // Enable/Disable objectname events # define TOUCH_TS_USE_CALIBRATION false // Disable/Enable calibration # define TOUCH_TS_LOG_CALIBRATION true // Enable/Disable calibration logging # define TOUCH_TS_ROTATION_FLIPPED false // Enable/Disable rotation flipped 180 deg. +# define TOUCH_TS_DEDUPLICATE true // Enable/Disable deduplication of events +# define TOUCH_TS_INIT_OBJECTEVENT true // Enable/Disable drawing of touch objects # define TOUCH_TS_X_RES 320 // Pixels, should match with the screen it is mounted on # define TOUCH_TS_Y_RES 480 -# define TOUCH_DEBOUNCE_MILLIS 100 // Debounce delay for On/Off button function +# define TOUCH_DEBOUNCE_MILLIS 50 // Debounce delay for On/Off button function # define TOUCH_DEF_SWIPE_MINIMAL 3 // Minimal swipe pixels # define TOUCH_DEF_SWIPE_MARGIN 10 // Default swipe margin -# define TOUCH_MAX_COLOR_INPUTLENGTH 11 // 11 Characters is enough to type in all recognized color names and values -# define TOUCH_MaxObjectNameLength 15 // 15 character objectnames -# define TOUCH_MaxCaptionNameLength 30 // 30 character captions, to allow variable names -# define TOUCH_MAX_CALIBRATION_COUNT 1 // -# define TOUCH_MAX_OBJECT_COUNT 40 // This count of touchobjects should be enough, because of limited +# define TOUCH_MAX_COLOR_INPUTLENGTH 11 // 11 Characters is enough to type in all recognized color names and values +# define TOUCH_MaxObjectNameLength 15 // 15 character objectnames +# define TOUCH_MaxCaptionNameLength 30 // 30 character captions, to allow variable names +# define TOUCH_MAX_CALIBRATION_COUNT 1 // +# define TOUCH_MAX_OBJECT_COUNT 40 // This count of touchobjects should be enough, because of limited // settings storage, 1024 bytes -# define TOUCH_EXTRA_OBJECT_COUNT 5 // The number of empty objects to show if max not reached +# define TOUCH_EXTRA_OBJECT_COUNT 5 // The number of empty objects to show if max not reached # define TOUCH_ARRAY_SIZE (TOUCH_MAX_OBJECT_COUNT + TOUCH_MAX_CALIBRATION_COUNT) # define TOUCH_MAX_BUTTON_GROUPS 255 // Max. allowed button groups @@ -205,6 +208,10 @@ # define TOUCH_OBJECT_GROUP_ACTIONGROUP 8 // 8 bits used as action group # define TOUCH_OBJECT_GROUP_ACTION 16 // 4 bits used as action option +# define TOUCH_DEFAULT_COLOR_ON ADAGFX_BLUE +# define TOUCH_DEFAULT_COLOR_OFF ADAGFX_RED +# define TOUCH_DEFAULT_COLOR_CAPTION ADAGFX_WHITE +# define TOUCH_DEFAULT_COLOR_BORDER ADAGFX_WHITE # define TOUCH_DEFAULT_COLOR_DISABLED 0x9410 # define TOUCH_DEFAULT_COLOR_DISABLED_CAPTION 0x5A69 From e5a671e73d4b46c5787caceac3dda835a01c6b68 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 30 Sep 2023 20:08:21 +0200 Subject: [PATCH 069/113] [P123][TouchHandler] Fix merge issues, adjust log strings --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 128 +++++++++------------ src/src/PluginStructs/P123_data_struct.cpp | 14 +-- 2 files changed, 59 insertions(+), 83 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 1ee0826842..ad0ecb8dac 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -61,7 +61,7 @@ ESPEasy_TouchHandler::~ESPEasy_TouchHandler() {} */ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { # ifdef TOUCH_DEBUG - addLogMove(LOG_LEVEL_INFO, F("TOUCH DEBUG loadTouchObjects")); + addLog(LOG_LEVEL_INFO, F("TOUCH DEBUG loadTouchObjects")); # endif // TOUCH_DEBUG LoadCustomTaskSettings(event->TaskIndex, settingsArray, TOUCH_ARRAY_SIZE, 0); @@ -234,16 +234,14 @@ void ESPEasy_TouchHandler::init(struct EventStruct *event) { # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = concat(F("TOUCH DEBUG group: "), static_cast(_buttonGroup)); - log += concat(F(", max group: "), static_cast(*_buttonGroups.crbegin())); - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, strformat(F("TOUCH DEBUG group: %d, max group: %d"), _buttonGroup, *_buttonGroups.crbegin())); } # endif // ifdef TOUCH_DEBUG displayButtonGroup(event, _buttonGroup); // Initialize selected group and group 0 # ifdef TOUCH_DEBUG - addLogMove(LOG_LEVEL_INFO, F("TOUCH DEBUG group done.")); + addLog(LOG_LEVEL_INFO, F("TOUCH DEBUG group done.")); # endif // ifdef TOUCH_DEBUG } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH @@ -320,28 +318,18 @@ bool ESPEasy_TouchHandler::isValidAndTouchedTouchObject(const int16_t& x, if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log; log.reserve(72); - log = F("TOUCH DEBUG Touched: obj: "); - log += TouchObjects[objectNr].objectName; - log += ','; - log += TouchObjects[objectNr].top_left.x; - log += ','; - log += TouchObjects[objectNr].top_left.y; - log += ','; - log += TouchObjects[objectNr].width_height.x; - log += ','; - log += TouchObjects[objectNr].width_height.y; - log += F(" surface:"); - log += TouchObjects[objectNr].SurfaceAreas; - log += F(" x,y:"); - log += x; - log += ','; - log += y; - log += F(" sel:"); - log += selectedObjectName; - log += '/'; - log += selectedObjectIndex; - log += '/'; - log += selected ? 'T' : 'f'; + log = strformat(F("TOUCH DEBUG Touched: obj: %s,%d,%d,%d,%d surface:%d x,y:%d,%d sel:%s/%d/%c"), + TouchObjects[objectNr].objectName.c_str(), + TouchObjects[objectNr].top_left.x, + TouchObjects[objectNr].top_left.y, + TouchObjects[objectNr].width_height.x, + TouchObjects[objectNr].width_height.y, + TouchObjects[objectNr].SurfaceAreas, + x, + y, + selectedObjectName.c_str(), + selectedObjectIndex, + selected ? 'T' : 'f'); addLogMove(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG @@ -445,10 +433,8 @@ bool ESPEasy_TouchHandler::setTouchObjectState(struct EventStruct *event, if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log; log.reserve(72); - log += F("TOUCH setTouchObjectState: obj: "); - log += touchObject; - log += '/'; - log += objectNr; + log = strformat(F("TOUCH setTouchObjectState: obj: %s/%d"), + touchObject.c_str(), objectNr); if (success) { log += F(", new state: "); @@ -511,11 +497,8 @@ bool ESPEasy_TouchHandler::setTouchButtonOnOff(struct EventStruct *event, if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log; log.reserve(72); - log += F("TOUCH setTouchButtonOnOff: obj: "); - log += touchObject; - log += '/'; - log += objectNr; - log += F(", new state: "); + log = strformat(F("TOUCH setTouchButtonOnOff: obj: %s/%d, new state: "), + touchObject.c_str(), objectNr); log += state ? F("on") : F("off"); addLogMove(LOG_LEVEL_DEBUG, log); } @@ -598,11 +581,8 @@ bool ESPEasy_TouchHandler::setTouchObjectValue(struct EventStruct *event, if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log; log.reserve(72); - log += F("TOUCH setTouchObjectValue: obj: "); - log += touchObject; - log += '/'; - log += objectNr; - log += concat(F(", new value: "), static_cast(_value)); + log = strformat(F("TOUCH setTouchObjectValue: obj: %s/%d, new value: %d"), + touchObject.c_str(), objectNr, _value); addLogMove(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG @@ -735,14 +715,14 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log; log.reserve(90); - log = concat(F("TOUCH: button init, state: "), static_cast(state)); - log += concat(F(", group: "), static_cast(buttonGroup)); - log += concat(F(", mode: "), static_cast(mode)); - log += concat(F(", group: "), static_cast(get8BitFromUL(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_GROUP))); - log += F(", en: "); - log += bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_BUTTON); - log += concat(F(", object: "), static_cast(buttonNr)); - addLog(LOG_LEVEL_DEBUG, log); + log = strformat(F("TOUCH: button init, state: %d, group: %d, mode: %d, group: %d, en: %d, object: %d"), + state, + buttonGroup, + mode, + get8BitFromUL(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_GROUP), + bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_BUTTON), + buttonNr); + addLogMove(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG return true; @@ -1447,7 +1427,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { */ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { # ifdef TOUCH_DEBUG - addLogMove(LOG_LEVEL_INFO, F("TOUCH DEBUG webform_save start.")); + addLog(LOG_LEVEL_INFO, F("TOUCH DEBUG webform_save start.")); # endif // ifdef TOUCH_DEBUG String config; @@ -1635,7 +1615,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { if (loglevelActiveFor(LOG_LEVEL_INFO) && !config.isEmpty()) { config.replace(TOUCH_SETTINGS_SEPARATOR, ','); - addLogMove(LOG_LEVEL_INFO, strformat(F("Save touch object #%d settings: %s"), objectNr, config)); + addLogMove(LOG_LEVEL_INFO, strformat(F("Save touch object #%d settings: %s"), objectNr, config.c_str())); } # endif // ifdef TOUCH_DEBUG } @@ -1685,18 +1665,11 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, if (success && Touch_Settings.logEnabled && - // REQUIRED for calibration and setting up objects, so do not make this optional! + // Thisi log is REQUIRED for calibration and setting up objects, so do not make this optional! loglevelActiveFor(LOG_LEVEL_INFO)) { - String log; - log.reserve(72); - log = concat(F("Touch calibration rx= "), static_cast(rx)); // Space before the logged values for readability - log += concat(F(", ry= "), static_cast(ry)); - log += concat(F("; z= "), static_cast(z)); // Always log the z value even if not used. - log += concat(F(", x= "), static_cast(x)); - log += concat(F(", y= "), static_cast(y)); - log += concat(F("; ox= "), static_cast(ox)); - log += concat(F(", oy= "), static_cast(oy)); - addLogMove(LOG_LEVEL_INFO, log); + // Space before the logged values for readability. Always log the z value even if not used. + addLogMove(LOG_LEVEL_INFO, strformat(F("Touch calibration rx= %d, ry= %d; z= %d, x= %d, y= %d; ox= %d, oy= %d"), + rx, ry, z, x, y, ox, oy)); } // No events to handle if rules not enabled @@ -1706,14 +1679,26 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, // Do NOT send a Z event for each touch? if (!bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z) && validDeviceIndex(DeviceIndex)) { + // FIXME TD-er: Should not change anything in the Device vector. + # ifdef ESP8266 Device[DeviceIndex].VType = Sensor_VType::SENSOR_TYPE_DUAL; Device[DeviceIndex].ValueCount = 2; + # else // ifdef ESP8266 + Device.getDeviceStructForEdit(DeviceIndex).VType = Sensor_VType::SENSOR_TYPE_DUAL; + Device.getDeviceStructForEdit(DeviceIndex).ValueCount = 2; + # endif // ifdef ESP8266 } sendData(event); // Send X/Y(/Z) event if (!bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z) && validDeviceIndex(DeviceIndex)) { // Reset device configuration + // FIXME TD-er: Should not change anything in the Device vector. + # ifdef ESP8266 Device[DeviceIndex].VType = Sensor_VType::SENSOR_TYPE_TRIPLE; Device[DeviceIndex].ValueCount = 3; + # else // ifdef ESP8266 + Device.getDeviceStructForEdit(DeviceIndex).VType = Sensor_VType::SENSOR_TYPE_TRIPLE; + Device.getDeviceStructForEdit(DeviceIndex).ValueCount = 3; + # endif // ifdef ESP8266 } } @@ -1763,10 +1748,8 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log; log.reserve(72); - log += F("Touch Swiped, direction: "); - log += toString(swipe); - log += concat(F(", dx: "), static_cast(delta_x)); - log += concat(F(", dy: "), static_cast(delta_y)); + log += strformat(F("Touch Swiped, direction: %s, dx: %d, dy: %d"), + String(toString(swipe)).c_str(), delta_x, delta_y); addLogMove(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG @@ -1851,9 +1834,7 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, # ifdef TOUCH_DEBUG String log; log.reserve(72); - log += F("Swiped/touched, object: "); - log += _lastObjectName; - log += ':'; + log += strformat(F("Swiped/touched, object: %s:"), _lastObjectName.c_str()); log += toString(swipe); addLogMove(LOG_LEVEL_INFO, log); # endif // ifdef TOUCH_DEBUG @@ -1947,12 +1928,9 @@ bool ESPEasy_TouchHandler::plugin_write(struct EventStruct *event, if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { String log; log.reserve(90); - log = concat(F("TOUCH PLUGIN_WRITE arguments Par1:"), event->Par1); - log += concat(F(", 2: "), event->Par2); - log += concat(F(", 3: "), event->Par3); - log += concat(F(", 4: "), event->Par4); - log += concat(F(", string: "), string); - addLog(LOG_LEVEL_DEBUG, log); + log = strformat(F("TOUCH PLUGIN_WRITE arguments Par1: %d, 2: %d, 3: %d, 4: %d, string: %s"), + event->Par1, event->Par2, event->Par3, event->Par4, string.c_str()); + addLogMove(LOG_LEVEL_DEBUG, log); } # endif // ifdef TOUCH_DEBUG diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index b7e907227a..c2c7508a30 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -31,7 +31,7 @@ P123_data_struct::~P123_data_struct() { */ void P123_data_struct::reset() { # ifdef PLUGIN_123_DEBUG - addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen reset.")); + addLog(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen reset.")); # endif // PLUGIN_123_DEBUG if (nullptr != touchscreen) { delete touchscreen; } @@ -69,7 +69,8 @@ bool P123_data_struct::init(struct EventStruct *event) { } # ifdef PLUGIN_123_DEBUG - addLogMove(LOG_LEVEL_INFO, concat(F("P123 DEBUG Plugin"), nullptr != touchscreen ? F(" & touchscreen") : F("")) + F(" initialized.")); + addLogMove(LOG_LEVEL_INFO, + concat(concat(F("P123 DEBUG Plugin"), nullptr != touchscreen ? F(" & touchscreen") : F("")), F(" initialized."))); } else { addLogMove(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen initialization FAILED.")); # endif // PLUGIN_123_DEBUG @@ -150,11 +151,8 @@ bool P123_data_struct::plugin_write(struct EventStruct *event, # ifdef PLUGIN_123_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log(concat(F("P123 WRITE arguments Par1:"), event->Par1)); - log += concat(F(", 2: "), event->Par2); - log += concat(F(", 3: "), event->Par3); - log += concat(F(", 4: "), event->Par4); - addLog(LOG_LEVEL_INFO, log); + addLog(LOG_LEVEL_INFO, strformat(F("P123 WRITE arguments Par1: %d, 2: %d, 3: %d, 4: %d"), + event->Par1, event->Par2, event->Par3, event->Par4)); } # endif // ifdef PLUGIN_123_DEBUG @@ -295,7 +293,7 @@ void P123_data_struct::setRotation(uint8_t n) { # ifdef PLUGIN_123_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("P123 DEBUG Rotation set: "), static_cast(n))); + addLogMove(LOG_LEVEL_INFO, strformat(F("P123 DEBUG Rotation set: %d"), _rotation)); } # endif // PLUGIN_123_DEBUG } From 98ae0ae36166dcc48c350d1dbe56d4497f7dc45b Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 30 Sep 2023 20:55:22 +0200 Subject: [PATCH 070/113] [TouchHandler] Fix typo --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index ad0ecb8dac..c75bfe1086 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -1665,7 +1665,7 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, if (success && Touch_Settings.logEnabled && - // Thisi log is REQUIRED for calibration and setting up objects, so do not make this optional! + // This log is REQUIRED for calibration and setting up objects, so do not make this optional! loglevelActiveFor(LOG_LEVEL_INFO)) { // Space before the logged values for readability. Always log the z value even if not used. addLogMove(LOG_LEVEL_INFO, strformat(F("Touch calibration rx= %d, ry= %d; z= %d, x= %d, y= %d; ox= %d, oy= %d"), From e99942eb04796a0b21f92a992add472f3a3a6512 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 1 Oct 2023 21:03:21 +0200 Subject: [PATCH 071/113] [P123][TouchHandler] Fix valuecount issue, implement missing `GET_I2C_ADDRESS` function --- src/_P123_FT62x6Touch.ino | 24 ++++++++++- src/src/Helpers/ESPEasy_TouchHandler.cpp | 46 ++++++++++------------ src/src/Helpers/ESPEasy_TouchHandler.h | 3 ++ src/src/PluginStructs/P123_data_struct.cpp | 4 +- src/src/PluginStructs/P123_data_struct.h | 2 + 5 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index 24359ba62d..d4656c7eac 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -2,11 +2,14 @@ #ifdef USES_P123 // ####################################################################################################### -// #################################### Plugin 123: FT6206 Touchscreen ################################### +// #################################### Plugin 123: FT62x6 Touchscreen ################################### // ####################################################################################################### /** * Changelog: + * 2023-10-01 tonhuisman: Re-implement (fix) switching of X/Y/Z vs X/Y output values using PLUGIN_GET_DEVICEVALUECOUNT, store (also) in task + * settings for speed + * Implement PLUGIN_I2C_GET_ADDRESS function * 2023-08-15 tonhuisman: Implement Extended CustomTaskSettings * 2022-12-04 tonhuisman: Remove [Testing] tag from plugin name * 2022-09-26 tonhuisman: Add nullptr checks, improved log/string handling @@ -91,7 +94,7 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_I2C_HAS_ADDRESS: { - success = (event->Par1 == 0x38); // Fixed I2C address + success = (event->Par1 == P123_I2C_ADDRESS); // Fixed I2C address break; } @@ -106,6 +109,23 @@ boolean Plugin_123(uint8_t function, struct EventStruct *event, String& string) success = true; break; } + + # if FEATURE_I2C_GET_ADDRESS + case PLUGIN_I2C_GET_ADDRESS: + { + event->Par1 = P123_I2C_ADDRESS; + success = true; + break; + } + # endif // if FEATURE_I2C_GET_ADDRESS + + case PLUGIN_GET_DEVICEVALUECOUNT: + { + event->Par1 = P123_CONFIG_VTYPE; + success = true; + break; + } + case PLUGIN_WEBFORM_LOAD: { # ifdef PLUGIN_123_DEBUG diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index c75bfe1086..ec05dc5461 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -847,6 +847,21 @@ bool ESPEasy_TouchHandler::decrementButtonPage(struct EventStruct *event) { # endif // if TOUCH_FEATURE_EXTENDED_TOUCH +/** + * Get the PLUGIN_GET_DEVICEVTYPE, based on the user-selected setting for including Z-axis + */ +uint8_t ESPEasy_TouchHandler::get_device_valuecount(struct EventStruct *event) { + if (!_settingsLoaded) { + loadTouchObjects(event); + _settingsLoaded = true; + } + + if (!bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z)) { + return getValueCountFromSensorType(Sensor_VType::SENSOR_TYPE_DUAL); + } + return getValueCountFromSensorType(Sensor_VType::SENSOR_TYPE_TRIPLE); +} + /** * Load the settings onto the webpage */ @@ -1653,9 +1668,10 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, const int16_t & z) { bool success = false; - // Avoid event-storms by deduplicating coordinates + // Avoid event-storms by deduplicating coordinates, ignore z value when no z-event is generated if (!_deduplicate || - (_deduplicate && ((TOUCH_VALUE_X != x) || (TOUCH_VALUE_Y != y) || (TOUCH_VALUE_Z != z)))) { + (_deduplicate && ((TOUCH_VALUE_X != x) || (TOUCH_VALUE_Y != y) || + (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z) && (TOUCH_VALUE_Z != z))))) { success = true; TOUCH_VALUE_X = x; TOUCH_VALUE_Y = y; @@ -1677,32 +1693,10 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, if (success && bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_XY)) { // Send events for each touch const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex); - // Do NOT send a Z event for each touch? - if (!bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z) && validDeviceIndex(DeviceIndex)) { - // FIXME TD-er: Should not change anything in the Device vector. - # ifdef ESP8266 - Device[DeviceIndex].VType = Sensor_VType::SENSOR_TYPE_DUAL; - Device[DeviceIndex].ValueCount = 2; - # else // ifdef ESP8266 - Device.getDeviceStructForEdit(DeviceIndex).VType = Sensor_VType::SENSOR_TYPE_DUAL; - Device.getDeviceStructForEdit(DeviceIndex).ValueCount = 2; - # endif // ifdef ESP8266 - } - sendData(event); // Send X/Y(/Z) event - - if (!bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z) && validDeviceIndex(DeviceIndex)) { // Reset device configuration - // FIXME TD-er: Should not change anything in the Device vector. - # ifdef ESP8266 - Device[DeviceIndex].VType = Sensor_VType::SENSOR_TYPE_TRIPLE; - Device[DeviceIndex].ValueCount = 3; - # else // ifdef ESP8266 - Device.getDeviceStructForEdit(DeviceIndex).VType = Sensor_VType::SENSOR_TYPE_TRIPLE; - Device.getDeviceStructForEdit(DeviceIndex).ValueCount = 3; - # endif // ifdef ESP8266 - } + sendData(event); // Send X/Y(/Z) event } - if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME)) { // Send events for objectname if within reach, and swipes + if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME)) { // Send events for objectname if within reach, and swipes String selectedObjectName; int8_t selectedObjectIndex = -1; diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index 5f55fbf607..ea1c9c557e 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -11,6 +11,8 @@ /***** * Changelog: + * 2023-10-01 tonhuisman: Re-implement (fix) switching of X/Y/Z vs X/Y output values not by changing the DeviceVector but using + * PLUGIN_GET_DEVICEVALUECOUNT plugin function. * 2023-08-15 tonhuisman: Implement Extended CustomTaskSettings, minor improvements * 2022-09-26 tonhuisman: Fix issue with touch-disable option. Code optimizations, improved log/string handling * Make Swipe feature part of Extended Touch feature @@ -309,6 +311,7 @@ class ESPEasy_TouchHandler { bool setTouchObjectValue(struct EventStruct *event, const String & touchObject, const int16_t & value); + uint8_t get_device_valuecount(struct EventStruct *event); bool plugin_webform_load(struct EventStruct *event); bool plugin_webform_save(struct EventStruct *event); bool plugin_fifty_per_second(struct EventStruct *event, diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index c2c7508a30..6774d0bd1b 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -130,7 +130,9 @@ bool P123_data_struct::plugin_webform_load(struct EventStruct *event) { */ bool P123_data_struct::plugin_webform_save(struct EventStruct *event) { if (nullptr != touchHandler) { - return touchHandler->plugin_webform_save(event); + const bool result = touchHandler->plugin_webform_save(event); + P123_CONFIG_VTYPE = touchHandler->get_device_valuecount(event); // Store 'locally' + return result; } return false; } diff --git a/src/src/PluginStructs/P123_data_struct.h b/src/src/PluginStructs/P123_data_struct.h index 31e316cba5..5a3c1115f0 100644 --- a/src/src/PluginStructs/P123_data_struct.h +++ b/src/src/PluginStructs/P123_data_struct.h @@ -23,6 +23,7 @@ # undef PLUGIN_123_DEBUG # endif // if defined(BUILD_NO_DEBUG) && defined(PLUGIN_123_DEBUG) +# define P123_I2C_ADDRESS (0x38) // Fixed value # define P123_CONFIG_DISPLAY_TASK PCONFIG(0) # define P123_COLOR_DEPTH PCONFIG_LONG(1) @@ -30,6 +31,7 @@ # define P123_CONFIG_ROTATION PCONFIG(2) # define P123_CONFIG_X_RES PCONFIG(3) # define P123_CONFIG_Y_RES PCONFIG(4) +# define P123_CONFIG_VTYPE PCONFIG(5) # define P123_CONFIG_DISPLAY_PREV PCONFIG(7) From 73f0915ae04aace89bcadacbcb616c22dc6b2490 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 1 Oct 2023 21:16:55 +0200 Subject: [PATCH 072/113] [TouchHandler] Remove unused variable assignment --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index ec05dc5461..baa9515976 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -1691,8 +1691,6 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, // No events to handle if rules not enabled if (Settings.UseRules) { if (success && bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_XY)) { // Send events for each touch - const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex); - sendData(event); // Send X/Y(/Z) event } From 6dfd5ef491e00531489554fb0b18d5f64457640c Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 31 Dec 2023 14:57:58 +0100 Subject: [PATCH 073/113] [TouchHandler] Fix compilation errors since Core code changes --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 24 +++++++++++++----------- src/src/Helpers/ESPEasy_TouchHandler.h | 9 ++++++--- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index baa9515976..468e82f2d7 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -2,6 +2,8 @@ #ifdef PLUGIN_USES_TOUCHHANDLER +# include "src/Commands/ExecuteCommand.h" + /**************************************************************************** * toString: Display-value for the touch action ***************************************************************************/ @@ -256,7 +258,7 @@ int ESPEasy_TouchHandler::parseStringToInt(const String & string, const int & defaultValue) { String parsed = parseStringKeepCase(string, indexFind, separator); - int result = defaultValue; + int32_t result = defaultValue; validIntFromString(parsed, result); @@ -346,7 +348,7 @@ int8_t ESPEasy_TouchHandler::getTouchObjectIndex(struct EventStruct *event, const bool & isButton) { if (touchObject.isEmpty()) { return -1; } - int index = -1; + int32_t index = -1; int16_t idx = -1; @@ -355,11 +357,11 @@ int8_t ESPEasy_TouchHandler::getTouchObjectIndex(struct EventStruct *event, # if TOUCH_FEATURE_EXTENDED_TOUCH - int grp = -1; + int32_t grp = -1; if (validIntFromString(part, grp) && validButtonGroup(static_cast(grp), false)) { part = touchObject.substring(idx + 1); - int btn = -1; + int32_t btn = -1; if (validIntFromString(part, btn)) { idx = 0; @@ -1670,12 +1672,12 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, // Avoid event-storms by deduplicating coordinates, ignore z value when no z-event is generated if (!_deduplicate || - (_deduplicate && ((TOUCH_VALUE_X != x) || (TOUCH_VALUE_Y != y) || - (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z) && (TOUCH_VALUE_Z != z))))) { - success = true; - TOUCH_VALUE_X = x; - TOUCH_VALUE_Y = y; - TOUCH_VALUE_Z = z; + (_deduplicate && ((TOUCH_GET_VALUE_X != x) || (TOUCH_GET_VALUE_Y != y) || + (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z) && (TOUCH_GET_VALUE_Z != z))))) { + success = true; + TOUCH_SET_VALUE_X(x); + TOUCH_SET_VALUE_Y(y); + TOUCH_SET_VALUE_Z(z); } if (success && @@ -2049,7 +2051,7 @@ bool ESPEasy_TouchHandler::plugin_get_config_value(struct EventStruct *event, success = true; # if TOUCH_FEATURE_SWIPE } else if (command.equals(F("swipedir"))) { - int state; + int32_t state; if (validIntFromString(parseString(string, 2), state)) { string = toString(static_cast(state)); diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index ea1c9c557e..98cf7de568 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -121,9 +121,12 @@ # define TOUCH_FLAGS_SWAP_LEFT_RIGHT 19 // Swaps Left and Right, Up and Down swipe directions for menu actions # define TOUCH_FLAGS_IGNORE_TOUCH 20 // Disable touch, use for object/button features only -# define TOUCH_VALUE_X UserVar[event->BaseVarIndex + 0] -# define TOUCH_VALUE_Y UserVar[event->BaseVarIndex + 1] -# define TOUCH_VALUE_Z UserVar[event->BaseVarIndex + 2] +# define TOUCH_GET_VALUE_X UserVar.getInt32(event->TaskIndex, 0) +# define TOUCH_GET_VALUE_Y UserVar.getInt32(event->TaskIndex, 1) +# define TOUCH_GET_VALUE_Z UserVar.getInt32(event->TaskIndex, 2) +# define TOUCH_SET_VALUE_X(N) UserVar.setInt32(event->TaskIndex, 0, N) +# define TOUCH_SET_VALUE_Y(N) UserVar.setInt32(event->TaskIndex, 1, N) +# define TOUCH_SET_VALUE_Z(N) UserVar.setInt32(event->TaskIndex, 2, N) # define TOUCH_TS_ROTATION 0 // Rotation 0-3 = 0/90/180/270 degrees # define TOUCH_TS_SEND_XY true // Enable/Disable X/Y events From cd060f46f095d89892dfb72ff6905f9ef02b5b6c Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 31 Dec 2023 15:03:56 +0100 Subject: [PATCH 074/113] [TouchHandler] Fix compilation errors since Core code changes --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 468e82f2d7..64369465e6 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -2022,7 +2022,7 @@ bool ESPEasy_TouchHandler::plugin_get_config_value(struct EventStruct *event, success = true; # if TOUCH_FEATURE_EXTENDED_TOUCH } else if (command.equals(F("hasgroup"))) { - int group; // We'll be ignoring group 0 if there are multiple button groups + int32_t group; // We'll be ignoring group 0 if there are multiple button groups if (validIntFromString(parseString(string, 2), group)) { string = validButtonGroup(group, true) ? 1 : 0; From c57dfe84f9379f129f5af8850cb3f7c7c354ec42 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 31 Dec 2023 21:40:25 +0100 Subject: [PATCH 075/113] [TouchHandler] Code optimization reducing .bin size --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 439 ++++++++++------------- src/src/Helpers/ESPEasy_TouchHandler.h | 1 + 2 files changed, 189 insertions(+), 251 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index 64369465e6..68a8fd6197 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -73,7 +73,7 @@ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { _buttonGroups.clear(); // Clear groups _buttonGroups.insert(0u); // Always have group 0 - for (uint8_t i = TOUCH_OBJECT_INDEX_END; i >= TOUCH_OBJECT_INDEX_START; i--) { + for (uint8_t i = TOUCH_OBJECT_INDEX_END; i >= TOUCH_OBJECT_INDEX_START; --i) { if (!settingsArray[i].isEmpty() && (lastObjectIndex < TOUCH_OBJECT_INDEX_START)) { lastObjectIndex = i; objectCount++; // Count actual number of objects @@ -151,7 +151,7 @@ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { TouchObjects.reserve(objectCount); uint8_t t = 0u; - for (uint8_t i = TOUCH_OBJECT_INDEX_START; i <= lastObjectIndex; i++) { + for (uint8_t i = TOUCH_OBJECT_INDEX_START; i <= lastObjectIndex; ++i) { if (!settingsArray[i].isEmpty()) { TouchObjects.push_back(tTouchObjects()); TouchObjects[t].flags = parseStringToInt(settingsArray[i], TOUCH_OBJECT_FLAGS, TOUCH_SETTINGS_SEPARATOR); @@ -171,8 +171,10 @@ void ESPEasy_TouchHandler::loadTouchObjects(struct EventStruct *event) { TouchObjects[t].colorDisabledCaption = parseStringToInt(settingsArray[i], TOUCH_OBJECT_COLOR_DISABCAPT, TOUCH_SETTINGS_SEPARATOR); TouchObjects[t].groupFlags = parseStringToInt(settingsArray[i], TOUCH_OBJECT_GROUPFLAGS, TOUCH_SETTINGS_SEPARATOR); - if (!validButtonGroup(get8BitFromUL(TouchObjects[t].flags, TOUCH_OBJECT_FLAG_GROUP))) { - _buttonGroups.insert(get8BitFromUL(TouchObjects[t].flags, TOUCH_OBJECT_FLAG_GROUP)); + const uint8_t g = get8BitFromUL(TouchObjects[t].flags, TOUCH_OBJECT_FLAG_GROUP); + + if (!validButtonGroup(g)) { + _buttonGroups.insert(g); } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH @@ -256,9 +258,8 @@ int ESPEasy_TouchHandler::parseStringToInt(const String & string, const uint8_t& indexFind, const char & separator, const int & defaultValue) { - String parsed = parseStringKeepCase(string, indexFind, separator); - - int32_t result = defaultValue; + const String parsed = parseStringKeepCase(string, indexFind, separator); + int32_t result = defaultValue; validIntFromString(parsed, result); @@ -289,10 +290,10 @@ bool ESPEasy_TouchHandler::isValidAndTouchedTouchObject(const int16_t& x, int8_t & selectedObjectIndex) { uint32_t lastObjectArea = 0u; bool selected = false; - uint16_t _x = static_cast(x); - uint16_t _y = static_cast(y); + const uint16_t _x = static_cast(x); + const uint16_t _y = static_cast(y); - for (size_t objectNr = 0; objectNr < TouchObjects.size(); objectNr++) { + for (size_t objectNr = 0; objectNr < TouchObjects.size(); ++objectNr) { const uint8_t group = get8BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_GROUP); if (!TouchObjects[objectNr].objectName.isEmpty() @@ -318,21 +319,19 @@ bool ESPEasy_TouchHandler::isValidAndTouchedTouchObject(const int16_t& x, # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log; - log.reserve(72); - log = strformat(F("TOUCH DEBUG Touched: obj: %s,%d,%d,%d,%d surface:%d x,y:%d,%d sel:%s/%d/%c"), - TouchObjects[objectNr].objectName.c_str(), - TouchObjects[objectNr].top_left.x, - TouchObjects[objectNr].top_left.y, - TouchObjects[objectNr].width_height.x, - TouchObjects[objectNr].width_height.y, - TouchObjects[objectNr].SurfaceAreas, - x, - y, - selectedObjectName.c_str(), - selectedObjectIndex, - selected ? 'T' : 'f'); - addLogMove(LOG_LEVEL_DEBUG, log); + addLog(LOG_LEVEL_DEBUG, + strformat(F("TOUCH DEBUG Touched: obj: %s,%d,%d,%d,%d surface:%d x,y:%d,%d sel:%s/%d/%c"), + TouchObjects[objectNr].objectName.c_str(), + TouchObjects[objectNr].top_left.x, + TouchObjects[objectNr].top_left.y, + TouchObjects[objectNr].width_height.x, + TouchObjects[objectNr].width_height.y, + TouchObjects[objectNr].SurfaceAreas, + x, + y, + selectedObjectName.c_str(), + selectedObjectIndex, + selected ? 'T' : 'f')); } # endif // ifdef TOUCH_DEBUG } @@ -349,8 +348,7 @@ int8_t ESPEasy_TouchHandler::getTouchObjectIndex(struct EventStruct *event, if (touchObject.isEmpty()) { return -1; } int32_t index = -1; - - int16_t idx = -1; + int32_t idx = -1; if ((idx = touchObject.indexOf('.')) > -1) { String part = touchObject.substring(0, idx); @@ -366,9 +364,9 @@ int8_t ESPEasy_TouchHandler::getTouchObjectIndex(struct EventStruct *event, if (validIntFromString(part, btn)) { idx = 0; - for (size_t objectNr = 0; objectNr < TouchObjects.size(); objectNr++) { + for (size_t objectNr = 0; objectNr < TouchObjects.size(); ++objectNr) { if (!TouchObjects[objectNr].objectName.isEmpty() - && (get8BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_GROUP) == grp) + && (get8BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_GROUP) == static_cast(grp)) && (!isButton || bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTON))) { idx++; @@ -393,7 +391,7 @@ int8_t ESPEasy_TouchHandler::getTouchObjectIndex(struct EventStruct *event, return static_cast(index - 1); } - for (size_t objectNr = 0; objectNr < TouchObjects.size(); objectNr++) { + for (size_t objectNr = 0; objectNr < TouchObjects.size(); ++objectNr) { if (!TouchObjects[objectNr].objectName.isEmpty() && touchObject.equalsIgnoreCase(TouchObjects[objectNr].objectName) && (!isButton || bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_BUTTON))) { @@ -412,7 +410,7 @@ bool ESPEasy_TouchHandler::setTouchObjectState(struct EventStruct *event, if (touchObject.isEmpty()) { return false; } bool success = false; - int8_t objectNr = getTouchObjectIndex(event, touchObject); + const int8_t objectNr = getTouchObjectIndex(event, touchObject); if (objectNr > -1) { success = true; // Succes if matched object @@ -459,9 +457,8 @@ bool ESPEasy_TouchHandler::setTouchObjectState(struct EventStruct *event, int8_t ESPEasy_TouchHandler::getTouchObjectState(struct EventStruct *event, const String & touchObject) { if (touchObject.isEmpty()) { return false; } - int8_t result = -1; - - int8_t objectNr = getTouchObjectIndex(event, touchObject); + int8_t result = -1; + const int8_t objectNr = getTouchObjectIndex(event, touchObject); if (objectNr > -1) { result = bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED) ? 1 : 0; @@ -476,9 +473,8 @@ bool ESPEasy_TouchHandler::setTouchButtonOnOff(struct EventStruct *event, const String & touchObject, const bool & state) { if (touchObject.isEmpty()) { return false; } - bool success = false; - - int8_t objectNr = getTouchObjectIndex(event, touchObject, true); + bool success = false; + const int8_t objectNr = getTouchObjectIndex(event, touchObject, true); if ((objectNr > -1) && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED) @@ -515,9 +511,8 @@ bool ESPEasy_TouchHandler::setTouchButtonOnOff(struct EventStruct *event, int16_t ESPEasy_TouchHandler::getTouchObjectValue(struct EventStruct *event, const String & touchObject) { if (touchObject.isEmpty()) { return -1; } - int16_t result = -1; // invalid object - - int8_t objectNr = getTouchObjectIndex(event, touchObject); + int16_t result = -1; // invalid object + const int8_t objectNr = getTouchObjectIndex(event, touchObject); if ((objectNr > -1) && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED)) { @@ -538,10 +533,9 @@ bool ESPEasy_TouchHandler::setTouchObjectValue(struct EventStruct *event, const String & touchObject, const int16_t & value) { if (touchObject.isEmpty()) { return false; } - bool success = false; - - int8_t objectNr = getTouchObjectIndex(event, touchObject, false); - int16_t _value = value; + bool success = false; + const int8_t objectNr = getTouchObjectIndex(event, touchObject, false); + int16_t _value = value; if ((objectNr > -1) && bitRead(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_ENABLED)) { @@ -581,11 +575,9 @@ bool ESPEasy_TouchHandler::setTouchObjectValue(struct EventStruct *event, # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log; - log.reserve(72); - log = strformat(F("TOUCH setTouchObjectValue: obj: %s/%d, new value: %d"), - touchObject.c_str(), objectNr, _value); - addLogMove(LOG_LEVEL_DEBUG, log); + addLogMove(LOG_LEVEL_DEBUG, + strformat(F("TOUCH setTouchObjectValue: obj: %s/%d, new value: %d"), + touchObject.c_str(), objectNr, _value)); } # endif // ifdef TOUCH_DEBUG } @@ -606,7 +598,7 @@ bool ESPEasy_TouchHandler::parseRangeToInt16(const String& range, tmp = parseString(range, 2); if (validFrom && validFloatFromString(tmp, rangeTo) && - !essentiallyEqual(rangeFrom, 0.0f) && !essentiallyEqual(rangeTo, 0.0f)) { + !essentiallyZero(rangeFrom) && !essentiallyZero(rangeTo)) { lowRange = static_cast(rangeFrom); highRange = static_cast(rangeTo); return true; @@ -621,7 +613,7 @@ bool ESPEasy_TouchHandler::parseRangeToInt16(const String& range, void ESPEasy_TouchHandler::displayButtonGroup(struct EventStruct *event, const int16_t & buttonGroup, const int8_t & mode) { - for (int objectNr = 0; objectNr < static_cast(TouchObjects.size()); objectNr++) { + for (int8_t objectNr = 0; objectNr < static_cast(TouchObjects.size()); ++objectNr) { displayButton(event, objectNr, buttonGroup, mode); delay(0); @@ -629,16 +621,10 @@ void ESPEasy_TouchHandler::displayButtonGroup(struct EventStruct *event, if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME)) { // Send an event #Group,, with the selected group and the mode (-3..0) - String eventCommand; - eventCommand.reserve(24); - eventCommand += getTaskDeviceName(event->TaskIndex); - eventCommand += '#'; - eventCommand += F("Group"); - eventCommand += '='; // Add arguments - eventCommand += buttonGroup; - eventCommand += ','; - eventCommand += mode; - eventQueue.addMove(std::move(eventCommand)); + eventQueue.add(strformat(F("%s#Group=%d,%d"), + getTaskDeviceName(event->TaskIndex).c_str(), + buttonGroup, + mode)); } delay(0); @@ -652,8 +638,8 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, const int16_t & buttonGroup, int8_t mode) { if ((buttonNr < 0) || (buttonNr >= static_cast(TouchObjects.size()))) { return false; } // sanity check - int8_t state = 99; - int16_t group = get8BitFromUL(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_GROUP); + int8_t state = 99; + const int16_t group = get8BitFromUL(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_GROUP); # if TOUCH_FEATURE_EXTENDED_TOUCH Touch_action_e action = static_cast(get4BitFromUL(TouchObjects[buttonNr].groupFlags, TOUCH_OBJECT_GROUP_ACTION)); @@ -685,7 +671,7 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, # if TOUCH_FEATURE_EXTENDED_TOUCH if (isArrow) { // Auto-Enable/Disable the arrow buttons - bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); + const bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); state = 1; // always get ON state! if (action == Touch_action_e::DecrementGroup) { // Left arrow @@ -715,16 +701,14 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log; - log.reserve(90); - log = strformat(F("TOUCH: button init, state: %d, group: %d, mode: %d, group: %d, en: %d, object: %d"), - state, - buttonGroup, - mode, - get8BitFromUL(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_GROUP), - bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_BUTTON), - buttonNr); - addLogMove(LOG_LEVEL_DEBUG, log); + addLog(LOG_LEVEL_DEBUG, + strformat(F("TOUCH: button init, state: %d, group: %d, mode: %d, group: %d, en: %d, object: %d"), + state, + buttonGroup, + mode, + get8BitFromUL(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_GROUP), + bitRead(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_BUTTON), + buttonNr)); } # endif // ifdef TOUCH_DEBUG return true; @@ -749,9 +733,9 @@ bool ESPEasy_TouchHandler::validButtonGroup(const int16_t& group, */ bool ESPEasy_TouchHandler::handleButtonSwipe(struct EventStruct *event, const int16_t & swipeValue) { - bool success = false; - Swipe_action_e swipe = static_cast(swipeValue); - bool swapped = bitRead(Touch_Settings.flags, TOUCH_FLAGS_SWAP_LEFT_RIGHT); + bool success = false; + const Swipe_action_e swipe = static_cast(swipeValue); + const bool swapped = bitRead(Touch_Settings.flags, TOUCH_FLAGS_SWAP_LEFT_RIGHT); if (swipe == Swipe_action_e::Up) { if (swapped) { @@ -827,7 +811,7 @@ bool ESPEasy_TouchHandler::decrementButtonGroup(struct EventStruct *event) { * Increment button group by page (+10), if max. group > 0 then min. group = 1 */ bool ESPEasy_TouchHandler::incrementButtonPage(struct EventStruct *event) { - bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); + const bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); if (validButtonGroup(_buttonGroup + (pgupInvert ? -10 : 10))) { return setButtonGroup(event, _buttonGroup + (pgupInvert ? -10 : 10)); @@ -839,7 +823,7 @@ bool ESPEasy_TouchHandler::incrementButtonPage(struct EventStruct *event) { * Decrement button group by page (+10), if max. group > 0 then min. group = 1 */ bool ESPEasy_TouchHandler::decrementButtonPage(struct EventStruct *event) { - bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); + const bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); if (validButtonGroup(_buttonGroup + (pgupInvert ? 10 : -10))) { return setButtonGroup(event, _buttonGroup + (pgupInvert ? 10 : -10)); @@ -858,10 +842,10 @@ uint8_t ESPEasy_TouchHandler::get_device_valuecount(struct EventStruct *event) { _settingsLoaded = true; } - if (!bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z)) { - return getValueCountFromSensorType(Sensor_VType::SENSOR_TYPE_DUAL); - } - return getValueCountFromSensorType(Sensor_VType::SENSOR_TYPE_TRIPLE); + return getValueCountFromSensorType( + bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z) + ? Sensor_VType::SENSOR_TYPE_TRIPLE + : Sensor_VType::SENSOR_TYPE_DUAL); } /** @@ -886,8 +870,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { bitWrite(choice3, TOUCH_FLAGS_SEND_Z, bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_Z)); bitWrite(choice3, TOUCH_FLAGS_SEND_OBJECTNAME, bitRead(Touch_Settings.flags, TOUCH_FLAGS_SEND_OBJECTNAME)); { - # define TOUCH_EVENTS_OPTIONS 6 - const __FlashStringHelper *options3[TOUCH_EVENTS_OPTIONS] = + const __FlashStringHelper *options3[] = { F("None"), F("X and Y"), F("X, Y and Z"), @@ -901,8 +884,8 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { F("Objectnames, X, Y and Z") # endif // if TOUCH_FEATURE_EXTENDED_TOUCH }; - const int optionValues3[TOUCH_EVENTS_OPTIONS] = { 0, 1, 3, 4, 5, 7 }; // Already used as a bitmap! - addFormSelector(F("Events"), F("events"), TOUCH_EVENTS_OPTIONS, options3, optionValues3, choice3); + const int optionValues3[] = { 0, 1, 3, 4, 5, 7 }; // Already used as a bitmap! + addFormSelector(F("Events"), F("events"), NR_ELEMENTS(optionValues3), options3, optionValues3, choice3); addFormCheckBox(F("Draw buttons when started"), F("initobj"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_INIT_OBJECTEVENT)); # ifndef LIMIT_BUILD_SIZE @@ -1210,11 +1193,11 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { # endif // if TOUCH_FEATURE_EXTENDED_TOUCH - uint8_t maxIdx = std::min(static_cast(TouchObjects.size() + TOUCH_EXTRA_OBJECT_COUNT), TOUCH_MAX_OBJECT_COUNT); - String parsed; + const uint8_t maxIdx = std::min(static_cast(TouchObjects.size() + TOUCH_EXTRA_OBJECT_COUNT), TOUCH_MAX_OBJECT_COUNT); + String parsed; TouchObjects.resize(maxIdx, tTouchObjects()); - for (int objectNr = 0; objectNr < maxIdx; objectNr++) { + for (int8_t objectNr = 0; objectNr < maxIdx; ++objectNr) { html_TR_TD(); addHtml(F(" ")); addHtmlInt(objectNr + 1); // Arrayindex to objectindex @@ -1342,14 +1325,10 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { html_TD(2); // Start with some blank columns # if TOUCH_FEATURE_EXTENDED_TOUCH { - # if TOUCH_FEATURE_TOOLTIPS - String buttonGroupToolTip(concat(F("Button-group [0.."), static_cast(TOUCH_MAX_BUTTON_GROUPS))); - buttonGroupToolTip += ']'; - # endif // if TOUCH_FEATURE_TOOLTIPS addNumericBox(getPluginCustomArgName(objectNr + 1600), get8BitFromUL(TouchObjects[objectNr].flags, TOUCH_OBJECT_FLAG_GROUP), 0, TOUCH_MAX_BUTTON_GROUPS # if TOUCH_FEATURE_TOOLTIPS - , F("widenumber"), buttonGroupToolTip + , F("widenumber"), strformat(F("Button-group [0..%d]"), TOUCH_MAX_BUTTON_GROUPS) # endif // if TOUCH_FEATURE_TOOLTIPS ); } @@ -1428,8 +1407,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { addNumericBox(getPluginCustomArgName(objectNr + 2100), get8BitFromUL(TouchObjects[objectNr].groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP), 0, TOUCH_MAX_BUTTON_GROUPS # if TOUCH_FEATURE_TOOLTIPS - , F("widenumber") - , F("Action group") + , F("widenumber"), F("Action group") # endif // if TOUCH_FEATURE_TOOLTIPS ); # endif // if TOUCH_FEATURE_EXTENDED_TOUCH @@ -1528,7 +1506,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { String error; - for (int objectNr = 0; objectNr < TOUCH_MAX_OBJECT_COUNT; objectNr++) { + for (int8_t objectNr = 0; objectNr < TOUCH_MAX_OBJECT_COUNT; ++objectNr) { config.clear(); config += webArg(getPluginCustomArgName(objectNr + 100)); // Name config.trim(); // Remove leading/trailing whitespace from name @@ -1538,8 +1516,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { if (!ExtraTaskSettings.checkInvalidCharInNames(config.c_str()) || numStart) { // Check for invalid characters in objectname - error += concat(F("Invalid character in objectname #"), objectNr + 1); - error += F(". "); + error += strformat(F("Invalid character in objectname #%d. "), objectNr + 1); error += numStart ? F("Should not start with a digit.\n") : F("Do not use ',-+/*=^%!#[]{}()' or space.\n"); } config += TOUCH_SETTINGS_SEPARATOR; @@ -1568,15 +1545,16 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { bitWrite(flags, TOUCH_OBJECT_FLAG_BUTTON, isFormItemChecked(getPluginCustomArgName(objectNr + 600))); // On/Off button # endif // if TOUCH_FEATURE_EXTENDED_TOUCH - config += ull2String(flags); // Flags + // REMARK: Converting the code below to strformat() increases the build by 200 bytes! + config += ull2String(flags); // Flags config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 200)); // Top x + config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 200)); // Top x config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 300)); // Top y + config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 300)); // Top y config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 400)); // Bottom x + config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 400)); // Bottom x config += TOUCH_SETTINGS_SEPARATOR; - config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 500)); // Bottom y + config += toStringNoZero(getFormItemIntCustomArgName(objectNr + 500)); // Bottom y # if TOUCH_FEATURE_EXTENDED_TOUCH config += TOUCH_SETTINGS_SEPARATOR; @@ -1632,7 +1610,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { if (loglevelActiveFor(LOG_LEVEL_INFO) && !config.isEmpty()) { config.replace(TOUCH_SETTINGS_SEPARATOR, ','); - addLogMove(LOG_LEVEL_INFO, strformat(F("Save touch object #%d settings: %s"), objectNr, config.c_str())); + addLogMove(LOG_LEVEL_INFO, strformat(F("Save touch object #%d settings: %s"), objectNr + 1, config.c_str())); } # endif // ifdef TOUCH_DEBUG } @@ -1645,7 +1623,7 @@ bool ESPEasy_TouchHandler::plugin_webform_save(struct EventStruct *event) { error = SaveCustomTaskSettings(event->TaskIndex, settingsArray, TOUCH_ARRAY_SIZE, 0); if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, strformat(F("TOUCH: Save settings, size: %d"), saveSize)); + addLogMove(LOG_LEVEL_INFO, concat(F("TOUCH: Save settings, size: "), saveSize)); } if (!error.isEmpty()) { @@ -1740,11 +1718,9 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log; - log.reserve(72); - log += strformat(F("Touch Swiped, direction: %s, dx: %d, dy: %d"), - String(toString(swipe)).c_str(), delta_x, delta_y); - addLogMove(LOG_LEVEL_DEBUG, log); + addLog(LOG_LEVEL_DEBUG, + strformat(F("Touch Swiped, direction: %s, dx: %d, dy: %d"), + String(toString(swipe)).c_str(), delta_x, delta_y)); } # endif // ifdef TOUCH_DEBUG } @@ -1826,11 +1802,9 @@ bool ESPEasy_TouchHandler::plugin_fifty_per_second(struct EventStruct *event, # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE # ifdef TOUCH_DEBUG - String log; - log.reserve(72); - log += strformat(F("Swiped/touched, object: %s:"), _lastObjectName.c_str()); - log += toString(swipe); - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, + strformat(F("Swiped/touched, object: %s:%s"), _lastObjectName.c_str(), + String(toString(swipe)).c_str())); # endif // ifdef TOUCH_DEBUG if (swipe != Swipe_action_e::None) { @@ -1876,23 +1850,19 @@ void ESPEasy_TouchHandler::releaseTouch(struct EventStruct *event) { if (_lastSwipe == Swipe_action_e::None) # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE { - eventCommand += _lastObjectName; - eventCommand += '='; // Add arguments - eventCommand += _last_point.x; - eventCommand += ','; - eventCommand += _last_point.y; - eventCommand += ','; - eventCommand += _last_point_z.x; + eventCommand += strformat(F("%s=%d,%d,%d"), + _lastObjectName.c_str(), + _last_point.x, + _last_point.y, + _last_point_z.x); } # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE else { - eventCommand += F("Swiped="); // Add arguments - eventCommand += static_cast(_lastSwipe); - eventCommand += ','; - eventCommand += _last_delta_x; - eventCommand += ','; - eventCommand += _last_delta_y; - _lastSwipe = Swipe_action_e::None; + eventCommand += strformat(F("Swiped=%d,%d,%d"), // Add arguments + static_cast(_lastSwipe), + _last_delta_x, + _last_delta_y); + _lastSwipe = Swipe_action_e::None; } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE eventQueue.addMove(std::move(eventCommand)); @@ -1914,86 +1884,83 @@ bool ESPEasy_TouchHandler::plugin_write(struct EventStruct *event, command = parseString(string, 1); - if (command.equals(F("touch"))) { + if (equals(command, F("touch"))) { arguments.reserve(24); subcommand = parseString(string, 2); # ifdef TOUCH_DEBUG if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log; - log.reserve(90); - log = strformat(F("TOUCH PLUGIN_WRITE arguments Par1: %d, 2: %d, 3: %d, 4: %d, string: %s"), - event->Par1, event->Par2, event->Par3, event->Par4, string.c_str()); - addLogMove(LOG_LEVEL_DEBUG, log); + addLog(LOG_LEVEL_DEBUG, strformat(F("TOUCH PLUGIN_WRITE arguments Par1: %d, 2: %d, 3: %d, 4: %d, string: %s"), + event->Par1, event->Par2, event->Par3, event->Par4, string.c_str())); } # endif // ifdef TOUCH_DEBUG - if (subcommand.equals(F("enable"))) { // touch,enable,[,...] : Enable disabled objectname(s) + if (equals(subcommand, F("enable"))) { // touch,enable,[,...] : Enable disabled objectname(s) arguments = parseString(string, arg); while (!arguments.isEmpty()) { success |= setTouchObjectState(event, arguments, true); arguments = parseString(string, ++arg); } - } else if (subcommand.equals(F("disable"))) { // touch,disable,[,...] : Disable enabled objectname(s) + } else if (equals(subcommand, F("disable"))) { // touch,disable,[,...] : Disable enabled objectname(s) arguments = parseString(string, arg); while (!arguments.isEmpty()) { success |= setTouchObjectState(event, arguments, false); arguments = parseString(string, ++arg); } - } else if (subcommand.equals(F("on"))) { // touch,on,[,...] : Switch TouchButton(s) on + } else if (equals(subcommand, F("on"))) { // touch,on,[,...] : Switch TouchButton(s) on arguments = parseString(string, arg); while (!arguments.isEmpty()) { success |= setTouchButtonOnOff(event, arguments, true); arguments = parseString(string, ++arg); } - } else if (subcommand.equals(F("off"))) { // touch,off,[,...] : Switch TouchButton(s) off + } else if (equals(subcommand, F("off"))) { // touch,off,[,...] : Switch TouchButton(s) off arguments = parseString(string, arg); while (!arguments.isEmpty()) { success |= setTouchButtonOnOff(event, arguments, false); arguments = parseString(string, ++arg); } - } else if (subcommand.equals(F("toggle"))) { // touch,toggle,[,...] : Switch TouchButton(s) to the other state + } else if (equals(subcommand, F("toggle"))) { // touch,toggle,[,...] : Switch TouchButton(s) to the other state arguments = parseString(string, arg); while (!arguments.isEmpty()) { - int16_t state = getTouchObjectValue(event, arguments); + const int16_t state = getTouchObjectValue(event, arguments); if (state > -1) { success |= setTouchButtonOnOff(event, arguments, state == 0); } arguments = parseString(string, ++arg); } - } else if (subcommand.equals(F("set"))) { // touch,set,, : Set TouchObject value + } else if (equals(subcommand, F("set"))) { // touch,set,, : Set TouchObject value arguments = parseString(string, arg); success = setTouchObjectValue(event, arguments, event->Par3); # if TOUCH_FEATURE_EXTENDED_TOUCH # if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE - } else if (subcommand.equals(F("swipe"))) { // touch,swipe, : Switch button group via swipe value + } else if (equals(subcommand, F("swipe"))) { // touch,swipe, : Switch button group via swipe value success = handleButtonSwipe(event, event->Par2); # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE - } else if (subcommand.equals(F("setgrp"))) { // touch,setgrp, : Activate button group + } else if (equals(subcommand, F("setgrp"))) { // touch,setgrp, : Activate button group success = setButtonGroup(event, event->Par2); - } else if (subcommand.equals(F("incgrp"))) { // touch,incgrp : increment group and Activate + } else if (equals(subcommand, F("incgrp"))) { // touch,incgrp : increment group and Activate success = incrementButtonGroup(event); - } else if (subcommand.equals(F("decgrp"))) { // touch,decgrp : Decrement group and Activate + } else if (equals(subcommand, F("decgrp"))) { // touch,decgrp : Decrement group and Activate success = decrementButtonGroup(event); - } else if (subcommand.equals(F("incpage"))) { // touch,incpage : increment page and Activate + } else if (equals(subcommand, F("incpage"))) { // touch,incpage : increment page and Activate success = incrementButtonPage(event); - } else if (subcommand.equals(F("decpage"))) { // touch,decpage : Decrement page and Activate + } else if (equals(subcommand, F("decpage"))) { // touch,decpage : Decrement page and Activate success = decrementButtonPage(event); - } else if (subcommand.equals(F("updatebutton"))) { // touch,updatebutton,[,[,]] : Update a button + } else if (equals(subcommand, F("updatebutton"))) { // touch,updatebutton,[,[,]] : Update a button arguments = parseString(string, 3); // Check for a valid button name or number, returns a 0-based index - int index = getTouchObjectIndex(event, arguments, true); + const int8_t index = getTouchObjectIndex(event, arguments, true); if (index > -1) { - bool hasPar3 = !parseString(string, 4).isEmpty(); - bool hasPar4 = !parseString(string, 5).isEmpty(); + const bool hasPar3 = !parseString(string, 4).isEmpty(); + const bool hasPar4 = !parseString(string, 5).isEmpty(); if (hasPar4) { success = displayButton(event, index, event->Par3, event->Par4); @@ -2017,11 +1984,11 @@ bool ESPEasy_TouchHandler::plugin_get_config_value(struct EventStruct *event, bool success = false; const String command = parseString(string, 1); - if (command.equals(F("buttongroup"))) { + if (equals(command, F("buttongroup"))) { string = getButtonGroup(); success = true; # if TOUCH_FEATURE_EXTENDED_TOUCH - } else if (command.equals(F("hasgroup"))) { + } else if (equals(command, F("hasgroup"))) { int32_t group; // We'll be ignoring group 0 if there are multiple button groups if (validIntFromString(parseString(string, 2), group)) { @@ -2031,26 +1998,24 @@ bool ESPEasy_TouchHandler::plugin_get_config_value(struct EventStruct *event, string = '0'; // invalid number = false } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH - } else if (command.equals(F("enabled"))) { - const String arguments = parseStringKeepCase(string, 2); - int8_t enabled = getTouchObjectState(event, arguments); + } else if (equals(command, F("enabled"))) { + const int8_t enabled = getTouchObjectState(event, parseStringKeepCase(string, 2)); if (enabled > -1) { string = enabled; success = true; } - } else if (command.equals(F("state"))) { - const String arguments = parseStringKeepCase(string, 2); - int16_t state = getTouchObjectValue(event, arguments); + } else if (equals(command, F("state"))) { + const int16_t state = getTouchObjectValue(event, parseStringKeepCase(string, 2)); string = state; success = true; # if TOUCH_FEATURE_EXTENDED_TOUCH - } else if (command.equals(F("pagemode"))) { + } else if (equals(command, F("pagemode"))) { string = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); success = true; # if TOUCH_FEATURE_SWIPE - } else if (command.equals(F("swipedir"))) { + } else if (equals(command, F("swipedir"))) { int32_t state; if (validIntFromString(parseString(string, 2), state)) { @@ -2084,18 +2049,15 @@ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, eventCommand.reserve(120); extraCommand.reserve(48); - extraCommand += getTaskDeviceName(event->TaskIndex); - extraCommand += '#'; - extraCommand += TouchObjects[objectIndex].objectName; - extraCommand += '='; // Add arguments: (%eventvalue#%) + // Task with added arguments: (%eventvalue#%) + extraCommand += strformat(F("%s#%s="), getTaskDeviceName(event->TaskIndex).c_str(), TouchObjects[objectIndex].objectName.c_str()); if (bitRead(Touch_Settings.flags, TOUCH_FLAGS_DRAWBTN_VIA_RULES)) { eventCommand = extraCommand; - } else { // Handle via direct btn commands - if (_displayTask != event->TaskIndex) { // Add arguments for display - eventCommand += '['; - eventCommand += _displayTask + 1; - eventCommand += F("].adagfx_trigger,btn,"); // Internal command trigger + } else { // Handle via direct btn commands + if (_displayTask != event->TaskIndex) { // Add arguments for display + // Internal command trigger + eventCommand += strformat(F("[%d].adagfx_trigger,btn,"), _displayTask + 1); } else { addLog(LOG_LEVEL_ERROR, F("TOUCH: No valid Display task selected.")); return; @@ -2120,42 +2082,35 @@ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, } } eventCommand += ','; - eventCommand += mode; // (2 = mode) + eventCommand += mode; // (2 = mode) extraCommand += ','; - extraCommand += mode; // (2 = mode) // duplicate - - if (_displayTask != event->TaskIndex) { // Add arguments for display - eventCommand += ','; - eventCommand += TouchObjects[objectIndex].top_left.x; // (3 = x) - eventCommand += ','; - eventCommand += TouchObjects[objectIndex].top_left.y; // (4 = y) - eventCommand += ','; - eventCommand += TouchObjects[objectIndex].width_height.x; // (5 = width) - eventCommand += ','; - eventCommand += TouchObjects[objectIndex].width_height.y; // (6 = height) - eventCommand += ','; - eventCommand += objectIndex + 1; // Adjust to displayed index (7 = id) - eventCommand += ','; // (8 = type + layout, 4+4 bit, side by side) - eventCommand += get8BitFromUL(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_BUTTONTYPE) * factor; + extraCommand += mode; // (2 = mode) // duplicate + + if (_displayTask != event->TaskIndex) { // Add arguments for display + eventCommand += strformat( + F(",%d,%d,%d,%d,%d,%d"), + TouchObjects[objectIndex].top_left.x, // (3 = x) + TouchObjects[objectIndex].top_left.y, // (4 = y) + TouchObjects[objectIndex].width_height.x, // (5 = width) + TouchObjects[objectIndex].width_height.y, // (6 = height) + objectIndex + 1, // Adjust to displayed index (7 = id) + // (8 = type + layout, 4+4 bit, side by side) + get8BitFromUL(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_BUTTONTYPE) * factor); # if TOUCH_FEATURE_EXTENDED_TOUCH - eventCommand += ','; // (9 = ON color) - eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorOn == 0 - ? Touch_Settings.colorOn - : TouchObjects[objectIndex].colorOn, - _colorDepth); - eventCommand += ','; // (10 = OFF color) - eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorOff == 0 - ? Touch_Settings.colorOff - : TouchObjects[objectIndex].colorOff, - _colorDepth); - eventCommand += ','; // (11 = Caption color) - eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorCaption == 0 - ? Touch_Settings.colorCaption - : TouchObjects[objectIndex].colorCaption, - _colorDepth); - eventCommand += ','; // (12 = Font scaling) - eventCommand += get4BitFromUL(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_FONTSCALE); - eventCommand += ','; // (13 = ON caption, default=object name) + eventCommand += strformat( + F(",%s,%s,%s,%d,"), + AdaGFXcolorToString(TouchObjects[objectIndex].colorOn == 0 + ? Touch_Settings.colorOn + : TouchObjects[objectIndex].colorOn, _colorDepth).c_str(), // (9 = ON color) + AdaGFXcolorToString(TouchObjects[objectIndex].colorOff == 0 + ? Touch_Settings.colorOff + : TouchObjects[objectIndex].colorOff, _colorDepth).c_str(), // (10 = OFF color) + AdaGFXcolorToString(TouchObjects[objectIndex].colorCaption == 0 + ? Touch_Settings.colorCaption + : TouchObjects[objectIndex].colorCaption, _colorDepth).c_str(), // (11 = Caption color) + get4BitFromUL(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_FONTSCALE)); // (12 = Font scaling) + + // (13 = ON caption, default=object name) String _capt; if (TouchObjects[objectIndex].captionOn.isEmpty()) { @@ -2181,54 +2136,36 @@ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, _capt = TouchObjects[objectIndex].captionOff; } _capt.replace('_', ' '); // Replace all '_' by space - eventCommand += wrapWithQuotesIfContainsParameterSeparatorChar(_capt); - eventCommand += ','; // (15 = Border color) - eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorBorder == 0 - ? Touch_Settings.colorBorder - : TouchObjects[objectIndex].colorBorder, - _colorDepth); - eventCommand += ','; // (16 = Disabled color) - eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorDisabled == 0 - ? Touch_Settings.colorDisabled - : TouchObjects[objectIndex].colorDisabled, - _colorDepth); - eventCommand += ','; // (17 = Disabled caption color) - eventCommand += AdaGFXcolorToString(TouchObjects[objectIndex].colorDisabledCaption == 0 - ? Touch_Settings.colorDisabledCaption - : TouchObjects[objectIndex].colorDisabledCaption, - _colorDepth); + eventCommand += strformat( + F("%s,%s,%s,%s"), + wrapWithQuotesIfContainsParameterSeparatorChar(_capt).c_str(), + AdaGFXcolorToString(TouchObjects[objectIndex].colorBorder == 0 + ? Touch_Settings.colorBorder + : TouchObjects[objectIndex].colorBorder, _colorDepth).c_str(), // (15 = Border color) + AdaGFXcolorToString(TouchObjects[objectIndex].colorDisabled == 0 + ? Touch_Settings.colorDisabled + : TouchObjects[objectIndex].colorDisabled, _colorDepth).c_str(), // (16 = Disabled color) + AdaGFXcolorToString(TouchObjects[objectIndex].colorDisabledCaption == 0 + ? Touch_Settings.colorDisabledCaption + : TouchObjects[objectIndex].colorDisabledCaption, _colorDepth).c_str()); // (17 = Disabled caption color) # endif // if TOUCH_FEATURE_EXTENDED_TOUCH - eventCommand += ','; - eventCommand += _displayTask + 1; // What TaskIndex? (18) or (9) - eventCommand += ','; // Group (19) or (10) - eventCommand += get8BitFromUL(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_GROUP); + eventCommand += strformat( + F(",%d,%d"), + _displayTask + 1, // What TaskIndex? (18) or (9) + get8BitFromUL(TouchObjects[objectIndex].flags, TOUCH_OBJECT_FLAG_GROUP)); // Group (19) or (10) # if TOUCH_FEATURE_EXTENDED_TOUCH - eventCommand += ','; // Group mode (20) - uint8_t action = get4BitFromUL(TouchObjects[objectIndex].groupFlags, TOUCH_OBJECT_GROUP_ACTION); - - if (!groupSwitch && (static_cast(action) != Touch_action_e::Default)) { - switch (static_cast(action)) { - case Touch_action_e::ActivateGroup: - eventCommand += get8BitFromUL(TouchObjects[objectIndex].groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP); - break; - case Touch_action_e::IncrementGroup: - eventCommand += -2; - break; - case Touch_action_e::DecrementGroup: - eventCommand += -3; - break; - case Touch_action_e::IncrementPage: - eventCommand += -4; - break; - case Touch_action_e::DecrementPage: - eventCommand += -5; - break; - case Touch_action_e::Default: - eventCommand += -1; // Ignore - break; + eventCommand += ','; // Group mode (20) + const uint8_t action = get4BitFromUL(TouchObjects[objectIndex].groupFlags, TOUCH_OBJECT_GROUP_ACTION); + const Touch_action_e actGrp = static_cast(action); + + if (!groupSwitch && (Touch_action_e::Default != actGrp)) { + if (Touch_action_e::ActivateGroup == actGrp) { + eventCommand += get8BitFromUL(TouchObjects[objectIndex].groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP); + } else { + eventCommand += (action * -1); // Default is already ignored } } else { - eventCommand += -1; // No group to activate + eventCommand += -1; // No group to activate } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH } diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index 98cf7de568..e19ee2842c 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -11,6 +11,7 @@ /***** * Changelog: + * 2023-12-31 tonhuisman: Code optimizations reducing .bin size (ESP32) with ~1kB * 2023-10-01 tonhuisman: Re-implement (fix) switching of X/Y/Z vs X/Y output values not by changing the DeviceVector but using * PLUGIN_GET_DEVICEVALUECOUNT plugin function. * 2023-08-15 tonhuisman: Implement Extended CustomTaskSettings, minor improvements From 4c06453ed1e6639437e567f0242389f45d8d11fe Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 31 Dec 2023 21:42:57 +0100 Subject: [PATCH 076/113] [P123] Minor code optimiziations --- src/_P123_FT62x6Touch.ino | 1 + src/src/PluginStructs/P123_data_struct.cpp | 19 +++++++++---------- src/src/PluginStructs/P123_data_struct.h | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index d4656c7eac..4705a03ee4 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -7,6 +7,7 @@ /** * Changelog: + * 2023-12-31 tonhuisman: Code optimizations * 2023-10-01 tonhuisman: Re-implement (fix) switching of X/Y/Z vs X/Y output values using PLUGIN_GET_DEVICEVALUECOUNT, store (also) in task * settings for speed * Implement PLUGIN_I2C_GET_ADDRESS function diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index 6774d0bd1b..fff920f5ed 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -34,10 +34,9 @@ void P123_data_struct::reset() { addLog(LOG_LEVEL_INFO, F("P123 DEBUG Touchscreen reset.")); # endif // PLUGIN_123_DEBUG - if (nullptr != touchscreen) { delete touchscreen; } + delete touchscreen; touchscreen = nullptr; - - if (nullptr != touchHandler) { delete touchHandler; } + delete touchHandler; touchHandler = nullptr; } @@ -149,7 +148,7 @@ bool P123_data_struct::plugin_write(struct EventStruct *event, command = parseString(string, 1); subcommand = parseString(string, 2); - if (isInitialized() && command.equals(F("touch"))) { + if (isInitialized() && equals(command, F("touch"))) { # ifdef PLUGIN_123_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { @@ -158,13 +157,13 @@ bool P123_data_struct::plugin_write(struct EventStruct *event, } # endif // ifdef PLUGIN_123_DEBUG - if (subcommand.equals(F("rot"))) { // touch,rot,<0..3> : Set rotation to 0, 90, 180, 270 degrees + if (equals(subcommand, F("rot"))) { // touch,rot,<0..3> : Set rotation to 0, 90, 180, 270 degrees setRotation(static_cast(event->Par2 % 4)); success = true; - } else if (subcommand.equals(F("flip"))) { // touch,flip,<0|1> : Flip rotation by 0 or 180 degrees + } else if (equals(subcommand, F("flip"))) { // touch,flip,<0|1> : Flip rotation by 0 or 180 degrees setRotationFlipped(event->Par2 > 0); success = true; - } else { // Rest of the commands handled by ESPEasy_TouchHandler + } else { // Rest of the commands handled by ESPEasy_TouchHandler success = touchHandler->plugin_write(event, string); } } @@ -295,7 +294,7 @@ void P123_data_struct::setRotation(uint8_t n) { # ifdef PLUGIN_123_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, strformat(F("P123 DEBUG Rotation set: %d"), _rotation)); + addLogMove(LOG_LEVEL_INFO, concat(F("P123 DEBUG Rotation set: "), _rotation)); } # endif // PLUGIN_123_DEBUG } @@ -337,7 +336,7 @@ int8_t P123_data_struct::getTouchObjectIndex(struct EventStruct *event, if (nullptr != touchHandler) { return touchHandler->getTouchObjectIndex(event, touchObject, isButton); } - return false; + return -1; } /** @@ -399,7 +398,7 @@ void P123_data_struct::scaleRawToCalibrated(int16_t& x, /** * Get the current button group */ -int16_t P123_data_struct::getButtonGroup() { +int16_t P123_data_struct::getButtonGroup() const { if (nullptr != touchHandler) { return touchHandler->getButtonGroup(); } diff --git a/src/src/PluginStructs/P123_data_struct.h b/src/src/PluginStructs/P123_data_struct.h index 5a3c1115f0..7b06b034e4 100644 --- a/src/src/PluginStructs/P123_data_struct.h +++ b/src/src/PluginStructs/P123_data_struct.h @@ -23,7 +23,7 @@ # undef PLUGIN_123_DEBUG # endif // if defined(BUILD_NO_DEBUG) && defined(PLUGIN_123_DEBUG) -# define P123_I2C_ADDRESS (0x38) // Fixed value +# define P123_I2C_ADDRESS (0x38) // Fixed value # define P123_CONFIG_DISPLAY_TASK PCONFIG(0) # define P123_COLOR_DEPTH PCONFIG_LONG(1) @@ -93,7 +93,7 @@ struct P123_data_struct : public PluginTaskData_base void scaleRawToCalibrated(int16_t& x, int16_t& y); - int16_t getButtonGroup(); + int16_t getButtonGroup() const; bool validButtonGroup(int16_t buttonGroup, bool ignoreZero = false); bool setButtonGroup(struct EventStruct *event, From ca3192f60c8bc90b4dc22b5d0c364b79de133b99 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Fri, 12 Jan 2024 23:26:57 +0100 Subject: [PATCH 077/113] [Core] String optimizations --- src/src/DataTypes/ESPEasyFileType.cpp | 2 +- src/src/Helpers/ESPEasy_Storage.cpp | 130 ++++++++------------------ 2 files changed, 38 insertions(+), 94 deletions(-) diff --git a/src/src/DataTypes/ESPEasyFileType.cpp b/src/src/DataTypes/ESPEasyFileType.cpp index 9a2c060c57..a8b3bf4bc0 100644 --- a/src/src/DataTypes/ESPEasyFileType.cpp +++ b/src/src/DataTypes/ESPEasyFileType.cpp @@ -25,7 +25,7 @@ bool isProtectedFileType(const String& filename) const int8_t mPerc = mask.indexOf('%'); if ((mPerc > -1) && fname.startsWith(mask.substring(0, mPerc))) { - for (uint8_t n = 0; n < TASKS_MAX && !isTaskSpecificConfig; n++) { + for (uint8_t n = 0; n < TASKS_MAX && !isTaskSpecificConfig; ++n) { isTaskSpecificConfig |= (fname.equalsIgnoreCase(strformat(mask, n + 1))); } } diff --git a/src/src/Helpers/ESPEasy_Storage.cpp b/src/src/Helpers/ESPEasy_Storage.cpp index 21008e2a96..e941789d20 100644 --- a/src/src/Helpers/ESPEasy_Storage.cpp +++ b/src/src/Helpers/ESPEasy_Storage.cpp @@ -867,10 +867,8 @@ bool getAndLogSettingsParameters(bool read, SettingsType::Enum settingsType, int if (loglevelActiveFor(LOG_LEVEL_DEBUG_DEV)) { String log = read ? F("Read") : F("Write"); - log += F(" settings: "); - log += SettingsType::getSettingsTypeString(settingsType); - log += F(" index: "); - log += index; + log += concat(F(" settings: "), SettingsType::getSettingsTypeString(settingsType)); + log += concat(F(" index: "), index); addLogMove(LOG_LEVEL_DEBUG_DEV, log); } #endif // ifndef BUILD_NO_DEBUG @@ -943,8 +941,7 @@ String LoadStringArray(SettingsType::Enum settingsType, int index, String string } if ((!tmpString.isEmpty()) && (stringCount < nrStrings)) { - result += F("Incomplete custom settings for index "); - result += (index + 1); + result += concat(F("Incomplete custom settings for index "), index + 1); move_special(strings[stringCount], std::move(tmpString)); } return result; @@ -1234,11 +1231,7 @@ String SaveCustomTaskSettings(taskIndex_t TaskIndex, String strings[], uint16_t } String getCustomTaskSettingsError(uint8_t varNr) { - String error = F("Error: Text too long for line "); - - error += varNr + 1; - error += '\n'; - return error; + return strformat(F("Error: Text too long for line %d\n"), varNr + 1); } /********************************************************************************************\ @@ -1501,10 +1494,7 @@ String doSaveToFile(const char *fname, int index, const uint8_t *memAddress, int #ifndef ESP32 if (allocatedOnStack(memAddress)) { - String log = F("SaveToFile: "); - log += fname; - log += F(" ERROR, Data allocated on stack"); - addLog(LOG_LEVEL_ERROR, log); + addLog(LOG_LEVEL_ERROR, strformat(F("SaveToFile: %s ERROR, Data allocated on stack"), fname)); // return log; // FIXME TD-er: Should this be considered a breaking error? } @@ -1513,11 +1503,9 @@ String doSaveToFile(const char *fname, int index, const uint8_t *memAddress, int if (index < 0) { #ifndef BUILD_NO_DEBUG - String log = F("SaveToFile: "); - log += fname; - log += F(" ERROR, invalid position in file"); + const String log = strformat(F("SaveToFile: %s ERROR, invalid position in file"), fname); #else - String log = F("Save error"); + const String log = F("Save error"); #endif addLog(LOG_LEVEL_ERROR, log); return log; @@ -1564,24 +1552,14 @@ String doSaveToFile(const char *fname, int index, const uint8_t *memAddress, int f.close(); #ifndef BUILD_NO_DEBUG if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log; - log.reserve(48); - log += F("FILE : Saved "); - log += fname; - log += F(" offset: "); - log += index; - log += F(" size: "); - log += datasize; - addLogMove(LOG_LEVEL_INFO, log); + addLogMove(LOG_LEVEL_INFO, strformat(F("FILE : Saved %s offset: %d size: %d"), fname, index, datasize)); } #endif } else { #ifndef BUILD_NO_DEBUG - String log = F("SaveToFile: "); - log += fname; - log += F(" ERROR, Cannot save to file"); + const String log = strformat(F("SaveToFile: %s ERROR, Cannot save to file"), fname); #else - String log = F("Save error"); + const String log = F("Save error"); #endif addLog(LOG_LEVEL_ERROR, log); @@ -1605,11 +1583,9 @@ String ClearInFile(const char *fname, int index, int datasize) { if (index < 0) { #ifndef BUILD_NO_DEBUG - String log = F("ClearInFile: "); - log += fname; - log += F(" ERROR, invalid position in file"); + const String log = strformat(F("ClearInFile: %s ERROR, invalid position in file"), fname); #else - String log = F("Save error"); + const String log = F("Save error"); #endif addLog(LOG_LEVEL_ERROR, log); @@ -1635,11 +1611,9 @@ String ClearInFile(const char *fname, int index, int datasize) f.close(); } else { #ifndef BUILD_NO_DEBUG - String log = F("ClearInFile: "); - log += fname; - log += F(" ERROR, Cannot save to file"); + const String log = strformat(F("ClearInFile: %s ERROR, Cannot save to file"), fname); #else - String log = F("Save error"); + const String log = F("Save error"); #endif addLog(LOG_LEVEL_ERROR, log); return log; @@ -1656,11 +1630,9 @@ String LoadFromFile(const char *fname, int offset, uint8_t *memAddress, int data { if (offset < 0) { #ifndef BUILD_NO_DEBUG - String log = F("LoadFromFile: "); - log += fname; - log += F(" ERROR, invalid position in file"); + const String log = strformat(F("LoadFromFile: %s ERROR, invalid position in file"), fname); #else - String log = F("Load error"); + const String log = F("Load error"); #endif addLog(LOG_LEVEL_ERROR, log); return log; @@ -1700,15 +1672,12 @@ String LoadFromFile(const char *fname, int offset, uint8_t *memAddress, int data \*********************************************************************************************/ String getSettingsFileIndexRangeError(bool read, SettingsType::Enum settingsType, int index) { if (settingsType >= SettingsType::Enum::SettingsType_MAX) { - String error = F("Unknown settingsType: "); - error += static_cast(settingsType); - return error; + return concat(F("Unknown settingsType: "), static_cast(settingsType)); } String error = read ? F("Load") : F("Save"); #ifndef BUILD_NO_DEBUG error += SettingsType::getSettingsTypeString(settingsType); - error += F(" index out of range: "); - error += index; + error += concat(F(" index out of range: "), index); #else error += F(" error"); #endif @@ -1719,13 +1688,7 @@ String getSettingsFileDatasizeError(bool read, SettingsType::Enum settingsType, String error = read ? F("Load") : F("Save"); #ifndef BUILD_NO_DEBUG error += SettingsType::getSettingsTypeString(settingsType); - error += '('; - error += index; - error += F(") datasize("); - error += datasize; - error += F(") > max_size("); - error += max_size; - error += ')'; + error += strformat(F("(%d) datasize(%d) > max_size(%d)"), index, datasize, max_size); #else error += F(" error"); #endif @@ -1996,9 +1959,7 @@ String createCacheFilename(unsigned int count) { #ifdef ESP32 fname = '/'; #endif // ifdef ESP32 - fname += F("cache_"); - fname += String(count); - fname += F(".bin"); + fname += strformat(F("cache_%d.bin"), count); return fname; } @@ -2103,9 +2064,7 @@ String getPartitionType(uint8_t pType, uint8_t pSubType) { if (partitionType == ESP_PARTITION_TYPE_APP) { if ((partitionSubType >= ESP_PARTITION_SUBTYPE_APP_OTA_MIN) && (partitionSubType < ESP_PARTITION_SUBTYPE_APP_OTA_MAX)) { - String result = F("OTA partition "); - result += (partitionSubType - ESP_PARTITION_SUBTYPE_APP_OTA_MIN); - return result; + return concat(F("OTA partition "), partitionSubType - ESP_PARTITION_SUBTYPE_APP_OTA_MIN); } switch (partitionSubType) { @@ -2136,26 +2095,12 @@ String getPartitionType(uint8_t pType, uint8_t pSubType) { default: break; } } - String result = F("Unknown("); - result += partitionSubType; - result += ')'; - return result; + return strformat(F("Unknown(%d)"), partitionSubType); } String getPartitionTableHeader(const String& itemSep, const String& lineEnd) { - String result; - - result += F("Address"); - result += itemSep; - result += F("Size"); - result += itemSep; - result += F("Label"); - result += itemSep; - result += F("Partition Type"); - result += itemSep; - result += F("Encrypted"); - result += lineEnd; - return result; + return strformat(F("Address%sSize%sLabel%sPartition Type%sEncrypted%s"), + itemSep.c_str(), itemSep.c_str(), itemSep.c_str(), itemSep.c_str(), lineEnd.c_str()); } String getPartitionTable(uint8_t pType, const String& itemSep, const String& lineEnd) { @@ -2166,16 +2111,17 @@ String getPartitionTable(uint8_t pType, const String& itemSep, const String& lin if (_mypartiterator) { do { const esp_partition_t *_mypart = esp_partition_get(_mypartiterator); - result += formatToHex(_mypart->address); - result += itemSep; - result += formatToHex_decimal(_mypart->size, 1024); - result += itemSep; - result += _mypart->label; - result += itemSep; - result += getPartitionType(_mypart->type, _mypart->subtype); - result += itemSep; - result += (_mypart->encrypted ? F("Yes") : F("-")); - result += lineEnd; + result += strformat(F("%x%s%s%s%s%s%s%s%s%s"), + _mypart->address, + itemSep, + formatToHex_decimal(_mypart->size, 1024), + itemSep, + _mypart->label, + itemSep, + getPartitionType(_mypart->type, _mypart->subtype).c_str(), + itemSep, + String(_mypart->encrypted ? F("Yes") : F("-")).c_str(), + lineEnd); } while ((_mypartiterator = esp_partition_next(_mypartiterator)) != nullptr); } esp_partition_iterator_release(_mypartiterator); @@ -2206,8 +2152,7 @@ String downloadFileType(const String& url, const String& user, const String& pas } } else { if (fileExists(filename)) { - String filename_bak = filename; - filename_bak += F("_bak"); + const String filename_bak = strformat(F("%s_bak"), filename.c_str()); if (fileExists(filename_bak)) { if (!ResetFactoryDefaultPreference.delete_Bak_Files() || !tryDeleteFile(filename_bak)) { return F("Could not rename to _bak"); @@ -2215,8 +2160,7 @@ String downloadFileType(const String& url, const String& user, const String& pas } // Must download it to a tmp file. - String tmpfile = filename; - tmpfile += F("_tmp"); + const String tmpfile = strformat(F("%s_tmp"),filename.c_str()); if (!downloadFile(fullUrl, tmpfile, user, pass, error)) { return error; From bada8daced2e9413c5625d77c6f7daf4b566aff5 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 13 Mar 2024 23:28:59 +0100 Subject: [PATCH 078/113] [TouchHandler] Also reverse the Left/Right navigation when Up/Down navigation is reversed --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 18 +++++++++++------- src/src/Helpers/ESPEasy_TouchHandler.h | 8 +++++--- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index beb14b210d..c18de2345e 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -675,10 +675,10 @@ bool ESPEasy_TouchHandler::displayButton(struct EventStruct *event, state = 1; // always get ON state! if (action == Touch_action_e::DecrementGroup) { // Left arrow - bitWrite(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED, validButtonGroup(buttonGroup - 1, true)); + bitWrite(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED, validButtonGroup(buttonGroup - (pgupInvert ? -1 : 1), true)); } else if (action == Touch_action_e::IncrementGroup) { // Right arrow - bitWrite(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED, validButtonGroup(buttonGroup + 1, true)); + bitWrite(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED, validButtonGroup(buttonGroup + (pgupInvert ? -1 : 1), true)); } else if (action == Touch_action_e::DecrementPage) { // Down arrow or Up arrow bitWrite(TouchObjects[buttonNr].flags, TOUCH_OBJECT_FLAG_ENABLED, validButtonGroup(buttonGroup + (pgupInvert ? 10 : -10), true)); @@ -791,8 +791,10 @@ bool ESPEasy_TouchHandler::setButtonGroup(struct EventStruct *event, * Increment button group if that group exists, if max. group > 0 then min. group = 1 */ bool ESPEasy_TouchHandler::incrementButtonGroup(struct EventStruct *event) { - if (validButtonGroup(_buttonGroup + 1)) { - return setButtonGroup(event, _buttonGroup + 1); + const bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); + + if (validButtonGroup(_buttonGroup + (pgupInvert ? -1 : 1))) { + return setButtonGroup(event, _buttonGroup + (pgupInvert ? -1 : 1)); } return false; } @@ -801,8 +803,10 @@ bool ESPEasy_TouchHandler::incrementButtonGroup(struct EventStruct *event) { * Decrement button group if that group exists, if max. group > 0 then min. group = 1 */ bool ESPEasy_TouchHandler::decrementButtonGroup(struct EventStruct *event) { - if (validButtonGroup(_buttonGroup - 1)) { - return setButtonGroup(event, _buttonGroup - 1); + const bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); + + if (validButtonGroup(_buttonGroup - (pgupInvert ? -1 : 1))) { + return setButtonGroup(event, _buttonGroup - (pgupInvert ? -1 : 1)); } return false; } @@ -1048,7 +1052,7 @@ bool ESPEasy_TouchHandler::plugin_webform_load(struct EventStruct *event) { bitRead(Touch_Settings.flags, TOUCH_FLAGS_DRAWBTN_VIA_RULES)); addFormCheckBox(F("Enable/Disable page buttons"), F("pagebtns"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_AUTO_PAGE_ARROWS)); - addFormCheckBox(F("PageUp/PageDown reversed"), F("pageblw"), + addFormCheckBox(F("Navigation Left/Right/Up/ Down menu reversed"), F("pageblw"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU)); addFormCheckBox(F("Swipe Left/Right/Up/Down menu reversed"), F("swipeswap"), bitRead(Touch_Settings.flags, TOUCH_FLAGS_SWAP_LEFT_RIGHT)); diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index e19ee2842c..35715af0d6 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -11,6 +11,8 @@ /***** * Changelog: + * 2024-03-13 tonhuisman: Change PageUp/PageDown reversed option to Navigation Left/Right/Up/Down menu reversed, to also swap the behavior + * of the left and right navigation buttons, like the Up/Down navigation already had. * 2023-12-31 tonhuisman: Code optimizations reducing .bin size (ESP32) with ~1kB * 2023-10-01 tonhuisman: Re-implement (fix) switching of X/Y/Z vs X/Y output values not by changing the DeviceVector but using * PLUGIN_GET_DEVICEVALUECOUNT plugin function. @@ -71,8 +73,8 @@ * hasgroup,groupNr : Check if group exists, ignores group 0 * enabled,objectName|objectNr : Check if object is enabled * state,objectName|objectNr : Get current object state (buttons: on = 1, off = 0, sliders: value 0..100 (=percentage) or explicit value) - * pagemode : Get the PageUp/PageDown mode, 0 = up=pgup, 1 = up=pgdown - * swipedir,directionId : Get the name for the direction provided in numeric form + * pagemode : Get the Left/Right/Up/Down menu mode, 0 = normal, 1 = reversed + * swipedir,directionId : Get the name for the swipe direction provided in numeric form */ # define TOUCH_DEBUG // Additional debugging information @@ -118,7 +120,7 @@ # define TOUCH_FLAGS_INITIAL_GROUP 8 // Initial group to activate, 8 bits # define TOUCH_FLAGS_DRAWBTN_VIA_RULES 16 // Draw buttons using rule # define TOUCH_FLAGS_AUTO_PAGE_ARROWS 17 // Automatically enable/disable paging buttons -# define TOUCH_FLAGS_PGUP_BELOW_MENU 18 // Group-page below current menu (reverts Up/Down buttons) +# define TOUCH_FLAGS_PGUP_BELOW_MENU 18 // Group-page below current menu (reverts Left/Right/Up/Down menu buttons) # define TOUCH_FLAGS_SWAP_LEFT_RIGHT 19 // Swaps Left and Right, Up and Down swipe directions for menu actions # define TOUCH_FLAGS_IGNORE_TOUCH 20 // Disable touch, use for object/button features only From 377d5ea555d28d90dafed3e63a7ccbacd97dcd8e Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 21 Mar 2024 22:36:36 +0100 Subject: [PATCH 079/113] [TouchHandler] Rename inc/dec subcommands to next/prev --- src/src/Helpers/ESPEasy_TouchHandler.cpp | 48 ++++++++++++------------ src/src/Helpers/ESPEasy_TouchHandler.h | 19 +++++----- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/src/src/Helpers/ESPEasy_TouchHandler.cpp b/src/src/Helpers/ESPEasy_TouchHandler.cpp index c18de2345e..fb27bf617d 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.cpp +++ b/src/src/Helpers/ESPEasy_TouchHandler.cpp @@ -739,30 +739,30 @@ bool ESPEasy_TouchHandler::handleButtonSwipe(struct EventStruct *event, if (swipe == Swipe_action_e::Up) { if (swapped) { - decrementButtonPage(event); + prevButtonPage(event); } else { - incrementButtonPage(event); + nextButtonPage(event); } success = true; } else if (swipe == Swipe_action_e::Right) { if (swapped) { - decrementButtonGroup(event); + prevButtonGroup(event); } else { - incrementButtonGroup(event); + nextButtonGroup(event); } success = true; } else if (swipe == Swipe_action_e::Down) { if (swapped) { - incrementButtonPage(event); + nextButtonPage(event); } else { - decrementButtonPage(event); + prevButtonPage(event); } success = true; } else if ((swipe == Swipe_action_e::Left)) { if (swapped) { - incrementButtonGroup(event); + nextButtonGroup(event); } else { - decrementButtonGroup(event); + prevButtonGroup(event); } success = true; } @@ -790,7 +790,7 @@ bool ESPEasy_TouchHandler::setButtonGroup(struct EventStruct *event, /** * Increment button group if that group exists, if max. group > 0 then min. group = 1 */ -bool ESPEasy_TouchHandler::incrementButtonGroup(struct EventStruct *event) { +bool ESPEasy_TouchHandler::nextButtonGroup(struct EventStruct *event) { const bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); if (validButtonGroup(_buttonGroup + (pgupInvert ? -1 : 1))) { @@ -802,7 +802,7 @@ bool ESPEasy_TouchHandler::incrementButtonGroup(struct EventStruct *event) { /** * Decrement button group if that group exists, if max. group > 0 then min. group = 1 */ -bool ESPEasy_TouchHandler::decrementButtonGroup(struct EventStruct *event) { +bool ESPEasy_TouchHandler::prevButtonGroup(struct EventStruct *event) { const bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); if (validButtonGroup(_buttonGroup - (pgupInvert ? -1 : 1))) { @@ -814,7 +814,7 @@ bool ESPEasy_TouchHandler::decrementButtonGroup(struct EventStruct *event) { /** * Increment button group by page (+10), if max. group > 0 then min. group = 1 */ -bool ESPEasy_TouchHandler::incrementButtonPage(struct EventStruct *event) { +bool ESPEasy_TouchHandler::nextButtonPage(struct EventStruct *event) { const bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); if (validButtonGroup(_buttonGroup + (pgupInvert ? -10 : 10))) { @@ -826,7 +826,7 @@ bool ESPEasy_TouchHandler::incrementButtonPage(struct EventStruct *event) { /** * Decrement button group by page (+10), if max. group > 0 then min. group = 1 */ -bool ESPEasy_TouchHandler::decrementButtonPage(struct EventStruct *event) { +bool ESPEasy_TouchHandler::prevButtonPage(struct EventStruct *event) { const bool pgupInvert = bitRead(Touch_Settings.flags, TOUCH_FLAGS_PGUP_BELOW_MENU); if (validButtonGroup(_buttonGroup + (pgupInvert ? 10 : -10))) { @@ -1948,14 +1948,14 @@ bool ESPEasy_TouchHandler::plugin_write(struct EventStruct *event, # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE } else if (equals(subcommand, F("setgrp"))) { // touch,setgrp, : Activate button group success = setButtonGroup(event, event->Par2); - } else if (equals(subcommand, F("incgrp"))) { // touch,incgrp : increment group and Activate - success = incrementButtonGroup(event); - } else if (equals(subcommand, F("decgrp"))) { // touch,decgrp : Decrement group and Activate - success = decrementButtonGroup(event); - } else if (equals(subcommand, F("incpage"))) { // touch,incpage : increment page and Activate - success = incrementButtonPage(event); - } else if (equals(subcommand, F("decpage"))) { // touch,decpage : Decrement page and Activate - success = decrementButtonPage(event); + } else if (equals(subcommand, F("nextgrp"))) { // touch,nextgrp : next group and Activate + success = nextButtonGroup(event); + } else if (equals(subcommand, F("prevgrp"))) { // touch,prevgrp : previous group and Activate + success = prevButtonGroup(event); + } else if (equals(subcommand, F("nextpage"))) { // touch,nextpage : next page and Activate + success = nextButtonPage(event); + } else if (equals(subcommand, F("prevpage"))) { // touch,prevpage : previous page and Activate + success = prevButtonPage(event); } else if (equals(subcommand, F("updatebutton"))) { // touch,updatebutton,[,[,]] : Update a button arguments = parseString(string, 3); @@ -2197,16 +2197,16 @@ void ESPEasy_TouchHandler::generateObjectEvent(struct EventStruct *event, setButtonGroup(event, get8BitFromUL(TouchObjects[objectIndex].groupFlags, TOUCH_OBJECT_GROUP_ACTIONGROUP)); break; case Touch_action_e::IncrementGroup: - incrementButtonGroup(event); + nextButtonGroup(event); break; case Touch_action_e::DecrementGroup: - decrementButtonGroup(event); + prevButtonGroup(event); break; case Touch_action_e::IncrementPage: - incrementButtonPage(event); + nextButtonPage(event); break; case Touch_action_e::DecrementPage: - decrementButtonPage(event); + prevButtonPage(event); break; case Touch_action_e::Default: break; diff --git a/src/src/Helpers/ESPEasy_TouchHandler.h b/src/src/Helpers/ESPEasy_TouchHandler.h index 35715af0d6..b91764694d 100644 --- a/src/src/Helpers/ESPEasy_TouchHandler.h +++ b/src/src/Helpers/ESPEasy_TouchHandler.h @@ -2,15 +2,14 @@ #define HELPERS_ESPEASY_TOUCHHANDLER_H #include "../../_Plugin_Helper.h" -#include "../../ESPEasy_common.h" #include "../Helpers/AdafruitGFX_helper.h" #ifdef PLUGIN_USES_TOUCHHANDLER -# include "../Commands/InternalCommands.h" # include /***** * Changelog: + * 2024-03-20 tonhuisman: Change inc/dec* commands to next/prev* commands to more accurately describe their function * 2024-03-13 tonhuisman: Change PageUp/PageDown reversed option to Navigation Left/Right/Up/Down menu reversed, to also swap the behavior * of the left and right navigation buttons, like the Up/Down navigation already had. * 2023-12-31 tonhuisman: Code optimizations reducing .bin size (ESP32) with ~1kB @@ -60,10 +59,10 @@ * touch,toggle,[,...] : Switch TouchButton(s) to the other state (must be enabled) * touch,swipe, : Switch button group according to swipe direction * touch,setgrp, : Switch to button group - * touch,incgrp : Switch to next button group - * touch,decgrp : Switch to previous button group - * touch,incpage : Switch to next button group page (+10) - * touch,decpage : Switch to previous button group page (-10) + * touch,nextgrp : Switch to next button group + * touch,prevgrp : Switch to previous button group + * touch,nextpage : Switch to next button group page (+10) + * touch,prevpage : Switch to previous button group page (-10) * touch,updatebutton,[,[,]] : Update a button by name or number */ /** @@ -346,10 +345,10 @@ class ESPEasy_TouchHandler { # endif // if TOUCH_FEATURE_EXTENDED_TOUCH && TOUCH_FEATURE_SWIPE bool setButtonGroup(struct EventStruct *event, const int16_t & buttonGroup); - bool incrementButtonGroup(struct EventStruct *event); - bool decrementButtonGroup(struct EventStruct *event); - bool incrementButtonPage(struct EventStruct *event); - bool decrementButtonPage(struct EventStruct *event); + bool nextButtonGroup(struct EventStruct *event); + bool prevButtonGroup(struct EventStruct *event); + bool nextButtonPage(struct EventStruct *event); + bool prevButtonPage(struct EventStruct *event); void displayButtonGroup(struct EventStruct *event, const int16_t & buttonGroup, const int8_t & mode = 0); From 543645bdc7a9bec32d779272eaa38e3c1cd4b694 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 21 Mar 2024 22:39:05 +0100 Subject: [PATCH 080/113] [P123] Refactor increment/decrement functions to next/prev in line with TouchHandler --- src/_P123_FT62x6Touch.ino | 1 + src/src/CustomBuild/define_plugin_sets.h | 2 +- src/src/PluginStructs/P123_data_struct.cpp | 25 ++++++++-------------- src/src/PluginStructs/P123_data_struct.h | 10 ++++----- 4 files changed, 15 insertions(+), 23 deletions(-) diff --git a/src/_P123_FT62x6Touch.ino b/src/_P123_FT62x6Touch.ino index 4705a03ee4..e4796f38fc 100644 --- a/src/_P123_FT62x6Touch.ino +++ b/src/_P123_FT62x6Touch.ino @@ -7,6 +7,7 @@ /** * Changelog: + * 2024-03-21 tonhuisman: Refactor increment/decrement to next/prevButtonGroup/Page functions, to align with ESPEasy_TouchHandler * 2023-12-31 tonhuisman: Code optimizations * 2023-10-01 tonhuisman: Re-implement (fix) switching of X/Y/Z vs X/Y output values using PLUGIN_GET_DEVICEVALUECOUNT, store (also) in task * settings for speed diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index 10a0b3d463..38ef6b5575 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -2373,7 +2373,7 @@ To create/register a plugin, you have to : #define DISABLE_SOFTWARE_SERIAL #endif -#if defined(USES_P095) || defined(USES_P096) || defined(USES_P116) || defined(USES_P131) || defined(USES_P141) // Add any plugin that uses AdafruitGFX_Helper +#if defined(USES_P095) || defined(USES_P096) || defined(USES_P116) || defined(USES_P131) || defined(USES_P141) || defined(USES_P123) // Add any plugin that uses AdafruitGFX_Helper #ifndef PLUGIN_USES_ADAFRUITGFX #define PLUGIN_USES_ADAFRUITGFX // Ensure AdafruitGFX_helper is available for graphics displays (only) #endif diff --git a/src/src/PluginStructs/P123_data_struct.cpp b/src/src/PluginStructs/P123_data_struct.cpp index fff920f5ed..513b021182 100644 --- a/src/src/PluginStructs/P123_data_struct.cpp +++ b/src/src/PluginStructs/P123_data_struct.cpp @@ -2,14 +2,7 @@ #ifdef USES_P123 -# include "../ESPEasyCore/ESPEasyNetwork.h" - -# include "../Helpers/ESPEasy_Storage.h" -# include "../Helpers/Scheduler.h" -# include "../Helpers/StringConverter.h" -# include "../Helpers/SystemVariables.h" - -# include "../Commands/InternalCommands.h" +# include "../Helpers/AdafruitGFX_helper.h" /** * Constructor @@ -436,11 +429,11 @@ bool P123_data_struct::setButtonGroup(struct EventStruct *event, /** * Increment button group, if max. group > 0 then min. group = 1 */ -bool P123_data_struct::incrementButtonGroup(struct EventStruct *event) { +bool P123_data_struct::nextButtonGroup(struct EventStruct *event) { # if TOUCH_FEATURE_EXTENDED_TOUCH if (nullptr != touchHandler) { - return touchHandler->incrementButtonGroup(event); + return touchHandler->nextButtonGroup(event); } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH return false; @@ -449,11 +442,11 @@ bool P123_data_struct::incrementButtonGroup(struct EventStruct *event) { /** * Decrement button group, if max. group > 0 then min. group = 1 */ -bool P123_data_struct::decrementButtonGroup(struct EventStruct *event) { +bool P123_data_struct::prevButtonGroup(struct EventStruct *event) { # if TOUCH_FEATURE_EXTENDED_TOUCH if (nullptr != touchHandler) { - return touchHandler->decrementButtonGroup(event); + return touchHandler->prevButtonGroup(event); } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH return false; @@ -462,11 +455,11 @@ bool P123_data_struct::decrementButtonGroup(struct EventStruct *event) { /** * Increment button group page (+10), if max. group > 0 then min. group page (+10) = 1 */ -bool P123_data_struct::incrementButtonPage(struct EventStruct *event) { +bool P123_data_struct::nextButtonPage(struct EventStruct *event) { # if TOUCH_FEATURE_EXTENDED_TOUCH if (nullptr != touchHandler) { - return touchHandler->incrementButtonPage(event); + return touchHandler->nextButtonPage(event); } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH return false; @@ -475,11 +468,11 @@ bool P123_data_struct::incrementButtonPage(struct EventStruct *event) { /** * Decrement button group page (-10), if max. group > 0 then min. group = 1 */ -bool P123_data_struct::decrementButtonPage(struct EventStruct *event) { +bool P123_data_struct::prevButtonPage(struct EventStruct *event) { # if TOUCH_FEATURE_EXTENDED_TOUCH if (nullptr != touchHandler) { - return touchHandler->decrementButtonPage(event); + return touchHandler->prevButtonPage(event); } # endif // if TOUCH_FEATURE_EXTENDED_TOUCH return false; diff --git a/src/src/PluginStructs/P123_data_struct.h b/src/src/PluginStructs/P123_data_struct.h index 7b06b034e4..ebbd7349be 100644 --- a/src/src/PluginStructs/P123_data_struct.h +++ b/src/src/PluginStructs/P123_data_struct.h @@ -5,8 +5,6 @@ #ifdef USES_P123 -# include "../../ESPEasy_common.h" -# include "../Helpers/AdafruitGFX_helper.h" # include "../Helpers/ESPEasy_TouchHandler.h" # include @@ -98,10 +96,10 @@ struct P123_data_struct : public PluginTaskData_base bool ignoreZero = false); bool setButtonGroup(struct EventStruct *event, int16_t buttonGroup); - bool incrementButtonGroup(struct EventStruct *event); - bool decrementButtonGroup(struct EventStruct *event); - bool incrementButtonPage(struct EventStruct *event); - bool decrementButtonPage(struct EventStruct *event); + bool nextButtonGroup(struct EventStruct *event); + bool prevButtonGroup(struct EventStruct *event); + bool nextButtonPage(struct EventStruct *event); + bool prevButtonPage(struct EventStruct *event); void displayButtonGroup(struct EventStruct *event, int16_t buttonGroup, int8_t mode = 0); From 0d251ecf2dfe28c3f259b3de18ba4b0c8fbce800 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 21 Mar 2024 22:42:07 +0100 Subject: [PATCH 081/113] [P123][TouchHandler] Add documentation --- docs/source/Plugin/P123.rst | 79 ++++++ .../Plugin/P123_DeviceConfiguration.png | Bin 0 -> 24566 bytes .../Plugin/Touch_CalibrationSettings.png | Bin 0 -> 10244 bytes .../Plugin/Touch_ColorSelectionPart.png | Bin 0 -> 5990 bytes docs/source/Plugin/Touch_Configuration.repl | 246 ++++++++++++++++++ .../Plugin/Touch_DeviceConfiguration.png | Bin 0 -> 108830 bytes .../Plugin/Touch_DisplayColordepthOptions.png | Bin 0 -> 39996 bytes docs/source/Plugin/Touch_EventsOptions.png | Bin 0 -> 32329 bytes .../Plugin/Touch_GroupLayoutExample.png | Bin 0 -> 26062 bytes docs/source/Plugin/Touch_Touch_Action.png | Bin 0 -> 7655 bytes docs/source/Plugin/Touch_Touch_Border.png | Bin 0 -> 1795 bytes docs/source/Plugin/Touch_Touch_Caption.png | Bin 0 -> 1998 bytes docs/source/Plugin/Touch_Touch_Color.png | Bin 0 -> 4564 bytes docs/source/Plugin/Touch_Touch_Disabled.png | Bin 0 -> 1763 bytes docs/source/Plugin/Touch_Touch_Layout.png | Bin 0 -> 14996 bytes docs/source/Plugin/Touch_Touch_Name.png | Bin 0 -> 1888 bytes docs/source/Plugin/Touch_Touch_On.png | Bin 0 -> 1319 bytes docs/source/Plugin/Touch_Touch_Position.png | Bin 0 -> 3189 bytes docs/source/Plugin/Touch_Touch_Shape.png | Bin 0 -> 7387 bytes docs/source/Plugin/Touch_commands.repl | 109 ++++++++ docs/source/Plugin/Touch_config_values.repl | 44 ++++ docs/source/Plugin/Touch_events.repl | 44 ++++ docs/source/Plugin/_Plugin.rst | 1 + docs/source/Plugin/_plugin_categories.repl | 2 +- .../Plugin/_plugin_substitutions_p12x.repl | 13 + docs/source/Reference/Command.rst | 5 + docs/source/Reference/Events.rst | 5 + 27 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 docs/source/Plugin/P123.rst create mode 100644 docs/source/Plugin/P123_DeviceConfiguration.png create mode 100644 docs/source/Plugin/Touch_CalibrationSettings.png create mode 100644 docs/source/Plugin/Touch_ColorSelectionPart.png create mode 100644 docs/source/Plugin/Touch_Configuration.repl create mode 100644 docs/source/Plugin/Touch_DeviceConfiguration.png create mode 100644 docs/source/Plugin/Touch_DisplayColordepthOptions.png create mode 100644 docs/source/Plugin/Touch_EventsOptions.png create mode 100644 docs/source/Plugin/Touch_GroupLayoutExample.png create mode 100644 docs/source/Plugin/Touch_Touch_Action.png create mode 100644 docs/source/Plugin/Touch_Touch_Border.png create mode 100644 docs/source/Plugin/Touch_Touch_Caption.png create mode 100644 docs/source/Plugin/Touch_Touch_Color.png create mode 100644 docs/source/Plugin/Touch_Touch_Disabled.png create mode 100644 docs/source/Plugin/Touch_Touch_Layout.png create mode 100644 docs/source/Plugin/Touch_Touch_Name.png create mode 100644 docs/source/Plugin/Touch_Touch_On.png create mode 100644 docs/source/Plugin/Touch_Touch_Position.png create mode 100644 docs/source/Plugin/Touch_Touch_Shape.png create mode 100644 docs/source/Plugin/Touch_commands.repl create mode 100644 docs/source/Plugin/Touch_config_values.repl create mode 100644 docs/source/Plugin/Touch_events.repl diff --git a/docs/source/Plugin/P123.rst b/docs/source/Plugin/P123.rst new file mode 100644 index 0000000000..40f9d653b3 --- /dev/null +++ b/docs/source/Plugin/P123.rst @@ -0,0 +1,79 @@ +.. include:: ../Plugin/_plugin_substitutions_p12x.repl +.. _P123_page: + +|P123_typename| +================================================== + +|P123_shortinfo| + +Plugin details +-------------- + +Type: |P123_type| + +Name: |P123_name| + +Status: |P123_status| + +GitHub: |P123_github|_ + +Maintainer: |P123_maintainer| + +Used libraries: |P123_usedlibraries| + +Supported hardware +------------------ + +Some displays are available with a touch overlay mounted on top of the visible side of the display. There's a choice of resistive and capacitive touch overlays. The FT62x6 series of touch overlays are of the Capacitive kind, being very easy to interact with, comparable to modern smartphones, where you can use a finger for the interaction. (Resistive touch overlays usually require a special pen to be used, or pressed with a finger-nail, to get a response.) + +The FT62x6 touch overlay (or touch screen), can be found on several displays, f.e. some M5Stack devices and the WT32-SC01 display unit. + +Device configuration +-------------------- + +.. image:: P123_DeviceConfiguration.png + +* **Name**: Required by ESPEasy, must be unique among the list of available devices/tasks. + +* **Enabled**: The device can be disabled or enabled. When not enabled the device should not use any resources. + +I2C options +^^^^^^^^^^^ + +The available settings here depend on the build used. At least the **Force Slow I2C speed** option is available, but selections for the I2C Multiplexer can also be shown. For details see the :ref:`Hardware_page` + +Device Settings +^^^^^^^^^^^^^^^ + +* **Display task**: Select the display task the touch screen is mounted on. By default, the current task is selected (and ignored), as any other guess would be wrong, and there is no 'None' selection available. + +When choosing the correct task, the current display resolution, color depth and rotation settings are tried to be fetched from that task, and copied here in the matching settings. If no settings can be obtained, defaults will be applied. + +The configured display will be used to draw the objects, if any, that can be configured below, on. + +* **Screen Width (px) (x)**: Width of the display, the (horizontal) ``x`` coordinate, in pixels. + +* **Screen Height (px) (y)**: Height of the display, the (vertical) ``y`` coordinate, in pixels. Top/Left coordinate is 0,0. + +* **Rotation**: The rotation setting should match the rotation setting of the display, and can be selected as Normal (0), +90, +180 or +270 degrees. + +* **Display Color-depth**: If the display settings have been retrieved successfully, this setting can not be changed, but will be used from the display. This value is used to select the correct color mappings for displaying the Objects. + +* **Touch minimum pressure**: This setting determines the relative pressure or sensitivity of the touch display. Lower values make it more sensitive. The range is 0 to 255. + +.. include:: Touch_Configuration.repl + + +Change log +---------- + +.. versionadded:: 2.0 + ... + + |added| + Initial release version. + + + + + diff --git a/docs/source/Plugin/P123_DeviceConfiguration.png b/docs/source/Plugin/P123_DeviceConfiguration.png new file mode 100644 index 0000000000000000000000000000000000000000..52b8fca5e36bcaa16243fd2d1b4be58933c0e91c GIT binary patch literal 24566 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 S?GcC)a#j|$#|zCb-TE8ILqT`| literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/Touch_CalibrationSettings.png b/docs/source/Plugin/Touch_CalibrationSettings.png new file mode 100644 index 0000000000000000000000000000000000000000..9245158d246b3f1649ac8a08fa52783473e7c8b3 GIT binary patch literal 10244 zcmdsd3slnC`l!_$ZA$TUno}A+>R9PC`A8>GOs%mpv$V3Z^4XZ0C88*bB4Xo=Ok-(j zY5B~_(tKe)Kn20VQbVOQHAO&tkrEFdfG^v*FE>HbuUX{|MqY1Z}0E@ z?eE*yPq;X0tlP3qMMXu!>BzxTDk^GlpxwDv75Ih~CmjKPRDX1KJP7;(pE(B-7Wnx( z^vL;e6%|m^(*LTv-+{nDr+37ma}fvq!~GCJp%Fpw5EZ}62sq4NW8EGzV>1h5b2Ibr zx9;9*ZU(h5hg$5>Sm*HZ_10tlffxOvx4H*~T!KewtaCqYVQ%c^V7AA2kD1k0@hfHvd2HlnEir zx%`Qtp&wtna~896;m-Eh=`-PTc*>JF?;NRv1W6*}v6nlhX}tnutlR-HcW7ZQYobIV zXkl4?-l-GR+XF}BU%XPTtClk!Ka-qDWSRAg&wj$h>D0F4Ab9VZ_9T2UJgy^$!qlq` zS1pLz`)7ONV{@+{c4RNj9kdEW>7^FyJytmu zQ0a?A zqI`&5h1P(pIWWe|+9l)dp;ir^{bT0GZ9wp5Ay!AVIgdF#D#HCC1*5<&gfZ_%-Z-d> zAmp2z%{KdN8$8{G6)nfnAoy_9U^$-oCrWoBZfe2}RSS?M+>scwJ|-Rcqgj^v5er_c zrY%nxon`GA7E+t3-L{k1o=g19pVZ`cO=1FdGe6Xpqv_auW^+2T6fl^fC`k zH-6@|D?)UH8eLDD{g__DL}-oH#?m}K)6~fVkl;U*WIN`K`kxkh690eu$z<1xVhgjm z#>>t9tJLAEmYeH*KWn=R&11PQezg{R#TtCmDBiT(`9LL+xZG0tiy8SZ%T1$ru5tD) zO?|u5_KI0Q$@@1CCnhG2rV6WLykxr-UItj&0OahjR~-(X#S+AxTv1j`uzT zXOo}UVdl3xMO|nxiJ6=7myEQgO~@ByVKvKBl7T5-y+sf7BrvV4ZWo*?R;!gZetczh z16VhH{zR)pj~I6DA5M>EhF&CfZ@cpmsdsrFU!r&D?mB;}wVPiQ{Z4%WFr% z-<)=pebKAMJ_`M1g(x6D)Oy24@!8IAFD=t1^qw}2$()!U`n~==GJ4uFH;BaKGcp`g8>2WG%H-04~aio|~)Vn^bAvc-g%3ybC#R(%L7- ze_~RD#vD-lD#k&!SZTgy5Jh@ETz#Q~*A=2h2ZMelO&2hp#Yw^JxC{DvbI-bZ1wo7w zoJ633bBeHr2RoD?B`n4s zf{=Y1b)i2Rplbj9^a!asURro%E1GXg)ivZ%a%kZ;5HY=iNQA!7JZE^$Tw+k zcd46)C)80Q8wnI4AbIaF%u3>9&oZ*x}f+x+7Z5o(Z?vfRKG2}8~MoiEx zM)v0Do_?7JaiDoVltoV|W}2q`n^r}UVIHPgV0M7edFoyh=LMg^DlC&3hG)6cTRj=Y zig=3#FOn_ACjgbj?zPZj6F5lHoRIlA00qu_Sgtrhl$Q@6?~7jCjfm`CGZ1pw!(B6k z-!(K>|H%wln3G{@g<<=s%YYu_fPw~ z=)ciGWHGDcghwf7$XM&}4!2SLVH9=a{cF5;eil!_BZLgF$Ke1GYmxf&ELq-)=;FfW zXBo4y%-vDveIij=ebbSZA-72U(d{`uN-CtT*!ivrvsZ2jOz5&-h^O5E!PD~HeHKUo z_x9=p!#X(OD*ZH?+~{3?j9o3aeb;c&HJPpVdL1eU|lLH|TZdMRzQ! zJ2vGi+^ARMgy~DwggUMu-G59K8)nw4!BM5ZQXf&J@eaq;_N8g>v?M--RM2-v8C3-a zdWInK64%f_3|63BVm2SewODkdp>GTeoG&F3&k#RE9Dy#%%yc4O0)(E_=(U|%n4>X; zGP!@XGA<@1T55eH9oR7-eFM`tiQijh5<#BK6fVHR(Y$gKi@gJPBTOxpeWz91h&b{Q z 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 S&Go;3mpfc`vBg~qxcxs+=%4)n literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/Touch_Configuration.repl b/docs/source/Plugin/Touch_Configuration.repl new file mode 100644 index 0000000000..889eba89f1 --- /dev/null +++ b/docs/source/Plugin/Touch_Configuration.repl @@ -0,0 +1,246 @@ + + +Touch configuration +^^^^^^^^^^^^^^^^^^^ + +.. note:: This part of the configuration describes the generic ESPEasy Touch Helper module, used for all touch-screen plugins. + +.. image:: Touch_DeviceConfiguration.png + + +* **Flip rotation 180°**: This checkbox allowes to compensate for when the touch overlay is mounted rotated on the display. + +* **Events**: Select the events that should be generated on touch actions. + +.. image:: Touch_EventsOptions.png + +* *None*: No events will be generated. + +* *X and Y*: Only an event with the X and Y positions on touch will be generated. + +* *X, Y and Z*: Similar to the X and Y event, but with the Z value, pressure strength, added. + +* *Objectnames and Button groups*: An event is generated only when a defined object, see below, is touched, or a button group is changed. + +* *Objectnames, Button groups, X and Y*: An event is generated when a defined object is touched, or a button group is changed. Also an event with the X and Y touch position is generated. + +* *Objectnames, Button groups, X, Y and Z*: An event is generated when a defined object is touched, or a button group is changed. Also an event with the X and Y touch position and Z pressure strength is generated. + +.. .. separator + +* **Draw buttons when started**: When enabled the objects (often touch-buttons) will be drawn when the plugin is started. This requires that the display is already initialized, so that should have a lower task number than this task. + +* **Prevent duplicate events**: Suppress duplicate events. + +* **Ignore touch-screen**: When enabled will not get any touch data from the touch-screen, so the plugin can be used to draw objects, and control them using other buttons, usually below or at a side of the display. For example when using a M5Stack Core, that doesn't have a touch-screen, but does have 3 buttons below the display. + +Calibration +^^^^^^^^^^^ + +To be able to adjust for an improper aligned touch overlay on a display, there is an option for calibration available. When changing the **Calibrate to screen resolution** setting from No to Yes, the page will be submitted, and the calibration input fields will be available. + +.. image:: Touch_CalibrationSettings.png + +* **Top-left**: Enter the adjusted X and Y position for the most top/left position that matches with the touch screen. + +* **Bottom-right**: Enter the adjusted X and Y position for the most bottom/right position that matches with the touch screen. + +To get accurate values, a touch-pen could be used to touch the screen, and enabling the **Enable logging for calibration** option will add Info logging with the exact coordinates that are touched. It might help to set the **Events** temporarily to *None*. + +NB: This calibration is usually only needed for Resistive touch panels, that can also use this generic TouchHelper module, so for a Capacitive touch screen it can be set to No. + +Object settings +^^^^^^^^^^^^^^^ + +This section has some generic and default settings for the Touch Objects defined below. + +.. note:: *Definition*: **An On/Off button is any defined object that's not a slider (Layout)** + +* **Default On/Off button colors**: The default state-colors to be used for On/Off buttons: On, Off, Border, Caption, Disabled, Disabled caption. + +The colors can be selected by name (limited set available), ``#RRGGBB`` (24 bit) value or ``#hhhh`` (RGB565) (16 bit) value (and that's also how they are stored). + +Color input fields have a list of predefined color names available, that can be filtered/selected by typing a part of the name, or using the dropdown arrow in the input field (Chromium-based browsers): + +.. image:: Touch_ColorSelectionPart.png + +To select another value from the list, the input has to be (partially) cleared before it will show other options. + +Customized colors can be selected from a color picker that supports RGB565 color selection. For example `here `_ + +* **Initial button group**: Select the button group that should be activated on plugin start (Button groups will be explained below). Should be left at 0 if no button groups are used. + +* **Draw buttons via Rules**: This setting should be left disabled, as it requires specific rules for drawing the buttons on the display. Documentation to be added. + +* **Enable/Disable page buttons**: Paging buttons (navigation, + or - 10 action), disabled by default, can be enabled here, when they need to be used. + +* **Navigation Left/Right/Up/Down menu reversed**: Navigation across button groups can be seen as 'Moving the view up, down, left or right' (normal) or 'Moving the buttons behind the viewport' (reversed). Enabling this setting will also revert the ``nextgrp``, ``prevgrp``, ``nextpage`` and ``prevpage`` subcommands, to move in the other direction. + +* **Swipe Left/Right/Up/Down menu reversed**: Similar to the navigation buttons, this setting is for reversing the swipe actions left/right and up/down. Swipe actions have to be processed in Rules, no default actions are currently available. + +* **Debounce delay for On/Off buttons**: The minimum time a button has to be touched before it's toggled. + +* **Minimal swipe movement**: When dragging this amount of pixels a swipe action is recognized. + +* **Maximum swipe margin**: The margin for a swipe to be completed. + +Touch objects +^^^^^^^^^^^^^ + +For each column, or related group of columns, a description is available: + +.. image:: Touch_Touch_On.png + +* **#**: The (internal) object number, can be used to address an object in the commands (see below). + +* **On**: Enable or Disable the button (shown disabled and not state changed when touched). A Slide control that is set to disabled, can not be changed by dragging the handle up/down or left/right (depending on orientation), but the value **can** be set using the ``touch,set,...`` command, see below. + +.. image:: Touch_Touch_Name.png + +* **Objectname**: The name is a required field, when this is left or made empty, **the object will be removed from the list!** when saved. When no Caption is set, this will be used as the caption, with any underscore characters ``_`` replaced by a space. + +* **Button-group**: For paging to work, buttons (objects) have to be placed in Button groups. Buttons in group 0 are *always* drawn, so buttons in group 0 are intended to be used as global navigation buttons, or non-menu buttons/objects. + +.. image:: Touch_Touch_Position.png + +* **Top-left x**: The X position for the top/left corner of the button. + +* **Width**: The width of the button. + +* **Top-left y**: The Y position for the top/left corner of the button. + +* **Height**: The height of the button. + +(All positions and sizes are in pixels) + +.. image:: Touch_Touch_Shape.png + +* **Button**: This selection defines the shape of the button on screen: + +* *None*: No button is drawn. + +* *Square*: A square button is drawn. + +* *Rounded*: A rounded button with a corner radius of 5% of the longest side is drawn. + +* *Circle*: A circle or ellipsis is drawn within the width/height defined. + +* *Arrow, left*, *Arrow, up*, *Arrow, right*, *Arrow, down*: A triangle button, pointing in the direction as named, is drawn. To be used as navigation or +/- button. + +.. .. separator + +* **Inverted**: Invert the On/Off values for the button, so the value can immediately be used to set f.e. a GPIO state. + +.. image:: Touch_Touch_Layout.png + +* **Layout**: Select the layout of the caption within the button, it determines a) the caption alignment of the button, or b) the button to be a bitmap-button or c) the button to behave like a slide control: + +* *Centered*, *Left-aligned*, *Top-aligned*, *Right-aligned*, *Bottom-aligned*, *Left-Top-aligned*, *Right-Top-aligned*, *Left-Bottom-aligned*, *Right-Bottom-aligned*: Align the caption as the layout implies. + +* *No caption*: Just draw a button without any caption. + +* *Bitmap image*: Draw a bitmap, starting at the left-top, instead of a caption. The color is drawn first, so when using a smaller bitmap, the surface color is still reflecting the On/Off state. The name of the bitmap is to be entered in the ON caption/OFF caption fields, and can be *prepended* with an x/y offset in pixels to 'move' the bitmap to a desired position on the button. Example: ``5,5,shape.bmp`` will draw shape.bmp starting at offset 5,5 from the left top of the button. + +* *Slide control*: Draw the configured button shape, and include a slide-bar that can be swiped up/down or left/right, determined by the width/height. In the center of the button, the current value is shown. By default the range is 0..100%, but the min/max values can be set by entering a ``,`` value-pair in the **OFF caption** field. The direction can be reverted by swapping the ```` and ```` values. An initial value can be set in the **ON caption** field. These values can use decimals for fine control, f.e. ``18.5,24.9``. + +.. .. separator + +* **Font scale**: Sets the font-size of the currently active font for drawing the captions on a button, or the value of the slide control. Range 0..10, 0 works as if 1 was set. + +.. image:: Touch_Touch_Color.png + +* **ON color**: A non-default ON color can be set here. + +* **OFF color**: A non-default OFF color can be set here. + +.. image:: Touch_Touch_Caption.png + +* **ON caption**: The caption to show if the button-state is ON. When empty, and the layout is not set to No caption, the Objectname will be used as ON caption. Any underscores ``_`` will be replaced by a space. + +* **OFF caption**: The caption to show if the button-state is OFF. When empty, the ON caption will also be used for the OFF state, though the color(s) will change to either the default or configured OFF color. + +Also, variables can be used for ON and OFF captions, that will be evaluated when the button is (re-)drawn. The caption content can be updated from rules, see the ``touch,updatebutton...`` command, below. + +For a Bitmap layout, the name of the bitmap file should be set in the ON caption field, and for the Off state, another bitmap (or the same if it is to be used as a single-action button) filename can be entered. To shift the bitmap across the button, an x and or y offset for the image can be prepended to the filename, like ``[,[,]]``. The x/y offset will be applied from the top/left position of the button. + +.. image:: Touch_Touch_Border.png + +* **Border color**: The color to use for drawing a 1 pixel border around the button. When not set the default border color is used, and when that's also not set, no border is drawn. + +* **Caption color**: A specific caption color, instead of the default Caption color, can be used. + +.. image:: Touch_Touch_Disabled.png + +* **Disab. cap clr**: Disabled-caption color, the caption color to use when the button is disabled. + +* **Disabled clr**: Disabled color, the button surface color when the button is disabled. When empty, the default disabled color is used. + +.. image:: Touch_Touch_Action.png + +* **Touch action**: What action to perform when the object is activated (touched). + +* *Default*: The regular On/Off action will be applied. + +* *Activate Group*: Activate the group configured in **Action group**. For the Home button this could activate group 1, the first set of buttons. + +* *Next Group*: Increment the currently active group by 1, if that group is defined. + +* *Previous Group*: Decrement the currently active group by 1, unless that would activate group 0, as group 0 is always active. + +* *Next Page (+10)*: Increment the current group by 10, to go to the next 'page', if that group is defined. + +* *Previous Page (-10)*: Decrement the current group by 10, to go to the previous 'page', if that group is defined, and \> 0. + +.. .. separator + +The Next/Previous Group and Page buttons will be automatically enabled and disabled, based on availability of the Button Group they are supposed to jump to. + +.. .. separator + +* **Action group**: The group to activate for the *Activate group* touch action. + +Button groups +~~~~~~~~~~~~~ + +To show a larger number of button-like touch objects in a small space, button groups have been designed. + +The basic idea is that all buttons in a group are displayed when the group is activated. To be able to navigate from group to group, some navigation controls are required, and these reside by default in the always visible group 0, though they can also be included in a group, but would immediately disappear when touched and a different group is activated. Groups are identified by positive numbers in range 0..255. + +To enable not only linear navigation across these groups, paging is also implemented, where switching to the next page implies adding or subtracting 10 to/from the current group number. + +The navigation buttons check themselves if it is possible to navigate in their configured direction, and depending on a group they can navigate to, the button is disabled or enabled accordingly. These navigation buttons will ignore switching to group 0, as that's the group these navigation buttons should be in. + +Example layout with Groups and Pages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Below image shows a possible menu system, to be displayed in a small area on the display. The numbers on the buttons are the Button group they belong to. There is only 1 group visible, and of course the navigation- or base-group 0. 2 button sizes are used, defining smaller buttons makes it quite hard to show a meaningful caption on each button. + +The display that is used for this setup has a resolution of 480x320 pixels, and is used in landscape mode. The menu is drawn at the bottom of the display, where the height of all buttons is 49 (pixels). The small buttons, like in group 1, have a width of 99, and not 100, to have a 1 pixel gap between the buttons that makes it look better then when they are flush next to each other. The wide buttons, like group 11, are 149 pixels wide. + +The navigation buttons are all 49x49 pixels, and except for the Home button, have a triangle shape matching their direction. The navigation, by default, is 'moving' the [Visible area] window across the menu sections, though enabling the **Navigation Left/Right/Up/Down menu reversed** option, above, will change that to virtually move the buttons behind the viewport. + +.. image:: Touch_GroupLayoutExample.png + +Swiping +~~~~~~~ + +To support slider controls and generic swipe actions, as available on smartphones, tablets and even some computer screens, swipe support has been included. + +Swiping is supported by generating events during the swipe action, that include the directionId (1..8), and x,y offset since the last swipe action. See Get Config Values for ``swipedir``, below. + +Commands available +------------------ + +.. include:: Touch_commands.repl + +Events +------ + +.. include:: Touch_events.repl + +Get Config Values +----------------- + +Get Config Values retrieves values or settings from the sensor or plugin, and can be used in Rules, Display plugins, Formula's etc. The square brackets **are** part of the variable. Replace ```` by the **Name** of the task. + +.. include:: Touch_config_values.repl diff --git a/docs/source/Plugin/Touch_DeviceConfiguration.png b/docs/source/Plugin/Touch_DeviceConfiguration.png new file mode 100644 index 0000000000000000000000000000000000000000..6b40df6560bd5394fa69e725207c381160403bc8 GIT binary patch literal 108830 zcmdSB2UL@3yEe-BeT<`wFp7YMGFDKUs5F6O6crH#>77Uyfsha>p(HX6BBDV7L1`I9 zMQQ|ugwBW%sZkIDB-B7M5RwoAgphUuI>p)hKWCqR{d=Ev-nCd{z2)i8U9RhX?&K@h z7TdP&*(xO^waxOvSvx7I%@8T6Z+YK+E4fqs=HZm&-?u(i7H1{@B^S@U9x3^^1#-a^ zCM6~NZvF3@N1C#GB{whJIOlTXtUt^*JP2|lC=4v+do4UH)PLJny%XBUPG}!LcKqaz zT0b5?aZ1nV)JcPFTb&(F9M`rtKc=Uxcg*NV?H?_|!U9A5e>C6t$}(nXP)h1YDa*5` z9iqMFM+umNM7Ww@%Yz^HGXAyej$iCU#|IIL-(=*0uAM;MSpZ+|l>fmuc~EL9mA(J` z4@p}e_~p7@c>X4OTjFrf8P%Usb{)KI^{vlOTNzjOgufrMo;n!rz$%+7W}YIz=PbAk z6RZx6^G(xhaMVDNePJ;sf$zUezP{dL$%TtVvXzh@$#-=(eZH|Ue+2T)=br{=5@bIA zdcEB;B^TRp!1=>FLR&HaDBDj#Y<&i%cV zvj#h)%l}@;o%iRTwJ3a5%<+KcxwLEq>eJAek{n)=45?-?ji|Bi!rNw0-c{xc$5@wE3Blh7e za@#417!@6qvm&kyiubh%0MG9m5B3uIzybcQrupVWk-((rTmuH&dgljz_2V({=pNJx ze-YAn9zOzVWmiP980a@DFB(zlHWP|4u>4@ z`M)auy+n9s?Tv(34dLPx6SoL`GuotOy7Qf*-l?NI>B6~Bq<0l~nDucDzwO12nz*Va3?-4r`WGFjg=K%rW=uZIAag zHofPJVfYh|@$XK1T5CZ{s%7cUJ)u<7zqjt}beuHU6)&K(r)#aY2zbRhWP$7S@2V3*YKLr3iUgAJqZbMcouCqfV% zdMS*5EW2|$o)e1xUfsA~Nvxbhx=2pw;?I>jyVJE$X4sy%EhhR&`qJRK+aQ6D1R4Y5BoZHgRU3|17tSIuk~f26-Bw#tAZ= zrGDKf3|Ahs68FeGIclCoeO=xnJgtEuE%#vEy;|d%*LKx2=)A;WW7~;A$~I2#nDf^X zp|k@kUG8DohdG|F0sLy)Pgm`gW-8j5b)32wH|O}(3;?QZC%}hXlS>-Dk^=DZ|0ovz zF*y78CjU3dsI>By(9hygB1N})eExa!{*{R@k~HL(O-i3{UOliA_vOvKPg@fAP8XyC zu8+lWdZ>?_y@Hryjj`b)!toe-8FjY5kp08wS3M3$>yUo{@`nl8d@jq7)yVbY{I*%` zBsy-fqa%omz=5JAXB_~Nvs>CVsJ5YHya82-ec4s{=E-@l?9WaVKq^|>`6w>Bw( zZ>|KVlhng}c`>L4POr%*@yNe6zJmyN;voNeBMxBrHygl@GH{I5FKqxy+8`Ch8Y{a? zguPTz+qip^UMT6qPhWJyLfn;!x7&Vgk=}R~4d_wt?33aTEz>Nf+ zR0A}OlIw)8d61L6Ri9LmkGrm`0Ybp3k#(7{YqZy(emg_-$B{sqGZ5J$$P z_t<|KOjET?8FJhHDlG`c(fMaplPtNMB&=>P2bb3yg1wz&7bh5OtbD*g=n$aJy{aqs zfl>6zW)y=rD6LC+`DfcSAej0VhRpC{5%n|x%dDFRk;;%f2L`xV7`&Cd*XGYI8O0{J zBZLbJV++5`)?pSH?Km#P*{g>COXYEG)0F*hw9HB{Kg)7bDE^?Q%6JWv0fDJ6+ijLc zF1J|z6cTtYf%IrFV&7hZ_o8No`bwbtAJb1r3bdP0Obirf%zGr8WZaMV=#u%Q-cCQx z5u)Kthy6~w@%V~kS|;s7GdiO!$@4ZRS6oanh9{7gDBbr+LJyXqv8rL(mt{4^LR_#>?{oyE%(m2$Vh{=uEfsl>IeBM6XGvM_QsmSRjj5mH*stKnkX$L z9DfRu3r)s(OgO1f-x62aG2%Q;^OXJ7M{7nvJa>9RdHMBAN1d&Rh>vcn%}8WiZuw6t=vD;n&qV38DMVk+(m0F`{EmJwtyjK?r zByg`Akgs)io~Vgrsi4qiuI^xYnxXHM=Hp>SX`PL&(!%ksX=!Yjr^hGwHZFE*_H0&* zyP|-hT`&Qg06uR&qGUxHwz!8i4m9wY^^RY*ZBJAaColTK=U2)T*7tRx4cY+HEQ)b# z%0GZ}{gaEG^g z;7*UILrIdS;+~QEC{})Mq?m=^*5@~T-rGaFzO-Db<-Ep!W+VQ;kb$2|lp6Z(f3C=! z6$EFZH;>vYwCG7+b)pM5_vD#yb1Y_ai3u;A~>urctp?%!heYjY(vrT3+d{4M7U1--&ze&)1}^@k}|0yyU*fx&N; zWyCr!4x5x2nU1r5K0xH(n}gxrj`NZ!Vq_n*JSJ{^a)-7`>x2w3gRq->3Rc8U$OPci zgLnKphr4$kuJ|zgxcmpBfNg~5I&Qz-Xo~f7o_c{MywVLTA%r*$8n8L(D(V989ci%g zf|j+WHi29JqkB?d!KoxoejeI*Qmn2rQ;gp%spKX@q6Tg=&<1m&@6^!mtSiCG>@LB094+L?;98yGkHuj#VZ>M5a0Mc8pR%S%|`C`YJE*;`a~vR1a*UP z+TpFpA8m8Zo6l1SRtRR2uOkan3k)EJQ9Y*C9JrT^xooq{5I3W^L8b#eUnyb6SY@zk zr&ZhB2{Z3SH$vHO-PK2WF)pc3_%{L?ezT;TMakB0TS1Qx$3k_1Nv-<3$L*KvvV!F? zoL|@@H}IM){u<&v4VNxxs*@)k<9Q^~?Mr=Q2mze+U3ZgVJMlK)e%HfMv5x5RsCY6) z>?VM(`oguWJ5NWF?ui~Wc??ol4w*Zi&4?tmVO)Ad8scB!3s;TMw!GJ9ca7V84)9*O zpaOVN)Y5z%(SewW^LetDSdAtLKgwr1&dSr&@b`d$cpMUXZ?S22dB6ALXfmay=pw#V z+VwH=*(vKrn^@FnodP8{QjGbgbs!jPq)=Eaj)EVhevH4qqmCEq-t z!E2e7?34@gNLdq}28>lMxnWk#h&`Gf{5iz$?a4$vzUONy3^E;qF-j5!4>ay#l{}nQ zj-MTUmqAsf6xk1zI?pBTV6_sx>z-*G{Ro#Wm}$ z<;_q#$>=;nUSo#OjcJZ_p0 zF;p)0Me0#kbE`HTEVtdC0Sl&Op-hg-Pp#xZdB=7pT6$MC11d^?H!$vwV(88JR5aP# z4G-=ym|gxIYy3vHibL=_2ZHCHz~84>dvx7;daM|Ogfb~<*L=F8wKJKi!<@$+%?jM; zQ_YCxXw0Qzq|N+toA26go7B-pJ+raWko$vzd@E+bb#3>j

f|r0z-K&}3COIR;~r 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 nH^F!qNN!d6Cx_!za z+nVxXk1XnJ$)}LAd7;EbjJX9*Q>%NUW~Am9Bl5Hon{k9r%(<3E z7F!RK!@_gylK49?=}U-M0~(*kv5yOKZJBGu*%ar>>roCveCeQ8x=6#7CwK|z&miXS zs+3yMk+ZU@dMIRpuguHABAfE8A!F>xcXH9lBP@Z*(ML-BDVSHRGT|Z7MJZu)(Swz0 z2b^~hha^cy;vx&#DDt6-3EtG&3_v#ute)l?mY6ahV zLG2;%Vwrvnewj^ydpR|wQ<;W(Tu3}ETb1=)9+aA=taEq<$ z_;5F#qqOo)4`v`5`UBLd+2}2pWCgLsHB ze{2x~Tn&_crWPGjY4wqBmmlZV^aicux3#YPS3z&iv-*M;@Ik{t?cKC+GUVp4VfIAg zi3NNMOD*63zJ!ZgQqvhoq+OWW%+Kx-U6(qxnzF9SO_rmVC$N1>RzZaQWU%~%BZL_$ z=^_a`yw|~)SB#xTJGgfxHC7|?KS&JOvw$(BOCEj|hW!EW&}rUN%arAxiJpmmG~c&x z;L}?$19}w9E&Sf0@G;(mT%EZfYZXZc7ggx(^7CVYL-qLO%-D!C8l)Rqz3uhd%jjZC zmiOgLJQ%c;`~o|l)tuYoVVGS_3PA+wkaJ!r?8!eWh|bTi8aR{|em%oL>;RWJP%z_l zBp-LJwz<0JFIos6sA)!Ai-Rcp7e)6t=BVX)l_YJsxGX#_-eN&|hg6dbTZ%MET6fUG z6#h`k{6SXeBtLnB8jjL(!25el&PCNon7{6twK5i4494neZ;=~p!|!OX5qZgXl7Ko+ z%{gL-PKJ|L+cf_IEu1LvpzB}ZF)s;rW_z+zWg3vn4d@{1E@%}t^A;I1X>8i#^RTCQ z2YZzAwrw+7_kB;11&PDD2$M`oJu#1RU1Ij|Zr;$YVKY9m<-4!OR34W61cTC>iBJss(PdH_Cw`? zUs03IB_(~aN0QWLFG=rPime8huRoXgNM4!#0v^?&R17THqca_Pqn~BU$0KvNxS?jb zv}jSm74z3YfO`oiy}Xo@i&KEO#VMO`TFY zSJ!bSPI;o`;N8lAE)ktf&N&jXyv^fQnnr9~Af^*-%2cFLlIjD_Ej3jVSPP{y!r(h) zp48G83gQLfjW+S62Wk(dD6b-$G!h)4<`FVU5Gp7yw~AVRXz%8rPQ6a_bE`_JFKj}_1k?3c8^ zi5`p?;i}<9N>}U2vu+Rt->^lF2pht9{Xzm}j`iL!Hy0Pm5oFcXI76J^eE z>xWZZ8iV~>?sOQ255?Ifn6$Lwwwpc>5gAd^wKG$jF;3PquYei5^M0;UXL)Z7F;@&_ z+ko&h!#@wj>^mh!$-n_|PwoSoSmqua2$dax5M|AKK>D!{XQCfmwHg@`CoM$fNPVKD zB{v}aa|JiH6fOFMdR}W;SIx;njr8DRxZ;%lT$icpU9*8zP0DBf0BiGwd_wfn z9P)eCT&I{|UZQXM9RF?$2l<*JI98M+iM@2iG$$?2aVH^Q5J)*~xY#pH4jJ^)T}aUv z%gXlPZo~7&TDDcP^7GL{jYyp+$HNjhWdahO?E@0z?HY3A8u9yKVxD+572!K6GfVB) zqKKGL{oNR*v2HDXa2-uB%!EH9Ma7I6P7eC7?zEu1kH2kg4l|f|>QvP917W?FYnnvt%}iuaVb?tp9MS85 zUvJ#q{hYCE_jreRZ_xshR9KI;x+pZS11fxz4#K4CMuqY-Ugu|J;GD>#K_kI}n75HB zEDAT=D&I@MzLQ zlV@eGC!g9`mSZHa9CpZw^E(XTgBXi@WVwTTVkr)(F;*E>G&G;`*1A%*xt!JL=6^$0 zL~dD>J-UkL|Hu_N8pu!gH@2`Omq9SPB^1&0Zm}18 z+X@(#22HqTwu<1Wu)UJrY@NWZLUq>a@9)G`bsCbKXpni8SM#42CZd|WUdOp72x++0=4^jo@4!ygrPYxNq zE$g1QV#f{5t}LJQ*cQT2Ds>;ZcskRRnl~eL)rGFh7M41O!u*)V&K3c;PSg zWl3*^_R*T#`(ky2n{Do8Es5RhQqOoAx5b(jJo_M|%O#PGC;gJB!$Fe}5+;uqdk7hK zm}IfsxHEW65B+sx5+vh_dZkseN50e6=bl-kMZ>8ihbjXJTDy~= z3qwjR?7$9Sc~ba|6t)YPpSje8Glan8Iv>S5;0^PUl2Ve-w;fNvU#;!kvUUUJ(WDaR zJ*QVKxecOEHYys1^r|&^LUgD-Y5n*@;m(&oo;UNXs?YfUNk%i1-Yn+{!Px}MqD z*3x3iYUiAk*6+aQgP~j4R6U$dbF+N}BPEIYE@D`Uuv%{9hq2R#e9py^pqjT{u$oWH zWC1mX4of@gZN@Ck^gh7-7P*#X#>AG&GmW~_Gc8ixUO8oiA+wEOgf9p0`N6E&=I-x# z<2&KKifSINyQbCU2F9lbU!8Y!yy8=j3%s7D{T=z>&bTDn<~JWZUibPQ-+cY>{V;-H z%2`L4JoPU~yXMxl-)U5$n_k{rBaI2m^HLn#0qWYi_xQmGR*;h; z_};BNx+YEk>1q1RS!A$A$CfI{gQaxd<~cNKjCzHLxncb|S@~%#sscaH3*ZZ;1?B5D z1Yw8R_fx&8@t(13vJh@$N)Z`kj!^zBe`Y6tA$lZWtOB2o1jXbn7&R%V{~Q%#kg5_KT|Tz(oQHo#J6!i@UiRx2*uGaULUhu%dZ{u@QPAx)UbvZN5?e7i zcg!e>|0WL46SixEUI-dE@OG) zt>_7!-FNzrKq`TSh$M8$qn?YU!c*eh`X*GR{0#fNqz1*S7+-A1ZlYtvO`du)oa${{ zr&^z^6rvbmCia;!S01wQ3lZJ!Wpl0ApC13jphSxiZ_SaxTR4&mFYOQVf}uygM1w-a z+jJhMnwXDt_m2@qmrq|?x@5vD=FZzTa3EqC1{cQ#Hxp#${EK@D?gDg&MU?k<*dJaE z-Xruz@1mWF%UN%?e2=?um1)F$yr3^`RD5;mqL%N;K2y?e6EB)R0figI_yrfTtn;4Iz>U5cf}y#l~TDW-~O3 zqR&<6 zm|~I@s`NJ*-i0LW$<}S4C^nG@R9d^+8<Uml@G&{J&6v{Ch<3wv#N`qkhfO=F^HxSZ1IU08sGE)&N*8U)h;*102{_f(S_#6D&$Au9Is2W*>R-hfddEz0Z!Pue zy=q>rS>AZ<5fvjcN(v5c!Jb<8C!}mJj*_qRhT=ofNSo;&MkmZ&_nmvm9bDhKYba`0 z5`k$kF73^-naN#9hzVpgIa*62W4mOLOxhG@I+0OSeJ`c~G9uPGvkb0W)R^H8@1?wW zCk%rGGPUnW2weVT08-8mvN4hzHRC;|Pk21(_j3Xxy;K)PVbA5RoTxt@j zAt>e&38DguY(ABWkD$=_Zo-BjX7C&d9Jlm(K{nK%SpRJmsckQuXjM^ak1VizAp5lN z>)M+vPUNxg)=Uwl?->uoFs8rXQ`fV=6$7Q3V6U+`e96zTNX5R30=|_t9UMsK5 zAIBVTb0z)0YYwXa&tF>g=>Z;va;tA!^Qto$_@~y{$gdl~Y66)Rze6|cwxY+;fBBd} zSN`_pqmTL4g8yILto{1J7!q80@!EI+Kh~(;zRYZ->?MJD5Q`Ro25huZMN3Z+C=Ivm zk1vjtjf5CFl3JFX>tbnn)~x!NOtZ9Zq%5*v*wdg%rKcch#yP_GlZHH@9EcCQfMO?; z#qOFPRFJ$^pK1{|shFYLuu30l4>jTqiGpgm zfg_8LXIuq1WJnnzFVGLV9qwVY$xwfE^RDeg!Pw!*;FQ6_;D|mJ}YS**!W18xtNQdS&v})yn zx{^|l(S3Mn?BX;WkR9*F?{7%G>6%2p=GxuhIG*I9B&=Dt@~6(2HWV5f{NGIv(|jMO zav_%=E*y!BJkP&3kj~aTPBZIC&<=aHt`Q1pWdBzG%ty^)T9o^xy&IJ3u9@|7BHl;o z9izx5^#+@=u0eAa`sMHFef5yxY@m@FC{JP&=XZ-uC`}`D^NjC89$$k6#x8*Kjf>1p zcg|$GJBPNeH!m&Ee;Slz{t02*6+I7r`rn;z)hyPj~ zMNfo}{g8!ev7y%BPd7$LJWFDuXCBQFak2N-amkBcW7h1@K5PQnkr8$F2V7%`^7s8X zU-9;j7*t?O7C6zqA>U`IZvMM#(z;%1*9g;S55*$7S$oI~)1%IcL-r_qE|3I)6POsY zGlZS^`KKxmYxc_SWP$e(+|X4M_goNBh}L4WG|t3aL8<#>l+WRa?+8Y^hrid`$wxK8 z*^ca(d$i+-2y1xz^*mCZ+4ns@9l|hn&ZdLPv5Y>=zyfUxhAr4ctC`l|WzSX;M-8(l zdTxt;xyR1$@fFS5=Sn=QKe3TsNOm}aedkSllieURQu2f3h?&HdOL-O1F6z(gw?vQJ zwye|U;-j2+^Opv{3+w2BKC{41f^mi(Phi#NK~nZY;>ZTV!;EQ^qpD*w8?BNxF-pPw{c6H1k*inJf47aubB8GhPiM&uG?mOpbiQB;9gbHt+}H73x9 z#T5tTnnhYN2md`3qhIwCG0(1BqlpHkr5=f0Y_h9{$pwf&ekx%Nb$*g!8nZNxjQps~ zOB83vgyOL~eZikTIo^@EMO=?Du0i8#EuKZKb1h+b+y@bA$V#1nIXJ+IL|!3_krYx6 z?SnvsgIbCcODR}X9q2>cqsLrMXu7cE-^!Glj zKxU_Jr)U%dZ!nk?wQ*FRUvZpkj4}ZyRUsxsZ%CpynxwhPYF%|&{t4Z_PM<>T1aJTS z0@?3oZ)i3%4!;+ebt%teX!g%VwPS_^9cSjGH0Iftc-GvraI#&@j8|=7Cuh7|c$~fb zc88gvCq9_Eya%OBrl-wa5l61mwi&kIzR?(2-S|!VVh?_D^`k@U35sX`2#foaVdTZN zNMu4MHJqX->_25iX4;c8TN-H^_oF$Gc87G3XOz+`EWxkfebI+J5e_@W;;NuBWIp83aoskDX=)$8C%rT0`{nuE ziqomaR-$?nW~M9L@_lE7sf8OM`0io_X@}he#y>qz&64nut8tlBn!p7< zpi_qTZ)qWx=C#dGi>5!vji<)&=2I#rZxyPGmOM!(MYU^V9$D$!3>{khxF$$=k*49D zN|7$U18Ymdd&xe_^CJG1MvMMgevBMZ&SWw^kV{1Ydl{6{a7HKsrHy9&m7ops2Mp~PFIK;H(6jdD4g zqEkg{qGHfNSVx0s)JR;6%%kAIdq_Wxd0OT5OdSo+A(i9P<=K-FN~H0K2ESVWm@Cv2 z#!0T|GQp5&y+E~-d<<7|s-Q(!YQ>vH2o{Db;`*|>?HeZP9bAN3hziZ+r$jTJ9_@*N zal#P8Hr4o2_0}sOk(+Km5w_<#$c-{18snl+K>DrlnBWl02YcBOWRIGP;ym$52Z_cN z2~KRBfAf~a+e>I#Pc;^SaXH zQ7ACK^6)bng#ISEhY<6f&oWoa(OA+L_M(;4J0E8wsw8VA^V88p>a!o&8djZ8B)>`) zEJ)-a$y5_tF`^OG$??Y(Aac?IiS&%1;2D8SwAcQT#`}$aL8w83eKtGx^WZS4w`s%y z&t^C2cw#B7N=kLV|GNX$Xuhd4_1P<}az9>f8ZBtgy0hGMYeYYF#x-40QAoQ*42h!o z6he|E+kGH5Y`Bc0=@{Co3XyYF9>p2%5MPa@_EFn8iSmX$nD_!)(t#@-L}5csA$z%w z4K?H@QVq_uH^U#K+KgftwP5l(n?8ZKCW377v)W{M8*i> zQm(du-PVcQnkR!o@%||$+SS%^v%*MJH9>Q*c--i_hdm5(KymO%p$j>7z|A4@3Z~2vdqCO4;us zfXxhR#Y@O@@*n?ZROBXHC2h7-eiH#IVkpNThj`!_hu#q;j9rbAM%vUe>bViWbslkI zMKjhp5J{i4kQAS}sML1>^)c)F*ZxZ!Jz{>wU?=+r`5OPcNUL<98K+tQgT42FYC7xs zg;howMPWuAPysWJ1q($ibP^R68=#`1(nUc95_(S}gMto1R8T-lL_`FX-djL~h;$GF z1QIDGKE8*6qBHM(pZC7^d%k*u$fxY3o`8;WD z>_#qidC~v*FP{wB0WE>Y>&uWz&4y!MgyUMR$dWZ1Rgi|wba*Vi1zS%-O!>*|gic?N zl`Y}_*+Y`$;GV>C|KL&_x?``xk7Fw<@DF2Q{WmmTLx$|fYD?pQs(*l#{3Kudq7ivI z{ZZla1I^w2$&}AElWBXYFYqeo_jzjIcaNDpYA`{vum?GQ#zQ(!8<0qB0e`n5|>#u>9vJ~?A<%(Bz5u#=o3k2sb5(0KGEvSSPKls>g% ztWXGTOwaZ$bRE85l5y+gU8|Am;MQMonmrplgQ)MctxVB2rLNiP7Nako$3`3EtT`$9 zbP4tMa;i?XKE%=U8CR+(mPojlZjUZI+9F>f@x;sk44e0~T1M{fQ zbg52u+%rj$B^nWv+Z-L#Jvwz&D%gb=9sA>M$_{&>!+Vr`?eb)Ijhf#b$1!GJm4zZ- zYt+>Ee4NZ9#&)C9Y~(K7*oeu2kFPRRaOw26iPq(&8kDFzW>KCX@kI)>2Pm)fLIsvj z@)=4%+QxH}z+LktI=kTcqqN8B>WjBurs?FKsCGVf%!SvNXoD*#D&1?@Njow`Lg&eg zZFs{XPnspT!GadMzPX#W$#N^oF_y3An}3-^4Gt@ma-7^Y66uJ zhzynzwewpg&3f2`;?UCE=lNWMDX37>Fnq$?eAT)`Zj=FI7g49?Fdwl@Bzt&$HzyJ=3i z6Zf{GOnCSV?z4f+3oXh8gnFa}alEbZ#$MJI*GnXR+-eZqYL^Xnkvw^Z1KEfZ-rNT{ z+V%LGXvv`rJ2_fhK?y1cN8zUt{*W)Pqy*?SEPiAY#iYhT-`uyztv6V-!j5HQz1Ik% z%4{&1l*>fIHgizz$WC?&SvX=;YctRT5Mk(1oqJr&jy^QaeNuKQl6jWUBU=yoLWjF$8eSG!Q;jds!xb<486 zXINMvFNWsTqitmv8$7=u{)rv(I)}CKSMf+iuH-MbTC=oYG6nlW_#sZ)8lE41)3I4x5 z*!ZJZEIHMJlb~V}fkkH)`LSKl7`U(nf=(<#3sF<+wt}823&w|C>E}$+%#NdN-~Zh2 zN~hFUF{BPeb$H5I9_KFRIQwm7h`MmfOEm1Xp?6%><_S`6BM4dK^EhsyeOo$|UO$|f zM1HNT!!ISh9fhFXU`FV%n+yiddzbg?)uoJf)3y*<^%OAS1*wfXuQo#cem-OEH`ZNf zY>q>fYQbPe)mc)K?V(1!R!TB_jk2?xiZYvxlc)oQo2P9y8p|o1y2G!Xe4K?|J}ACu z>o8rxl`@&^`NvUMXnHuxsXc^y;*hOE*ueQKuWanwG z8I-lrZ>;FjSu5G@(uDC2&sbDhrWY_QcL=uc6?3zda1UlRQ)MxK(X*p$UWlga#t*S~ zo55#Jbyb?tfrXS`zRuB%*5*zPtJ{wD(bhrPb?PZ9yqaA#Hcz(r{h=uLzEl3^>7FuV zu@9roBuurws^V#2p=3KV9FMtLMEyJDdr>S_bvN%p%(6k^CC%k*rli z5Sw(H_Zo(GP=n7PgNI;EcTmqH_=|3#&QXjgr{s46=1a*>iJhE}>*(Vd znXvQ*Ws6tJW*r4y7h)9%2QBrgop%N?`Y*UAj~#Uydg3ZSeI5n-BVYNAmdoUb4&Oqz zu?ZWXm;y1iHM}uervW!Fx)>Xq6robFhwGo&B(-Qz|C-=x!^ilRd+Yd3nJ-Y?MIUvH zh=u2RT+*=D=+KG+Dw5{<;bgu?L~n-2H(Y_JkVOjN>`aygVN`;;K(yV^wyh)ytQ@sSj|+jG zH|8uEQ_-0kk%e5qO|i!n$l6yYpDZ=2b37)@!jrV$Yr;~MyK0+=51eC8z+vs;LzJ*5 zsSPV}fet{kOLV_XYu zX(#G8X+cI;5gYxIi(IKE+w4|Ymame?v=k6A7hu9tmax>P?@qq23MuPWaN(*U%dw7h z>A^>08H_`vAVn8{vwiFZ~k@D_t@bz;YrEeD2?YvrD;a*`^X>j=qLCHgg&8D@!fvd z5u#4i$#GBDx9jRnhTJKhqg}aejZ;HL=prB5oBX_pAfZ&mo$*rMRBg@ZlU7U~`-MSu z0cE<9y(JGFJG1Ifj=^2qphqxS3a{M>NiN22mg1MM1*&{HVGnwb-SFw5!}n2xfLB^F zPGPJ+_cU}qB6?x0$Eg`hyW0p2-sFxGT6Xj=MsnOj;$rzK6kQ4xsn{p@%tzm|h$^Wv zm3C6i-^8z#_my@^QU|C15do98G-7&@9RD#7ZrYdO&5n+>}JHF1TQHeobvpL9(Zbtf7gO#m%8sYb|Q z<5a@HJ%zZ7%Y_i-U|5B$sUb`!xHY|oj zF8NWro*dQ}EP+!rO{w0WIKI6xc1Qdct8f1tQ$Md6eU+{+R-YNoM!!NaA@zg-hOaQD z)T?%hGDYnLy3l4^#ic`R^GcDreMFFwcEZF18G01F)cdcbJD2cxJC4P1R1EW0EV_!lU#{0nct88X)8jwSGk;&kD4 zt`Cn7Lz(eg#uEucd+=MdUzTY*Twm9M>&|4!oRRcSkms&kk53+8R$6{yNxp10so@{< zQZOK*6{atMbhMI3Rj`Y%%;3mzv-NLldK93O_u8h8WETS{_JYZ8XeUBzJVKC5W9P_{G+j$q2tgBaawDQ_Y4-vCLWVmn zRxg+~fm|5p4cN7*z@kwItmd{(RYs4sUgZP9HB71JgI100m8V6Aw~+57qkZ)9i}K5& zrPFKl=%$zua@?tOqJ}fQ$kuR%x8@XID&CJvB$Irf%K&Asq(GZw4Xf5}Ab?5`-E2$5 zK3LsAiM7>uzBDW-k2dY}h+jVV4@Z+n-!1fI()Rl*!@;(?7I2JqMAI`VY#v4H4~YW* zsfGL>Tr&2H>!~c8;o{1`KyWA_-1?-BJn<7uW5;zjMT!*%5gg2#dVBH4hq0h$pO&?J z??lIl;o9e6`E8@4$2;Pd7QIMNg6M4j_?$D@Bq)Br-}miC^Y;B|ntUg%LP3~$^XQ1< zNc7?mJZ5}XtQ3tKicn8XWQg@Yhb(HseBjl@BSRcUvA{iSE7s`9z{J35|o&JgeS!b`fKYqRXkvN^p-uX1L?c$j(l zsu5f~O?TW0==o&DEXcZ5f0vISU89@2GQ=*|23r}Y08XiYR{}l9&A-4K*UL<$w&&d< zx{#8rLQ(U0o98`cnD7dtqZP4DBHhMoWT-fmn~%BlXjaCv!t6%eMZyhSHQ+)GiWklL zLf^9|0QIa^jz2N4SXU!^ak#U(f;#oEDuSxtR21SIdIDx~d}H>Ok&Ifks|t-Bw8UMu z@8@d))&aJ}L$v0{1|Hm^XOPDtA2aBL$j21tABfQ8^nMA?NLxi=2cwRfzBM@AW@dz% zc8<&+^9;i)E*Ck2oi%%zT7mcbfnzLk&gR$ry=!z3)^u$F1Cq-G>WK5gp{d8i_w33t zIoWuRP0U|cPtf~N0PgP6?esrDZc8;0M?Sb!<-1Bx zrVlu@`@VN*Plz1aWr-q(Ry4dTz8jn*aYwqT9*oUESu zMUiB`uILxoZ=864k;8pJW_D>n)h=-3!9-E1ka>@yYG{S_*u*{Tgq$uc|L&Lnk+(&%>o79-wlrG1_o|WFRj@!R0 zQFifcC_dS4ia4(h?|wB#DmB&hnt0|jGhvPBWqOWvC#PBB%7$Z*isARz{Z5GvLYrq0 zyvbSIk5M`X6_dBRnmq4RXc*X(fqA?R_VIW+-aWtVX43B#$NL@7hf&VF?f%tp>X7<~ zD$JfEhWu=^)6y&M!0Je=>3Q>4k6b6gd>i*q9a_%~Z|F%@! zUW2J*(rvuq#Rp!ZaGroRykEMk3jWh<3~k{zSycm#mmO2*)okKMZWpKHHO}P=<7cCL zu-xHrQ7F6Wgea6?QN(XL==k#9kWe^@gTRvll& zmpUQ}s)JFBPABaH&hpLE!@ya-{tS|2;La=knzr7{h@R*;WBR~q#Ql}~=4TKs@Nmdq zu)R|TJs9JxcaHw3!P6r${6(jie5ao^9RKPcpHH8~Z1m{BC*)Py{x^n*XFHJU-kL#=p8FWN&sWuUrKl-qiKRtBMMK-q_JeD7Mc?SS?xNWStY zB~)MZXV{ZAh1wG2WfE;ipUB>uygK1{@rYb+ba(ekR{k&@5$iY}(_$HccKb*OL_ zKl^DxdLZ;U*SfKJgwS!)c>#~Zax0l04OF|fBfjgkl1DSLs{vICN$nxO=R>G=G7v`p zS@&IKer@Yr*Zb@$gdlea>fT2Xq=c0R@|6=KVzaiwCA|6Ns@p}Ix!r$(o08BiN@6Xm znq_g{I8myhv-+8a;G0^n>9pob>=PtqG#NSu z#qMP;?uY5-!hf2L38{_SF(4$IP7;lN0oEmW6u>fp7|EFUjQ68B^ zPLr>-5N4!2rvNn|pD~$I>x4te?wj9_2I0za7UiXGyGo&wCN40}Szgpbm2`*_{oQc= zt&?qU1~^GK?hy}Jz?@e8#RsnKiDwI<9nCy^)KguWcQX6xJV$NBjwOt6Ze8$M| zjqE`TmkC#*6LSvAla#P zPL%A_`fE&_>JYGFp|i`MzxZnTBUo}U637l)Bs<1uX-Azd!qkEb_Di$$L4HkA zB$(ub{Em`Mr%wuW2=`3;@|;e?cJKJRzkI#sjU9}rnW=1MbKsS zC7gJ@Oo&R^%oWC`ihzN_&`sUrk9V0onp46F1tKNv_V;refQnIDcxa`xNBYkYt$`!^ zvFO*?9BIK9w-5~3%N!-PrKXoX)!Fu;f~UH%Y81U)H0|GX9555#v7^Hpg6Wcs&LqUQ zhujkBalGgePH>~YLZkailXQzWW!#?eZJ2v5yV3Xcow<;|{%3UkTJEpi#h!@VyRs-7 zk3p=lWWE&3Rg~u8ci|6R0+uKNSfYdf3KJT`g`t}~)(Q>ut1SYqh**0t7m_;>UC`So zA4FYec!IM6j9rN!DT@JP`7vKu_OBUBp6ANUgc*d^9_EI%#elo3QuvFC0LeDVM3~6X z108-KwnY^C9ZVL*e*R!{+7@Wke_YoA84AW((Vd$l%DMx6_FXEw3Df#g>zlH?0dNXS zesO>ePdl(_FTl>jh=Sh2^JNpzk}a5v-Z}8 z&vbQ4o%eRnKdMy;IvNx$$E|fz-*oT_?Qm6ryxvIol!!RI$qB%-S=?ptEzh~YxJxjE zpM3mLMBY6PAkW)xScFRSCqRJj^Nqg$mC%vO|FXeu|8nT(O$+{U_ckb-{_Zc}gI||} za+4oE{N<0;ptgVEPemUb-U2FmfBgO3W1yMlZ@+&CT%#W++VqFWF#Y)Z!y==5wY@|r z5N`vwH(}2U8Laek;7OPREW2k;5co=!)>WGjVtA?{VaS( zbE_DbU(VVtM9c!_=GcQkO(#H{8$7P2P#r`Ix2=s9vSNpa8ZFaNj3~qfj4t2Sy?1Dw zAq^MKByhbcbV7={5a&6n_gj=F8JX`hlo;_(pjl6WbAr!!DQo03a|I#;f*r1SDg#6& zae8yz!4d6H6_OMnhU;f6jJotGMhkvm5$0aC_4`7?mF4kJP6sRLygzKaLG`U%*u zD@UE|m(8h=LOqBvwc5~|E16!{rKt<{fycv-%st?_90 zuv>z()`#Vw70c@amacQ#VZAsI85F3>6f?e!x_)|8|7UIlfY;E=d%u%^NxT_-dvD|} z9%Zyv1s9{^DHqYADLXsUl)i8r`vzsxu3Q${8`kKTpvC=QGO8#2cw$G1)9-ekzc+bu znWJqA98REm$Nuf|udBX;)8_s~a3o+@aA%>bEH|rudSgb%1!LonsKNbbwd`zw5bQY# zWpPcG2|EYWFuF)qGv6{->e9KlSjHz~uWKv%rK{5sUIFF-EftsNMi|lFx3Wj-6o{FD zHXE_r_iD`YyTD;~{$Cr`%w*TljpwI$i_^7|vB0k#!wuY~B0M*4$3GBEE-PM^7?7*+ z1*aQamj#L3Ijt9T1=fMPe!2$-a?9U>hB?p;;AuO*2vAMa(r!!M2rGp=^H%Cnt^Lk0 z2XaQGGVy^xbuN4IALe8F=F%MJI29ske{y>kUd7k&sayDVb36{jGCuf*(+{B(XIo0IkQoY<3XN>t7z zwedCgeM$Myq!KrK&!}N<>M!%s^ zIZbOd_BukwEoo+=DJ9GT5@CIVUtaKCS{29@NKal=`lLm@Mxz%%sppMLgRV+D-D%{` zZPF1+>@dYxCMC%uHJ|23TblhUHG22*t|J6XuYpc==xfwxI@zeq^L?L^L_s8wfZ5Ej zE9!!IPDFe@Koriqa5JID)Ui2c?zZJgD$S=|V5^%I>|4X2(oWoq=U^AO(#2jx9%)d4 zNrJ);O1DYf5B&~(0lv zEY}O$J`$_jEQKkiIrEUd*c&ngmZM6o^Dw`B<{$VysAJ~}dq8w<$SV|#@oun!7cr5y zC^kOhnw_jQ9)wKlb>p&#OH;E-NpU+3mdZPH{w?H;WSCXSCyu@P#&Zxpu`!=46jWkN zf(ww+h)d;F-GS38o+JFFTE14dH;Gc1H8g$VA;CH9GaUnf=lqHsc1IqP7QW1jH5o?x zY8N%wZc1bC=%Un^`QW10R9`PUk-isaPe^-%dSZm;w6>WdN^?tfc<2H}zj5#)ry)E) z29;B<;JBxHe-flYA(Kyghxp|q;?O?ra9-zbYJMP%OXACU4~oA$urLEVcMkNaOW;!$ zr>iwhU1Y~qNBDAiw~9Gkn&YyjL$f+1D>7V&_#~h}c)KsX>~4RGcJ|Kk^r;}z)h>gtaTqogDPY8wV^*;N1knqG2w;jxVzALk8&H&3%b?PMU^R7&c~z+={0MNbTU^9U}n-^#wnI-)|r3HKHbuk zjHdNXEH=#l5d_rxAHr(Gvj<^q>>e3}-rFtLWz21GTn;9Wz~`cQ1)N0k$@JDE*B*^V z|0AyUh$w~jCu>I@wcN_u+#9>kQ5f4~IZxjaaheXU+x)8yz(clQhO(gMmq5c%g;xn4P^y#zzZ-z$=PZNx47&oG;%vD zre66|7kxj=IhLL|Fi9~jUv)x9Wb7YYF&^uila^HXp>*hL61%1`#S*2)CqYF9bow8V zwG;pfnvblz@o{`j?U~^W?hHS}Tt;#S#)0B2>VD0(`#`CPl~D;ViPL zeYT4U+Cizp@GMP;eOx0VZFANw*>I1BYGAVOP)t?Ai@ju5iwu<3Hj$O)jAg`I9}`aD zQS=>oqyCTh=VWI~J^nrL+Ly&!hBF%O{CVG1&9~tyw4fMS=L91XJ6SstXg@1`qMRKP z2i{$Hq$m7>(VK0@$P!Cm4+n}|kI>@_#O8aX?Q;goLGpvW;ut1{hdNwj_*5-8I}cVI zmRsJfX6FyDa{EYnX%#%I@3=Q`QZ-2!|3J6@X`9q|rC6RP3Olj{ZY5l0KJFdF&5JO& z_K;tG{U5AA2KD$G>J?Mp1^~svPvg{2`?|$FQT|1JT)i8`=P(#6u7ppT@69pVifX*!hXXX=mK<2?jM-m~k5k(D% zO)esth4wX|5)Cy@H%WyEXYkBT!7IK4)&51dFt3}B8^0iA|1+TG?S>XoE4a+~ziI(6 zl`1Z^af`Oppv0`R49d^qT{5JB_WKV?xqzMln9`Y9d3bPX4QdT*Hnc+X`MXl@Wu`lE zg;XQlWgddm?me1INYVBj6AfOB#rq@cDTbl!l`L z3_V;OBHISughD|6TDyUQseGKp$NV~*RT2S7&!GTF+KBa0=xdtGBfXf!R011w`!1!& zlPo`K;5G72VzqU=o~n&<`R|rHg3s5jqHnhz`&q#|UY1#i*~IHkj2(PH$b{48YXSCs z0qTC`sAA4?swQ^;siY#lNo9p9;l{3OsEpYJ%ujnii_j`FfL86_?y-3;V=!|Yu2GZS z=(!TW=N8DzkHcj_YVH+Ws5ct~e}{j8&}l(-@Al8OAJls@6AFH>=&(OMa`fjp*iXU# zCFeH0fb1lE(GAjsdhT4v22u15@~z)0VMWLC8!Wf!@a{PPaq05e+~uL!%;aq}p$bY+ zy~9=b95@FYHqB-qK}P;t*>2E6NbcuNe@1Zt6JIGE+dZ2L2BoMmS70FV{s^1Qbv^;` zt=a7GcR77P)Pj8S?VkhXnUerITkk7>v*%}^zvlqGn#PZOjjboUr9tfdL!NuqFc?OE zfnN^&6o#vW{!pkYlPm8b=n%gz3g*2)1Xyjtj0m;5`)$7bN|GYUQRdnR;e9-St7N(5q51p`dI0O# zf`ibih{1MLrP**<1Q48>3hTkN$u5>j&89y|&wT%n_y8~`+lwGfxq4Jc=@-!Nmp>w3 z%jf00Lgxxdj|13hzX-O{%!S_`VD|Gy`VvrPDe$U@bS)cYJ;YX4f=U%OYCs_!A_($_ zo1ak8z6Q8A#qqcK0T301OP@jUa-Qn;IWPi9gQoUK+Y*B*1y_Q$Gpbe@+a;-OHzHPz z_5oW`Ga7{cR3=a|6TEKF00FI~h(OB_!nfe~p17ek3X}cI9f?B^qSzFS9sokho$R9MCwA;q6 z85Tz@yLzcF??IcC+?^jFAJLZPiJ+@PbI=tyD*%MdzNIb+WV1meo6|rxzZMgUK*BKI zRYriiSeBx*>HA^v&_h2!Ccv{;D?&Dp#?K*}w|}V^7|CdCRK6gi9Cw}?QcIMEHyvF7 zZvr_11%H+*pDcK{e{CT4Sb4z4 z8iAH*CHp5t0}(-pB=Dw40zpalny6yv1T_qQ!2`xQ!;3nO%Hn2nGW4hK!NA0B@ryD5lalYQ6J#up>w00h)KeR1%!kSp`@C%->hU4N-tT3l||x0B&2wZ;JmrUk33v zjK4BvAuRUGEPj_>%DgP9!I}r#2xZLu+y^h7eZj?m_Wu!W6FH>-ZT zL=VtCo0Ns}CMre7w8mAw5|mS=%z4+x4_XV>VfvV`eWH)P$24bM;3R{uWLpOzb>?wK zU{HY8zAehmw=rl06P&8{bmNX7Vi0$Ea6WJf{E^$wUi<>KE&tD1@bY_1Y_^Lf!EnZ) zdx9EX>AF&3EAkF82o_xju(r*0kp9zT0XI;Tod<;=&kW})&_KdPjIZICGFJ%l^RIQ^ z<9Bm5@Ryd~{v)6a?DPScVf64E>VF8>cAU*!`6_hl!%V`n4&bYZGUm74?f}zyvPUo^ zYJ2&vFb0_SZ$R7aUz$r;B;N%7EW~z=kI)K4?>eh$BanUaUdB_8P3B&ON9r$k72m{t z%WG~-b`Z{b!NL8;3o3&&gUeBfElx-~!By+-4I(N5$=hkkRQwlV@9r0qt_ zjfLl-|8vji2h`|)TUYc?Z&r`zw+o1z=3&NvL$TQx(C~kFHB!U~k7vPXfglTuc)Z{? z@Ny~F*Nqz?nAz0^K#pS4CoCvc zn#9Q+3p$NJJt$6?#_4G^PyI)HjpJ&Pnl7w2t+9DL_$Hr}0`D$3(^Kl3?=$h~AY-Ce z@9v6zSCR9~VbI#xZ0|i!kWLvMek;W73kLxN3kn(K>33OTALQd9^vFI9>v>cSWL>wP zSy-nC!ZO!9@9iYHrr#;eI+QTm2Hy|3_KoCJ-HhC ztF+|6h~!TkP)=uF${9|btE8!T!v7RwP-zA_ zR*GbtDiL`W1*dc0gmjT#UIWQG=PT-@tt*7KqfM~rZc1j-c-j~lYI_`1+8u%3F?qEc z-rJbf@!x5xa|2nzrCnQW{qsTbO(6%tr#^|6C?X-#5cyns9qIDOut$yi2-bt7(?0C2 zdwxIaM&yG(=N84hW$q{Uds=KM^jI>e_(pR3>{ zC-=E6X34;~rNioF69EHLU2OXRt zuj1d$B6;`@+P_!`evP-(jB2EuF6}5vy?;W{mfBV1-&E`x6MDK|D&JZg+~Z|hDsbpG z3Pw{-dbZH+t`Mck>(8i=+SxTYK?#Quv2hFGTl*l{+6HYAYu;VIc-GOYac?4koc{-F ze!+S3zl6#6PUgqZo~2K5e=T&s(e@BTA49BR??8dv)ieB#J+3G0?W&z=Eua!mr_^ux zlTNAo066}xNf6{_``Z&K-dzoe!%tlKiqWj))w<)RSYwN#+~QM=5(FqkAG%mR%2o)M zvMA3@OgSP^(108RC8oXt=YP^r)uW$LA{a>kZTTnLVh6LdEjg!LqEh}Vu;b%y)N84h zdIcqu*!2@bC-;SSV-8Q(bWOj&EbSJ-qitnX$|N!#&B ztWpofPYs~kF&6YU`nOjrPjG_Y$P1UjiTreF4|P_y=fjAlW?3a2A?gwNuTpn{)=>XB zOp=OY32-JxB@-Uw-#Wv%ZA;8u7Ufx7UoTul+b06G2ADSPx8f8F645Ek_@FfIFtWuh z&*HtK$2-gNT=er~=V(sllF7Fjr44_v!%AvmUiKDJ<5C+MTfO(2F?sYxg$QxbK*d@v zWG6W1rPs$f_q1X-`AGXo=$lS$-{+8xo`inTdJkl`3&W>un?EYH zr^z^}?2~pkk;rRi@Q#qZ{M!Y@;%NL$o-M*pUfQW<3aBkbrqRw|iSO~t^a>UPRdV^= zX!gQZuBOhftz4MAJG^A*LGPykI@`?oNaNgA6c{*ar+*YOJ=|jJ51G-8bLztir%~z( z_nZA5TiBrpY38PUMXX>tDK($uWAE!dBXqhnLlTvFbi+0I*Q1TPuHLsdgvA(L;S%nV ze}6^3ecfX_JKMSev5LSdsW61Kj){^qEjLGw#;`)( zh`Q^zGEOF!R3fW)^^yL(!B+^!omUxHMlsq+T-yC$^zC>cYkVwC=1M2NbU(jkr#%0r7aIJ*EE zH}fJ{kn_eo;|&h3S#dL_&YPf$<+uy$@~!kM6ebEX5i3ERa#9JuV#Q zj>xMa_D5llXdUB-edNOvajBkB-f>E-*nod-b{p!u`I^1)OuMFOErA(RqYW`OEms-> zazI%!<~1ows<31=;aJn9?oG$8349i8YrLNOBC=5Z4S8rLG`1NtNC>i zV;bbvS&-{8_y35t!-#$*P{81N>gO9FJzjJr8_+J)-}&4=QO6MT$xVT)4d|6TafDgQ=juxFqc5C~l3Q0=BGm=W``j2{ul>{`sLaKwuVnSSc zA%+%%*@{soFM&TpfnD$tm0*nkS#nR}wAgJlKsA<;N_Y(sDUCW$f5IYrEG&&g*6VNR z>B1(zjlSc*Cpn)EjQ>KGvIltD+duaa_u&L3EQpf7Q5L5gWs&dPM{%!@nP4kKav+D; z%zRua3*f%zV3!Ts`^VdCp3HXL`q<(~*HMu*-j(Gfgc3^9kucP|h5L_v-!oKe0c(U{ z&jjz2eDa|>F|cRVs5bRyds2PbOqIF6<@S`xpqwbLGO?%{+Y*Q%rV08R$J$Grk!V& zbR?0Y*z_hcw2Qdb;SB$kWA$z*a~k&l*S^Jn(MA2gy^8<0SMmS$D$bXG|64D^{-5hr z6knJL1!d=dZ?As znQd23r#6~gAPH*16zTyuYrEG3t@yHrr&y?NhDxbIGwba*<8@J(pZcKa^ zoM(gG}E$1?ft({H&43S78o?*H+AA&EBpqI z#i-C1pE~$~bECfQ;hr^)l+8h4YwqE3DzXJ?qyY z#jVhz#L2CNt|jgOX}lv_FE0C7!;&Hv`DeIjgEn6iZBS@UV~}rN?v{nLcP!=l(Z{YB zzZC4%6HO-1eGVltu$N4_9M)D+`yJ>^=H$?yu$B$d(C%0c=G@{Y=rO*0Z(Nlh}cZ&AJn%*p|KcJxB69HcO(vb6{SxWTS;j$@ATumZ)BfY8I3t2%Z>y1ZEGy1Z7Zr_rTTTKv~HLlIU6dM z0Z3U^FK+tq@1)d-NICNZDa2(YjTYq&y-;ND48lURRg}tQV``^7p%)@%H1~EtV^37w zI>?+fTelD5aHP*KO3G@*S!>MER&x~S*~1n6>#_ctWJFQ(j6lGp#MRnALLqNrC1I&$8;9xn`aUpWX}sq9I&e= zz+76QU3C`iYAmL68Jb_-19~Y9yrCB!i@2Y`9z<^o5+GJ+n*En12~qBlf!*muFSaYp zkzi)nSZWBOpQy=JO|Uh%p>?(16Vo*|(M-SB+e8PUg09N7#k706v5=Kb{zR}V6Mh-a z|FA35U{_2{7w*b3%M5C?$F+o)gF1=I1QG9GB6^Mkdb%B;4eHKRhcV1e4&B*Rv_n1D zp$)j-cN4sQ(!}yH)VCp&fRGb^(}(y0YNamm^6CZp-!(x(%^r-d&D3nMP1p_5TutrI zC0N+U>H4;_!Og2BWKd=80o~SW&0lBK$HpnsA8!8pW&mZqP;KCs`uYON4@HIog$PvI zJGR&K;2HWrO;`gCRAWM5aFSPlNCW<=)@w-raUCJy8r0?QQdQ|Em-nup9I?u5M+Rr$ z^F)Ca!l-L0F|+ZZn(Q6+`-d73aFrvR4thUDqk?NP>`}aai06NMt=mc7)Y#5#aQ3}U zcdTJy3DO$|UWLMzk~uc2O6{0Qk^P%x6tI8B53*2~_qx8QI~>h^Zw;0FZVT1ezAFpE zXSC(PjYape&qRvMoT>?Gb-qrIGh6*h^rQ2S^Hs+2Mx-%uU%f_&`I%i3eJc4B$%0aeV~Q|B5O7?+!3DJ?+Ny701u$2Ab^@s*%+f zruhwadMxo_orpGLc5LVZFYT$+#cJSz1-Sw0Mx$GRp8Wkd2N?NoaQh$7lSDhHBynx% zyh}6p+{-O~>z85YFjOny^ffV)6|bS*b1-WMY~HJybDNn)Up%7!#?QANf3#)pe>=9w z7R>+uXxIR|f^8)Z)?QwMRdQpWj`tEU7g*ap%nbV&C)R#5p4z+U$ z@269u5o0~P<4S7F@F#-{{Jbg4II`UZWZGn>MuQ*ZCY zQ$>DsWsp=fC=W=7hVS}Z(Zx)JUZmCfIgrm$5?&t!OCXd6j}syueJ7XN zm_qPZOksESwPQtg+5Wq3DDde*8p{r*YKj0oj%!rV>6(I4L>t8A?BnsbyQBt&C4Oda zgrRh}p=IakR!Vv;0l6Nt6BKw{y~xtaNS^h=oscyTe2}UOeev0|-Wvygp&JbZM0+S0 z=j=I}WMDC{Sw$B)DHxxfBZ)C#?$vrO)Kn9dN~yRU_(I%%bZ*4a_ZegERv?A}TBVos zXcL2TWuXfV{PD9u+nWZy@KpoS=y8_aBgUwvMSkF7T|Sd7Ep;M8-6@R$xhs!LwRg=8 z()G|LmRA09kH`pAGPddsGpZJP_;%1eFGPNh+zyt*EdSlrorDw_K@M9S$A)OBU{7AU zYEr+{x2ru0hKJ&{@Xlaya+H8;b>g*=QcqMNi&Brzdm^zSZFzCqHQ;8P=sJGw*VpRi zK|7=Kal!$ZI+{^yxnDSl<$F`dgTs$_w3%fjImWVGM_gaK$^j7&{j%CG<38JJJ$-S; z)33hs++=5gla-dEf)wtme`AWZ-^kAq1lN7|<07I+Qk7g1%Zd<=n$(lg{7Y2ES?SL6 zt`%{4?v!y`ydX!j3`wghM2|6@UAw&HFXT2jcb`*_#)MI>LuIR)k2lgXQidA!YipVv zj=~{W4sT$T0@WaH^CP{PMxqFS0Ud2Qe$g%|tieoabu%ralBjUn^QmZiQ zShg%`gl6!V)A-sxw!a`buo2%G&{r$o38Dc)4mH8W*{0pI|ElAqDvNr&L=ryVZf{Fj zb6H1I&pUOgdfP0-MsTTqy?FngA2tIlPL)_bTBxF3j4}OOcGK8WFGbBMV4OS0PI16a z`glqdbJ-Z#tM~CZs)HA+0$4Z>&+@hxi!e8}6D)&p%fvO4@b#mdYp)$<*VX{mHa)DZ zWVbiF+SAgdDx8{-zyS3S(NNif)Q(hkq`B#EJuwxn&Iv)1_rozGZGOA8e%w;-K1i^d zQ#!Y}%Hh+?Sq=@?$7!*5)w2`?Yd>7u%=8qFs@S4g4P0RYd&UHnHAQ)|%FF`@jI7Uy zovd4|xRsr(Qa`L*>l956Z?NADYAe;GEer2SO0jkaZf7XlpmnnI=vdqOA#(W}?DL@y z#5&xiXQirUxQ0`V@RHd5N|oROh_0A84?^2imI|6(8yg`5R#l6bF|w4Sa)kB!@rGe)#L3R3#o>y2nL$1V6bd^J9vGEP9fehZq2_DiCwXrf% zO0B|g(d%vVAsJAQcxXMoWmR_r;m>8=+3uv5<2E11skX8O*)Uc&E5vblme}R2R(-5J z965GoFEKlIW*K}^3QcR&jxHd3vv7H&n;i=_+VK;dBvLpa96Tbc+#~+!HC#j}vUQ&j zPV}kt5Kxz@Uo?SJI2K8qG%_iPEg921C0_{PcHouaGas<|*~8bd@>_ekVQtu!%8 z0$qzi#ujsq+%7}glWSAZ)IV9ex7k)}>cq=$evQ-Ype`e=es1a}#57dM^(I~7pD+UJ z6q03gL$<&eC$b<0xmf5;D1nsG6R@hAy=}~rKjpF}zo)6*RJLHGON*{P-9aqxDfzNb zYEJpqqLmljcnW*m+-cC(=AJ0DP4|Ih3)3v9AGUS&<))w=iEomOuxeP|fP$US2r8Jw zfO|5ORqWB?2%?}4Z_pm%k7snTBXNehVZju*U@A^!og(MY160@A zsOl%?`AL`9!Y$9n?PBTd?UgO43ZL}`!bXzn6B=V-?o%bYo#je`$w{<>fP=O`^7Bb- zl>l{wPqC84jPAJ?ql4{E1~l_7Y~5 zA6Aih+YqP(6N?KtQa4!pbHFD!r8wrkT51EVeJyHIAemd!Bl_@i1CCR5Vt6%FX)W>#eWJmF3gbB`oUv+C21jjEc zqV@8S`DAz)(-FE3WQ;Bv%;yb(dlia&iV+xemifkCDc3bcHl9QFk5%Cn$Ekgz!|BF( z77EwJJYR*gGyHdro&>h(81Q=>+a%Q1w@2Zk<+;P2Ceu0>ZQMmiz&)F-fj$kjb48y6 znZPI4)D5W7*$kc;>Gf}IM9I!)(aE+s;B8tWFoxQ&zzDp3v-%w~S2OW?eX_dLM^^&O zh_MWQMkv`+hW7L(GiueYsQ_+A{+-*e#>1pZ!R7^&xHqJ7C5q+$)83iJHFa$bAEi!J z#MS{7gxabo4u}XrAW%z{Iv^@4piCA8kr`ynBu=y>nR}fmMGXi7tLGK-t!Z1roOoppL`S#_EE7`hIAZl@MlZH0{u z9*noLfch`HmxBjmKDfaF_F3SwUfHU~9PhCm^iUs8i{50p$869f|7BT8f|U!J*G(k< zB<;PbI$Zr_oG-H@T90tdHO}5VxLQ!P%f{Zkq1NZ{c{2xRKs#aySr2fxO_z_F(z;Y) zOYJrViNQm4;JAw+tJrrWdeX|xtf+a`ybMi~mn43N8R$*Ayf+`*ccO3S= zbGrgd1UR^;u^7+ErjaXP)4qKpgyJQuZV*`fAP)w3Tm?ab$GtBFUz;YRK3m_{;Y&H; zmU0e&DjoJ18OAce5+vb&2Dju+IR!LZ z%zAXcWlxq?=7Z<*h9RlDoHa3XJ?^k5vsQd*_lfpMW0`1nDB#uZ@E(tP?{JXxi{FU;hNk_mc z(w=c-y^we*A9?E=j>s7zqi7(w{L2mx5NL4wA@OHmo?ZpqC|%Bt{-R$5;uA@6OnN!$ zZ*)i?>rgl7P~hW6(4hgN7yKVY4-Lxy5@3PnZ7HC2ifHY|Ej!glMn42<&zS{i^usqG zo7R$3>C|6FsHHA{2%&<3d<0-%AnzA*Jp$pvQ-O^3((c+u4|cnrer}M%CES2_SwNGQ3z0qUmx6XC|RqOa7H_ zH*t6=qFLZq`4=z=I4OsN%}DvtUyn`lKpgbWT=jseq6kO~AQ->M2}XjU`jfLjm4>K; zeq;h*X;8kG-4`|IDv+$57tA_tz5pP(a>Yy&Zx%_~Fwbz{inp?_24IBi<&03*+wkBl z;00FqAF(OfM0d(2`sR#@{uH6zy#e+?lJ|WkJZr~TVEg;|jgh+ZmplW&<5#}R)<2Py znP3Wt$uEn!Kp1H4qg2{NUA>w&Dffr2d9dH}bTy=nhtSb)U-REldd<5lp~gdaGYDjm zZ+Ehvp4BIZj4wu_=2$i!pmS7P%#s&_mwtJF0-R-r<1D6m+~w+y4g|%%@EtDKZZmk) z-pL-dm*7zYV$xtZ$ymVDY={N^#7Ajt9(XOaS|928=P*q6v&cwB{H7*Ctw)F z(c+Q;T=P7nfNPH{{{f3C>x4h_?QiNNX|+wzsWh3ivjFOr%!?>(Y6+Cz!w2Ba6QAHs znM5LiL}qWCmPp*2dFVm_x7P_ZfO?R}AgQvAFW^&mksl1S1O<$YF@QwmOuq42o$>!g4VW9z9h+Z4HZrLvm;THk(|iZYtN9kw8!Kd z!KYwLW=4XPhUr;w$Su|$y)xQC{{bC+N6w$LkdtTG@B6Cd{E0>JMM~HhqSo={bppNQ z7i?6I{KlU?H#@Z=w@(i`a7IGHLE1N#{qVbFlvF$$KAmTCbtV0}!>UhyJ`W(=FrRQl ze(5Y;3?@{z{Y|S6?{VJhfGuMo#>U(=U%CT>0EBL9%g5tN28Zs}6Y@xRvogxX#RRdp zc^FRH-u&flWMWC?k?C9j2{utim1L$sLIE%Z^fxqq5A<#o0CKHzAP2Od93l;B*Voa$ zKMt~DUvzGk2a(rruNUaWAfVF5KO~=k-+xT%IPQWL0eo~&1W?bv@$C!XAVnt``a6b< zU7t`Vt>?2YZ6j>ml{d}`7%9d=)n$h@Anbm8W(Xqx?MsKsfGM))E}H@$d3l0XPqq2V zYFvyq2!z~m|1D`6Mb!z8y2~tEbq1I_4;>j`qu|pUz+UY|xl6M+eUC$>Egcd;0Ec)2 zwBfgRCP3CkDPlI5RbT<%U<^e-c#4i`L9zhS=!L2 zkgUf&EseDsD`iu>nQ(p~ttrVQ6E?=hO1Ufp8Kb4U9_}<@mAAmzXo?p&kiJ{|@aJPO z%_)|#Tp!70kAP7?pd1%8)N-vn%)qPldIFk@5Q}(TQ;3d?;8;p|(iTyd8ViEr4VwFy zjlYdeJfkZfs;lcpjnoLkhN5_h<^qUsh*|rJ3Ng1^&YWZ+vgTx{XR(osB(0#^Of#A#)7!U z=pl?OCky0^0nLcJ?PopBrF$#~BkB%0ms2`_T34-xzvSz_c+{>=iV4We(cLtlPW_Hm z)#bCn)K2|5kUa&2dYrRfPKZ|I=;HLQGY*~oH1W$Ov|%uLdv#9xFH`wXEHeQm78Sx> zp3k9ych`-L^Aaf{HOdIQVapcE7pF9TZ&O<~4n>Me!l=;@Dk02cjG$zXNN)b9#Yw>^B||4^d`|`P+ozTHIuLkRnvxLEVTGP)Z(mD}~;+|r6V$Fy6IJlhRVQqZL z_v$e9^$cQ9&DSQWCv`i{*||$@XbHn@o=Ef=r$OMR4&=(fj>7q0;lKxpT=4~d_FN`O-Y zjy$!K)8!kDwZBD+tPl$Z&n7bA6BiIfMyHCsP`i*3bY6lhK7$R!e-XmLUBHqXf+#G$ zimPYEFusWiUZaBImcoZ^Fc-;4;b>Gs0$?XwQ9RkvPV8WzzE)Iw+`jo5#P#eq?4%Ac zwnZ|xN))xs*8M5F;4IYKz!XJEz|`)feZo*ca-A&TUl!5LKmGbLtf;Ab0&z+%tTz0<|Wy#by;{L%irHC%>yfgKjyjpbVB^6p_fPT(g~W zTbLJ%80o)(6_wtnylZqmMd$&Pr7`B!uT*0fGh?icR9u?r{QWy&F}EU15V9kbV(}@j zQ!H-^uCyzdn zt<(ck@ZeQ0Z`3~PBIx5;ltAT3sFv52 znZbRa+GXtVy$UIU=XHps+$_lip6;l#PEI3gWh*LDFftqbZQi3dHCC&%F*tTi@tS^xV^TN z7giRfrNVcX=Eg9=lk<@98ZfKly7LR9$X@@7U^5!n2YR^;Ju9J}4iqnH6e6 zTiA}yDNI3e&aztwA_mQu@K%gtjQn_eUCW;nup3R5AkX6m^ooZM#^IMHvbCEtG2!Lo zEpa`>iU4Xm#IsGsS*qeDfjknuk2Ejm0)UsvxAV&I0q`UqIn0%?wf{XpPU-c>n@i$t5p2Lzc1bC42I(05>2 z20mCqzSv@?2Z+6#ab8cdc*5J=2695aSLuz;7*BPqm>-hoXnHh*kQp5$+xX8?mCnM6gV6=P?5dJK8x-Wcka_*)9G({kYQu@f zscGexW1@;P2}e4^lYUhpJ(^)4TnA~Yx`)rPWvabm$T-z;+cQ@MVUlIs-~~WDRNL)( zWLqsro2RSRP35${c{T!gabQt0rXF4RTX^?+^-KC~x7vO6&%z9}0_nB6;*NYN`n#Mx zIag^ZEcAlmQU)e1T)FruD$<8Sz?HKE=h4p+*i{$)r2oUh$Jcp3s^yf**lHA?IG^T3 zN(+MggvC?Fx%sH>-&9ERc!)(aMuE6Qug4l_g$-FIMT0l&4LnK9=Q*Pqw%E^RW%vIo z*9mM4z|PE%;=h~HFU%B*=q1Q3su0yCHG&rV2U!LLjxM2-f8VD63e=n7FaV+;GJL4_ zSNu($ZEENC(a!0ylqbWECon?{oonA;*Cl%^vSDd=OI)nGjn%@YQnLPaElh@U<8YroY?9bc45@ zn#i3HV3Nz7kbl)4=GpTPkccQD&yA2@wC&bbnG>mn3LG~NGvU2^lR^XOgfcn@XaGrG ztpa?IDS!r?1D9E8VY6yps&?TLW2c-B-N-aR(SRc;qiEpZn)Z(rZN*iJ-U zY1CpWj-WrjZsh0e{zh)@t5ZRPV890(+(21sJ7 z5gq^>D6n}`ZR2uSXMxHu2?XBx2-lDji zGJe;BqbLCQ0V*p100BynhVgb|HVC#XIiUZ>Z6?o~C$D}{bQe_c$peN8LO4z0q<^4MiYT_cLa{k_F_%y4sYyL7cE2GjCNkmfLzIITGv@%nB+DqJjgC@V<^fRGk{sIx zAmb89sF|Dd!jc{h%#KNYSlEsw$*P7s{^VB4u>#F(i9Ye*%)9+1n_FQ)rvhg`if?rB zNS7|A@i1rCDMJNwY4EG2rAW~cM`Fr!P0sxVH3JcVKdAx&jK}M0D*JA!yPCiGd5FVI z!1Lpu;kg?C11@e~L{SQ;`E#%%a~fu17scZ14_xUX>}`=c+HJcB;AX(c;#w3N)X=P! zCtE_J$L~*}v^74j;oHJDMdIBNMlQ0UB&rsD{U?(rD`-lYRnYXy;v$uDe_6%f2mUt; zfVq|YLr4e0O|!6{to&h*sl|>C!}l*`Ja%T66?unwWiOf^^!-Ds1}u+RoK8`jv!v9e zyHee9)?9ah0R z1nMj+#>e-mL$=+S)oo)fvAW1zy~I+xhe)TTnrdYp1f`Zfa@y+Vyab4qF&1@S21O0E zyevLJP@ghag^t;)HiBnmot^-zdKcX5l5AC}vQ;$(p(cqS0=Iyjk~p)@@$gbHWko>A z-@3_PK)aiLY=7>$2MO%np}1PZQNEp(yrK^<@kY&T);p%~#v^%g*Pl>v*{TcydRjEA zVrmKAr5Q3>x8Leb*g?mfI4~ZhY&>ybJSBhNb~j|4AGOydhdtr2z~}GOr_%SUd9{O; z36`feoRuXa)Ouno+D&2HEVl1Di!cJ%P)Fg1Q>ztR%<+%-Y8GmfE2-GUTObi zR?coR$+senFVAO~B?W{D$B|45!5l{5)c$x{R`dq;Jo!X@TCDnM(VN$7khfD-)!~{W z+TasbMjiLv2HBXYfKZ9;)^ zc$){hvDOhYO5{PUPPJ=Jwc&Nnqp7feeV>lnT|F5ROdU4?Wp^Jev)!@^xxd19a>G}n zXp6>&a&tLNQxc|4HM>~;bERZx%7L3J|2Y>mZAmFEAHhs}82`c7XP0>W*Wuy+$=khs zu0jH@l2FW|Qtotg>`f$BhuR_d}uH{B31W;+&^M8Dy-LnW`60OZzb~-_LADdz);9-y) z6DZ1=-;~*yRxd2$4AI(H6C$YMCY2Fqz&EbaOt46Smol%Nj`nRup;}Z3cUHfe&IRa1 z;{lVE{i%$wFN=Vc4uxm(MUWgxPYsQk=Io8PAvkJ%==GuPluF0R-E|0S3@N;)l-oVw z)r+LEtDUeVB9C5)${^Q`y+*xes=)$>{0LmHwzmm73yLnL?=TEC@zsYb^ZGeCpdbi6 zH5HYY9B>ic#gDo=TayS!JmZS<_jI$7#@H5KxEjhwtVtG}r%a04P}VH#g$fR?bPk79 zF3w+bT3Q+|=DQ7(5>Fgf06q)+y&;7``P>^+Hh&K zPp^|?2WcOc$agGN^i%24eDB^O$Vqc4pUHYrK{9oGwg<zp>n^Wb zTNKe9-Zg;7c#ZnJC_;W6^80)r3ae=}EPNm;HY{&1jBDZtgM?)ghmauILjK2ozE5wv z^)Yym&80UjYR<&~A0SF?C%h(3?LaMHT~70xuefd@BviRr9G8ng|7L4wUr%u^m+;+u zDf5}C!;KU*6fasr%mmRkh%dbiUD89GI!*X@1Gut+)+=3(e)g}*(Z4KkA20l|C_?z4 z8J;?IUzg&4Lwskn_C$rmk~|K3qnl{X?}{=@$l|-dZ?a6$e$X2Wx!S-mBkOd zJCmg^M&mIrN6U9;{IpfQB!OPrMhHw+w3kcjRUu)M#2uq|ypH%+g4zu(Klg$%Pn1T4 zQE2*X-xwpwH2J;Z6Ve%0z6N)J^gI{*KD)dIg3aD1UxENNcka8v2=_ zi`hFc$gsZ50`vWuQE7TCuT9OF$z5yY-?(ga72-$FF+D%d-+zpkChe^(fOrsKr4h#6 zCq_FflYEsYA^Mqv*-aVkd{}V_dBm|bKcZWJD64l0F4}Vj8Wi9t3}Gu2%`*4*^K;UU-q>i2 zF385*62GzriMdhDX+YA0%n!g)R2&G46|(eIqKd<63wLFnK9oQ&!j;0tmghlzQDQf5 zO8pK>G3M3+`GD>xSAhzmwzR`$l+ELJvtHxo;NB12hB@)Z)i(e) zgh*@xh+fBa|F+#jy|w|AVIDhE@{@eTee0uLu^oKm+BF>$U*75vM+b;hNxil~>-+t~dsaYpK+|JcaS+6OP|U4OlvIz42}~| z6LRVyuX9yM{g3yQ1%YqNPVRy6K~ZnF(k)OgJl)B)xCr$UiZA}S;rCQSKMYUuc=XjV zezB&3Hit3y*6%YfWPqnl8s9m8=s~3liOyE(&QCiIPCTiY^oXCce@0b*bkDd7iQU-U z3>A2=K|y0wPh~}D-9R9cwLMp5=Gef_=^bAYAr$qe-^CGB)m-#H=8`?m>qe`P^3#st z;W~o?)|{811Z;YmqI-~Dy~)xE5VN6zK5%VHDp!H3oarIC7B2u#2|*f+_ke+Oe7G>b zi0!jxXe2DkM4O40ov=LuYsjDhmvGiTZ~i3DY?!# zsNkhfnd}BUZu^@9Jl0J%=z`EM!YXg(N`NbrOlI{N=vHsAy!YpV-2bUtd5nNPBWva) zn}Dxvoq4Mr0m{7Tq)_xmr7d-9Pqyn!FZg4TX%5Kd0dNR|QgBArzCn*T@j&);i(Mz)d>rV3 zJC}a=CuciMx4z-u)&?`CZNq_oNBK-hi$lzD0qb~4l|!agBhMa&+)r5etBTf-nw5uO zmm|LWYVnQ@^-WIS_O4V@uD`7J`?q?_wimwHUo>B1!}^DpZ|wfkYLVZvIcq{&-wc`c zIC3(blgtsqJM?j+S*7Z6+cxa*#1PGqq>Edml>5cpX|)@@Y})Gex~4Q`xVU`t*|brt zzJ6L4cPyMUWiwaq_cE>&S8GXfg*Cd!JQzJGBp5{Pj~`4Yo~~~GP1S7Dg*QG}KS}*u zz6tc|7^?Ro`GW~K*|EpCW;3A|Pe_p>c^sni)+eo=*?sPB-s=y6*K~CwIBm9LwTiu? zGTM7rC1R|X=zU{$gtH%WbXNQf&=rNti~zOiv?YI3|cRf>P)4fI{L zIBMKZjin|s&aCJ~I$Kjvo+v9{)M|P%lZ;{r83=~Br{%HrmM1yKrslu5YtvkkUO8fN zKqys5h$iE&l^jwg*&G%2j`lwHJaC;^9A zT^wgUp!1}4``r(%$IoF-7S@Jerc!%EBOJl5Vmo3A%bNsX&wE zqu*Y!trAR~?U@g=P&S9j*Z@B3x&Gyp%o8`sE?Io&vclbSK!39OKu9P6F!q2m3emaW zZ+TL5yt?^mpZiL=Tvr+Z0hR6l!32P?5-a7u#~=km%mjkyyl8An(!K$WIOE}^pb(HZ zbrAr^OeUyL{Mj5YZ%N7LT-+RHuk6qRF-W}WMAbDCw`_e~alq+uQQ=h6l{fG;{pBmqP{M;{` zHqi{f`#;>|C1sLjtmc*{caI;O^;g-GI+@JO{_yy)!HHD$X&w0f;*_K-DgB=w;{Vh_ z{M5Jryru&s@#Z;81bn8sc#svWikuiBx4kcqq??l>5B2b1C67t;?sQL*tz1|y3(w`x zVPYgwA09P!`7dNAoet>pU5}uH%>+2Etq|<@U3JuLn z0pgF6*h)#)%!fNA_-%ulPEJhrXltTSvEBG-c^g(nSH&6=kLOuuXFmB6lQ^|+2|gJHpC^rN##^hkYfF)PSz001 z08R6%-9!S#Bl#^wU9(r>&%4yI?K7L&Z+RMib5qw z`)Z?FPyDZ-r=Ff#y_@=m`VirJ?T~vfcjIn!Z&qjw0r{O$h|XPO`0&{3fJJ^Si3{hZ zF}+XECHYwO)TfvFSe5%!l)ZbnSfSW1;~|jBq{U0j`L1910UbO!)Dma7EXo&OK7mKaUuSUgY5ZwthYh0{ZI&p-zPN-UE{p6l|Gr`UC zBItF|rs|d^VF{9T-Fj%#93RozvB@EJegKvzlqd#V5=$m2K^El+R?hw1)y@unGhgz; z=gTsKy=R1+YX8_nvZ0sVe7$@APeI#N{W34_M6P6eSd}yn*|Az$eA~7CVp%EUWJ6X; zs8%(Mz}KOTnL%Kui0=gyf?^VQZ`9R$cStAR9SvI8al_sses352hx-`H4gZ8zuyVg; znDLD-GOd;zQ;nZjl1Y5wH@-V~$p&gC8{7Eyv<^2rTOG=~ovW|CpD#kIf$d7mO#yPQ4?vm0b=L6^@y?o1E_*AeqknBl=a=qQrRZi+nH?V?r z8B5gj{dO^MwR3!U2uE@)O&TZEWO^c0#IQ-7Sv`JQOYDL{F$^)E)869 zSSyO~B6&FZx>Cr~Nng6*BheNYUEWPBLK@z_LLqYq-q)3g79js=?VxdMt0|KiF_&aw zcc{j!Fh{0f$`seWTi#2q0!^pO6ogI7v>I?zCYeh!Q9f6+I9@2{dp9d)E2QF1C7v!> zTD}O`nlDD!A6$qG%U)F1Lmw=p;T4NTvII&JC03QTUo+=bAm3BnaRh0aFExAk%PY=K zo>u~RVPV;&*y)D^+^ zD%7}goronBN`s{w6ueN_94mdY=o-^Pr5P3~sYZa8?ZOe+Ds>CY1>FgaW*Iq$m|Nv{ zt3>4Ul@ic6*80giWgbV({4m{RFA-Df!1|wf75`J4+6+hK|2O%!&6vvUNhXfv=2`I! zrD0z}c^jmpgpE_;t%8rek=q7dy!R=nXd4K-lP$)plErY|zK3Uqs2FpTk)RAZE|&UR zw6T+r+T7nzH6}5iZ1%7+DYU}dKxH)XqVWUPsKJvVD;=(%GMg9T zgH)pvJWt)KGdR16dCa<)?O*j2rIJU0Qhg34+h%oqK__HNHb4T!6*OAp#?i$3!w+ve z)Q0IaC8ebb`X>iS9&YAqX!Wn~BM&dUN<6_jt-ZTh?^H!-IP%Cy<8W^7I!uONgU>}D zY>#UvDLx3BUvYze-O5W%R95FRq?a9gz-W7b^Y9#l&HjB-ZxSlv&YC|}2 ziC-kccwW5iB0m_GacOh7YjvUXmZQMK5EFaiLa=NB8A|m;rtnl`Lk+hfynC2<#`Bf# zcMA>?_q7$Kp4@(fR$F$Cu*J&xcwXe>OhbV&*;{+qs(q@Q}P$ED2W@KV9B2>$Q_P`M&iZK-9)<$QhGdp9(i~!t2~+d zY)zx)y3rtQdUI*7U_O7(a#&-sjT6jHt(IYdeGH|-5AI2Z9(jStsNgB<$HoyM%&_C! zRgC3~xul+;t4+2Sw`#_AkHF%@cdJy$D9SO#5x!9lHe6UGS7sv2A=`C2wVm0z~$ZqcT_y#3=g=aAJt zL@QRjEzU>d;5Pff6G_XpEi1%b40K*^R$PF0FjC*PLCtkvb(o`GSKEjq>p`-~8}(JK z?9RfCuma&kA#~78IIr#X!z0byt33F0p&D@q>3HU+l5J7=x4 zXKVXNSt-BzN;u-3{o~G%)KwGFixbC!G3P`lYOkKU>AoU1-yr=Qz5?IBHjXpYarU<^ zs;Ozrs1))*cO|=GV|s?~-gSvxV>TOwo6Hx`Y7pHSry%UMiQIluTNUex!;SGbgX&kn z?z!(~dnY9n1V#=So#GZ!g~r~U529R!Yte3Zp-@g@=n+EjAhr5PV`4a-tzjKh_E@w> zttIM(7ve(_$C)&0iX<${4Hdstf=Fs^lydy>FiqJ*alT)73(O#A z$4g__rS_VNu4H)6@iwIWb_Z|>Y0i)NKtG--^*hzMJRzpPu%@Cy z;Ud^M;r>p%=LT`j1N!+_cDoW-dJuEHRWCE485@(x$9klWbf-;;fpLg_>VA@|MmE>1 zHNe|9>pX)_i_$4pH+G}(-sYeBfoGkQ{WKN(@X$+IcvFkNXeTiy2}T36KbojeB_m_V zr(_(Q)ZTHawE@Gk-!K#%{|cKnYL)4==RmvF37zC)oEJ6g{A0gzvMfrCcd*Q>4YS{M zuOTUo!Ptz=bT_Mvj)#xue*U&DPj6vYBrNTh;S$R=Yill#tp1fVH{H-Lv+3rX?yvYO zywmyLkDmC}G@p1!saSZRg|=tnYTdB?eWNTTr3Fm~zTbNyIi}6gyQ{yB8MiLJ zSM_eJEMZnPZ`N#6p&N&b6jGRd} zh*{w}=6|YF^AUD+BQJB+wxc{uq*~1NG4=_)RazKdJDjuODV^qvsczwxhloiV+>2lZ zo{3{NRt*U@`8!5DbLbtITWSVRa5-Hm$)~eORMU>+HO(1-p4gmUN4^I7ak@=X?TGIUfhY2?Hr!QrGV%F2j|!}nzyf9`ku#dTcU zyJHCt$Hq!BI#V2pqOS{vhO*9VPA)zYRzcM-d4s&(#P-_FsPoKNb&7bpxpvHAd%J@L zH~d9lH+#5_yATl=YioKv6v{baniA+eST=lzQIVg~{;;b~|5-(Ma?9a1*jg{5J0o=i ztihT1g8E8$qM`Aodsgd1oFCzt?wUhHD<7k8Onl8XcuVOg5rd>KD{vm3gkx8T@AS2^ zF*Ebtm5#sRfCHApnyrFwDkv$wZ-}DH5MJ#V!pMT(f zc=`A3`ugp*6E#7vl83qi99d4h3<{$b zy>U_dz`~Ul`M)kYkJgVpJHc8?t?=yGzSyt(mplWt6GgNubxpQvYG?hIkDsJtu5yh- zF-!gOT?`REQB7dRyI^Psn~hYNMSmwG$dN z)HcFi;0M&w8_E7zrHKa`og814R$nlJoC&-=zNe=2J5$0iW49sntXBr)Bsb|s{~CYR zq>)}eYo{Tq>C5xh?JIy;LbdBiSLN3zg&9X)xnY#;a8*?pTs7G^a?IH88Eg|$5ycSgm_x=hOj4+jkn&VJr{hVOUNa0bkkX#|# zSM5^x^Uh=M<(OybyzIiKq(WMO#BW^=%RgOYn6szQF}2e?a~`qv)^$EPtfn>VF_z@1 z;n5uJZYbP9^|#W6BAo_F1F=><)}pXrbA6g4SiM{HWY@3Bq!@>4|!m} z$@byC2=r7}Le_(Djjc_!wULh|L!C_1 zvbm&@(lpmW>Jdt5Kj)TZP=R>VDL)aP^`i}IZB%kVsJVe&u^NqPa4m00*pbuReE3p$ zqBzdriJRj|eb0L%{>DRD4_T#WFPkikaLJb#QRc_#ZOEv`ZTKvv>dY5QZ6i9~l~Z`` zsDhE@C9x>8FWx3MF%WmyF7}sAemZ-xqxf?(wsmAtggbu6=`Nya!iWtzaE?!Cay5et zQ(cnU{E)e>V2)2mepsyLi$I20n@smq?kr@4jOnVv&Dd%h{g|FfBT?stD_l_pm}#Zl zQ|v6Ok(T+8V$6F{_p1isj(E0u?jORscS1ajO0Hx(9 zDU&4aC~>yS-p*UCcC9qnaYUE*P>t)GVYh~UkLgeT+Fr|z7EXod_x|dzyfImC`mc``p-r7 z<@fAr>Wqpn#j;mv55k+jzkG@?5sv$p6gxRNiP2rzwUQf2z~ZYV2Vdtq|%_Qv};ms%u|U zWV_zU;&k3n%!OpjQ+Y#6F9PSlH)GPY<)&5no6kz3R!DDTSX0JdZozAIrTNOjONL+Z zYY?Ex_80loOAniMCLC-6+aKNY5WcM1wXc|dt(}#1^#I@O)Dp# o7z;~Xi4INq2oio==qFWHW@>IV%`S`r4J#ekcjWt=?_93@KkPwnU;qFB literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/Touch_DisplayColordepthOptions.png b/docs/source/Plugin/Touch_DisplayColordepthOptions.png new file mode 100644 index 0000000000000000000000000000000000000000..e6f98da9b1767873ec2f92dc82e1389f8fcff17e GIT binary patch literal 39996 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 literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/Touch_EventsOptions.png b/docs/source/Plugin/Touch_EventsOptions.png new file mode 100644 index 0000000000000000000000000000000000000000..af7f9dda254676933eaa6f2e9ad9ad585f0995cd GIT binary patch literal 32329 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 GkpBmz*GaSh literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/Touch_GroupLayoutExample.png b/docs/source/Plugin/Touch_GroupLayoutExample.png new file mode 100644 index 0000000000000000000000000000000000000000..b3f182bded34ef8895e22a0610bce1852b406bcd GIT binary patch literal 26062 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 F{{aG3`#Jys literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/Touch_Touch_Action.png b/docs/source/Plugin/Touch_Touch_Action.png new file mode 100644 index 0000000000000000000000000000000000000000..5ffce5b74c27d6e7070aa73f1270004eaee31f3f GIT binary patch literal 7655 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 literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/Touch_Touch_Border.png b/docs/source/Plugin/Touch_Touch_Border.png new file mode 100644 index 0000000000000000000000000000000000000000..fef92413044c68612fd9691c3c7537af851cb66a GIT binary patch literal 1795 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 literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/Touch_Touch_Disabled.png b/docs/source/Plugin/Touch_Touch_Disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..22f51568c70ccf6dbf2240142414a852c82e569f GIT binary patch literal 1763 zcmb_deLT~79RK~+rZx?Q9p(6yhm^Ut>DX|pLz)<PvSTtTR?4B5kL;kmVtdMbVR2 z9um{T$ipfxJ&5V$P^gWy@{}c!t{u0oS9kTgyZ`R{dVRj{*X#57{PB5se?Ffys)rjI zwE_hI0Ghnj#Y>HRwPY{RQ2&}&YG%}+5wgwAMUCqB&iMvU4M@&bKP~`hwSEvV&q`~h zS{WGQ>Ko${#%1nfabj4}Q2;YyUo<-mjl$!saW>Wj9AN{5g$Q{24L0_8JR0Tvrx~Jz zg$FYaKy+4AXmlJJMfb5GSbIC;@YZ-75weEdqNBqj!=R7OMDbIlIvzu^%VwYWz?a3_ zyyQ4NSHA#ETdor859DWs!7mPb3%n=>_+W64-gWk9Qm6xQ|M5gxtOndIAwlVD`$QDEhuc;YV0Nd{t_NF2mUtek%q_Mcx9g0a(-cli7Dd#z$WBL$NRec=BztJC1hkzi82CbgtDsJ-=!x**sCoHM_T;S&>O@J$_)TW~tm*$Fo1qCa~!G zPEnvdQ+yXTQg_Ly%5i!rC?v+5wJ&!VyRl~k8M(2qpatd7xx9@W0==JUTd=wfr-oNx zHZca~ubXkWqybxNKK9DndbgHV68?^bCiHqZX_Bd+n2vWv8{amx;M*59Smcxp_29|E zaeS_CpwW9jaObMureW?EF)q4f7yCtS)BevA0BXCOab&J7K-#*r|!>8-= ze{Iz8PkLK=>IhG=ig!8?xEuzKB*(2+{-WsB6Gyalc$WiF3_|^maW3U#j?PSnmW}9DOvujV zk$~7-OJjeP@sfvDGsgc;eobvD&@I!1bV~})Tt9LwbETUGV$SM!gs*Q-oo4BdW#`t< z09%4Ri(A%o88g@o6GjVh6Ep^>O7UC0&MnQiX!cBV2EL4$1^kA6_=yi$2?rlUnuAU2 z%ZB6cMEaK#_nC%2=~vFd&a-88KPQG30^4DZpkP-6JEcO@fp@R+;h%@dLlw$d1L<7V zs73ZIBhRi$Y%jNbnx-dyFxn`<@}0QteY<(2tMV*QjkKwbx=ZZmabwMBq(eR23AMSL1(yXtk}UL_cMQz7X+sEuR*mt$3iy-ke{1*z;h zIBq8k@w&ONZC9gDE=qBy`Z@0?Y~+H(Qhe)p5JRHTFSkMpl|=l6N(pohX3~IU#HacF zvPGSb`}*jR=pRi}54zeh2K8!o7Be|Hc}t-PBW~QdMpuWM#N(Mfl}c&fwUt?l+uOH^ znwroOiDYPc+U6jSmxD~LKa`Mwva_=rnwg>Xgi!*(;S(n5>kpfDFE@Ym!d-^aDJYG9 z-p;>!#nH*>NJ`3LT1hPjArgsXLLSh%qBRlb_d$_rXG$<2XxCzdPRz|<2JqPi`Z9yR z7Sm_yVn4Vnzvgz(HS6HL-VuW*v5QuB69&qNs&8mOnV6VVI5CG%I%(SG z0Srd8^)p3~eMmL4lQ}7$HWYrt^wNWt)00K;sfRVAsr4Bo>5CJL?zI4k{UyIYmg+w% zD2_vTYD@K(YDx7eE9R<$IJ*Zhor|5d4nHd651p7&lNxZMK@j$!;WrLpPFd gs^wnjzeV+KaVojP$uPh~_2E{LT|Hc?zYj|O1HLie@c;k- literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/Touch_Touch_Layout.png b/docs/source/Plugin/Touch_Touch_Layout.png new file mode 100644 index 0000000000000000000000000000000000000000..8bece80e332066441e4a0e2991180c82e48d396c GIT binary patch literal 14996 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 literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/Touch_Touch_Name.png b/docs/source/Plugin/Touch_Touch_Name.png new file mode 100644 index 0000000000000000000000000000000000000000..0ff61872e10e8c80fbc3dc8f7982d792c631fa4b GIT binary patch literal 1888 zcmb7Fc~H~W76wI1;tQZ`RtSj;A_64|2quE;O9a_9MuY$nX@sz&0U`)$A%O}EU|CWE zB2<=xB&PllsVs&hrlbbw3$@dEPXBs$=G^bzbI;8E&V2XWL-Y6b zgn;*gRa8_USTDCgC1xtENJCvYkCKJyN>GpV_HqmUTXQOr>!1nm0P zG2ACAI)a!6$C2VL#9x7calzIo%OF<-(h`X{2DgNJ#>Yp;M!`P@e>KUURr0xwb#o4; zgsm{V1D7z`U$u1=oDHU%@$eT5vP};5YWTa?>EYL2xAd5h zJjc&Hsg_q*_=h>~*5DP1nv9*o&gewaZa>1Qlxl})H zFj*vJXBs{6N^6`mq@Sa9qz^#vZfn$aoRYjv1a81e3`qxBwtA9!D;mXY$~3i0E2^SF zymEQHx4dV=3uDwFpYcV+_Lx6XENkAwWG~XffhYf2+wCl6q&M}DV1xcvp|_28wP${( z)y|a3xr4ug^(28qb_hY9>GfKjJ?jh{S~_e`hl zD*Xy$s>@v!6Kd9@yM+nZFZU39!k)_cXVV+ns(MS#*rOxtQLPl?p|l_Fjq&Zz@%Df= z8PfUQ=DO+&EOyG*R#LuO@aM9-LC8P{izDN_yRD}HJaZOVZFb_4@{VV)1Wu{wet*+2 z8ch%Oxx|nU^)t;+BvD-lI0jzUU)->~f||@4mW~u3x(V99vD)vaEs`zymxp4y#^h|i z7;@91pLjtW?j@m{)}HMrsks*6*oNMoNkQ8+U+Zak?Btm50Lr#~s&*a@{6FqvfT1l+ z|1CpM+ZmV1-r)5j$-ZN5Ih3j{_8AVOSoTgg52hYqU$T!(E!Y8gVi^?A;EsHc*;gk! z@wAM!O`ZMu$$?{#QX4L1?NFUi%6|AZSs^4yUO=1@$qcC%%qJPKs}sRvKQSY_vfv3WXQ0AO-Ygm zjGpsQD1&JDLW8CAjwqe&0-DnjX5x<1c>AzyQ>&zeabRhC)#F1OMwf6d%lY5SFk>Fm zS1+unbLX9Oc|4m}w6ULl94QgNtI1poMVW1*{Xx+SEm|GRTuJ0Q$?JQRlTReDz zWgVm~M8nY+9pTt(c22h4?hWuxAIk_?xmAbZKp&W2#&e0QfxOH`VIBKs7KX^v3Mdob z{(u(0sWhYvI7!@_cQ5upwa4i!9}-(=`G^}I^2Z&DA!_;LcX~ObRf#0Bn#rt~J|o8A za2g_=p`NyB>SD)fb|1c`ESnSo4Q$nJRr!?8r@a65a7#-##HFcrblOsxAeO{hoB;o5 zb1bc`UQijs`fY2|)!mIG5C~6Wnko5$(yWY(5Qc+>Vs04o@P3Q6;=Gy+3!=05W$~`+ z#YS+KT+cH2|7duKDYV43yZy1p44k)?#(@M|#?Aoyu*g|0q?O_5&BMl4 z$alHr8=`LQ!8vSYW?mX#VsM9&q2qEKBne-r9L<6`tv~X?t*1v^Y{L&mT9&FhH<!>JQID0BLsoas+5erMKAY2(v6WxGhxk|Fza{xol|MKy9jusM-D;Fr McVD;rF5x-<1)$1}3IG5A literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/Touch_Touch_On.png b/docs/source/Plugin/Touch_Touch_On.png new file mode 100644 index 0000000000000000000000000000000000000000..3f30584747fd594c8a5d103f921fa9ba22d92fc1 GIT binary patch literal 1319 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 literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/Touch_Touch_Shape.png b/docs/source/Plugin/Touch_Touch_Shape.png new file mode 100644 index 0000000000000000000000000000000000000000..bb407d645d7bee2e2200b6fd3c211f9fda765e98 GIT binary patch literal 7387 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 b$MlC^zg@cHe=Pg=KT0FK8SXjuO6>mvNWWQA literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/Touch_commands.repl b/docs/source/Plugin/Touch_commands.repl new file mode 100644 index 0000000000..6e365fe735 --- /dev/null +++ b/docs/source/Plugin/Touch_commands.repl @@ -0,0 +1,109 @@ +.. csv-table:: + :header: "Command Syntax", "Extra information" + :widths: 20, 30 + + " + + ``touch,enable,[,...]`` + + ```` Select an object either by name or number. Numbers match with the numbers shown in the web UI or ESPEasy. + + "," + + Enable 1 or more objects. Select the objects by name or number. Using names makes it more easy to re-use a script. + + " + " + ``touch,disable,[,...]`` + + ```` Select an object either by name or number. Numbers match with the numbers shown in the web UI or ESPEasy. + + "," + Disable 1 or more objects. Select the objects by name or number. Using names makes it more easy to re-use a script. + " + " + ``touch,on,[,...]`` + + ```` Select a button either by name or number. Numbers match with the numbers shown in the web UI or ESPEasy. + + "," + Switch 1 or more button objects On. Select the buttons by name or number. Using names makes it more easy to re-use a script. + " + " + ``touch,off,[,...]`` + + ```` Select a button either by name or number. Numbers match with the numbers shown in the web UI or ESPEasy. + + "," + Switch 1 or more buttons Off. Select the buttons by name or number. Using names makes it more easy to re-use a script. + " + " + ``touch,toggle,[,...]`` + + ```` Select a button either by name or number. Numbers match with the numbers shown in the web UI or ESPEasy. + + "," + Toggle the state for 1 or more buttons. Select the buttons by name or number. Using names makes it more easy to re-use a script. + " + " + ``touch,set,,`` + + ```` Select an object either by name or number. Numbers match with the numbers shown in the web UI or ESPEasy. + + ```` Set to a value, for object buttons, only 0 and <> 0 count. 0 will set the state to Off, and <> 0 will set the state to On. + + "," + + Set the state or value of an object. Select the object by name or number. + + When selecting a value of 0, the state will be changed to Off. When selecting a numeric value <> 0, the state will be set to On. + + For non-button objects, like a slide control, set the current value for the slider. Must be within the range of the slider. + " + " + ``touch,swipe,`` + + ```` The numeric direction of a swipe, 1 = ``up``, 3 = ``right``, 5 = ``down``, 7 = ``left``, to change the currently selected group. + + "," + Select another button-group as if it was selected by a swipe on the display. + " + " + ``touch,setgrp,`` + + ```` Group to select. Numeric, and has to be an exisiting group number. + + "," + Select a button-group by number. + " + " + ``touch,nextgrp`` + "," + Select the next button-group. + " + " + ``touch,prevgrp`` + "," + Select the previous button-group. + " + " + ``touch,nextpage`` + "," + Select the next button-page (group + 10). When **Navigation Left/Right/Up/Down menu reversed** is enabled, the group number wil be *decreased* by 10! + " + " + ``touch,prevpage`` + "," + Select the previous button-page (group - 10). When **Navigation Left/Right/Up/Down menu reversed** is enabled, the group number wil be *increased* by 10! + " + " + ``touch,updatebutton,[,[,]]`` + + ```` Select either a button by name or number. Numbers match with the numbers shown in the web UI or ESPEasy. + + ```` The group number for the button. + + ```` The mode of the button. + "," + Update the button according to the current state, on the display. To update a dynamic caption or external state/value change. + " diff --git a/docs/source/Plugin/Touch_config_values.repl b/docs/source/Plugin/Touch_config_values.repl new file mode 100644 index 0000000000..4b35d91752 --- /dev/null +++ b/docs/source/Plugin/Touch_config_values.repl @@ -0,0 +1,44 @@ +.. csv-table:: + :header: "Config value", "Information" + :widths: 20, 30 + + " + | ``[#buttongroup]`` + "," + | Get the currently active buttongroup. + " + " + | ``[#hasgroup,]`` + "," + | Returns ``1`` if ```` exists, or ``0`` if it doesn't exist. + " + " + | ``[#enabled,]`` + "," + | Returns ``1`` if an object with that ``name`` or ``number`` exists, and ``0`` if not. + " + " + | ``[#state,]`` + "," + | Returns the state for an object, by ``name`` or ``number``, ``1`` if ``On`` and ``0`` if ``Off``, or the current value for a Slide control (0..100 or in the range set for the slide control). + " + " + | ``[#pagemode]`` + "," + | Returns the Left/Right/Up/Down menu mode, ``0`` = normal, ``1`` = reversed. + " + " + | ``[#swipedir,]`` + "," + | Returns the name for the provided directionId. + + | 0 = ``None`` + | 1 = ``Up`` + | 2 = ``Up-Right`` + | 3 = ``Right`` + | 4 = ``Right-Down`` + | 5 = ``Down`` + | 6 = ``Down-Left`` + | 7 = ``Left`` + | 8 = ``Left-Up`` + " diff --git a/docs/source/Plugin/Touch_events.repl b/docs/source/Plugin/Touch_events.repl new file mode 100644 index 0000000000..d596aa2d1c --- /dev/null +++ b/docs/source/Plugin/Touch_events.repl @@ -0,0 +1,44 @@ +.. csv-table:: + :header: "Event", "Extra information" + :widths: 20, 30 + + " + | ``#Swiped=,,`` + + | ``directionId``: The direction that was swiped. + + | ``deltaX``: Difference from previous X position. + + | ``deltaY``: Difference from previous Y position. + "," + | This event is generated when a swipe-touch is detected. For Slide controls, no Swipe events are generated, but an event with the new value of the control. + " + " + | ``#=,,`` + + | ``x``: The x coordinate for the touched position. + + | ``y``: The y coordinate for the touched position. + + | ``z``: The pressure applied for the touch (when supported by the touch panel). + "," + | This event is generated on touch actions that do not involve switching a button-state, like non-button controls. + " + " + | ``#=,`` + + | ``state``: The new state for the button, 0 or 1, or the new value for a Slide control. + + | ``mode``: The mode of the button. (TODO: mode values to be added) + "," + | This event is generated on state-changing actions for Button and Slide controls. + " + " + | ``#Group=,`` + + | ``groupNr``: The new group number that is activated. + + | ``mode``: The mode used for activating the group. (TODO: group-mode values to be added) + "," + | This event is generated when a group is activated. + " diff --git a/docs/source/Plugin/_Plugin.rst b/docs/source/Plugin/_Plugin.rst index ef84eecea6..b53e3a672c 100644 --- a/docs/source/Plugin/_Plugin.rst +++ b/docs/source/Plugin/_Plugin.rst @@ -360,6 +360,7 @@ There are different released versions of ESP Easy: ":ref:`P120_page`","|P120_status|","P120" ":ref:`P121_page`","|P121_status|","P121" ":ref:`P122_page`","|P122_status|","P122" + ":ref:`P123_page`","|P123_status|","P123" ":ref:`P124_page`","|P124_status|","P124" ":ref:`P125_page`","|P125_status|","P125" ":ref:`P126_page`","|P126_status|","P126" diff --git a/docs/source/Plugin/_plugin_categories.repl b/docs/source/Plugin/_plugin_categories.repl index 187b37c09d..4d74f43a7e 100644 --- a/docs/source/Plugin/_plugin_categories.repl +++ b/docs/source/Plugin/_plugin_categories.repl @@ -29,6 +29,6 @@ .. |Plugin_Regulator| replace:: :ref:`P021_page` .. |Plugin_RFID| replace:: :ref:`P008_page`, :ref:`P017_page`, :ref:`P040_page`, :ref:`P111_page` .. |Plugin_Switch_input| replace:: :ref:`P001_page`, :ref:`P009_page`, :ref:`P019_page`, :ref:`P059_page`, :ref:`P080_page`, :ref:`P091_page`, :ref:`P097_page`, :ref:`P143_page` -.. |Plugin_Touch| replace:: :ref:`P097_page`, :ref:`P099_page` +.. |Plugin_Touch| replace:: :ref:`P097_page`, :ref:`P099_page`, :ref:`P123_page` .. |Plugin_Weight| replace:: :ref:`P067_page` diff --git a/docs/source/Plugin/_plugin_substitutions_p12x.repl b/docs/source/Plugin/_plugin_substitutions_p12x.repl index 4ae505c75f..09725dd87c 100644 --- a/docs/source/Plugin/_plugin_substitutions_p12x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p12x.repl @@ -37,6 +37,19 @@ .. |P122_compileinfo| replace:: `.` .. |P122_usedlibraries| replace:: `I2C` +.. |P123_name| replace:: :cyan:`FT62x6` +.. |P123_type| replace:: :cyan:`Touch` +.. |P123_typename| replace:: :cyan:`Touch - FT62x6` +.. |P123_porttype| replace:: `.` +.. |P123_status| replace:: :yellow:`DISPLAY` +.. |P123_github| replace:: P123_FT62x6Touch.ino +.. _P123_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P123_FT62x6Touch.ino +.. |P123_usedby| replace:: `.` +.. |P123_shortinfo| replace:: `FT62x6 Display capacitive touch overlay` +.. |P123_maintainer| replace:: `tonhuisman` +.. |P123_compileinfo| replace:: `.` +.. |P123_usedlibraries| replace:: `Adafruit_FT6206_Library (modified)` + .. |P124_name| replace:: :cyan:`MultiRelay` .. |P124_type| replace:: :cyan:`Output` .. |P124_typename| replace:: :cyan:`Output - I2C Multi Relay` diff --git a/docs/source/Reference/Command.rst b/docs/source/Reference/Command.rst index 7eadb88af7..0b052265f9 100644 --- a/docs/source/Reference/Command.rst +++ b/docs/source/Reference/Command.rst @@ -716,6 +716,11 @@ P118 :ref:`P118_page` .. include:: ../Plugin/P118_commands.repl +P123 :ref:`P123_page` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. include:: ../Plugin/P123_commands.repl + P124 :ref:`P124_page` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/Reference/Events.rst b/docs/source/Reference/Events.rst index 6289f67c59..9184292a3b 100644 --- a/docs/source/Reference/Events.rst +++ b/docs/source/Reference/Events.rst @@ -535,6 +535,11 @@ P115 :ref:`P115_page` .. include:: ../Plugin/P115_events.repl +P123 :ref:`P123_page` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. include:: ../Plugin/P123_events.repl + P129 :ref:`P129_page` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 58f008172bf50e5e91121f65755d1f40dfe3a5dc Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 24 Mar 2024 13:52:52 +0100 Subject: [PATCH 082/113] [P123] Docs minor corrections --- docs/source/Plugin/Touch_commands.repl | 2 +- docs/source/Plugin/Touch_config_values.repl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/Plugin/Touch_commands.repl b/docs/source/Plugin/Touch_commands.repl index 6e365fe735..845fa7130b 100644 --- a/docs/source/Plugin/Touch_commands.repl +++ b/docs/source/Plugin/Touch_commands.repl @@ -103,7 +103,7 @@ ```` The group number for the button. - ```` The mode of the button. + ```` The mode of the button. Available options: 0 = normal, -1 = initial, -2 = clear button area. "," Update the button according to the current state, on the display. To update a dynamic caption or external state/value change. " diff --git a/docs/source/Plugin/Touch_config_values.repl b/docs/source/Plugin/Touch_config_values.repl index 4b35d91752..af43910313 100644 --- a/docs/source/Plugin/Touch_config_values.repl +++ b/docs/source/Plugin/Touch_config_values.repl @@ -15,7 +15,7 @@ " | ``[#enabled,]`` "," - | Returns ``1`` if an object with that ``name`` or ``number`` exists, and ``0`` if not. + | Returns ``1`` if an object with that ``name`` or ``number`` is enabled, and ``0`` if not. " " | ``[#state,]`` From 5f373575bcce532a4154c8ff00e509b0097977e1 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 1 Apr 2024 15:10:47 +0200 Subject: [PATCH 083/113] [Storage] Use correct parameter-type for strformat() --- src/src/Helpers/ESPEasy_Storage.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/src/Helpers/ESPEasy_Storage.cpp b/src/src/Helpers/ESPEasy_Storage.cpp index 3720f1f189..a42422b3b5 100644 --- a/src/src/Helpers/ESPEasy_Storage.cpp +++ b/src/src/Helpers/ESPEasy_Storage.cpp @@ -2129,15 +2129,15 @@ String getPartitionTable(uint8_t pType, const String& itemSep, const String& lin const esp_partition_t *_mypart = esp_partition_get(_mypartiterator); result += strformat(F("%x%s%s%s%s%s%s%s%s%s"), _mypart->address, - itemSep, + itemSep.c_str(), formatToHex_decimal(_mypart->size, 1024), - itemSep, + itemSep.c_str(), _mypart->label, - itemSep, + itemSep.c_str(), getPartitionType(_mypart->type, _mypart->subtype).c_str(), - itemSep, + itemSep.c_str(), String(_mypart->encrypted ? F("Yes") : F("-")).c_str(), - lineEnd); + lineEnd.c_str()); } while ((_mypartiterator = esp_partition_next(_mypartiterator)) != nullptr); } esp_partition_iterator_release(_mypartiterator); From adefd7391da58085131134a41e6dfe025bc606ec Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 1 Apr 2024 15:10:58 +0200 Subject: [PATCH 084/113] [Storage] Use correct parameter-type for strformat() --- src/src/Helpers/ESPEasy_Storage.cpp | 599 +++++++++++++++++----------- 1 file changed, 361 insertions(+), 238 deletions(-) diff --git a/src/src/Helpers/ESPEasy_Storage.cpp b/src/src/Helpers/ESPEasy_Storage.cpp index a42422b3b5..5941a5f6b2 100644 --- a/src/src/Helpers/ESPEasy_Storage.cpp +++ b/src/src/Helpers/ESPEasy_Storage.cpp @@ -12,8 +12,8 @@ #include "../DataTypes/SPI_options.h" #if FEATURE_MQTT -#include "../ESPEasyCore/Controller.h" -#endif +# include "../ESPEasyCore/Controller.h" +#endif // if FEATURE_MQTT #include "../ESPEasyCore/ESPEasy_Log.h" #include "../ESPEasyCore/ESPEasyNetwork.h" #include "../ESPEasyCore/ESPEasyWifi.h" @@ -53,11 +53,11 @@ #if FEATURE_RTC_CACHE_STORAGE # include "../Globals/C016_ControllerCache.h" -#endif +#endif // if FEATURE_RTC_CACHE_STORAGE #ifdef ESP32 -#include -#endif +# include +#endif // ifdef ESP32 #ifdef ESP32 String patch_fname(const String& fname) { @@ -66,7 +66,8 @@ String patch_fname(const String& fname) { } return String('/') + fname; } -#endif + +#endif // ifdef ESP32 /********************************************************************************************\ file system error handling @@ -75,6 +76,7 @@ String patch_fname(const String& fname) { String FileError(int line, const char *fname) { String log = strformat(F("FS : Error while reading/writing %s in %d"), fname, line); + addLog(LOG_LEVEL_ERROR, log); return log; } @@ -95,7 +97,7 @@ String flashGuard() { #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("flashGuard")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER if (RTC.flashDayCounter > MAX_FLASHWRITES_PER_DAY) { @@ -125,7 +127,7 @@ String appendToFile(const String& fname, const uint8_t *data, unsigned int size) return EMPTY_STRING; } -bool fileExists(const __FlashStringHelper * fname) +bool fileExists(const __FlashStringHelper *fname) { return fileExists(String(fname)); } @@ -133,30 +135,35 @@ bool fileExists(const __FlashStringHelper * fname) bool fileExists(const String& fname) { #ifdef USE_SECOND_HEAP HeapSelectDram ephemeral; - #endif + #endif // ifdef USE_SECOND_HEAP const String patched_fname = patch_fname(fname); - auto search = Cache.fileExistsMap.find(patched_fname); + auto search = Cache.fileExistsMap.find(patched_fname); + if (search != Cache.fileExistsMap.end()) { return search->second; } bool res = ESPEASY_FS.exists(patched_fname); #if FEATURE_SD + if (!res) { res = SD.exists(patched_fname); } - #endif + #endif // if FEATURE_SD + // Only keep track of existing files or non-existing filenames that may be requested several times. // Not the non-existing files from the cache controller #if FEATURE_RTC_CACHE_STORAGE - if (res || !isCacheFile(patched_fname)) - #endif + + if (res || !isCacheFile(patched_fname)) + #endif // if FEATURE_RTC_CACHE_STORAGE { Cache.fileExistsMap.emplace( std::make_pair( - patched_fname, + patched_fname, res)); } + if (Cache.fileCacheClearMoment == 0) { if (node_time.timeSource == timeSource_t::No_time_source) { // use some random value as we don't have a time yet @@ -171,6 +178,7 @@ bool fileExists(const String& fname) { fs::File tryOpenFile(const String& fname, const String& mode, FileDestination_e destination) { START_TIMER; fs::File f; + if (fname.isEmpty() || equals(fname, '/')) { return f; } @@ -183,15 +191,16 @@ fs::File tryOpenFile(const String& fname, const String& mode, FileDestination_e } clearFileCaches(); } + if ((destination == FileDestination_e::ANY) || (destination == FileDestination_e::FLASH)) { f = ESPEASY_FS.open(patch_fname(fname), mode.c_str()); } - # if FEATURE_SD + #if FEATURE_SD if (!f && ((destination == FileDestination_e::ANY) || (destination == FileDestination_e::SD))) { f = SD.open(patch_fname(fname).c_str(), mode.c_str()); } - # endif // if FEATURE_SD + #endif // if FEATURE_SD STOP_TIMER(TRY_OPEN_FILE); @@ -200,11 +209,13 @@ fs::File tryOpenFile(const String& fname, const String& mode, FileDestination_e bool fileMatchesTaskSettingsType(const String& fname) { const String config_dat_file = patch_fname(getFileName(FileType::CONFIG_DAT)); + return config_dat_file.equalsIgnoreCase(patch_fname(fname)); } bool tryRenameFile(const String& fname_old, const String& fname_new, FileDestination_e destination) { clearFileCaches(); + if (fileExists(fname_old) && !fileExists(fname_new)) { if (fileMatchesTaskSettingsType(fname_old)) { clearAllCaches(); @@ -212,10 +223,12 @@ bool tryRenameFile(const String& fname_old, const String& fname_new, FileDestina clearAllButTaskCaches(); } bool res = false; + if ((destination == FileDestination_e::ANY) || (destination == FileDestination_e::FLASH)) { res = ESPEASY_FS.rename(patch_fname(fname_old), patch_fname(fname_new)); } #if FEATURE_SD && defined(ESP32) // FIXME ESP8266 SDClass doesn't support rename + if (!res && ((destination == FileDestination_e::ANY) || (destination == FileDestination_e::SD))) { res = SD.rename(patch_fname(fname_old), patch_fname(fname_new)); } @@ -229,24 +242,28 @@ bool tryDeleteFile(const String& fname, FileDestination_e destination) { if (fname.length() > 0) { #if FEATURE_RTC_CACHE_STORAGE + if (isCacheFile(fname)) { ControllerCache.closeOpenFiles(); } - #endif + #endif // if FEATURE_RTC_CACHE_STORAGE + if (fileMatchesTaskSettingsType(fname)) { clearAllCaches(); } else { clearAllButTaskCaches(); } bool res = false; + if ((destination == FileDestination_e::ANY) || (destination == FileDestination_e::FLASH)) { res = ESPEASY_FS.remove(patch_fname(fname)); } #if FEATURE_SD + if (!res && ((destination == FileDestination_e::ANY) || (destination == FileDestination_e::SD))) { res = SD.remove(patch_fname(fname)); } - #endif + #endif // if FEATURE_SD // A call to GarbageCollection() will at most erase a single block. (e.g. 8k block size) // A deleted file may have covered more than a single block, so try to clear multiple blocks. @@ -271,7 +288,7 @@ bool BuildFixes() } #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("BuildFixes")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER serialPrintln(F("\nBuild changed!")); if (Settings.Build < 145) @@ -283,7 +300,7 @@ bool BuildFixes() { #ifdef LIMIT_BUILD_SIZE serialPrintln(F("Fix reset Pin")); - #endif + #endif // ifdef LIMIT_BUILD_SIZE Settings.Pin_Reset = -1; } @@ -292,10 +309,10 @@ bool BuildFixes() // Have to patch settings to make sure no bogus data is being used. #ifdef LIMIT_BUILD_SIZE serialPrintln(F("Fix settings with uninitalized data or corrupted by switching between versions")); - #endif - Settings.UseRTOSMultitasking = false; - Settings.Pin_Reset = -1; - Settings.SyslogFacility = DEFAULT_SYSLOG_FACILITY; + #endif // ifdef LIMIT_BUILD_SIZE + Settings.UseRTOSMultitasking = false; + Settings.Pin_Reset = -1; + Settings.SyslogFacility = DEFAULT_SYSLOG_FACILITY; Settings.MQTTUseUnitNameAsClientId_unused = DEFAULT_MQTT_USE_UNITNAME_AS_CLIENTID; } @@ -303,27 +320,33 @@ bool BuildFixes() Settings.ResetFactoryDefaultPreference = 0; Settings.OldRulesEngine(DEFAULT_RULES_OLDENGINE); } + if (Settings.Build < 20105) { Settings.I2C_clockSpeed = DEFAULT_I2C_CLOCK_SPEED; } + if (Settings.Build <= 20106) { // ClientID is now defined in the controller settings. #if FEATURE_MQTT controllerIndex_t controller_idx = firstEnabledMQTT_ControllerIndex(); + if (validControllerIndex(controller_idx)) { - MakeControllerSettings(ControllerSettings); //-V522 + MakeControllerSettings(ControllerSettings); // -V522 + if (AllocatedControllerSettings()) { LoadControllerSettings(controller_idx, *ControllerSettings); String clientid; + if (Settings.MQTTUseUnitNameAsClientId_unused) { clientid = F("%sysname%"); + if (Settings.appendUnitToHostname()) { clientid += F("_%unit%"); } } else { - clientid = F("ESPClient_%mac%"); + clientid = F("ESPClient_%mac%"); } safe_strncpy(ControllerSettings->ClientID, clientid, sizeof(ControllerSettings->ClientID)); @@ -334,50 +357,62 @@ bool BuildFixes() } #endif // if FEATURE_MQTT } + if (Settings.Build < 20107) { Settings.WebserverPort = 80; } + if (Settings.Build < 20108) { #ifdef ESP32 - // Ethernet related settings are never used on ESP8266 + + // Ethernet related settings are never used on ESP8266 Settings.ETH_Phy_Addr = DEFAULT_ETH_PHY_ADDR; Settings.ETH_Pin_mdc_cs = DEFAULT_ETH_PIN_MDC; Settings.ETH_Pin_mdio_irq = DEFAULT_ETH_PIN_MDIO; Settings.ETH_Pin_power_rst = DEFAULT_ETH_PIN_POWER; Settings.ETH_Phy_Type = DEFAULT_ETH_PHY_TYPE; Settings.ETH_Clock_Mode = DEFAULT_ETH_CLOCK_MODE; -#endif - Settings.NetworkMedium = DEFAULT_NETWORK_MEDIUM; +#endif // ifdef ESP32 + Settings.NetworkMedium = DEFAULT_NETWORK_MEDIUM; } + if (Settings.Build < 20109) { Settings.SyslogPort = 514; } + if (Settings.Build < 20110) { - Settings.I2C_clockSpeed_Slow = DEFAULT_I2C_CLOCK_SPEED_SLOW; + Settings.I2C_clockSpeed_Slow = DEFAULT_I2C_CLOCK_SPEED_SLOW; Settings.I2C_Multiplexer_Type = I2C_MULTIPLEXER_NONE; Settings.I2C_Multiplexer_Addr = -1; + for (taskIndex_t x = 0; x < TASKS_MAX; x++) { Settings.I2C_Multiplexer_Channel[x] = -1; } Settings.I2C_Multiplexer_ResetPin = -1; } + if (Settings.Build < 20111) { #ifdef ESP32 constexpr uint8_t maxStatesesp32 = NR_ELEMENTS(Settings.PinBootStates_ESP32); + for (uint8_t i = 0; i < maxStatesesp32; ++i) { Settings.PinBootStates_ESP32[i] = 0; } - #endif + #endif // ifdef ESP32 } + if (Settings.Build < 20112) { - Settings.WiFi_TX_power = 70; // 70 = 17.5dBm. unit: 0.25 dBm - Settings.WiFi_sensitivity_margin = 3; // Margin in dBm on top of sensitivity. + Settings.WiFi_TX_power = 70; // 70 = 17.5dBm. unit: 0.25 dBm + Settings.WiFi_sensitivity_margin = 3; // Margin in dBm on top of sensitivity. } + if (Settings.Build < 20113) { Settings.NumberExtraWiFiScans = 0; } + if (Settings.Build < 20114) { #ifdef USES_P003 + // P003_Pulse was always using the pull-up, now it is a setting. constexpr pluginID_t PLUGIN_ID_P003_PULSE(3); @@ -386,8 +421,9 @@ bool BuildFixes() Settings.TaskDevicePin1PullUp[taskIndex] = true; } } - #endif + #endif // ifdef USES_P003 } + if (Settings.Build < 20115) { if (Settings.InitSPI != static_cast(SPI_Options_e::UserDefined)) { // User-defined SPI pins set to None Settings.SPI_SCLK_pin = -1; @@ -396,6 +432,7 @@ bool BuildFixes() } } #ifdef USES_P053 + if (Settings.Build < 20116) { // Added PWR button, init to "-none-" constexpr pluginID_t PLUGIN_ID_P053_PMSx003(53); @@ -405,15 +442,16 @@ bool BuildFixes() Settings.TaskDevicePluginConfig[taskIndex][3] = -1; } } + // Remove PeriodicalScanWiFi // Reset to default 0 for future use. Settings.VariousBits_1.unused_15 = 0; } - #endif + #endif // ifdef USES_P053 // Starting 2022/08/18 // Use get_build_nr() value for settings transitions. - // This value will also be shown when building using PlatformIO, when showing the Compile time defines + // This value will also be shown when building using PlatformIO, when showing the Compile time defines Settings.Build = get_build_nr(); Settings.StructSize = sizeof(Settings); @@ -431,26 +469,29 @@ void fileSystemCheck() { #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("fileSystemCheck")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER addLog(LOG_LEVEL_INFO, F("FS : Mounting...")); #if defined(ESP32) && defined(USE_LITTLEFS) - if (getPartionCount(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS) != 0 + + if ((getPartionCount(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS) != 0) && ESPEASY_FS.begin()) -#else +#else // if defined(ESP32) && defined(USE_LITTLEFS) + if (ESPEASY_FS.begin()) -#endif +#endif // if defined(ESP32) && defined(USE_LITTLEFS) { clearAllCaches(); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, strformat( - F("FS : " + F("FS : " #ifdef USE_LITTLEFS - "LittleFS" -#else - "SPIFFS" -#endif - " mount successful, used %u bytes of %u"), - SpiffsUsedBytes(), SpiffsTotalBytes())); + "LittleFS" +#else // ifdef USE_LITTLEFS + "SPIFFS" +#endif // ifdef USE_LITTLEFS + " mount successful, used %u bytes of %u"), + SpiffsUsedBytes(), SpiffsTotalBytes())); } // Run garbage collection before any file is open. @@ -461,15 +502,16 @@ void fileSystemCheck() } fs::File f = tryOpenFile(SettingsType::getSettingsFileName(SettingsType::Enum::BasicSettings_Type), "r"); - if (f) { - f.close(); + + if (f) { + f.close(); } else { ResetFactory(false); } } else { - const __FlashStringHelper * log = F("FS : Mount failed"); + const __FlashStringHelper *log = F("FS : Mount failed"); serialPrintln(log); addLog(LOG_LEVEL_ERROR, log); ResetFactory(); @@ -488,6 +530,7 @@ bool FS_format() { // #endif // #else return ESPEASY_FS.format(); + // #endif } @@ -497,7 +540,7 @@ bool FS_format() { int getPartionCount(uint8_t pType, uint8_t pSubType) { esp_partition_type_t partitionType = static_cast(pType); - esp_partition_subtype_t subtype = static_cast(pSubType); + esp_partition_subtype_t subtype = static_cast(pSubType); esp_partition_iterator_t _mypartiterator = esp_partition_find(partitionType, subtype, NULL); int nrPartitions = 0; @@ -510,19 +553,18 @@ int getPartionCount(uint8_t pType, uint8_t pSubType) { return nrPartitions; } - -#endif +#endif // ifdef ESP32 #ifdef ESP8266 bool clearPartition(ESP8266_partition_type ptype) { uint32_t address; - int32_t size; - int32_t sector = getPartitionInfo(ESP8266_partition_type::rf_cal, address, size); + int32_t size; + int32_t sector = getPartitionInfo(ESP8266_partition_type::rf_cal, address, size); + while (size > 0) { - if (!ESP.flashEraseSector(sector)) return false; + if (!ESP.flashEraseSector(sector)) { return false; } ++sector; size -= SPI_FLASH_SEC_SIZE; - } return true; } @@ -535,7 +577,7 @@ bool clearWiFiSDKpartition() { return clearPartition(ESP8266_partition_type::wifi); } -#endif +#endif // ifdef ESP8266 /********************************************************************************************\ @@ -548,9 +590,9 @@ bool GarbageCollection() { START_TIMER; if (ESPEASY_FS.gc()) { -#ifndef BUILD_NO_DEBUG +# ifndef BUILD_NO_DEBUG addLog(LOG_LEVEL_INFO, F("FS : Success garbage collection")); -#endif +# endif // ifndef BUILD_NO_DEBUG STOP_TIMER(FS_GC_SUCCESS); return true; } @@ -570,8 +612,8 @@ String SaveSettings(bool forFactoryReset) { #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("SaveSettings")); - #endif - String err; + #endif // ifndef BUILD_NO_RAM_TRACKER + String err; { Settings.StructSize = sizeof(Settings); @@ -585,21 +627,25 @@ String SaveSettings(bool forFactoryReset) } if (!COMPUTE_STRUCT_CHECKSUM_UPDATE(SettingsStruct, Settings) - /* - computeChecksum( - Settings.md5, - reinterpret_cast(&Settings), - sizeof(SettingsStruct), - offsetof(SettingsStruct, md5)) - */ + + /* + computeChecksum( + Settings.md5, + reinterpret_cast(&Settings), + sizeof(SettingsStruct), + offsetof(SettingsStruct, md5)) + */ ) { - err = SaveToFile(SettingsType::getSettingsFileName(SettingsType::Enum::BasicSettings_Type).c_str(), 0, reinterpret_cast(&Settings), sizeof(Settings)); - } -#ifndef BUILD_NO_DEBUG + err = SaveToFile(SettingsType::getSettingsFileName(SettingsType::Enum::BasicSettings_Type).c_str(), + 0, + reinterpret_cast(&Settings), + sizeof(Settings)); + } +#ifndef BUILD_NO_DEBUG else { addLog(LOG_LEVEL_INFO, F("Skip saving settings, not changed")); } -#endif +#endif // ifndef BUILD_NO_DEBUG } if (err.length()) { @@ -607,10 +653,11 @@ String SaveSettings(bool forFactoryReset) } #ifndef BUILD_MINIMAL_OTA + // Must check this after saving, or else it is not possible to fix multiple // issues which can only corrected on different pages. if (!SettingsCheck(err)) { return err; } -#endif +#endif // ifndef BUILD_MINIMAL_OTA // } @@ -620,7 +667,7 @@ String SaveSettings(bool forFactoryReset) } String SaveSecuritySettings(bool forFactoryReset) { - String err; + String err; SecuritySettings.validate(); memcpy(SecuritySettings.ProgmemMd5, CRCValues.runTimeMD5, 16); @@ -631,23 +678,26 @@ String SaveSecuritySettings(bool forFactoryReset) { if (SecuritySettings.updateChecksum()) { // Settings have changed, save to file. - err = SaveToFile(SettingsType::getSettingsFileName(SettingsType::Enum::SecuritySettings_Type).c_str(), 0, reinterpret_cast(&SecuritySettings), sizeof(SecuritySettings)); + err = SaveToFile(SettingsType::getSettingsFileName(SettingsType::Enum::SecuritySettings_Type).c_str(), + 0, + reinterpret_cast(&SecuritySettings), + sizeof(SecuritySettings)); // Security settings are saved, may be update of WiFi settings or hostname. if (!forFactoryReset && !NetworkConnected()) { - if (SecuritySettings.hasWiFiCredentials() && active_network_medium == NetworkMedium_t::WIFI) { + if (SecuritySettings.hasWiFiCredentials() && (active_network_medium == NetworkMedium_t::WIFI)) { WiFiEventData.wifiConnectAttemptNeeded = true; WiFi_AP_Candidates.force_reload(); // Force reload of the credentials and found APs from the last scan resetWiFi(); AttemptWiFiConnect(); } } - } + } #ifndef BUILD_NO_DEBUG else { addLog(LOG_LEVEL_INFO, F("Skip saving SecuritySettings, not changed")); } -#endif +#endif // ifndef BUILD_NO_DEBUG // FIXME TD-er: How to check if these have changed? if (forFactoryReset) { @@ -655,13 +705,16 @@ String SaveSecuritySettings(bool forFactoryReset) { } ExtendedControllerCredentials.save(); - if (!forFactoryReset) + + if (!forFactoryReset) { afterloadSettings(); + } return err; } void afterloadSettings() { ExtraTaskSettings.clear(); // make sure these will not contain old settings. + if ((Settings.Version != VERSION) || (Settings.PID != ESP_PROJECT_PID)) { // Not valid settings, so do not continue return; @@ -670,7 +723,9 @@ void afterloadSettings() { // Load ResetFactoryDefaultPreference from provisioning.dat if available. // FIXME TD-er: Must actually move content of Provisioning.dat to NVS and then delete file uint32_t pref_temp = Settings.ResetFactoryDefaultPreference; + #ifdef ESP32 + if (pref_temp == 0) { if (ResetFactoryDefaultPreference.getPreference() == 0) { // Try loading from NVS @@ -679,25 +734,30 @@ void afterloadSettings() { pref_temp = ResetFactoryDefaultPreference.getPreference(); } } - #endif + #endif // ifdef ESP32 #if FEATURE_CUSTOM_PROVISIONING + if (fileExists(getFileName(FileType::PROVISIONING_DAT))) { MakeProvisioningSettings(ProvisioningSettings); + if (ProvisioningSettings.get()) { loadProvisioningSettings(*ProvisioningSettings); + if (ProvisioningSettings->matchingFlashSize()) { - if (pref_temp == 0 && ProvisioningSettings->ResetFactoryDefaultPreference.getPreference() != 0) + if ((pref_temp == 0) && (ProvisioningSettings->ResetFactoryDefaultPreference.getPreference() != 0)) { pref_temp = ProvisioningSettings->ResetFactoryDefaultPreference.getPreference(); + } } } } - #endif + #endif // if FEATURE_CUSTOM_PROVISIONING // TODO TD-er: Try to get the information from more locations to make it more persistent // Maybe EEPROM location? ResetFactoryDefaultPreference_struct pref(pref_temp); + if (modelMatchingFlashSize(pref.getDeviceModel())) { ResetFactoryDefaultPreference = pref_temp; } @@ -705,7 +765,7 @@ void afterloadSettings() { Scheduler.setEcoMode(Settings.EcoPowerMode()); #ifdef ESP32 setCpuFrequencyMhz(Settings.EcoPowerMode() ? getCPU_MinFreqMHz() : getCPU_MaxFreqMHz()); - #endif + #endif // ifdef ESP32 if (!Settings.UseRules) { eventQueue.clear(); @@ -721,15 +781,19 @@ String LoadSettings() clearAllButTaskCaches(); #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("LoadSettings")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER uint8_t oldSettingsChecksum[16] = { 0 }; memcpy(oldSettingsChecksum, Settings.md5, 16); - String err; + String err; - err = LoadFromFile(SettingsType::getSettingsFileName(SettingsType::Enum::BasicSettings_Type).c_str(), 0, reinterpret_cast(&Settings), sizeof(SettingsStruct)); + err = + LoadFromFile(SettingsType::getSettingsFileName(SettingsType::Enum::BasicSettings_Type).c_str(), + 0, + reinterpret_cast(&Settings), + sizeof(SettingsStruct)); if (memcmp(oldSettingsChecksum, Settings.md5, 16) != 0) { // File has changed, so need to flush all task caches. @@ -741,22 +805,27 @@ String LoadSettings() } if (!BuildFixes()) { - #ifndef BUILD_NO_DEBUG + if (COMPUTE_STRUCT_CHECKSUM(SettingsStruct, Settings)) { - addLog(LOG_LEVEL_INFO, concat(F("CRC : Settings CRC"), F("...OK"))); - } else{ + addLog(LOG_LEVEL_INFO, concat(F("CRC : Settings CRC"), F("...OK"))); + } else { addLog(LOG_LEVEL_ERROR, concat(F("CRC : Settings CRC"), F("...FAIL"))); } - #endif + #endif // ifndef BUILD_NO_DEBUG } Settings.validate(); initSerial(); - err = LoadFromFile(SettingsType::getSettingsFileName(SettingsType::Enum::SecuritySettings_Type).c_str(), 0, reinterpret_cast(&SecuritySettings), sizeof(SecurityStruct)); + err = + LoadFromFile(SettingsType::getSettingsFileName(SettingsType::Enum::SecuritySettings_Type).c_str(), + 0, + reinterpret_cast(&SecuritySettings), + sizeof(SecurityStruct)); #ifndef BUILD_NO_DEBUG + if (SecuritySettings.checksumMatch()) { addLog(LOG_LEVEL_INFO, concat(F("CRC : SecuritySettings CRC"), F("...OK "))); @@ -767,7 +836,7 @@ String LoadSettings() else { addLog(LOG_LEVEL_ERROR, concat(F("CRC : SecuritySettings CRC"), F("...FAIL"))); } -#endif +#endif // ifndef BUILD_NO_DEBUG ExtendedControllerCredentials.load(); @@ -778,8 +847,6 @@ String LoadSettings() return err; } - - /********************************************************************************************\ Disable Plugin, based on bootFailedCount \*********************************************************************************************/ @@ -801,16 +868,16 @@ uint8_t disablePlugin(uint8_t bootFailedCount) { uint8_t disableAllPlugins(uint8_t bootFailedCount) { if (bootFailedCount > 0) { --bootFailedCount; + for (taskIndex_t i = 0; i < TASKS_MAX; ++i) { - // Disable temporarily as unit crashed - // FIXME TD-er: Should this be stored? - Settings.TaskDeviceEnabled[i] = false; + // Disable temporarily as unit crashed + // FIXME TD-er: Should this be stored? + Settings.TaskDeviceEnabled[i] = false; } } return bootFailedCount; } - /********************************************************************************************\ Disable Controller, based on bootFailedCount \*********************************************************************************************/ @@ -830,6 +897,7 @@ uint8_t disableController(uint8_t bootFailedCount) { uint8_t disableAllControllers(uint8_t bootFailedCount) { if (bootFailedCount > 0) { --bootFailedCount; + for (controllerIndex_t i = 0; i < CONTROLLER_MAX; ++i) { Settings.ControllerEnabled[i] = false; } @@ -837,7 +905,6 @@ uint8_t disableAllControllers(uint8_t bootFailedCount) { return bootFailedCount; } - /********************************************************************************************\ Disable Notification, based on bootFailedCount \*********************************************************************************************/ @@ -858,13 +925,15 @@ uint8_t disableNotification(uint8_t bootFailedCount) { uint8_t disableAllNotifications(uint8_t bootFailedCount) { if (bootFailedCount > 0) { --bootFailedCount; + for (uint8_t i = 0; i < NOTIFICATION_MAX; ++i) { - Settings.NotificationEnabled[i] = false; + Settings.NotificationEnabled[i] = false; } } return bootFailedCount; } -#endif + +#endif // if FEATURE_NOTIFIER /********************************************************************************************\ Disable Rules, based on bootFailedCount @@ -877,7 +946,6 @@ uint8_t disableRules(uint8_t bootFailedCount) { return bootFailedCount; } - bool getAndLogSettingsParameters(bool read, SettingsType::Enum settingsType, int index, int& offset, int& max_size) { #ifndef BUILD_NO_DEBUG @@ -891,21 +959,26 @@ bool getAndLogSettingsParameters(bool read, SettingsType::Enum settingsType, int return SettingsType::getSettingsParameters(settingsType, index, offset, max_size); } - /********************************************************************************************\ Load array of Strings from Custom settings Use maxStringLength = 0 to optimize for size (strings will be concatenated) \*********************************************************************************************/ -String LoadStringArray(SettingsType::Enum settingsType, int index, String strings[], uint16_t nrStrings, uint16_t maxStringLength, uint32_t offset_in_block) +String LoadStringArray(SettingsType::Enum settingsType, + int index, + String strings[], + uint16_t nrStrings, + uint16_t maxStringLength, + uint32_t offset_in_block) { int offset, max_size; + if (!SettingsType::getSettingsParameters(settingsType, index, offset, max_size)) { #ifndef BUILD_NO_DEBUG return F("Invalid index for custom settings"); - #else + #else // ifndef BUILD_NO_DEBUG return F("Save error"); - #endif + #endif // ifndef BUILD_NO_DEBUG } const uint32_t bufferSize = 128; @@ -913,7 +986,7 @@ String LoadStringArray(SettingsType::Enum settingsType, int index, String string // FIXME TD-er: For now stack allocated, may need to be heap allocated? if (maxStringLength >= bufferSize) { return F("Max 128 chars allowed"); } - char buffer[bufferSize] = {0}; + char buffer[bufferSize] = { 0 }; String result; uint32_t readPos = offset_in_block; @@ -921,16 +994,16 @@ String LoadStringArray(SettingsType::Enum settingsType, int index, String string uint32_t stringCount = 0; const uint16_t estimatedStringSize = maxStringLength > 0 ? maxStringLength : bufferSize; - String tmpString; + String tmpString; tmpString.reserve(estimatedStringSize); { while (stringCount < nrStrings && static_cast(readPos) < max_size) { const uint32_t readSize = std::min(bufferSize, max_size - readPos); result += LoadFromFile(settingsType, - index, - reinterpret_cast(&buffer), - readSize, - readPos); + index, + reinterpret_cast(&buffer), + readSize, + readPos); for (uint32_t i = 0; i < readSize && stringCount < nrStrings; ++i) { const uint32_t curPos = readPos + i; @@ -967,25 +1040,32 @@ String LoadStringArray(SettingsType::Enum settingsType, int index, String string Save array of Strings from Custom settings Use maxStringLength = 0 to optimize for size (strings will be concatenated) \*********************************************************************************************/ -String SaveStringArray(SettingsType::Enum settingsType, int index, const String strings[], uint16_t nrStrings, uint16_t maxStringLength, uint32_t posInBlock) +String SaveStringArray(SettingsType::Enum settingsType, + int index, + const String strings[], + uint16_t nrStrings, + uint16_t maxStringLength, + uint32_t posInBlock) { // FIXME TD-er: Must add some check to see if the existing data has changed before saving. int offset, max_size; + if (!SettingsType::getSettingsParameters(settingsType, index, offset, max_size)) { #ifndef BUILD_NO_DEBUG return F("Invalid index for custom settings"); - #else + #else // ifndef BUILD_NO_DEBUG return F("Save error"); - #endif + #endif // ifndef BUILD_NO_DEBUG } #ifdef ESP8266 uint16_t bufferSize = 256; - #endif + #endif // ifdef ESP8266 #ifdef ESP32 uint16_t bufferSize = 1024; - #endif + #endif // ifdef ESP32 + if (bufferSize > max_size) { bufferSize = max_size; } @@ -1015,7 +1095,8 @@ String SaveStringArray(SettingsType::Enum settingsType, int index, const String } int bufpos = 0; - for ( ; bufpos < bufferSize && stringCount < nrStrings; ++bufpos) { + + for (; bufpos < bufferSize && stringCount < nrStrings; ++bufpos) { if (stringReadPos == 0) { // We're at the start of a string curStringLength = strings[stringCount].length(); @@ -1057,6 +1138,7 @@ String SaveStringArray(SettingsType::Enum settingsType, int index, const String } #if FEATURE_EXTENDED_CUSTOM_SETTINGS + if ((SettingsType::Enum::CustomTaskSettings_Type == settingsType) && ((writePos - posInBlock) <= DAT_TASKS_CUSTOM_SIZE)) { // Not needed, so can be deleted DeleteExtendedCustomTaskSettingsFile(settingsType, index); @@ -1069,8 +1151,6 @@ String SaveStringArray(SettingsType::Enum settingsType, int index, const String return result; } - - /********************************************************************************************\ Save Task settings to file system \*********************************************************************************************/ @@ -1078,14 +1158,14 @@ String SaveTaskSettings(taskIndex_t TaskIndex) { #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("SaveTaskSettings")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER if (ExtraTaskSettings.TaskIndex != TaskIndex) { #ifndef BUILD_NO_DEBUG return F("SaveTaskSettings taskIndex does not match"); - #else + #else // ifndef BUILD_NO_DEBUG return F("Save error"); - #endif + #endif // ifndef BUILD_NO_DEBUG } START_TIMER @@ -1094,32 +1174,34 @@ String SaveTaskSettings(taskIndex_t TaskIndex) if (!Cache.matchChecksumExtraTaskSettings(TaskIndex, ExtraTaskSettings.computeChecksum())) { // Clear task device value names before saving, will generate again when loading them later. ExtraTaskSettings.clearDefaultTaskDeviceValueNames(); - ExtraTaskSettings.validate(); // Validate before saving will reduce nr of saves as it is more likely to not have changed the next time it will be saved. + ExtraTaskSettings.validate(); // Validate before saving will reduce nr of saves as it is more likely to not have changed the next time + // it will be saved. // Call to validate() may have changed the content, so re-compute the checksum. - // This is how it is now stored, so we can now also update the + // This is how it is now stored, so we can now also update the // ExtraTaskSettings cache. This may prevent a reload. Cache.updateExtraTaskSettingsCache_afterLoad_Save(); err = SaveToFile(SettingsType::Enum::TaskSettings_Type, - TaskIndex, - reinterpret_cast(&ExtraTaskSettings), - sizeof(struct ExtraTaskSettingsStruct)); + TaskIndex, + reinterpret_cast(&ExtraTaskSettings), + sizeof(struct ExtraTaskSettingsStruct)); #if !defined(PLUGIN_BUILD_MINIMAL_OTA) && !defined(ESP8266_1M) + if (err.isEmpty()) { err = checkTaskSettings(TaskIndex); } -#endif +#endif // if !defined(PLUGIN_BUILD_MINIMAL_OTA) && !defined(ESP8266_1M) + // FIXME TD-er: Is this still needed as it is also cleared on PLUGIN_INIT and PLUGIN_EXIT? UserVar.clear_computed(ExtraTaskSettings.TaskIndex); - } + } #ifndef LIMIT_BUILD_SIZE else { addLog(LOG_LEVEL_INFO, F("Skip saving task settings, not changed")); - } -#endif +#endif // ifndef LIMIT_BUILD_SIZE STOP_TIMER(SAVE_TASK_SETTINGS); return err; } @@ -1132,6 +1214,7 @@ String LoadTaskSettings(taskIndex_t TaskIndex) if (ExtraTaskSettings.TaskIndex == TaskIndex) { return EMPTY_STRING; // already loaded } + if (!validTaskIndex(TaskIndex)) { return EMPTY_STRING; // Un-initialized task index. } @@ -1139,24 +1222,26 @@ String LoadTaskSettings(taskIndex_t TaskIndex) ExtraTaskSettings.clear(); const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(TaskIndex); + if (!validDeviceIndex(DeviceIndex)) { // No need to load from storage, as there is no plugin assigned to this task. ExtraTaskSettings.TaskIndex = TaskIndex; // Needed when an empty task was requested // FIXME TD-er: Do we need to keep a cache of an empty task? - // Maybe better to do this? - Cache.clearTaskCache(TaskIndex); -// Cache.updateExtraTaskSettingsCache_afterLoad_Save(); + // Maybe better to do this? + Cache.clearTaskCache(TaskIndex); + + // Cache.updateExtraTaskSettingsCache_afterLoad_Save(); return EMPTY_STRING; } #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("LoadTaskSettings")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER const String result = LoadFromFile( - SettingsType::Enum::TaskSettings_Type, - TaskIndex, - reinterpret_cast(&ExtraTaskSettings), + SettingsType::Enum::TaskSettings_Type, + TaskIndex, + reinterpret_cast(&ExtraTaskSettings), sizeof(struct ExtraTaskSettingsStruct)); // After loading, some settings may need patching. @@ -1166,10 +1251,10 @@ String LoadTaskSettings(taskIndex_t TaskIndex) // Nr of decimals cannot be configured, so set them to 0 just to be sure. for (uint8_t i = 0; i < VARS_PER_TASK; ++i) { ExtraTaskSettings.TaskDeviceValueDecimals[i] = 0; - } + } } loadDefaultTaskValueNames_ifEmpty(TaskIndex); - + ExtraTaskSettings.validate(); Cache.updateExtraTaskSettingsCache_afterLoad_Save(); STOP_TIMER(LOAD_TASK_SETTINGS); @@ -1183,7 +1268,7 @@ bool _CDN_url_loaded = false; String get_CDN_url_custom() { if (!_CDN_url_loaded) { - String strings[] = {EMPTY_STRING}; + String strings[] = { EMPTY_STRING }; LoadStringArray( SettingsType::Enum::CdnSettings_Type, 0, @@ -1194,9 +1279,10 @@ String get_CDN_url_custom() { return _CDN_url_cache; } -void set_CDN_url_custom(const String &url) { +void set_CDN_url_custom(const String& url) { _CDN_url_cache = url; _CDN_url_cache.trim(); + if (!_CDN_url_cache.isEmpty() && !_CDN_url_cache.endsWith(F("/"))) { _CDN_url_cache.concat('/'); } @@ -1219,6 +1305,7 @@ void set_CDN_url_custom(const String &url) { SettingsType::Enum::CdnSettings_Type, 0, strings, NR_ELEMENTS(strings), 255, 0); } + #endif // if FEATURE_ALTERNATIVE_CDN_URL /********************************************************************************************\ @@ -1228,7 +1315,7 @@ String SaveCustomTaskSettings(taskIndex_t TaskIndex, const uint8_t *memAddress, { #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("SaveCustomTaskSettings")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER return SaveToFile(SettingsType::Enum::CustomTaskSettings_Type, TaskIndex, memAddress, datasize, posInBlock); } @@ -1240,7 +1327,7 @@ String SaveCustomTaskSettings(taskIndex_t TaskIndex, String strings[], uint16_t { #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("SaveCustomTaskSettings")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER return SaveStringArray( SettingsType::Enum::CustomTaskSettings_Type, TaskIndex, strings, nrStrings, maxStringLength, posInBlock); @@ -1267,7 +1354,7 @@ String LoadCustomTaskSettings(taskIndex_t TaskIndex, uint8_t *memAddress, int da START_TIMER; #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("LoadCustomTaskSettings")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER String result = LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, TaskIndex, memAddress, datasize, offset_in_block); STOP_TIMER(LOAD_CUSTOM_TASK_STATS); return result; @@ -1282,10 +1369,10 @@ String LoadCustomTaskSettings(taskIndex_t TaskIndex, String strings[], uint16_t START_TIMER; #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("LoadCustomTaskSettings")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER String result = LoadStringArray(SettingsType::Enum::CustomTaskSettings_Type, - TaskIndex, - strings, nrStrings, maxStringLength, offset_in_block); + TaskIndex, + strings, nrStrings, maxStringLength, offset_in_block); STOP_TIMER(LOAD_CUSTOM_TASK_STATS); return result; } @@ -1297,7 +1384,7 @@ String SaveControllerSettings(controllerIndex_t ControllerIndex, ControllerSetti { #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("SaveControllerSettings")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER START_TIMER; @@ -1308,16 +1395,16 @@ String SaveControllerSettings(controllerIndex_t ControllerIndex, ControllerSetti if (checksum == (Cache.controllerSettings_checksums[ControllerIndex])) { #ifndef BUILD_NO_DEBUG addLog(LOG_LEVEL_INFO, concat(F("Skip saving ControllerSettings: "), checksum.toString())); -#endif +#endif // ifndef BUILD_NO_DEBUG return EMPTY_STRING; } const String res = SaveToFile(SettingsType::Enum::ControllerSettings_Type, ControllerIndex, - reinterpret_cast(&controller_settings), sizeof(controller_settings)); + reinterpret_cast(&controller_settings), sizeof(controller_settings)); Cache.controllerSettings_checksums[ControllerIndex] = checksum; #ifdef ESP32 Cache.setControllerSettings(ControllerIndex, controller_settings); - #endif + #endif // ifdef ESP32 STOP_TIMER(SAVE_CONTROLLER_SETTINGS); return res; @@ -1329,14 +1416,15 @@ String SaveControllerSettings(controllerIndex_t ControllerIndex, ControllerSetti String LoadControllerSettings(controllerIndex_t ControllerIndex, ControllerSettingsStruct& controller_settings) { #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("LoadControllerSettings")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER START_TIMER #ifdef ESP32 + if (Cache.getControllerSettings(ControllerIndex, controller_settings)) { STOP_TIMER(LOAD_CONTROLLER_SETTINGS_C); return EMPTY_STRING; } - #endif + #endif // ifdef ESP32 String result = LoadFromFile(SettingsType::Enum::ControllerSettings_Type, ControllerIndex, reinterpret_cast(&controller_settings), sizeof(controller_settings)); @@ -1346,7 +1434,7 @@ String LoadControllerSettings(controllerIndex_t ControllerIndex, ControllerSetti Cache.controllerSettings_checksums[ControllerIndex] = controller_settings.computeChecksum(); #ifdef ESP32 Cache.setControllerSettings(ControllerIndex, controller_settings); - #endif + #endif // ifdef ESP32 STOP_TIMER(LOAD_CONTROLLER_SETTINGS); return result; } @@ -1358,7 +1446,7 @@ String ClearCustomControllerSettings(controllerIndex_t ControllerIndex) { #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("ClearCustomControllerSettings")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER // addLog(LOG_LEVEL_DEBUG, F("Clearing custom controller settings")); return ClearInFile(SettingsType::Enum::CustomControllerSettings_Type, ControllerIndex); @@ -1371,7 +1459,7 @@ String SaveCustomControllerSettings(controllerIndex_t ControllerIndex, const uin { #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("SaveCustomControllerSettings")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER return SaveToFile(SettingsType::Enum::CustomControllerSettings_Type, ControllerIndex, memAddress, datasize); } @@ -1382,25 +1470,27 @@ String LoadCustomControllerSettings(controllerIndex_t ControllerIndex, uint8_t * { #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("LoadCustomControllerSettings")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER return LoadFromFile(SettingsType::Enum::CustomControllerSettings_Type, ControllerIndex, memAddress, datasize); } - #if FEATURE_CUSTOM_PROVISIONING + /********************************************************************************************\ Save Provisioning Settings \*********************************************************************************************/ String saveProvisioningSettings(ProvisioningStruct& ProvisioningSettings) { - String err; + String err; ProvisioningSettings.validate(); memcpy(ProvisioningSettings.ProgmemMd5, CRCValues.runTimeMD5, 16); + if (!COMPUTE_STRUCT_CHECKSUM_UPDATE(ProvisioningStruct, ProvisioningSettings)) { // Settings have changed, save to file. - err = SaveToFile_trunc(getFileName(FileType::PROVISIONING_DAT, 0).c_str(), 0, (uint8_t *)&ProvisioningSettings, sizeof(ProvisioningStruct)); + err = + SaveToFile_trunc(getFileName(FileType::PROVISIONING_DAT, 0).c_str(), 0, (uint8_t *)&ProvisioningSettings, sizeof(ProvisioningStruct)); } return err; } @@ -1410,8 +1500,13 @@ String saveProvisioningSettings(ProvisioningStruct& ProvisioningSettings) \*********************************************************************************************/ String loadProvisioningSettings(ProvisioningStruct& ProvisioningSettings) { - String err = LoadFromFile(getFileName(FileType::PROVISIONING_DAT, 0).c_str(), 0, (uint8_t *)&ProvisioningSettings, sizeof(ProvisioningStruct)); -#ifndef BUILD_NO_DEBUG + String err = LoadFromFile(getFileName(FileType::PROVISIONING_DAT, 0).c_str(), + 0, + (uint8_t *)&ProvisioningSettings, + sizeof(ProvisioningStruct)); + +# ifndef BUILD_NO_DEBUG + if (COMPUTE_STRUCT_CHECKSUM(ProvisioningStruct, ProvisioningSettings)) { addLog(LOG_LEVEL_INFO, F("CRC : ProvisioningSettings CRC ...OK ")); @@ -1423,22 +1518,23 @@ String loadProvisioningSettings(ProvisioningStruct& ProvisioningSettings) else { addLog(LOG_LEVEL_ERROR, F("CRC : ProvisioningSettings CRC ...FAIL")); } -#endif +# endif // ifndef BUILD_NO_DEBUG ProvisioningSettings.validate(); return err; } -#endif +#endif // if FEATURE_CUSTOM_PROVISIONING #if FEATURE_NOTIFIER + /********************************************************************************************\ Save Controller settings to file system \*********************************************************************************************/ String SaveNotificationSettings(int NotificationIndex, const uint8_t *memAddress, int datasize) { - #ifndef BUILD_NO_RAM_TRACKER + # ifndef BUILD_NO_RAM_TRACKER checkRAM(F("SaveNotificationSettings")); - #endif + # endif // ifndef BUILD_NO_RAM_TRACKER return SaveToFile(SettingsType::Enum::NotificationSettings_Type, NotificationIndex, memAddress, datasize); } @@ -1447,12 +1543,14 @@ String SaveNotificationSettings(int NotificationIndex, const uint8_t *memAddress \*********************************************************************************************/ String LoadNotificationSettings(int NotificationIndex, uint8_t *memAddress, int datasize) { - #ifndef BUILD_NO_RAM_TRACKER + # ifndef BUILD_NO_RAM_TRACKER checkRAM(F("LoadNotificationSettings")); - #endif + # endif // ifndef BUILD_NO_RAM_TRACKER return LoadFromFile(SettingsType::Enum::NotificationSettings_Type, NotificationIndex, memAddress, datasize); } -#endif + +#endif // if FEATURE_NOTIFIER + /********************************************************************************************\ Init a file with zeros on file system \*********************************************************************************************/ @@ -1460,7 +1558,7 @@ String InitFile(const String& fname, int datasize) { #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("InitFile")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER FLASH_GUARD(); fs::File f = tryOpenFile(fname, "w"); @@ -1486,7 +1584,7 @@ String InitFile(SettingsType::Enum settingsType) String InitFile(SettingsType::SettingsFileEnum file_type) { - return InitFile(SettingsType::getSettingsFileName(file_type), + return InitFile(SettingsType::getSettingsFileName(file_type), SettingsType::getInitFileSize(file_type)); } @@ -1507,36 +1605,37 @@ String SaveToFile_trunc(const char *fname, int index, const uint8_t *memAddress, String doSaveToFile(const char *fname, int index, const uint8_t *memAddress, int datasize, const char *mode) { #ifndef BUILD_NO_DEBUG -#ifndef ESP32 +# ifndef ESP32 if (allocatedOnStack(memAddress)) { addLog(LOG_LEVEL_ERROR, strformat(F("SaveToFile: %s ERROR, Data allocated on stack"), fname)); // return log; // FIXME TD-er: Should this be considered a breaking error? } -#endif // ifndef ESP32 -#endif +# endif // ifndef ESP32 +#endif // ifndef BUILD_NO_DEBUG if (index < 0) { #ifndef BUILD_NO_DEBUG const String log = strformat(F("SaveToFile: %s ERROR, invalid position in file"), fname); - #else + #else // ifndef BUILD_NO_DEBUG const String log = F("Save error"); - #endif + #endif // ifndef BUILD_NO_DEBUG addLog(LOG_LEVEL_ERROR, log); return log; } START_TIMER; #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("SaveToFile")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER FLASH_GUARD(); - + #ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLog(LOG_LEVEL_INFO, concat(F("SaveToFile: free stack: "), getCurrentFreeStack())); } - #endif + #endif // ifndef BUILD_NO_DEBUG delay(1); unsigned long timer = millis() + 50; fs::File f = tryOpenFile(fname, mode); @@ -1567,26 +1666,28 @@ String doSaveToFile(const char *fname, int index, const uint8_t *memAddress, int } f.close(); #ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, strformat(F("FILE : Saved %s offset: %d size: %d"), fname, index, datasize)); } - #endif + #endif // ifndef BUILD_NO_DEBUG } else { #ifndef BUILD_NO_DEBUG const String log = strformat(F("SaveToFile: %s ERROR, Cannot save to file"), fname); - #else + #else // ifndef BUILD_NO_DEBUG const String log = F("Save error"); - #endif + #endif // ifndef BUILD_NO_DEBUG addLog(LOG_LEVEL_ERROR, log); return log; } STOP_TIMER(SAVEFILE_STATS); #ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLogMove(LOG_LEVEL_INFO, concat(F("SaveToFile: free stack after: "), getCurrentFreeStack())); } - #endif + #endif // ifndef BUILD_NO_DEBUG // OK return EMPTY_STRING; @@ -1600,9 +1701,9 @@ String ClearInFile(const char *fname, int index, int datasize) if (index < 0) { #ifndef BUILD_NO_DEBUG const String log = strformat(F("ClearInFile: %s ERROR, invalid position in file"), fname); - #else + #else // ifndef BUILD_NO_DEBUG const String log = F("Save error"); - #endif + #endif // ifndef BUILD_NO_DEBUG addLog(LOG_LEVEL_ERROR, log); return log; @@ -1610,7 +1711,7 @@ String ClearInFile(const char *fname, int index, int datasize) #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("ClearInFile")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER FLASH_GUARD(); fs::File f = tryOpenFile(fname, "r+"); @@ -1628,9 +1729,9 @@ String ClearInFile(const char *fname, int index, int datasize) } else { #ifndef BUILD_NO_DEBUG const String log = strformat(F("ClearInFile: %s ERROR, Cannot save to file"), fname); - #else + #else // ifndef BUILD_NO_DEBUG const String log = F("Save error"); - #endif + #endif // ifndef BUILD_NO_DEBUG addLog(LOG_LEVEL_ERROR, log); return log; } @@ -1647,9 +1748,9 @@ String LoadFromFile(const char *fname, int offset, uint8_t *memAddress, int data if (offset < 0) { #ifndef BUILD_NO_DEBUG const String log = strformat(F("LoadFromFile: %s ERROR, invalid position in file"), fname); - #else + #else // ifndef BUILD_NO_DEBUG const String log = F("Load error"); - #endif + #endif // ifndef BUILD_NO_DEBUG addLog(LOG_LEVEL_ERROR, log); return log; } @@ -1657,14 +1758,15 @@ String LoadFromFile(const char *fname, int offset, uint8_t *memAddress, int data START_TIMER; #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("LoadFromFile")); - #endif - + #endif // ifndef BUILD_NO_RAM_TRACKER + fs::File f = tryOpenFile(fname, "r"); - SPIFFS_CHECK(f, fname); + SPIFFS_CHECK(f, fname); const int fileSize = f.size(); + if (fileSize > offset) { - SPIFFS_CHECK(f.seek(offset, fs::SeekSet), fname); - + SPIFFS_CHECK(f.seek(offset, fs::SeekSet), fname); + if (fileSize < (offset + datasize)) { const int newdatasize = datasize + offset - fileSize; @@ -1691,24 +1793,26 @@ String getSettingsFileIndexRangeError(bool read, SettingsType::Enum settingsType return concat(F("Unknown settingsType: "), static_cast(settingsType)); } String error = read ? F("Load") : F("Save"); + #ifndef BUILD_NO_DEBUG error += SettingsType::getSettingsTypeString(settingsType); error += concat(F(" index out of range: "), index); - #else + #else // ifndef BUILD_NO_DEBUG error += F(" error"); - #endif + #endif // ifndef BUILD_NO_DEBUG return error; } String getSettingsFileDatasizeError(bool read, SettingsType::Enum settingsType, int index, int datasize, int max_size) { String error = read ? F("Load") : F("Save"); + #ifndef BUILD_NO_DEBUG error += SettingsType::getSettingsTypeString(settingsType); error += strformat(F("(%d) datasize(%d) > max_size(%d)"), index, datasize, max_size); - #else + #else // ifndef BUILD_NO_DEBUG error += F(" error"); - #endif - + #endif // ifndef BUILD_NO_DEBUG + return error; } @@ -1757,7 +1861,7 @@ String LoadFromFile(SettingsType::Enum settingsType, int index, uint8_t *memAddr #if FEATURE_EXTENDED_CUSTOM_SETTINGS , taskIndex #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS - ); + ); return LoadFromFile(fname.c_str(), (offset + offset_in_block), memAddress + dataOffset, datasize); } @@ -1776,7 +1880,7 @@ String SaveToFile(SettingsType::Enum settingsType, int index, const uint8_t *mem int dataOffset = 0; #if FEATURE_EXTENDED_CUSTOM_SETTINGS - int taskIndex = INVALID_TASK_INDEX; // Use base filename + int taskIndex = INVALID_TASK_INDEX; // Use base filename if ((SettingsType::Enum::CustomTaskSettings_Type == settingsType) && (posInBlock + datasize > (DAT_TASKS_CUSTOM_SIZE))) { // max_size already handled above @@ -1785,8 +1889,9 @@ String SaveToFile(SettingsType::Enum settingsType, int index, const uint8_t *mem dataOffset = (DAT_TASKS_CUSTOM_SIZE - posInBlock); // Bytes to keep 'local' # ifndef BUILD_NO_DEBUG const String styp = SettingsType::getSettingsTypeString(settingsType); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLog(LOG_LEVEL_INFO, strformat(F("ExtraSaveToFile: %s file: %s size: %d pos: %d"), + addLog(LOG_LEVEL_INFO, strformat(F("ExtraSaveToFile: %s file: %s size: %d pos: %d"), styp.c_str(), fname.c_str(), dataOffset, posInBlock)); } # endif // ifndef BUILD_NO_DEBUG @@ -1808,24 +1913,27 @@ String SaveToFile(SettingsType::Enum settingsType, int index, const uint8_t *mem #if FEATURE_EXTENDED_CUSTOM_SETTINGS , taskIndex #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS - ); + ); + if (!fileExists(fname)) { #if FEATURE_EXTENDED_CUSTOM_SETTINGS + if (!validTaskIndex(taskIndex)) { #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS - InitFile(settingsType); + InitFile(settingsType); #if FEATURE_EXTENDED_CUSTOM_SETTINGS - } else { - InitFile(fname, DAT_TASKS_CUSTOM_EXTENSION_SIZE); // Initialize task-specific file - } + } else { + InitFile(fname, DAT_TASKS_CUSTOM_EXTENSION_SIZE); // Initialize task-specific file + } #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS } #ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLog(LOG_LEVEL_INFO, concat(F("SaveToFile: "), SettingsType::getSettingsTypeString(settingsType)) + strformat(F(" file: %s task: %d"), fname.c_str(), index + 1)); } - #endif + #endif // ifndef BUILD_NO_DEBUG return SaveToFile(fname.c_str(), offset + posInBlock, memAddress + dataOffset, datasize); } @@ -1837,6 +1945,7 @@ String ClearInFile(SettingsType::Enum settingsType, int index) { return getSettingsFileIndexRangeError(read, settingsType, index); } #if FEATURE_EXTENDED_CUSTOM_SETTINGS + if (SettingsType::Enum::CustomTaskSettings_Type == settingsType) { max_size = DAT_TASKS_CUSTOM_SIZE; // Don't also wipe the external size inside the config.dat file... DeleteExtendedCustomTaskSettingsFile(settingsType, index); @@ -1855,6 +1964,7 @@ bool DeleteExtendedCustomTaskSettingsFile(SettingsType::Enum settingsType, int i if (fileExists(fname)) { const bool deleted = tryDeleteFile(fname); // Don't need the extension file anymore, so delete it # ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLog(LOG_LEVEL_INFO, concat(F("CustomTaskSettings: Removing no longer needed file: "), fname)); } @@ -1864,7 +1974,9 @@ bool DeleteExtendedCustomTaskSettingsFile(SettingsType::Enum settingsType, int i } return false; } + #endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS + /********************************************************************************************\ Check file system area settings \*********************************************************************************************/ @@ -1872,7 +1984,7 @@ int SpiffsSectors() { #ifndef BUILD_NO_RAM_TRACKER checkRAM(F("SpiffsSectors")); - #endif + #endif // ifndef BUILD_NO_RAM_TRACKER #if defined(ESP8266) # ifdef CORE_POST_2_6_0 uint32_t _sectorStart = ((uint32_t)&_FS_start - 0x40200000) / SPI_FLASH_SEC_SIZE; @@ -1905,6 +2017,7 @@ size_t SpiffsUsedBytes() { size_t SpiffsTotalBytes() { static size_t result = 1; // Do not output 0, this may be used in divisions. + if (result == 1) { #ifdef ESP32 result = ESPEASY_FS.totalBytes(); @@ -1920,9 +2033,10 @@ size_t SpiffsTotalBytes() { size_t SpiffsBlocksize() { static size_t result = 1; + if (result == 1) { #ifdef ESP32 - result = 8192; // Just assume 8k, since we cannot query it + result = 8192; // Just assume 8k, since we cannot query it #endif // ifdef ESP32 #ifdef ESP8266 fs::FSInfo fs_info; @@ -1935,9 +2049,10 @@ size_t SpiffsBlocksize() { size_t SpiffsPagesize() { static size_t result = 1; + if (result == 1) { #ifdef ESP32 - result = 256; // Just assume 256, since we cannot query it + result = 256; // Just assume 256, since we cannot query it #endif // ifdef ESP32 #ifdef ESP8266 fs::FSInfo fs_info; @@ -1949,7 +2064,7 @@ size_t SpiffsPagesize() { } size_t SpiffsFreeSpace() { - int freeSpace = SpiffsTotalBytes() - SpiffsUsedBytes(); + int freeSpace = SpiffsTotalBytes() - SpiffsUsedBytes(); const size_t blocksize = SpiffsBlocksize(); if (freeSpace < static_cast(2 * blocksize)) { @@ -1965,6 +2080,7 @@ bool SpiffsFull() { } #if FEATURE_RTC_CACHE_STORAGE + /********************************************************************************************\ Handling cached data \*********************************************************************************************/ @@ -1972,16 +2088,16 @@ String createCacheFilename(unsigned int count) { String fname; fname.reserve(16); - #ifdef ESP32 + # ifdef ESP32 fname = '/'; - #endif // ifdef ESP32 + # endif // ifdef ESP32 fname += strformat(F("cache_%d.bin"), count); return fname; } // Match string with an integer between '_' and ".bin" int getCacheFileCountFromFilename(const String& fname) { - if (!isCacheFile(fname)) return -1; + if (!isCacheFile(fname)) { return -1; } int startpos = fname.indexOf('_'); if (startpos < 0) { return -1; } @@ -2008,7 +2124,7 @@ bool getCacheFileCounters(uint16_t& lowest, uint16_t& highest, size_t& filesizeH lowest = 65535; highest = 0; filesizeHighest = 0; -#ifdef ESP8266 +# ifdef ESP8266 fs::Dir dir = ESPEASY_FS.openDir(F("cache")); while (dir.next()) { @@ -2026,8 +2142,8 @@ bool getCacheFileCounters(uint16_t& lowest, uint16_t& highest, size_t& filesizeH } } } -#endif // ESP8266 -#ifdef ESP32 +# endif // ESP8266 +# ifdef ESP32 fs::File root = ESPEASY_FS.open(F("/")); fs::File file = root.openNextFile(); @@ -2035,6 +2151,7 @@ bool getCacheFileCounters(uint16_t& lowest, uint16_t& highest, size_t& filesizeH { if (!file.isDirectory()) { const String fname(file.name()); + if (fname.startsWith(F("/cache")) || fname.startsWith(F("cache"))) { int count = getCacheFileCountFromFilename(fname); @@ -2047,18 +2164,18 @@ bool getCacheFileCounters(uint16_t& lowest, uint16_t& highest, size_t& filesizeH highest = count; filesizeHighest = file.size(); } -#ifndef BUILD_NO_DEBUG +# ifndef BUILD_NO_DEBUG } else { if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLog(LOG_LEVEL_INFO, concat(F("RTC : Cannot get count from: "), fname)); } -#endif +# endif // ifndef BUILD_NO_DEBUG } } } file = root.openNextFile(); } -#endif // ESP32 +# endif // ESP32 if (lowest <= highest) { return true; @@ -2067,7 +2184,8 @@ bool getCacheFileCounters(uint16_t& lowest, uint16_t& highest, size_t& filesizeH highest = 0; return false; } -#endif + +#endif // if FEATURE_RTC_CACHE_STORAGE /********************************************************************************************\ Get partition table information @@ -2102,12 +2220,12 @@ String getPartitionType(uint8_t pType, uint8_t pSubType) { case ESP_PARTITION_SUBTYPE_DATA_COREDUMP: return F("COREDUMP"); case ESP_PARTITION_SUBTYPE_DATA_ESPHTTPD: return F("ESPHTTPD"); case ESP_PARTITION_SUBTYPE_DATA_FAT: return F("FAT"); - case ESP_PARTITION_SUBTYPE_DATA_SPIFFS: - #ifdef USE_LITTLEFS + case ESP_PARTITION_SUBTYPE_DATA_SPIFFS: + # ifdef USE_LITTLEFS return F("LittleFS"); - #else + # else // ifdef USE_LITTLEFS return F("SPIFFS"); - #endif + # endif // ifdef USE_LITTLEFS default: break; } } @@ -2115,8 +2233,10 @@ String getPartitionType(uint8_t pType, uint8_t pSubType) { } String getPartitionTableHeader(const String& itemSep, const String& lineEnd) { + const char *itemSep_str = itemSep.c_str(); + return strformat(F("Address%sSize%sLabel%sPartition Type%sEncrypted%s"), - itemSep.c_str(), itemSep.c_str(), itemSep.c_str(), itemSep.c_str(), lineEnd.c_str()); + itemSep_str, itemSep_str, itemSep_str, itemSep_str, lineEnd.c_str()); } String getPartitionTable(uint8_t pType, const String& itemSep, const String& lineEnd) { @@ -2127,15 +2247,16 @@ String getPartitionTable(uint8_t pType, const String& itemSep, const String& lin if (_mypartiterator) { do { const esp_partition_t *_mypart = esp_partition_get(_mypartiterator); + const char *itemSep_str = itemSep.c_str(); result += strformat(F("%x%s%s%s%s%s%s%s%s%s"), _mypart->address, - itemSep.c_str(), - formatToHex_decimal(_mypart->size, 1024), - itemSep.c_str(), + itemSep_str, + formatToHex_decimal(_mypart->size, 1024).c_str(), + itemSep_str, _mypart->label, - itemSep.c_str(), + itemSep_str, getPartitionType(_mypart->type, _mypart->subtype).c_str(), - itemSep.c_str(), + itemSep_str, String(_mypart->encrypted ? F("Yes") : F("-")).c_str(), lineEnd.c_str()); } while ((_mypartiterator = esp_partition_next(_mypartiterator)) != nullptr); @@ -2155,7 +2276,7 @@ String downloadFileType(const String& url, const String& user, const String& pas } String filename = getFileName(filetype, filenr); - String fullUrl = joinUrlFilename(url, filename); + String fullUrl = joinUrlFilename(url, filename); String error; if (ResetFactoryDefaultPreference.deleteFirst()) { @@ -2169,6 +2290,7 @@ String downloadFileType(const String& url, const String& user, const String& pas } else { if (fileExists(filename)) { const String filename_bak = strformat(F("%s_bak"), filename.c_str()); + if (fileExists(filename_bak)) { if (!ResetFactoryDefaultPreference.delete_Bak_Files() || !tryDeleteFile(filename_bak)) { return F("Could not rename to _bak"); @@ -2176,7 +2298,7 @@ String downloadFileType(const String& url, const String& user, const String& pas } // Must download it to a tmp file. - const String tmpfile = strformat(F("%s_tmp"),filename.c_str()); + const String tmpfile = strformat(F("%s_tmp"), filename.c_str()); if (!downloadFile(fullUrl, tmpfile, user, pass, error)) { return error; @@ -2234,6 +2356,7 @@ String downloadFileType(FileType::Enum filetype, unsigned int filenr) } } String res = downloadFileType(url, user, pass, filetype, filenr); + clearAllCaches(); return res; } From f933b02c8340f383107f038be467450475a3d6d1 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 20 Apr 2024 23:06:24 +0200 Subject: [PATCH 085/113] [P167] Enable use of SEN5x sensor, other improvements --- src/Custom-sample.h | 2 + src/_P167_Vindstyrka.ino | 626 +++----- src/src/CustomBuild/define_plugin_sets.h | 6 + src/src/PluginStructs/P167_data_struct.cpp | 1568 +++++++++----------- src/src/PluginStructs/P167_data_struct.h | 345 +++-- 5 files changed, 1134 insertions(+), 1413 deletions(-) diff --git a/src/Custom-sample.h b/src/Custom-sample.h index 50a2a44b4a..86ca11e67e 100644 --- a/src/Custom-sample.h +++ b/src/Custom-sample.h @@ -539,6 +539,8 @@ static const char DATA_ESPEASY_DEFAULT_MIN_CSS[] PROGMEM = { // #define USES_P164 // Gases - ENS16x TVOC/eCO2 // #define USES_P166 // Output - GP8403 Dual channel DAC (Digital Analog Converter) +// #define USES_P167 // Environment - Sensirion SEN5x / Ikea Vindstyrka + /* ####################################################################################################### ########### Controllers diff --git a/src/_P167_Vindstyrka.ino b/src/_P167_Vindstyrka.ino index b97fd6a47f..8906b5e413 100644 --- a/src/_P167_Vindstyrka.ino +++ b/src/_P167_Vindstyrka.ino @@ -4,143 +4,94 @@ // ####################################################################################################### // ######################## Plugin 167 IKEA Vindstyrka I2C Sensor (SEN5x) ############################ // ####################################################################################################### -// 19-06-2023 AndiBaciu creation based upon https://github.com/RobTillaart/SHT2x + +/** Changelog: + * 2024-04-20 tonhuisman: Replace dewpoint calculation by standard calculation, fix issue with status bits, reduce strings + * Remove unneeded code and variables, move most defines to P167_data_struct.h + * Implement Get Config Value to retrieve all available values from a single instance + * Implement multi-instance use (using an I2C multiplexer, as the address isn't configurable) + * Use enum classes where applicable + * Keeping the FSM in place + * 2024-04-19 tonhuisman: Source formatting using Uncrustify (ESPEasy standard) and string handling modifications + * 2023-06-19 AndiBaciu creation based upon https://github.com/RobTillaart/SHT2x + */ # include "./src/PluginStructs/P167_data_struct.h" -#define PLUGIN_167 -#define PLUGIN_ID_167 167 // plugin id -#define PLUGIN_NAME_167 "Environment - Sensirion SEN5x (IKEA Vindstyrka)" // What will be dislpayed in the selection list -#define PLUGIN_VALUENAME1_167 "Temperature" // variable output of the plugin. The label is in quotation marks -#define PLUGIN_VALUENAME2_167 "Humidity" // multiple outputs are supporte -#define PLUGIN_VALUENAME3_167 "tVOC" // multiple outputs are supported -#define PLUGIN_VALUENAME4_167 "NOx" // multiple outputs are supported -#define PLUGIN_VALUENAME5_167 "PM 1.0" // multiple outputs are supported -#define PLUGIN_VALUENAME6_167 "PM 2.5" // multiple outputs are supported -#define PLUGIN_VALUENAME7_167 "PM 4.0" // multiple outputs are supported -#define PLUGIN_VALUENAME8_167 "PM 10.0" // multiple outputs are supported -#define PLUGIN_VALUENAME9_167 "DewPoint" // multiple outputs are supported -#define PLUGIN_DEFAULT_NAME_1 "IKEA_Vindstyrka" -#define PLUGIN_DEFAULT_NAME_2 "Sensirion_SEN5x" - -// PIN/port configuration is stored in the following: -// CONFIG_PIN1 - The first GPIO pin selected within the task -// CONFIG_PIN2 - The second GPIO pin selected within the task -// CONFIG_PIN3 - The third GPIO pin selected within the task -// CONFIG_PORT - The port in case the device has multiple in/out pins -// -// Custom configuration is stored in the following: -// PCONFIG(x) -// x can be between 1 - 8 and can store values between -32767 - 32768 (16 bit) -// -// N.B. these are aliases for a longer less readable amount of code. See _Plugin_Helper.h -// -// PCONFIG_LABEL(x) is a function to generate a unique label used as HTML id to be able to match -// returned values when saving a configuration. - -// Make accessing specific parameters more readable in the code -// #define Pxxx_OUTPUT_TYPE_INDEX 2 -# define P167_I2C_ADDRESS PCONFIG(0) -# define P167_I2C_ADDRESS_LABEL PCONFIG_LABEL(0) -# define P167_MODEL PCONFIG(1) -# define P167_MODEL_LABEL PCONFIG_LABEL(1) -# define P167_MON_SCL_PIN PCONFIG(2) -# define P167_MON_SCL_PIN_LABEL PCONFIG_LABEL(2) -# define P167_QUERY1 PCONFIG(3) -# define P167_QUERY2 PCONFIG(4) -# define P167_QUERY3 PCONFIG(5) -# define P167_QUERY4 PCONFIG(6) -# define P167_SEN_FIRST PCONFIG(7) -# define P167_SEN_ATTEMPT PCONFIG_LONG(1) - - -# define P167_I2C_ADDRESS_DFLT 0x69 -# define P167_MON_SCL_PIN_DFLT 13 -# define P167_MODEL_DFLT 0 // Vindstyrka or SEN54 -# define P167_QUERY1_DFLT 0 // Temperature (C) -# define P167_QUERY2_DFLT 1 // Humidity (%) -# define P167_QUERY3_DFLT 5 // PM2.5 (ug/m3) -# define P167_QUERY4_DFLT 2 // tVOC (index) - - -# define P167_NR_OUTPUT_VALUES 4 -# define P167_NR_OUTPUT_OPTIONS 10 -# define P167_QUERY1_CONFIG_POS 3 -# define P167_MAX_ATTEMPT 3 // Number of tentative before declaring NAN value - -//# define LIMIT_BUILD_SIZE 1 - -// These pointers may be used among multiple instances of the same plugin, -// as long as the same settings are used. -P167_data_struct *Plugin_167_SEN = nullptr; -boolean Plugin_167_init = false; - -void IRAM_ATTR Plugin_167_interrupt(); - -// Forward declaration helper functions -const __FlashStringHelper* p167_getQueryString(uint8_t query); -const __FlashStringHelper* p167_getQueryValueString(uint8_t query); -unsigned int p167_getRegister(uint8_t query, uint8_t model); -float p167_readVal(uint8_t query, uint8_t node, unsigned int model); - - -// A plugin has to implement the following function - -boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) -{ - // function: reason the plugin was called - // event: ??add description here?? - // string: ??add description here?? +# define PLUGIN_167 +# define PLUGIN_ID_167 167 // plugin id +# define PLUGIN_NAME_167 "Environment - Sensirion SEN5x (IKEA Vindstyrka)" // What will be dislpayed in the selection list + +boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) { boolean success = false; switch (function) { - - case PLUGIN_DEVICE_ADD: { // This case defines the device characteristics - Device[++deviceCount].Number = PLUGIN_ID_167; - Device[deviceCount].Type = DEVICE_TYPE_I2C; - Device[deviceCount].VType = Sensor_VType::SENSOR_TYPE_QUAD; - Device[deviceCount].Ports = 0; - Device[deviceCount].PullUpOption = true; - Device[deviceCount].InverseLogicOption = false; - Device[deviceCount].FormulaOption = true; - Device[deviceCount].ValueCount = P167_NR_OUTPUT_VALUES; - Device[deviceCount].SendDataOption = true; - Device[deviceCount].TimerOption = true; - Device[deviceCount].I2CNoDeviceCheck = true; - Device[deviceCount].GlobalSyncOption = true; - Device[deviceCount].PluginStats = true; - Device[deviceCount].OutputDataType = Output_Data_type_t::Simple; + Device[++deviceCount].Number = PLUGIN_ID_167; + Device[deviceCount].Type = DEVICE_TYPE_I2C; + Device[deviceCount].VType = Sensor_VType::SENSOR_TYPE_QUAD; + Device[deviceCount].Ports = 0; + Device[deviceCount].PullUpOption = false; + Device[deviceCount].InverseLogicOption = false; + Device[deviceCount].FormulaOption = true; + Device[deviceCount].ValueCount = 4; + Device[deviceCount].SendDataOption = true; + Device[deviceCount].TimerOption = true; + Device[deviceCount].I2CNoDeviceCheck = true; + Device[deviceCount].I2CMax100kHz = true; // SEN5x only supports up to 100 kHz + Device[deviceCount].GlobalSyncOption = true; + Device[deviceCount].PluginStats = true; + Device[deviceCount].OutputDataType = Output_Data_type_t::Simple; break; } case PLUGIN_GET_DEVICENAME: { - // return the device name string = F(PLUGIN_NAME_167); break; } + case PLUGIN_GET_DEVICEVALUECOUNT: + { + if (P167_I2C_ADDRESS_DFLT == PCONFIG(0)) { + PCONFIG(P167_SENSOR_TYPE_INDEX) = getValueCountFromSensorType(Sensor_VType::SENSOR_TYPE_QUAD); + } + event->Par1 = P167_NR_OUTPUT_VALUES; + success = true; + break; + } + + + case PLUGIN_GET_DEVICEVTYPE: + { + if (P167_I2C_ADDRESS_DFLT == PCONFIG(0)) { + PCONFIG(P167_SENSOR_TYPE_INDEX) = getValueCountFromSensorType(Sensor_VType::SENSOR_TYPE_QUAD); + } + event->sensorType = static_cast(PCONFIG(P167_SENSOR_TYPE_INDEX)); + event->idx = P167_SENSOR_TYPE_INDEX; + success = true; + break; + } + + case PLUGIN_GET_DEVICEVALUENAMES: { - // called when the user opens the module configuration page - // it allows to add a new row for each output variable of the plugin - // For plugins able to choose output types, see P026_Sysinfo.ino. - for (uint8_t i = 0; i < VARS_PER_TASK; ++i) - { - if ( i < P167_NR_OUTPUT_VALUES) - { + if (P167_I2C_ADDRESS_DFLT == PCONFIG(0)) { + PCONFIG(P167_SENSOR_TYPE_INDEX) = getValueCountFromSensorType(Sensor_VType::SENSOR_TYPE_QUAD); + } + + for (uint8_t i = 0; i < VARS_PER_TASK; ++i) { + if (i < P167_NR_OUTPUT_VALUES) { uint8_t choice = PCONFIG(i + P167_QUERY1_CONFIG_POS); - safe_strncpy(ExtraTaskSettings.TaskDeviceValueNames[i], p167_getQueryValueString(choice), sizeof(ExtraTaskSettings.TaskDeviceValueNames[i])); - } - else - { + safe_strncpy(ExtraTaskSettings.TaskDeviceValueNames[i], P167_getQueryValueString(choice), + sizeof(ExtraTaskSettings.TaskDeviceValueNames[i])); + } else { ZERO_FILL(ExtraTaskSettings.TaskDeviceValueNames[i]); } } @@ -150,15 +101,14 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_SET_DEFAULTS: { - // Set a default config here, which will be called when a plugin is assigned to a task. - P167_I2C_ADDRESS = P167_I2C_ADDRESS_DFLT; - P167_MODEL = P167_MODEL_DFLT; - P167_QUERY1 = P167_QUERY1_DFLT; - P167_QUERY2 = P167_QUERY2_DFLT; - P167_QUERY3 = P167_QUERY3_DFLT; - P167_QUERY4 = P167_QUERY4_DFLT; - P167_MON_SCL_PIN = P167_MON_SCL_PIN_DFLT; - P167_SEN_FIRST = 99; + P167_MODEL = P167_MODEL_DFLT; + P167_QUERY1 = P167_QUERY1_DFLT; + P167_QUERY2 = P167_QUERY2_DFLT; + P167_QUERY3 = P167_QUERY3_DFLT; + P167_QUERY4 = P167_QUERY4_DFLT; + P167_MON_SCL_PIN = P167_MON_SCL_PIN_DFLT; + PCONFIG(P167_SENSOR_TYPE_INDEX) = static_cast(Sensor_VType::SENSOR_TYPE_QUAD); + success = true; break; } @@ -175,36 +125,21 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_I2C_HAS_ADDRESS: - case PLUGIN_WEBFORM_SHOW_I2C_PARAMS: + { - const uint8_t i2cAddressValues[] = { P167_I2C_ADDRESS_DFLT }; - if (function == PLUGIN_WEBFORM_SHOW_I2C_PARAMS) - { - if (P167_SEN_FIRST == event->TaskIndex) // If first SEN, serial config available - { - //addFormSelectorI2C(P167_I2C_ADDRESS_LABEL, 3, i2cAddressValues, P167_I2C_ADDRESS); - addFormSelectorI2C(F("i2c_addr"), 1, i2cAddressValues, P167_I2C_ADDRESS); - addFormNote(F("Vindstyrka, SEN54, SEN55 default i2c address: 0x69")); - } - } - else - { - success = intArrayContains(1, i2cAddressValues, event->Par1); - } + success = P167_I2C_ADDRESS_DFLT == event->Par1; break; } case PLUGIN_WEBFORM_SHOW_GPIO_DESCR: { - if (P167_SEN_FIRST == event->TaskIndex) // If first SEN, serial config available - { - if(P167_MODEL==0) - { - string = F("MonPin SCL: "); - string += formatGpioLabel(P167_MON_SCL_PIN, false); - } + // if (P167_SEN_FIRST == event->TaskIndex) { // If first SEN, serial config available + if (P167_MODEL == 0) { + string = strformat(F("MonPin SCL: %s"), formatGpioLabel(P167_MON_SCL_PIN, false).c_str()); } + + // } success = true; break; } @@ -212,13 +147,16 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_LOAD_OUTPUT_SELECTOR: { + if (P167_I2C_ADDRESS_DFLT == PCONFIG(0)) { + PCONFIG(P167_SENSOR_TYPE_INDEX) = getValueCountFromSensorType(Sensor_VType::SENSOR_TYPE_QUAD); + } const __FlashStringHelper *options[P167_NR_OUTPUT_OPTIONS]; - for (int i = 0; i < P167_NR_OUTPUT_OPTIONS; ++i) - { - options[i] = p167_getQueryString(i); + + for (int i = 0; i < P167_NR_OUTPUT_OPTIONS; ++i) { + options[i] = P167_getQueryString(i); } - for (uint8_t i = 0; i < P167_NR_OUTPUT_VALUES; ++i) - { + + for (uint8_t i = 0; i < P167_NR_OUTPUT_VALUES; ++i) { const uint8_t pconfigIndex = i + P167_QUERY1_CONFIG_POS; sensorTypeHelper_loadOutputSelector(event, pconfigIndex, i, P167_NR_OUTPUT_OPTIONS, options); } @@ -229,89 +167,59 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_LOAD: { - // this case defines what should be displayed on the web form, when this plugin is selected - // The user's selection will be stored in - // PCONFIG(x) (custom configuration) - - if (Plugin_167_SEN == nullptr) - { - P167_SEN_FIRST = event->TaskIndex; // To detect if first SEN or not + const __FlashStringHelper *options_model[] = { + toString(P167_model::Vindstyrka), + toString(P167_model::SEN54), + toString(P167_model::SEN55), + }; + const int options_model_value[] = { + P167_MODEL_VINDSTYRKA, + P167_MODEL_SEN54, + P167_MODEL_SEN55, + }; + constexpr uint8_t optCount = NR_ELEMENTS(options_model_value); + + addFormSelector(F("Model Type"), P167_MODEL_LABEL, optCount, + options_model, options_model_value, P167_MODEL, true); + addFormNote(F("Changing the Model Type will reload the page.")); + + if (P167_MODEL == P167_MODEL_VINDSTYRKA) { + addFormPinSelect(PinSelectPurpose::Generic_input, F("MonPin SCL"), F("taskdevicepin3"), P167_MON_SCL_PIN); + addFormNote(F("Pin for monitoring I2C communication between Vindstyrka controller and SEN54. " + "(Only when Model Type: IKEA Vindstyrka is selected.)")); } - if (P167_SEN_FIRST == event->TaskIndex) // If first SEN, serial config available - { - addHtml(F("
This SEN5x is the first. Its configuration of Pins will affect next SEN5x.")); - addHtml(F("
If several SEN5x's foreseen, don't use other pins.
")); - - const __FlashStringHelper *options_model[3] = { F("IKEA Vindstyrka"), F("Sensirion SEN54"), F("Sensirion SEN55")}; - - addFormSelector(F("Model Type"), P167_MODEL_LABEL, 3, options_model, nullptr, P167_MODEL); - - if(P167_MODEL==0) - { - addFormPinSelect(PinSelectPurpose::Generic_input, F("MonPin SCL"), F("taskdevicepin3"), P167_MON_SCL_PIN); - addFormNote(F("Pin for monitoring i2c communication between Vindstyrka controller and SEN5x. (Only when Model - IKEA Vindstyrka is selected.)")); - } - - if (Plugin_167_SEN != nullptr) - { - addRowLabel(F("Device info")); - String prodname; - String sernum; - uint8_t firmware; - Plugin_167_SEN->getEID(prodname, sernum, firmware); - String txt = F("ProdName: "); - txt += prodname; - txt += F(" Serial Number: "); - txt += sernum; - txt += F(" Firmware: "); - txt += String (firmware); - addHtml(txt); - - addRowLabel(F("Device status")); - txt = F("Speed warning: "); - txt += String((bool)Plugin_167_SEN->getStatusInfo(sensor_speed)); - txt += F(" , Auto Cleaning: "); - txt += String((bool)Plugin_167_SEN->getStatusInfo(sensor_autoclean)); - txt += F(" , GAS Error: "); - txt += String((bool)Plugin_167_SEN->getStatusInfo(sensor_gas)); - txt += F(" , RHT Error: "); - txt += String((bool)Plugin_167_SEN->getStatusInfo(sensor_rht)); - txt += F(" , LASER Error: "); - txt += String((bool)Plugin_167_SEN->getStatusInfo(sensor_laser)); - txt += F(" , FAN Error: "); - txt += String((bool)Plugin_167_SEN->getStatusInfo(sensor_fan)); - addHtml(txt); - - addRowLabel(F("Check (pass/fail/errCode)")); - txt = Plugin_167_SEN->getSuccCount(); - txt += '/'; - txt += Plugin_167_SEN->getErrCount(); - txt += '/'; - txt += Plugin_167_SEN->getErrCode(); - addHtml(txt); - - } - } - else - { - addHtml(F("
This SEN5x is the NOT the first. Model and Pins config are DISABLED. Configuration is available in the first SEN5x plugin.")); - addHtml(F("
Only output value can be configured.
")); - - //looking for FIRST task Named "IKEA_Vindstyrka or Sensirion_SEN5x" - //Cache.taskIndexName - uint8_t allready_defined=88; - if(P167_MODEL==0) - { - allready_defined=findTaskIndexByName(PLUGIN_DEFAULT_NAME_1); - } - else - { - allready_defined=findTaskIndexByName(PLUGIN_DEFAULT_NAME_2); - } - P167_SEN_FIRST = allready_defined; + P167_data_struct *Plugin_167_SEN = static_cast(getPluginTaskData(event->TaskIndex)); + + if (Plugin_167_SEN != nullptr) { + addRowLabel(F("Device info")); + String prodname; + String sernum; + uint8_t firmware; + Plugin_167_SEN->getEID(prodname, sernum, firmware); + addHtml(strformat(F("ProdName: %s, Serial Number: %s, Firmware: %d"), + prodname.c_str(), sernum.c_str(), firmware)); + + addRowLabel(F("Device status")); + addHtml(strformat(F("Speed warning: %d, Auto Cleaning: %d, GAS Error: %d, " + "RHT Error: %d, LASER Error: %d, FAN Error: %d"), + Plugin_167_SEN->getStatusInfo(P167_statusinfo::sensor_speed), + Plugin_167_SEN->getStatusInfo(P167_statusinfo::sensor_autoclean), + Plugin_167_SEN->getStatusInfo(P167_statusinfo::sensor_gas), + Plugin_167_SEN->getStatusInfo(P167_statusinfo::sensor_rht), + Plugin_167_SEN->getStatusInfo(P167_statusinfo::sensor_laser), + Plugin_167_SEN->getStatusInfo(P167_statusinfo::sensor_fan) + )); + + addRowLabel(F("Check (pass/fail/errCode)")); + addHtml(strformat(F("%d/%d/%d"), + Plugin_167_SEN->getSuccCount(), + Plugin_167_SEN->getErrCount(), + Plugin_167_SEN->getErrCode() + )); } + addFormCheckBox(F("Technical logging"), P167_ENABLE_LOG_LABEL, P167_ENABLE_LOG); success = true; break; } @@ -319,36 +227,22 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_WEBFORM_SAVE: { - // this case defines the code to be executed when the form is submitted - // the plugin settings should be saved to PCONFIG(x) - // ping configuration should be read from CONFIG_PIN1 and stored - // Save output selector parameters. - for (uint8_t i = 0; i < P167_NR_OUTPUT_VALUES; ++i) - { + for (uint8_t i = 0; i < P167_NR_OUTPUT_VALUES; ++i) { const uint8_t pconfigIndex = i + P167_QUERY1_CONFIG_POS; - const uint8_t choice = PCONFIG(pconfigIndex); - sensorTypeHelper_saveOutputSelector(event, pconfigIndex, i, p167_getQueryValueString(choice)); + const uint8_t choice = PCONFIG(pconfigIndex); + sensorTypeHelper_saveOutputSelector(event, pconfigIndex, i, P167_getQueryValueString(choice)); } - P167_MODEL = getFormItemInt(P167_MODEL_LABEL); - P167_I2C_ADDRESS = P167_I2C_ADDRESS_DFLT; - if(P167_MODEL==0) - P167_MON_SCL_PIN = getFormItemInt(F("taskdevicepin3")); - P167_SEN_FIRST = P167_SEN_FIRST; - - if (P167_SEN_FIRST == event->TaskIndex) // For first task set default name - { - if(P167_MODEL==0) - { - strcpy(ExtraTaskSettings.TaskDeviceName, PLUGIN_DEFAULT_NAME_1); // populate default name. - } - else - { - strcpy(ExtraTaskSettings.TaskDeviceName, PLUGIN_DEFAULT_NAME_2); // populate default name. - } + P167_MODEL = getFormItemInt(P167_MODEL_LABEL); + P167_ENABLE_LOG = isFormItemChecked(P167_ENABLE_LOG_LABEL); + + if (P167_MODEL == P167_MODEL_VINDSTYRKA) { + P167_MON_SCL_PIN = getFormItemInt(F("taskdevicepin3")); + } else { + P167_MON_SCL_PIN = -1; // None } - - Plugin_167_init = false; // Force device setup next time + + success = true; break; } @@ -356,44 +250,26 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_INIT: { - // this case defines code to be executed when the plugin is initialised - // This will fail if the set to be first taskindex is no longer enabled - - if (P167_SEN_FIRST == event->TaskIndex) // If first SEN5x, config available - { - if (Plugin_167_SEN != nullptr) - { - delete Plugin_167_SEN; - Plugin_167_SEN = nullptr; - } + if (P167_I2C_ADDRESS_DFLT == PCONFIG(0)) { + PCONFIG(P167_SENSOR_TYPE_INDEX) = getValueCountFromSensorType(Sensor_VType::SENSOR_TYPE_QUAD); + } + initPluginTaskData(event->TaskIndex, new (std::nothrow) P167_data_struct()); + P167_data_struct *Plugin_167_SEN = static_cast(getPluginTaskData(event->TaskIndex)); - Plugin_167_SEN = new (std::nothrow) P167_data_struct(); - - if (Plugin_167_SEN != nullptr) - { - Plugin_167_SEN->setupModel(P167_MODEL); - Plugin_167_SEN->setupDevice(P167_I2C_ADDRESS); - if(P167_MODEL==0) - { - Plugin_167_SEN->setupMonPin(P167_MON_SCL_PIN); - pinMode(P167_MON_SCL_PIN, INPUT_PULLUP); - attachInterrupt(P167_MON_SCL_PIN, Plugin_167_interrupt, RISING); - } - Plugin_167_SEN->reset(); + if (Plugin_167_SEN != nullptr) { + Plugin_167_SEN->setupModel(static_cast(P167_MODEL)); + Plugin_167_SEN->setupDevice(P167_I2C_ADDRESS_DFLT); + Plugin_167_SEN->setLogging(P167_ENABLE_LOG); + + if (P167_MODEL == P167_MODEL_VINDSTYRKA) { + Plugin_167_SEN->setupMonPin(P167_MON_SCL_PIN); } + success = Plugin_167_SEN->reset(); } - - //UserVar[event->BaseVarIndex] = NAN; - //UserVar[event->BaseVarIndex + 1] = NAN; - //UserVar[event->BaseVarIndex + 2] = NAN; - //UserVar[event->BaseVarIndex + 3] = NAN; - UserVar.setFloat(event->BaseVarIndex, 0, NAN); - UserVar.setFloat(event->BaseVarIndex, 1, NAN); - UserVar.setFloat(event->BaseVarIndex, 2, NAN); - UserVar.setFloat(event->BaseVarIndex, 3, NAN); - success = true; - Plugin_167_init = true; + for (taskVarIndex_t v = 0; v < P167_NR_OUTPUT_VALUES; ++v) { + UserVar.setFloat(event->TaskIndex, v, NAN); + } break; } @@ -401,17 +277,10 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_EXIT: { - if (P167_SEN_FIRST == event->TaskIndex) // If first SEN5x, config available - { - if (Plugin_167_SEN != nullptr) - { - if(P167_MODEL==0) - { - Plugin_167_SEN->disableInterrupt_monpin(); - } - delete Plugin_167_SEN; - Plugin_167_SEN = nullptr; - } + P167_data_struct *Plugin_167_SEN = static_cast(getPluginTaskData(event->TaskIndex)); + + if ((Plugin_167_SEN != nullptr) && (P167_MODEL == P167_MODEL_VINDSTYRKA)) { + Plugin_167_SEN->disableInterrupt_monpin(); } success = true; @@ -421,141 +290,82 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_READ: { - // code to be executed to read data - // It is executed according to the delay configured on the device configuration page, only once - - if(event->TaskIndex!=P167_SEN_FIRST) - { - //All the DATA are in the first task of IKEA_Vindstyrka - //so all you have to do is to load this data in the current taskindex data - initPluginTaskData(event->TaskIndex, getPluginTaskData(P167_SEN_FIRST) ); - } + P167_data_struct *Plugin_167_SEN = static_cast(getPluginTaskData(event->TaskIndex)); - if (nullptr != Plugin_167_SEN) - { - if (Plugin_167_SEN->inError()) - { - //UserVar[event->BaseVarIndex] = NAN; - //UserVar[event->BaseVarIndex + 1] = NAN; - //UserVar[event->BaseVarIndex + 2] = NAN; - //UserVar[event->BaseVarIndex + 3] = NAN; - UserVar.setFloat(event->BaseVarIndex, 0, NAN); - UserVar.setFloat(event->BaseVarIndex, 1, NAN); - UserVar.setFloat(event->BaseVarIndex, 2, NAN); - UserVar.setFloat(event->BaseVarIndex, 3, NAN); - addLog(LOG_LEVEL_ERROR, F("Vindstyrka / SEN5X: in Error!")); - } - else - { - if(event->TaskIndex==P167_SEN_FIRST) - { - Plugin_167_SEN->startMeasurements(); // getting ready for another read cycle + if (nullptr != Plugin_167_SEN) { + if (Plugin_167_SEN->inError()) { + for (taskVarIndex_t v = 0; v < P167_NR_OUTPUT_VALUES; ++v) { + UserVar.setFloat(event->TaskIndex, v, NAN); } + addLog(LOG_LEVEL_ERROR, F("Vindstyrka / SEN5X: in Error!")); + } else { + // if (event->TaskIndex == P167_SEN_FIRST) { + Plugin_167_SEN->startMeasurements(); // getting ready for another read cycle + // } - //UserVar[event->BaseVarIndex] = Plugin_167_SEN->getRequestedValue(P167_QUERY1); - //UserVar[event->BaseVarIndex + 1] = Plugin_167_SEN->getRequestedValue(P167_QUERY2); - //UserVar[event->BaseVarIndex + 2] = Plugin_167_SEN->getRequestedValue(P167_QUERY3); - //UserVar[event->BaseVarIndex + 3] = Plugin_167_SEN->getRequestedValue(P167_QUERY4); - UserVar.setFloat(event->BaseVarIndex, 0, Plugin_167_SEN->getRequestedValue(P167_QUERY1)); - UserVar.setFloat(event->BaseVarIndex, 1, Plugin_167_SEN->getRequestedValue(P167_QUERY2)); - UserVar.setFloat(event->BaseVarIndex, 2, Plugin_167_SEN->getRequestedValue(P167_QUERY3)); - UserVar.setFloat(event->BaseVarIndex, 3, Plugin_167_SEN->getRequestedValue(P167_QUERY4)); + for (taskVarIndex_t v = 0; v < P167_NR_OUTPUT_VALUES; ++v) { + UserVar.setFloat(event->TaskIndex, v, Plugin_167_SEN->getRequestedValue(PCONFIG(P167_QUERY1_CONFIG_POS + v))); + } + success = true; } } - success = true; break; } - case PLUGIN_ONCE_A_SECOND: + case PLUGIN_FIFTY_PER_SECOND: { - // code to be executed once a second. Tasks which do not require fast response can be added here - success = true; - } + P167_data_struct *Plugin_167_SEN = static_cast(getPluginTaskData(event->TaskIndex)); + if (nullptr != Plugin_167_SEN) { + Plugin_167_SEN->monitorSCL(); // Vindstryka / SEN5X FSM evaluation + Plugin_167_SEN->update(); + } - case PLUGIN_TEN_PER_SECOND: - { - // code to be executed 10 times per second. Tasks which require fast response can be added here - // be careful on what is added here. Heavy processing will result in slowing the module down! + // } success = true; } + case PLUGIN_GET_CONFIG_VALUE: - case PLUGIN_FIFTY_PER_SECOND: { - // code to be executed 10 times per second. Tasks which require fast response can be added here - // be careful on what is added here. Heavy processing will result in slowing the module down! - if(event->TaskIndex==P167_SEN_FIRST) - { - if (nullptr != Plugin_167_SEN) - { - Plugin_167_SEN->monitorSCL(); // Vind / SEN5X FSM evaluation - Plugin_167_SEN->update(); + P167_data_struct *Plugin_167_SEN = static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr != Plugin_167_SEN) { + for (uint8_t v = 0; v < P167_VALUE_COUNT && !success; ++v) { + if (string.equalsIgnoreCase(P167_getQueryValueString(v))) { + string = Plugin_167_SEN->getRequestedValue(v); + success = true; + break; + } } } - success = true; + break; } - } // switch - return success; -} // function + case PLUGIN_WRITE: + { + P167_data_struct *Plugin_167_SEN = static_cast(getPluginTaskData(event->TaskIndex)); + if (nullptr != Plugin_167_SEN) { + const String cmd = parseString(string, 1); -/// @brief -/// @param query -/// @return -const __FlashStringHelper* p167_getQueryString(uint8_t query) -{ - switch(query) - { - case 0: return F("Temperature (C)"); - case 1: return F("Humidity (% RH)"); - case 2: return F("tVOC (VOC index)"); - case 3: return F("NOx (NOx index)"); - case 4: return F("PM 1.0 (ug/m3)"); - case 5: return F("PM 2.5 (ug/m3)"); - case 6: return F("PM 4.0 (ug/m3)"); - case 7: return F("PM 10.0 (ug/m3)"); - case 8: return F("DewPoint (C)"); - } - return F(""); -} - -/// @brief -/// @param query -/// @return -const __FlashStringHelper* p167_getQueryValueString(uint8_t query) -{ - switch(query) - { - case 0: return F("Temperature"); - case 1: return F("Humidity"); - case 2: return F("tVOC"); - case 3: return F("NOx"); - case 4: return F("PM1p0"); - case 5: return F("PM2p5"); - case 6: return F("PM4p0"); - case 7: return F("PM10p0"); - case 8: return F("DewPoint"); - } - return F(""); -} - - -// When using interrupts we have to call the library entry point -// whenever an interrupt is triggered -void IRAM_ATTR Plugin_167_interrupt() -{ - //addLog(LOG_LEVEL_ERROR, F("********* SEN5X: interrupt apear!")); - if (Plugin_167_SEN) - { - Plugin_167_SEN->checkPin_interrupt(); - } -} + if (equals(cmd, F("sen5x"))) { + const String subcmd = parseString(string, 2); + if (equals(subcmd, F("startclean"))) { + Plugin_167_SEN->startCleaning(); + success = true; + } + } + } + break; + } + } // switch + return success; +} // Plugin_167 -#endif //USES_P167 \ No newline at end of file +#endif // USES_P167 diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index f0ee7eec64..c89fa069d9 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -1935,6 +1935,9 @@ To create/register a plugin, you have to : #ifndef USES_P166 #define USES_P166 // Output - GP8403 DAC 0-10V #endif + #ifndef USES_P167 + #define USES_P167 // Environment - Sensirion SEN5x / Ikea Vindstyrka + #endif // Controllers #ifndef USES_C011 @@ -2328,6 +2331,9 @@ To create/register a plugin, you have to : #ifndef USES_P166 #define USES_P166 // Output - GP8403 DAC 0-10V #endif + #ifndef USES_P167 + #define USES_P167 // Environment - SensirionSEN5x / Ikea Vindstyrka + #endif // Controllers #ifndef USES_C015 diff --git a/src/src/PluginStructs/P167_data_struct.cpp b/src/src/PluginStructs/P167_data_struct.cpp index c9bc7d5a9e..6991bbf5f1 100644 --- a/src/src/PluginStructs/P167_data_struct.cpp +++ b/src/src/PluginStructs/P167_data_struct.cpp @@ -1,198 +1,176 @@ /////////////////////////////////////////////////////////////////////////////////////////////////// -// P167 device class for IKEA Vindstyrka SEN54 temperature , humidity and air quality sensors +// P167 device class for IKEA Vindstyrka SEN54 and Sensirion SEN5x temperature, humidity and air quality sensors // See datasheet https://sensirion.com/media/documents/6791EFA0/62A1F68F/Sensirion_Datasheet_Environmental_Node_SEN5x.pdf // and info about extra request https://sensirion.com/media/documents/2B6FC1F3/6409E74A/PS_AN_Read_RHT_VOC_and_NOx_RAW_signals_D1.pdf // Based upon code from Rob Tillaart, Viktor Balint, https://github.com/RobTillaart/SHT2x -// Rewritten and adapted for ESPeasy by andibaciu -// 2023-06-20 Initial version by andibaciu +// Rewritten and adapted for ESPeasy by andibaciu and tonhuisman +// changelog in _P167_Vindstyrka.ino ////////////////////////////////////////////////////////////////////////////////////////////////// #include "../PluginStructs/P167_data_struct.h" #include "../ESPEasyCore/ESPEasyGPIO.h" +#include "../Helpers/CRC_functions.h" #include - #ifndef CORE_POST_3_0_0 - #ifdef ESP8266 - #define IRAM_ATTR ICACHE_RAM_ATTR - #endif - #endif - #ifdef USES_P167 -#define P167_START_MEAS 0x0021 // Start measurement command -#define P167_START_MEAS_RHT_GAS 0x0037 // Start measurement RHT/Gas command -#define P167_STOP_MEAS 0x0104 // Stop measurement command -#define P167_READ_DATA_RDY_FLAG 0x0202 // Read Data Ready Flag command -#define P167_READ_MEAS 0x03C4 // Read measurement command -#define P167_R_W_TEMP_COMP_PARAM 0x60B2 // Read/Write Temperature Compensation Parameters command -#define P167_R_W_TWARM_START_PARAM 0x60C6 // Read/Write Warm Start Parameters command -#define P167_R_W_VOC_ALG_PARAM 0x60D0 // Read/Write VOC Algorithm Tuning Parameters command -#define P167_R_W_NOX_ALG_PARAM 0x60E1 // Read/Write NOx Algorithm Tuning Parameters command -#define P167_R_W_RH_T_ACC_Mode 0x60F7 // Read/Write RH/T Acceleration Mode command -#define P167_R_W_VOC_ALG_STATE 0x6181 // Read/Write VOC Algorithm State command -#define P167_START_FAN_CLEAN 0x5607 // Start fan cleaning command -#define P167_R_W_AUTOCLEN_PARAM 0x8004 // Read/Write Autocleaning Interval Parameters command -#define P167_READ_PROD_NAME 0xD014 // Read Product Name command -#define P167_READ_SERIAL_NO 0xD033 // Read Serial Number command -#define P167_READ_FIRM_VER 0xD100 // Read Firmware Version command -#define P167_READ_DEVICE_STATUS 0xD206 // Read Device Status command -#define P167_CLEAR_DEVICE_STATUS 0xD210 // Clear Device Status command -#define P167_RESET_DEVICE 0xD304 // Reset Device command -#define P167_READ_RAW_MEAS 0x03D2 // Read relative humidity and temperature +# define P167_START_MEAS 0x0021 // Start measurement command +# define P167_START_MEAS_RHT_GAS 0x0037 // Start measurement RHT/Gas command +# define P167_STOP_MEAS 0x0104 // Stop measurement command +# define P167_READ_DATA_RDY_FLAG 0x0202 // Read Data Ready Flag command +# define P167_READ_MEAS 0x03C4 // Read measurement command +# define P167_R_W_TEMP_COMP_PARAM 0x60B2 // Read/Write Temperature Compensation Parameters command +# define P167_R_W_TWARM_START_PARAM 0x60C6 // Read/Write Warm Start Parameters command +# define P167_R_W_VOC_ALG_PARAM 0x60D0 // Read/Write VOC Algorithm Tuning Parameters command +# define P167_R_W_NOX_ALG_PARAM 0x60E1 // Read/Write NOx Algorithm Tuning Parameters command +# define P167_R_W_RH_T_ACC_Mode 0x60F7 // Read/Write RH/T Acceleration Mode command +# define P167_R_W_VOC_ALG_STATE 0x6181 // Read/Write VOC Algorithm State command +# define P167_START_FAN_CLEAN 0x5607 // Start fan cleaning command +# define P167_R_W_AUTOCLEN_PARAM 0x8004 // Read/Write Autocleaning Interval Parameters command +# define P167_READ_PROD_NAME 0xD014 // Read Product Name command +# define P167_READ_SERIAL_NO 0xD033 // Read Serial Number command +# define P167_READ_FIRM_VER 0xD100 // Read Firmware Version command +# define P167_READ_DEVICE_STATUS 0xD206 // Read Device Status command +# define P167_CLEAR_DEVICE_STATUS 0xD210 // Clear Device Status command +# define P167_RESET_DEVICE 0xD304 // Reset Device command +# define P167_READ_RAW_MEAS 0x03D2 // Read relative humidity and temperature // which are not compensated for temperature offset, and the // VOC and NOx raw signals (proportional to the logarithm of the // resistance of the MOX layer). It returns 4x2 bytes (+ 1 CRC // byte each) command (see second datasheet fron header for more info) -#define P167_READ_RAW_MYS_MEAS 0x03F5 // Read relative humidity and temperature and MYSTERY word (probably signed offset temperature) - - -#define P167_START_MEAS_DELAY 50 // Timeout value for start measurement command [ms] -#define P167_START_MEAS_RHT_GAS_DELAY 50 // Timeout value for start measurement RHT/Gas command [ms] -#define P167_STOP_MEAS_DELAY 200 // Timeout value for start measurement command [ms] -#define P167_READ_DATA_RDY_FLAG_DELAY 20 // Timeout value for read data ready flag command [ms] -#define P167_READ_MEAS_DELAY 20 // Timeout value for read measurement command [ms] -#define P167_R_W_TEMP_COMP_PARAM_DELAY 20 // Timeout value for read/write temperature compensation parameters command [ms] -#define P167_R_W_WARM_START_PARAM_DELAY 20 // Timeout value for read/write warm start parameters command [ms] -#define P167_R_W_VOC_ALG_PARAM_DELAY 20 // Timeout value for read/write VOC algorithm tuning parameters command [ms] -#define P167_R_W_NOX_ALG_PARAM_DELAY 20 // Timeout value for read/write NOx algorithm tuning parameters command [ms] -#define P167_R_W_RH_T_ACC_MODE_DELAY 20 // Timeout value for read/write RH/T acceleration mode command [ms] -#define P167_R_W_VOC_ALG_STATE_DELAY 20 // Timeout value for read/write VOC algorithm State command [ms] -#define P167_START_FAN_CLEAN_DELAY 20 // Timeout value for start fan cleaning command [ms] -#define P167_R_W_AUTOCLEN_PARAM_DELAY 20 // Timeout value for read/write autoclean interval parameters command [ms] -#define P167_READ_PROD_NAME_DELAY 20 // Timeout value for read product name command [ms] -#define P167_READ_SERIAL_NO_DELAY 20 // Timeout value for read serial number command [ms] -#define P167_READ_FIRM_VER_DELAY 20 // Timeout value for read firmware version command [ms] -#define P167_READ_DEVICE_STATUS_DELAY 20 // Timeout value for read device status command [ms] -#define P167_CLEAR_DEVICE_STATUS_DELAY 20 // Timeout value for clear device status command [ms] -#define P167_RESET_DEVICE_DELAY 100 // Timeout value for reset device command [ms] -#define P167_READ_RAW_MEAS_DELAY 20 // Timeout value for read raw temp and humidity command [ms] - - -#define P167_MAX_RETRY 250 // Give up after amount of retries befoe going to error - +# define P167_READ_RAW_MYS_MEAS 0x03F5 // Read relative humidity and temperature and MYSTERY word (probably signed offset + // temperature) + + +# define P167_START_MEAS_DELAY 50 // Timeout value for start measurement command [ms] +# define P167_START_MEAS_RHT_GAS_DELAY 50 // Timeout value for start measurement RHT/Gas command [ms] +# define P167_STOP_MEAS_DELAY 200 // Timeout value for start measurement command [ms] +# define P167_READ_DATA_RDY_FLAG_DELAY 20 // Timeout value for read data ready flag command [ms] +# define P167_READ_MEAS_DELAY 20 // Timeout value for read measurement command [ms] +# define P167_R_W_TEMP_COMP_PARAM_DELAY 20 // Timeout value for read/write temperature compensation parameters command [ms] +# define P167_R_W_WARM_START_PARAM_DELAY 20 // Timeout value for read/write warm start parameters command [ms] +# define P167_R_W_VOC_ALG_PARAM_DELAY 20 // Timeout value for read/write VOC algorithm tuning parameters command [ms] +# define P167_R_W_NOX_ALG_PARAM_DELAY 20 // Timeout value for read/write NOx algorithm tuning parameters command [ms] +# define P167_R_W_RH_T_ACC_MODE_DELAY 20 // Timeout value for read/write RH/T acceleration mode command [ms] +# define P167_R_W_VOC_ALG_STATE_DELAY 20 // Timeout value for read/write VOC algorithm State command [ms] +# define P167_START_FAN_CLEAN_DELAY 20 // Timeout value for start fan cleaning command [ms] +# define P167_R_W_AUTOCLEN_PARAM_DELAY 20 // Timeout value for read/write autoclean interval parameters command [ms] +# define P167_READ_PROD_NAME_DELAY 20 // Timeout value for read product name command [ms] +# define P167_READ_SERIAL_NO_DELAY 20 // Timeout value for read serial number command [ms] +# define P167_READ_FIRM_VER_DELAY 20 // Timeout value for read firmware version command [ms] +# define P167_READ_DEVICE_STATUS_DELAY 20 // Timeout value for read device status command [ms] +# define P167_CLEAR_DEVICE_STATUS_DELAY 20 // Timeout value for clear device status command [ms] +# define P167_RESET_DEVICE_DELAY 100 // Timeout value for reset device command [ms] +# define P167_READ_RAW_MEAS_DELAY 20 // Timeout value for read raw temp and humidity command [ms] + +# define P167_MAX_RETRY 250 // Give up after amount of retries befoe going to error + + +const __FlashStringHelper* toString(P167_model model) { + switch (model) { + case P167_model::Vindstyrka: return F("IKEA Vindstyrka"); + case P167_model::SEN54: return F("Sensirion SEN54"); + case P167_model::SEN55: return F("Sensirion SEN55"); + } + return F(""); +} -#define SCL_MONITOR_PIN 13 //pin13 as monitor scl i2c +/// @brief +/// @param query +/// @return +const __FlashStringHelper* P167_getQueryString(uint8_t query) { + switch (query) { + case 0: return F("Temperature (C)"); + case 1: return F("Humidity (% RH)"); + case 2: return F("tVOC (VOC index)"); + case 3: return F("NOx (NOx index)"); + case 4: return F("PM 1.0 (ug/m3)"); + case 5: return F("PM 2.5 (ug/m3)"); + case 6: return F("PM 4.0 (ug/m3)"); + case 7: return F("PM 10.0 (ug/m3)"); + case 8: return F("DewPoint (C)"); + } + return F(""); +} +/// @brief +/// @param query +/// @return +const __FlashStringHelper* P167_getQueryValueString(uint8_t query) { + switch (query) { + case 0: return F("Temperature"); + case 1: return F("Humidity"); + case 2: return F("tVOC"); + case 3: return F("NOx"); + case 4: return F("PM1p0"); + case 5: return F("PM2p5"); + case 6: return F("PM4p0"); + case 7: return F("PM10p0"); + case 8: return F("DewPoint"); + } + return F(""); +} ////////////////////////////////////////////////////////////////////////////////////////////////// // // PUBLIC // -P167_data_struct::P167_data_struct() -{ - _errCount = 0; - - _Temperature = 0.0; - _rawTemperature = 0.0; - _mysTemperature = 0.0; - _Humidity = 0.0; - _rawHumidity = 0.0; - _mysHumidity = 0.0; - _DewPoint = 0.0; - - _tVOC = 0.0; - _rawtVOC = 0.0; - _NOx = 0.0; - _rawNOx = 0.0; - _mysOffset = 0.0; - _PM1p0 = 0.0; - _PM2p5 = 0.0; - _PM4p0 = 0.0; - _PM10p0 = 0.0; - - _devicestatus.val = (uint32_t)0; - - _readingerrcode = VIND_ERR_NO_ERROR; - _readingerrcount = 0; - _readingsuccesscount = 0; - - _model = 0; - _i2caddr = 0; - _monpin = 0; - - _state = P167_state::Uninitialized; - _eid_productname = F(""); - _eid_serialnumber = F(""); - _firmware = 0; - _last_action_started = 0; - _userreg = 0; +P167_data_struct::P167_data_struct() { + // } - -P167_data_struct::~P167_data_struct() -{ +P167_data_struct::~P167_data_struct() { // } // Initialize/setup device properties // Must be called at least once before oP167::Wairperating the device -bool P167_data_struct::setupDevice(uint8_t i2caddr) -{ +bool P167_data_struct::setupDevice(uint8_t i2caddr) { _i2caddr = i2caddr; -#ifdef PLUGIN_167_DEBUG - if (loglevelActiveFor(LOG_LEVEL_INFO)) - { - String log = F("SEN5x: Setup with address= "); - log += formatToHex(_i2caddr); - addLog(LOG_LEVEL_INFO, log); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLog(LOG_LEVEL_INFO, strformat(F("SEN5x: Setup with address= 0x%02x"), _i2caddr)); } -#endif return true; } - -bool P167_data_struct::setupMonPin(uint8_t monpin) -{ - if (validGpio(monpin)) - { +bool P167_data_struct::setupMonPin(int16_t monpin) { + if (validGpio(monpin)) { _monpin = monpin; - pinMode(_monpin, INPUT_PULLUP); //declare monitoring pin as input with pullup's - //attachInterruptArg(digitalPinToInterrupt(_monpin), reinterpret_cast(checkPin), this, CHANGE); - //enableInterrupt_monpin(); + pinMode(_monpin, INPUT_PULLUP); // declare monitoring pin as input with pullup's + attachInterruptArg(digitalPinToInterrupt(_monpin), + reinterpret_cast(Plugin_167_interrupt), + this, + RISING); - #ifdef PLUGIN_167_DEBUG - if (loglevelActiveFor(LOG_LEVEL_INFO)) - { - String log = F("SEN5x: Setup I2C SCL monpin= "); - log += _monpin; - addLog(LOG_LEVEL_INFO, log); + # ifdef PLUGIN_167_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLog(LOG_LEVEL_INFO, strformat(F("SEN5x: Setup I2C SCL monpin= %d"), _monpin)); } - #endif + # endif // ifdef PLUGIN_167_DEBUG return true; } - else - return false; -} - - -void P167_data_struct::enableInterrupt_monpin(void) -{ - //attachInterruptArg(digitalPinToInterrupt(_monpin), reinterpret_cast(checkPin), this, CHANGE); + return false; } - -void P167_data_struct::disableInterrupt_monpin(void) -{ - //detachInterrupt(digitalPinToInterrupt(_monpin)); +void P167_data_struct::disableInterrupt_monpin(void) { + detachInterrupt(digitalPinToInterrupt(_monpin)); } // Initialize/setup device properties -// Must be called at least once before oP167::Wairperating the device -bool P167_data_struct::setupModel(uint8_t model) -{ +// Must be called at least once before operating the device +bool P167_data_struct::setupModel(P167_model model) { _model = model; -#ifdef PLUGIN_167_DEBUG - if (loglevelActiveFor(LOG_LEVEL_INFO)) - { - String log = F("SEN5x: Setup model= "); - log += String(_model); - addLog(LOG_LEVEL_INFO, log); + # ifdef PLUGIN_167_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLog(LOG_LEVEL_INFO, strformat(F("SEN5x: Setup model= %s"), String(toString(_model)).c_str())); } -#endif + # endif // ifdef PLUGIN_167_DEBUG return true; } @@ -201,434 +179,354 @@ bool P167_data_struct::setupModel(uint8_t model) // This is a state machine that is evaluated step by step by calling update() repetatively // NOTE: Function is expected to run as critical section w.r.t. other provided functions // This is typically met in ESPeasy plugin context when called from within the plugin -bool P167_data_struct::update() -{ - bool stable = false; // signals when a stable state is reached -#ifdef PLUGIN_167_DEBUG +bool P167_data_struct::update() { + bool stable = false; // signals when a stable state is reached + + # ifdef PLUGIN_167_DEBUG P167_state oldState = _state; -#endif + # endif // ifdef PLUGIN_167_DEBUG - if(statusMonitoring == false) + if (!statusMonitoring) { return stable; + } - switch(_state) - { + switch (_state) { case P167_state::Uninitialized: - //we have to stop trying after a while - if (_errCount>P167_MAX_RETRY) - { - _state = P167_state::Error; - stable = true; - } - else if (I2C_wakeup(_i2caddr) != 0) // Try to access the I2C device - { - if (loglevelActiveFor(LOG_LEVEL_ERROR)) - { - String log = F("SEN5x : Not found at I2C address: "); - log += String(_i2caddr, HEX); - addLog(LOG_LEVEL_ERROR, log); + + // we have to stop trying after a while + if (_errCount > P167_MAX_RETRY) { + _state = P167_state::Error; + stable = true; + } else if (I2C_wakeup(_i2caddr) != 0) { // Try to access the I2C device + if (loglevelActiveFor(LOG_LEVEL_ERROR)) { + addLog(LOG_LEVEL_ERROR, strformat(F("SEN5x: Not found at I2C address: 0x%02x"), _i2caddr)); } _errCount++; - } - else if (_model==0 ) //sensor is Vindstyrka and d'ont need to be reset - { - _errCount = 0; // Device is reachable and initialized, reset error counter - if (writeCmd(P167_READ_FIRM_VER)) // Issue a reset command - { - _state = P167_state::Read_firm_version; // Will take <20ms according to datasheet + } else if (_model == P167_model::Vindstyrka) { // sensor is Vindstyrka and don't need to be reset + _errCount = 0; // Device is reachable and initialized, reset error counter + + if (writeCmd(P167_READ_FIRM_VER)) { // Issue a reset command + _state = P167_state::Read_firm_version; // Will take <20ms according to datasheet _last_action_started = millis(); } } - else if (_model==1) - { - _errCount = 0; // Device is reachable and initialized, reset error counter - if (writeCmd(P167_RESET_DEVICE)) // Issue a reset command - { - _state = P167_state::Wait_for_reset; // Will take <20ms according to datasheet + else if ((_model == P167_model::SEN54) || (_model == P167_model::SEN55)) { + _errCount = 0; // Device is reachable and initialized, reset error counter + + if (writeCmd(P167_RESET_DEVICE)) { // Issue a reset command + _state = P167_state::Wait_for_reset; // Will take <20ms according to datasheet _last_action_started = millis(); } } - break; + break; case P167_state::Wait_for_reset: - if (timeOutReached(_last_action_started + P167_RESET_DEVICE_DELAY)) //we need to wait for the chip to reset - { - if (I2C_wakeup(_i2caddr) != 0) - { + + if (timeOutReached(_last_action_started + P167_RESET_DEVICE_DELAY)) { // we need to wait for the chip to reset + if (I2C_wakeup(_i2caddr) != 0) { _errCount++; - _state = P167_state::Uninitialized; // Retry - } - else - { - _errCount = 0; // Device is reachable and initialized, reset error counter - if (writeCmd(P167_READ_FIRM_VER)) - { - _state = P167_state::Read_firm_version; // Will take <20ms according to datasheet + _state = P167_state::Uninitialized; // Retry + } else { + _errCount = 0; // Device is reachable and initialized, reset error counter + + if (writeCmd(P167_READ_FIRM_VER)) { + _state = P167_state::Read_firm_version; // Will take <20ms according to datasheet _last_action_started = millis(); } } } - break; + break; case P167_state::Read_firm_version: - if (timeOutReached(_last_action_started + P167_READ_FIRM_VER_DELAY)) - { + + if (timeOutReached(_last_action_started + P167_READ_FIRM_VER_DELAY)) { // Start read flag - if (!getFirmwareVersion()) - { + if (!getFirmwareVersion()) { _errCount++; - _state = P167_state::Uninitialized; // Retry - } - else if(!writeCmd(P167_READ_PROD_NAME)) - { + _state = P167_state::Uninitialized; // Retry + } else if (!writeCmd(P167_READ_PROD_NAME)) { _errCount++; - _state = P167_state::Uninitialized; // Retry - } - else - { + _state = P167_state::Uninitialized; // Retry + } else { _last_action_started = millis(); - _state = P167_state::Read_prod_name; + _state = P167_state::Read_prod_name; } } - break; + break; case P167_state::Read_prod_name: - if (timeOutReached(_last_action_started + P167_READ_PROD_NAME_DELAY)) - { + + if (timeOutReached(_last_action_started + P167_READ_PROD_NAME_DELAY)) { // Start read flag - if (!getProductName()) - { + if (!getProductName()) { _errCount++; - _state = P167_state::Uninitialized; // Retry - } - else if(!writeCmd(P167_READ_SERIAL_NO)) - { + _state = P167_state::Uninitialized; // Retry + } else if (!writeCmd(P167_READ_SERIAL_NO)) { _errCount++; - _state = P167_state::Uninitialized; // Retry - } - else - { + _state = P167_state::Uninitialized; // Retry + } else { _last_action_started = millis(); - _state = P167_state::Read_serial_no; + _state = P167_state::Read_serial_no; } } - break; + break; case P167_state::Read_serial_no: - if (timeOutReached(_last_action_started + P167_READ_SERIAL_NO_DELAY)) - { + + if (timeOutReached(_last_action_started + P167_READ_SERIAL_NO_DELAY)) { // Start read flag - if (!getSerialNumber()) - { + if (!getSerialNumber()) { _errCount++; - _state = P167_state::Uninitialized; // Retry - } - else if(!writeCmd(P167_READ_SERIAL_NO)) - { + _state = P167_state::Uninitialized; // Retry + } else if (!writeCmd(P167_READ_SERIAL_NO)) { // Read serialno again? _errCount++; - _state = P167_state::Uninitialized; // Retry - } - else - { + _state = P167_state::Uninitialized; // Retry + } else { _last_action_started = millis(); - _state = P167_state::Initialized; + _state = P167_state::Initialized; } } - break; + break; case P167_state::Write_user_reg: _state = P167_state::Initialized; - break; + break; case P167_state::Initialized: - // For now trigger the first read cycle automatically - //_state = P167_state::Ready; - break; + + // Trigger the first read cycle automatically on regular SEN5x + if (_model != P167_model::Vindstyrka) { + _state = P167_state::Ready; + } + break; case P167_state::Ready: - // Ready to execute a measurement cycle - if( _model==0 || _model==1 || _model==2) - { + + // Ready to execute a measurement cycle, for Vindstyrka we're eavesdropping so no command needed? + if ((_model == P167_model::Vindstyrka) || (_model == P167_model::SEN54) || (_model == P167_model::SEN55)) { // Start measuring data - if (!writeCmd(P167_START_MEAS)) - { + if (!writeCmd(P167_START_MEAS)) { _errCount++; - _state = P167_state::Uninitialized; // Retry - } - else - { + _state = P167_state::Uninitialized; // Retry + } else { _last_action_started = millis(); - _state = P167_state::Wait_for_start_meas; + _state = P167_state::Wait_for_start_meas; } } - break; + break; case P167_state::Wait_for_start_meas: - if (timeOutReached(_last_action_started + P167_START_MEAS_DELAY)) - { + + if (timeOutReached(_last_action_started + P167_START_MEAS_DELAY)) { // Start read flag - if (!writeCmd(P167_READ_DATA_RDY_FLAG)) - { + if (!writeCmd(P167_READ_DATA_RDY_FLAG)) { _errCount++; - _state = P167_state::Uninitialized; // Retry - } - else - { + _state = P167_state::Uninitialized; // Retry + } else { _last_action_started = millis(); - _state = P167_state::Wait_for_read_flag; + _state = P167_state::Wait_for_read_flag; } } - break; + break; case P167_state::Wait_for_read_flag: - if (timeOutReached(_last_action_started + P167_READ_DATA_RDY_FLAG_DELAY)) - { - if(readDataRdyFlag()) - { + + if (timeOutReached(_last_action_started + P167_READ_DATA_RDY_FLAG_DELAY)) { + if (readDataRdyFlag()) { // Ready to execute a measurement cycle - if (!writeCmd(P167_READ_MEAS)) - { + if (!writeCmd(P167_READ_MEAS)) { _errCount++; - _state = P167_state::Uninitialized; // Retry - } - else - { + _state = P167_state::Uninitialized; // Retry + } else { _last_action_started = millis(); - _state = P167_state::Wait_for_read_meas; + _state = P167_state::Wait_for_read_meas; } - } - else //Ready Flag NOT ok, so send again Start Measurement - { + } else { // Ready Flag NOT ok, so send again Start Measurement // Start measuring data - if (!writeCmd(P167_START_MEAS)) - { + if (!writeCmd(P167_START_MEAS)) { _errCount++; - _state = P167_state::Uninitialized; // Retry - } - else - { + _state = P167_state::Uninitialized; // Retry + } else { _last_action_started = millis(); - _state = P167_state::Wait_for_start_meas; + _state = P167_state::Wait_for_start_meas; } } } - break; + break; - case P167_state::Wait_for_read_meas: - if (timeOutReached(_last_action_started + P167_READ_MEAS_DELAY)) - { - if (!readMeasValue()) // Read the previously measured temperature - { + case P167_state::Wait_for_read_meas: + + if (timeOutReached(_last_action_started + P167_READ_MEAS_DELAY)) { + if (!readMeasValue()) { // Read the previously measured temperature _errCount++; - //_state = P167_state::Uninitialized; // Lost connection + + // _state = P167_state::Uninitialized; // Lost connection _state = P167_state::cmdSTARTmeas; - } - else - { - if (!writeCmd(P167_READ_RAW_MEAS)) - { + } else { + if (!writeCmd(P167_READ_RAW_MEAS)) { _errCount++; - _state = P167_state::Uninitialized; // Retry - } - else - { + _state = P167_state::Uninitialized; // Retry + } else { _last_action_started = millis(); - _state = P167_state::Wait_for_read_raw_meas; + _state = P167_state::Wait_for_read_raw_meas; } } } - break; + break; case P167_state::Wait_for_read_raw_meas: - //make sure we wait for the measurement to complete - if (timeOutReached(_last_action_started + P167_READ_RAW_MEAS_DELAY)) - { - if (!readMeasRawValue()) - { + + // make sure we wait for the measurement to complete + if (timeOutReached(_last_action_started + P167_READ_RAW_MEAS_DELAY)) { + if (!readMeasRawValue()) { _errCount++; - //_state = P167_state::Uninitialized; // Lost connection + + // _state = P167_state::Uninitialized; // Lost connection _state = P167_state::cmdSTARTmeas; - } - else - { - if (!writeCmd(P167_READ_RAW_MYS_MEAS)) - { + } else { + if (!writeCmd(P167_READ_RAW_MYS_MEAS)) { _errCount++; - _state = P167_state::Uninitialized; // Retry - } - else - { + _state = P167_state::Uninitialized; // Retry + } else { _last_action_started = millis(); - _state = P167_state::Wait_for_read_raw_MYS_meas; + _state = P167_state::Wait_for_read_raw_MYS_meas; } } } - break; + break; case P167_state::Wait_for_read_raw_MYS_meas: - //make sure we wait for the measurement to complete - if (timeOutReached(_last_action_started + P167_READ_RAW_MEAS_DELAY)) - { - if (!readMeasRawMYSValue()) - { + + // make sure we wait for the measurement to complete + if (timeOutReached(_last_action_started + P167_READ_RAW_MEAS_DELAY)) { + if (!readMeasRawMYSValue()) { _errCount++; - //_state = P167_state::Uninitialized; // Lost connection + + // _state = P167_state::Uninitialized; // Lost connection _state = P167_state::cmdSTARTmeas; - } - else - { - if (!writeCmd(P167_READ_DEVICE_STATUS)) - { + } else { + if (!writeCmd(P167_READ_DEVICE_STATUS)) { _errCount++; - _state = P167_state::Uninitialized; // Retry - } - else - { + _state = P167_state::Uninitialized; // Retry + } else { _last_action_started = millis(); - _state = P167_state::Wait_for_read_status; + _state = P167_state::Wait_for_read_status; } calculateValue(); stable = true; } } - break; + break; case P167_state::Wait_for_read_status: - //make sure we wait for the measurement to complete - if (timeOutReached(_last_action_started + P167_READ_DEVICE_STATUS_DELAY)) - { - if (!readDeviceStatus()) - { + + // make sure we wait for the measurement to complete + if (timeOutReached(_last_action_started + P167_READ_DEVICE_STATUS_DELAY)) { + if (!readDeviceStatus()) { _errCount++; - //_state = P167_state::Uninitialized; // Lost connection + + // _state = P167_state::Uninitialized; // Lost connection _state = P167_state::cmdSTARTmeas; - } - else - { + } else { _last_action_started = millis(); - _state = P167_state::cmdSTARTmeas; - stable = true; + _state = P167_state::cmdSTARTmeas; + stable = true; } } - break; + break; case P167_state::cmdSTARTmeas: + // Start measuring data - if(_model==0) - { - if (!writeCmd(P167_START_MEAS)) - { + if (_model == P167_model::Vindstyrka) { + if (!writeCmd(P167_START_MEAS)) { _errCount++; - _state = P167_state::Uninitialized; // Retry - } - else - { + _state = P167_state::Uninitialized; // Retry + } else { _last_action_started = millis(); - _state = P167_state::IDLE; + _state = P167_state::IDLE; } - } - else - { + } else { _state = P167_state::IDLE; } - break; + break; case P167_state::IDLE: - stepMonitoring = 1; + stepMonitoring = 1; startMonitoringFlag = false; - if(!_errmeas && !_errmeasraw && !_errmeasrawmys) + + if (!_errmeas && !_errmeasraw && !_errmeasrawmys) { _state = P167_state::New_Values_Available; + } stable = true; - break; + break; case P167_state::Error: case P167_state::New_Values_Available: - //this state is used outside so all we need is to stay here + // this state is used outside so all we need is to stay here stable = true; - break; - - //Missing states (enum values) to be checked by the compiler + break; + // Missing states (enum values) to be checked by the compiler } // switch -#ifdef PLUGIN_167_DEBUG - if (_state != oldState) - { - if (loglevelActiveFor(LOG_LEVEL_INFO)) - { - String log = F("SEN5x : *** state transition "); - log += String((int)oldState); - log += F("-->"); - log += String((int)_state); - addLog(LOG_LEVEL_INFO, log); + # ifdef PLUGIN_167_DEBUG + + if (_state != oldState) { + if (loglevelActiveFor(LOG_LEVEL_INFO) && _enableLogging) { + addLog(LOG_LEVEL_INFO, strformat(F("SEN5x: State transition %d-->%d"), static_cast(oldState), static_cast(_state))); } } -#endif + # endif // ifdef PLUGIN_167_DEBUG return stable; } +bool P167_data_struct::monitorSCL() { + if (_model == P167_model::Vindstyrka) { + if (startMonitoringFlag) { + if (stepMonitoring == 1) { + lastSCLLowTransitionMonitoringTime = monpinLastTransitionTime / 1000; -bool P167_data_struct::monitorSCL() -{ - if(_model==0) - { - if(startMonitoringFlag) - { - - if(stepMonitoring==1) - { - lastSCLLowTransitionMonitoringTime = monpinLastTransitionTime/1000; - if(millis() - lastSCLLowTransitionMonitoringTime < 100) - { + if (millis() - lastSCLLowTransitionMonitoringTime < 100) { statusMonitoring = false; return true; - } - else - { - lastSCLLowTransitionMonitoringTime = monpinLastTransitionTime/1000; - statusMonitoring = true; + } else { + lastSCLLowTransitionMonitoringTime = monpinLastTransitionTime / 1000; + statusMonitoring = true; stepMonitoring++; } } - if(stepMonitoring==2) - { - if(millis() - lastSCLLowTransitionMonitoringTime < 100) - { - lastSCLLowTransitionMonitoringTime = monpinLastTransitionTime/1000; - statusMonitoring = false; - stepMonitoring = 1; + if (stepMonitoring == 2) { + if (millis() - lastSCLLowTransitionMonitoringTime < 100) { + lastSCLLowTransitionMonitoringTime = monpinLastTransitionTime / 1000; + statusMonitoring = false; + stepMonitoring = 1; return true; - } - else if(millis() - lastSCLLowTransitionMonitoringTime > 700) - { - statusMonitoring = false; - stepMonitoring = 1; + } else if (millis() - lastSCLLowTransitionMonitoringTime > 700) { + statusMonitoring = false; + stepMonitoring = 1; startMonitoringFlag = false; - //if _state not finish reading process then start from begining - if(_state >= P167_state::Wait_for_read_meas && _state < P167_state::New_Values_Available) - { + // if _state not finish reading process then start from begining + if ((_state >= P167_state::Wait_for_read_meas) && (_state < P167_state::New_Values_Available)) { _state = P167_state::Ready; } return true; - } - else - { - //processing + } else { + // processing } } } monpinValuelast = monpinValue; } - if(_model == 1 || _model == 2) - { - statusMonitoring = true; + if ((_model == P167_model::SEN54) || (_model == P167_model::SEN55)) { + statusMonitoring = true; startMonitoringFlag = false; - stepMonitoring = 0; + stepMonitoring = 0; } return true; @@ -637,10 +535,8 @@ bool P167_data_struct::monitorSCL() ////////////////////////////////////////////////////////////////////////////////////////////////// // Returns the I2C connection state // Note: based upon the FSM state without actual accessing the device -bool P167_data_struct::isConnected() const -{ - switch (_state) - { +bool P167_data_struct::isConnected() const { + switch (_state) { case P167_state::Initialized: case P167_state::Ready: case P167_state::Wait_for_start_meas: @@ -664,7 +560,7 @@ bool P167_data_struct::isConnected() const return false; break; - //Missing states (enum values) to be checked by the compiler + // Missing states (enum values) to be checked by the compiler } return false; } @@ -672,119 +568,105 @@ bool P167_data_struct::isConnected() const ////////////////////////////////////////////////////////////////////////////////////////////////// // Returns if the device communication is in error // Note: based upon the FSM state without actual accessing the device -bool P167_data_struct::inError() const -{ +bool P167_data_struct::inError() const { return _state == P167_state::Error; } ////////////////////////////////////////////////////////////////////////////////////////////////// // Returns if new acquired values are available -bool P167_data_struct::newValues() const -{ +bool P167_data_struct::newValues() const { return _state == P167_state::New_Values_Available; } ////////////////////////////////////////////////////////////////////////////////////////////////// // Restart the FSM used to access the device -bool P167_data_struct::reset() -{ +bool P167_data_struct::reset() { startMonitoringFlag = true; - stepMonitoring = 1; - _state = P167_state::Uninitialized; + stepMonitoring = 1; + _state = P167_state::Uninitialized; return true; } ////////////////////////////////////////////////////////////////////////////////////////////////// // Start a new measurement cycle -bool P167_data_struct::startMeasurements() -{ - if ((_state == P167_state::New_Values_Available) || (_state == P167_state::Initialized) || (_state == P167_state::IDLE)) - { +bool P167_data_struct::startMeasurements() { + if ((_state == P167_state::New_Values_Available) || + (_state == P167_state::Initialized) || + (_state == P167_state::IDLE)) { _state = P167_state::Ready; } startMonitoringFlag = true; - stepMonitoring = 1; + stepMonitoring = 1; return true; } ////////////////////////////////////////////////////////////////////////////////////////////////// // Get the electronic idenfification data store in the device -// Note: The data is read from the device during initialization -bool P167_data_struct::getEID(String &eid_productname, String &eid_serialnumber, uint8_t &firmware) const -{ - eid_productname = _eid_productname; +// Note: The data is read from the device during initialization +bool P167_data_struct::getEID(String& eid_productname, String& eid_serialnumber, uint8_t& firmware) const { + eid_productname = _eid_productname; eid_serialnumber = _eid_serialnumber; - firmware = _firmware; + firmware = _firmware; return true; } ////////////////////////////////////////////////////////////////////////////////////////////////// // Get the status informasion about different part of the sensor // Note: The data is read from the device after every measurement read request -bool P167_data_struct::getStatusInfo(param_statusinfo param) -{ - switch(param) - { - case sensor_speed: - return (bool) _devicestatus.speed; - break; - - case sensor_autoclean: - return (bool) _devicestatus.autoclean; - break; - - case sensor_gas: - return (bool) _devicestatus.gas; - break; - - case sensor_rht: - return (bool) _devicestatus.rht; - break; +bool P167_data_struct::getStatusInfo(P167_statusinfo param) { + switch (param) { + case P167_statusinfo::sensor_speed: + return _devicestatus.speed; - case sensor_laser: - return (bool) _devicestatus.laser; - break; - - case sensor_fan: - return (bool) _devicestatus.fan; - break; + case P167_statusinfo::sensor_autoclean: + return _devicestatus.autoclean; + + case P167_statusinfo::sensor_gas: + return _devicestatus.gas; + + case P167_statusinfo::sensor_rht: + return _devicestatus.rht; + + case P167_statusinfo::sensor_laser: + return _devicestatus.laser; + + case P167_statusinfo::sensor_fan: + return _devicestatus.fan; } return true; } ////////////////////////////////////////////////////////////////////////////////////////////////// // Return the previously measured raw humidity data [bits] -float P167_data_struct::getRequestedValue(uint8_t request) const -{ - //float requested_value=0; - switch(request) - { - case 0: +float P167_data_struct::getRequestedValue(uint8_t request) const { + switch (request) { + case 0: { - if(_model==0) - return (float) _TemperatureX; - else - return (float) _Temperature; + if (_model == P167_model::Vindstyrka) { + return _TemperatureX; + } else { + return _Temperature; + } } - case 1: + case 1: { - if(_model==0) - return (float) _HumidityX; - else - return (float) _Humidity; + if (_model == P167_model::Vindstyrka) { + return _HumidityX; + } else { + return _Humidity; + } } - case 2: return (float) _tVOC; - case 3: return (float) _NOx; - case 4: return (float) _PM1p0; - case 5: return (float) _PM2p5; - case 6: return (float) _PM4p0; - case 7: return (float) _PM10p0; - case 8: return (float) _DewPoint; + case 2: return _tVOC; + case 3: return _NOx; + case 4: return _PM1p0; + case 5: return _PM2p5; + case 6: return _PM4p0; + case 7: return _PM10p0; + case 8: return _DewPoint; } - return -1; + return -1.0f; } - ////////////////////////////////////////////////////////////////////////////////////////////////// // // PROTECTED @@ -792,40 +674,10 @@ float P167_data_struct::getRequestedValue(uint8_t request) const ////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// -uint8_t P167_data_struct::crc8(const uint8_t *data, uint8_t len) -{ - // CRC-8 formula from page 14 of SHT spec pdf - // Sensirion_Humidity_Sensors_SHT2x_CRC_Calculation.pdf - const uint8_t POLY = 0x31; - uint8_t crc = 0xFF; - - for (uint8_t j = 0; j> 8)); Wire.write((uint8_t)cmd); @@ -846,353 +697,346 @@ bool P167_data_struct::writeCmd(uint16_t cmd, uint8_t value) return Wire.endTransmission() == 0; } - - -bool P167_data_struct::writeCmd(uint16_t cmd, uint8_t length, uint8_t *buffer) -{ +bool P167_data_struct::writeCmd(uint16_t cmd, uint8_t length, uint8_t *buffer) { Wire.beginTransmission(_i2caddr); Wire.write((uint8_t)(cmd >> 8)); Wire.write((uint8_t)cmd); - for (int i = 0; i < length; i++) { + + for (int i = 0; i < length; ++i) { Wire.write(*(buffer + i)); } return Wire.endTransmission() == 0; } ////////////////////////////////////////////////////////////////////////////////////////////////// -bool P167_data_struct::readBytes(uint8_t n, uint8_t *val, uint8_t maxDuration) -{ +bool P167_data_struct::readBytes(uint8_t n, uint8_t *val, uint8_t maxDuration) { // TODO check if part can be delegated to the I2C_access libraray from ESPeasy - Wire.requestFrom(_i2caddr, (uint8_t) n); + Wire.requestFrom(_i2caddr, (uint8_t)n); uint32_t start = millis(); - while (Wire.available() < n) - { - if (timePassedSince(start) > maxDuration) - { + + while (Wire.available() < n) { + if (timePassedSince(start) > maxDuration) { return false; } yield(); } - for (uint8_t i = 0; i < n; i++) - { + for (uint8_t i = 0; i < n; i++) { val[i] = Wire.read(); } return true; } - ////////////////////////////////////////////////////////////////////////////////////////////////// // Read data ready flag from device -bool P167_data_struct::readDataRdyFlag() -{ - uint8_t value=0; +bool P167_data_struct::readDataRdyFlag() { + uint8_t value = 0; uint8_t buffer[3]; - if (!readBytes(3, (uint8_t*) &buffer[0], P167_READ_DATA_RDY_FLAG_DELAY)) - { + if (!readBytes(3, (uint8_t *)&buffer[0], P167_READ_DATA_RDY_FLAG_DELAY)) { return false; } - if (crc8(&buffer[0], 2) == buffer[2]) - { + + if (calc_CRC8(&buffer[0], 2) == buffer[2]) { value += buffer[1]; } return value; } - ////////////////////////////////////////////////////////////////////////////////////////////////// // Read measurement values results from device -bool P167_data_struct::readMeasValue() -{ - uint16_t value=0; - int16_t valuesign=0; - uint8_t buffer[24]; - bool condition=true; +bool P167_data_struct::readMeasValue() { + uint16_t value = 0; + int16_t valuesign = 0; + uint8_t buffer[24]; + bool condition = true; _errmeas = false; - if (!readBytes(24, (uint8_t*) &buffer[0], P167_READ_MEAS_DELAY)) - { + + if (!readBytes(24, (uint8_t *)&buffer[0], P167_READ_MEAS_DELAY)) { _errmeas = true; return false; } - String log = F("SEN5x : *** meas value "); - for(int xx=0; xx<24;xx++) - { - log += String((int)buffer[xx]); - log += F(" "); + String log = F("SEN5x: Measured value "); + + for (int xx = 0; xx < 24; ++xx) { + log += buffer[xx]; + log += ' '; } - if(_model==0 || _model==1) - condition=(buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) || (buffer[6] == 0xFF && buffer[7] == 0xFF) || (buffer[9] == 0xFF && buffer[10] == 0xFF) || (buffer[12] == 0xFF && buffer[13] == 0xFF) || (buffer[15] == 0xFF && buffer[16] == 0xFF) || (buffer[18] == 0xFF && buffer[19] == 0xFF); - if(_model==2) - condition=(buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) || (buffer[6] == 0xFF && buffer[7] == 0xFF) || (buffer[9] == 0xFF && buffer[10] == 0xFF) || (buffer[12] == 0xFF && buffer[13] == 0xFF) || (buffer[15] == 0xFF && buffer[16] == 0xFF) || (buffer[18] == 0xFF && buffer[19] == 0xFF) || (buffer[21] == 0xFF && buffer[22] == 0xFF); - - if(condition) - { + if ((_model == P167_model::Vindstyrka) || (_model == P167_model::SEN54)) { + condition = (buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) || + (buffer[6] == 0xFF && buffer[7] == 0xFF) || (buffer[9] == 0xFF && buffer[10] == 0xFF) || + (buffer[12] == 0xFF && buffer[13] == 0xFF) || + (buffer[15] == 0xFF && buffer[16] == 0xFF) || (buffer[18] == 0xFF && buffer[19] == 0xFF); + } + + if (_model == P167_model::SEN55) { + condition = (buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) || + (buffer[6] == 0xFF && buffer[7] == 0xFF) || (buffer[9] == 0xFF && buffer[10] == 0xFF) || + (buffer[12] == 0xFF && buffer[13] == 0xFF) || + (buffer[15] == 0xFF && buffer[16] == 0xFF) || (buffer[18] == 0xFF && buffer[19] == 0xFF) || + (buffer[21] == 0xFF && buffer[22] == 0xFF); + } + + if (condition) { log += F("- error"); - addLog(LOG_LEVEL_INFO, log); + + if (_enableLogging) { + addLog(LOG_LEVEL_ERROR, log); + } _errmeas = true; _readingerrcount++; return false; - } - else - { - for(int xx=0; xx<8; xx++) - { - if ((crc8(&buffer[xx*3], 2) == buffer[xx*3+2]) && (buffer[xx*3] != 0xFF || buffer[xx*3+1] != 0xFF)) - { - value = buffer[xx*3] << 8; - value += buffer[xx*3+1]; - valuesign = buffer[xx*3] << 8; - valuesign += buffer[xx*3+1]; - if(xx==0) - _PM1p0 = (float)value/10; - if(xx==1) - _PM2p5 = (float)value/10; - if(xx==2) - _PM4p0 = (float)value/10; - if(xx==3) - _PM10p0 = (float)value/10; - if(xx==4) - _Humidity = (float)valuesign/100.0; - if(xx==5) - _Temperature = (float)valuesign/200.0; - if(xx==6) - _tVOC = (float)valuesign/10.0; - if(xx==7) + } else { + for (int xx = 0; xx < 8; ++xx) { + if ((calc_CRC8(&buffer[xx * 3], 2) == buffer[xx * 3 + 2]) && ((buffer[xx * 3] != 0xFF) || (buffer[xx * 3 + 1] != 0xFF))) { + value = buffer[xx * 3] << 8; + value += buffer[xx * 3 + 1]; + valuesign = buffer[xx * 3] << 8; + valuesign += buffer[xx * 3 + 1]; + + if (xx == 0) { + _PM1p0 = value / 10.0f; + } + + if (xx == 1) { + _PM2p5 = value / 10.0f; + } + + if (xx == 2) { + _PM4p0 = value / 10.0f; + } + + if (xx == 3) { + _PM10p0 = value / 10.0; + } + + if (xx == 4) { + _Humidity = valuesign / 100.0f; + } + + if (xx == 5) { + _Temperature = valuesign / 200.0f; + } + + if (xx == 6) { + _tVOC = valuesign / 10.0f; + } + + if (xx == 7) { - if(_model==2) - _NOx = (float)valuesign/10.0; - else - _NOx = (float)0.0; + if (_model == P167_model::SEN55) { + _NOx = valuesign / 10.0f; + } else { + _NOx = 0.0f; + } } - } - else - { + } else { _errmeasrawmys = true; } } - if(_errmeas == true) - { + if (_errmeas) { log += F("- crc error"); _readingerrcount++; - } - else - { + } else { log += F("- pass"); _readingsuccesscount++; + + if (_enableLogging) { + addLog(LOG_LEVEL_INFO, log); + } + return !_errmeas; } - addLog(LOG_LEVEL_INFO, log); - return !_errmeas; } return true; } - ////////////////////////////////////////////////////////////////////////////////////////////////// // Read measurement values results from device -bool P167_data_struct::readMeasRawValue() -{ - uint16_t value=0; - int16_t valuesign=0; - uint8_t buffer[12]; - bool condition=true; +bool P167_data_struct::readMeasRawValue() { + uint16_t value = 0; + int16_t valuesign = 0; + uint8_t buffer[12]; + bool condition = true; _errmeasraw = false; - if (!readBytes(12, (uint8_t*) &buffer[0], P167_READ_RAW_MEAS_DELAY)) - { + + if (!readBytes(12, &buffer[0], P167_READ_RAW_MEAS_DELAY)) { _errmeasraw = true; return false; } - String log = F("SEN5x : *** meas RAW value "); - for(int xx=0; xx<12;xx++) - { - log += String((int)buffer[xx]); - log += F(" "); + String log = F("SEN5x: Measured RAW value 0x"); + + for (int xx = 0; xx < 12; xx++) { + log += strformat(F("%02x "), buffer[xx]); + } + + if ((_model == P167_model::Vindstyrka) || (_model == P167_model::SEN54)) { + condition = (buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) || + (buffer[6] == 0xFF && buffer[7] == 0xFF); // || (buffer[9] == 0xFF && buffer[10] == 0xFF)) + } + + if (_model == P167_model::SEN55) { + condition = (buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) || + (buffer[6] == 0xFF && buffer[7] == 0xFF) || (buffer[9] == 0xFF && buffer[10] == 0xFF); } - if(_model==0 || _model==1) - condition=(buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) || (buffer[6] == 0xFF && buffer[7] == 0xFF);// || (buffer[9] == 0xFF && buffer[10] == 0xFF)) - if(_model==2) - condition=(buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) || (buffer[6] == 0xFF && buffer[7] == 0xFF) || (buffer[9] == 0xFF && buffer[10] == 0xFF); - - if(condition) - { + if (condition) { log += F("- error"); - addLog(LOG_LEVEL_INFO, log); + + if (_enableLogging) { + addLog(LOG_LEVEL_ERROR, log); + } _errmeasraw = true; _readingerrcount++; return false; - } - else - { - for(int xx=0; xx<4; xx++) - { - if ((crc8(&buffer[xx*3], 2) == buffer[xx*3+2]) && (buffer[xx*3] != 0xFF || buffer[xx*3+1] != 0xFF)) - { - value = buffer[xx*3] << 8; - value += buffer[xx*3+1]; - valuesign = buffer[xx*3] << 8; - valuesign += buffer[xx*3+1]; - if(xx==0) - _rawHumidity = (float)valuesign/100.0; - if(xx==1) - _rawTemperature = (float)valuesign/200.0; - if(xx==2) - _rawtVOC = (float)value/10.0; - if(xx==3) - { - if(_model==2) - _rawNOx = (float)value/10.0; - else - _rawNOx = (float)0.0; + } else { + for (int xx = 0; xx < 4; ++xx) { + if ((calc_CRC8(&buffer[xx * 3], 2) == buffer[xx * 3 + 2]) && ((buffer[xx * 3] != 0xFF) || (buffer[xx * 3 + 1] != 0xFF))) { + value = buffer[xx * 3] << 8; + value += buffer[xx * 3 + 1]; + valuesign = buffer[xx * 3] << 8; + valuesign += buffer[xx * 3 + 1]; + + if (xx == 0) { + _rawHumidity = valuesign / 100.0f; + } else + + if (xx == 1) { + _rawTemperature = valuesign / 200.0f; + } else + + if (xx == 2) { + _rawtVOC = value / 10.0f; + } else + + if (xx == 3) { + if (_model == P167_model::SEN55) { + _rawNOx = value / 10.0f; + } else { + _rawNOx = 0.0f; + } } - } - else - { + } else { _errmeasrawmys = true; } } - if(_errmeasraw == true) - { + if (_errmeasraw) { log += F("- crc error"); _readingerrcount++; - } - else - { + } else { log += F("- pass"); _readingsuccesscount++; } - addLog(LOG_LEVEL_INFO, log); + + if (_enableLogging) { + addLog(LOG_LEVEL_INFO, log); + } return !_errmeasraw; } - + return true; } - - ////////////////////////////////////////////////////////////////////////////////////////////////// // Read measurement values results from device -bool P167_data_struct::readMeasRawMYSValue() -{ - uint16_t value=0; - int16_t valuesign=0; +bool P167_data_struct::readMeasRawMYSValue() { + int16_t valuesign = 0; uint8_t buffer[9]; _errmeasrawmys = false; - if (!readBytes(9, (uint8_t*) &buffer[0], P167_READ_RAW_MEAS_DELAY)) - { + + if (!readBytes(9, (uint8_t *)&buffer[0], P167_READ_RAW_MEAS_DELAY)) { _errmeasrawmys = true; return false; } - String log = F("SEN5x : *** meas MYS value "); - for(int xx=0; xx<9;xx++) - { - log += String((int)buffer[xx]); - log += F(" "); + String log = F("SEN5x: Measured MYS value 0x"); + + for (int xx = 0; xx < 9; ++xx) { + log += strformat(F("%02x "), buffer[xx]); } - if((buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) || (buffer[6] == 0xFF && buffer[7] == 0xFF)) - { + if (((buffer[0] == 0xFF) && (buffer[1] == 0xFF)) || + ((buffer[3] == 0xFF) && (buffer[4] == 0xFF)) || + ((buffer[6] == 0xFF) && (buffer[7] == 0xFF))) { log += F("- error"); - addLog(LOG_LEVEL_INFO, log); + + if (_enableLogging) { + addLog(LOG_LEVEL_ERROR, log); + } _errmeasrawmys = true; _readingerrcount++; return false; - } - else - { - for(int xx=0; xx<3; xx++) - { - if ((crc8(&buffer[xx*3], 2) == buffer[xx*3+2]) && (buffer[xx*3] != 0xFF || buffer[xx*3+1] != 0xFF)) - { - value = buffer[xx*3] << 8; - value += buffer[xx*3+1]; - valuesign = buffer[xx*3] << 8; - valuesign += buffer[xx*3+1]; - if(xx==0) - _mysHumidity = (float)valuesign/100.0; - if(xx==1) - _mysTemperature = (float)valuesign/200.0; - if(xx==2) - _mysOffset = (float)valuesign/200.0; - } - else - { + } else { + for (int xx = 0; xx < 3; ++xx) { + if ((calc_CRC8(&buffer[xx * 3], 2) == buffer[xx * 3 + 2]) && ((buffer[xx * 3] != 0xFF) || (buffer[xx * 3 + 1] != 0xFF))) { + valuesign = buffer[xx * 3] << 8; + valuesign += buffer[xx * 3 + 1]; + + if (xx == 0) { + _mysHumidity = valuesign / 100.0f; + } else + + if (xx == 1) { + _mysTemperature = valuesign / 200.0f; + } else + + if (xx == 2) { + _mysOffset = valuesign / 200.0f; + } + } else { _errmeasrawmys = true; } } - if(_errmeasrawmys == true) - { + if (_errmeasrawmys) { log += F("- crc error"); _readingerrcount++; - } - else - { + } else { log += F("- pass"); _readingsuccesscount++; } - addLog(LOG_LEVEL_INFO, log); + + if (_enableLogging) { + addLog(LOG_LEVEL_INFO, log); + } return !_errmeasrawmys; } - + return true; } - ////////////////////////////////////////////////////////////////////////////////////////////////// // Calculate DewPoint, F Temp, F HUM -bool P167_data_struct::calculateValue() -{ - float lnval; - float rapval; - float aval = 17.62; - float bval = 243.12; - float Dp; - float eeval = 0.0; - - if(_model==0) - { - //_TemperatureX = _mysTemperature + _mysOffset - (_mysOffset<0.0?(_mysOffset*(-1.0)):_mysOffset)/2; - //_TemperatureX = _mysTemperature + _mysOffset*3.0/2.0; - _TemperatureX = _mysTemperature + _mysOffset - 2.4; //(2.4 - temperature offset because enclosure and esp8266 power disipation) - - //version formula with DewPoint - //lnval = logf(_mysHumidity/100.0); - //rapval = (aval * _mysTemperature)/(bval+_mysTemperature); - //Dp = (bval*(lnval+rapval))/(aval-lnval-rapval); - //eeval = expf((aval*Dp)/(bval+Dp))/expf((aval*_TemperatureX)/(bval+_TemperatureX)); - //_HumidityX = eeval*100.0; - - //version formula with interpolation - _HumidityX = _Humidity+(_TemperatureX-_Temperature)*((_rawHumidity-_Humidity)/(_rawTemperature-_Temperature)); - lnval = logf(_HumidityX/100.0); - rapval = (aval * _TemperatureX)/(bval+_TemperatureX); - Dp = (bval*(lnval+rapval))/(aval-lnval-rapval); - - if(_HumidityX < 0.0) - _HumidityX = 0.0; - if(_HumidityX > 100.0) - _HumidityX = 100.0; - } - else - { - lnval = logf(_Humidity/100.0); - rapval = (aval * _Temperature)/(bval+_Temperature); - Dp = (bval*(lnval+rapval))/(aval-lnval-rapval); +bool P167_data_struct::calculateValue() { + if (_model == P167_model::Vindstyrka) { + _TemperatureX = _mysTemperature + _mysOffset - 2.4f; // (2.4 - temperature offset because enclosure and esp8266 power disipation) + + + // version formula with interpolation + _HumidityX = _Humidity + (_TemperatureX - _Temperature) * ((_rawHumidity - _Humidity) / (_rawTemperature - _Temperature)); + + _DewPoint = compute_dew_point_temp(_TemperatureX, _HumidityX); + + if (_HumidityX < 0.0f) { + _HumidityX = 0.0f; + } + + if (_HumidityX > 100.0f) { + _HumidityX = 100.0f; + } + } else { + _DewPoint = compute_dew_point_temp(_Temperature, _Humidity); } - _DewPoint = Dp; return true; } @@ -1200,203 +1044,207 @@ bool P167_data_struct::calculateValue() ////////////////////////////////////////////////////////////////////////////////////////////////// // Retrieve SEN5x identification code // Sensirion_SEN5x -bool P167_data_struct::getProductName() -{ - String prodname=F(""); +bool P167_data_struct::getProductName() { + String prodname; uint8_t buffer[48]; - //writeCmd(P167_READ_PROD_NAME); - if (!readBytes(48, (uint8_t *) buffer, P167_READ_PROD_NAME_DELAY)) - { + + // writeCmd(P167_READ_PROD_NAME); + if (!readBytes(48, (uint8_t *)buffer, P167_READ_PROD_NAME_DELAY)) { return false; } - for (uint8_t i = 1; i <= 16; i++) - { - if(crc8(&buffer[i*3-3], 2) == buffer[i*3-1]) - { - if (buffer[i*3-3] < 32) + + for (uint8_t i = 1; i <= 16; ++i) { + if (calc_CRC8(&buffer[i * 3 - 3], 2) == buffer[i * 3 - 1]) { + if (buffer[i * 3 - 3] < 32) { break; - prodname+=char(buffer[i*3-3]); - if (buffer[i*3-2] < 32) + } + prodname += char(buffer[i * 3 - 3]); + + if (buffer[i * 3 - 2] < 32) { break; - prodname+=char(buffer[i*3-2]); + } + prodname += char(buffer[i * 3 - 2]); } } - _eid_productname=prodname; + _eid_productname = prodname; - String log = F("SEN5x : *** Product name: "); - log += String(prodname); - addLog(LOG_LEVEL_INFO, log); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLog(LOG_LEVEL_INFO, concat(F("SEN5x: Product name: "), prodname)); + } return true; } ////////////////////////////////////////////////////////////////////////////////////////////////// // Retrieve SEN54 Serial Number -bool P167_data_struct::getSerialNumber() -{ - String serno=F(""); +bool P167_data_struct::getSerialNumber() { + String serno; uint8_t buffer[48]; - //writeCmd(P167_READ_SERIAL_NO); - if (!readBytes(48, (uint8_t *) buffer, P167_READ_SERIAL_NO_DELAY)) - { + + // writeCmd(P167_READ_SERIAL_NO); + if (!readBytes(48, (uint8_t *)buffer, P167_READ_SERIAL_NO_DELAY)) { return false; } - for (uint8_t i = 1; i <= 16; i++) - { - if(crc8(&buffer[i*3-3], 2) == buffer[i*3-1]) - { - if (buffer[i*3-3] < 32) + + for (uint8_t i = 1; i <= 16; ++i) { + if (calc_CRC8(&buffer[i * 3 - 3], 2) == buffer[i * 3 - 1]) { + if (buffer[i * 3 - 3] < 32) { break; - serno+=char(buffer[i*3-3]); - if (buffer[i*3-2] < 32) + } + serno += char(buffer[i * 3 - 3]); + + if (buffer[i * 3 - 2] < 32) { break; - serno+=char(buffer[i*3-2]); + } + serno += char(buffer[i * 3 - 2]); } } - _eid_serialnumber=serno; + _eid_serialnumber = serno; - String log = F("SEN5x : *** Serial number: "); - log += String(serno); - addLog(LOG_LEVEL_INFO, log); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLog(LOG_LEVEL_INFO, concat(F("SEN5x: Serial number: "), serno)); + } return true; } ////////////////////////////////////////////////////////////////////////////////////////////////// // Retrieve SEN54 Firmware version from device -bool P167_data_struct::getFirmwareVersion() -{ +bool P167_data_struct::getFirmwareVersion() { uint8_t version = 0; uint8_t read_data[3]; - //writeCmd(P167_READ_FIRM_VER); - if (!readBytes(3, (uint8_t *) &read_data, P167_READ_FIRM_VER_DELAY)) - { + + // writeCmd(P167_READ_FIRM_VER); + if (!readBytes(3, (uint8_t *)&read_data, P167_READ_FIRM_VER_DELAY)) { return false; } - if( read_data[2] == crc8(&read_data[0],2) ) - { - version=read_data[0]; - } - else - { - version=0; + + if (read_data[2] == calc_CRC8(&read_data[0], 2)) { + version = read_data[0]; } - _firmware=version; + _firmware = version; - String log = F("SEN5x : *** Firmware version: "); - log += String((uint8_t)version); - addLog(LOG_LEVEL_INFO, log); + addLog(LOG_LEVEL_INFO, strformat(F("SEN5x: Firmware version: %d"), version)); return true; } ////////////////////////////////////////////////////////////////////////////////////////////////// // Retrieve SEN54 Device Status from device -bool P167_data_struct::readDeviceStatus() -{ - uint32_t value=0; - uint8_t bufferstatus[6]; - //writeCmd(P167_READ_FIRM_VER); +bool P167_data_struct::readDeviceStatus() { + uint32_t value = 0; + uint8_t bufferstatus[6]; + + // writeCmd(P167_READ_FIRM_VER); _errdevicestatus = false; - if (!readBytes(6, (uint8_t *) &bufferstatus, P167_READ_DEVICE_STATUS_DELAY)) - { + + if (!readBytes(6, (uint8_t *)&bufferstatus, P167_READ_DEVICE_STATUS_DELAY)) { _errdevicestatus = true; return false; } - String log = F("SEN5x : *** device status "); - for(int xx=0; xx<6;xx++) - { - log += String((int)bufferstatus[xx]); - log += F(" "); + String log = F("SEN5x: device status 0x"); + + for (int xx = 0; xx < 6; ++xx) { + log += strformat(F("%02x "), bufferstatus[xx]); } - - if ((crc8(&bufferstatus[0], 2) == bufferstatus[2]) && (crc8(&bufferstatus[3], 2) == bufferstatus[5])) - { - value = bufferstatus[0] << 8; - value += bufferstatus[1] << 8; - value += bufferstatus[3] << 8; - value += bufferstatus[4] << 8; + + if ((calc_CRC8(&bufferstatus[0], 2) == bufferstatus[2]) && (calc_CRC8(&bufferstatus[3], 2) == bufferstatus[5])) { + value = bufferstatus[0] << 24; + value += bufferstatus[1] << 16; + value += bufferstatus[3] << 8; + value += bufferstatus[4] << 0; _devicestatus.val = value; - } - else - { + } else { _errdevicestatus = true; } - if(_errdevicestatus == true) - { + if (_errdevicestatus) { log += F("- crc error"); _readingerrcount++; - } - else - { - log += String((bool)_devicestatus.speed); - log += String((bool)_devicestatus.autoclean); - log += String((bool)_devicestatus.gas); - log += String((bool)_devicestatus.rht); - log += String((bool)_devicestatus.laser); - log += String((bool)_devicestatus.fan); - log += F(" - pass"); + } else { + log += strformat(F(", flags: sp:%d cln:%d gas:%d rht:%d las:%d fan:%d - pass"), + _devicestatus.speed, + _devicestatus.autoclean, + _devicestatus.gas, + _devicestatus.rht, + _devicestatus.laser, + _devicestatus.fan); _readingsuccesscount++; } - addLog(LOG_LEVEL_INFO, log); + + if (_enableLogging) { + addLog(LOG_LEVEL_INFO, log); + } return !_errdevicestatus; } - -uint16_t P167_data_struct::getErrCode(bool _clear) -{ +uint16_t P167_data_struct::getErrCode(bool _clear) { uint16_t _tmp = _readingerrcode; - if (_clear == true) + + if (_clear == true) { clearErrCode(); - return (_tmp); + } + return _tmp; } -uint16_t P167_data_struct::getErrCount(bool _clear) -{ +uint16_t P167_data_struct::getErrCount(bool _clear) { uint16_t _tmp = _readingerrcount; - if (_clear == true) + + if (_clear == true) { clearErrCount(); - return (_tmp); + } + return _tmp; } -uint16_t P167_data_struct::getSuccCount(bool _clear) -{ +uint16_t P167_data_struct::getSuccCount(bool _clear) { uint16_t _tmp = _readingsuccesscount; - if (_clear == true) + + if (_clear == true) { clearSuccCount(); - return (_tmp); + } + return _tmp; } -void P167_data_struct::clearErrCode() -{ +void P167_data_struct::clearErrCode() { _readingerrcode = VIND_ERR_NO_ERROR; } -void P167_data_struct::clearErrCount() -{ +void P167_data_struct::clearErrCount() { _readingerrcount = 0; } -void P167_data_struct::clearSuccCount() -{ +void P167_data_struct::clearSuccCount() { _readingsuccesscount = 0; } - -void IRAM_ATTR P167_data_struct::checkPin_interrupt() -{ - //ISR_noInterrupts(); // s0170071: avoid nested interrups due to bouncing. - - monpinValue++; +void IRAM_ATTR P167_data_struct::checkPin_interrupt() { + monpinValue = monpinValue + 1; // volatile monpinLastTransitionTime = getMicros64(); + // Mark pin value changed monpinChanged = false; - if(monpinValue!=monpinValuelast) + + if (monpinValue != monpinValuelast) { monpinChanged = true; + } +} - //ISR_interrupts(); // enable interrupts again. +void P167_data_struct::setLogging(bool logStatus) { + _enableLogging = logStatus; +} + +void P167_data_struct::startCleaning() { + writeCmd(P167_START_FAN_CLEAN); // Don't wait for a response, as the command causes the fan to run for 10 seconds +} + +// When using interrupts we have to call the library entry point +// whenever an interrupt is triggered +void IRAM_ATTR P167_data_struct::Plugin_167_interrupt(P167_data_struct *self) { + // addLog(LOG_LEVEL_ERROR, F("********* SEN5X: interrupt apear!")); + if (self) { + self->checkPin_interrupt(); + } } -#endif // USES_P167 \ No newline at end of file +#endif // USES_P167 diff --git a/src/src/PluginStructs/P167_data_struct.h b/src/src/PluginStructs/P167_data_struct.h index 1686b351b5..795a5e4c77 100644 --- a/src/src/PluginStructs/P167_data_struct.h +++ b/src/src/PluginStructs/P167_data_struct.h @@ -1,60 +1,85 @@ ////////////////////////////////////////////////////////////////////////////////////////////////// -// P167 device class for IKEA Vindstyrka SEN54 temperature , humidity and air quality sensors +// P167 device class for IKEA Vindstyrka SEN54 temperature , humidity and air quality sensors // See datasheet https://sensirion.com/media/documents/6791EFA0/62A1F68F/Sensirion_Datasheet_Environmental_Node_SEN5x.pdf // and info about extra request https://sensirion.com/media/documents/2B6FC1F3/6409E74A/PS_AN_Read_RHT_VOC_and_NOx_RAW_signals_D1.pdf // Based upon code from Rob Tillaart, Viktor Balint, https://github.com/RobTillaart/SHT2x -// Rewritten and adapted for ESPeasy by andibaciu -// 2023-06-20 Initial version by andibaciu +// Rewritten and adapted for ESPeasy by andibaciu and tonhuisman +// changelog in _P167_Vindstyrka.ino ////////////////////////////////////////////////////////////////////////////////////////////////// #include "../../_Plugin_Helper.h" #include "../ESPEasyCore/ESPEasyGPIO.h" #ifdef USES_P167 -#ifdef LIMIT_BUILD_SIZE -#define PLUGIN_167_DEBUG false -#else -#define PLUGIN_167_DEBUG false // set to true for extra log info in the debug -#endif +# ifdef LIMIT_BUILD_SIZE +# define PLUGIN_167_DEBUG false +# else // ifdef LIMIT_BUILD_SIZE +# define PLUGIN_167_DEBUG true // set to true for extra log info in the debug +# endif // ifdef LIMIT_BUILD_SIZE -// Vindstyrka device properties -//#define P167_I2C_ADDRESS_DFLT 0x69 +// ------------------------------------------------------------------------------ +# define VIND_ERR_NO_ERROR 0 // no error +# define VIND_ERR_CRC_ERROR 1 // crc error +# define VIND_ERR_WRONG_BYTES 2 // bytes b0,b1 or b2 wrong +# define VIND_ERR_NOT_ENOUGHT_BYTES 3 // not enough bytes from sdm +# define VIND_ERR_TIMEOUT 4 // timeout +// ------------------------------------------------------------------------------ + +// Make accessing specific parameters more readable in the code +# define P167_ENABLE_LOG PCONFIG(0) +# define P167_ENABLE_LOG_LABEL PCONFIG_LABEL(0) +# define P167_MODEL PCONFIG(1) +# define P167_MODEL_LABEL PCONFIG_LABEL(1) +# define P167_MON_SCL_PIN PCONFIG(2) +# define P167_QUERY1 PCONFIG(3) +# define P167_QUERY2 PCONFIG(4) +# define P167_QUERY3 PCONFIG(5) +# define P167_QUERY4 PCONFIG(6) + + +# define P167_I2C_ADDRESS_DFLT 0x69 +# define P167_MON_SCL_PIN_DFLT 13 +# define P167_MODEL_DFLT P167_MODEL_VINDSTYRKA // Vindstyrka or SEN54 or SEN55 +# define P167_QUERY1_DFLT 0 // Temperature (C) +# define P167_QUERY2_DFLT 1 // Humidity (%) +# define P167_QUERY3_DFLT 5 // PM2.5 (ug/m3) +# define P167_QUERY4_DFLT 2 // tVOC (index) + + +# define P167_NR_OUTPUT_OPTIONS 10 +# define P167_QUERY1_CONFIG_POS 3 +# define P167_SENSOR_TYPE_INDEX (P167_QUERY1_CONFIG_POS + VARS_PER_TASK) +# define P167_NR_OUTPUT_VALUES getValueCountFromSensorType(static_cast(PCONFIG(P167_SENSOR_TYPE_INDEX))) +# define P167_MAX_ATTEMPT 3 // Number of tentative before declaring NAN value +# define P167_VALUE_COUNT 9 // Number of available values -//------------------------------------------------------------------------------ -#define VIND_ERR_NO_ERROR 0 // no error -#define VIND_ERR_CRC_ERROR 1 // crc error -#define VIND_ERR_WRONG_BYTES 2 // bytes b0,b1 or b2 wrong -#define VIND_ERR_NOT_ENOUGHT_BYTES 3 // not enough bytes from sdm -#define VIND_ERR_TIMEOUT 4 // timeout -//------------------------------------------------------------------------------ ////////////////////////////////////////////////////////////////////////////////////////////////// -// Access to the Vindstyrka device is mainly by sequencing a Finate State Machine -enum class P167_state { - Uninitialized = 0, // Initial state, unknown status of sensor device - Wait_for_reset, // Reset being performed - Read_firm_version, // Reading firmware version - Read_prod_name, // Reading production - Read_serial_no, // Reading serial number - Write_user_reg, // Write the user register - Initialized, // Initialization completed - Ready, // Aquisition request is pending, ready to measure - Wait_for_start_meas, // Start measurement started - Wait_for_read_flag, // Read meas flag started - Wait_for_read_meas, // Read meas started - Wait_for_read_raw_meas, // RAW Read meas started - Wait_for_read_raw_MYS_meas, // RAW Read meas MYSTERY started - Wait_for_read_status, // Read status - cmdSTARTmeas, // send command START meas to leave SEN5x ready flag for Vindstyrka - IDLE, // Sensor device in IDLE mode - New_Values_Available, // Acqusition finished, new data available - Error // Sensor device cannot be accessed or in error +// Access to the Vindstyrka device is mainly by sequencing a Final State Machine +enum class P167_state : uint8_t { + Uninitialized = 0, // Initial state, unknown status of sensor device + Wait_for_reset, // Reset being performed + Read_firm_version, // Reading firmware version + Read_prod_name, // Reading production + Read_serial_no, // Reading serial number + Write_user_reg, // Write the user register + Initialized, // Initialization completed + Ready, // Aquisition request is pending, ready to measure + Wait_for_start_meas, // Start measurement started + Wait_for_read_flag, // Read meas flag started + Wait_for_read_meas, // Read meas started + Wait_for_read_raw_meas, // RAW Read meas started + Wait_for_read_raw_MYS_meas, // RAW Read meas MYSTERY started + Wait_for_read_status, // Read status + cmdSTARTmeas, // send command START meas to leave SEN5x ready flag for Vindstyrka + IDLE, // Sensor device in IDLE mode + New_Values_Available, // Acqusition finished, new data available + Error // Sensor device cannot be accessed or in error }; -enum param_statusinfo -{ +enum class P167_statusinfo : uint8_t { sensor_speed = 0, sensor_autoclean, sensor_gas, @@ -63,76 +88,101 @@ enum param_statusinfo sensor_fan }; +enum class P167_model : uint8_t { + Vindstyrka = 0u, + SEN54 = 1u, + SEN55 = 2u, +}; + + +# define P167_MODEL_VINDSTYRKA static_cast(P167_model::Vindstyrka) +# define P167_MODEL_SEN54 static_cast(P167_model::SEN54) +# define P167_MODEL_SEN55 static_cast(P167_model::SEN55) + +const __FlashStringHelper* toString(P167_model model); + +const __FlashStringHelper* P167_getQueryString(uint8_t query); +const __FlashStringHelper* P167_getQueryValueString(uint8_t query); + +// unsigned int P167_getRegister(uint8_t query, +// P167_model model); +// float P167_readVal(uint8_t query, +// uint8_t node, +// P167_model model); ////////////////////////////////////////////////////////////////////////////////////////////////// // ESPeasy standard PluginTaskData structure for this plugin -//struct P167_data_struct : public PluginTaskData_base -class P167_data_struct -{ - +struct P167_data_struct : public PluginTaskData_base { public: + P167_data_struct(); - //virtual ~P167_data_struct(); + + // virtual ~P167_data_struct(); ~P167_data_struct(); - void checkPin_interrupt(void); + void checkPin_interrupt(void); + static void Plugin_167_interrupt(P167_data_struct *self); ///////////////////////////////////////////////////////// // This method runs the FSM step by step on each call // Returns true when a stable state is reached - bool update(); - bool monitorSCL(); + bool update(); + bool monitorSCL(); ///////////////////////////////////////////////////////// // (re)configure the device properties // This will result in resetting and reloading the device - bool setupDevice(uint8_t i2caddr); - bool setupModel(uint8_t model); - bool setupMonPin(uint8_t monpin); - void enableInterrupt_monpin(void); - void disableInterrupt_monpin(void); - + bool setupDevice(uint8_t i2caddr); + bool setupModel(P167_model model); + bool setupMonPin(int16_t monpin); + void disableInterrupt_monpin(void); + + void setLogging(bool logStatus); + ///////////////////////////////////////////////////////// // check sensor is reachable over I2C - bool isConnected() const; + bool isConnected() const; ///////////////////////////////////////////////////////// - bool newValues() const; + bool newValues() const; ///////////////////////////////////////////////////////// - bool inError() const; + bool inError() const; ///////////////////////////////////////////////////////// // Reset the FSM to initial state - bool reset(); + bool reset(); ///////////////////////////////////////////////////////// // Trigger a measurement cycle // Only perform the measurements with big interval to prevent the sensor from warming up. - bool startMeasurements(); - - bool getStatusInfo(param_statusinfo param); + bool startMeasurements(); + + bool getStatusInfo(P167_statusinfo param); + ///////////////////////////////////////////////////////// // Electronic Identification Code // Sensirion_Humidity_SHT2x_Electronic_Identification_Code_V1.1.pdf // Electronic ID bytes - bool getEID(String &eid_productname, String &eid_serialnumber, uint8_t &firmware) const; + bool getEID(String & eid_productname, + String & eid_serialnumber, + uint8_t& firmware) const; ///////////////////////////////////////////////////////// // Temperature, humidity, DewPoint, PMxpy retrieval // Note: values are fetched from memory and reflect latest succesful read cycle - float getRequestedValue(uint8_t request) const; + float getRequestedValue(uint8_t request) const; - uint16_t getErrCode(bool _clear = false); //return last errorcode (optional clear this value, default false) - uint16_t getErrCount(bool _clear = false); //return total errors count (optional clear this value, default false) - uint16_t getSuccCount(bool _clear = false); //return total success count (optional clear this value, default false) - void clearErrCode(); //clear last errorcode - void clearErrCount(); //clear total errors count - void clearSuccCount(); //clear total success count + uint16_t getErrCode(bool _clear = false); // return last errorcode (optional clear this value, default false) + uint16_t getErrCount(bool _clear = false); // return total errors count (optional clear this value, default false) + uint16_t getSuccCount(bool _clear = false); // return total success count (optional clear this value, default false) + void clearErrCode(); // clear last errorcode + void clearErrCount(); // clear total errors count + void clearSuccCount(); // clear total success count + void startCleaning(); // Start a fan cleaning session. -//protected: private: union devicestatus @@ -140,83 +190,88 @@ class P167_data_struct uint32_t val; struct { - uint16_t dummy1:10; - bool speed; - bool dummy2; - bool autoclean; - uint16_t dummy3:11; - bool gas; - bool rht; - bool laser; - bool fan; - uint16_t dummy4:4; + uint32_t dummy4 : 4; // bit 0..3 + uint32_t fan : 1; // bit 4 + uint32_t laser : 1; // bit 5 + uint32_t rht : 1; // bit 6 + uint32_t gas : 1; // bit 7 + uint32_t dummy3 : 11; // bit 8..18 + uint32_t autoclean : 1; // bit 19 + uint32_t dummy2 : 1; // bit 20 + uint32_t speed : 1; // bit 21 + uint32_t dummy1 : 10; // bit 22..31 }; }; - devicestatus _devicestatus; - P167_state _state; - - uint8_t crc8(const uint8_t *data, uint8_t len); - bool writeCmd(uint16_t cmd); - bool writeCmd(uint16_t cmd, uint8_t value); - bool writeCmd(uint16_t cmd, uint8_t length, uint8_t *buffer); - bool readBytes(uint8_t n, uint8_t *val, uint8_t maxDuration); - - bool readMeasValue(); - bool readMeasRawValue(); - bool readMeasRawMYSValue(); - bool readDataRdyFlag(); - bool readDeviceStatus(); - bool calculateValue(); - - bool getProductName(); - bool getSerialNumber(); - bool getFirmwareVersion(); - - - float _Humidity; // Humidity as fetched from the device [bits] - float _HumidityX; // Humidity as calculated - float _Temperature; // Temperature as fetched from the device [bits] - float _TemperatureX; // Temperature as calculated - float _DewPoint; // DewPoint as calculated - float _rawHumidity; // Humidity as fetched from the device without compensation[bits] - float _rawTemperature; // Temperature as fetched from the device without compensation[bits] - float _mysHumidity; // Humidity as fetched from the device without compensation[bits] - float _mysTemperature; // Temperature as fetched from the device without compensation[bits] - float _tVOC; // tVOC as fetched from the device[bits] - float _NOx; // NOx as fetched from the device[bits] - float _rawtVOC; // tVOC as fetched from the device without compensation[bits] - float _rawNOx; // NOx as fetched from the device without compensation[bits] - float _mysOffset; // Temperature Offset fetched from the device[bits] - float _PM1p0; // PM1.0 as fetched from the device[bits] - float _PM2p5; // PM2.5 as fetched from the device[bits] - float _PM4p0; // PM4.0 as fetched from the device[bits] - float _PM10p0; // PM10.0 as fetched from the device[bits] - uint8_t _model; // Selected sensor model - uint8_t _i2caddr; // Programmed I2C address - uint8_t _monpin; // Pin to monitor I2C SCL to find when VindStyrka finish i2c communication - unsigned long _last_action_started; // Timestamp for last action that takes processing time - uint16_t _errCount; // Number of errors since last successful access - String _eid_productname; // Electronic Device ID - Product Name, read at initialization - String _eid_serialnumber; // Electronic Device ID - Serial Number, read at initialization - uint8_t _firmware; // Firmware version numer, read at initialization - uint8_t _userreg; // TODO debugging only - uint16_t _readingerrcode = VIND_ERR_NO_ERROR; // 4 = timeout; 3 = not enough bytes; 2 = number of bytes OK but bytes b0,b1 or b2 wrong, 1 = crc error - uint16_t _readingerrcount = 0; // total errors couter - uint32_t _readingsuccesscount = 0; // total success couter - bool _errmeas; - bool _errmeasraw; - bool _errmeasrawmys; - bool _errdevicestatus; - uint8_t stepMonitoring; // step for Monitorin SCL pin algorithm - bool startMonitoringFlag; // flag to START/STOP Monitoring algorithm - bool statusMonitoring; // flag for status return from Monitoring algorithm - unsigned long lastSCLLowTransitionMonitoringTime; // last time when SCL i2c pin rising - - volatile uint32_t monpinValue = 0; - volatile uint32_t monpinValuelast = 0; - volatile uint8_t monpinChanged = 0; - volatile uint64_t monpinLastTransitionTime = 0; - + devicestatus _devicestatus; + P167_state _state; + + bool writeCmd(uint16_t cmd); + bool writeCmd(uint16_t cmd, + uint8_t value); + bool writeCmd(uint16_t cmd, + uint8_t length, + uint8_t *buffer); + bool readBytes(uint8_t n, + uint8_t *val, + uint8_t maxDuration); + + bool readMeasValue(); + bool readMeasRawValue(); + bool readMeasRawMYSValue(); + bool readDataRdyFlag(); + bool readDeviceStatus(); + bool calculateValue(); + + bool getProductName(); + bool getSerialNumber(); + bool getFirmwareVersion(); + + + float _Humidity = 0.0f; // Humidity as fetched from the device [bits] + float _HumidityX = 0.0f; // Humidity as calculated + float _Temperature = 0.0f; // Temperature as fetched from the device [bits] + float _TemperatureX = 0.0f; // Temperature as calculated + float _DewPoint = 0.0f; // DewPoint as calculated + float _rawHumidity = 0.0f; // Humidity as fetched from the device without compensation[bits] + float _rawTemperature = 0.0f; // Temperature as fetched from the device without compensation[bits] + float _mysHumidity = 0.0f; // Humidity as fetched from the device without compensation[bits] + float _mysTemperature = 0.0f; // Temperature as fetched from the device without compensation[bits] + float _tVOC = 0.0f; // tVOC as fetched from the device[bits] + float _NOx = 0.0f; // NOx as fetched from the device[bits] + float _rawtVOC = 0.0f; // tVOC as fetched from the device without compensation[bits] + float _rawNOx = 0.0f; // NOx as fetched from the device without compensation[bits] + float _mysOffset = 0.0f; // Temperature Offset fetched from the device[bits] + float _PM1p0 = 0.0f; // PM1.0 as fetched from the device[bits] + float _PM2p5 = 0.0f; // PM2.5 as fetched from the device[bits] + float _PM4p0 = 0.0f; // PM4.0 as fetched from the device[bits] + float _PM10p0 = 0.0f; // PM10.0 as fetched from the device[bits] + P167_model _model = P167_model::Vindstyrka; // Selected sensor model + uint8_t _i2caddr = 0; // Programmed I2C address + uint8_t _monpin = 0; // Pin to monitor I2C SCL to find when VindStyrka finish i2c communication + unsigned long _last_action_started = 0; // Timestamp for last action that takes processing time + uint16_t _errCount = 0; // Number of errors since last successful access + String _eid_productname; // Electronic Device ID - Product Name, read at initialization + String _eid_serialnumber; // Electronic Device ID - Serial Number, read at initialization + uint8_t _firmware = 0; // Firmware version numer, read at initialization + uint8_t _userreg = 0; // TODO debugging only + uint16_t _readingerrcode = VIND_ERR_NO_ERROR; // 4 = timeout; 3 = not enough bytes; 2 = number of bytes OK but bytes b0,b1 + // or b2 wrong, 1 = crc error + uint16_t _readingerrcount = 0; // total errors couter + uint32_t _readingsuccesscount = 0; // total success couter + uint8_t stepMonitoring = 0; // step for Monitorin SCL pin algorithm + bool _errmeas = false; + bool _errmeasraw = false; + bool _errmeasrawmys = false; + bool _errdevicestatus = false; + bool startMonitoringFlag = false; // flag to START/STOP Monitoring algorithm + bool statusMonitoring = false; // flag for status return from Monitoring algorithm + bool _enableLogging = false; // flag for enabling some technical logging + unsigned long lastSCLLowTransitionMonitoringTime = 0; // last time when SCL i2c pin rising + + volatile uint32_t monpinValue = 0; + volatile uint32_t monpinValuelast = 0; + volatile uint8_t monpinChanged = 0; + volatile uint64_t monpinLastTransitionTime = 0; }; -#endif // USES_P167 \ No newline at end of file +#endif // USES_P167 From 5112a8e2600c4b988957705d6640d1bc32b113a3 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 22 Apr 2024 23:03:20 +0200 Subject: [PATCH 086/113] [P167] Minor corrections and some cleanup --- src/_P167_Vindstyrka.ino | 9 ++++++++- src/src/PluginStructs/P167_data_struct.cpp | 1 - src/src/PluginStructs/P167_data_struct.h | 9 +-------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/_P167_Vindstyrka.ino b/src/_P167_Vindstyrka.ino index 8906b5e413..4da26aaa37 100644 --- a/src/_P167_Vindstyrka.ino +++ b/src/_P167_Vindstyrka.ino @@ -335,7 +335,14 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) if (nullptr != Plugin_167_SEN) { for (uint8_t v = 0; v < P167_VALUE_COUNT && !success; ++v) { if (string.equalsIgnoreCase(P167_getQueryValueString(v))) { - string = Plugin_167_SEN->getRequestedValue(v); + # ifndef BUILD_NO_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + addLog(LOG_LEVEL_DEBUG, strformat(F("SEN5x: Get Config Value: %s: %.2f"), + string.c_str(), Plugin_167_SEN->getRequestedValue(v))); + } + # endif // ifndef BUILD_NO_DEBUG + string = toString(Plugin_167_SEN->getRequestedValue(v)); success = true; break; } diff --git a/src/src/PluginStructs/P167_data_struct.cpp b/src/src/PluginStructs/P167_data_struct.cpp index 6991bbf5f1..94eb882e09 100644 --- a/src/src/PluginStructs/P167_data_struct.cpp +++ b/src/src/PluginStructs/P167_data_struct.cpp @@ -427,7 +427,6 @@ bool P167_data_struct::update() { if (!readDeviceStatus()) { _errCount++; - // _state = P167_state::Uninitialized; // Lost connection _state = P167_state::cmdSTARTmeas; } else { _last_action_started = millis(); diff --git a/src/src/PluginStructs/P167_data_struct.h b/src/src/PluginStructs/P167_data_struct.h index 795a5e4c77..601a592b30 100644 --- a/src/src/PluginStructs/P167_data_struct.h +++ b/src/src/PluginStructs/P167_data_struct.h @@ -47,7 +47,7 @@ # define P167_QUERY4_DFLT 2 // tVOC (index) -# define P167_NR_OUTPUT_OPTIONS 10 +# define P167_NR_OUTPUT_OPTIONS 9 # define P167_QUERY1_CONFIG_POS 3 # define P167_SENSOR_TYPE_INDEX (P167_QUERY1_CONFIG_POS + VARS_PER_TASK) # define P167_NR_OUTPUT_VALUES getValueCountFromSensorType(static_cast(PCONFIG(P167_SENSOR_TYPE_INDEX))) @@ -104,12 +104,6 @@ const __FlashStringHelper* toString(P167_model model); const __FlashStringHelper* P167_getQueryString(uint8_t query); const __FlashStringHelper* P167_getQueryValueString(uint8_t query); -// unsigned int P167_getRegister(uint8_t query, -// P167_model model); -// float P167_readVal(uint8_t query, -// uint8_t node, -// P167_model model); - ////////////////////////////////////////////////////////////////////////////////////////////////// // ESPeasy standard PluginTaskData structure for this plugin @@ -118,7 +112,6 @@ struct P167_data_struct : public PluginTaskData_base { P167_data_struct(); - // virtual ~P167_data_struct(); ~P167_data_struct(); void checkPin_interrupt(void); From 8b7392f6067f635832116393ea3b1f5d374d594d Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 25 Apr 2024 22:06:18 +0200 Subject: [PATCH 087/113] [P167] Add to I2C Scanner --- src/src/WebServer/I2C_Scanner.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/WebServer/I2C_Scanner.cpp b/src/src/WebServer/I2C_Scanner.cpp index e9428ad504..b1151a0644 100644 --- a/src/src/WebServer/I2C_Scanner.cpp +++ b/src/src/WebServer/I2C_Scanner.cpp @@ -333,7 +333,7 @@ String getKnownI2Cdevice(uint8_t address) { result += F("MPU6050,DS1307,DS3231,PCF8523,ITG3205,CDM7160"); break; case 0x69: - result += F("ITG3205,CDM7160"); + result += F("ITG3205,CDM7160,SEN5x"); break; case 0x70: result += F("Adafruit Motorshield v2 (Catchall),HT16K33,TCA9543a/6a/8a I2C multiplexer,PCA9540 I2C multiplexer"); From 63daf2689aafd14c4ef2c52dc744ad01358264e6 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 25 Apr 2024 22:06:42 +0200 Subject: [PATCH 088/113] [P167] Update EasyColorCode docs --- static/espeasy.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/static/espeasy.js b/static/espeasy.js index 2795f95294..ae9d78d9a5 100644 --- a/static/espeasy.js +++ b/static/espeasy.js @@ -110,6 +110,8 @@ var commonPlugins = [ "ld2410", "ld2410,factoryreset", "ld2410,logall", //P166 "gp8403", "gp8403,volt,", "gp8403,mvolt,", "gp8403,range,", "gp8403,preset,", "gp8403,init,", + //P167 + "sen5x", "sen5x,reset", ]; var pluginDispKind = [ //P095 From 553079a659c57d1f050bf6d72c3948a796c16b99 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 30 Apr 2024 23:43:46 +0200 Subject: [PATCH 089/113] [P167] Fix storing setting to enable tech. logging --- src/_P167_Vindstyrka.ino | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_P167_Vindstyrka.ino b/src/_P167_Vindstyrka.ino index 4da26aaa37..40ff9eb355 100644 --- a/src/_P167_Vindstyrka.ino +++ b/src/_P167_Vindstyrka.ino @@ -219,7 +219,7 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) )); } - addFormCheckBox(F("Technical logging"), P167_ENABLE_LOG_LABEL, P167_ENABLE_LOG); + addFormCheckBox(F("Technical logging"), P167_ENABLE_LOG_LABEL, P167_ENABLE_LOG == 1); success = true; break; } @@ -234,7 +234,7 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) sensorTypeHelper_saveOutputSelector(event, pconfigIndex, i, P167_getQueryValueString(choice)); } P167_MODEL = getFormItemInt(P167_MODEL_LABEL); - P167_ENABLE_LOG = isFormItemChecked(P167_ENABLE_LOG_LABEL); + P167_ENABLE_LOG = isFormItemChecked(P167_ENABLE_LOG_LABEL) ? 1 : 0; if (P167_MODEL == P167_MODEL_VINDSTYRKA) { P167_MON_SCL_PIN = getFormItemInt(F("taskdevicepin3")); @@ -259,7 +259,7 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) if (Plugin_167_SEN != nullptr) { Plugin_167_SEN->setupModel(static_cast(P167_MODEL)); Plugin_167_SEN->setupDevice(P167_I2C_ADDRESS_DFLT); - Plugin_167_SEN->setLogging(P167_ENABLE_LOG); + Plugin_167_SEN->setLogging(P167_ENABLE_LOG == 1); if (P167_MODEL == P167_MODEL_VINDSTYRKA) { Plugin_167_SEN->setupMonPin(P167_MON_SCL_PIN); From bba9669bfc2e156ca0629ecc71cbdd11c8ad52d5 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 2 May 2024 20:53:36 +0200 Subject: [PATCH 090/113] [P167] Use fixed label for model and tech. logging --- src/_P167_Vindstyrka.ino | 6 +++--- src/src/PluginStructs/P167_data_struct.h | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/_P167_Vindstyrka.ino b/src/_P167_Vindstyrka.ino index 40ff9eb355..4da26aaa37 100644 --- a/src/_P167_Vindstyrka.ino +++ b/src/_P167_Vindstyrka.ino @@ -219,7 +219,7 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) )); } - addFormCheckBox(F("Technical logging"), P167_ENABLE_LOG_LABEL, P167_ENABLE_LOG == 1); + addFormCheckBox(F("Technical logging"), P167_ENABLE_LOG_LABEL, P167_ENABLE_LOG); success = true; break; } @@ -234,7 +234,7 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) sensorTypeHelper_saveOutputSelector(event, pconfigIndex, i, P167_getQueryValueString(choice)); } P167_MODEL = getFormItemInt(P167_MODEL_LABEL); - P167_ENABLE_LOG = isFormItemChecked(P167_ENABLE_LOG_LABEL) ? 1 : 0; + P167_ENABLE_LOG = isFormItemChecked(P167_ENABLE_LOG_LABEL); if (P167_MODEL == P167_MODEL_VINDSTYRKA) { P167_MON_SCL_PIN = getFormItemInt(F("taskdevicepin3")); @@ -259,7 +259,7 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) if (Plugin_167_SEN != nullptr) { Plugin_167_SEN->setupModel(static_cast(P167_MODEL)); Plugin_167_SEN->setupDevice(P167_I2C_ADDRESS_DFLT); - Plugin_167_SEN->setLogging(P167_ENABLE_LOG == 1); + Plugin_167_SEN->setLogging(P167_ENABLE_LOG); if (P167_MODEL == P167_MODEL_VINDSTYRKA) { Plugin_167_SEN->setupMonPin(P167_MON_SCL_PIN); diff --git a/src/src/PluginStructs/P167_data_struct.h b/src/src/PluginStructs/P167_data_struct.h index 601a592b30..66d260330f 100644 --- a/src/src/PluginStructs/P167_data_struct.h +++ b/src/src/PluginStructs/P167_data_struct.h @@ -28,9 +28,9 @@ // Make accessing specific parameters more readable in the code # define P167_ENABLE_LOG PCONFIG(0) -# define P167_ENABLE_LOG_LABEL PCONFIG_LABEL(0) +# define P167_ENABLE_LOG_LABEL F("enlg") # define P167_MODEL PCONFIG(1) -# define P167_MODEL_LABEL PCONFIG_LABEL(1) +# define P167_MODEL_LABEL F("mdl") # define P167_MON_SCL_PIN PCONFIG(2) # define P167_QUERY1 PCONFIG(3) # define P167_QUERY2 PCONFIG(4) From 827966cff312c8054755edb06da4cdd9e8df4bf5 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 4 May 2024 15:02:50 +0200 Subject: [PATCH 091/113] [AdaGFX_Helper] Fix typo in `default` font definition --- src/src/Helpers/AdafruitGFX_helper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index 1418a8d441..f90a833df2 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -987,7 +987,7 @@ struct tFontArgs { /* *INDENT-OFF* */ constexpr tFontArgs fontargs[] = { - { nullptr, 9, 6, 0, false, 0u }, + { nullptr, 6, 9, 0, false, 0u }, { &Seven_Segment24pt7b, 21, 42, 35, true, 1u }, { &Seven_Segment18pt7b, 16, 33, 26, true, 2u }, { &FreeSans9pt7b, 10, 16, 12, false, 3u }, From c743593b106727c72b77d4e0fc38f5fda6710892 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 4 May 2024 19:17:46 +0200 Subject: [PATCH 092/113] [P116] Add Default font selection if AdafruitGFX_Helper fonts are included in build --- src/_P116_ST77xx.ino | 15 ++++++++++++++- src/src/PluginStructs/P116_data_struct.cpp | 16 ++++++++++++++-- src/src/PluginStructs/P116_data_struct.h | 13 +++++++++++-- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/_P116_ST77xx.ino b/src/_P116_ST77xx.ino index 2ca9433685..1931858ce6 100644 --- a/src/_P116_ST77xx.ino +++ b/src/_P116_ST77xx.ino @@ -8,6 +8,7 @@ // History: +// 2024-05-04 tonhuisman: Add Default font selection setting, if AdafruitGFX_Helper fonts are included // 2024-03-17 tonhuisman: Add support for another alternative initialization for ST7735 displays, as the display controller // used on the LilyGO TTGO T-Display (16 MB) seems to be a ST7735, despite being documented as ST7789 // By default (also) only enabled on ESP32 builds @@ -197,6 +198,10 @@ boolean Plugin_116(uint8_t function, struct EventStruct *event, String& string) AdaGFXFormTextPrintMode(F("mode"), P116_CONFIG_FLAG_GET_MODE); + # if ADAGFX_FONTS_INCLUDED + AdaGFXFormDefaultFont(F("deffont"), P116_CONFIG_DEFAULT_FONT); + # endif // if ADAGFX_FONTS_INCLUDED + AdaGFXFormFontScaling(F("fontscale"), P116_CONFIG_FLAG_GET_FONTSCALE); addFormCheckBox(F("Clear display on exit"), F("clearOnExit"), bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_CLEAR_ON_EXIT)); @@ -266,6 +271,9 @@ boolean Plugin_116(uint8_t function, struct EventStruct *event, String& string) P116_CONFIG_DISPLAY_TIMEOUT = getFormItemInt(F("timer")); P116_CONFIG_BACKLIGHT_PIN = getFormItemInt(F("backlight")); P116_CONFIG_BACKLIGHT_PERCENT = getFormItemInt(F("backpercentage")); + # if ADAGFX_FONTS_INCLUDED + P116_CONFIG_DEFAULT_FONT = getFormItemInt(F("deffont")); + # endif // if ADAGFX_FONTS_INCLUDED uint32_t lSettings = 0; bitWrite(lSettings, P116_CONFIG_FLAG_NO_WAKE, !isFormItemChecked(F("NoDisplay"))); // Bit 0 NoDisplayOnReceivingText, @@ -340,7 +348,12 @@ boolean Plugin_116(uint8_t function, struct EventStruct *event, String& string) P116_CONFIG_FLAG_GET_CMD_TRIGGER)), P116_CONFIG_GET_COLOR_FOREGROUND, P116_CONFIG_GET_COLOR_BACKGROUND, - bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_BACK_FILL) == 0)); + bitRead(P116_CONFIG_FLAGS, P116_CONFIG_FLAG_BACK_FILL) == 0 + # if ADAGFX_FONTS_INCLUDED + , + P116_CONFIG_DEFAULT_FONT + # endif // if ADAGFX_FONTS_INCLUDED + )); P116_data_struct *P116_data = static_cast(getPluginTaskData(event->TaskIndex)); success = (nullptr != P116_data) && P116_data->plugin_init(event); // Start the display diff --git a/src/src/PluginStructs/P116_data_struct.cpp b/src/src/PluginStructs/P116_data_struct.cpp index ce5aa3c43b..46fc4ab86b 100644 --- a/src/src/PluginStructs/P116_data_struct.cpp +++ b/src/src/PluginStructs/P116_data_struct.cpp @@ -106,10 +106,18 @@ P116_data_struct::P116_data_struct(ST77xx_type_e device, String commandTrigger, uint16_t fgcolor, uint16_t bgcolor, - bool textBackFill) + bool textBackFill + # if ADAGFX_FONTS_INCLUDED + , + const uint8_t defaultFontId + # endif // if ADAGFX_FONTS_INCLUDED + ) : _device(device), _rotation(rotation), _fontscaling(fontscaling), _textmode(textmode), _backlightPin(backlightPin), _backlightPercentage(backlightPercentage), _displayTimer(displayTimer), _displayTimeout(displayTimer), _commandTrigger(commandTrigger), _fgcolor(fgcolor), _bgcolor(bgcolor), _textBackFill(textBackFill) + # if ADAGFX_FONTS_INCLUDED + , _defaultFontId(defaultFontId) + # endif // if ADAGFX_FONTS_INCLUDED { _commandTrigger.toLowerCase(); _commandTriggerCmd = concat(_commandTrigger, F("cmd")); @@ -255,7 +263,11 @@ bool P116_data_struct::plugin_init(struct EventStruct *event) { _fgcolor, _bgcolor, true, - _textBackFill); + _textBackFill + # if ADAGFX_FONTS_INCLUDED + , _defaultFontId + # endif // if ADAGFX_FONTS_INCLUDED + ); if (nullptr != gfxHelper) { displayOnOff(true); diff --git a/src/src/PluginStructs/P116_data_struct.h b/src/src/PluginStructs/P116_data_struct.h index 675102456e..83ce82d13a 100644 --- a/src/src/PluginStructs/P116_data_struct.h +++ b/src/src/PluginStructs/P116_data_struct.h @@ -43,6 +43,7 @@ # define P116_CONFIG_TYPE PCONFIG(2) // Type of device # define P116_CONFIG_BACKLIGHT_PIN PCONFIG(3) // Backlight pin # define P116_CONFIG_BACKLIGHT_PERCENT PCONFIG(4) // Backlight percentage +# define P116_CONFIG_DEFAULT_FONT PCONFIG(5) // Default font # define P116_CONFIG_COLORS PCONFIG_ULONG(3) // 2 Colors fit in 1 long # define P116_CONFIG_FLAGS PCONFIG_ULONG(0) // All flags @@ -130,8 +131,13 @@ struct P116_data_struct : public PluginTaskData_base { String commandTrigger, uint16_t fgcolor = ADAGFX_WHITE, uint16_t bgcolor = ADAGFX_BLACK, - bool textBackFill = true); - P116_data_struct() = delete; + bool textBackFill = true + # if ADAGFX_FONTS_INCLUDED + , + const uint8_t defaultFontId = 0 + # endif // if ADAGFX_FONTS_INCLUDED + ); + P116_data_struct() = delete; virtual ~P116_data_struct(); bool plugin_init(struct EventStruct *event); @@ -185,6 +191,9 @@ struct P116_data_struct : public PluginTaskData_base { uint16_t _fgcolor; uint16_t _bgcolor; bool _textBackFill; + # if ADAGFX_FONTS_INCLUDED + uint8_t _defaultFontId; + # endif // if ADAGFX_FONTS_INCLUDED String _commandTriggerCmd; From 3d4c5d45b75ba276d46a01015cf4678d67762e53 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 5 May 2024 17:01:15 +0200 Subject: [PATCH 093/113] [P167] Add command `sen5x,techlog,<0|1>` --- src/_P167_Vindstyrka.ino | 6 ++++++ static/espeasy.js | 2 +- static/espeasy.min.js | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/_P167_Vindstyrka.ino b/src/_P167_Vindstyrka.ino index 4da26aaa37..d1daf1f0ee 100644 --- a/src/_P167_Vindstyrka.ino +++ b/src/_P167_Vindstyrka.ino @@ -6,6 +6,7 @@ // ####################################################################################################### /** Changelog: + * 2024-05-05 tonhuisman: Add subcommand sen5x,techlog,<1|0> to enable/disable Technical logging option. 0 = Off, any other value is on * 2024-04-20 tonhuisman: Replace dewpoint calculation by standard calculation, fix issue with status bits, reduce strings * Remove unneeded code and variables, move most defines to P167_data_struct.h * Implement Get Config Value to retrieve all available values from a single instance @@ -365,6 +366,11 @@ boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) if (equals(subcmd, F("startclean"))) { Plugin_167_SEN->startCleaning(); success = true; + } else + if (equals(subcmd, F("techlog"))) { + P167_ENABLE_LOG = event->Par2 == 0 ? 0 : 1; + Plugin_167_SEN->setLogging(P167_ENABLE_LOG); + success = true; } } } diff --git a/static/espeasy.js b/static/espeasy.js index 571609ab21..38d82da502 100644 --- a/static/espeasy.js +++ b/static/espeasy.js @@ -115,7 +115,7 @@ var commonPlugins = [ //P166 "gp8403", "gp8403,volt,", "gp8403,mvolt,", "gp8403,range,", "gp8403,preset,", "gp8403,init,", //P167 - "sen5x", "sen5x,reset", + "sen5x", "sen5x,reset", "sen5x,techlog,", ]; var pluginDispKind = [ //P095 diff --git a/static/espeasy.min.js b/static/espeasy.min.js index 313f61be4d..7ec46a6537 100644 --- a/static/espeasy.min.js +++ b/static/espeasy.min.js @@ -1 +1 @@ -var rEdit,commonAtoms=["And","Or"],commonKeywords=["If","Else","Elseif","Endif"],commonCommands=["AccessInfo","Background","Build","ClearAccessBlock","ClearRTCam","Config","ControllerDisable","ControllerEnable","DateTime","Debug","Dec","DeepSleep","DisablePriorityTask","DNS","DST","EraseSDKWiFi","ExecuteRules","Gateway","I2Cscanner","Inc","IP","Let","Load","LogEntry","LogPortStatus","LoopTimerSet","LoopTimerSet_ms","MemInfo","MemInfoDetail","Name","Password","PostToHTTP","Publish","PublishR","Reboot","Reset","Save","SendTo","SendToHTTP","SendToUDP","Settings","Subnet","Subscribe","TaskClear","TaskClearAll","TaskDisable","TaskEnable","TaskRun","TaskValueSet","TaskValueSetAndRun","TimerPause","TimerResume","TimerSet","TimerSet_ms","TimeZone","UdpPort","UdpTest","Unit","UseNTP","WdConfig","WdRead","WiFi","WiFiAllowAP","WiFiAPMode","WiFiConnect","WiFiDisconnect","WiFiKey","WiFiKey2","WiFiMode","WiFiScan","WiFiSSID","WiFiSSID2","WiFiSTAMode","Event","AsyncEvent","GPIO","GPIOToggle","LongPulse","LongPulse_mS","Monitor","Pulse","PWM","Servo","Status","Tone","RTTTL","UnMonitor",],commonEvents=["Clock#Time","Login#Failed","MQTT#Connected","MQTT#Disconnected","MQTTimport#Connected","MQTTimport#Disconnected","Rules#Timer","System#Boot","System#BootMode","System#Sleep","System#Wake","TaskExit#","TaskInit#","ThingspeakReply","Time#Initialized","Time#Set","WiFi#APmodeDisabled","WiFi#APmodeEnabled","WiFi#ChangedAccesspoint","WiFi#ChangedWiFichannel","WiFi#Connected","WiFi#Disconnected"],commonPlugins=["ResetPulseCounter","SetPulseCounterTotal","LogPulseStatistic","analogout","MCPGPIO","MCPGPIOToggle","MCPLongPulse","MCPLongPulse_ms","MCPPulse","Status,MCP","Monitor,MCP","MonitorRange,MCP","UnMonitorRange,MCP","UnMonitor,MCP","MCPGPIORange","MCPGPIOPattern","MCPMode","MCPModeRange","ExtGpio","ExtPwm","ExtPulse","ExtLongPulse","Status,EXT,","LCDCmd","LCD","PCFGPIO","PCFGPIOToggle","PCFLongPulse","PCFLongPulse_ms","PCFPulse","Status,PCF","Monitor,PCF","MonitorRange,PCF","UnMonitorRange,PCF","UnMonitor,PCF","PCFGPIORange","PCFGPIOpattern","PCFMode","PCFmodeRange","pcapwm","pcafrq","mode2","OLED","OLEDCMD","OLEDCMD,on","OLEDCMD,off","OLEDCMD,clear","IRSEND","IRSENDAC","OledFramedCmd","OledFramedCmd,Display","OledFramedCmd,low","OledFramedCmd,med","OledFramedCmd,high","OledFramedCmd,Frame","OledFramedCmd,linecount","OledFramedCmd,leftalign","OledFramedCmd,align","OledFramedCmd,userDef1","OledFramedCmd,userDef2","NeoPixel","NeoPixelAll","NeoPixelLine","NeoPixelHSV","NeoPixelAllHSV","NeoPixelLineHSV","NeoPixelBright","MotorShieldCmd,DCMotor","MotorShieldCmd,Stepper","Sensair_SetRelay","PMSX003","PMSX003,Wake","PMSX003,Sleep","PMSX003,Reset","encwrite","Play","Vol","Eq","Mode","Repeat","tareChanA","tareChanB","7dn","7dst","7dsd","7dtext","7ddt","7dt","7dtfont","7dtbin","7don","7doff","7output","HLWCalibrate","HLWReset","csecalibrate","cseclearpulses","csereset","WemosMotorShieldCMD","LolinMotorShieldCMD","GPS","GPS,Sleep","GPS,Wake","GPS#GotFix","GPS#LostFix","GPS#Travelled","homieValueSet","HeatPumpir","MitsubishiHP","MitsubishiHP,temperature","MitsubishiHP,power","MitsubishiHP,mode","MitsubishiHP,fan","MitsubishiHP,vane","MitsubishiHP,widevane","Culreader_Write","Touch","Touch,Rot","Touch,Flip","Touch,Enable","Touch,Disable","Touch,On","Touch,Off","Touch,Toggle","Touch,Setgrp","Touch,Incgrp","Touch,Decgrp","Touch,Incpage","Touch,Decpage","Touch,Updatebutton","WakeOnLan","DotMatrix","DotMatrix,clear","DotMatrix,update","DotMatrix,size","DotMatrix,txt","DotMatrix,settxt","DotMatrix,content","DotMatrix,alignment","DotMatrix,anim.in","DotMatrix,anim.out","DotMatrix,speed","DotMatrix,pause","DotMatrix,font","DotMatrix,layout","DotMatrix,inverted","DotMatrix,specialeffect","DotMatrix,offset","DotMatrix,brightness","DotMatrix,repeat","DotMatrix,setbar","DotMatrix,bar","Thermo","Thermo,Up","Thermo,Down","Thermo,Mode","Thermo,ModeBtn","Thermo,Setpoint","Max1704xclearalert","scdgetabc","scdgetalt","scdgettmp","scdsetcalibration","scdsetfrc","scdgetinterval","multirelay","multirelay,on","multirelay,off","multirelay,set","multirelay,get","multirelay,loop","ShiftOut","ShiftOut,Set","ShiftOut,SetNoUpdate","ShiftOut,Update","ShiftOut,SetAll","ShiftOut,SetAllNoUpdate","ShiftOut,SetAllLow","ShiftOut,SetAllHigh","ShiftOut,SetChipCount","ShiftOut,SetHexBin","cdmrst","nfx","nfx,off","nfx,on","nfx,dim","nfx,line,","nfx,hsvline,","nfx,one,","nfx,hsvone,","nfx,all,","nfx,rgb,","nfx,fade,","nfx,hsv,","nfx,colorfade,","nfx,rainbow","nfx,kitt,","nfx,comet,","nfx,theatre,","nfx,scan,","nfx,dualscan,","nfx,twinkle,","nfx,twinklefade,","nfx,sparkle,","nfx,wipe,","nfx,dualwipe","nfx,fire","nfx,fireflicker","nfx,faketv","nfx,simpleclock","nfx,stop","nfx,statusrequest","nfx,fadetime,","nfx,fadedelay,","nfx,speed,","nfx,count,","nfx,bgcolor","ShiftIn","ShiftIn,PinEvent","ShiftIn,ChipEvent","ShiftIn,SetChipCount","ShiftIn,SampleFrequency","ShiftIn,EventPerPin","scd4x","scd4x,storesettings","scd4x,facoryreset","scd4x,selftest","scd4x,setfrc,","axp","axp,ldo2","axp,ldo3","axp,ldoio","axp,gpio0","axp,gpio1","axp,gpio2","axp,gpio3","axp,gpio4","axp,dcdc2","axp,dcdc3","axp,ldo2map","axp,ldo3map","axp,ldoiomap","axp,dcdc2map","axp,dcdc3map","axp,ldo2perc","axp,ldo3perc","axp,ldoioperc","axp,dcdc2perc","axp,dcdc3perc","I2CEncoder","I2CEncoder,bright","I2CEncoder,led1","I2CEncoder,led2","I2CEncoder,gain","I2CEncoder,set","cachereader","cachereader,readpos","cachereader,sendtaskinfo","cachereader,flush","tm1621","tm1621,write,","tm1621,writerow,","tm1621,voltamp,","tm1621,energy,","tm1621,celcius,","tm1621,fahrenheit,","tm1621,humidity,","tm1621,raw,","dac","dac,1","dac,2","sht4x","sht4x,startup","ld2410","ld2410,factoryreset","ld2410,logall","digipot","digipot,reset","digipot,shutdown","digipot,","gp8403","gp8403,volt,","gp8403,mvolt,","gp8403,range,","gp8403,preset,","gp8403,init,",],pluginDispKind=["tft","ili9341","ili9342","ili9481","ili9486","ili9488","epd","eink","epaper","il3897","uc8151d","ssd1680","ws2in7","ws1in54","st77xx","st7735","st7789","st7796","neomatrix","neo","pcd8544",],pluginDispCmd=["cmd,on","cmd,off","cmd,clear","cmd,backlight","cmd,bright","cmd,deepsleep","cmd,seq_start","cmd,seq_end","cmd,inv","cmd,rot",",clear",",rot",",tpm",",txt",",txp",",txz",",txc",",txs",",txtfull",",asciitable",",font",",l",",lh",",lv",",lm",",lmr",",r",",rf",",c",",cf",",rf",",t",",tf",",rr",",rrf",",px",",pxh",",pxv",",bmp",",btn",",win",",defwin",",delwin",],commonTag=["On","Do","Endon"],commonNumber=["toBin","toHex","Constrain","XOR","AND:","OR:","Ord","bitRead","bitSet","bitClear","bitWrite","urlencode"],commonMath=["Log","Ln","Abs","Exp","Sqrt","Sq","Round","Sin","Cos","Tan","aSin","aCos","aTan","Sin_d","Cos_d","Tan_d","aSin_d","aCos_d","aTan_d"],commonWarning=["delay","Delay","ResetFlashWriteCounter"],taskSpecifics=["settings.Enabled","settings.Interval","settings.ValueCount","settings.Controller1.Enabled","settings.Controller2.Enabled","settings.Controller3.Enabled","settings.Controller1.Idx","settings.Controller2.Idx","settings.Controller3.Idx"],AnythingElse=["%eventvalue%","%eventpar%","%eventname%","%sysname%","%bootcause%","%systime%","%systm_hm%","%systm_hm_0%","%systm_hm_sp%","%systime_am%","%systime_am_0%","%systime_am_sp%","%systm_hm_am%","%systm_hm_am_0%","%systm_hm_am_sp%","%lcltime%","%sunrise%","%s_sunrise%","%m_sunrise%","%sunset%","%s_sunset%","%m_sunset%","%lcltime_am%","%syshour%","%syshour_0%","%sysmin%","%sysmin_0%","%syssec%","%syssec_0%","%sysday%","%sysday_0%","%sysmonth%","%sysmonth_0%","%sysyear%","%sysyear_0%","%sysyears%","%sysweekday%","%sysweekday_s%","%unixtime%","%uptime%","%uptime_ms%","%rssi%","%ip%","%unit%","%unit_0%","%ssid%","%bssid%","%wi_ch%","%iswifi%","%vcc%","%mac%","%mac_int%","%isntp%","%ismqtt%","%dns%","%dns1%","%dns2%","%flash_freq%","%flash_size%","%flash_chip_vendor%","%flash_chip_model%","%fs_free%","%fs_size%","%cpu_id%","%cpu_freq%","%cpu_model%","%cpu_rev%","%cpu_cores%","%board_name%","%inttemp%","substring","indexOf","indexOf_ci","equals","equals_ci","strtol","timeToMin","timeToSec","%ethwifimode%","%ethconnected%","%ethduplex%","%ethspeed%","%ethstate%","%ethspeedstate%","%c_w_dir%","%c_c2f%","%c_ms2Bft%","%c_dew_th%","%c_alt_pres_sea%","%c_sea_pres_alt%","%c_cm2imp%","%c_mm2imp%","%c_m2day%","%c_m2dh%","%c_m2dhm%","%c_s2dhms%","%c_2hex%","%c_u2ip%","%c_uname%","%c_uage%","%c_ubuild%","%c_ubuildstr%","%c_uload%","%c_utype%","%c_utypestr%","var","int"];for(const element2 of pluginDispKind)commonPlugins=commonPlugins.concat(element2);for(const element2 of pluginDispKind)for(const element3 of pluginDispCmd){let e=element2+element3;commonPlugins=commonPlugins.concat(e)}var EXTRAWORDS=commonAtoms.concat(commonPlugins,commonKeywords,commonCommands,commonEvents,commonTag,commonNumber,commonMath,commonWarning,taskSpecifics,AnythingElse);function initCM(){CodeMirror.commands.autocomplete=function(e){e.showHint({hint:CodeMirror.hint.anyword})},(rEdit=CodeMirror.fromTextArea(document.getElementById("rules"),{tabSize:2,indentWithTabs:!1,lineNumbers:!0,autoCloseBrackets:!0,extraKeys:{"Ctrl-Space":"autocomplete",Tab(e){"null"===e.getMode().name?e.execCommand("insertTab"):e.somethingSelected()?e.execCommand("indentMore"):e.execCommand("insertSoftTab")},"Shift-Tab":e=>e.execCommand("indentLess")}})).on("change",function(){rEdit.save()}),rEdit.on("inputRead",function(e,t){var n=e.getCursor(),o=e.getTokenAt(n);/[\w%,.]/.test(t.text)&&"comment"!=o.type&&e.showHint({completeSingle:!1})})}!function(e){"object"==typeof exports&&"object"==typeof module?e(require("codemirror")):"function"==typeof define&&define.amd?define(["codemirror"],e):e(CodeMirror)}(function(e){"use strict";e.defineMode("espeasy",function(){var e={};function t(t,n){for(var o=0;oe.toLowerCase());commonCommands=commonCommands.concat(n);var o=commonEvents.map(e=>e.toLowerCase());commonEvents=commonEvents.concat(o);var i=commonPlugins.map(e=>e.toLowerCase());commonPlugins=commonPlugins.concat(i);var a=commonAtoms.map(e=>e.toLowerCase());commonAtoms=commonAtoms.concat(a);var s=commonKeywords.map(e=>e.toLowerCase());commonKeywords=commonKeywords.concat(s);var r=commonTag.map(e=>e.toLowerCase());commonTag=commonTag.concat(r);var c=commonNumber.map(e=>e.toLowerCase());commonNumber=commonNumber.concat(c);var l=commonMath.map(e=>e.toLowerCase());commonMath=commonMath.concat(l);var m=AnythingElse.map(e=>e.toLowerCase());AnythingElse=AnythingElse.concat(m);var d=taskSpecifics.map(e=>e.toLowerCase());function u(t,n){if(t.eatSpace())return null;t.sol();var o=t.next();if(/\d/.test(o)){if("0"==o)return"x"===t.next()?(t.eatWhile(/\w/),"number"):(t.eatWhile(/\d|\./),"number");if(t.eatWhile(/\d|\./),!t.match("d")&&!t.match("output")&&(t.eol()||/\D/.test(t.peek())))return"number"}if(/\w/.test(o))for(let i of EXTRAWORDS){let a=i.substring(1);(i.includes(":")||i.includes(",")||i.includes("."))&&t.match(a)}if(/\w/.test(o)&&(t.eatWhile(/[\w]/),t.match(".gpio")||t.match(".pulse")||t.match(".frq")||t.match(".pwm")))return"def";if("\\"===o)return t.next(),null;if("("===o||")"===o)return"bracket";if("{"===o||"}"===o||":"===o)return"number";if("/"==o)return/\//.test(t.peek())?(t.skipToEnd(),"comment"):"operator";if("'"==o&&(t.eatWhile(/[^']/),t.match("'")))return"attribute";if("+"===o||"="===o||"<"===o||">"===o||"-"===o||","===o||"*"===o||"!"===o)return"operator";if("%"==o){if(/\d/.test(t.next()))return"number";if(t.eatWhile(/[^\s\%]/),t.match("%"))return"hr"}if("["==o&&(t.eatWhile(/[^\s\]]/),t.eat("]")))return"hr";t.eatWhile(/\w/);var s=t.current();return/\w/.test(o)&&t.match("#")?(t.eatWhile(/[\w.#]/),"events"):"#"===o?(t.eatWhile(/\w/),"number"):e.hasOwnProperty(s)?e[s]:null}return taskSpecifics=taskSpecifics.concat(d),t("atom",commonAtoms),t("keyword",commonKeywords),t("builtin",commonCommands),t("events",commonEvents),t("def",commonPlugins),t("tag",commonTag),t("number",commonNumber),t("bracket",commonMath),t("warning",commonWarning),t("hr",AnythingElse),t("comment",taskSpecifics),{startState:function(){return{tokens:[]}},token:function(e,t){var n,o;return n=e,((o=t).tokens[0]||u)(n,o)},closeBrackets:"[]{}''\"\"``()",lineComment:"//",fold:"brace"}})}),function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],mod):e(CodeMirror)}(function(e){var t={pairs:"()[]{}''\"\"",closeBefore:")]}'\":;>",triples:"",explode:"[]{}"},n=e.Pos;function o(e,n){return"pairs"==n&&"string"==typeof e?e:"object"==typeof e&&null!=e[n]?e[n]:t[n]}e.defineOption("autoCloseBrackets",!1,function(t,n,s){s&&s!=e.Init&&(t.removeKeyMap(i),t.state.closeBrackets=null),n&&(a(o(n,"pairs")),t.state.closeBrackets=n,t.addKeyMap(i))});var i={Backspace:function t(i){var a=r(i);if(!a||i.getOption("disableInput"))return e.Pass;for(var s=o(a,"pairs"),c=i.listSelections(),l=0;l=0;l--){var u=c[l].head;i.replaceRange("",n(u.line,u.ch-1),n(u.line,u.ch+1),"+delete")}},Enter:function t(n){var i=r(n),a=i&&o(i,"explode");if(!a||n.getOption("disableInput"))return e.Pass;for(var s=n.listSelections(),l=0;l=0&&i.getRange(b,n(b.line,b.ch+3))==a+a+a?"skipThree":"skip";else if(h&&b.ch>1&&p.indexOf(a)>=0&&i.getRange(n(b.line,b.ch-2),b)==a+a){if(b.ch>2&&/\bstring/.test(i.getTokenTypeAt(n(b.line,b.ch-2))))return e.Pass;P="addFour"}else if(h){var M=0==b.ch?" ":i.getRange(n(b.line,b.ch-1),b);if(e.isWordChar(y)||M==a||e.isWordChar(M))return e.Pass;P="both"}else{if(!(x&&(0===y.length||/\s/.test(y)||f.indexOf(y)>-1)))return e.Pass;P="both"}if(S){if(S!=P)return e.Pass}else S=P}var v=u%2?m.charAt(u-1):a,D=u%2?a:m.charAt(u+1);i.operation(function(){if("skip"==S)c(i,1);else if("skipThree"==S)c(i,3);else if("surround"==S){for(var e=i.getSelections(),t=0;t0?{line:s.head.line,ch:s.head.ch+t}:{line:s.head.line-1};n.push({anchor:r,head:r})}e.setSelections(n,i)}function l(t){var o=e.cmpPos(t.anchor,t.head)>0;return{anchor:new n(t.anchor.line,t.anchor.ch+(o?-1:1)),head:new n(t.head.line,t.head.ch+(o?1:-1))}}function m(e,t){var o=e.getRange(n(t.line,t.ch-1),n(t.line,t.ch+1));return 2==o.length?o:null}function d(e,t){var o=e.getTokenAt(n(t.line,t.ch+1));return/\bstring/.test(o.type)&&o.start==t.ch&&(0==t.ch||!/\bstring/.test(e.getTokenTypeAt(t)))}a(t.pairs+"`")}); \ No newline at end of file +var rEdit,commonAtoms=["And","Or"],commonKeywords=["If","Else","Elseif","Endif"],commonCommands=["AccessInfo","Background","Build","ClearAccessBlock","ClearRTCam","Config","ControllerDisable","ControllerEnable","DateTime","Debug","Dec","DeepSleep","DisablePriorityTask","DNS","DST","EraseSDKWiFi","ExecuteRules","Gateway","I2Cscanner","Inc","IP","Let","Load","LogEntry","LogPortStatus","LoopTimerSet","LoopTimerSet_ms","MemInfo","MemInfoDetail","Name","Password","PostToHTTP","Publish","PublishR","Reboot","Reset","Save","SendTo","SendToHTTP","SendToUDP","Settings","Subnet","Subscribe","TaskClear","TaskClearAll","TaskDisable","TaskEnable","TaskRun","TaskValueSet","TaskValueSetAndRun","TimerPause","TimerResume","TimerSet","TimerSet_ms","TimeZone","UdpPort","UdpTest","Unit","UseNTP","WdConfig","WdRead","WiFi","WiFiAllowAP","WiFiAPMode","WiFiConnect","WiFiDisconnect","WiFiKey","WiFiKey2","WiFiMode","WiFiScan","WiFiSSID","WiFiSSID2","WiFiSTAMode","Event","AsyncEvent","GPIO","GPIOToggle","LongPulse","LongPulse_mS","Monitor","Pulse","PWM","Servo","Status","Tone","RTTTL","UnMonitor",],commonEvents=["Clock#Time","Login#Failed","MQTT#Connected","MQTT#Disconnected","MQTTimport#Connected","MQTTimport#Disconnected","Rules#Timer","System#Boot","System#BootMode","System#Sleep","System#Wake","TaskExit#","TaskInit#","ThingspeakReply","Time#Initialized","Time#Set","WiFi#APmodeDisabled","WiFi#APmodeEnabled","WiFi#ChangedAccesspoint","WiFi#ChangedWiFichannel","WiFi#Connected","WiFi#Disconnected"],commonPlugins=["ResetPulseCounter","SetPulseCounterTotal","LogPulseStatistic","analogout","MCPGPIO","MCPGPIOToggle","MCPLongPulse","MCPLongPulse_ms","MCPPulse","Status,MCP","Monitor,MCP","MonitorRange,MCP","UnMonitorRange,MCP","UnMonitor,MCP","MCPGPIORange","MCPGPIOPattern","MCPMode","MCPModeRange","ExtGpio","ExtPwm","ExtPulse","ExtLongPulse","Status,EXT,","LCDCmd","LCD","PCFGPIO","PCFGPIOToggle","PCFLongPulse","PCFLongPulse_ms","PCFPulse","Status,PCF","Monitor,PCF","MonitorRange,PCF","UnMonitorRange,PCF","UnMonitor,PCF","PCFGPIORange","PCFGPIOpattern","PCFMode","PCFmodeRange","pcapwm","pcafrq","mode2","OLED","OLEDCMD","OLEDCMD,on","OLEDCMD,off","OLEDCMD,clear","IRSEND","IRSENDAC","OledFramedCmd","OledFramedCmd,Display","OledFramedCmd,low","OledFramedCmd,med","OledFramedCmd,high","OledFramedCmd,Frame","OledFramedCmd,linecount","OledFramedCmd,leftalign","OledFramedCmd,align","OledFramedCmd,userDef1","OledFramedCmd,userDef2","NeoPixel","NeoPixelAll","NeoPixelLine","NeoPixelHSV","NeoPixelAllHSV","NeoPixelLineHSV","NeoPixelBright","MotorShieldCmd,DCMotor","MotorShieldCmd,Stepper","Sensair_SetRelay","PMSX003","PMSX003,Wake","PMSX003,Sleep","PMSX003,Reset","encwrite","Play","Vol","Eq","Mode","Repeat","tareChanA","tareChanB","7dn","7dst","7dsd","7dtext","7ddt","7dt","7dtfont","7dtbin","7don","7doff","7output","HLWCalibrate","HLWReset","csecalibrate","cseclearpulses","csereset","WemosMotorShieldCMD","LolinMotorShieldCMD","GPS","GPS,Sleep","GPS,Wake","GPS#GotFix","GPS#LostFix","GPS#Travelled","homieValueSet","HeatPumpir","MitsubishiHP","MitsubishiHP,temperature","MitsubishiHP,power","MitsubishiHP,mode","MitsubishiHP,fan","MitsubishiHP,vane","MitsubishiHP,widevane","Culreader_Write","Touch","Touch,Rot","Touch,Flip","Touch,Enable","Touch,Disable","Touch,On","Touch,Off","Touch,Toggle","Touch,Setgrp","Touch,Incgrp","Touch,Decgrp","Touch,Incpage","Touch,Decpage","Touch,Updatebutton","WakeOnLan","DotMatrix","DotMatrix,clear","DotMatrix,update","DotMatrix,size","DotMatrix,txt","DotMatrix,settxt","DotMatrix,content","DotMatrix,alignment","DotMatrix,anim.in","DotMatrix,anim.out","DotMatrix,speed","DotMatrix,pause","DotMatrix,font","DotMatrix,layout","DotMatrix,inverted","DotMatrix,specialeffect","DotMatrix,offset","DotMatrix,brightness","DotMatrix,repeat","DotMatrix,setbar","DotMatrix,bar","Thermo","Thermo,Up","Thermo,Down","Thermo,Mode","Thermo,ModeBtn","Thermo,Setpoint","Max1704xclearalert","scdgetabc","scdgetalt","scdgettmp","scdsetcalibration","scdsetfrc","scdgetinterval","multirelay","multirelay,on","multirelay,off","multirelay,set","multirelay,get","multirelay,loop","ShiftOut","ShiftOut,Set","ShiftOut,SetNoUpdate","ShiftOut,Update","ShiftOut,SetAll","ShiftOut,SetAllNoUpdate","ShiftOut,SetAllLow","ShiftOut,SetAllHigh","ShiftOut,SetChipCount","ShiftOut,SetHexBin","cdmrst","nfx","nfx,off","nfx,on","nfx,dim","nfx,line,","nfx,hsvline,","nfx,one,","nfx,hsvone,","nfx,all,","nfx,rgb,","nfx,fade,","nfx,hsv,","nfx,colorfade,","nfx,rainbow","nfx,kitt,","nfx,comet,","nfx,theatre,","nfx,scan,","nfx,dualscan,","nfx,twinkle,","nfx,twinklefade,","nfx,sparkle,","nfx,wipe,","nfx,dualwipe","nfx,fire","nfx,fireflicker","nfx,faketv","nfx,simpleclock","nfx,stop","nfx,statusrequest","nfx,fadetime,","nfx,fadedelay,","nfx,speed,","nfx,count,","nfx,bgcolor","ShiftIn","ShiftIn,PinEvent","ShiftIn,ChipEvent","ShiftIn,SetChipCount","ShiftIn,SampleFrequency","ShiftIn,EventPerPin","scd4x","scd4x,storesettings","scd4x,facoryreset","scd4x,selftest","scd4x,setfrc,","axp","axp,ldo2","axp,ldo3","axp,ldoio","axp,gpio0","axp,gpio1","axp,gpio2","axp,gpio3","axp,gpio4","axp,dcdc2","axp,dcdc3","axp,ldo2map","axp,ldo3map","axp,ldoiomap","axp,dcdc2map","axp,dcdc3map","axp,ldo2perc","axp,ldo3perc","axp,ldoioperc","axp,dcdc2perc","axp,dcdc3perc","I2CEncoder","I2CEncoder,bright","I2CEncoder,led1","I2CEncoder,led2","I2CEncoder,gain","I2CEncoder,set","cachereader","cachereader,readpos","cachereader,sendtaskinfo","cachereader,flush","tm1621","tm1621,write,","tm1621,writerow,","tm1621,voltamp,","tm1621,energy,","tm1621,celcius,","tm1621,fahrenheit,","tm1621,humidity,","tm1621,raw,","dac","dac,1","dac,2","sht4x","sht4x,startup","ld2410","ld2410,factoryreset","ld2410,logall","digipot","digipot,reset","digipot,shutdown","digipot,","gp8403","gp8403,volt,","gp8403,mvolt,","gp8403,range,","gp8403,preset,","gp8403,init,","sen5x","sen5x,reset","sen5x,techlog,",],pluginDispKind=["tft","ili9341","ili9342","ili9481","ili9486","ili9488","epd","eink","epaper","il3897","uc8151d","ssd1680","ws2in7","ws1in54","st77xx","st7735","st7789","st7796","neomatrix","neo","pcd8544",],pluginDispCmd=["cmd,on","cmd,off","cmd,clear","cmd,backlight","cmd,bright","cmd,deepsleep","cmd,seq_start","cmd,seq_end","cmd,inv","cmd,rot",",clear",",rot",",tpm",",txt",",txp",",txz",",txc",",txs",",txtfull",",asciitable",",font",",l",",lh",",lv",",lm",",lmr",",r",",rf",",c",",cf",",rf",",t",",tf",",rr",",rrf",",px",",pxh",",pxv",",bmp",",btn",",win",",defwin",",delwin",],commonTag=["On","Do","Endon"],commonNumber=["toBin","toHex","Constrain","XOR","AND:","OR:","Ord","bitRead","bitSet","bitClear","bitWrite","urlencode"],commonMath=["Log","Ln","Abs","Exp","Sqrt","Sq","Round","Sin","Cos","Tan","aSin","aCos","aTan","Sin_d","Cos_d","Tan_d","aSin_d","aCos_d","aTan_d"],commonWarning=["delay","Delay","ResetFlashWriteCounter"],taskSpecifics=["settings.Enabled","settings.Interval","settings.ValueCount","settings.Controller1.Enabled","settings.Controller2.Enabled","settings.Controller3.Enabled","settings.Controller1.Idx","settings.Controller2.Idx","settings.Controller3.Idx"],AnythingElse=["%eventvalue%","%eventpar%","%eventname%","%sysname%","%bootcause%","%systime%","%systm_hm%","%systm_hm_0%","%systm_hm_sp%","%systime_am%","%systime_am_0%","%systime_am_sp%","%systm_hm_am%","%systm_hm_am_0%","%systm_hm_am_sp%","%lcltime%","%sunrise%","%s_sunrise%","%m_sunrise%","%sunset%","%s_sunset%","%m_sunset%","%lcltime_am%","%syshour%","%syshour_0%","%sysmin%","%sysmin_0%","%syssec%","%syssec_0%","%sysday%","%sysday_0%","%sysmonth%","%sysmonth_0%","%sysyear%","%sysyear_0%","%sysyears%","%sysweekday%","%sysweekday_s%","%unixtime%","%uptime%","%uptime_ms%","%rssi%","%ip%","%unit%","%unit_0%","%ssid%","%bssid%","%wi_ch%","%iswifi%","%vcc%","%mac%","%mac_int%","%isntp%","%ismqtt%","%dns%","%dns1%","%dns2%","%flash_freq%","%flash_size%","%flash_chip_vendor%","%flash_chip_model%","%fs_free%","%fs_size%","%cpu_id%","%cpu_freq%","%cpu_model%","%cpu_rev%","%cpu_cores%","%board_name%","%inttemp%","substring","indexOf","indexOf_ci","equals","equals_ci","strtol","timeToMin","timeToSec","%ethwifimode%","%ethconnected%","%ethduplex%","%ethspeed%","%ethstate%","%ethspeedstate%","%c_w_dir%","%c_c2f%","%c_ms2Bft%","%c_dew_th%","%c_alt_pres_sea%","%c_sea_pres_alt%","%c_cm2imp%","%c_mm2imp%","%c_m2day%","%c_m2dh%","%c_m2dhm%","%c_s2dhms%","%c_2hex%","%c_u2ip%","%c_uname%","%c_uage%","%c_ubuild%","%c_ubuildstr%","%c_uload%","%c_utype%","%c_utypestr%","var","int"];for(const element2 of pluginDispKind)commonPlugins=commonPlugins.concat(element2);for(const element2 of pluginDispKind)for(const element3 of pluginDispCmd){let e=element2+element3;commonPlugins=commonPlugins.concat(e)}var EXTRAWORDS=commonAtoms.concat(commonPlugins,commonKeywords,commonCommands,commonEvents,commonTag,commonNumber,commonMath,commonWarning,taskSpecifics,AnythingElse);function initCM(){CodeMirror.commands.autocomplete=function(e){e.showHint({hint:CodeMirror.hint.anyword})},(rEdit=CodeMirror.fromTextArea(document.getElementById("rules"),{tabSize:2,indentWithTabs:!1,lineNumbers:!0,autoCloseBrackets:!0,extraKeys:{"Ctrl-Space":"autocomplete",Tab(e){"null"===e.getMode().name?e.execCommand("insertTab"):e.somethingSelected()?e.execCommand("indentMore"):e.execCommand("insertSoftTab")},"Shift-Tab":e=>e.execCommand("indentLess")}})).on("change",function(){rEdit.save()}),rEdit.on("inputRead",function(e,t){var n=e.getCursor(),o=e.getTokenAt(n);/[\w%,.]/.test(t.text)&&"comment"!=o.type&&e.showHint({completeSingle:!1})})}!function(e){"object"==typeof exports&&"object"==typeof module?e(require("codemirror")):"function"==typeof define&&define.amd?define(["codemirror"],e):e(CodeMirror)}(function(e){"use strict";e.defineMode("espeasy",function(){var e={};function t(t,n){for(var o=0;oe.toLowerCase());commonCommands=commonCommands.concat(n);var o=commonEvents.map(e=>e.toLowerCase());commonEvents=commonEvents.concat(o);var i=commonPlugins.map(e=>e.toLowerCase());commonPlugins=commonPlugins.concat(i);var a=commonAtoms.map(e=>e.toLowerCase());commonAtoms=commonAtoms.concat(a);var s=commonKeywords.map(e=>e.toLowerCase());commonKeywords=commonKeywords.concat(s);var r=commonTag.map(e=>e.toLowerCase());commonTag=commonTag.concat(r);var c=commonNumber.map(e=>e.toLowerCase());commonNumber=commonNumber.concat(c);var l=commonMath.map(e=>e.toLowerCase());commonMath=commonMath.concat(l);var m=AnythingElse.map(e=>e.toLowerCase());AnythingElse=AnythingElse.concat(m);var d=taskSpecifics.map(e=>e.toLowerCase());function u(t,n){if(t.eatSpace())return null;t.sol();var o=t.next();if(/\d/.test(o)){if("0"==o)return"x"===t.next()?(t.eatWhile(/\w/),"number"):(t.eatWhile(/\d|\./),"number");if(t.eatWhile(/\d|\./),!t.match("d")&&!t.match("output")&&(t.eol()||/\D/.test(t.peek())))return"number"}if(/\w/.test(o))for(let i of EXTRAWORDS){let a=i.substring(1);(i.includes(":")||i.includes(",")||i.includes("."))&&t.match(a)}if(/\w/.test(o)&&(t.eatWhile(/[\w]/),t.match(".gpio")||t.match(".pulse")||t.match(".frq")||t.match(".pwm")))return"def";if("\\"===o)return t.next(),null;if("("===o||")"===o)return"bracket";if("{"===o||"}"===o||":"===o)return"number";if("/"==o)return/\//.test(t.peek())?(t.skipToEnd(),"comment"):"operator";if("'"==o&&(t.eatWhile(/[^']/),t.match("'")))return"attribute";if("+"===o||"="===o||"<"===o||">"===o||"-"===o||","===o||"*"===o||"!"===o)return"operator";if("%"==o){if(/\d/.test(t.next()))return"number";if(t.eatWhile(/[^\s\%]/),t.match("%"))return"hr"}if("["==o&&(t.eatWhile(/[^\s\]]/),t.eat("]")))return"hr";t.eatWhile(/\w/);var s=t.current();return/\w/.test(o)&&t.match("#")?(t.eatWhile(/[\w.#]/),"events"):"#"===o?(t.eatWhile(/\w/),"number"):e.hasOwnProperty(s)?e[s]:null}return taskSpecifics=taskSpecifics.concat(d),t("atom",commonAtoms),t("keyword",commonKeywords),t("builtin",commonCommands),t("events",commonEvents),t("def",commonPlugins),t("tag",commonTag),t("number",commonNumber),t("bracket",commonMath),t("warning",commonWarning),t("hr",AnythingElse),t("comment",taskSpecifics),{startState:function(){return{tokens:[]}},token:function(e,t){var n,o;return n=e,((o=t).tokens[0]||u)(n,o)},closeBrackets:"[]{}''\"\"``()",lineComment:"//",fold:"brace"}})}),function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],mod):e(CodeMirror)}(function(e){var t={pairs:"()[]{}''\"\"",closeBefore:")]}'\":;>",triples:"",explode:"[]{}"},n=e.Pos;function o(e,n){return"pairs"==n&&"string"==typeof e?e:"object"==typeof e&&null!=e[n]?e[n]:t[n]}e.defineOption("autoCloseBrackets",!1,function(t,n,s){s&&s!=e.Init&&(t.removeKeyMap(i),t.state.closeBrackets=null),n&&(a(o(n,"pairs")),t.state.closeBrackets=n,t.addKeyMap(i))});var i={Backspace:function t(i){var a=r(i);if(!a||i.getOption("disableInput"))return e.Pass;for(var s=o(a,"pairs"),c=i.listSelections(),l=0;l=0;l--){var u=c[l].head;i.replaceRange("",n(u.line,u.ch-1),n(u.line,u.ch+1),"+delete")}},Enter:function t(n){var i=r(n),a=i&&o(i,"explode");if(!a||n.getOption("disableInput"))return e.Pass;for(var s=n.listSelections(),l=0;l=0&&i.getRange(b,n(b.line,b.ch+3))==a+a+a?"skipThree":"skip";else if(h&&b.ch>1&&p.indexOf(a)>=0&&i.getRange(n(b.line,b.ch-2),b)==a+a){if(b.ch>2&&/\bstring/.test(i.getTokenTypeAt(n(b.line,b.ch-2))))return e.Pass;P="addFour"}else if(h){var M=0==b.ch?" ":i.getRange(n(b.line,b.ch-1),b);if(e.isWordChar(y)||M==a||e.isWordChar(M))return e.Pass;P="both"}else{if(!(x&&(0===y.length||/\s/.test(y)||f.indexOf(y)>-1)))return e.Pass;P="both"}if(S){if(S!=P)return e.Pass}else S=P}var v=u%2?m.charAt(u-1):a,D=u%2?a:m.charAt(u+1);i.operation(function(){if("skip"==S)c(i,1);else if("skipThree"==S)c(i,3);else if("surround"==S){for(var e=i.getSelections(),t=0;t0?{line:s.head.line,ch:s.head.ch+t}:{line:s.head.line-1};n.push({anchor:r,head:r})}e.setSelections(n,i)}function l(t){var o=e.cmpPos(t.anchor,t.head)>0;return{anchor:new n(t.anchor.line,t.anchor.ch+(o?-1:1)),head:new n(t.head.line,t.head.ch+(o?1:-1))}}function m(e,t){var o=e.getRange(n(t.line,t.ch-1),n(t.line,t.ch+1));return 2==o.length?o:null}function d(e,t){var o=e.getTokenAt(n(t.line,t.ch+1));return/\bstring/.test(o.type)&&o.start==t.ch&&(0==t.ch||!/\bstring/.test(e.getTokenTypeAt(t)))}a(t.pairs+"`")}); \ No newline at end of file From 16c84e4978affd2043f149010f394bfb0f6b2ef7 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 5 May 2024 22:22:47 +0200 Subject: [PATCH 094/113] [P167] Add command `sen5x,techlog,<0|1>` --- static/espeasy.js | 2 +- static/espeasy.min.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/static/espeasy.js b/static/espeasy.js index 38d82da502..7020ef0cab 100644 --- a/static/espeasy.js +++ b/static/espeasy.js @@ -115,7 +115,7 @@ var commonPlugins = [ //P166 "gp8403", "gp8403,volt,", "gp8403,mvolt,", "gp8403,range,", "gp8403,preset,", "gp8403,init,", //P167 - "sen5x", "sen5x,reset", "sen5x,techlog,", + "sen5x", "sen5x,startclean", "sen5x,techlog,", ]; var pluginDispKind = [ //P095 diff --git a/static/espeasy.min.js b/static/espeasy.min.js index 7ec46a6537..5fb49bc94f 100644 --- a/static/espeasy.min.js +++ b/static/espeasy.min.js @@ -1 +1 @@ -var rEdit,commonAtoms=["And","Or"],commonKeywords=["If","Else","Elseif","Endif"],commonCommands=["AccessInfo","Background","Build","ClearAccessBlock","ClearRTCam","Config","ControllerDisable","ControllerEnable","DateTime","Debug","Dec","DeepSleep","DisablePriorityTask","DNS","DST","EraseSDKWiFi","ExecuteRules","Gateway","I2Cscanner","Inc","IP","Let","Load","LogEntry","LogPortStatus","LoopTimerSet","LoopTimerSet_ms","MemInfo","MemInfoDetail","Name","Password","PostToHTTP","Publish","PublishR","Reboot","Reset","Save","SendTo","SendToHTTP","SendToUDP","Settings","Subnet","Subscribe","TaskClear","TaskClearAll","TaskDisable","TaskEnable","TaskRun","TaskValueSet","TaskValueSetAndRun","TimerPause","TimerResume","TimerSet","TimerSet_ms","TimeZone","UdpPort","UdpTest","Unit","UseNTP","WdConfig","WdRead","WiFi","WiFiAllowAP","WiFiAPMode","WiFiConnect","WiFiDisconnect","WiFiKey","WiFiKey2","WiFiMode","WiFiScan","WiFiSSID","WiFiSSID2","WiFiSTAMode","Event","AsyncEvent","GPIO","GPIOToggle","LongPulse","LongPulse_mS","Monitor","Pulse","PWM","Servo","Status","Tone","RTTTL","UnMonitor",],commonEvents=["Clock#Time","Login#Failed","MQTT#Connected","MQTT#Disconnected","MQTTimport#Connected","MQTTimport#Disconnected","Rules#Timer","System#Boot","System#BootMode","System#Sleep","System#Wake","TaskExit#","TaskInit#","ThingspeakReply","Time#Initialized","Time#Set","WiFi#APmodeDisabled","WiFi#APmodeEnabled","WiFi#ChangedAccesspoint","WiFi#ChangedWiFichannel","WiFi#Connected","WiFi#Disconnected"],commonPlugins=["ResetPulseCounter","SetPulseCounterTotal","LogPulseStatistic","analogout","MCPGPIO","MCPGPIOToggle","MCPLongPulse","MCPLongPulse_ms","MCPPulse","Status,MCP","Monitor,MCP","MonitorRange,MCP","UnMonitorRange,MCP","UnMonitor,MCP","MCPGPIORange","MCPGPIOPattern","MCPMode","MCPModeRange","ExtGpio","ExtPwm","ExtPulse","ExtLongPulse","Status,EXT,","LCDCmd","LCD","PCFGPIO","PCFGPIOToggle","PCFLongPulse","PCFLongPulse_ms","PCFPulse","Status,PCF","Monitor,PCF","MonitorRange,PCF","UnMonitorRange,PCF","UnMonitor,PCF","PCFGPIORange","PCFGPIOpattern","PCFMode","PCFmodeRange","pcapwm","pcafrq","mode2","OLED","OLEDCMD","OLEDCMD,on","OLEDCMD,off","OLEDCMD,clear","IRSEND","IRSENDAC","OledFramedCmd","OledFramedCmd,Display","OledFramedCmd,low","OledFramedCmd,med","OledFramedCmd,high","OledFramedCmd,Frame","OledFramedCmd,linecount","OledFramedCmd,leftalign","OledFramedCmd,align","OledFramedCmd,userDef1","OledFramedCmd,userDef2","NeoPixel","NeoPixelAll","NeoPixelLine","NeoPixelHSV","NeoPixelAllHSV","NeoPixelLineHSV","NeoPixelBright","MotorShieldCmd,DCMotor","MotorShieldCmd,Stepper","Sensair_SetRelay","PMSX003","PMSX003,Wake","PMSX003,Sleep","PMSX003,Reset","encwrite","Play","Vol","Eq","Mode","Repeat","tareChanA","tareChanB","7dn","7dst","7dsd","7dtext","7ddt","7dt","7dtfont","7dtbin","7don","7doff","7output","HLWCalibrate","HLWReset","csecalibrate","cseclearpulses","csereset","WemosMotorShieldCMD","LolinMotorShieldCMD","GPS","GPS,Sleep","GPS,Wake","GPS#GotFix","GPS#LostFix","GPS#Travelled","homieValueSet","HeatPumpir","MitsubishiHP","MitsubishiHP,temperature","MitsubishiHP,power","MitsubishiHP,mode","MitsubishiHP,fan","MitsubishiHP,vane","MitsubishiHP,widevane","Culreader_Write","Touch","Touch,Rot","Touch,Flip","Touch,Enable","Touch,Disable","Touch,On","Touch,Off","Touch,Toggle","Touch,Setgrp","Touch,Incgrp","Touch,Decgrp","Touch,Incpage","Touch,Decpage","Touch,Updatebutton","WakeOnLan","DotMatrix","DotMatrix,clear","DotMatrix,update","DotMatrix,size","DotMatrix,txt","DotMatrix,settxt","DotMatrix,content","DotMatrix,alignment","DotMatrix,anim.in","DotMatrix,anim.out","DotMatrix,speed","DotMatrix,pause","DotMatrix,font","DotMatrix,layout","DotMatrix,inverted","DotMatrix,specialeffect","DotMatrix,offset","DotMatrix,brightness","DotMatrix,repeat","DotMatrix,setbar","DotMatrix,bar","Thermo","Thermo,Up","Thermo,Down","Thermo,Mode","Thermo,ModeBtn","Thermo,Setpoint","Max1704xclearalert","scdgetabc","scdgetalt","scdgettmp","scdsetcalibration","scdsetfrc","scdgetinterval","multirelay","multirelay,on","multirelay,off","multirelay,set","multirelay,get","multirelay,loop","ShiftOut","ShiftOut,Set","ShiftOut,SetNoUpdate","ShiftOut,Update","ShiftOut,SetAll","ShiftOut,SetAllNoUpdate","ShiftOut,SetAllLow","ShiftOut,SetAllHigh","ShiftOut,SetChipCount","ShiftOut,SetHexBin","cdmrst","nfx","nfx,off","nfx,on","nfx,dim","nfx,line,","nfx,hsvline,","nfx,one,","nfx,hsvone,","nfx,all,","nfx,rgb,","nfx,fade,","nfx,hsv,","nfx,colorfade,","nfx,rainbow","nfx,kitt,","nfx,comet,","nfx,theatre,","nfx,scan,","nfx,dualscan,","nfx,twinkle,","nfx,twinklefade,","nfx,sparkle,","nfx,wipe,","nfx,dualwipe","nfx,fire","nfx,fireflicker","nfx,faketv","nfx,simpleclock","nfx,stop","nfx,statusrequest","nfx,fadetime,","nfx,fadedelay,","nfx,speed,","nfx,count,","nfx,bgcolor","ShiftIn","ShiftIn,PinEvent","ShiftIn,ChipEvent","ShiftIn,SetChipCount","ShiftIn,SampleFrequency","ShiftIn,EventPerPin","scd4x","scd4x,storesettings","scd4x,facoryreset","scd4x,selftest","scd4x,setfrc,","axp","axp,ldo2","axp,ldo3","axp,ldoio","axp,gpio0","axp,gpio1","axp,gpio2","axp,gpio3","axp,gpio4","axp,dcdc2","axp,dcdc3","axp,ldo2map","axp,ldo3map","axp,ldoiomap","axp,dcdc2map","axp,dcdc3map","axp,ldo2perc","axp,ldo3perc","axp,ldoioperc","axp,dcdc2perc","axp,dcdc3perc","I2CEncoder","I2CEncoder,bright","I2CEncoder,led1","I2CEncoder,led2","I2CEncoder,gain","I2CEncoder,set","cachereader","cachereader,readpos","cachereader,sendtaskinfo","cachereader,flush","tm1621","tm1621,write,","tm1621,writerow,","tm1621,voltamp,","tm1621,energy,","tm1621,celcius,","tm1621,fahrenheit,","tm1621,humidity,","tm1621,raw,","dac","dac,1","dac,2","sht4x","sht4x,startup","ld2410","ld2410,factoryreset","ld2410,logall","digipot","digipot,reset","digipot,shutdown","digipot,","gp8403","gp8403,volt,","gp8403,mvolt,","gp8403,range,","gp8403,preset,","gp8403,init,","sen5x","sen5x,reset","sen5x,techlog,",],pluginDispKind=["tft","ili9341","ili9342","ili9481","ili9486","ili9488","epd","eink","epaper","il3897","uc8151d","ssd1680","ws2in7","ws1in54","st77xx","st7735","st7789","st7796","neomatrix","neo","pcd8544",],pluginDispCmd=["cmd,on","cmd,off","cmd,clear","cmd,backlight","cmd,bright","cmd,deepsleep","cmd,seq_start","cmd,seq_end","cmd,inv","cmd,rot",",clear",",rot",",tpm",",txt",",txp",",txz",",txc",",txs",",txtfull",",asciitable",",font",",l",",lh",",lv",",lm",",lmr",",r",",rf",",c",",cf",",rf",",t",",tf",",rr",",rrf",",px",",pxh",",pxv",",bmp",",btn",",win",",defwin",",delwin",],commonTag=["On","Do","Endon"],commonNumber=["toBin","toHex","Constrain","XOR","AND:","OR:","Ord","bitRead","bitSet","bitClear","bitWrite","urlencode"],commonMath=["Log","Ln","Abs","Exp","Sqrt","Sq","Round","Sin","Cos","Tan","aSin","aCos","aTan","Sin_d","Cos_d","Tan_d","aSin_d","aCos_d","aTan_d"],commonWarning=["delay","Delay","ResetFlashWriteCounter"],taskSpecifics=["settings.Enabled","settings.Interval","settings.ValueCount","settings.Controller1.Enabled","settings.Controller2.Enabled","settings.Controller3.Enabled","settings.Controller1.Idx","settings.Controller2.Idx","settings.Controller3.Idx"],AnythingElse=["%eventvalue%","%eventpar%","%eventname%","%sysname%","%bootcause%","%systime%","%systm_hm%","%systm_hm_0%","%systm_hm_sp%","%systime_am%","%systime_am_0%","%systime_am_sp%","%systm_hm_am%","%systm_hm_am_0%","%systm_hm_am_sp%","%lcltime%","%sunrise%","%s_sunrise%","%m_sunrise%","%sunset%","%s_sunset%","%m_sunset%","%lcltime_am%","%syshour%","%syshour_0%","%sysmin%","%sysmin_0%","%syssec%","%syssec_0%","%sysday%","%sysday_0%","%sysmonth%","%sysmonth_0%","%sysyear%","%sysyear_0%","%sysyears%","%sysweekday%","%sysweekday_s%","%unixtime%","%uptime%","%uptime_ms%","%rssi%","%ip%","%unit%","%unit_0%","%ssid%","%bssid%","%wi_ch%","%iswifi%","%vcc%","%mac%","%mac_int%","%isntp%","%ismqtt%","%dns%","%dns1%","%dns2%","%flash_freq%","%flash_size%","%flash_chip_vendor%","%flash_chip_model%","%fs_free%","%fs_size%","%cpu_id%","%cpu_freq%","%cpu_model%","%cpu_rev%","%cpu_cores%","%board_name%","%inttemp%","substring","indexOf","indexOf_ci","equals","equals_ci","strtol","timeToMin","timeToSec","%ethwifimode%","%ethconnected%","%ethduplex%","%ethspeed%","%ethstate%","%ethspeedstate%","%c_w_dir%","%c_c2f%","%c_ms2Bft%","%c_dew_th%","%c_alt_pres_sea%","%c_sea_pres_alt%","%c_cm2imp%","%c_mm2imp%","%c_m2day%","%c_m2dh%","%c_m2dhm%","%c_s2dhms%","%c_2hex%","%c_u2ip%","%c_uname%","%c_uage%","%c_ubuild%","%c_ubuildstr%","%c_uload%","%c_utype%","%c_utypestr%","var","int"];for(const element2 of pluginDispKind)commonPlugins=commonPlugins.concat(element2);for(const element2 of pluginDispKind)for(const element3 of pluginDispCmd){let e=element2+element3;commonPlugins=commonPlugins.concat(e)}var EXTRAWORDS=commonAtoms.concat(commonPlugins,commonKeywords,commonCommands,commonEvents,commonTag,commonNumber,commonMath,commonWarning,taskSpecifics,AnythingElse);function initCM(){CodeMirror.commands.autocomplete=function(e){e.showHint({hint:CodeMirror.hint.anyword})},(rEdit=CodeMirror.fromTextArea(document.getElementById("rules"),{tabSize:2,indentWithTabs:!1,lineNumbers:!0,autoCloseBrackets:!0,extraKeys:{"Ctrl-Space":"autocomplete",Tab(e){"null"===e.getMode().name?e.execCommand("insertTab"):e.somethingSelected()?e.execCommand("indentMore"):e.execCommand("insertSoftTab")},"Shift-Tab":e=>e.execCommand("indentLess")}})).on("change",function(){rEdit.save()}),rEdit.on("inputRead",function(e,t){var n=e.getCursor(),o=e.getTokenAt(n);/[\w%,.]/.test(t.text)&&"comment"!=o.type&&e.showHint({completeSingle:!1})})}!function(e){"object"==typeof exports&&"object"==typeof module?e(require("codemirror")):"function"==typeof define&&define.amd?define(["codemirror"],e):e(CodeMirror)}(function(e){"use strict";e.defineMode("espeasy",function(){var e={};function t(t,n){for(var o=0;oe.toLowerCase());commonCommands=commonCommands.concat(n);var o=commonEvents.map(e=>e.toLowerCase());commonEvents=commonEvents.concat(o);var i=commonPlugins.map(e=>e.toLowerCase());commonPlugins=commonPlugins.concat(i);var a=commonAtoms.map(e=>e.toLowerCase());commonAtoms=commonAtoms.concat(a);var s=commonKeywords.map(e=>e.toLowerCase());commonKeywords=commonKeywords.concat(s);var r=commonTag.map(e=>e.toLowerCase());commonTag=commonTag.concat(r);var c=commonNumber.map(e=>e.toLowerCase());commonNumber=commonNumber.concat(c);var l=commonMath.map(e=>e.toLowerCase());commonMath=commonMath.concat(l);var m=AnythingElse.map(e=>e.toLowerCase());AnythingElse=AnythingElse.concat(m);var d=taskSpecifics.map(e=>e.toLowerCase());function u(t,n){if(t.eatSpace())return null;t.sol();var o=t.next();if(/\d/.test(o)){if("0"==o)return"x"===t.next()?(t.eatWhile(/\w/),"number"):(t.eatWhile(/\d|\./),"number");if(t.eatWhile(/\d|\./),!t.match("d")&&!t.match("output")&&(t.eol()||/\D/.test(t.peek())))return"number"}if(/\w/.test(o))for(let i of EXTRAWORDS){let a=i.substring(1);(i.includes(":")||i.includes(",")||i.includes("."))&&t.match(a)}if(/\w/.test(o)&&(t.eatWhile(/[\w]/),t.match(".gpio")||t.match(".pulse")||t.match(".frq")||t.match(".pwm")))return"def";if("\\"===o)return t.next(),null;if("("===o||")"===o)return"bracket";if("{"===o||"}"===o||":"===o)return"number";if("/"==o)return/\//.test(t.peek())?(t.skipToEnd(),"comment"):"operator";if("'"==o&&(t.eatWhile(/[^']/),t.match("'")))return"attribute";if("+"===o||"="===o||"<"===o||">"===o||"-"===o||","===o||"*"===o||"!"===o)return"operator";if("%"==o){if(/\d/.test(t.next()))return"number";if(t.eatWhile(/[^\s\%]/),t.match("%"))return"hr"}if("["==o&&(t.eatWhile(/[^\s\]]/),t.eat("]")))return"hr";t.eatWhile(/\w/);var s=t.current();return/\w/.test(o)&&t.match("#")?(t.eatWhile(/[\w.#]/),"events"):"#"===o?(t.eatWhile(/\w/),"number"):e.hasOwnProperty(s)?e[s]:null}return taskSpecifics=taskSpecifics.concat(d),t("atom",commonAtoms),t("keyword",commonKeywords),t("builtin",commonCommands),t("events",commonEvents),t("def",commonPlugins),t("tag",commonTag),t("number",commonNumber),t("bracket",commonMath),t("warning",commonWarning),t("hr",AnythingElse),t("comment",taskSpecifics),{startState:function(){return{tokens:[]}},token:function(e,t){var n,o;return n=e,((o=t).tokens[0]||u)(n,o)},closeBrackets:"[]{}''\"\"``()",lineComment:"//",fold:"brace"}})}),function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],mod):e(CodeMirror)}(function(e){var t={pairs:"()[]{}''\"\"",closeBefore:")]}'\":;>",triples:"",explode:"[]{}"},n=e.Pos;function o(e,n){return"pairs"==n&&"string"==typeof e?e:"object"==typeof e&&null!=e[n]?e[n]:t[n]}e.defineOption("autoCloseBrackets",!1,function(t,n,s){s&&s!=e.Init&&(t.removeKeyMap(i),t.state.closeBrackets=null),n&&(a(o(n,"pairs")),t.state.closeBrackets=n,t.addKeyMap(i))});var i={Backspace:function t(i){var a=r(i);if(!a||i.getOption("disableInput"))return e.Pass;for(var s=o(a,"pairs"),c=i.listSelections(),l=0;l=0;l--){var u=c[l].head;i.replaceRange("",n(u.line,u.ch-1),n(u.line,u.ch+1),"+delete")}},Enter:function t(n){var i=r(n),a=i&&o(i,"explode");if(!a||n.getOption("disableInput"))return e.Pass;for(var s=n.listSelections(),l=0;l=0&&i.getRange(b,n(b.line,b.ch+3))==a+a+a?"skipThree":"skip";else if(h&&b.ch>1&&p.indexOf(a)>=0&&i.getRange(n(b.line,b.ch-2),b)==a+a){if(b.ch>2&&/\bstring/.test(i.getTokenTypeAt(n(b.line,b.ch-2))))return e.Pass;P="addFour"}else if(h){var M=0==b.ch?" ":i.getRange(n(b.line,b.ch-1),b);if(e.isWordChar(y)||M==a||e.isWordChar(M))return e.Pass;P="both"}else{if(!(x&&(0===y.length||/\s/.test(y)||f.indexOf(y)>-1)))return e.Pass;P="both"}if(S){if(S!=P)return e.Pass}else S=P}var v=u%2?m.charAt(u-1):a,D=u%2?a:m.charAt(u+1);i.operation(function(){if("skip"==S)c(i,1);else if("skipThree"==S)c(i,3);else if("surround"==S){for(var e=i.getSelections(),t=0;t0?{line:s.head.line,ch:s.head.ch+t}:{line:s.head.line-1};n.push({anchor:r,head:r})}e.setSelections(n,i)}function l(t){var o=e.cmpPos(t.anchor,t.head)>0;return{anchor:new n(t.anchor.line,t.anchor.ch+(o?-1:1)),head:new n(t.head.line,t.head.ch+(o?1:-1))}}function m(e,t){var o=e.getRange(n(t.line,t.ch-1),n(t.line,t.ch+1));return 2==o.length?o:null}function d(e,t){var o=e.getTokenAt(n(t.line,t.ch+1));return/\bstring/.test(o.type)&&o.start==t.ch&&(0==t.ch||!/\bstring/.test(e.getTokenTypeAt(t)))}a(t.pairs+"`")}); \ No newline at end of file +var rEdit,commonAtoms=["And","Or"],commonKeywords=["If","Else","Elseif","Endif"],commonCommands=["AccessInfo","Background","Build","ClearAccessBlock","ClearRTCam","Config","ControllerDisable","ControllerEnable","DateTime","Debug","Dec","DeepSleep","DisablePriorityTask","DNS","DST","EraseSDKWiFi","ExecuteRules","Gateway","I2Cscanner","Inc","IP","Let","Load","LogEntry","LogPortStatus","LoopTimerSet","LoopTimerSet_ms","MemInfo","MemInfoDetail","Name","Password","PostToHTTP","Publish","PublishR","Reboot","Reset","Save","SendTo","SendToHTTP","SendToUDP","Settings","Subnet","Subscribe","TaskClear","TaskClearAll","TaskDisable","TaskEnable","TaskRun","TaskValueSet","TaskValueSetAndRun","TimerPause","TimerResume","TimerSet","TimerSet_ms","TimeZone","UdpPort","UdpTest","Unit","UseNTP","WdConfig","WdRead","WiFi","WiFiAllowAP","WiFiAPMode","WiFiConnect","WiFiDisconnect","WiFiKey","WiFiKey2","WiFiMode","WiFiScan","WiFiSSID","WiFiSSID2","WiFiSTAMode","Event","AsyncEvent","GPIO","GPIOToggle","LongPulse","LongPulse_mS","Monitor","Pulse","PWM","Servo","Status","Tone","RTTTL","UnMonitor",],commonEvents=["Clock#Time","Login#Failed","MQTT#Connected","MQTT#Disconnected","MQTTimport#Connected","MQTTimport#Disconnected","Rules#Timer","System#Boot","System#BootMode","System#Sleep","System#Wake","TaskExit#","TaskInit#","ThingspeakReply","Time#Initialized","Time#Set","WiFi#APmodeDisabled","WiFi#APmodeEnabled","WiFi#ChangedAccesspoint","WiFi#ChangedWiFichannel","WiFi#Connected","WiFi#Disconnected"],commonPlugins=["ResetPulseCounter","SetPulseCounterTotal","LogPulseStatistic","analogout","MCPGPIO","MCPGPIOToggle","MCPLongPulse","MCPLongPulse_ms","MCPPulse","Status,MCP","Monitor,MCP","MonitorRange,MCP","UnMonitorRange,MCP","UnMonitor,MCP","MCPGPIORange","MCPGPIOPattern","MCPMode","MCPModeRange","ExtGpio","ExtPwm","ExtPulse","ExtLongPulse","Status,EXT,","LCDCmd","LCD","PCFGPIO","PCFGPIOToggle","PCFLongPulse","PCFLongPulse_ms","PCFPulse","Status,PCF","Monitor,PCF","MonitorRange,PCF","UnMonitorRange,PCF","UnMonitor,PCF","PCFGPIORange","PCFGPIOpattern","PCFMode","PCFmodeRange","pcapwm","pcafrq","mode2","OLED","OLEDCMD","OLEDCMD,on","OLEDCMD,off","OLEDCMD,clear","IRSEND","IRSENDAC","OledFramedCmd","OledFramedCmd,Display","OledFramedCmd,low","OledFramedCmd,med","OledFramedCmd,high","OledFramedCmd,Frame","OledFramedCmd,linecount","OledFramedCmd,leftalign","OledFramedCmd,align","OledFramedCmd,userDef1","OledFramedCmd,userDef2","NeoPixel","NeoPixelAll","NeoPixelLine","NeoPixelHSV","NeoPixelAllHSV","NeoPixelLineHSV","NeoPixelBright","MotorShieldCmd,DCMotor","MotorShieldCmd,Stepper","Sensair_SetRelay","PMSX003","PMSX003,Wake","PMSX003,Sleep","PMSX003,Reset","encwrite","Play","Vol","Eq","Mode","Repeat","tareChanA","tareChanB","7dn","7dst","7dsd","7dtext","7ddt","7dt","7dtfont","7dtbin","7don","7doff","7output","HLWCalibrate","HLWReset","csecalibrate","cseclearpulses","csereset","WemosMotorShieldCMD","LolinMotorShieldCMD","GPS","GPS,Sleep","GPS,Wake","GPS#GotFix","GPS#LostFix","GPS#Travelled","homieValueSet","HeatPumpir","MitsubishiHP","MitsubishiHP,temperature","MitsubishiHP,power","MitsubishiHP,mode","MitsubishiHP,fan","MitsubishiHP,vane","MitsubishiHP,widevane","Culreader_Write","Touch","Touch,Rot","Touch,Flip","Touch,Enable","Touch,Disable","Touch,On","Touch,Off","Touch,Toggle","Touch,Setgrp","Touch,Incgrp","Touch,Decgrp","Touch,Incpage","Touch,Decpage","Touch,Updatebutton","WakeOnLan","DotMatrix","DotMatrix,clear","DotMatrix,update","DotMatrix,size","DotMatrix,txt","DotMatrix,settxt","DotMatrix,content","DotMatrix,alignment","DotMatrix,anim.in","DotMatrix,anim.out","DotMatrix,speed","DotMatrix,pause","DotMatrix,font","DotMatrix,layout","DotMatrix,inverted","DotMatrix,specialeffect","DotMatrix,offset","DotMatrix,brightness","DotMatrix,repeat","DotMatrix,setbar","DotMatrix,bar","Thermo","Thermo,Up","Thermo,Down","Thermo,Mode","Thermo,ModeBtn","Thermo,Setpoint","Max1704xclearalert","scdgetabc","scdgetalt","scdgettmp","scdsetcalibration","scdsetfrc","scdgetinterval","multirelay","multirelay,on","multirelay,off","multirelay,set","multirelay,get","multirelay,loop","ShiftOut","ShiftOut,Set","ShiftOut,SetNoUpdate","ShiftOut,Update","ShiftOut,SetAll","ShiftOut,SetAllNoUpdate","ShiftOut,SetAllLow","ShiftOut,SetAllHigh","ShiftOut,SetChipCount","ShiftOut,SetHexBin","cdmrst","nfx","nfx,off","nfx,on","nfx,dim","nfx,line,","nfx,hsvline,","nfx,one,","nfx,hsvone,","nfx,all,","nfx,rgb,","nfx,fade,","nfx,hsv,","nfx,colorfade,","nfx,rainbow","nfx,kitt,","nfx,comet,","nfx,theatre,","nfx,scan,","nfx,dualscan,","nfx,twinkle,","nfx,twinklefade,","nfx,sparkle,","nfx,wipe,","nfx,dualwipe","nfx,fire","nfx,fireflicker","nfx,faketv","nfx,simpleclock","nfx,stop","nfx,statusrequest","nfx,fadetime,","nfx,fadedelay,","nfx,speed,","nfx,count,","nfx,bgcolor","ShiftIn","ShiftIn,PinEvent","ShiftIn,ChipEvent","ShiftIn,SetChipCount","ShiftIn,SampleFrequency","ShiftIn,EventPerPin","scd4x","scd4x,storesettings","scd4x,facoryreset","scd4x,selftest","scd4x,setfrc,","axp","axp,ldo2","axp,ldo3","axp,ldoio","axp,gpio0","axp,gpio1","axp,gpio2","axp,gpio3","axp,gpio4","axp,dcdc2","axp,dcdc3","axp,ldo2map","axp,ldo3map","axp,ldoiomap","axp,dcdc2map","axp,dcdc3map","axp,ldo2perc","axp,ldo3perc","axp,ldoioperc","axp,dcdc2perc","axp,dcdc3perc","I2CEncoder","I2CEncoder,bright","I2CEncoder,led1","I2CEncoder,led2","I2CEncoder,gain","I2CEncoder,set","cachereader","cachereader,readpos","cachereader,sendtaskinfo","cachereader,flush","tm1621","tm1621,write,","tm1621,writerow,","tm1621,voltamp,","tm1621,energy,","tm1621,celcius,","tm1621,fahrenheit,","tm1621,humidity,","tm1621,raw,","dac","dac,1","dac,2","sht4x","sht4x,startup","ld2410","ld2410,factoryreset","ld2410,logall","digipot","digipot,reset","digipot,shutdown","digipot,","gp8403","gp8403,volt,","gp8403,mvolt,","gp8403,range,","gp8403,preset,","gp8403,init,","sen5x","sen5x,startclean","sen5x,techlog,",],pluginDispKind=["tft","ili9341","ili9342","ili9481","ili9486","ili9488","epd","eink","epaper","il3897","uc8151d","ssd1680","ws2in7","ws1in54","st77xx","st7735","st7789","st7796","neomatrix","neo","pcd8544",],pluginDispCmd=["cmd,on","cmd,off","cmd,clear","cmd,backlight","cmd,bright","cmd,deepsleep","cmd,seq_start","cmd,seq_end","cmd,inv","cmd,rot",",clear",",rot",",tpm",",txt",",txp",",txz",",txc",",txs",",txtfull",",asciitable",",font",",l",",lh",",lv",",lm",",lmr",",r",",rf",",c",",cf",",rf",",t",",tf",",rr",",rrf",",px",",pxh",",pxv",",bmp",",btn",",win",",defwin",",delwin",],commonTag=["On","Do","Endon"],commonNumber=["toBin","toHex","Constrain","XOR","AND:","OR:","Ord","bitRead","bitSet","bitClear","bitWrite","urlencode"],commonMath=["Log","Ln","Abs","Exp","Sqrt","Sq","Round","Sin","Cos","Tan","aSin","aCos","aTan","Sin_d","Cos_d","Tan_d","aSin_d","aCos_d","aTan_d"],commonWarning=["delay","Delay","ResetFlashWriteCounter"],taskSpecifics=["settings.Enabled","settings.Interval","settings.ValueCount","settings.Controller1.Enabled","settings.Controller2.Enabled","settings.Controller3.Enabled","settings.Controller1.Idx","settings.Controller2.Idx","settings.Controller3.Idx"],AnythingElse=["%eventvalue%","%eventpar%","%eventname%","%sysname%","%bootcause%","%systime%","%systm_hm%","%systm_hm_0%","%systm_hm_sp%","%systime_am%","%systime_am_0%","%systime_am_sp%","%systm_hm_am%","%systm_hm_am_0%","%systm_hm_am_sp%","%lcltime%","%sunrise%","%s_sunrise%","%m_sunrise%","%sunset%","%s_sunset%","%m_sunset%","%lcltime_am%","%syshour%","%syshour_0%","%sysmin%","%sysmin_0%","%syssec%","%syssec_0%","%sysday%","%sysday_0%","%sysmonth%","%sysmonth_0%","%sysyear%","%sysyear_0%","%sysyears%","%sysweekday%","%sysweekday_s%","%unixtime%","%uptime%","%uptime_ms%","%rssi%","%ip%","%unit%","%unit_0%","%ssid%","%bssid%","%wi_ch%","%iswifi%","%vcc%","%mac%","%mac_int%","%isntp%","%ismqtt%","%dns%","%dns1%","%dns2%","%flash_freq%","%flash_size%","%flash_chip_vendor%","%flash_chip_model%","%fs_free%","%fs_size%","%cpu_id%","%cpu_freq%","%cpu_model%","%cpu_rev%","%cpu_cores%","%board_name%","%inttemp%","substring","indexOf","indexOf_ci","equals","equals_ci","strtol","timeToMin","timeToSec","%ethwifimode%","%ethconnected%","%ethduplex%","%ethspeed%","%ethstate%","%ethspeedstate%","%c_w_dir%","%c_c2f%","%c_ms2Bft%","%c_dew_th%","%c_alt_pres_sea%","%c_sea_pres_alt%","%c_cm2imp%","%c_mm2imp%","%c_m2day%","%c_m2dh%","%c_m2dhm%","%c_s2dhms%","%c_2hex%","%c_u2ip%","%c_uname%","%c_uage%","%c_ubuild%","%c_ubuildstr%","%c_uload%","%c_utype%","%c_utypestr%","var","int"];for(const element2 of pluginDispKind)commonPlugins=commonPlugins.concat(element2);for(const element2 of pluginDispKind)for(const element3 of pluginDispCmd){let e=element2+element3;commonPlugins=commonPlugins.concat(e)}var EXTRAWORDS=commonAtoms.concat(commonPlugins,commonKeywords,commonCommands,commonEvents,commonTag,commonNumber,commonMath,commonWarning,taskSpecifics,AnythingElse);function initCM(){CodeMirror.commands.autocomplete=function(e){e.showHint({hint:CodeMirror.hint.anyword})},(rEdit=CodeMirror.fromTextArea(document.getElementById("rules"),{tabSize:2,indentWithTabs:!1,lineNumbers:!0,autoCloseBrackets:!0,extraKeys:{"Ctrl-Space":"autocomplete",Tab(e){"null"===e.getMode().name?e.execCommand("insertTab"):e.somethingSelected()?e.execCommand("indentMore"):e.execCommand("insertSoftTab")},"Shift-Tab":e=>e.execCommand("indentLess")}})).on("change",function(){rEdit.save()}),rEdit.on("inputRead",function(e,t){var n=e.getCursor(),o=e.getTokenAt(n);/[\w%,.]/.test(t.text)&&"comment"!=o.type&&e.showHint({completeSingle:!1})})}!function(e){"object"==typeof exports&&"object"==typeof module?e(require("codemirror")):"function"==typeof define&&define.amd?define(["codemirror"],e):e(CodeMirror)}(function(e){"use strict";e.defineMode("espeasy",function(){var e={};function t(t,n){for(var o=0;oe.toLowerCase());commonCommands=commonCommands.concat(n);var o=commonEvents.map(e=>e.toLowerCase());commonEvents=commonEvents.concat(o);var i=commonPlugins.map(e=>e.toLowerCase());commonPlugins=commonPlugins.concat(i);var a=commonAtoms.map(e=>e.toLowerCase());commonAtoms=commonAtoms.concat(a);var s=commonKeywords.map(e=>e.toLowerCase());commonKeywords=commonKeywords.concat(s);var r=commonTag.map(e=>e.toLowerCase());commonTag=commonTag.concat(r);var c=commonNumber.map(e=>e.toLowerCase());commonNumber=commonNumber.concat(c);var l=commonMath.map(e=>e.toLowerCase());commonMath=commonMath.concat(l);var m=AnythingElse.map(e=>e.toLowerCase());AnythingElse=AnythingElse.concat(m);var d=taskSpecifics.map(e=>e.toLowerCase());function u(t,n){if(t.eatSpace())return null;t.sol();var o=t.next();if(/\d/.test(o)){if("0"==o)return"x"===t.next()?(t.eatWhile(/\w/),"number"):(t.eatWhile(/\d|\./),"number");if(t.eatWhile(/\d|\./),!t.match("d")&&!t.match("output")&&(t.eol()||/\D/.test(t.peek())))return"number"}if(/\w/.test(o))for(let i of EXTRAWORDS){let a=i.substring(1);(i.includes(":")||i.includes(",")||i.includes("."))&&t.match(a)}if(/\w/.test(o)&&(t.eatWhile(/[\w]/),t.match(".gpio")||t.match(".pulse")||t.match(".frq")||t.match(".pwm")))return"def";if("\\"===o)return t.next(),null;if("("===o||")"===o)return"bracket";if("{"===o||"}"===o||":"===o)return"number";if("/"==o)return/\//.test(t.peek())?(t.skipToEnd(),"comment"):"operator";if("'"==o&&(t.eatWhile(/[^']/),t.match("'")))return"attribute";if("+"===o||"="===o||"<"===o||">"===o||"-"===o||","===o||"*"===o||"!"===o)return"operator";if("%"==o){if(/\d/.test(t.next()))return"number";if(t.eatWhile(/[^\s\%]/),t.match("%"))return"hr"}if("["==o&&(t.eatWhile(/[^\s\]]/),t.eat("]")))return"hr";t.eatWhile(/\w/);var s=t.current();return/\w/.test(o)&&t.match("#")?(t.eatWhile(/[\w.#]/),"events"):"#"===o?(t.eatWhile(/\w/),"number"):e.hasOwnProperty(s)?e[s]:null}return taskSpecifics=taskSpecifics.concat(d),t("atom",commonAtoms),t("keyword",commonKeywords),t("builtin",commonCommands),t("events",commonEvents),t("def",commonPlugins),t("tag",commonTag),t("number",commonNumber),t("bracket",commonMath),t("warning",commonWarning),t("hr",AnythingElse),t("comment",taskSpecifics),{startState:function(){return{tokens:[]}},token:function(e,t){var n,o;return n=e,((o=t).tokens[0]||u)(n,o)},closeBrackets:"[]{}''\"\"``()",lineComment:"//",fold:"brace"}})}),function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],mod):e(CodeMirror)}(function(e){var t={pairs:"()[]{}''\"\"",closeBefore:")]}'\":;>",triples:"",explode:"[]{}"},n=e.Pos;function o(e,n){return"pairs"==n&&"string"==typeof e?e:"object"==typeof e&&null!=e[n]?e[n]:t[n]}e.defineOption("autoCloseBrackets",!1,function(t,n,s){s&&s!=e.Init&&(t.removeKeyMap(i),t.state.closeBrackets=null),n&&(a(o(n,"pairs")),t.state.closeBrackets=n,t.addKeyMap(i))});var i={Backspace:function t(i){var a=r(i);if(!a||i.getOption("disableInput"))return e.Pass;for(var s=o(a,"pairs"),c=i.listSelections(),l=0;l=0;l--){var u=c[l].head;i.replaceRange("",n(u.line,u.ch-1),n(u.line,u.ch+1),"+delete")}},Enter:function t(n){var i=r(n),a=i&&o(i,"explode");if(!a||n.getOption("disableInput"))return e.Pass;for(var s=n.listSelections(),l=0;l=0&&i.getRange(b,n(b.line,b.ch+3))==a+a+a?"skipThree":"skip";else if(h&&b.ch>1&&p.indexOf(a)>=0&&i.getRange(n(b.line,b.ch-2),b)==a+a){if(b.ch>2&&/\bstring/.test(i.getTokenTypeAt(n(b.line,b.ch-2))))return e.Pass;P="addFour"}else if(h){var M=0==b.ch?" ":i.getRange(n(b.line,b.ch-1),b);if(e.isWordChar(y)||M==a||e.isWordChar(M))return e.Pass;P="both"}else{if(!(x&&(0===y.length||/\s/.test(y)||f.indexOf(y)>-1)))return e.Pass;P="both"}if(S){if(S!=P)return e.Pass}else S=P}var v=u%2?m.charAt(u-1):a,D=u%2?a:m.charAt(u+1);i.operation(function(){if("skip"==S)c(i,1);else if("skipThree"==S)c(i,3);else if("surround"==S){for(var e=i.getSelections(),t=0;t0?{line:s.head.line,ch:s.head.ch+t}:{line:s.head.line-1};n.push({anchor:r,head:r})}e.setSelections(n,i)}function l(t){var o=e.cmpPos(t.anchor,t.head)>0;return{anchor:new n(t.anchor.line,t.anchor.ch+(o?-1:1)),head:new n(t.head.line,t.head.ch+(o?1:-1))}}function m(e,t){var o=e.getRange(n(t.line,t.ch-1),n(t.line,t.ch+1));return 2==o.length?o:null}function d(e,t){var o=e.getTokenAt(n(t.line,t.ch+1));return/\bstring/.test(o.type)&&o.start==t.ch&&(0==t.ch||!/\bstring/.test(e.getTokenTypeAt(t)))}a(t.pairs+"`")}); \ No newline at end of file From efcfaa519027c36b8a03fd41f5b76373982bb29c Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 5 May 2024 22:23:24 +0200 Subject: [PATCH 095/113] [P167] Add documentation --- docs/source/Plugin/P167.rst | 144 ++++++++++++++++++ .../Plugin/P167_DeviceConfiguration.png | Bin 0 -> 77983 bytes docs/source/Plugin/P167_DeviceInfo.png | Bin 0 -> 9526 bytes docs/source/Plugin/P167_ModelTypeOptions.png | Bin 0 -> 14444 bytes .../Plugin/P167_NumberOutputValuesOptions.png | Bin 0 -> 14999 bytes docs/source/Plugin/P167_ValueOptions.png | Bin 0 -> 29315 bytes docs/source/Plugin/P167_commands.repl | 18 +++ docs/source/Plugin/P167_config_values.repl | 41 +++++ docs/source/Plugin/_Plugin.rst | 1 + docs/source/Plugin/_plugin_categories.repl | 2 +- docs/source/Plugin/_plugin_sets_overview.repl | 55 +++++-- .../Plugin/_plugin_substitutions_p16x.repl | 13 ++ docs/source/Reference/Command.rst | 5 + 13 files changed, 262 insertions(+), 17 deletions(-) create mode 100644 docs/source/Plugin/P167.rst create mode 100644 docs/source/Plugin/P167_DeviceConfiguration.png create mode 100644 docs/source/Plugin/P167_DeviceInfo.png create mode 100644 docs/source/Plugin/P167_ModelTypeOptions.png create mode 100644 docs/source/Plugin/P167_NumberOutputValuesOptions.png create mode 100644 docs/source/Plugin/P167_ValueOptions.png create mode 100644 docs/source/Plugin/P167_commands.repl create mode 100644 docs/source/Plugin/P167_config_values.repl diff --git a/docs/source/Plugin/P167.rst b/docs/source/Plugin/P167.rst new file mode 100644 index 0000000000..fc713a9af1 --- /dev/null +++ b/docs/source/Plugin/P167.rst @@ -0,0 +1,144 @@ +.. include:: ../Plugin/_plugin_substitutions_p16x.repl +.. _P167_page: + +|P167_typename| +================================================== + +|P167_shortinfo| + +Plugin details +-------------- + +Type: |P167_type| + +Name: |P167_name| + +Status: |P167_status| + +GitHub: |P167_github|_ + +Maintainers: |P167_maintainer| + +Used libraries: |P167_usedlibraries| + +Description +----------- + +The Sensirion SEN5x series of sensors measure Particle matter (all models), Temperature, Humidity and tVOC (SEN54, SEN55) and NOx (SEN55). + +This plugin can read all values from these sensors. + +In addition, the IKEA Vindstryka, that has a Sensirion SEN54 installed, can be 'piggy-backed' with this plugin, eavesdropping on the I2C communication to retrieve the values. + +Hardware setup +-------------- + +When connecting a stand-alone Sensirion SEN5x sensor, be sure to power the unit with 5V! The I2C SDA/SCL signals are 3.3V safe, so these can be directly connected to an ESP. Pull-up resistors to 3.3V might be needed, as they are not installed on the device! + +When installing an ESP inside an IKEA Vindstyrka, the procedure of wiring this is rather simple: Connect GND, SDA and SCL to the configured ESP pins (VCC for the ESP can also be reused from the power source for the Vindstryka), connect the **MonPin SCL** configured GPIO pin *also* to SCL, and the plugin will wait for the IKEA controller to finish communication and then fetch the data from the sensor. + +Configuration +------------- + +.. image:: P167_DeviceConfiguration.png + :alt: Device configuration + + +* **Name**: Required by ESPEasy, must be unique among the list of available devices/tasks. + +* **Enabled**: The device can be disabled or enabled. When not enabled the device should not use any resources. + +I2C options +^^^^^^^^^^^ + +The available settings here depend on the build used. At least the **Force Slow I2C speed** option is available, but selections for the I2C Multiplexer can also be shown. For details see the :ref:`Hardware_page` + +.. note:: According to the documentation, the SEN5x sensors support a max. I2C Clock Speed of 100 kHz, **Force Slow I2C speed** should be checked. (ESPEasy has a default setting of 100 kHz for I2C Slow device Clock Speed). + +Device Settings +^^^^^^^^^^^^^^^ + +* **Model Type**: Select the sensor model used, available options: + +.. image:: P167_ModelTypeOptions.png + :alt: Model Type options + +* *IKEA Vindstyrka* The default, with the ability to eavesdrop on the I2C communication of the Vindstyrka controller and the installed SEN54. + +* *SEN54* The stand-alone model, providing the same values as the Vindstyrka. This setting can also be used when installing a SEN50, but only the PM\* values can be used on that sensor. + +* *SEN55* The most advanced model, adding support for the NOx index. + +When this setting is changed, the page is saved and reloaded to show/hide the extra setting to configure **MonPin SCL**. + +* **MonPin SCL**: (Only available for IKEA Vindstryka) Select the GPIO pin that is connected to the I2C SCL pin. It is used to monitor the I2C communication by the IKEA controller, so we don't interfere with that. + +If the plugin is enabled, Device info for the sensor is shown in the Configuration page: + +.. image:: P167_DeviceInfo.png + +This includes any warnings received from the sensor, and counters for pass/fail/errCode. + +The fail value is usually 1 or 2, because of the startup time of the sensor, that often returns a failed reading. If this number increases, then a check for I2C pull-up resistors should be done, as that is a requirement for I2C devices, and also, short wires have to be used for stable communication. + +* **Technical logging**: When debugging the hardware setup, especially when installing an ESP inside an IKEA Vindstryka, it can be helpful to have some extra technical logging available (INFO level). + +Output Configuration +^^^^^^^^^^^^^^^^^^^^ + +* **Number Output Values**: Select the number of values that should be available, available options: + +.. image:: P167_NumberOutputValuesOptions.png + +.. .. + +* **Value 1..4**: For each of the output values configured, the value to show can be configured, some defaults have been preselected. Available options: + +.. image:: P167_ValueOptions.png + +A description per available value is documented below, in the **Get Config Values** section. + +N.B.: NOx is only available when a Sensirion SEN55 is installed, but is always shown as an option. + +N.B.2: The selected options determine the names of the output Values. + +Data Acquisition +^^^^^^^^^^^^^^^^ + +This group of settings, **Single event with all values**, **Send to Controller** and **Interval** settings are standard available configuration items. Send to Controller is only visible when one or more Controllers are configured. + +* **Interval** By default, Interval will be set to 60 sec. The data will be collected and optionally sent to any configured controllers using this interval. + +Values +^^^^^^ + +The plugin provides the configured values, with their default names. + +Per Value is a **Stats** checkbox available, that when checked, gathers the data and presents recent data in a graph, as described here: :ref:`Task Value Statistics: ` + +Commands available +^^^^^^^^^^^^^^^^^^ + +.. include:: P167_commands.repl + +Get Config Values +^^^^^^^^^^^^^^^^^ + +Get Config Values retrieves values or settings from the sensor or plugin, and can be used in Rules, Display plugins, Formula's etc. The square brackets **are** part of the variable. Replace ```` by the **Name** of the task. + +.. include:: P167_config_values.repl + +Change log +---------- + +.. versionchanged:: 2.0 + ... + + |added| 2024-05-05 Enable support for SEN54 and SEN55 stand-alone sensors, add Get Config Values and Commands. + + |added| 2024-04-15 Initial release version supporting IKEA Vindstyrka. + + + + + diff --git a/docs/source/Plugin/P167_DeviceConfiguration.png b/docs/source/Plugin/P167_DeviceConfiguration.png new file mode 100644 index 0000000000000000000000000000000000000000..6db0ae2f4dde77c3f55f6ae1d11dee48f69c4d60 GIT binary patch literal 77983 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

1zIW%0wotn2;!lwWQu$Yq)%O=w+ZO( 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 literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/P167_DeviceInfo.png b/docs/source/Plugin/P167_DeviceInfo.png new file mode 100644 index 0000000000000000000000000000000000000000..4a89e8f6079ae3f4aaa85c2d5a37859f4cab1ac5 GIT binary patch literal 9526 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 N(l@xTaBtwd(&!drPQt>iYjW? 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 XJ})9-NALbui1h@bqf)1275Tpa-1@RD literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/P167_NumberOutputValuesOptions.png b/docs/source/Plugin/P167_NumberOutputValuesOptions.png new file mode 100644 index 0000000000000000000000000000000000000000..d663799bbbda49e3fa8c95adfaeeb6f75a706007 GIT binary patch literal 14999 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 literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/P167_ValueOptions.png b/docs/source/Plugin/P167_ValueOptions.png new file mode 100644 index 0000000000000000000000000000000000000000..5aefb9d608e4f99ae5509e3e7c05928fd56830c3 GIT binary patch literal 29315 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 Rz|TdZJyuYcFO@Y3{$Kb^Xn+6! literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/P167_commands.repl b/docs/source/Plugin/P167_commands.repl new file mode 100644 index 0000000000..b43f468b3e --- /dev/null +++ b/docs/source/Plugin/P167_commands.repl @@ -0,0 +1,18 @@ +.. csv-table:: + :header: "Command Syntax", "Extra information" + :widths: 20, 30 + + " + | ``sen5x,Startclean`` + "," + | Starts the built-in cleaning cycle of the device. + + | N.B.: Normally this shouldn't be needed, as the device automatically starts a weekly cleaning cycle, but if the device is not switched on continuously, this timer is reset on power-on, but the device still needs a cleaning cycle once a week. + " + " + | ``sen5x,Techlog,<0|1>`` + "," + | Enable or disable the ``Technical logging`` option, available in the UI. This logging is mostly useful when debugging either the hardware or software setup. + + | The setting is *not* stored automatically. + " diff --git a/docs/source/Plugin/P167_config_values.repl b/docs/source/Plugin/P167_config_values.repl new file mode 100644 index 0000000000..2afe5fd11a --- /dev/null +++ b/docs/source/Plugin/P167_config_values.repl @@ -0,0 +1,41 @@ +.. csv-table:: + :header: "Config value", "Information" + :widths: 20, 30 + + " + | ``[#Temperature]`` + "," + | Returns the last measured Temperature in degrees Celcius. + " + " + | ``[#Humidity]`` + "," + | Returns the last measured Humidity (%RH). + " + " + | ``[#tVOC]`` + "," + | Returns the last measured tVOC (total volatile organic compounds) value in range 0..500 index points. Measurement range 0..1000 ppm. + " + " + | ``[#NOx]`` + "," + | Returns the last measured NOx value (Nitrogen Oxides) value in range 0..500 index points. + " + " + | ``[#PM1p0]`` + + | ``[#PM2p5]`` + + | ``[#PM4p0]`` + + | ``[#PM10p0]`` + + "," + | Returns the last measured Particle concentration in μg/m\ :sup:`3` for respectively 1.0 μm, 2.5 μm, 4.0 μm and 10.0 μm particle size. + " + " + | ``[#Dewpoint]`` + "," + | Returns the calculated Dew point, from the Temperature and Humidity values. + " diff --git a/docs/source/Plugin/_Plugin.rst b/docs/source/Plugin/_Plugin.rst index c289e6b639..0aaec036db 100644 --- a/docs/source/Plugin/_Plugin.rst +++ b/docs/source/Plugin/_Plugin.rst @@ -389,6 +389,7 @@ There are different released versions of ESP Easy: ":ref:`P162_page`","|P162_status|","P162" ":ref:`P164_page`","|P164_status|","P164" ":ref:`P166_page`","|P166_status|","P166" + ":ref:`P167_page`","|P167_status|","P167" .. include:: _plugin_sets_overview.repl diff --git a/docs/source/Plugin/_plugin_categories.repl b/docs/source/Plugin/_plugin_categories.repl index c244aedad1..f8ea29496f 100644 --- a/docs/source/Plugin/_plugin_categories.repl +++ b/docs/source/Plugin/_plugin_categories.repl @@ -8,7 +8,7 @@ .. |Plugin_Energy_AC| replace:: :ref:`P076_page`, :ref:`P077_page`, :ref:`P078_page`, :ref:`P102_page`, :ref:`P108_page` .. |Plugin_Energy_DC| replace:: :ref:`P027_page`, :ref:`P085_page`, :ref:`P115_page`, :ref:`P132_page` .. |Plugin_Energy_Heat| replace:: :ref:`P088_page`, :ref:`P093_page` -.. |Plugin_Environment| replace:: :ref:`P004_page`, :ref:`P005_page`, :ref:`P006_page`, :ref:`P014_page`, :ref:`P024_page`, :ref:`P028_page`, :ref:`P030_page`, :ref:`P031_page`, :ref:`P032_page`, :ref:`P034_page`, :ref:`P039_page`, :ref:`P047_page`, :ref:`P051_page`, :ref:`P068_page`, :ref:`P069_page`, :ref:`P072_page`, :ref:`P103_page`, :ref:`P105_page`, :ref:`P106_page`, :ref:`P122_page`, :ref:`P150_page`, :ref:`P151_page`, :ref:`P153_page`, :ref:`P154_page` +.. |Plugin_Environment| replace:: :ref:`P004_page`, :ref:`P005_page`, :ref:`P006_page`, :ref:`P014_page`, :ref:`P024_page`, :ref:`P028_page`, :ref:`P030_page`, :ref:`P031_page`, :ref:`P032_page`, :ref:`P034_page`, :ref:`P039_page`, :ref:`P047_page`, :ref:`P051_page`, :ref:`P068_page`, :ref:`P069_page`, :ref:`P072_page`, :ref:`P103_page`, :ref:`P105_page`, :ref:`P106_page`, :ref:`P122_page`, :ref:`P150_page`, :ref:`P151_page`, :ref:`P153_page`, :ref:`P154_page`, :ref:`P167_page` .. |Plugin_Extra_IO| replace:: :ref:`P011_page`, :ref:`P022_page` .. |Plugin_Gases| replace:: :ref:`P049_page`, :ref:`P052_page`, :ref:`P083_page`, :ref:`P090_page`, :ref:`P117_page`, :ref:`P127_page`, :ref:`P135_page`, :ref:`P145_page`, :ref:`P147_page`, :ref:`P164_page` .. |Plugin_Generic| replace:: :ref:`P003_page`, :ref:`P026_page`, :ref:`P033_page`, :ref:`P037_page`, :ref:`P081_page`, :ref:`P100_page`, :ref:`P146_page` diff --git a/docs/source/Plugin/_plugin_sets_overview.repl b/docs/source/Plugin/_plugin_sets_overview.repl index 1364a0a3da..e308ddae0f 100644 --- a/docs/source/Plugin/_plugin_sets_overview.repl +++ b/docs/source/Plugin/_plugin_sets_overview.repl @@ -316,20 +316,26 @@ Build set: :yellow:`COLLECTION C` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" + ":ref:`P007_page`","P007" + ":ref:`P008_page`","P008" + ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" + ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" + ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" + ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -340,6 +346,9 @@ Build set: :yellow:`COLLECTION C` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" + ":ref:`P041_page`","P041" + ":ref:`P042_page`","P042" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P045_page`","P045" @@ -513,26 +522,20 @@ Build set: :yellow:`COLLECTION E` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" - ":ref:`P007_page`","P007" - ":ref:`P008_page`","P008" - ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" - ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" - ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" - ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -543,9 +546,6 @@ Build set: :yellow:`COLLECTION E` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" - ":ref:`P040_page`","P040" - ":ref:`P041_page`","P041" - ":ref:`P042_page`","P042" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P045_page`","P045" @@ -623,20 +623,26 @@ Build set: :yellow:`COLLECTION F` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" + ":ref:`P007_page`","P007" + ":ref:`P008_page`","P008" + ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" + ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" + ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" + ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -647,6 +653,9 @@ Build set: :yellow:`COLLECTION F` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" + ":ref:`P041_page`","P041" + ":ref:`P042_page`","P042" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P045_page`","P045" @@ -723,20 +732,26 @@ Build set: :yellow:`COLLECTION G` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" + ":ref:`P007_page`","P007" + ":ref:`P008_page`","P008" + ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" + ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" + ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" + ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -747,6 +762,9 @@ Build set: :yellow:`COLLECTION G` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" + ":ref:`P041_page`","P041" + ":ref:`P042_page`","P042" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P045_page`","P045" @@ -878,6 +896,7 @@ Build set: :yellow:`CLIMATE` ":ref:`P153_page`","P153" ":ref:`P154_page`","P154" ":ref:`P164_page`","P164" + ":ref:`P167_page`","P167" ":ref:`C001_page`","C001" ":ref:`C002_page`","C002" ":ref:`C003_page`","C003" @@ -888,6 +907,7 @@ Build set: :yellow:`CLIMATE` ":ref:`C008_page`","C008" ":ref:`C009_page`","C009" ":ref:`C010_page`","C010" + ":ref:`C011_page`","C011" ":ref:`C013_page`","C013" Build set: :yellow:`DISPLAY` @@ -906,20 +926,26 @@ Build set: :yellow:`DISPLAY` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" + ":ref:`P007_page`","P007" + ":ref:`P008_page`","P008" + ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" + ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" + ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" + ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -930,6 +956,9 @@ Build set: :yellow:`DISPLAY` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" + ":ref:`P041_page`","P041" + ":ref:`P042_page`","P042" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P049_page`","P049" @@ -1190,26 +1219,20 @@ Build set: :yellow:`NEOPIXEL` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" - ":ref:`P007_page`","P007" - ":ref:`P008_page`","P008" - ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" - ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" - ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" - ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -1220,7 +1243,6 @@ Build set: :yellow:`NEOPIXEL` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" - ":ref:`P040_page`","P040" ":ref:`P041_page`","P041" ":ref:`P042_page`","P042" ":ref:`P043_page`","P043" @@ -1411,6 +1433,7 @@ Build set: :yellow:`MAX` ":ref:`P162_page`","P162" ":ref:`P164_page`","P164" ":ref:`P166_page`","P166" + ":ref:`P167_page`","P167" ":ref:`C001_page`","C001" ":ref:`C002_page`","C002" ":ref:`C003_page`","C003" diff --git a/docs/source/Plugin/_plugin_substitutions_p16x.repl b/docs/source/Plugin/_plugin_substitutions_p16x.repl index 13817a1baf..7cd0eb5fa1 100644 --- a/docs/source/Plugin/_plugin_substitutions_p16x.repl +++ b/docs/source/Plugin/_plugin_substitutions_p16x.repl @@ -36,3 +36,16 @@ .. |P166_maintainer| replace:: `tonhuisman` .. |P166_compileinfo| replace:: `.` .. |P166_usedlibraries| replace:: `modified version of DFRobot_GP8403` + +.. |P167_name| replace:: :cyan:`Sensirion SEN5x (IKEA Vindstyrka)` +.. |P167_type| replace:: :cyan:`Environment` +.. |P167_typename| replace:: :cyan:`Environment - Sensirion SEN5x (IKEA Vindstyrka)` +.. |P167_porttype| replace:: `.` +.. |P167_status| replace:: :yellow:`CLIMATE` +.. |P167_github| replace:: P167_Vindstyrka.ino +.. _P167_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P167_Vindstyrka.ino +.. |P167_usedby| replace:: `.` +.. |P167_shortinfo| replace:: `Sensirion SEN5x (IKEA Vindstyrka)` +.. |P167_maintainer| replace:: `AndiBaciu, tonhuisman` +.. |P167_compileinfo| replace:: `.` +.. |P167_usedlibraries| replace:: `.` diff --git a/docs/source/Reference/Command.rst b/docs/source/Reference/Command.rst index ce8effc278..fd84381300 100644 --- a/docs/source/Reference/Command.rst +++ b/docs/source/Reference/Command.rst @@ -805,6 +805,11 @@ P166 :ref:`P166_page` .. include:: ../Plugin/P166_commands.repl +P167 :ref:`P167_page` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. include:: ../Plugin/P167_commands.repl + .. .. *** Insert regular plugin commands above this remark! *** .. _AdafruitGFX Helper commands: From 784888be85f8f99c6984d0273f478d3c834d3656 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sun, 5 May 2024 22:23:56 +0200 Subject: [PATCH 096/113] [C011] Documentation correction --- docs/source/Controller/_controller_substitutions.repl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/Controller/_controller_substitutions.repl b/docs/source/Controller/_controller_substitutions.repl index 5ee958e394..0683e988a7 100644 --- a/docs/source/Controller/_controller_substitutions.repl +++ b/docs/source/Controller/_controller_substitutions.repl @@ -114,7 +114,7 @@ .. |C011_name| replace:: :cyan:`Generic HTTP Advanced` .. |C011_type| replace:: :cyan:`Controller` .. |C011_typename| replace:: :cyan:`Controller - Generic HTTP Advanced` -.. |C011_status| replace:: :yellow:`COLLECTION` +.. |C011_status| replace:: :yellow:`COLLECTION` :yellow:`CLIMATE` .. |C011_github| replace:: C011.cpp .. _C011_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_C011.cpp .. |C011_usedby| replace:: `.` From 5cd0d9531cf95f21703cc873b7d217e8fac42758 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 7 May 2024 20:35:35 +0200 Subject: [PATCH 097/113] [AdaGFX_Helper] Fix typo in `default` font definition --- src/src/Helpers/AdafruitGFX_helper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index 276a6f9d65..1407b1ea89 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -970,7 +970,7 @@ struct tFontArgs { /* *INDENT-OFF* */ constexpr tFontArgs fontargs[] = { - { nullptr, 9, 6, 0, false, 0u }, + { nullptr, 6, 9, 0, false, 0u }, { &Seven_Segment24pt7b, 21, 42, 35, true, 1u }, { &Seven_Segment18pt7b, 16, 33, 26, true, 2u }, { &FreeSans9pt7b, 10, 16, 12, false, 3u }, From 473625daf8c8f9ca95f8e8dc489e965f8a565cf9 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 8 May 2024 21:59:29 +0200 Subject: [PATCH 098/113] [AdaGFX_Helper] Fix merge conflict --- src/src/Helpers/AdafruitGFX_helper.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index f90a833df2..b389860b82 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -2211,6 +2211,14 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } } break; + case adagfx_commands_e::delwin: // delwin: delete window, don't delete window 0 + + if ((argCount == 1) && (nParams[0] > 0)) { + // logWindows(F(" deLwin ")); // use for debugging only + + success = deleteWindow(nParams[0]); + } + break; # endif // if ADAGFX_ENABLE_FRAMED_WINDOW } return success; From 80b7baadda682db1c13c4c0e7f588adb7d4e9976 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 8 May 2024 22:01:19 +0200 Subject: [PATCH 099/113] [AdaGFX_Helper] Fix font offset issues --- lib/Adafruit_GFX_Library/Fonts/TomThumb.h | 2 + src/src/Helpers/AdafruitGFX_helper.cpp | 100 +++++++++++++--------- src/src/Helpers/AdafruitGFX_helper.h | 7 +- 3 files changed, 68 insertions(+), 41 deletions(-) diff --git a/lib/Adafruit_GFX_Library/Fonts/TomThumb.h b/lib/Adafruit_GFX_Library/Fonts/TomThumb.h index 7d49239cf4..2d855086b6 100644 --- a/lib/Adafruit_GFX_Library/Fonts/TomThumb.h +++ b/lib/Adafruit_GFX_Library/Fonts/TomThumb.h @@ -49,7 +49,9 @@ ** Andreas Merkle (web@blue-andi.de) */ +#ifndef TOMTHUMB_USE_EXTENDED #define TOMTHUMB_USE_EXTENDED 0 +#endif const uint8_t TomThumbBitmaps[] PROGMEM = { 0x00, /* 0x20 space */ diff --git a/src/src/Helpers/AdafruitGFX_helper.cpp b/src/src/Helpers/AdafruitGFX_helper.cpp index b389860b82..762aa22899 100644 --- a/src/src/Helpers/AdafruitGFX_helper.cpp +++ b/src/src/Helpers/AdafruitGFX_helper.cpp @@ -1042,13 +1042,13 @@ constexpr tFontArgs fontargs[] = { &whitrabt12pt7b, 13, 20, 16, false, 18u }, # endif // ifdef ADAGFX_FONTS_EXTRA_12PT_WHITERABBiT # ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTO - { &Roboto_Regular12pt7b, 13, 20, 16, true, 19u }, + { &Roboto_Regular12pt7b, 13, 20, 20, true, 19u }, # endif // ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTO # ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOCONDENSED - { &RobotoCondensed_Regular12pt7b, 13, 20, 16, true, 20u }, + { &RobotoCondensed_Regular12pt7b, 13, 20, 20, true, 20u }, # endif // ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOCONDENSED # ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOMONO - { &RobotoMono_Regular12pt7b, 13, 20, 16, false, 21u }, + { &RobotoMono_Regular12pt7b, 13, 20, 20, false, 21u }, # endif // ifdef ADAGFX_FONTS_EXTRA_12PT_ROBOTOMONO # endif // ifdef ADAGFX_FONTS_EXTRA_12PT_INCLUDED # ifdef ADAGFX_FONTS_EXTRA_16PT_INCLUDED @@ -1073,10 +1073,10 @@ constexpr tFontArgs fontargs[] = { &whitrabt18pt7b, 21, 30, 26, false, 27u }, # endif // ifdef ADAGFX_FONTS_EXTRA_18PT_WHITERABBiT # ifdef ADAGFX_FONTS_EXTRA_18PT_SEVENSEG_B - { &_7segment18pt7b, 21, 30, 0, false, 28u }, + { &_7segment18pt7b, 21, 30, 30, false, 28u }, # endif // ifdef ADAGFX_FONTS_EXTRA_18PT_SEVENSEG_B # ifdef ADAGFX_FONTS_EXTRA_18PT_LCD14COND - { &LCD14cond18pt7b, 24, 30, 0, false, 29u }, + { &LCD14cond18pt7b, 24, 30, 30, false, 29u }, # endif // ifdef ADAGFX_FONTS_EXTRA_18PT_LCD14COND # endif // ifdef ADAGFX_FONTS_EXTRA_18PT_INCLUDED # ifdef ADAGFX_FONTS_EXTRA_20PT_INCLUDED @@ -1086,39 +1086,38 @@ constexpr tFontArgs fontargs[] = # endif // ifdef ADAGFX_FONTS_EXTRA_20PT_INCLUDED # ifdef ADAGFX_FONTS_EXTRA_24PT_INCLUDED # ifdef ADAGFX_FONTS_EXTRA_24PT_SEVENSEG_B - { &_7segment24pt7b, 26, 34, 0, false, 31u }, + { &_7segment24pt7b, 26, 34, 38, false, 31u }, # endif // ifdef ADAGFX_FONTS_EXTRA_24PT_SEVENSEG_B # ifdef ADAGFX_FONTS_EXTRA_24PT_LCD14COND - { &LCD14cond24pt7b, 26, 34, 0, false, 32u }, + { &LCD14cond24pt7b, 26, 34, 38, false, 32u }, # endif // ifdef ADAGFX_FONTS_EXTRA_24PT_LCD14COND # endif // ifdef ADAGFX_FONTS_EXTRA_24PT_INCLUDED }; /* *INDENT-ON* */ # endif // if ADAGFX_FONTS_INCLUDED -String AdaGFXgetFontName(uint8_t fontId) { +String AdaGFXgetFontName(uint8_t fontId, bool includeFontId) { # if ADAGFX_FONTS_INCLUDED - constexpr uint32_t font_max = NR_ELEMENTS(fontargs); + const uint32_t idx = AdaGFXgetFontIndexForFontId(fontId); + char tmp[30]{}; // Longest name so far is 23 + \0 + String fontName(GetTextIndexed(tmp, sizeof(tmp), idx, adagfx_fonts)); - if (fontId < font_max) { - const uint32_t idx = AdaGFXgetFontIndexForFontId(fontId); - char tmp[30]{}; // Longest name so far is 23 + \0 - String fontName(GetTextIndexed(tmp, sizeof(tmp), idx, adagfx_fonts)); - return fontName; + if (includeFontId) { + fontName = strformat(F("%s (%d)"), tmp, fontargs[idx]._fontId); } - # endif // if ADAGFX_FONTS_INCLUDED + return fontName; + # else // if ADAGFX_FONTS_INCLUDED return EMPTY_STRING; + # endif // if ADAGFX_FONTS_INCLUDED } uint32_t AdaGFXgetFontIndexForFontId(uint8_t fontId) { # if ADAGFX_FONTS_INCLUDED constexpr uint32_t font_max = NR_ELEMENTS(fontargs); - if (fontId < font_max) { - for (uint32_t idx = 0; idx < font_max; ++idx) { - if (fontargs[idx]._fontId == fontId) { - return idx; - } + for (uint32_t idx = 0; idx < font_max; ++idx) { + if (fontargs[idx]._fontId == fontId) { + return idx; } } # endif // if ADAGFX_FONTS_INCLUDED @@ -1137,8 +1136,8 @@ void AdaGFXFormDefaultFont(const __FlashStringHelper *id, for (uint32_t idx = 0; idx < font_max; ++idx) { const bool selected = (fontargs[idx]._fontId == selectedIndex); - String fontName(GetTextIndexed(tmp, sizeof(tmp), idx, adagfx_fonts)); - addSelector_Item(fontName, + GetTextIndexed(tmp, sizeof(tmp), idx, adagfx_fonts); + addSelector_Item(strformat(F("%s (%d)"), tmp, fontargs[idx]._fontId), fontargs[idx]._fontId, selected); } @@ -1152,6 +1151,7 @@ void AdafruitGFX_helper::setFontById(uint8_t fontId) { const int font_i = AdaGFXgetFontIndexForFontId(fontId); if ((font_i >= 0) && (font_i < font_max)) { + _fontId = fontargs[font_i]._fontId; _display->setFont(fontargs[font_i]._f); calculateTextMetrics(fontargs[font_i]._width, fontargs[font_i]._height, @@ -1496,12 +1496,19 @@ bool AdafruitGFX_helper::processCommand(const String& string) { # if ADAGFX_FONTS_INCLUDED if (argCount == 1) { + int font_i = 0; sParams[0].toLowerCase(); constexpr int font_max = NR_ELEMENTS(fontargs); - const int font_i = GetCommandCode(sParams[0].c_str(), adagfx_fonts); + + if ((nParams[0] > 0) || equals(sParams[0], F("0"))) { + font_i = AdaGFXgetFontIndexForFontId(nParams[0]); // Set font by fontId + } else { + font_i = GetCommandCode(sParams[0].c_str(), adagfx_fonts); + } if ((font_i >= 0) && (font_i < font_max)) { + _fontId = fontargs[font_i]._fontId; _display->setFont(fontargs[font_i]._f); calculateTextMetrics(fontargs[font_i]._width, fontargs[font_i]._height, @@ -1848,7 +1855,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { const Button_layout_e buttonLayout = static_cast(nParams[7] & 0xF0); // Check mode & state: -2, -1, 0, 1 to select used colors - # if ADAGFX_ENABLE_BUTTON_SLIDER + # if ADAGFX_ENABLE_BUTTON_SLIDER if (buttonLayout == Button_layout_e::Slider) { if (nParams[1] == -2) { @@ -1859,7 +1866,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { borderColor = _bgcolor; } } else - # endif // if ADAGFX_ENABLE_BUTTON_SLIDER + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER { if (nParams[0] == 0) { fillColor = offColor; @@ -1878,9 +1885,9 @@ bool AdafruitGFX_helper::processCommand(const String& string) { if ((buttonType != Button_type_e::None) || clearArea) { drawButtonShape( - # if ADAGFX_ENABLE_BUTTON_SLIDER + # if ADAGFX_ENABLE_BUTTON_SLIDER buttonLayout == Button_layout_e::Slider ? Button_type_e::Square : - # endif // if ADAGFX_ENABLE_BUTTON_SLIDER + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER buttonType, // Clear full square for slider nParams[2] + _xo, nParams[3] + _yo, nParams[4], nParams[5], _bgcolor, _bgcolor); @@ -1902,9 +1909,9 @@ bool AdafruitGFX_helper::processCommand(const String& string) { // Determine alignment parameters if ((nParams[0] == 1) || (nParams[0] == -1) // 1 = on+enabled, -1 = on+disabled - # if ADAGFX_ENABLE_BUTTON_SLIDER + # if ADAGFX_ENABLE_BUTTON_SLIDER || (buttonLayout == Button_layout_e::Slider) - # endif // if ADAGFX_ENABLE_BUTTON_SLIDER + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER ) { newString = sParams[12].isEmpty() ? sParams[6] : sParams[12]; } else { @@ -1956,7 +1963,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { nParams[2] += w2 / 2; // A little margin from left nParams[3] += (nParams[5] - h1 * 1.5); // bottom align + a little margin break; - # if ADAGFX_ENABLE_BMP_DISPLAY + # if ADAGFX_ENABLE_BMP_DISPLAY case Button_layout_e::Bitmap: { // Use ON/OFF caption to specify (full) bitmap filename if (!newString.isEmpty()) { @@ -1980,21 +1987,21 @@ bool AdafruitGFX_helper::processCommand(const String& string) { } break; } - # endif // if ADAGFX_ENABLE_BMP_DISPLAY + # endif // if ADAGFX_ENABLE_BMP_DISPLAY case Button_layout_e::NoCaption: - # if ADAGFX_ENABLE_BUTTON_SLIDER + # if ADAGFX_ENABLE_BUTTON_SLIDER case Button_layout_e::Slider: // Nothing to do here (yet) - # endif // if ADAGFX_ENABLE_BUTTON_SLIDER + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER break; } if ((buttonLayout != Button_layout_e::NoCaption) - # if ADAGFX_ENABLE_BUTTON_SLIDER + # if ADAGFX_ENABLE_BUTTON_SLIDER && (buttonLayout != Button_layout_e::Slider) - # endif // if ADAGFX_ENABLE_BUTTON_SLIDER - # if ADAGFX_ENABLE_BMP_DISPLAY + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER + # if ADAGFX_ENABLE_BMP_DISPLAY && (buttonLayout != Button_layout_e::Bitmap) - # endif // if ADAGFX_ENABLE_BMP_DISPLAY + # endif // if ADAGFX_ENABLE_BMP_DISPLAY ) { // Set position and colors, then print _display->setCursor(nParams[2] + _xo, nParams[3] + _yo); @@ -2004,7 +2011,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { // restore colors _display->setTextColor(_fgcolor, _bgcolor); } - # if ADAGFX_ENABLE_BUTTON_SLIDER + # if ADAGFX_ENABLE_BUTTON_SLIDER if (buttonLayout == Button_layout_e::Slider) { // 1) Determine direction from w/h @@ -2146,7 +2153,7 @@ bool AdafruitGFX_helper::processCommand(const String& string) { _display->print(newString); } } - # endif // if ADAGFX_ENABLE_BUTTON_SLIDER + # endif // if ADAGFX_ENABLE_BUTTON_SLIDER // restore font scaling _display->setTextSize(_fontscaling); @@ -2228,7 +2235,11 @@ bool AdafruitGFX_helper::processCommand(const String& string) { * Get a config value from the plugin ***************************************************************************/ # if ADAGFX_ENABLE_GET_CONFIG_VALUE -const char adagfx_getcommands[] PROGMEM = "win|iswin|width|height|length|textheight|rot|txs|tpm"; +const char adagfx_getcommands[] PROGMEM = "win|iswin|width|height|length|textheight|rot|txs|tpm" + # if ADAGFX_FONTS_INCLUDED + "|font" + # endif // if ADAGFX_FONTS_INCLUDED +; enum class adagfx_getcommands_e : int8_t { invalid = -1, win = 0, @@ -2240,6 +2251,9 @@ enum class adagfx_getcommands_e : int8_t { rot, txs, tpm, + # if ADAGFX_FONTS_INCLUDED + font, + # endif // if ADAGFX_FONTS_INCLUDED }; bool AdafruitGFX_helper::pluginGetConfigValue(String& string) { @@ -2325,6 +2339,12 @@ bool AdafruitGFX_helper::pluginGetConfigValue(String& string) { success = true; break; } + # if ADAGFX_FONTS_INCLUDED + case adagfx_getcommands_e::font: + string = AdaGFXgetFontName(_fontId); + success = true; + break; + # endif // if ADAGFX_FONTS_INCLUDED case adagfx_getcommands_e::invalid: break; } diff --git a/src/src/Helpers/AdafruitGFX_helper.h b/src/src/Helpers/AdafruitGFX_helper.h index 5c4ed90b8a..8bc6e20e7d 100644 --- a/src/src/Helpers/AdafruitGFX_helper.h +++ b/src/src/Helpers/AdafruitGFX_helper.h @@ -12,6 +12,9 @@ ***************************************************************************/ /************ * Changelog: + * 2024-05-07 tonhuisman: Correct font related functions, add [#font] to return the currently selected fontname + * Accept numeric font Ids to select a different font: ,font, + * Show font ID in Default font selector * 2024-04-17 tonhuisman: Add AdaGFXFormDefaultFont() selector and some support functions * Add default font selection at initialization * 2024-04-16 tonhuisman: Add font TomThumb, 3x5 pixel font to be used on a NeoMatrix 5x29 display. Disabled by LIMIT_BUILD_SIZE. @@ -438,7 +441,8 @@ uint16_t AdaGFXrgb565ToColor7(const uint16_t& color); // Convert rgb565 color to # endif // if ADAGFX_SUPPORT_7COLOR void AdaGFXFormLineSpacing(const __FlashStringHelper *id, uint8_t selectedIndex); -String AdaGFXgetFontName(uint8_t fontId); +String AdaGFXgetFontName(uint8_t fontId, + bool includeFontId = false); uint32_t AdaGFXgetFontIndexForFontId(uint8_t fontId); void AdaGFXFormDefaultFont(const __FlashStringHelper *id, uint8_t selectedIndex); @@ -610,6 +614,7 @@ class AdafruitGFX_helper { int8_t _rotation = 0; bool _displayInverted = false; int8_t _lineSpacing = 15; // Default fontheight * fontsize + uint8_t _fontId = 0; uint16_t _display_x = 0; uint16_t _display_y = 0; From 015cd51801bb5cd20243219369a7244e4d76e293 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Fri, 10 May 2024 22:17:24 +0200 Subject: [PATCH 100/113] [P080] Add Event with iButton address --- docs/source/Plugin/P080.rst | 59 +++++++++++- .../Plugin/P080_DeviceConfiguration.png | Bin 0 -> 40596 bytes docs/source/Plugin/P080_events.repl | 32 +++++++ docs/source/Plugin/P080_iButtonExamples.png | Bin 0 -> 239573 bytes docs/source/Plugin/_plugin_sets_overview.repl | 53 +++++++---- src/_P080_DallasIButton.ino | 86 +++++++++++++++--- 6 files changed, 193 insertions(+), 37 deletions(-) create mode 100644 docs/source/Plugin/P080_DeviceConfiguration.png create mode 100644 docs/source/Plugin/P080_events.repl create mode 100644 docs/source/Plugin/P080_iButtonExamples.png diff --git a/docs/source/Plugin/P080.rst b/docs/source/Plugin/P080.rst index 6ba8f2d21d..52d86fffb1 100644 --- a/docs/source/Plugin/P080.rst +++ b/docs/source/Plugin/P080.rst @@ -1,4 +1,4 @@ -.. include:: ../Plugin/_plugin_substitutions_p08x.repl +.. include:: ../Plugin/_plugin_substitutions_p08x.repl .. _P080_page: |P080_typename| @@ -26,15 +26,64 @@ Supported hardware |P080_usedby| +Description +----------- + +The iButton, developed by Dallas, now Maxim, is a uniquely coded key or button, that can be used for access control or similar identity checks. They use RFID technology to transfer their ID to the receiver, once in close proximity of the receiver. + +The iButtons often come in the shape shown in the image, and can be added to a keychain for easy access and use. + +These buttons are available as read-only iButtons, and as rewritable iButtons, where the user can change the ID of the button. Both types can be read by ESPEasy, but ESPEasy does not provide tools or features to write an ID to the rewritable buttons, separate tools for that can be obtained elsewhere. + +.. image:: P080_iButtonExamples.png + :width: 200px + +Configuration +------------- + +.. image:: P080_DeviceConfiguration.png + :alt: Device configuration + + +* **Name**: Required by ESPEasy, must be unique among the list of available devices/tasks. + +* **Enabled**: The device can be disabled or enabled. When not enabled the device should not use any resources. + +Sensor +^^^^^^ + +* **GPIO 1-Wire**: The reader only needs a single GPIO pin and GND to be connected. The 1-Wire hardware configured in ESPEasy requires a ca. 4k7 ohm (range 1k..10k depending on wire-length) pull-up resistor between VCC (3.3V) and the GPIO pin. Internal pull-up of the ESP is not sufficient! + +Any additional wires on the receiver unit mostly are used for indicator leds, that can optionally be controlled using standard GPIO commands from rules. (Don't forget a resistor to limit the current...) + +Device Settings +^^^^^^^^^^^^^^^ + +* **Device Address**: Select the desired device ID that the plugin should respond to. This requires the device to be enabled, and an iButton in contact with the receiver when opening the device settings page! + +* **Event with iButton address**: With this option enabled, and the **Device Address** selection left to ``- None -``, instead of responding to a single iButton, an event is generated for any iButton that is recognized by the receiver. And once removed, the same event is generated without the iButton address. This can be processed in rules. See below in the **Events** chapter for a more detailed description. + +Data Acquisition +^^^^^^^^^^^^^^^^ + +This group of settings, **Single event with all values** and **Send to Controller** settings are standard available configuration items. Send to Controller is only visible when one or more Controllers are configured. + +* **Interval** By default, Interval will be set to 0 sec. as it is optional. It is the frequency used to send the value to any Controllers configured for this device. + +Values +^^^^^^ + +The plugin provides the ``iButton`` value, that shows either 0 (no iButton on the receiver) or 1 (iButton recognized). + .. Commands available .. ^^^^^^^^^^^^^^^^^^ .. .. include:: P080_commands.repl -.. Events -.. ~~~~~~ +Events +~~~~~~ -.. .. include:: P080_events.repl +.. include:: P080_events.repl Change log ---------- @@ -42,6 +91,8 @@ Change log .. versionchanged:: 2.0 ... + |added| 2024-05-10 Support for Event with iButton address. + |added| Major overhaul for 2.0 release. diff --git a/docs/source/Plugin/P080_DeviceConfiguration.png b/docs/source/Plugin/P080_DeviceConfiguration.png new file mode 100644 index 0000000000000000000000000000000000000000..5125c5a81a9f5ec9ee9ec10210c215102a5548b9 GIT binary patch literal 40596 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 LNkvXXu0mjfOhWB@ literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/P080_events.repl b/docs/source/Plugin/P080_events.repl new file mode 100644 index 0000000000..935be482d5 --- /dev/null +++ b/docs/source/Plugin/P080_events.repl @@ -0,0 +1,32 @@ +.. csv-table:: + :header: "Event", "Example" + :widths: 30, 30 + + " + ``#Address,[,,]`` + + ````: 0 or 1, depending on wether the iButton is on the receiver (1) or not (0). + + ````: The high 4 bytes of the (64 bit) iButton address in hexadecimal presentation. The address parts are only included if the iButton is on the receiver. + + ````: The low 4 bytes of the iButton address in hexadecimal presentation. + + "," + + This event is generated when the **Event with iButton address** setting is enabled, and no **Device Address** is selected (None). + + As the iButton address is in fact a 64 bit value (though only 57 bits are used), this value is too large to be stored in a regular variable in the rules, so they are split into 2 parts, as often sets of iButtons are produced in the same batch, where the higher bits are the same, and only the lower bits vary. + + These numbers can be processed, f.e. in a lookup table, similar to the example `Validate a RFID tag against a sorted list <../Rules/Rules.html#validate-a-rfid-tag-against-a-sorted-list>`_ (though somewhat different...) + + Simply logging the data could look like this: + + .. code-block:: none + + On iButton#Address Do + Let,1,%eventvalue2% + Let,2,%eventvalue3% + LogEntry,'iButton %eventvalue2%_%eventvalue3% = %eventvalue1% (%v1% / %v2%)' + Endon + + " diff --git a/docs/source/Plugin/P080_iButtonExamples.png b/docs/source/Plugin/P080_iButtonExamples.png new file mode 100644 index 0000000000000000000000000000000000000000..5ff56bf4967359cdf0cc1ac36d52f30b1fd72764 GIT binary patch literal 239573 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�y1r z$|sK>nJeEbde49yk5>1x*=wIY1=b6%CNCd~*Bi>rJGAS+F4O?IqB0bd5(g^xf|NC; zLfZf^#fm|*%6$VbH}|hFr`u0R!Px=NZ~HyXY;(W+H7L&*rhSr%yd}Xb+kNb4h#6~0 zRHJacf(Gb;l6T<7*uOZ_YeXvN0J%Ij^cQ{4I9{LThYa+9mS0~g!Bi*wA4ig{A3pwf zHyYkd*W-}N-h+HVx?cTn=?Of5oJd=pt*X?`vRnZQoc4=5p0Q1D<${~r*Q?#vHT;8Y zl>L8|+C+y1UJP#otI@daW;INk^&+2!B>n#t|Fl_}P(hKOmFFfWJQ(0CT8zm{oGZB$ zA!JR=iqHeOc0l6Iy2-d>AslwIPwxjaq+E&L|A~lomS7Zx-fr!5{lmF3C~O_u1pdZd zYX#2Ecq{*$XkoFN<{vC!Q>1Q+XTf(SfixO3`Ca17Y)Sml(kpkG9}BuFS$fHTDG#?z zyZJR1=wM|}Z=Fb+fZvSi(KP$zHziyXSfyOk^ zIBot9lI&UR#m!xyMYM(&cen163a9|N+R3x&Na?jqGPiyA?C&p?Vqs`J*xcD40Er=y zs1}o|`cgn{EUPRlov4_x9vfVHbZ+Gz<&oH`1O&f@Fhnn26G!^%$g4|*S_J@*DCJC1 zt z_{fwFl7L-#m%Q_K6`p{0Cdbsw>Dw)e)NJ#l#bidM;f_1vwcYGi&l`$%YFKgMU z-t1>`(T0$&&aXN4ehtoBt@FPL4lZ6x`L7!kRO{yoyNcuWcL31qT zl)0@wV^-esd+o;j+vLT)o;L&8_RgAO&2Xe$m>T_Ko?&{G*6rqS5{In$GNIn~`S->| z$9bsYbw79hOMy4@d!uE32tLxL7PG6`K}FrYQ1b zB{D~jLkCdk!mim#OL)*}9%WQD;RUrXnF1D*yF$Ys?bl-83~nD^Pq5UhkuZ$K0-3kq zDjAK$CD@$k+%m;K9i_7Hm5+hLV$eEQmUYqA4<&vs9a(w1TO)fz$nNdh%B`dtY^rvS z3Nv5PJTfhLXJF_E%lC_ob!^A1NHmUuehz(Qhe|SEhP@gggD~SYSXLJ|Xm|UF4%d4Q z(3-BTGKZyE=MP(#dq(aKJytd=H}Y=rk8}_6XGCBpp3RpM`iKZkFCL^`7Sfjx+{Pyz z!e2uvo3z!gu%WePJwA~kFoY-i7YNVPxWm>(%gT!CO)31{JZ2%UXfWGyvKi;i?69a) znP|ZMUfBE-S6_WEdRmj!$kv5;#0e0l(@8#YB#ZY=i+NdY?WBrNx=1?pvMZT9OQnIm zi+ahy4ZF(C_}Kp9UgC8F;EOb>+Kmu%_H%zOLgm~XhDSzX-YiRm7_Rv>Hk=|O5F3*f?>%RSBtN>WmjB7-xJQDffD*sC+H!F)K*(vD!s zL2c)ZgISxnD^Fd#l@s^U(oH&^aVQQG?YQLj2^{U23Y$Cb{F`8d2-GdDW|;oW*fAIP zhdJ9}y`PZP^|p{xxh4UOjmPbLiF2-SL=8zXO&a*#-Cz2^(8ZVKmvhXuSIH>cP+aSy058Ozb{RY%| zq^4uh6r>2Zmggi4Euj}IxMTOwb}K6fqVwQ!rxt!W-SMpSYhW|orV8>%(iQ!}l%?3p zX1<=pA&ULMb4p50#sf@|HS9MO>`0|(_K!Pj5F{=w1)px<9>GyC*=u8uE3L{IG%ki= zo>Sx?b(cv0oa|%^?sf(v@8wvaNw9QYVo>2_ub^#@R!ak|)wuimo3IRBHBq>2ltJxwqP`Je$t;QzO7)3g6WQ|qInNmer2>( zB@MkQuC_fl1K1wn*dBl7o$~qa#-`gnCR$GK$AcEda%WDN&!wP|E@GZtw%@y}FXkyW z%^Q(eXZg(5RIq_5fMV2zB}KM4Vprc)7xuddzVDvdtvGnIw){T0-seBEKhI`V`&SSG zl?vn*c0!m<&{S3IfcO>g5Ea!jPt3>(^C(8k6F91Mo%%=W0}ZK_`MZ4g<4|8Y-a9u^ z$ClaZ#g{jpsRHGGJnU?m)!;Kz@Vo1QYDrcM>F1c#E2=!BJlzfXSc(?U#3TvuOo{Sf z`cIrlqEK-XN!s%FY2P*LTHu2k%W70$>75TQ3?kC;N!w>XCVN)-(0C|AfQXf;uzJ7w z;GWVqk8X*IJYe~*=QM?>*>kihcqOsnvkC%*W$C*8(O`DWn?Y_>^#H5#?(;SR#d~*a z;@5QAYD%>CeSk0Cexft#Ci20iM#D){?)?mlr>yhVSC5(gAQS9Y%<=@7e(i540z}+MwjLz|dIh3?L)@LE1nOQ}^HV-jtqE z6_w<9pi4n0r~F;A2~>PXZEMsl7XD!=U5Ur@4QQurmXbUk7A@sA9!7;eZk}*MDHyx- zF>vlD`6unx1WIj^kfKN0B2abD=XrU6(YyvRgd?)7;03fsxkhw?oFSYA&Npn}`31<4 zG)b!eTV;B4(yv!-5-fScfnL4E0R6H~_Qp*ux=@N>gUk~*iYHB%e>k@IsLYzR_L0FT zVeDomQZAv^=NcQ%vhDC(%$-YI%%g3O8GYI*;tP;3U7iV{c(bv9k<>Y{NzsR!LVs<5 z;iS|MDE)y(fnA>(WW=<-Gi|0$%twIBHQ8f9ilRh`L@;4FrV_c__TcbS4v}e8L)6hI zuSDv>#W@98&8`vjdZqh$?J5MH(xpBFH6);Uil8xZsPyThESf!TXbngYVanQP3~S?sl_V}+4jVv55vDrv4~wzSoVB|Fk|{b zQuH%?ytXQ~n)&o^bhSS1I<5G!qJ9i|6?-pEpwC3Sh&BD#op)U97ok54M&C;ScRzvo1*xFeYGjBIhrYm!ZIz! ztuYTc7R05biGz-yg_muOZZ*HJj-6}(R6#Rw=-qi+xa-yr45))RYw$d|9i|$#%oGK)@_)__$(&^gdzlUsH^hU3=+rAb<$9BAk1s3T_z;$Q*dV4OPyF0KhVU zs+6r?KaERrJ0=~=xR14$sJw6!9Ut+P1HgQbn$l6EvG zz`IA12E&N!Z(4T!DTbJiDhhqCmLm%0rRdmJb1uc3^4zU_v1P{$JyCjv{{VawLG&WVE~IJC!6=C(bywO4 zTTdMC`z2gWxn@EL#642S-=0QQ8an0OIaapd)lQ=4WBujc1Rh_S-FWx|a%>#IS2MVo;SFa}BreXip zk4)Hqp)v5Yc@YHgx>GrB6^xmX%k5^mYZU)fUv?r|3>)CeRqFJY)75!5@9_b(3Uif5 zv+n!L{kRGtJ=AY>^VLv32`82ZX<*f519NIa5pJxnyHy@-4eI0g<8dG!XRX?)YIBlUT|lz|Bpdp%%I8`a_6$jF8E9CHV~55jT4M( z97l)LU~(kWlwE44O3eHx|A9Mfa?bi?e=s3S;Qh)Fy)U}&L3oN^x37ekc?b2q?aruXna??u+;(KJUUY=jQ3-}lXTl1w!unKy+Y!<}rMbx}>YZs#ZDFXxH z6$bI=Mnh8MlZ(D)fy*zS&&}wVC5e?ZFtIC!k-$X-G)t!a=#ToKV9Ac1EU@32MeN>Z zxKNP&vSSab_kU=q;TbK7=;Ogd)%U|6aq149nAcs!5*;?UOAUDv5=TU+8ALr3#D*w8 zf5+68_d1htS1y?@S>w0{k`?D2;T3QYGqI-N&h%KK%ZXql6d<{9!Eb9hRA;W(ZSjSF z+U{}zqygN$8cYXN6wz7ANb=b#2cCCjB*q*#as8y=G^yi&BCOpgimylA$yu5v6D{|| z+mi5Rud9;xx;3|D>OgkmdlBQJlCS%`DX`Zpd^-Eg+;zo}hnv9{>%SB9g&l>GI0aOG zN4gW6JUGA!zhMSDcq`o?%ab&SuOv>-Chzfv=iJ|J(~MI znmH)VY%qqkS5Z1w*oj|#_`WF?ITMQ%tpD9LrxSm&T4$%y6qwDZXLJ8_tZmP_=nqj0N8A+8C9D!dmrQgvzeuf#4C*(#MbK@yN?+z+vha zJ3*T}b0?7^zWmspRl@&aCH`luv{Oc4caa6?q`x=npq8K8#tw2O1*>2qq?J`4G@_H0 ze*$?bv~g5M5FKwFv88!+o-)@&uU!9CNe2~@$Ks;CDh5_)Oxs#@tGqB&AZ11hEk;AC zP{c-k4GU$fm|iJQe%vdoCUEfTiu?bw01QjkiXd{oCpY<)0S%1Vxe9Cr{L=s~!*4q< z@J>?0u~hJb6=hGDEzh$85qAWpp3|$KOF)L)MRxJem26B`?OE#ro|P9zj-^BHkBqzC)(;L0g zoiY>brtAzcZ+hlN8ic<$&L~k%`$2IOY}rI1TG(%WtaYauU($N!(g{dE^SA%tzH38K;&x0}_n+m>p67yadG+LL^OD=oYFj zXe2%G*~A?hbt56BuQ9=%f3kl!TZP`6|6!_~$c-GQyzIeyaX^3oCBnS^>lQ4vx85i6 z%xJ^Cr%@kp1IBR_N;i z5pb(8WL)%Nu2Q2b9K5UXsfCigq5)~zgK40v9a$V*Q^RRVvlpQQBa zrq+MCEm!`7ia<*J%y{7^R3p$qh=0b=9~f~1=vcL8;H)UCmgu`gA&@;4r5NOJun5KY z4xvrj!qr)_E4w30vewpjS%xr+X5FbN|414ot+rPtsRxjLM0ksQcN}&iOB-!ux!fuf z9s`#i3}PLb=W;aXz_i)bXgb_g@lKmcIaK6UmAQS&_<4wBg2w3qs~Ft_3~I-CwhPf<9$EZliP*n#|CLW%Nw>ALiw@C}ih}my?AD z5^oqW;Jp8jDOl=7cS`^}w1!Jw8M(2wb5Sb6gG*r?h~y<{_l<+~*V21k(ug#cXhb9m zOAY}g7%-C&6hyeNzQTw#Ay2z}SGIl^`L=j^6J02j-^O9U6`$!EAje+NR7rU=G%sa zr_jN}v5@5g)#}GlH$3TG?t;5GN5+Q%1s$O)LhZu8i>;Jb9J>x$9cUc4Bq&)BDqVB8 zP}qPb5Ud&-y9`5NohugtpXkRHkt3XS5O%5VB(O3*Rr)|hV`R}Zy=$aTI0yq%+oBAX zhCl+`Tn>%%_ep1@Q@f|7*e<+J=8?v|h*WUq+MA-l7Jgc9C&*^1_mc#D2X6Q(p?r7{ zgNRT$t?zQ@lbzZ~g#CZx2-%y^=~JQ(`e#YBBR1`wS1fc2N%!1ID{O29W}8EO&BF9K z-1N-Y>0*FPV`U$)74hroZB@4djP(Hq*?9+hkG82p4f zKpMn9^ZK~3k@#3HKsk)$pZ%U$FBIobgbfj8=*C3O>1;Ms!zYvqj0O{Hftbq0@V3Wr zE^Bk?2h2Sa!UEcwrOEHxuR;P6Ag#bjlDZ72W?`7%pK+U*lkASzDZ|eRjjZP!Ar07j zJH=^tN-_+tbTO%k7c0-zIF4D>#Bl&*Ha^lb!WQ{=Ct6z^$FYjYMU)XnerSu|!Ct?Y z)0`(8X8e1eUp79V!<^w8VfKZ;P8e|-hez@>%BFcu+QCoT?E_Mz|7Ri;Jv0Qpfol+d z8{R}pF6vjAjT4^6&dFCg15~fU86*PF1tzNOJNy@NS$)I3J1epsfI9IK7yLPVKspTC^C2U&IkkX;(SUh0CziSs?| zed_u~d)Ra5iP1{-?jEc4QJ)Vjfn@)SZ?J6ik&7G}R&pypewWIP>~2tI>#rk7F;%h5 zPdlBZsbVNSYIcln{h0?{u(SN~WfN!XJH2Qa_2nP@-!j>Cy?YZ1areRQ8Phc{{G;G9 z@Lt`Kr1Jw{^(Kx4VLU6E0^X;8cM0ELX_QE>{-~1YR=uG|hu!vwc82YB3|dRLw^2(N zHP~WQhPl(a#Xk|q{MbeB8sD1^R7SpAE?E8nmc!5 zL{RUm`cj{lg8%bUz$3#2kEl-)tWCxFYegel(<32bFfwtM-p;J0I<)W*5*tdLpqJFx z8@7Ckd)UgWgk}3ldnv2w@pmy?L077bncfOf0XcQ9EvWR|vBXxCc4QtVL2Y7{BQO63 zH1!Lfdq4gZOT8V_PE`8} zyFR_bmC$bg!~0!+mCFKls>d;Wt|A}$RRa%gLF4*cJgeGv`ukm=bWmMT{nLoWvB!v_ zNBTc3u_iMpPyCls3&TKceWO?vZt2E{C3U%;} zv@Y-w1TtkG+jL?Q?nqy}rz4wiC=O zr-}F8Ro#_ke&Hli^Vc2`m_ocZoVgA~bMLW>ti*r4cVpQo5Cli-LZ^Nn+t|I#wUn4X zPtGONE`9C_w|4zgHw5o;>lx_?Oy(|P)8@48t15LmXB?=%_jRYFYrw(w^GyFN+kh7$ zN6N!ySG1Hcp)&|G`5R4h%|T|zxQ_{FFTSRusO~phgjS%!I4qFUvc?KC%j83_omf$e z9`o0H2f=ycQ+*naqwD}@{(TqN)(YUZek+$_2p6AN(V0E`gP>>mBDz zf~^EI`yeamE3u@fzxtAgInUio_U8E{e0cM`sAoKfDp^jglhRLaBL`<}Z5be&Ry2ga zWmezUz%kS+4z*agE}t%H3PaXQKz!l!nFT4=F?d^$YElNSS9-7%cht)kmGPFfVbqF4 zeWq|E|Oi-YBnsJdza+t3KKpvl}HmKk(>UhNfXjLT6#Mn(2v8N5ROHcIv>@Uv{h3-sCgc2u2Bc zt?&pGD~rdp1l$38y>pb!a9C9xe_q;<@jG#gpn=tAGK60!&R;mGYv#wh{(0ghEExw_ zLs^vKABUc=g+y-U2aCEW8WzBYPdM*NWJCrx8(J3lJlRUbr`bJI#`}4vs7>|e&Phto zg`0|wgSmO)(;aj^TzHB*n&|Bw04w)Q(K%h6VZwTB28j87hmH@kjCG@(uq%>o+S@3H zTOY@&wBP}6a?_t{{Q%_KpcMsxtRHp-@K=eolBUQxjTqA&2ezfA1C!4lS+QlZO6|+Unl-7Bv!X6wR@Z@5B_~Wf|M7~x{MgSb zPklDMN382v4qu_QSF~^MI8n0*M*w$+w-N@@Tf2K94IH#E_diqcKpZH^Jl?YIp7R%` z0G1NI3R3-rMw5I#(h&?7QD<@-?DEiP?!Z;eEq${yV`}Pbc6Bj}It2m*UXe^O0sqGU zAV>IB@Oj}EA{+KvvWj08!{ASmw5t;Gi3)yP6Zu&6pPMmc?Bl=m9uaOIEb&YZF{`@g zUFjS9qa*BA=Tv9m{c{k{ngm#;oOR4VsfDz zEmgiDB;xZV)j=KF#~mQ9PAW&%k}?FB^2oH>W>Jz|<<~i;)jbz2!2D}a1rgZwx+F-l zh+3BR@xki&HsENF0UZCqT33ji`{{p;Ke*PKMu}&A38VNd8-R@7g!;}@wBC_u!^&<7 z`ci)a-)xa+Pf@`4awY`=kuJUC4{0nsiMole zsTKH&(M#-xJGouhk_)yXg(_*Dpy*8SJxddGa`887SshA)?^uNGKg(h}m&5O9S|^&j zsKmJ~BTay{`b4m{xr8YnuA@eE-s{Xkg=hNLhFLL=y5h}u#hw_sx)#=t6A}i&kLaMh z=4veHTk*ePrgn;s)x%yN$PsfhC+D#cOuI}K5Kuc9B5wsj-GB&3h$T_hDs3#d8~BWe z4Q&hOA8b_v5XWuLVrHOplp09P`YYzx?ppfru3*E!Z4Nfhfn3C*uSY@Q``wXB`hfKY zpz{WX1^;Euuwdo8_c0_T)l?S-X1Scr21^dSdoFH1)vst)e-^!?Z;k?3MtG#v#8+O- zYX_bu@ly&ed`Y!_H_qZBy#G$PTC_ts`&<)(R+#yI#@wOSIx&qE&Se3L&BEZ(q7@68 zQ>knZJ=JIax#{~{^_c!2 zmkA;+K`!s^P@eDy9d@(B#w!M{A|a`$nh6ZGgwy*v3@NzLM2guv<3jf8uMQ$(OH% z!rrF`x=D9>y&}mqBB%(u$%1{b#goKstBuris5)%KMsUwxtx8;PFDR)==_tl3RJ*Q- z>FE=KFvJWJ7*1!7+m0XQ8F?_nD2-~@pMpU^UEU0CAWLJdA~r4yT5`o`oBQ6oU*M~_ z5AM*ufVMMZ)?$4g$pd*e{!<_uclN<4HWznDQQCKhaoKx4e~1 z8ob9$SnNCws+E`E)!dpfY93sMxngcQq!W7Dbkj}zn5I_6XM^GJN#>4{M#Gjf(YUdJ z=pTJM^2CX}9n;yqx{dio?K?)hgh6<+VlSqEK^gz{Fv=y{%T1$sJ}@OQLp>m(RzPeocBM=vWz_|0r=7X&aPoZ$J5)bw5iboI@+dV7cqG7`%C5M zVN9D4QCWHeA4c}-!v&59gGr&@m#Y}kur99jNsW@JDvA#@$;BpZeT3UiQaZDk7`PCZUXr^8RIa2S>k z1G5-9)@oO5)^mhf3F3sYu5}5xA2-coJksY9$3HV+rhTKE-TlAtuUXi>lQ^2X@prN2~H!Oh6k8;X@zSWDFL5FHcGn}cJnzAAskw*ltMS_;f zwAysTv>k)^%c(=AX{J8t<2e?a>P>dM-0Pqs572(wRYZhS7a_Gc%_}ap7UwA9^(V{W z_{+KQcYjNRd7_F$G7)~zRI1s70ZsunhThA;d?ZMwT$I_K_|0xgU%vChyQ@!(tC1X4 z_iK}!jw|OIOwi->l+58!38XgN`ncCt1@?CK9i(D(Tb>m@tw{?XjQt2{Gglmy}W zt*yQ%xkDW{AE8<%kQdp~cS^4&5!<0}=)bf@FmHg{6$KKZW{NKixEsZwSJv>9Zgf{x zZ>3u(t-k0#3Rn~0%aQr~jD;^h0GhGPS#aAVGg;aC%1Tj@kP zb#}K9(UJX`t4}%YJolzqGcy+TYU>Rb_)0v>%O=0@!TqsO!pgAePe@X57_jP9^xz)_ z9F^+3%_3=ns`{-;*puE*Lid<7)lPdbv)SfnQPw)KcUNrBsc(t6FmWX@?8Id%2%sQ| z)2(-w`cQ?g*?((R+BPOST)9>^EYE+_VLdZQb?l?zd?>jX6=)qne+0%_4Ixw~q>Ehg zYSILBmiACTuFUSbm%uGcz!C`$-EGgBYh_YbkdvafjAda<C1CG1c0@xFFp$^u@l-WQStfo5eZi^Ho1;EX|7vr$2 z&AYYTiK)Rxhyu9V5xQs%9~JDfH9-Al&@Y1|J`KP8tka93z5xUqQpk=u!e1~J%_aDK zWM=7$fVn6$f9vj@kR>c$0y&7(RV0jDx$LXNs~1-rfdp%9p;EV_Hi-poA4kbf7WiY| z?_>zuZEZ~Y36NvT58}KXVhNq5s8vp*Bevn(u12H@Xb=Yv`(+cm{KWp?I8@k)x!`9^ zOMd)%03HGrz7_vLc!&EN%POHYJVFt&Qb%kQhEeu20%)-?L1c7|{^P>~kw%k(itxXcd3PLee2rd!aEl$JDD^bL^N-G&O>R_Gs5zs%th3&T_rv$}YL5*cpR9Y}UH zr`z-S8q*_{ydq`DE%->un7d2H0EqFOdJVA|nAgZnPn2CxL|f74rU6=tFFJp4ULt~aPQ zw^gjcDV8_rEgl*DEs|-p;^%Y&2iW<0n2UJ(Wf*1Nd`1)AsI679rW_ObV^t#X;(+9O zv$$Ao$Uh@37yrK6HGhwOuP5G|oJ`ST|(S|ubGU@M``wxkG z3WSacYqF;MHMwtoc+4`Y2}|X*FC4Ff!qHj5Hg$mnj$Iuqy0A&npe7F=HUdB8vdsGF}M-6el-?rCCZ&Ahnvr))Y1eKmWmN=MZ?(7A*q?tAP$Fl{CVy2LeV4tpZM#+fz14eG^BVhOBwg-5 z!QMZnq;@Sk7f<~EDRBm)={&gL@KjD;S)(~WfaCzn?%x`AbT^3HN-MKss< zF|D=A^(vci672UuIX~(y&4bb?`j)cvBtEE`s8Tvka0ZRw6+~NYFYEbf5<{%~4V;Xv z+lonM1j*}n4J@a#Fc$aIq~p)%Dvkp=@`&Exf}b^#qBP7NMa}P|BQ>8-EZaJEJkSv( zVSscw+OF~$pS_idGaV8zhp4j6bo$c=uG}pU8W|KRRv??Z4C*sdR~sP;!K*Yl6vsaM z(RD3%1pau&`W9U$DjifN8X)c@y>SFmo3(pvYo1pk#kN6UIpmy@V` zkb^)s6Jt*_EU#65AJ;zq-u@s>7pd2Qf3sVw+I#3|%<(+svWLozA#yHrr8N4q;e{k7 zp=R|~3*BV2<-Oy#AK81a8J=9NEM0!O#{Bs_@ZF~yRy+R~1Ix{ecl!}?W$~*iIYNQV zcE4HJ(z~%VHvg%e#>Rn)T8T6aDVXgR?SJTXVKr=eslPo?ex=(*n%n24ti0J&Ep=oE zu{y4ff=nOIzAOIrzAQ6OBS96*#BZaI>M@Sf=Q8dV}|DZtc9=YX3$;ju+>#_AWtHVET+Aygn`*P{A|Mv0MpJ6&6-_z0z?fJ2 zVqKf1fGp1%;P?SsS|qsRl~$VePcy{Y2)SPU{AlD#q_BY`xSH>$XMigKw-RsEQa}+v zWDs`6Rl4sPSLIG)Q+@+yld5rYIaSSUP<{ZX8Au)vu(h}2hZYlZG`_9qS5yw5#}kMnlDo#Z9@IJ~UE_!WF))D-Zt z(-XH)(#N9Nci8|$(O57iC{oHGzg#qKaF`5pHu0cuEZo8~`W5OMHAmXHSKL!Q-2X`l z8cxed3p25Ka}kQ~eSbOT*Vq0ZP3Pgy=KH>V)JUkk_g+z3P+P3nMC`q4R8g%}JGL5W zMeQ9U_Nc18ilU05RE^rI_TC$do%AXDKI9)5#SO>fVA9V@PW>^5-BY%g<{%uKe+nL-QF4=YV{= z=3a~b_q`7)AH4t#8s{c@m#C94XvPaz-l+T9Y8)J)#GtvU$`{BG&_`PE*Bx)Z5142r zZUUNBGw>}jBM;hU^x;$XsK-viP8H7{{!LVB$P8naXed}fX{uu7nc^S=3)+H z^45ZOqHwqrgcL>)sOkEhmHW}ng_yY!7IE^Rr{X4@UCwyyO)+}z{L0R!`ryz@H;KG3 zMHs%V3g0b0<~A(lbA&0U2#$4VFp57e9j`m z=+p+brc}M>`Z}C8`p*a68vb>`g~|N&=u^*!z94{~yups78%y5aHAK)z)5L%W3ePhp$z| zoy~ZVF(2zF1-x4F1Uj^!YWOk=F1f5ERZ{0eT;uEAW>ip=?K9!~bP`Y|L8L<>*m!A$ z)cWH%I`BIdIoy$jIeq*)1T)DrXMG(L_Wo#Ck2H-<92W`+&SCp;{^NqYQ&^BiAB1Sf#>x)a|z zd^EHlqgV8)VUtgw{Y`{sxYM(;Wa1b2P7}sMn$Cw7uuU?ZSJU5`Nm?fNRR(gVzoI9z z4;HHVnD1!Yk4eGJuED8@z1t2F{^n;`pFDO7me& zBJ8(1>tW>Qtjnv>TFra{b(>F}M$gD!6w2oGZUc*qBH?28ng$e!A71_V);=t4&z%WI zuC&S7qB`bb$qv-JNEZ=+QHI~&BI=2|BTY6puP4-+?OroPXstb#0*W% znPg2)(B_GK4L9mspg`3h#B0#N2Beho@R1l{-68X^o0A zL*_|q$MNl@n%?{Qz@SZVaM*tKOBh4=)x`P*t%kf=V^=$XnzmY%fjv#~A3It<41f_N zW&5S11+LLJA(y;seaQH%|1*`fa0bVP@ z#klzxk0>h7rCd+;^i#lIMj79HZnI;&azX30Q$E^4i)~Wm9kUd$sLt(Nzgudh$rMlf z=f$1%kOtvaxktF}k4m~m&nvsxAX@G}KjBAI%=;MnzBYtOL5bb@z}^(- zn8)QJBiJDcA{n0UP$TY#1El9*Lq_665TX`H6GOTmDgiwH(9K#)pK`n=+a z@p%&;7`A^SFr}HTL}xcrkawMKY*X-n^}!A=R$OiiA}~RWr(CNu#iClhgl^6{%4Nbt z#c1WiPkyDay%><;n$+cPrhrYFa`+AqPKEw;lc7UAJ4lN2Y9|2}@2TFX66v|Gyve?V&Tg%F6DtzgSF0V3Zv|kFk-&xwDrX2n{JwPQ6fbxvLxYQ|*+1|888uM{c6H>+7 zjVbJ}?rx#E8iEfD<7+sc_|{zj3o%=bTp06k>_U{{(gnqROgKkm5G$;30NzX~d%sB< zAX-aYDd41^s8G5aB4SfWRr07+ie1WpJ-ILzYs)`g#%b%S;U*_r5C&K7{GV`c^ z5Q~bke>;x=)Xw{Ju^v!?LtHqAP111B87kDwQi?j@U%}sdDcj5f-?HE<{TlzbnVjGE z`>ts7HQem6W57~HANhVqy*tMUK7i-rwKZRrG;g5`)AMGU(#CYeB-oXh&ToA4QrzZ3 z!|of5Q)bVyF7PMSh`nk=|HY70u%U3_%^lv z3F{&~r221QRBe#}n~5tie?%{GY2l~k@Lx0#VWO~V;WAEy+>cOuE~}^;>1sy8Ybjf+QUQs8hy>`!l_rO(eRig zj$?d9yTbc~rvQ@m@rx2Dxh`Hl6SsowL3}`%dyI&!q8&;X-)_Ydk&EBZpp)8(n`q_Z z6Y-)p+rd#(%+onw=*-Ed+nzF_0R)m@($nej>sqtxT)l|0W>B4qUYGFK-kL|yA5wcK(O;eF#P zFIUvwqBeDAN9=wvlYZczp(t_5xjfwwWPnU}#Nj-81p}18D1{cwKq(`T#~y5ESEh7z zP?fc)@@Lq*=tA6|WHOkZhuGVKQCMry-KpAZ`;v{O(MQ9dKXy;aDTTl+Y}KU?KaU#I z$$U<(=>AGRO()+#I;<@&+UQH65U{UnQ~78x2cELO5(arNQf%SDdz;JtHyI?(*gayu z+>c%*A@EP||npsRP<9;XuwYaDH&&S+hA!PK8rkoMsS~)F`haR9* z=bu{e(7*hsuswEF(mP7!Q`#>eR1Q*$Er)`qa^H=ArL0^(zP5M`MX~;&6`Su}O;y_) z9IXT|!=)*)$thadbsKHYeWjc3e}^MfhNjPDTrsvGu!HnO@C_H@dgDR4qBfl+%w4XwU$m z-C7=bty}#^c;ye%DlbOD)F_Qk63tMoK4m{U?*5pWLj-6r#}Wglj(N_QUaWDVq3kIp z)Uk#m-IlBrK}8N#8&)*N$s>L(tmv{{d&h#)4bIXoQFSkUTU2~RvMz}jo^EDDQ%xaN zT&axI0Dcn3E$>G+EXZJVt>S}(D3O8&9DXW0`?J)Y%V*h3nq^M<5=J52d1|vXp| z5gYc_#7ngY{mZ1b>5sWbJGvmugcZgm*QnvlZ6tO2>HQU*3hK+-;%QG4dp|xTj+|q1CW=qND`wFRW~G%lJVYjl>A5$O zD^qDh=Ri7now<^b5gzGiHs`l0mL77;S&0juaCS!N5;~S18UZX>gP*})Z^_?Vq6&b! z9|X*U$E^7TzH8)fi}hm2sJZE1>*LXl8#r`xY&vSDjOf0~#iIHU&IKhVM}K>NO$ZCA z8b8K!cB+}Vvr%$D0ZAVV@{R43I#v|XiF${#nfM*uC z)FS%va(CJ^Q{>Y7S%OU)?U=!TGr+k01@0FwvJ(mz3;2|{Oq<>w6xLPwiz_2K=!voj z5}81%oZ$TzO}V|(C;XCLfFIA~1lUq8C(fcZ8j5XNsx<5-BI!Sv2~B0ypQ ze%*qM(HBo~bqOD69Nn2hYl#7cn_fLPEWy0srO?lO+B9l_CeU-{s0=T)u|C z)EEwmN99{iBaK`82Bi1-K(noqU(Z+-cOH>xyt=3A>d&#;$^1hWa8d2y6ZGn~o-nUD;gbKp3{rO~!j*(Txon;(fv ze$E`0EjX&=Zt6keH^4J#%*_DZ<*D);_>#4uAZVMS*C$KgcX*Uo`zUt)0Q8N~#K`t@ z;f<|jl`UYuz5g1_jR#KS#)b25g9sBN8K7UkiV|~jO)*K8E80()xo7QgJz^E#izm^v zUz;LBKpCDufi_X7Ped>kd; z9_q(_Nzt_`qdt-lPGIMmR={r{L69O5JlUtW1aa*yQd@NU16Xc0>?dc|n?_IgiLXwn z^cJ$G{foy=zcK3uB{`%MTrO%Zx{;IDxBZVp)mret7yQx^uS%1YfJ`q69U`4h3B<&^ zhUF)Z+yc4=tPlMBVe(q~(2jXOI?d58rAs`0wvYh}r|)LE=O6KhUh|bMx$If2k;917 zZ>i%s?+_WK|Hx?{Y9rDlqQ^5Gu!j*-!#2_I-q|3#fBuie0_2EbSu1@$Fa#XG))@IC zeD$Z7;#*X&?Q(QALA<-HRaRb!+MXTR`_>ze|ExBuuw6CvZGNQTd8!0pCQvH#>>*YSo^Yfm}+B0V|1Ltx~!CTOG z5j{Su1p{66nKo8SRTWU*bkfglZ}J+{xF4 zfgaqFF!MkHmdzx<)pz?`@d$%VJB}Niy-?Huhc1`Ivn)WX5-)oRwCxNxpzX(HD+dwT zcMF;PLV(|6aGlt=56!We``%k{%@ASfI31{^!eA*)`yW`-f!`X%Bk?U_tmbQ1aH|A& zH8JT{QuHRWh#))QMdRO_7$3{%mqrS_3#g8~6vn_B^7cHSH)~Ep@;PAfPXuXnL9FGG z$}+{^9j5)hlhnK~6wyG$@+|ru%jemm3tO64nBYU2U7}a(Sf03Z#|!FlmpX(+uDGrD zMb`=ft(MS2mezcdctK5JpEW6wdqQQgF?M_ZU)d*oGxJoVcu^LL6G5g1I9&``%#_C-2SQ%a2{7t|u;TH{ZdJG^&E`*s#|jwD04 z-!?)*(LjSpA;`KuhN0E-cm0CoFiK-?ol)5?#%xc)602F1EXYH7R^S-ekG6|Ik~z!z7hN4eqlWRcI|NW>-TS}SE%?Xb(VpYq`oy&fWWp;K61FMVd#O8Sg)D9 zK^+xQsTP7Lvz`y@=|egR^h)4zr_`C{f@4a~6L6|$B?tBK_l< z)>-i&3ujH4ZGp?q1_^kY4#Zeg`GdB!Go?io{Arpg9&4>oenM8l>qK2ZH|M`SNoxLw zL-)>NART!%KVT>=yRT$B7VUiwr_|wY7Zds;mYbrB&(t3U<+L~Majr6E_>0RvO^-`3 zZK$vD9$S5h-*=i@Rc2C$tHAA4NX==O8>kLS{c?WJr)o`14lXOfBAPxo;<9C#FB^by ztI>70=yQ|7LC^k;jh&Reht&;GLELFrg^P6AGTuK-Cl;vY1bI!a@7B|wiOK$9-zi55 zC*gg($`!?xtmch1YAbZcDd z8KK08-vMps;|UN0_OVNX=q9@ZOyH;q`rYIj-C4+aC`Vaab-%_ z?=rmAV3tVRi-wYBPL3y!Y4f`>K&52B!)4qtGjZOvXw)|x6cq}OKhvi~$D9+VvVo8I zMG|+$qCwp82H1Y_n+3u-i=Qw1JykSnjjrNjfKDbd{NofZl@q$?{g^4W%R?{32n<_* z8SG3SWkn)LL>+Bp@Lnojy*%?zd_3+H{G-H%!!{afq7huYbYw4p^4qw7 z1JXTKE75%jo-6XKM`M&imnH45a0ym|s6=OqmR5X+=cwk>a-?38s9{t{#nRx|>k#y% z2;$8~OXfU!UD&k>O@g*hXX`M1GiFHk;Qh z*XnBCdjcK+j7_n>%y{fU0$q={F-TY$3j63{lMvo8I8nu?7+tha^sT!mPR9eC5V)|R zZ!`^D2NOr2lwRXY6Gx&p!2=49(4T(6FCp>$Fu14{Y~fcg00Xe#5=^u+#!H}Ur#Fh* zWDwrOL#18=i-)@aye4IR5iO0IZ2aux_m{Co>qf{ZFwjhR78D;aeK??z|(1bUq{t^ez->AU$TSbaO3L@ z=@=Q4fbqphQPGLd7u%poLtdq{&7zi8z~pnnKv~M^ez-}$5ui%5oGNq?- zzUrH5d%bfp|E7}+)_q!{F#Bm>(jAEnzd|47J#`d}lie8F1XH(z!*rbwzFFI`Irnz8 z@n(m@LRk4T*a1Q~{nMfeY~)A)L+nPz06FZb)e>$wc@YpPVi@XksLQtFRLpM)Py!2{ zNNU6Xk))cI)O5k&WfU!YcB%9K6x5V*4gPYt&%1|m|7{&}8ReDO zZ+w~;I%y4>sD>0R=(S4(vD*N%9v+Vt898p-3r(6Tv`r#6o$KyVX85hL8?GkS=o00E z6o7A3^r2f2N{Ni=+*@T|7tCCHRB0W1^huquIe+_d_r$P;OBpnrVHJ7}ta3bb4sdV~ zu)0=U+b>$eH8{bTPHsNNkfyNoqv6pJw5zfAH}YRpqwE-rQ9gxhE0b~t_Cm%+F}>rl zDiv&&guIU$Eg8HQ=OrguLDJpHwN^gZBppqx%*?yqWQg3{pGULxjsaJH%M4A89)-0r zi`p;8ktV5HxdMLh4$uLBTDsIxaAyU6*S~kg*|!V_s$Hh{j3{a?Jai)5RtuGgVmVH#0E$NJ z*3m9Nab>MQcN~U0Ku(`7sB(a4(|w&6F|`v*22vWzRfjrq&mb6{XosfWRtQsdsOP~4RCo{ z;-tg;va$(Fqe;58JiDZT7^8UO03Hl>nN48optEAmt=86Ua2g7vvlI48`28SPQGF*3 znA#Z{_f}PNSwg^!L;qbs6os+~1+2ox1||Lr%l%CV@e*y)G0nH)iok`cnZK`le-+<3 zUa6I%KHx}SPpof=g4v|n+p%c{+p#8kmgBgq95@joPo1fTiGCPZzfXQRYzsj|NyaVD zLMsOzVJaH<2S6h{#hpCN#?(fJmPRp@gZEAf=N>k4xg&?%XoWI+W4^`syuC04CaJ7D zJ`a}q^*LLK)|PyflGju04@ou=LmzhxX66k(C&Au3Xt4#mT5HzL`SMX&4{K@hf5%lj z@z;4Y_f|8KiIUHL`ergXjUu8L$eSe|H!^{1o1kG&Zg;~p!>^psY~5tAFHouCMS~if zo-zC~!`EENRI`VF^7`YSVt=had*&MJ(~d2r_yd__C1Id93n{!TVXUdN^N54DKRE+_ zx<@rB#2wI&01@l>F!Dwdhab*koVzy~_W3@{kfW*hj+sL#N9St~sd6EWI;)U8K1K6z z5558mR{b-3UtLc76%`+ z->k}GwBL2qt8A6TEE$T_i0aksgC17aRZ(4~mMfjeZ(}ZiWKYVYF6ZNCsv0q?jA8zG z>iw!5>PW;laDKhc(6x21Bzb)^~ubScY)JyXUNa0L5 zbk~)#lO!7B{}6J@J@o~>5y9S;T^AFSEw8dln`c>9A%`Vn)E7?u4R?KCGPb=P|84!q z)Rb{js;*R%`1wufImK^}R*3xCPlY2QC` zaNmOrJCAXA-}M2g(M~TwiER|*FGx9@C)~f?q*p_~by8Sw;*yM7*2FYPr=|>Q)lK3KB>VyvL)H+4J{6BHwnNl*udMa$E^j6{d zcreTIIVx#()BM(xJ&N*R2&SOd_2Khp0P|*7DZ`r~FmJvV@`nZ~Z5sf2FKEr(LNAy_ zWvQNHMF8>chLl%((}kz=J{eX^=n z%CkgnfIt^o}g(C>ry%f*xo zx?pVT(p{O2?yGhgk0Wl}9nIO5vOtVk-pRTRqs#j@{xIj=lcXL$Jj2<+Wp7oWq)|`# z>j@F4rg`9QBgDNtOVaO6c zul>A=5!=_>8Ou8zVn=!Hq^PvcK zIFmUk@J^B)nADWE<+}ezEtA?nc;=x8jNUFW3dKu5UvmEPvY7RT>`p-;j)b%%>#dT_ z8P^mA%oqH$oTi{cEI={UTdT1O0jdBozo_nquKL}sm@007rkM`4p&?Hd>@$94-1W(| zEOk@fmqe)_v!PME)7gE$FL`=sA1?bS_ne1=L&|od^x5VK<3#M6 zn|zrOSpJOoZ4$+@NP}i#rCz<5xZhx5>9MO0FO)nY&-GrAz6#~#`F_pp&~*hsY-;Dq zxhjF}qBL(18ir@PNdSR%dTjGBRX+9-&o&zp5NeYvXsc1h9Zk%Kxh}LZ4*hlY z%~JGo-{xT-Qog7%{xEtn=bo6?K{9&(5vWN&bLe#Nqh&~g-iJawp}mn-c(jA8_Bv(2 zrmaGU3|~@hoEx%^oYEgw8rGvOM%(1SykP6Zw+!a2A5vg~Spv&murLPiycWqFI%!Rb zoHA)S3x*U}3BH)|q?{*zj>Chlh=I80>obwWuCGLYakpEtuLuBB!?A`pMFD*NDG^hq zoj&X^UasO7>f#Q?kawAR2`f0$^D9U7?Q%r@?&i)8 z#I;{CTgi0mwXV>+skzdyE2(xC3Hsw2l}nOJ;YwOp@h zT*DKZKT@}Wegnj8Z;LrOAAGSj*Mdfq!6*$ZBt}19Q~feURt~S3MEe-?nwq@{@nvB* zPC)En49=P?rx#)q--XL?qOYctemVCJG`;qdJ;;7utBmZ*f630n+ixK6T-YMY>f7`y z3Qm(1VCj8?qh(^68tq zhmxXo-u=qsxKp17n<8NcQSQ5#uG9+T^SpS2e+HKR75rr&t*_K{ZdFSkj}5q3r;Q1w z-(j>w@k=3UE)`{^toGB~Te=)y2M$BD@J-SZ>^cWog&-BpDlH~y5-ryNwaHA={CquQ zDWH7t{JMwA{vkt7z_ribAEJY^F~sF^VAxAe_L7@ag^&3=YP&0pB7cj(fIL|$7-^sf z_kOKP`b#em^rxi+_nbCBZO_l21g!V6_`1?MPVJBHIoz^vl0Pwb_#e zfe6aMQQ+w@{Z**7bl^7UmwpY0pWmKUq;aHv$qAz<8-?{vi?pFK zsWdydl}KpNu5o*TfxO1;o_)ADmCi8U@f=ji{Ml`F$9~9uhyObf5J~;Sa$E|Lij55+ z1F+NrQt=L?)m>5h3Bl(sAf5=cGmbJh9Fia{=!}|5U!{y$-}}c_V1p*SlJQ1+C|GDmQp8;a%ISfV~YOa%s zvE7eFZ{&wQK78rmxxqyCFfR-ybx@y;S3DA#Ac7qJ3N3IzCSYdk!>UDSa_5m1W_9D6_a3QGqr=>x^u!9OwceSQ`J^npPSFz-3<%oa~w!C zmE}J zLsCrdxBYhaD-SGYM-sB-INiA*P`thSI!Fe08|bCLMbQVXYA1RR%35`fWvZY%8P#t# z(SV2{$K}R{*FInBAHso;k2^akS10cn_nF*S=MNjf>cRO4s>P==}wyCX}I?GeF)@pt_`)e?c~ideVFm+0kt_uz5s zgR~4oVTK4QV-(@{tNv9b$RnGto6%#1oCae1o8nKP)mEoukjRk3Qvo5h;fsk35g``t z=!BvX<);tgRjd<#1ssHl=i|?c=_j%zBWFky%(msuK%Gy?n$d>=%I}z!zkg zNuM(wWD*=VH=wJh5Em6uQi6L`Q}4t-kT|g1<5kx?r27kul5(ibn>Vum_kT}~Wt%P& zc`;)H*@5-gMzEx&)yfu67E+e8JWfGRAL0GI@(k2M~A z^T8PoV*M_I0e0Z;Z>lV{)vlAkNU>wf;Q7y2tyALeayr?U$#y5X2i$0mK+Ex}wTJr% zJaHOi7aV`h=MkP%M0#yHNGq6y@1;t;6fWUVV1^xCR({yhuMJ~}7nEXOX-@-WULt{q zlJ<-EFt{16a?ebj%Gt^(TtbSi{D8*D>F3y*CUNU!Vz!>QDAEGV1jE;($%x5f^UVrm zTOT~VRZ1)n^nJ;i@XOGwd4rQXo84UW1Bn0aha6F&SOBh&T*{X zUhXNc3U6kzr<_6v`KPE-`)q4n{9-^cIZ_PR>CT(4)ep`@x_8q+w!ua$ny)kvsP|s6 zl@*P6m}*Jeow|NXRSfR5nGjgG8;XaT4U@DM%Yw`T7&Mxx) z_k;_B&fS<-jj|aO>wC5$Q8C79>LHhhH=^|egDx#VvrT7MIPtYKvw2YrCy0xL>7~9T z-ka^DFXgD455d_(mUoQdK0xa4gPNPm2#8NlAC#y5j3_fS5iH8cQd`u5f#^YTe*Qj) z5!5(=4y>OGmYAV6(4{W?%t!PHXcmUIS_N_6n%TM4^K~7{u6pjC+t6NmEmqlEf9gvt zpZ?cN^1xq|SL~O|h2ve?V?4djVEXwmT%p0RzFjLNXrRoApSJuo%#iv=dBK}Xw={46 zMD;PC;%6D0Ws@M{QQ@Zp-3!dx)Rfr%UbtOWYU`ThNy{=6<*l$wxqaDz!_(EIVVF6W z@@Q5*!#~l5pTQqc>EWe;mAI+nzx{^^_EvA&lYJ1#-z(6~Pp}qxwKQzsEnk0QN?7B? z715s?Z^>4D>#+{F@6#5reAu^$5Y{NvWzgAa7FJ|Qr5}6eU;nsPZ*b9ueB#%q6_TBA z*xm@C!ueZQo>JLdaoxP~3YXs;SVp>Y=|~WOabN?^ypwW9Z}IrAAJ4f&qdr)98Ju<; zXm(*Aj*f^f3yiUO2sOKX@&H51YY-BOV1MHGLIG5-?q4YKP~Fgsbsgn#42! z-zwhG+dS48VC;0P>y!F1wPW-)F;{_5g>@f@{__fVM)I2O#?6Y^FS_&(bw&Bi3zKvk zuoCLun9n_NuKv}r&i+%ufL>guFJ!3I`g9&})9S z;)~v)H2ov4!^@UyT#VO!-%OHwi`@hhS8@Uy`aJ;i7~u+1PTRmNZ1Rg~L#bh!L(H>t zu=v?oZ;#)lF#Aa4RpUa4N{r3Y!n9g6JbEn(S0_(|u4+CTxVQx{$#^sYe`wTj z&T=L)o;r!aK~!Y?&K?;%|3=Gr?T~LY#(+Mdt`uuL3Y)KCIWhPtbGgMn$GO!_EFHrt zP4cIb*YD5o-Jrt-(od@{3mSFkYd)6Ep{zHE+M{soq31r>%4JV0u`rq9OdRj-=VIgC z#RYQ}l~RsP)7Md?182@B9;wq<| zv(=gClBQ9vN2AJGqXrjv$8q226dW<)@C%pgJwg${uyqCG&&U*G-9h&>sxwP(hlbO# zx(L~$;L_OCPoSfgN#U6SWNmiDrT4#SP=aRhmY!jxo9#MToqc&iH*TYj0mJ@sp9G-2 zH4Q2gxF}RARtZlWzq&bw;YzY1gY#v7fVdAxauxyLx2-%+C4eoKcZy?gPj;7VYT&q& z!9Y(caaZ9PT#nJh0#1vb%32{MXS2Jd4E@h4FnvawD@pq|IfJt;cN7|rU-?!shIqgB zEhuThc1%@YKGGhO8g~8h1sC<{LbC1H++On0=stb6 zmB=#kljT)t$UXfOODOrems~=XVCB26=iD?bp%nLl9s(?Wp;7)<#u<}1CbXl!c)i{B zdb7GNEW_blBt(xej$6KW$=9dUpVOZFGnkHl4T&8nX_M;vW`yyLZR4b`NsTCV^t{&u zWq&s`s7ZB$Y-K>{bT?AWv5DOUYH^y}td_VQm37wWFgS<0JiU^+ptpvrkSHf43@cNw zGmVZJ#9alXTpMU3puV><01TzO_1JkfA>FU2DO?CBa^HA5oJgVT2tyRy;8+V+w|OHwG3zQ3N$6pr_D~lyy^s=K z^qb7)6>n+qX`t$N+av?ubuiLGjk}4krveN$Grm23%V++X;{HTfb|UDzno?ILYCqUn zATM#42fe0sEFp7Z$X^7MIQ@|2Iv-lG+dL}Ca2g#gF++Ee=fAl-`nE1>eZo=_QcQ;{F7jnXNqgSO7@A#-@d%w5ZlU8VdF|xjGPvh%d5SibW z0@|aEU`g?S&}MJ1`(*%}Dr~Kzp=0jF6(6g~PAn?UrC@Xwl~?-VhOcJw9?+AY z1msfZ>}w&%c(}&%xWA{CILdceqTq_*1+D9)99M(NT6+rEpz%linN-tdE!XWxLN4?T zkpIEK1=M_IrW6jza`ys5p4F&E`IKx7uQ%h^Hv;$w7ou*z zAg)E0rKmJVf$(Yq^}W*;W)(Kyo!MAADIxk_nW^v84U#nDAcpobAW#FaLI?Gsr1+zm z@ps+=z96Oq6sNHSP_3Wxovk#z__LoBPJt{?e)bsz3Pb1c9rr^L?xY*?hy=3zmzPxo z8qRvR%xJ63z;0|UNj!QQ2-l88x>pn?RD9<`mr}s~;+nA}h?o7jUb?|IEe{k;+ua3s zXjy!bp%-1Rgo9J&fxmy8owKyI(@^;9L_H^0I40 z@wpjt1^caE?>Twz)Zj0#o0OOxt9;4+C1k^@zfd?0a`67Sa-(F%dGxo<%r zwEr&*`Ub$yO(f7;%OVeV#&Ha5=EbBF5;Nl(U%&RW<MC*raHBgKDLds%yiuju|qkTTc*1k*#jW zGsxuq$%UW1rvaZNv!nEaMy|71@#hQiYO0q zasvirbx_z_#Zaol3?5vX*(WSZOCk89rgBN|kFqBg7Q8Gt%Tvb2kEC3PEi97c9<%23 zw#E=0G=x>~LLpCKsSk4Whc+*!9_ke9&gCeGbi}(jQp)Fml9#2;Q#{$i>?Jw;c=`az z#V?DK>?h`IzO64iJ}FIp4^3f^n4_%H>%fQGKrwrE(u*IXvo0$_Ci(+Fa-_-N;kIi< zCKa|O|8u=BgKEXqP2`h5jNXaJFmXr01BWC~>CfmMoB(}w^JUdXe6{aE}S5|1AFUzE+w$Fv!C1y&QM4Azw74A|M&kxg;hmV~xb8Xmw z!Y8y#Mz_1J@;7`OBnxl9Mv``S-jm?&SjPOZmSXqbB(B?k=ma51?{-^$dvjDQRf zEs$3Hr|&?C=+d$`Z0nkxzGPOpRrBv>@Kn0eX_{_0t(;;ex@#BtKiMViLWDHP)ip+K)4 z)!X@+#Du=>xUn^5&AZ+UTgiN|_*=@U=HR5|xcuwdfqj$3f>3TaxsIcT(y%`u{5uc& zB**;%2um$L6SHR!&w9!~vfeydAcB{a{N>|6RlQg*V*sb+=O+GmKZH9ChnsG4 zh;vf6Je@pPY>Oq>EV+X~!L}b$6?^+*^OT=pJBRI)Tc-?gQ8hYb<~ z0#?IZXL>t#8D{-#i1Ks0qL+jq&-aNB7EWao7jHGtyQObrPM-|qn-R!jI|p6+;dVAr z4Ox2jNTsyLjiQFE^Eex7^~(YT#Kyay+pXTciTUV~A0KMOnkn_?^^H{8qz4g9etP~X zh{lKlgG@;y$A_ECmqfvF=6P{+M@U|45Jyl=zcbV~$JN2_`S5+Fa&b|0j%zXBeU#O= zvPqp}Sruo|}rdmQFeb|?K5;Y~@Z!2NHuAy|<9g@(!4YS2udi&BIHQ@!< z!Yz|C)3^5@Tr#A)()D%q)^2+qt>|DO;SW!o?Qaj54%e*#e4YMcFU5o$D{2A&9jd00 zIaXFB$DucE4@W;GC5LrMAsOJX5g@>@PW$)BsBjiWqMs@c`ud2Rw*3RSu zQN=HemMJj)AZpae&FB~iT|lIE$0o}p9Wc2NAjg;H?kx8yyBilz7pgh5AT(#0+uuN9LKMa(MWCW20~N*7P&oi8(@p#e`X1brB|v zOT>ctv+&{3ccJJya}1au3fbUH?HPvvt!nT+PUL1~AtcZr6UK}-g=-6QCZnak7|nGB zR8WPO(O76fswRh!%0feVI%)~bS%gbq2&PTHg6;KRShk6_`Ij_4w&m9z`z8FTu>AR+ zfP+in_s+ery>lOI@A69d$Q=kKsDifLhy{n%<0W@HoMvvS&&*ZH)&JKEDW?ZgYF@Vw z4(Bh>>lCEwxMNkOW}j6Ny?{T?GJ>ELa23GaIm&|6DMG3%65f2JKBs`B=7v%?QeOm8 z58=}@*|Q|HoC26y)KdjH^&ToG2Njob*~uM9g{3aQgp`|*a-00AB!Wi`5PePKMRe70 z{fqcI1xpkfC@KV9g=ZDvCqSXA5gi{kptJo)+&`7H5rlfVr3HflC!wo< z`ywVTSV2gw8wROR^_ZH!8X>b+Bjv&;k$%zR@X7c+f<6XP!(4qHcb~r?B$oieu@5Q5{29li5;xIuta^Y;)hP7dnuq|LRb_Yl~DOXC7bdyXpow;uU z7LrT=!=aF3911HXq{0XhKLaUMEFTG?PqLAa8c z)E;@~kcome2ZqU;(wWS2^*?eLGWX-dci%^GULGPt12K+@E^BHEveU<-x-18+O{Fv{ zspet>sp$$iug;-@$}o_U%_OAaLc_56l?~Q9L3KSs4&4f$H*baS z+jqm~{d>$DmhJ6juphk#emnnw3->>YZT+2ScgS^Qg2JpY=8+ruS25(Q(8Q&_!^7zA zzM7DlLzvVKhm=z;JgFBdslFn(yJAdfr2bRBgh3de8_z$8RFgoY{3k)G2r5$B-zycx z$JP5ufO4veMLq$OOQn=SQwVvw1t`rcb!%R03IR2qHU@5$TxuuCI^ux#VBV%_?x}~6 za#T`fDKJBDquT3|8&P>wHN4-1&SUjB`O$Z{pC5W$3TK9t=c_QU(Ur%&EB0g3!UqvM z|B)epqB@~UvCn%5f%6{5l*?a0+1Fo2U^*dHxa>1QN`VtYOCFiBM@B`h{tS?^$}0^( zD(@D=RNajUi=H&W9iaqMhzC*;v#G2WuEyxa58#5I?M392ALEZVJ!^hA22$qw|6D-I zs*;VP?RacT1vdCi#vcFiLm)L7rni(&qJfnFr9!iN{qpf2CRVyDH1k|TMYU`#g$ji(|VtgD%#RhSkQVGPV z1Z5_gsGwRJiwUSwv^5nQ6=fp(Dl$<|h}M^-V@6#Lrq|{oF)9lCcJ8r0=5G~JI<*5KcG3XY>D1Aze)X{fdyHYT7^;y-GN}s^!m(`E0lf zzOVg)#@icz#`jzW`*td?-G6}Ze!}XVWmcfYJ9qH*9SAsbCt|kUg!A@4f|p$#IO&o^ z?l)Iff0e^?Y#4`E-=EO!_#NCm3qA%?4TQ^dzOICjibJ0ub<&4m@rS8=t}t^atw&WS z6k09-DRf=U39}8Dj0sY@FMv^XNo7G`GfBCGnPZAkQkJEmuY>UG5Tp_asW7+|S*XIc zz4PE!O~KA$3vOKnmU?p(!PNKbYC+G*792Uen17!G+)=stT*j?u_0h6y;74hSZt}$4 z(}(9@JAw&w??EEfk~;K;8kM4=v#SxXXa$0}9LML%zws&}@^6JLa|wdV??y=NeF&(( ze*jWvg3^F_0w??hrs;#A@^4*bAZ1*I!{(&~y$3Bw)!YYver{~VJ;?pqMnuki1R*qS zW8`c?Y7W=4U@alF5{rMj2bm2w}!@*T1Y<;7F)ES_120K&e`I*eP{)W8Aas|re7fl#V zq)jlYZl{mh%Ez15@;gm0tkHPWmWX{m$=Dr|h3AuaY^{C_Zl?pP*=sr;%Vl;BGo{t5 z>-h2G2nz{8MMW9TzhD7E!+epF5=VtZIF(N`a4IiOGo_I#hge0SrK*a#EGte&bwv)U z38|{`0+g5JQ3=H($ls3EmL{xQ_b?6}d=rkYPE-EL@?Yn&rd~Gj(pa<@QZ7)^S8#lmr<8`pV_G>DpUvc>> z3r??G1)o>6B$Nm#J0ayqNcnI0IYD(L>>ID->wjjs1GoPczI$$j-+rm8+u`#r;rH$` z1RcH|(c71xao2-*84i5l>P4^dr~Va=0Z8@pz&u4r{ecJOq9KqfHjskJk)ld**7EzV zfV*=nA>~W6Q?byTu_jQ$K&p2v-#dn|N+G1CQRz@oIm&1PhyW!uB@?9N{!{edG)qmX zk)h_cl=N%nrY4(e6-o>S!HAA<^q%sky2|7kagJ#hKL{zQH-*V7yq%D0p9^>U9K8M3 zOt{^_!r-fpqhI>6;dWhLS_K<36PR&&;uzY{|0y<<=4F2O6VzimbJ!iVs+iQa;AK zNxi##V*|yisbGitPTXkV|Qpe-ke;GcmL`8JZ?Lnj48n$COP)p zK*|JXxZT*keLH-7e9+KPht}3cL`4T7J!2g5RAHiQDgi_w4TV#Y1uQi}6r@UuCL54d zQ%wm}RpsiUTa0>IO7dvL#YQ3|C=g>3NAY!qxM1-@TzLL@Xl|-UBGp?|WC((SY={W6 zA$d$B6mhHCa1P)Sj#szjbu7O6Vh0IH^Rs&N4dre%dW=s0nl z*M+(`)B)o;5&bPeN^XVTJ{4WmkHSq=a~zci9!B_Ecfn`(O;k|7r82sjMo7K(OUsOc z8dOq()PQZc(lUa|CX);BjjQ3e%?hrty}1OoL%d>s_jcIcUk0DIZbr}>zeB^$2e7vH zC{A&kRGnhr`_-5J*Wd#j0x4I2JDkqjd6jqxe}s+vVrwlW)tk?QDUViQBK_3Dz0X0F zbIPixhb}=XhG0k~qzIPYRK9mS*CA*rkV5XbN+>F@&!J!mRj-h1P|Imn(1ZFyneg-+ zaw3B!MK#%cjw)4{3hU}6rOFeh{HUhVjFRf5vU5p2X+BCWMy=nWbr6aj^U>eF2p=Av zgPyME2&ir|(QU30y*2!avCCGToE-g2$^sQIBaC$-Br5mHl!BXV&dD3@Rg zHIUluqg<&WEWjz-KSW3QAt^Bw6H{Z6pPPc(nk>{*XQHt-*T6{DHob_ZdVNw* zs@a!^#+qDHR91ESYRUwubj)fgMPAkfESS@ZUT3!nKQ}=Wp1_GO^7!*11zolAw8Dd< ziwDa6&chh<<_ZMw{Udy~{*FrMT7pKdJ{pyharF(et1Oe-?-eSiR|uw8ernmPS6VRR z`+Q#iHTDkSh6<<>ludEBF<1Qerul;o?vP+BTXoe*+reNzc3 zf=-pr)euohz3nPAOKA1XMvr5bseIn&DCBxAccMblHBY6|NP<-7`-|W_c^~($6Al9@ zE9BMq-v=P&MU|8L+l_bL?!wf@-(bvTPa%BKV~C!$7SU~M5HNQwg62Ge2m&B>!9%F| zk4=bcSq}g7B?u{8h7bd()1&*0ThBZ1bOzJo0j&=YGl48%#t<{>dJrL0RUv9gU$zXv zg}0#Sl2?#)!Lx{({RARsJ!%3y!nmE$a~?AVXY;@QGUk12Bc|poQE5HyHr1q=(LKv^ zQaY5(e3#4g96J8ec03w81-tF4G@+A!GIrP#Xrqi7NSRK(qp_W~+kXn)3@E`?dyGd( zsdk^=fTH@WK>8%J3egHu*1Xp?AF3sSa-Yux+9VwCrPA{sgV*d4*ygXiDL<;LAi_1$ zB>(6MqqF!mo@v~3*WK{7+tJq2g4nnSj2fjJDMF_t-M~re zq{;&+qoxEa6`fVyl(8vh&Z?r!Ytx1RD+`VF1WZ#Isw;C)m^T@j)5ah>GZ}d~<55&F z!L-+CRXx7e5>uN`MPz3vKw`S)paxBIQcbl4UriqBstCc#EYwTAl}tlDVKr}NH72Hv z#=n05JEl5`E|lbYx7_dM0P@%uZ~R4&QqY9le&jKhezY42yLkn>>qaV|-w;mM3_@z- zFD)AhD|7!U;{vq3`m^Dt$IRmb)vsXR{y$V`zbB}qs0ga}?|}c?x5DS}EeP9pGb%SP z!|Kj=@F#AIN+=mVo#O-d>!-4EcSM;8KXfSCPL(1w4<&UZNGXCZ4+=F`xH!6I^LZ7nmCKLTRqY9K;Pc&uCeXqP zejtoY@~^5&s35JXSSa^Oc~E?;E86t2>OC=<+f|Dm=RCO7O2kP>ITVUsWB{dHt^Uq7 z!fh@(j}m6>H}Eg>p@CGd6vRkK^;^f32`F}T;rK}x3T9q|@#n8cC{Oq%aDpd)-}wU7 z8Uv|_IS-=#|7}Oo?3M6K{{uquZ%1&&-Gr3o?(-F-JZfqrq>iGbo;kaOX4h(G@+#LRvi(KFX`ySTlx*CM=4uD`X&xa=8R^n=}) zn0+H0-7t}8CUN;(LP`qY;0IVUAsbtLrxFgi1j1Ns^NprO5lRX_7o;pRkn&5%e*anm zNxh|{qP(Cw7m!kzdjM1(X^(9b_S?qcAh&P7KHD!AACxpYLKYv6ybbt1U^V9 z$Evst9DVv3Ua0AURMQRT?zgTJw-T_g{K-!d78s0+FE}6LQ<5-#Ts#%hG?e5`GH|LD zY~n;^C1y*Ds=*S+xFdIM~mt}*80F`MS4jQwiR^|wLxYtM3|!uq+n z?epp{VW;_ON!Zz`(Ch-$ftz8YiV8S*D+1o;)$H51BYOAE$a>{QJlOrVDWl|cE11GM zc#VBAFAh8xEf%DD-72m7DqLMZ=E;5`fixTay$$H=spJo(6g@7r?WiOW8u&P2&>4rm z6H-!PFsYFmlY z@4t?eMSL!E2}1LhB8ZR*sJ{<>jrYU1@j+uMHfy!&@Yn#P))@0U4NfD>11VWx>nemb ztwdDALkOz68$KnsVzeN7$&(12|0u%etVft2HJ55nz?!)RT1ArKaIeeIuHRUFnN`f?rVOjp_X8&lj|=ZO>)=Kq?V+fW)VtK zP$O;T^fD94Cpbw#$?B@I4V=7YL5dJ8orY#Atn(L4H&KC)Keh_0?qYHmVO8xLUXbaF zH~u0>d6Q@z22xuwijeZz^4m{<)R6h94T|>T{Z|!GF%(q8A?5eVFX2z~<7<63UI*W8 z*TZkm64(#iLKSr@VRbufM{bAx?WG7KsPZ@7g5@U<;eB^6G$%L+Da-f&h4Z@(g_Nt4 z%WZIV|B64r3wVN`N8mIQ1~r6K1$ukSp%xx}jut{n70eURcha9qD%8~OlbcRZQ4W;K zA!%tKrCcWC@)P7_s$xzs_2kiVjN8wuM4_bzQfg-CP;#*5n3MxW>*8xWjfn(`v-gik_}!bbPpw`?eEK{xNh>b7n}H#wAo*4mf!7 z`Nog#LgGBa;DRRzCnfT(LC6fMo7sd8RZ1{#=luOkm~zo02vqaK{G~=o`PbbC{{{{ z+F=`yx1%bsH#Cy~2{Q9h=NeL52%hb>SnRb;!kec01i|S)8m|V9!ggO3pj8VGK`Mw4 z^~ZsTR2-T%0|&l+C6C2!lh}H$Atfc%)Yyo`xOmK&*@nodP-J9Gq*}^ES;1t~69xuO za`O!X)F7lPN>7KB;H3N2YNOF$DX0PSK&qv=$N2R@t60I7{~`Dt$IY70yo zbojc%->*2eHYU-^&!bUhL zn8Jsk@}shf+qV?ikN*z$essVnDici6Z@DJrVDVIR9hyq{c8#WLqEgkAD9f(WT-0wc0z z4OP_&_|7Ce2%9OFJ%`+{K81j+KOl&Z3ap}Xum;71<`^P?N@UT{0W}uh6fOR*|YFJZ#|XNIz$tuVT4pv+am~TS&0b?*W#jo z--{eV?I-`eR=KuTgyY#h7Yw-jgup(6-igjTmt%d%6zue!Oh}EgR1^WV%~V8J0&Rj7 z>E|OzCF9-LVjPUlCp^Nqd zjfzN=l9GxFNU*I1p0#S5A- zb#f9;9RI+ab6jpIDGN`3vyjrV)7^vnPrQXO+wOtirr!?0$!qr4E%Vt(U~QnidbLqd zJ}>_gzAyd){xAIsVNd@8agY2IQIB1T@Yk+|?S-Gf_To>ByHBqZq7lSbL#IQ7Lyyib_EgDygJEiX1bqWz>~Z?msH6F2b*)gi0zIz3l;3Sh`~t zK{lP+S;6(F@`M1XAdM<0p;th4Ip*NhJN3N(hRK04jn+O1Qhlo5=fo`!K85hvHzR~6 z|HxSnBd+CPM6^7DfVP$JokfMzwhGA?Jcm-MDgT@$@XuX}fXaJeqlyZsQ}aXhlKS{L zDgPM{TP6Sv4UiaV9*`;OPxk|72vP)B?R^GPG1d1W`LdS~wCGVPsUeV>K{eI(FcPTz z&ila*6mvT+UUV0F9GWxjA6A3%tc)R$(r>d{W!GOjjCBdc*zT7~AWSg*qqh4b)4WlB zsy<;Yck=N=;dyv>bSWVcVJc2|11CNy-2Agb%2ZT0Z8~JWRz(XlFBQo5TjiP5Lc|oE zB}jK0NQJ^QLgedq6H;6K$Kiu1i?DulHTq9<@DS?Jl_<_WsRmEj$Bw>_gi&$GPM?aj z$*CAUDiZnmR^C$$K_kT?C8cOSV}g_td`&p|R5Kq`B`dX*&7ft{vQS%*gL*E6+$^$9QmuWt#rOtX?L0*~xl=f|?emdLV;MpF3 zF{GqWx_O|icOJ#$t@j~d)9*eJQa*wV&F{5q5cKLbh~hTVXr26h3b+L(ri+;d^A6 ziTa!N@=dtq_#PbV*HvX7y84xXY(eUaGqf!V|8&D;!a$FsujhWk=tA`OsytGgrKEZ* z;Bpo7xhg)^z>|Fw!ITTvsSvo^g9wgzsBe@}N^<8JNGYk7CKY88c;x~Vq~roD^MFJq z)l=aCi~y&cEP+aZqKfKKfm)SXDmH3LtwLO%7^kP2;EV^m+vtaV75yAm_L^)l)guIGvF zztGijjv!@*1u30^oa(Z8qzf+;&cb%z34~NCmx)*V9ji?0w%m*@>*l< zU_{@*=SWd;8yYHeuwZ5l3bIqsSkK?QIYNf-=~@2@&+_=QAte=JB@XJUtq+@APULO5 zAHkchAAr(nwo!JSG2d4Rsh6)s{4)g9;b(Aw>um4qr}F8;2I`#)H{Xv@&;AMlum0MC z)XTpxZb9GIt~N{KHdG+Sc0y4(R0nT??d@9;NJs@9UWTy4cOZJtZAg9T_qgSwz4!q7 z(Ymd_%@^*kVL?g-b4{-q*Lt`em&5H60A^7|)$w)#Ayr_O{jPHE4An|kF?tCL_o)D$ z`_)QgJWRfm)R9qBf|R26q?B@pL&~@TrMf)Pe%{9fBJ(~eDnW{HQXqzI8+hedkZKRJ zASET$+e+pTO1*ksb6?dS=&2e7)nLaZ==rb?eJAhcYd(T<*aoXV4~3NH+wdf2?m66! zakFkh#DXWNo*qSb+ggOTuBDnYM1Hx301G{|o#-Nu9y=t|7(utQ^FD9TA43R=5 zmEciQf>g3Gb?KEUmuj1BJR#MBUA|*2NNGOGes&=>Uvx-rJ6U+4CqSJpUWm zUQuv`Wl~f=CLvg5m40DLE~zqwmF#;9)zt0qrJ@QTsDckIL&V-&khSGDEdA(B9C0~# z!q?UN$i4Zzhvn|GJYapa=iqXF-2`C$c)zk3yT@a-bYA1V6}SpR5}-= zESKF-NUa`bfio>A1#!KyPlHrY{R39Wd&wObz2H&AT<`=Tr5b(@4?>C%9o@DZb>CDN z`X&VXeG}VWKWS(oxbDn zVRAEe`=%I$v&Hw*A@y18bVykhCoB`FB22RJKA&7dN(snv`$bbd2~z4QrK;!xRT%cz z6R|m|2+v>s&%kjf`qkUztbdi(m#B6cmtA@>;$x$6;dzS?7ZZuplq8gw=a_E16@rtL zi`T`bh&|QPBb-d}+0Oteg`d|}3NT*FGQer5$sPu!#(ILRZqSA)s7fnXVyJ@hn)0Js z2r30ys4?P?fAn2=zp%e~NSSIAyz1@Y3FQD!HVbz?g2)a3Ll{x*Y_?o|Ljx!*P+dz% zT?@Mu(QCg(G9mSB_eVI^L)oq%2P&OjE*0nYA-}f&+WR~~cCDGavQt&rOi87m!RHmL z_MeFc6sUGkQSHCUs3}GJ*^VrOU2Q(zUW$M>Z^oo2uf@_o?W3Yn4;pLx%^zpTmFMR- zhC)iV0X@#|@kBkF0GQ5Y87SAZ_6K zRKCfh)J%a{Sp|VLlXe~)9~I%9eP83wZR0+u+JYBS-k-=Iq;&39mVYOudcmR_k$A~7 z2w3nG!L-ht`~zmLA+!h|Di*cQDF4Szh@U}dcvM#AQB5fkmtY`JYN`Y&-X|0)N(iq~!b*@5 zoIIcspkyirI{>J16`jpCffQPoBK`y#fvT}KXBea!2&wwIoB>ly4na!Mg(JaanOuyP zo3W*#2yqln&_qPiv4Ml3H0=n!Nck^HNJ&jSX{swl1kZ)Ccj%#7t za;>R8VfUB;mmube^qcVXb$Fxq6nN1*SU!s1m^?lTA^#JPf-LF8dGa%&aB& z)FqtmNcy6J(d^`l7GpOJP@OJHBK(3NNnQ8(CShwp29^Qzgq(O=j_a!dsvXIJ>;Ep)%cMvLv=D^3(h|fR!R2{Icw}=1#g%sqXYEx~( zr}<-$AcY=;ly2=~FcL&=E4zToAv>fb>8ym9_x}2EEIxY#Cv7tFS-VK9{&cG*3!T*- ziwXB_UyQ)n55svGfz+A*Px`4IrR-y_);zBIs8m)82~Q`iTz9=oy$z?`Z^L8T8|Y5w z_UM6CIH*FtwK4w^_$}Sd|LN&M#Nig0+aHC=Jc`F6OM?_;lW=hXkU2#QQZOgN(&nWg zWuqjoo`RH$){}&?d73)xVdtvfNt*SfnJ-ZT+4)?k+D|f9kdh`nft-w+5=e=QlH{@4 zfLZW1DeX9|jI|YAvd1|w z)6wD*OJGD1QauPMe?qB;f|CX+iL48wlL^CSV+=O=3?Zb#(O~c*q$JPY~8Qk(VSrlgO`i%=DLQtp~$@Gn3A;pk#42pELZD_gXH zqf5W@zddP}?u{GQ!_U_T$pe#6KC}!$0e;BL7(_@VQ<8g6B(O^2T!J15FFL6Ds$$et z6`@b>Km-K1p+|RjE_Xvjs4x0Q_e6SfKcuJ1_^AZsWNA%>(rhQe5G69GS0elfsWK_I zA$?NCDJU*Z?u1maf>x>mq$8T|`yiFi*9<2#%ZfAM;bOp|grts#c2y ztE{bvT=F!W2_hrG;l6@%33`x7yau}jT5BW}{ZSYQoRF^`#gr4f=x#df$wzISkWzC} zvRVyN2I;4g@m4ZeSeg^pzo1If>vp^W{Z0akZpwY{d+2y6#AF9Rq zvs1O80sD8oETn9j#7*D#&}Jb0IuDN6aIrW+^A-JYp)0<ILhLvR@H7N@H!-J5T&JUr5@BU$!K1ZippUbsIJOYk$t6HUyz%i z-dhF&OO&8AA&N`8c#Kqkl_fntHxU(tR9h20WHY{U{lr>wpy9RfAmuECGo`?Ja!-fnWU<_wQVYV>XEvywK#OT$Dgc zf_L~^KEWfnpzQ<_7CZ~XB1u4Nhm=a4^qA_eQUik}M4d1+Zg>gKTV99Dwzr^Hj*3p| z!257M_%7VGy@{~K*KpsSW!PvvkJDBe`uM+T+Ruyrn}pPO!l;xW$W)oowSFI7{8c%5 z3F#(`&Ib}wdOM_C355^^8*5t@olOcISsd@$?NxbYz^>{ulKv-wQi3LxrNYrvCr-)^ zDN6G3SyW#Y!FVo<^IUF2c(y;lKl-zx;B(u_F=;#ZR?dh~y>v8sY z2~M7T5t4J_pZQ4$q@I0!8DhpxK|uX1cvs9sK*d+^teOKCsVh}QCsa8DLE{%7?wVx^ zQig3}lmU(n`H_awEdmya-(*-I4@PNU+3ZASEu! zdFfx_x9|}ZEqMhyRi*)HIlKTVHAc^52Nl)IH+c_x)qr0|8YRi=r$zid&SlA7AsH?N zRL{VzgFDgg1)!MEG`#QeIe||a!Q`;xJ z)4qZrDkr1}0F$_sECni<2@*3Mk44J72NP0GolXjE!FC`e6BvXQ0cGuweyYA&poFA2 zlG;xy4U%M*uuDbo$=E1IQADlT&eE2Nj`l)TlEWfZf!inW4_{+8ERq+-=#(V|;;j__m_&oSj z&w*R@7jUObag)G=(Q}b}!y35K(dc5IhavlYxE4);J0ayxm`GAs>621XeD-EOM^m4Z zfUatq0+mSMB%rz!Qc47sBWOY^f;a`P(l3?8o1ct#(EG~8@TQaUsL>$hK2q`yN-)Of z@GSondAF=U#jiIaCF==vw5nF}UuHUg24_E?f_c6Kl+=!L_9mpnk@U6$N))WLMM`(* z!*RG*8TN|Rs`BtnIvIo2!>$l)F!aay zvRiN}cOuriL~uK)bJZO!I=@RGrInID<`ReP#zcIZa|J%n8V;O1r`)+Zqq+j>#~xqx zS{vDZ7Jqr@PjGX0L2Xq9osb8jBYF`G>8c+|s|}T)qWi?v6yy$4qZp^VwO0I66FQh7-VQWqEYR`jY zbg;D*Y8QW#grG}cghb-?gSA!0O1Z;)#t*i0!BBbkq)$mDjg=5{sU0Qir+{Vd=tGAU zP4%bZ37|Mimg%|(C^KOw!4NjnaM&z$JSNwo-FhSEH}da%4*~fM_x~=P(~|^g9XiZI zxKBcgE>BevmL^F%q}0IPb0hc}$KvdTmbuQsFi*Wjql0b)P=r(E&a&KP`&y4pJq?~fzhZ_OqHq;)8CoW2bm`nNjX%xX@ zZ{{;{AFBL^0EKd?)})-39cCB$qqMMe72&5qMJFY-t1{n*G3^aHsig$w995y$TbxwQ zOzu;f7QcYk@F~c;c`=6Hz5)FPJ%&?-ta;8l;Q_gnJxz1{R<}*O!oT)u1Dx=*G*4>zgEHkUOdN|cBrDKosjx>uex#x;2o0L+<~jExeA_MZm6j!hrgFA;`@f9 zG(Q7HxiU5>MK!Ty)5TN{%|_~gDD(>PM$L#q)Yq1vuDTF)Bl7v$Jk$)&MKv|DB3E1H zytXnQqpAuCu40U>EyDQv5=?69JP= z6&p}TP<~+2>?a8dB;)Z3Zu7@|OQB!%XBCDnkdoYfIt8jnHA&hfft1QjL0A%0`t5H* zx9e@__PztB{U5-YpmI9=5#0BEfbPqm$35E@&{4IbLr!+9!1BA_@0XjTIN(yWb^PiA zq}sDp)0w*P%Qz^Bz*CtevguNK!F<*O?PuL!J{LfENG08Lx-4-v0~Cxb9r08gL6SsQ zn#Sd9f+Y_oDYs!NfXQ4!=UT>f6Sz*sXx+rW!_E9F{FcY#KI+c|*24tUBm6@=Mi4y+ z)A_5>#?RGZO6UG08-^n~uq&kana@`!C)Iwk1gCag%XLekS!Db}NUdG73w|m8gW%Eg z;8rCObK+2DDHkOYHzbhC_-zCHaz28t@6&J*NR@sLC&I+p9$GGPH0gCV?+N7Wa_Iso zSD9Y|sjh(&T_Gg}7QJ#mhCcaK^yKFVn(#H@WN+@P(VF|*>C}8GKS%oYi&1sYRzxH| zhBZs1Y5USI{PRrG-)cE<919Zju*EGJn*|=c=u2+AO}d_hQ#howye5HE5Z1Z$!=bR@ z1W*#z5en-FwRL(AKBhY|&@CBU_}H~ml3iVZ)S`=_66l5o&>^J|DyalYCbsDExbIwQ z2q9CTNk0U8JhHLZb1=5Kq+z2$lHm46voQi|RkLI;KW8`Y%K<4r0?i+r4TP&xD0W1o zVMfnboSO8R`i?&dq&m+Ppl&r<9~< zM1}?Mvl!9S-(4xt$C(byTR}=b%cYQ#`c*X}3y~NXi9bK^M+M;j8c1FGrR4ExlNs-| z?LqgiWianka3&<2RH~$>RRD!jgFHu2trAc@0-q(1pwDN2#OyPB=pn?mYc;B5Frv+@ zK}uwnu-y*s|Kv6VEPet;zSe2kW7MMrmbfSxMMl`!$6dr7-x4MNJM!Vvos+=jdhm$J`cr1LS_oAgw(UMG+)%w0RB zK}zPGkdhz^0hNN&_c*EI$*LN#OY$4&&h2|#u>fveAtf*+q`bMG_z8NMnyJQMKc@|#{t4D)pxPs7neE_76(-uN$3n3+0$T!hVZO}>DqyVg;Q#l$| zL0AkVI3lTDR4^SXTnNz`J9#(HCaY|y7+laU}Y6cS8@*s3q01V%-KQi(P< zWMjKNo6G539)K;zzS!bKCuayFfas!(;RIlBtTBY5kuYu2i=*;aa)4?BKVP#k3>yN{ z@QwdqY<=)$nA)W5gq`|7bWZ)F$^JSuhRtF|T)(~u=@EdE{A>jIdSGDxNDM6)jADTl zRaTg;n%c@sGF8-G)$nWrsu=ZzLMm{a zf|O`9K{sk-KE}|Mjjb)l#4+WlC)CP|GSDl?6M^1N2np~&H$rPb|8SLJNhP!GZ1#&m zVg5jjt}8=$uRu)x@I5VH$tvH`t_xrLfA#dEAa%K^jS$(+OWV+f4;7@G2r1*5=QLMk zmtMw8sWeFfs>k3#7vwePevDrJHkz$xaE5o;XSolH5MRb&Y5AAv0Jz+4#mw^u(QEz_ zaFsMkgpxC5P=J!ewRS+Q=pqTG5JyE&83`)q?ER0$!GrJCEU9mQO( z<~Egl{ZMKIx0^^uN+?;bqv(#zcX7Y}!?W^1&L84_AE*Ax=j1NycCP<5Pu}YZsw=rH zeNdETBDa|URpW}_v9u?^A_IrpWUQ1lJ4!#-z>5L2$!$ph03ZNKL_t)gl)=~JbGs70 zz6>Y#l;QYpx-lse-}Tq=J-?8{ht24j@-Tu0Qnj<;UM}J10x1nD(*NUAKN~5xu10wI zG&sk-3ir&nA*J7)h7&XuGZbjVaS5252o0y28E~d!a;uu5jkR*fzE21IR6o^aAf;q4 zC+|#1Ip<7*N9tSXQK3P~xprnJq$Ije+?03aG(^`gK;46z5tZ-~9=mUyD)IhHOzJ!~ zbzs#!58xY5DRL;m5rG7gKq|BoQX3@!EuGam0fTci4)iF+q2O{p?ngxv7_n&9$19h# zQ8$<_DT`prB=E8bCpsmkBF>8mog(a{wi|M=-I#+Nd~TaQhwqV27$sthA%?FNAj%-( zAZ#$wEg1vp#zMIN2+qUNXrOzeYf|M!2&g7q0Ac7)`SJMx0xS$0ebTTcs0>T%?&Rqt z&Rjc@I{V2lItu^VB$a9p?Ak>D_!B;Tke!_de_vOm4~RiYZYq_cDL+*~N}K?yKvlm~ zba((p3>%^xQtb$VQ#PR`a1uymE7gq9oRR>lz6+!rPD(u{ltzmPtI^d2R86kZ=xT0X zmCe_R@(9Z`gavuiO?jXP9aUsl5DIgWRb-!5*GfPYqq3^kRhOZ6Xdu?CUa3XY{=jeU zf9v#gww4$@uicoW^f`5SK7@Fg~yTh;g}x!IKy{+IoG%De5Sb9{v0 zsIo&UlOhCE`S?LFoBQ(lzC7W|xL%qCZ{l&fnag)_`Ch{2Hj2v%bbsP5|3YzYeZao% ze!j#O+>Gb(9!iK6@B*60IpJcai?h(d$>3p$C_Fy|wpJMw90ygsm#$3< zQRiG`sgPP#0w-})DRywu?X?%fbUF`b4;EqnX1=HGj24ll+PuF1=$>u`!n6K@o@3|2 zZOj6ASIoT_QniFs-7F*%QoSWBMBFQI%X$|EX^N8+N)^-LA`KYi}vd(Zk*sB~5)2G!enaQCP1a6-*!n5)yP=9;rAJRYO?x=lTR}G!Qbze1e9K z$hiPJTnezqvk3cqO0mbg6x+GH)gu=>Ju|V*C6OSCB#31ERis@6iy?^eS59ud%oTY2 z(dZP624f!rDVpyUK}o+^H^RvdDQ+)tYBu_7@N(*jX7?!m9L8eCz@cc{y2B1Bt$?2j z!n*W8{0o!CX2!xfbK&XagpAZd7?>OnKMy0alH*X2Ij|E_;#>q$HI)TOjO~Rn(icUi zQ&Z^xlcSmM0;H6K68F?a4oHpW@)!lD0yQ6_ASKOzRP7MDsT_>06XoNIQNxgvmWUqR z+z}Ss4N(z2F9RuwK1{)g;rZwj5sZ^3PpF&!|5`}Ncb4Cs9D_CHGZ?(+Rd}s<8phS) zrk{c5kD|5XMb7ILEMUr3K?#-)DIcFo zK*hssje*%DnhK>EPXZ(CO?nPM zb%m5W_bqbk8ri49&+X%+?2vMVqPvz%g)W0`D)oH?ar>~VSJ1J|ySPt^E>7xUc@bE( z_cdd1#g|CQdK~9l9e4jf@1%Z{1eK++0bhEAW1FELcIac2bJ7O!imMSwMWRL5n@|W> zO?-{IzS!!ShFxC8*ydV*?XE=xQz>@17GpPIbU3h(a7w3hl0m}q8fg*?Ay~R|Uvxfd zjFS)N(qtFNWq*RlTS@wvTJ!=R4M6J%&PIbYC)y#Y_f?rF*6I8%K*}zGngmpA_3DF; zEJCVhB3c)G&2vxOxz=6q??TFIG2^WlUV@j=h?0U_B*jFbSD+6HvIZePa{!8S1}aIG z2uVvM86r{#_EA9)wN+X_lt=<3wB;PlMOEdZZe*TPy~8nek$_2pD8|(l+m}VTN@HrX z>9R5jxePk4ER3xxLS9A^dUo?ba#DXpM}{CTM{DluS|PY3KMf-*ix3yr4`%s>S`T70$ z^4l#qE@cY4MEZ%Tl6L&br+1_KVgje>xr-qs4oV=k^b$zPn5^ZRR0eek-q0p=Kg{!hG}{KM!u1Z$n2=xeuRAIDAG>eF67MaWfZOQdc-#>Z1NJ zq&`=Yuy&D0$=~6b^9kMCTj(=-9=eZP1h=}cFM^bWp^Lm~XQ1-#MhvN+ji{staP-jG zU*y6dV;I#*b?gM@MW^sWAB~+(i3&^`?0^z!eM}MdKBy?FpHhoH2^)-qu-PdYTby3o7n%*8niPlxPWJF|LaIfak&KBVe43O5JVAt!L(;q_kP1Qr0o1_P zH*mWK!mNSYHcAs8;kAM8Z>w^FvVCuy7+81e|p z*WU;qXJ-r>T8zlxAVh`)pe!#P#o6Mb1}Vw7BpG-+Fd+yHFQr7 zK-JS7eV<0vT!56!HAra^H#NGNpsE@|$3<6FRj6_S2K##|NJV`Qq{>UOP+pdcjLgAk zZ*N!QQ8g&pFV>yOg*&+XZhpOf+uXeXq(kosfYHif?v~1a8{6d z3NFk32BUIMb~s(?q`E*#C8?Fn6kR}N*!3En)Z41bPe(`@4txNoZLcGcpKrpJ&#}Di zxPp|brYfID>C!L%(@iS*t@T^9nLnqKyqb4)(gY_V;Sw|Avrh!~P5M~7)89OB)>I*fvm$0!)N+Wr_S)%{qwTu-6jtmI4 zwMixksR&G`MVZu`j!VWxNyK4C4nIQ5YBZX@TzOY-7Lq@U#S zBMiI7phUh>Mn3Tcgjc?WnDNsQI&l%4>%{HNR^z0ksn3Iea_47|f`~=8tw!P1 zixC`s54N`)(XI_Y^Q77UY`oz+-G;Rz$6~2598#}ogHD>`G)T2{LMoELiK3#ZzS^>$ z>y_xX3=$|2&gDQf5J1g3sq`yzft0&alirgOP&u9tdngVKEX2;F0?drz&C;RcyzrPH)p{L;O@GwHmvywB zMMZug0z5oWRXGenULJ^x3`J?a1Wlx>Dz_qOT9e{~aeaFzFpRAyq^h*2J_n>6eN+-i zQ8)6tVA51T6%tZ~7%L&^of1$<(1Zk1NKi$N>X#Z_#X0va$!miHJdqd|jfD7Uw(#R(<0yV5=eD*SpN&BpBz%w4l7#ur61e97QPFffMM0& z=%k+3`i53%GB!RB=VrpDq&H2=c}xk0A(N#`AM^*_b2QS$vB#lzCCgp;({C!qco})h%0~N-8E(f-AMTAf; z_bCvQX1+972qJTPE`cKvfl?W`v{P1d8Bg|d-Z%{BaTswy=23jVEBJXPa(*?z^(*f4 zN-opkwR3%2HPA|!9nZk7RpkU^3%~4n)k!VQGv7UoR&-d*n6vr_B1XK7pi%STOF&6p zJe78clJQ6)-h@4x$Lcg0XuLNcO@rmK2bY1b}9 z>CLMU*ymm}E!?A7#h-f;K-t>)w>4qk|2%@PJtF9C!Xc$GxAkouMaVP}Fzci#Pl7RYp^)f6i41Je2NQ5%gi~mz z)bo1E(4C<2M}v`oGRSx<0n`P@B_;2-aDSWl-U6}By%TYAa5+Bn=#6vpSE(jH3xB67 zarBS|o*8}O1Kilq`J{x=O& zbW@#>qKxZbR%4i?6ui^=SKz+sO?WiCh=?UG;nr;nu-x3LfR(#u5`4ExC0?rOc>?DG zr1aw0E(0ko8-+M3EttYF=vOaEZWXAWfo|uE(C>Z~&U@a0%kD|khj3R>f3Kq3`j;@g z`6GOD<}l7Hmt%*NNTB=^|4}8Fz-l|o3-k(@EE4iv1(hDjltn0IsirkaUnFUaa9;X= zB;TG4;Eje>q%e_^ej>>pAt{X<*&tHsf@IuOyppX$^7lz%*aA)B#3V|vjn1eopW6^d zZ4x>@mY?BjN+JX~w{mX1p1Oe__9p5^N|t3#s9LY(F}jKn8&4NFfhWV2{5%u+dFmk9 z)@`R!uzg7)S`W|RZ=+Q;&&ajy#&u8?{NO1N~0v;5qXIFsuaM{eQ~`!c06}M0x=CJGA_!cT$9@{yUL|6YD%YE z%O=A!`yE{Wz-D~7`aEj>v=)7@T?*eTzR}#8^heppNy$F_DrO^T;(QFhqY2&m+=r!K zZq+*7e(p*2(?~A5cAS{;HD>#XQ<6%!KLja#{^viA=#+w4JpPbYEm_>f+1x@G)U1wNkD~iQ1Tj0 z(nKizU6QT6#Snrf*JL#ac%El}Y`FVr-UPMLDcGwU{Ma8RU$$@OPQ--tLR4@tK{Ffy z?#@V0ibX|PE}@j6D(4EMGE@7hh(1YWls_a+tBXcpP zDj(yji;Q{Yt!Zvb!o9hCu2>6HP?G*5 zI-`yRUikV`bV?nZtD=T7j!BvW390rJbP#YRJ~o@O37}#%en|yibd2J>n&7JD_H}$b zL5m!un}hY&lmO{^>RPC%K#K%vNTePCg)8~}ui`PGtXEKDxqdXCr;9RQMP0$qawVil zqV3FJoZXR)J?kDO#E#Q3+F%y&+mGRe-uJ;;Fw?r@*2{%`+cN+RR zyehwh2c42v`DA#NNq^IHxR=mD6@CHtqG_rkuy@%M_)xv;=b?M$bfjFl7=L>0I9{25 z0Av5U1);SBVWmV9N{&E6ZRAvf>I>zp0xB0Fx^5;a?p}}Jl;`l&lg(=U-_JhD`KoTN zEI8e~9Wz6d2!&8y>>ZHmL&!+bguvy3P^fv=UZQruS5 zKp?FZD5<(vk`y;Uh0$-)4Zv=%VOZmtkEL0>5jt{GImWg#_KYU~(sbd1y|HCILVSG@ z7ZHK9ltelzFBD`YqqJZ!%8Jqzq>2cstn^q6FUwH5_QuthL5-18Zb?(8gR1Hp98vTg zT@0qnLrUa;+GzW0qIz{hQfwbYga%@8Y68KOsPY45q|i;J#q)Ctq)O4v#|xiL{z!c{ zZE(%y^e-QytCcUi_GC!N}ea?!Rf#>IPLueE<5O?2sYhndx*IMOqW1Pg`x|jq-=;l?3+hn zSVE|+c$(`aME*JGRz0l+aBO%1hV8Gyu=7nq>I3Q%825ilNA)Ruw!Vv?)i0uA-6vSw zb{wazW>o`ACBf}-f&D2a1t}TOi#>$YAL(EO7*&LgB%969n)BMS>5vMQ6H@u{+B1}U zvYGo+vAp=l!)hKx=wvAn**c_pZaF2+sG9qg^e6<2=^6rr;4u+IW(i^E+{DLbi2@XN zB)XcKXrGG&Qrwm<4_1K|0ff=~{`{O4Q9Yj57VJ|Y+G3i zQ^y7Zs+||mHgyrb^f-#Eq!~v+-AjlXHyAe65Wbsstv z5qzK6aSIVUb^%haU5z0(H>32f9mxCj8WdeWANRa}6uGypMu0R6mU15wx?VMv?#uzH z>F^!)6~gGMM&7>#(S`5f=HD*CFOmq|Cg+m`6m-C}>j>r~mZH%qT7{f{KcxDpJ}C*H zkW3P)KZ&prP|313e#wi#@u0j3q5w)iaBJ5V_9g5)k zNJ1!_3RQqwr`Lca@7Y4F=l)u_{|zcjhNwGsI7eWYJ{Kqa$KpunXeq2x}{5QBSmz;hwz*fq45EhMu#X7pF4R69}2p)SrM&S03;J4~Il&_tHnXQL#f?uy) z{^maeq@)4Nh9d;Z!>Uim)-jSGDkF%7C`Y6MCE6wYT$=K7s61FqoSW&6EcsLk0a`^E zjpshDHEVWE|D+sozlqh+0p#B+f}dWFx@nkOV*gp@Z>U$H!Kw zuuC{`U$Fkl4lr>}gp;)n65hJl{e_ zwn~`vrN_}?;l-71b=Z|JB6|ERc+}5Okka6EK?0rV>z5+o+QsNm^f3mFosH<4*$Cu% z$@UN+&26LSAfSE@0_nuM*UmwYx;Y4{or%!;nTVOV03m!nWc=3%x#Ao2xoRo;Q}MrA zinyzmp#K$%F!0(H74`I#S2_X9-D2!cN^?_} zhm;oG*Uaae2pa*0gqRD2B%oof!ISc!6Y?f>{Hg8~VWj$)q$y7nYIjoNi288bNIs5K zPHL4Q46EsmRvUw{l3-e`n)-sU-Y71~PYv8{Hb`HW3_9+GZQS1ueUu9N*k(w;Ue^@t zcOHfVp0(KIo{Obk=~!{weXyQs{hsFIe>B-I)>bQ~zVQwsT)mM!U?6(*3WT4(JBAWc zg@lwislwa=D9F_!>Cy&9Au7Dv#gH0lmjV=_@=rmkv{U119m`x_E0KPhB(OrAY`W}sHQ#vmGqaD6{V>%AHm%{aqP%}E^rXs$u7Qe>YJ8r0k#6QyIB$6y{_EdENb@8F9+(Q}BY%hUQJH@Z z9Ra1={tk5OCDZz|3U=QMDK)6~DLSafVGuygx(&MN*HYI&H}Cf<6T6d?5|L`Zs~*!n zOVe}E+d*aADb;=7qXbmE)8564(9U<@+wdH6R=$k|?I&fq~4wR~JUx zhWEHRmx0tAf?z7T)lEgxbt@5&`w7w}%tOXC%MgFnHyH5iRRqprgiZVk0p(NBn{Fnu zZWj6xND-rFBVz1a^u2l!A|@?6!M%u@xCDVU^Wa%N8=k{wtKbYJ>5J+j-|88J=2R5k)q=D!^N>2^ZB=+v z2C4tNlQwclAZ6`n$C}&k!#oeEd@I2dqCWO9QVvMPbWvY>6Q5)(4_59-9Mc+oAOX{j zfbr$BKi0@Px}7z2U2D}Ct7xiEmwqSdgObPcoGgdh_u)mk5Oz`lSQ6Aq4Jlu2m0W?w zZUk8mY;cOiLC+K%G^XHO_cELg8iqserPyuE#%?|5?&UD0T#fBvMR+ef4c1LNz9$gp zA59mW)T8(Q5q;c!k(-hVFJD(gMh2rWKOLn-gHc|VWiN6#Km{hqAl`xT;TT(2dJ&{X zU!;EosWPQ;wL>qF1}uk0*Ou5Nq3(q$!E9noZ}jWa6NT9+s`_u&pot28R%wn*O_GGt z=Pu^azkY!Jqtnj^DX71>8sK}D%gZ}XB7gZ?@Lclb1ve#wa@P_#GKNZmDVkn}SIaB# z-u#BD{iK@TcF|?g-RPv4;ri$4q9n`u(;B1%cwOC-(vvDc!?04q)Bj2bbtiPwZi4Ph z0&2!}(9OIP#;+fS)3PVIjetrEkJqn#26_o?-}pQX+g^cg&l}Jkm_$v1VJ~6HJAYSx z9`D8%klpeDrk&Y~GYTv6MKAf{KXOvdebR?uYo!Bv8Rm}bV3Goe5_r%dqHD1b9+oRK zP?&#B82pxSpgXeO$=BV@J={y(&kMd}ns}6RIx^dX5IEUvxAR>DOnjY6B6}16M%QUl z4oW1=cv^6TB&bSYJCTHrJwe#q5Q}e*zsZZ|IhA`&7130X(j?7&(j54khd)Bp*pJ~}H%)`o zMVd){0>9eH2)$glCJ#*skb*E^XAnk zxnn&BT=xxPu3LoOSAUJrE9Rs7s5$68aS;-3UV|h;E%}yJ$ho@(`S)x@>Md)KcIz7S zzhNo*-n7X_)NyR`()8oHX*oUhgg6Cy!arSHMV_I2FP8UjkPrwg1KRI{Hn4+e4Bk>*Gn zE49wxjRvD9n&`$_ocyrWJpy}tlW`zu2zGi7#L1XSoQkhUYiun}1{dO>OA-!y#ABCB zU+mNm!~xd~9B?keZl9qr75o~PeCe8#z!R}KWh*wN^?<^ zpNX2PVX&A8De;)fMcKdZ|3lNy3MomIB;%&qxqO=Q1*i67;H;}E~}qX zx&9pPN+qPFQ<4Zi6(G_40t_uL!q`l$dqK;^x8@lwQt+bCbBP{&Qp?x3>RA{G9;c;$ zp^N%0A$21mbps)F3-mMZfqvefp0T8BUbzzW3q2`4z;kdktTl+lh9G$m1QcH2ul2R6Dl*m+!FacYKZLE^aHyVeg@S4=qXTU255e-*Nx9a(_4R zu?8X7B=F&OuDeBhz2zRxf6smVhTrLGY7EQ*p!N|2&2Y|3c}G?NYikj3CX2v{BYe_% z2a-qS@DH2{oEk`_@o^HN)Srr{drE|j+uP32Rh=fRjt|Az{e5w0S1%meNYJ+LRq0x! zkyFM^Nfrv#Or_2pGoE`mWb;LXcXaMxxO-n|WjZ{L7{w{1k^ zjcd?-!b12+HjA2R7gQ}xjZ!a4631$p*xgi3D{YLGgtAK{pG5RI;N)F39X?euMv5>S z`6c`b&kO>x{`U3g5%D-acy|qNKr|Rjl%pd@>Q6e^uSwDbcAmgT%N~5>9gmHwSuawJ zPm<_72}^I*#ZmD}O**OM8%ssl`jh4Vq!qff>@eGYaz6=Q=(5l$EW28*4#W5wvvJomnZl%$Z5 zYdNm!A|&?#|KMlo2r$VRlmcI87s4k6-mV6u_m4(lPO2(mSXz{>O2%jB3_xX75h9|x zBO@aYV@J_Rjg(PRids^vQ21ZJVf$(NvkAa z%S!2suTn*ML)+$_VZ6a zE2QMdA%{i+;UqHd6iz-GVCP8BZ9S-57 zmtcp>0ZRIqR0i~??T{jD7CxZBq?_?;!s=G&=KLP|`S-)P;2{_m5mJi@%B9k5Xm?(8 zX9meF*u>8uQGfPvRJsE);P*oq4u1mI!ym#(csVscjg+;oV$!!8aopO0^NUmCN~m+>HD`Y(mj}TQT^KCdB-DIbv>HihjRZ zgs3Z)B6!p{@EJK*jgRt_!MLO5s^qob)f^@F>e&>*Bsl<8wu)&=j|{X^Hp=` zq-G-h>cyDI&k-8?1n#^2BdDq+GFFU|4E#U&C8P_a+9YMmfpeIZI1C$&bW(&slU^Vt z(SEUK+)YL79RP>R70ex7Xv=Qhr_)2;0!WOo@72(3))^{&C@u+iAvI}=}o#sbY-&~9!= zn@wsb*&*d<=K9`A4wMZ?4<0~7_a2Cih`{iYV)(flk(m^W;_OuAqy$n0c>;kX)DAC1 zR7f|K>~#!bQZqubK{z1QMgI^|;+Tq6rit-&8jwWwRe4-rL|0YPDRF92cPcBnpMq5X zK0!!M2v@0@9FWS+NjgJ-n1 z4~}j_-?@K-C*k7c0904mVWrY1NxxNtWK(|*`qg#^by-NsdT~-uDUhiW9`o)dpne10 zOaf~5@1gtpe&`qf8HR5jg6^A#38}x*O-U^(Eo5ClWsqh>Ix0icGo7H)?|vPI{qIum ztD02CeeXfP^9?w!e-_CrUc?7yw_u-&`qP>ie$1pbPnhT)&hesk0^go^mp1)PuK$=< znRj^+{~xF_8cKKbxDq-L2-%zY9DPoZJExnu{2R?JaXW#QRW;#VO(0BwRYK0&>(%%r zTYDvJZ6hhEzFP|0dFosVtY-^hIXwi{(}Q6`#K zN26gmCV&cn=~w{Tk9tyGXg?N61)%MCFV2(r9D#SLi2KRK`Qrm{a9bR9HdVvYwt~N( z^AH=Uj)8Rc+aWb={ze36{TTrhG)Vb&g;W>$*3E?9sF{SsR8?}`pAIOXZaM<%XDE4; ze+oCsefSi3R{R}4)sj<>P^w)B@6ihtoIK_A^_Rlwa*z@?C9aC9(XyR)g_P(5r0DoY z%tg2IuMj`}YgFCUh{U3|QJeo1VQuB#xP#uY1Cq(`CvW(ZQ&GGA^1{tu;5i$XjJz7_ zokFnDApKDiJx5?k9VlHoAvH(?ls-ZW12^>b(V+8Fk7aQ0dP6ui z6Dm90`eLUC|DG=K1W^X|x#eN6YXL>a?$EBA10y5TOE z+RqbGCIuE2KE3+_qq(I6JDjCzHB+m<|^1lNp1*tj>Pzq86ks~dV07@QTK`1#S z>nDyLhKzxllS=3tj2xAt&z@X%a6Gay5>Q-}iK-DLNQjHZhL%QtKa+L?Xp_u2+O_7# z-c0`|rk@p3!fgkrI%T%uP)9pHKDigsi|MH7d{k(<15grLuI5i^X_VGVCUq_EpGvK? zLTjG8R0>k!vZUxDVWV60C%UKG2`K_<=I;os`?&5egc9MjRK(?F)Y6AENk$6p!?;os z<8m%dk9JTQw#hiE*Wj}EEkf!&80e~W68*RBMfhxa2{Fr_$18`La76l1{*6Dr4U%8$ zG}=tZ@$Iomyih;IVo)t^oBruD=s1sjNWCDsySZyABdrCjk`} zOFc~HDmo)cS0v-2N?}szj*5ZvTz94%=9B!9pCOzGCfn%(x~4K%2@~t7Y$^lRZ&P79 zo`8;HF=#s)N%bLwA_=SBXg?B;wnIJ9ao7i@BYtQ<+=J_U_`EmOoeDx50oHyr8K$EJ zgc=>%iAuh<6estmV0+5|9NzJJUPX74O42`do~~Omd`R%L1xwfML$CY?;6Guy9a3}P zQ>XPuc~PFqQO$raf#EAz$SJov(Hywe&W3BvTqU=f+1fZOm4QOS&;>4B77=J}wNttM zXPkd|89-ftlqPMRCqIX0og}-xFo4(Lqqa|T>BPlB{=}0VQX-4Vq{3}?Kky`0>Vs5*St-}Ck$`BCJbgOJcrML=QffX3 z>*$mk^%83CLPsTIqPR|y#nOFkbBiL7VsI=l9VdI{wgMQrS0wk`f_O z)i;r@YY>%;&3vED#$>v}EUb4Pg46L;SRW9J*)b_F?>-2#3cS(s`Te6{3fa!an}shv z;|+|nGg9OFBP})-z5Kn9mob1YYA}YCWnpMZ28s()P+B+`Wm&0+3+;|^RVAu?MxCTA zvO{XzMUcwH*y=n@|0JXw&Pl-J0F`R;t0`2?feC#>)Htcc*ie)dq-dEY(qb?qGXd#o zu_&OstE($Vc<-Lrwsj*+9Wu;A3d;(#1&EukJ+pF z5>(6o%>5BiE4Y4{mK8%F<+Sn{)kmeWwG&jvjnBeq`-^be^*UXYl%gl34t)&w6CXpj z^+mXCcoA`pZ{f9rEjXb@`FCkwa9k+?<(P-Zvi}nMNXg?T1V2OI8?MMtdj${&2#e6@)@8nQAs^K_) zG#f{EC16)e299leLG@4BEc6vtaZ_6I+0FaT5K?!-XB^$nnAz|hJy$`>r-pzMI7nZW zI4NoR6F}9|-G~SyEGF)!ui!?9hTgk5@xsLREEmmxUfJp>0mlX1W^9=ly5=xD;YFT#re z+v6ODT~a;Q*qyG4@G-b@AFc$DyV6F2Z?nD|*ZK2t09uSeXm$!fv$LNn$06Ay)~gH` zTBToUf^62MQ3Lr{lH;Z;Z8Q$V2B#E$)?92dG24Nxv>SZNkYBm_Nw5DE*^k&qCDzWqWm zVO)huA}hJ|#*7p|X^?V&X&fEY_?iNxO96E`NL}ilE`n5bf$F=84DrK&nBE8v_D03f zOyp-Ls$z$OQ)7^s5l={^qPAujA|ra^(7^*V29noM`aS;-Qs0>_g_OE<+vN8m6?knp zLA|%P39&OCgEQgb)cmZ1l(Xu4vO~%tM_-h~HQCh(DH-oXNEuf>3BzK-XFA=}v|BY$ z&AuDDh2ok7P>*VmTG3U6lr#^nkY+z|Q*>^tC>8Z5O@9(hK}WUy73g=p1^xa>s*g%{ zWD<-=C&6j&J8;|d8seMZ!)r&^;v6qHrnd9yWa#|j|HNdIc68f$oIQIGr%!#(<#(xP z=rC^QekAk5m2^k7uyhPVdwV82+6M7`(qNVXgQisb;NC)5TL;q>Nxi2OI;0djpG2HH zoJdDB7#&9k!@M&U2baZQ%k180_$&Y`KX%9J4-J$Kt0(EOZn7TDQ;cZ()Cn!0x}s?^ z@1`dkvF2l)+Gf>$a5R!#3oh(A@p)7p6GZD>8QqX$rO~Q32OqLV8uzZUS{yTBnjEtHG z(QP6jFm^88)I2(;S@5o!soaW7ofb490S*$};5v$Mk|w)a5rI;xxgP8|EYFnmI_maXwORUVy>3euJDlR-o{n)yTeWA%@&eK;7{Tl5YAMk$lhO z0iWW|>!$EV!ltG3`MI1_7f8v4>FmNanB&opFv-SVmjr?$RJkJwY7k&aS@|7KF*xLw ziNkKiI3j&X#x(5FWndTKutOI~AkhsGGTZdtoI7)!JGN6>sf{`}tS6M#>nVc~>y38F zvOz}(@q8kr*1Pn^dbdcda*M$-!fv@wf2``(A4|RZVWm?P!Ig*xLlV{)2NS9}XmrXX zm@??}GU(_CEkio@m%;bU!A@N+HnXL{V8Dn5Oc^(!^2VJT(9$$zaRqAqB zWHPsR@Vv9&jvIf2KqpsZ#>XQpz!!ab`K!?Lp`~Vu7;AS=4me$a)OR(muCP<%>!pdWh)^o-)OfDHVsr_{a{cIk3Z9+-qPr%$Wzd;wCn|ASQ5sq^>I zMe?8YH%K5wyTyv5yxVzU*DA!#eG2X?pMaAzwMo!}07Bf815%E@sIHP=2?3NyT+|u~ zfFNwX`Xh8--fD-`9duQHfbN^WKu@=%S0(2kA&dy76_45_fU?)5QpFPa-mB=QR*MKi zDb^?e-6Y9yUx$A8+tBU*fS{TL{h>*4Ca~Nj>Tk!}7|{44o;$RJj;fs(Tb>lsYyD4O zePqK9w4UCJV@F@&9nk%de0Y{N855NOYx`hW&ZWcLnhMjoIGE2x!O|KB>)CiJ3bxZx zu%7BmmlDGtDc#KBUa%e;NI0aTb$0@`F9}4$G!Ha<;)J#D8z?;*CmEEQKB7J*oIWw2 z**5@W)nn;fMy#CVjZM>f;oy=an75U|vTrC%`*YE; zZ!k;;lF@N60j47Z(0+`r>tr@vT`ry6FxZZdMC+b39Nrm)B?|`8LB7q4>wX@IW90i8 zepf51M?Zn^(X-%JH&=z8d#bGEGgM(gXSyg?LQ0zKJZr^SiDtrsPR2_${|TrFB!a

lPvX#zn}!Whsj8 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�ed1}T=~+f zNT_n9$CTZ?%CoySd3gO2<#%rKLZw%^caIkj@5uAFxO^_3j6I83Gk-W+=Y31orXM(( zF@od!KBpw3KW9=}QL?ikg`0fHU2jYFN;CG%vSjBJb2j{>W5sY2mVIl()FD+klkqZl zOXWI`w&cWtdSvGWk+I2_ZFBt?^F)ZeCGgRLyr^wQ3>Inp2q*ZBe=bmhrKpGl!mn`MK4kddu_Y!6u70}BreHI9Fu$R zJ$UroiGPos1ZhPQH&8`fk9!AkLhV*+a!gu|Q<4Ib04PEJrU`R#Z@C1I*2^ojkSeX_ zQ>Ep6Iltgnxwc#+T7p~aWq7t*C7@bH$m?5(?!TSpZPrmc=xZj7-ppea>8F)ecp&$L zF)Hof-hS;l8}Bbwzv|%FnIxNsQ)*h5qb4;dG*laZAAc}0o?3uCcW5KC=BH7&HL#(awa&E2ihd9qo+nQH1pj^2Tz_Tgl^ zG~i-ncYd)5VM}NfFOC*U19MNikDrx4r(6}>6iAgnxJ|gPH&v{xsOnJ#2WvB}>TTO* z4e8h}mX@ufNNg#PYFd|0NpXY*xYA0Fdv%Q0BKx|xi>611M)VL!_3Tm!srbKvRHf9y zTqAtcv4U0Qe3fyC@Q;A1gjC-it!SIr7<(%dss(xrM15(O7^guhx&l(_xP4LrNdo*I zhJT|0$_T07ii-XPsefnFdWu?C&$z1$xOr}j27D%8vU74XeRDAz_g$m06fhVBQWiPm zF;|%(@_w#JMyni^KqO;0x|DA*-Te)wdjwh;Do3B1V^W~0lmd`i0Q~o5l53Rq8nu2( zgO!3)g{#s)DRA1SGGk1vC@&({a6oi;68aM}FsK?>7Z#$sB#^qY82#miSX@|u)%gVk z6wl<1{5c%Hb5wfHs>iIBYCNU-5ZoYy36DHS7m>lJ6F$Ac1OU4OzU)tJ zM8;-sQkGhhJkx^B5`_UGIH!8?x)XooVstuxyL5_`s^UE`xd;~tiZk50_^Ke!M@HhZirG-!Rk9qVD+Oor=JW0C_90JPkrWj)@7D`b(Y!HW~Eak3tVe4)4vW2Vq388t=CEZ z_H$Clk0N=(ST;`_%Z?c{*fnoHsmqp-yt=A}%I^u1U?P=ItP0QVVtyWFMo=Nhv_}=oBlEilbN8=9-Lv z`dcaJ7-7`HICu3okW$ZAyhc4Q$404)6Rk*SRv#;K9l_PSsaegRj)_faAs~%zY=l&z zxV`q`N@E*0WaaWD>W_|bPU_Dt>MuzBcQyqoj9Dqv2)(AVXOu{f;Db|paQS5vw)-b* zi+-(hCSsi%M+!e@e`Z(mch+F%A_uli zv1H9iJqtfGVaz~1qu+Mr{mwd0?(50n%qR+y1!!xX**M#c(Lc7O<>wpeIQu$&gL83x zJxg;&4t+DR?64Wj?pv|!y#uRWI|M$-cy`}~kGLbho@sb@P0{3`vZA+NhJA}!I491) zDSjf(Q9ocC`XN?6Z(`{>086J=uyyKq`9lZl=zFJ^04XC%oP)>8*dygWJ*rR+$q1+mL#u1q&!XJ$ob#^087D1T-XDUz$N{G|6{jHw?JZK^TdHGcuiI zKw#)pjg|JnEV1!tuE9e<<-&4tQR@X#sb;nuG54Uvy)JiR`mwJ{W9Ej`=W%hde6$Ku zBKdq3q;mJ}!A@_2r=1O2<|;FU3OnCh;i4)- z&;M;m$$2fag<7+v041s@Q?CLzRO%!xeUeH@TPV)z@D$BWnVg)9?(}>ORQiic{svMO zXBHB4bULqOj3N8}K><~nG$>DaRDO#~H;On|I8XpkgR7-FE*#czzDUQFA`>pA*|24V zfmyHVSu@n0DgAZKeczmQ-}|#;W=pcyy-v>7VU!fC7WkG z`?)Wrc^{C!qY0blI z)2PyX9!^n{unQlCt^Yd$rM_4@cfj7M4Gxa2aC8vax5UY|1x_{z*jvYAXWbMV%NVRJ z8)IeBNE9W<4KWyMVr5kyYuiX1oTIR@uZxpw6dt~bxCeE^J$wLxO}{3r<5WVs&d0Z{ zIIF~&xceS^tpETZ07*naRJEFmORI%Aw_1#I8&T3ye7dY9`rRCA_SjC<`b+Q+_>K|Z zY?R)<>g}rBEdM^Fo(ZIsLsyV``ml@(%hof~Dwu4O2=Wag)0*TOg0*?543j`oOoP~L z@MD>-ia016M(gY~NR2Y7!We_EKr4W$HlfURsL2eQ>awrF>?-wGp3t40AAdu}^ciHV zUdG|{U0gkXjJtQv@!-C=?g!$?pWNo@lUqD4yTOx3(m|5_y<6wGbK|V+&v5VNY3UG2 zr{~5=Zr&>8>a7!8yiv@#i$xR_CX<`7o?q6^V*A|jZ2949rnha&Pu?NScME5Ub0~`~ z16d%Tns0DszPT;y47TK(d9Yuc-=(ltT&mlXl{NvKU%!hNWzzM1qE>4@);{C4%NA*g zW$RR{Mu@K;_EzS!Y2JjSgc#bWioBxcP3qE8K-D6)E>*ql=+!A+0M$fuQYyM?;%yWK=*Hl_x?Gg`n}YiMh(Mpb~Gb2*oTI7!%1k;fY_+o#6;K9 zLeS%z)~9E;4!Ahkv%espzaaG=+|)R_N>%crT=fFge3fxT@QM7dwEPM0AKHe)FF#>d zI2F^%1-4rG_bHekoq>7rbo7S=T=^9deHAX~kKobZ^E){GGf4djRvNI>`zlbWI#dc) zzb*FF9hzK`LTQ$OYToZaWrUQ;MFG|6g#;g-K)=k9WM0me-}AmSEO$6_C6$vW-{i`f zKyI8h;p{;jXYzHN&DL{xtu!3nb*va{%j`GI7~4mvQCNdG@F5Vd=!leEQlO^h`*kdqNAo82A=*#*Aa;xbfnA=CXGA zGFC5H$oTKSW7sDj@J8>B4D8d9PAy_-+cb)Sy}L2`>(5xTa26*I=kxI1RW6)4%z^w= zN{;3VwDxhLsDR4?u!79(jQ`;a-VgwOJ6IaDRd2IbFRGff9*+(Jq_)d(NnDI$ z^99%@EWjpi9yX1q}{g3wQhi&T|;ba8^~)TFt>`p%D#d8d?aR; zwXw3PiLHRk+9MH%kUsb|{+Q6#qp8_t230%Gl53xfYtllT1XeDI3vo?cgnzg7)O#nJ zz~-B%8?%m@AzyL&_$_J7RRG0rLpA@^O+|`6k&c~~&7$lH+XjBj3X5>|>FWxl>WGV~ zF2_OaHSuS+-k+@oe-`N6nXGdXK-ux51}SHOlm}x?{h1_yn`&R3#qJH5=2V-R;mz3k z_Qzx|U&`U^G|rzsDo*G;k8WS({@p9wy?2?r0;zinR1d|Wi&J|3`~fc%upZqMcXv%- zCC@#&!2JhjxP7~nYgdc8e4&VoXZKTBkj&8|X&gV6!P(LrP9Mu6D|H>eY+Ass6*E}z z$%lL!P=i?>wb|fWN1T)ovkb0GHM3{FII7(`S1o-0fVizoH9Aw|T94gb-hrFa{Vi7^ zTE;n>aU(}yZ!k#5tS&yTu5w>kl9U)n+l0onXcj4uiY76xA+6%0Xc$qA+M%9W$@eZv zv2;<85;*lz!+e^HGG;rktP7=}^;<-r5lX*br2EJDr{CtGeuGrs-iZu&r3-ayRl}!> z17SfvL`T;lPOd4oab2x^d|ZsENgcX(O~T8=jnWgx_zP11!A(I*4O3|Ow$wsjHAnTr zI7g*&;T?bSh!4)CD0Y$m2BXJK+wr84^cJd*;7k~;qHLaK83 zPtA9!_fwEE&XJiEsH(z~jKLHt>Q8}6E%+5ssZ#X%%c|7Ja?vuZPS2(K$;rH&`W3mC zQz?6TjiQoOT)jGot7m;VSFGdoK@(2rilfT0X4?c)#`o2+>|ImFb~j=6N6xIB5JB?t zzGP+0q^M{cx2~T3b*%Jx*<S18TN;%x3dj;E$_@R!Y86o*uP`6Msob$fHj(TziPRvKYX~7n9F&68PQ5Ry_1;X==ARrHZsNd?CXNCr7e*UAnPgp!nfBpK zu@q1RG-BIpA8=vg7Rs+)ru^|8=}bPP?12L7J?`HVmvsMzxF&&-%C_?Sp>)6mPPeaf zz4!>HvNOrwx`i$CX0m?Pc+%F+=8(YWV9Hv~A4ubRNj{HnotFFPJh$$i@@1M_`%JwvYvlJx7bmz^9v8bd zqS!f>g)R-C;3Vop(JD;|#0U0!30r|w^G4A)SzF@m?m~+=<(?u1Qgw}xYSu`cR5Ue$ zys1|+K!Z@{q()lKzCT0C7fd9OiUe`38d6@wrJi!TgV$+Er9D+ukaV7{)3zPLaK}uO2!4RMo6h33Jp}E6LNEX zb~*#k-D9AT3OczKot8b_2rAu`l>({N;;xnn zP^VG7bUgiXeqh(>wd|5>ymjL>E|pf{l(?vpB0Z&Lckh#*xtI6edY#XP4rcTE72LRfUV7H|xOe9= zM-Sz&W9teQ&z`{iao_V{@6NQWQH}obG4yNNh*uM$d9O=LdN!@k*RQ|A#+lPNUyw)n z^{c$NdmHZG5w~`qyI0S#b=?vM4;r9_>Av+wFLrKQ&7GU)Iew&ow3MwRZ(k+*P0XGC zGhYoEO5@tWeExnCKaWgg*azMWec6J6E%gkFx99a}Yx*~HXY1l9=1uVD%@11Q-*+Bv z-PhtQ4Z3H`1yo6#BaQqloJ1~7r{mFh8eTC|aBn_E;57!fh{0HRb;i^=9#gw0aXoc0 z6F8ZfM`G;|gHx46yaPH>t$H7#nhYki?f`-!Ud6lm%eV-n0&4XqFrq)+VSNa$_Xahh z-lA&lR|u&yfUtT>{RoSENdVTHs$o3{sM-~8-wrst#bRq;N1Rp-tZhQXX$i1wLowLZ z!NR=-_JJ?q-{>2vb)1i1tNFM!6R-+!9XhWf^wkVP#35FToQJ2|*KFNb$`iG=S7j-A zR*}uRGCciX0GC~oXGUN%3OrkqtB)YhP*e8R$W@Vi(y*^K^dndgVG@teoHx3Wa+MqJAEil-CT-(#hV|^hCjk*m zcL`^j0BXF6J+pK+Y}DJ5X|6J51X3u@cZG=`r$2 z=7d)DrM0-I)-4*+u5}|V(|AHtqobqdiGfdZyDaZv)P4$&2JQASXyj1x+~&&^kv zdE$={`%_w^ig_x{dG(Ozdl}V7pw+8eD||g2iK-ippSyzqDN3AEByrIZ#5bx<)5bMO zh_6qxrYe_TBXO=O2*ODd5q9EctPW1s za_s5VuwTg>%umcQf+}Bym;b0qpR02B86l;zS*U2i%FuQry#6))>ZU3)u`5t%x&Bnt z-vq4|m5GWv)Ic>&KsDz#P+eRpfLf2?;wsrMMyJBlPmJY_?2lNK^&S_lwB^)cd+9am zII>Sqaf&IK3v3xTK*yxl^^AH+$AmAOSh1ofTXs)m&%P~W=4X(RoyOHmXSsIaG-H4G z7H>C4rcM~mtXKbFOF~_f8+9Tsf>$ z$0@sbigSfI+WEp=+u1O82J^>{X6lIHY+JsFD<_Wf^uZl*Yj?DX?-$SRapOvYi$>z$cJb)JWj3r^$efvDnLlR|GbWGX!?yzca75vZP;> zCH)&((>_$k%nC*`U>lIo~E(H8yw@w@T&HZAx_jX((KuB=LD zmSba5EE*aCWe6ol7a;N>+2A1!y947yBg8#@FR=Pv=gw#W)Ko(N^X+Rg&b%r=`PAk3 z+GMz^f^UrTT`yEsTWxKZbSj@b=JJ_SESWQlUhUiAX=jVQPDjI#P$K;O@v*bQ$KHn8 z0p9dzmB>$Dea`gJKQVvOM3&Ehd^> zs$Q-%t{1F1C*sy*Lx_9j)_ljyQ|AN$i zbo+adQh=(+Zlg-h{i;LP@u$S`!sPSWeB&=!x-P!(@w0JBoQ+e9nYcEegnQf=s>F@JvEC5Ot8@_{RmWf- zhR!Mg6T1M+TtjgQXoF{HU%YGe!$aIpwZ;R8Yd4gR-M*l6_b+(k&2fD4={&ytdL}>o zIFnIh=JC_$xlEk2lplYZ&FFFS88K=$-wdC^J0A?E&nrXe)_X7=y1YyK&L5K4>V0Bj z-Xu~SSy*Tb&_LqlBbn&axzhpJ6}AfV+;yjv~7 zyW0lBUtLG+>&b-0tj5h}B-@kE@I<-drw=Jt>BF?4@IQYy{@$jhcGXbdi?SD_4*ZUd zUJ2|H7nQ3IC)*H2hTe}0KBhfEuauWSV&hbHou&vJGQVY)8&+ z+`_{L_b5}9lT|>57L@UnGI3Aodv-~~+gZEss(QL(ZDN9#K&nniAVc39$lRF|IeH|I zOJ|RA`|?SyoGIpP$w981F6NGOipuX@r~I}!^hdWTe|Sq?bDHDQIoi5-Hp2(KPOsP~ zYCGA}r9lJ(k`h=jegqd!9pdPrELJR^#fGiRNZ+u6(Or8m*vXr*Hokl(keZ~oV~d3c zDJE`Yn)0e#A7WkBy}%k+nlL_f!d~cCBJ)R6m$D zi4E~}v!``@yxK{=Sxs|N3R1Pig?==2um<42AoYJ^Q=fIkXRp41YJE|e z{O_#j`|~-t>>ee6nuH-=%?T+`O~pik>cm{kPR>PtY=)dyLw}=^kaATAH5RJyv8awq*KSCVbmT$LC#i%=oSr6X(9hm`y*j`e-s+ z_h(Ra{2={bdj(4iQ`$Cf%8j$9$jR8lYkhl?x^oLzsmYWcE28-1L2`?-NXtkjHDw$7 z_oZ{VD3_Ar16(+Lob%FvT)TXVs~1bfm6dS$;t5R`&K7e*U{q9)Nmlw!GSiaDl=s}V zZ9VH(E+TpJ8qS?O#N$UQl>IVK%kPl3YXc4H22<77l{G8oQ*tz)A@9A$$ZtPq?1*pZ z)43fDLaNd$u?GEaF9ecVdZd*9MOsOL6Kbkcyv$XWUGj5@urG zY_d40(Re40z^di|^tMefv#5*SsyaIBP;pWoqR!@rp%jp$ky&RWmf<(t{CdL0{9t!3NB&8%6zie(Fzux`yd)~s4B4dM!x zEMCI=dGi@J_Gdo(>|5R%_$fWQzeDTRuhY5XP?FkwNQ=04h^+Mzex7ada8Jb5B@R3L z`dC=Wbs7Ayw~fHfH31v9PWZ=shhO`dxOWhz*l_{XyRV|@%bEDrn=cOSV{Y9o=g#B% zJeDq+%KNB>yVbI9rGK_*AX7u+_sW=EvkkwvH6ukA&OTizd-YY>C4kx_u4#_Wl2N9% z{Ah4slr-j}bzV$04Q8HoO(r3{}>KM>npMv2D9_hT36o zWrl~dJ+>AGss;M-{#&n-y?ZOS&K=|Y;eDj9UBWDJJrlke%Jh+6v2E!r4om0fN=YI2 zE}!7}-3vUvb)FXwuk*P4I`^Jm=Rx^39+zDc5Z>X|)sw7QJcER&ND>>=qMpA8iS=u; zb@f7u_ocFQ!4y*0FQ;JjN(MJ+%IB^@d}ij%aGf1X1WcQCHf+<`vc+J_%BY4szjaY; z<`eE-yN12BHPx$DB~-54&dQW#O&V&6Ta#Kvkr-b`{+9J<5?!6dW|8=M*wd+9(_bN_ zTvVSwK}rqxHPd9At1`}48HWRnLw>(^Qop$?W3Yv`S94ezs1oSaqb2dNk+|AhQYSo! z@E|V|V(SvypgK|V-HH-O#YTtIsY5*B;sU2mo+zJLWq1BBNd5o1X+1;ry%bQDR}N)} z?n#gC=+iRZJ+}wv)QS-CqR9dUB>~mZnHWmvVNyB=-SO$-sK!?~DpjFZAeCu^RAp(9 ze;ZO2?o3NitAT0)rsC{ORVn&kK_%C8R0UJa6j04Ig6e{DQ)^|v9>ayTSf86g&10|g z{-Fje&v9h;W&=B>Su(D#3Bx+*_~s=$CQs_Y*9$*m*!G{9xMwlhC5LI#sU0S!I==jT z2)D0aX2_ebvvkf3P8J{JP(e0F3UkO6CzLPy!vds>m(Fnd%yF(>J;#+xr@1O%x^d+U z7ta-Q;mjd{(oxPzZ?&|fh=WC$oGg`}Ie&sPr;p0HgItgX=vZ;SIIwhz^3&P2X(cE1Fi(@lu-xRFChDYxgBMby!T5W;5|=jp#pPj+}+)R!f$miIh{hT;_fMW;uQy}LH_hoP(cQ2{Qn_0hlISc2` zllL3X=N}Je;DC?l)P5kXTMUq%_#oOQ4kn^jFTA{3;Or8Oqhnnh#GP2`s^Vf(9}m|= z^p?%2+TbGsI?C^;vQi{2rT)viY0zUU&K}<}W?Y82sOLQ7zP421sfycsVVs}+XWR4V zs@UIS9vm-av_N~OMIF+0VdUsS$S?%5&EUy0X~ZY%%=ked^@E86qfJ!GBVT?t3t@av zG%MeDpOQU0dGz2CkDuP6to#Ab)UaOZNnUB+k!qfv+TvG78w7l(Im z=d+i4(jl@I?{{v=@PRKe>!&YSJZ&T?8yB*3?L2mGSV(TldNOvdB5lVC3Np8GyfBqZ zClB!O_BjEeIP>TCC@&LmJ-EWt`&YCf>%zS|>C(C>wSxU<9#e<+2lSHr@+ceEE@aih z>C74VEko-y;zK82zEnwYb@r^+S+GU4$;6B$5#c<&eooqva`_u3V{d6mRGm761^VM` zZ$-<57}~XNLfe*&h;LG>0#e~}9HG^bN)pGSAf<--l#BXXNc{#VqyBy!sec_(Ed)}n z1VmxtzAXv%_mXQ3GeW9<1*Ga#)5_X+>eQUzU|+Uv+3=T>`oFblAG`WqKL6c9-wIIO zdG?G_c|7Dy8ZIegF{lW;{Q`#lDwx6uDw7g%RHx=)I5ypw95!zhrkSF>qcNyqIxPUA zBBcDkI6q}nMf9L@YT9}#^*$4|G9#vH(XR@ZS8KYIqtc@O6sQEA=avbeR*Re3gvrIt zTGXHZ+ys30_n}WleSTh3g%uMV8P?H+&yy_q;r+V&wBS?TS@tdOuN=wh{o9FZ))-p{ zTZVi#h@zrByf?5XS=%>ovG_21#Py^WW{`3)ll=3C$vJn3L)TAnRC=eymrKYJ02LHw z$p-v?J(F1Ymx(Mg?i}8w^id)m4u@OMIG#!a`_^aq_ z8)Ir+2Xl+6n3;KD?Jvz1n)tI;T-0=(6{AGMOzjwEP@(7Uj5ZBmOi&}{ zzxgh^mdxj-xafxuE(`GPi9^1{wq?tRtsah#vm@>N?cGRT#wHHtB@3M7amHr0t(#BsMwMo3 z66+UEW9Pa>>`U9q^(&|3KDxql(X$8gHoCOI-6v=FFeWx^)Yg zGUhvmz0r?1!fG(c%1zvq153qKt=BoRLO?aAdMJ-CpW#J$89$GaPL9DqqsR!nT%D*Q zzfWTGMs#Q!r-h#<%KNG{QgJc0G)PtRa}zkl)4fBiai~w#kE(#w|6%XEerxtol5xD@TP%ZC4vI0(!F~t%9bfcR%RM1xaK!N{q^_c|Jys>OaK5N z07*naRQK&~1u5;VE`*d8KvAf-b5+kF=hah2oNp)-q9$L3>HrR}vjDsfd>DSEYzYMB;H zp}#6_DtCJ!sCHqVzXS7I8!+ZA#Wmw=Iv;D!_^IxU>1@wOLCQr9V(5ym8Mu84A<v*r*L9YuEb z1%7z=fM0%j%Jb*qOMZF6<0lWfFK|++q3+(z)8FVC>#>3q@LNr&6~QI96UH7auYFq-w4e zM!o(gkW$Myc=CdiU(RQvRSm-QwY7!32?|oW;%wAe36yM@XmDVRf|R){)AdDJ=+ls$ z-;QJ1vqN4JOxl;h{KoP&h&4q}t!#w@*9%&~}Fmb>D z2Gp<5{6QbFZr)^~PHg95`e|-oKg-=4^4{c%6ThA&EQV-xv5k;isp!c=Xc^jvU`c(Xu4KuW1+ z;e6DeLF&(3)Ni1AlcB<--ykJG?Ai=(FAvI=E=rkFMYRRIfqp&&)Tu~qpVBlAtfo~A z?%lg1-rmJ|pf0M^Xa5h`{#KCsvrT;z3Q#IL1I;xODP_5E3 zQ|NDO7H!qS(v1b1G30N+B6kU8vc}SGeKXoMv1edAA0{pt$$)iZ7`bZ}JtqwkC!wQZ z8()sc?P2sMJvq5)87ZfZ5_3L{scA>>4_kmo=v3Uor;E#+fpf$xiiq1Rbz&}+Pb{QC z^eUQ0ETC`nGRDSiV8Xr-<{eqZV zN(#wW(#Vv~`Qoh{a>beDNqgbSSx!bCkSpVDq{p0;4(c3_AKfA|^^7= zXyN4st11}G%cD1xH)ViuZ-}dBJ4#mWL5=$TMC}+ebvoxSW($~}ncT(8=h~S4FVaap zeewf8J^q2`&z|t~>5u#*@cHS{9e%u@$HV+9{8*qm=qs{+U385HH?Inmu81zn{zWYf z)Q<%hwUkgdFQ$s4NaCivAGdD`tS%-Jd-@#A+SHzE}ifWoK5j2YjJC^j-pLBQ@YU(Y}~)*Y?in?wZQ$IgrR@6se+=f ze}2KoOAr}7Cs%?ixNd_samB3M-FXbNbgPze7`*()ve1g zqZ@OLC0U^>%2EN=BDoNHdczb#U#m(5fs_HiT2VNEy0kz0!x9E_2spgpIdHP&!i=j{Y~!z=f4v1_Xkk@ z%VR1?X^tv&0Xi)y?E=#}s>|{`uCF#N_`SIW-7Rre;;8ibvVVCAiPg2TzlSneE&%kEKl?HP^lu1U1s^F57^&B1fy7x;$GrTW3?R68(@ z3ZdWQb!r;b!)Fs5yNu=$A@oYw%$FCBF!xFrC+?ji{$>^l(kWfJf0^XO2zIPn!Wrqf zZd}de$9s94O^Rk^$Xx8Lj1=*(=i;R-8Z{22Uyp%QC{YnlxuUJ+VND%T5%;>3ZM_hO zj;nBL5rS*O?*&v}q4#Kj(Xt{|c2&?B%3)<)8&}un*gH4Jv)p@Btks?3DjDh49o)Wm zLkrvf&3nJ*SC!HI7p*Xiw$|sxlSkany+ZzlEY3$okZ}AUk^6SCZ~aR4u3O2;9a}jc z6UF&6k=)2k;pKyTxhQ(bvwPRXZ7Iifm-~6y>4(lON_56)>|bgN1Dsc^!V(d-le=-6|X#uEQ>1EhU@m zqFjSbeD+NQ!YC7b{sTy<1hX&i{lNMfP1$K)o#O@{!gS?0p)12SaTHT@I=<0cGFE5L zOba(=IF@2#&kq^iwlz1iGdUX@PEzCva?;}H(V;m-94&FRwqWh5#ajNo^^0fo-W+ZvUEM2? z$Gvx%s~3~lzIh32md=pR?FV_0Pqd(lom*D$$>3f%+UjW9*jJw0EIPJrM7LTs_|(Nq zOO6|&^I(C_j`>xp^GiX2T!`Gk%AgZSnbRnsF0OJ>RIW@h0a7q6ng-CanV$fvjyBxa zu2p?X7O|#DKy^B{R{8oAqyjZi_3ZeYqta9eEG5&rso$dd-d!j4M!y|zs%5fJQVzQB=LCK;nw36r6*uR>VOR#n|F9O9yHE&jz0Ry_@Q^gy#Fz(lv>ihoB z*#7pAQomCb<)``@)tbtEyymg&q(6T`-^@c4IkV8TP`40L8mMwsilbVC)%EolE-W2mG`d5%|>nhGc0*rbK0>u&8vSFjiT zgFP7KSeYwZ@!2^Ezwd|e-o|lMAJz(8aU-=xwx`>{Nqn()BGaQcFd}9rO-`)9D{LM% zXH*2^3=GlJFb|uInaYr%3PCCftjq*jO1iyYpgZt2mPf|nc6>4=4o$-8(0J?)jl=HX zIBXA$!)E_j%uY>0A2S{EGvCW&&%`=n8jh!D;CgC0-X~|$AbcTR&#Yo_@@6KVJH(-z z$s}D)V$a?U#6=&|qW|)9v$>alm6=n0FNKwTbzEGVP^?HNxzg;R4W~70(VNiI zQ9SWSWi$UK}r%E?m92;Qk;ES161#I-C za5S$Zw;NNe)&$&wH{)1;yMTEcWt#1vc%{i?U;g1g&qDF)IeB5>EOM*HZVPcw25&<3 z-W<`Z%;Dmmq=O!5v|xDvSNWa#jE0m(ufT^O7E^6G(fpISu%ar zWNu&2VfN@zEFAX@5qq~1b6^)&Qet?RcYz=Bb2JB~!RfyVQU#)X(d_~ib(+nS`&UVb zJj|Xg%ej0$nHN9b#Sq~m4eiJa{tcl0|^SOCf^ftDwHirsp9Se&AJ5o zR-u8P4}NvZYfh?N8-Zodb_Dp>MoXooI>LX*_P2*rp?T6SkQJmYqQkAw?C zuMf{2z%6PXMiud=Af+PgGM8Xfpvv8VRh|Nsiuw~!XoVD2D7q;m-I$SJcg zR|y0yWf(Gtvp7hu_Z8t(N*VzjA;(V_%8 z+oI^Ly>Rjfz|*rGWlD9Wm`f}Ayg!Pl@E9%I_4Aicc=YtKHUy%old5$wd0AN;T(^Nw z`}D!LN<}J{Dj}UtOZIMDLqT>1uOHpz*Pm7J#66x1aGpNCEzk8fzdXw4*C%&*rD_sA zyU(NBSBZ-_%!eOz#JfZhN)@rEbDPGTIJk`mcdqj6aXwdblGwI^f^oE|i<7Pt_PUY+ZXfL3x>3CGY8?Hy<06nM+GGO`UQ-!9 zbhQdf(*R}4#_|`yk`t%5?aX4UGVC@l&3;2^4jH`IV=Tb}b6Y+&8u-M_hOaF=7~@io z3C&tFerSI#XD5@DdWN*vP$rH0iXyHKcolc!tIr0JEg)`Mw>qI4)^R&4jpXPP@}3EN zG7@-jIfJ`b(#0{q3#LCp>Mf|=4EyE&2B|*)OC1wv<=;BT4RPqoQQehePwrjij(}F+|y6oRfG^lnJHQbXSF8mP5uO;wLh!J?*g?-WG$E`eI$L{MOL zoa_zSaG#o!Y8X&c%OU7jw<7+2RkRTFP8}Q5Spe6nWv~V*wb=Tfviyb`r$K5KMv<9- z$)M%vQ|XUX`zFi8@Aalo;_vil&g#uPRidd3y{W{qsL2>2R86f(n9IJo zfW`duRB=sH4Qbv_wK4ykCr)LluRivcIJa#r_B17D4EaPV4et0Z>#a+!jxse?w zcabBVT7JP5(o$nNxNjRqqmD6Of5UeZrc$nKRf@WnqNJlEj?UiL`Fwk3l z8|+Ip#b_vlxw!z$sw76cDmWJlri8dB2Zx$``Nb4&T+0){h=Y3kfS12K5m-Irr-%1B z8yCyS{sS1=z7rcJOd@Y4?uc$A+@YSc;gtX#~T&xSI%RWMy^Rin3WEqVp{ zGUbbbteG>O%PA53a`!T?p55d1FAwEv=too`y~jV^<*K}{E$f%jrF}Db9iH^-)q#xE z({iO>z}@TV>{&IN#iKuA-@>V!-ng74pMS|09fniis~e@Q8{ueK5ev(bm zW1PW^!A5%q8yp#G=FX^c4e3|E4dJ_XkZ|%ak%xA(L|(76qYbXEc8vb&6Vl^P@%eyW z)w6J2lme3pLXAa*`&v3BxnI`+dUP?Z zxq7c-u-w0?Jbn<}RQV5qRDfS)%}G@fNLBDIDGsWdmSv)zK&oz?3N&s|jkeav3o%bIIV&~v zXLW4@)>k)Rc3zxJvMNn4;1L<$g_K&T``-mA6-<$A3Z^iZ=VDY@+!Y|x#Z8@4b5y1U zzj{@V>gEf`!2I|~ z^oK`aIPy6L**6>?h2g|k7*EQ5PL0MaY@7x$HRNYdK-)P2-MYb8?GR9H{|x=cA(*Wh zh}GJUvD`ESi|z8bJ4T@2Baaz2R$SR+fzxDpPXu)F(|%Ke(wica>U|J*B`&U*I4%Fv z^BI0{2?wH%aydVT%&a($i|eRVsSM7}Zp@xFS338~l<+7=QAZc7Y|CR?Rh~n`N!T|0 z7MFk_0wo_T^d+zmNSTR?vT*XnvwV9zN;Y8pck@V2N#_3T8$7ySz%M`jz~cwENl%Jp z?)Q^v(Wn6vMvvm*og2LR{_shZ{GiaZnf*uJ0O6;Et)dwz0NEd_a)0G zf6c0CqdC51Da)sP$-Y&yIJR*i2`6@F$#WmxyCD#IfT<>zs?YU^m(L$_OkP*x2K5BE zwtPOkA8GNS!n0fwj_hEWWEu;-uc__rUspgOuj13Woqy)ZbkJl+vA>*_zaT!kLre%r5Za z*%RhW8iQ9+M{%fDj2ST)U%B8J=2(Q$7EXMox98B;6SM%D=`*Hjkn${96gyi>0kSpC zng$Tu#00A53RXe2Y3N^t+EvR6_?+n6zBV0N*VanKYmicDkqW`|#$CM$r0A^zC%T#( zlgjm{!1P|HhMG*JJp@8sR5f1(C?)kc!F2D?L?G3G&H}>D?Hk}DuCTbc^h)JDsadT& z4FhY@pl)UAii3IssWvVB<#$w^Z@&K2#E zY(o#sHouSM&OTV|=!5yrekSeu5aaFvnC<-t^L>Lc-#-+y1LBkp36Ktr!1nM+j9Ui@ zj0R)5V;H)Pa{h{5m@VsxZh23P8~dZ%^pQBM0q75mz&c_KhH!CT(&{3|iX$6`E?RVE zJi6HL1YT3n$4$dR0A(j&cZi#VThv^94$q+9w%MG&o6Y6)Xb!GlBppM2>6EQ`|NWkH z>>#e9cokf1TygfOk7G>%Wc{yjYWx+3Qq3_ll*CR~1WR!R=C);VD;0<@ zF=R<+dNVhNAMfRBa9X`|5xzC5FtE@2+{ix1>z^Kz86Cxr70a17b__d0LWntZkXz@o zc$jyEpKn~}!MSwu(vnDv3L`Q4B=M0aG{r_7BVH68dXUp64sc-4Ru1mn%Hchm$x4pq z(Y-ugKKp^!zy8dx(mws^KB-BOv~AS{7dsoi9Wjt+1y{L!HkOIqg4sFaJD%obvwO;P zn!8rU%O(gbt6G>@mB8ApIMxUB??6iF&mdKB z^*2aq`rUA!y8R|RJyX-2n_1$Z&WnS(pt&eb;-pm2#H0JF9@PzTvZ*|}n@4U=CN;{I zpp1(HpY`vJkAoeghZa=wVgG(uSr{l*#7zL@K*?YH z1*y6;39N}<&2m&LUy>@`?&6Z`(y2`y%}Kq{n_<6qA=Oi$^g*H25T6QJ@7~e00N5md zO+7lA?$f={nRRV1fNIx799siAw)3NlIIl9LUGXaILFrN+1U9Hm|Pus6x~q|N2ru^$P>e z9l|+sHbw=GLP%+#%3gtmKtV~b9F@wur{<;9@SooF=&HK*T+|sWkwr*XjLYS`l|3-7?~T>=K?1(P7V-q1g@i{e})SRnN=M!!$6@*9NCr8ZVNXB>3mvcwgb+n2?;WE}>K7)^9q0>{D+ zlAaOIt=x-5g&k+o*sp086u`V$Q+e^=0Z9iBG5_1qEF3?cgwW#}v>xW?aUmm>@j&S^O(Lg{3oP92lmM+rZ1RBj*P%;^)HK6!w!qk9NDyqlP?Bcvrp zOGkZP<0sH|kffLfon2+)RyPRF97AU%HT2c~|+aYY%GL z`{8Qki@9YbY%I!SZuG~#!SV=QZHT-K5Zs5&7Ih2 zR*bFMP@f00bq)*_NcAvS(Nmn%fTG@f)Tk{R<}7EmxQk;u*5mH1hWTt6+`l8+*32gS z_)dYvWzx@_*5>W5XC#q-;VjoO6S;A*aF|a3m3uLT+$+g%^)94ts(^}jAf?Soy%Uc9 z)=g<(DqKHxGgFg-lmgU){A_KQPf0=Q=Cw@jN&DlyTyoV~y}WF#R@K)-22jS)o>t;! z-wUeGh_V&=LT|^%7A`!`$kFl+HgDM+8!IDj0y8H8nRi)F4N}TQHEZmvL8`Gpszsx^ zT6T-N)x88#zM48KfVB75q~P@K9F<9Gjw(oNRgguuw)G34rtYKeYkC~*zU>5BqAu;k zIkgGW)S=x@CJBO&$nb1{wiX-PJF!E(N)kZjjfJ;Tn!8pYGU}V7RE`n zF`rx)v&sG#r#8Sat&ylPHZxkvxpI44pw?PV4#sRkkm{ zi4D>1T!Xj)U=HXK@ z51WC*>3IaluB7djaU>KZaA4yCEfUMzU_sTYRq4>7Gu~xt;AZEkmD6yl^)cr424bip z|BiiSETx0B71w9+XO zqrypwju45%lKaMnALaD%eS{y`!|9`YHL1kA$*~b6#)NVGVy4Miz51C)k8X4N)M2W6 zdtsr|v1{iR?n(zcx?gt=FPYE5r3(mjt3+A5YS>#<#oW9Cdh_Zylpcg@RdJ#|3$Us* zo8qM=vn(W3`<_32`t+|MC7soi=r|^ryRz221X~1Bs|+4|FYalG-at2<1>G$j`Ov#6 z16p;acbhIm9oWy%UfrbwtV-p|6W;wyh~~1T~V$ol^fUIm723E9P%rS?)y(5b@$e}KS1iLxaL#GcFE&q zXpp*>e_8gkwEd)r6L>k>Q_-_1gF3fkP^mI}F0XZzyjJc!P?0B8tyUE~TQeLTY_PYr zp-QDP8lcoVsb<=6p9xYbqE8JU`c^N)d*XaFz$i$y6F1cEH%PrZ?5DugwM~E~1*qR3 z^#)LNG*|XVNHr7%3Z(q#(8iDUZ5vRcxRd-Qii?YOrcLW0{QaupU#kNC0;&MtN?Hc= z_H6^PG}94w@{nmx>Rmwn$yej=wEe>&W!h0cV3mwhTPgF}l=m;^*JljNI*NP5T+CJN zr?}bDF2p>&Fg*RLfa=N`4OFVshA~b+rRJpGxTiOn)C)oLZvphq-~UV|NGZvFoVciX z0cYZ3Q(damMOu9}T9|!VN0U)x z__8i~P4zH*6M%7C1B{aeUeoIfumaG{6)lkc1p>8Y0=wm{#C5g9u)aHn?c&gO_Z6o# z2>r2<;L6!Ar42o(B~nd1eVnM{`#E)Z7m;#2Sp(Tg&Yg{yPWCdq ze8Qa@mq-<$emQ&)&JOm3gv=!V#D04D)ni)UK1_JOFU`yNOCY7QsEC92hqzRo zf~EKOSX7vSN2w{ynzjFTb5Z{mQflGyogbKAtvM^K%dp%~geB%hm?rLNh~Ap_b#`>J zccFuKMOxPjq(y)qr*<8nW*Ki>>}?o7<|{&XZ6G7&B=<9O$lfI|Uwe$?74rI4>?djY zJ`z^!BWc@lQg)vrSR9w)lej7l_42ty&Yz3s%GCsN^8{Si1yX8(Z=r5o z&k*OE$;~{0RbD!GZe?kDvtJ( z{M7K@E1}d+&t8xxA7rny`*Dbwhk2SQjgmp!j5hSAqW%=9L~0$Cmdo$mpb2eA@83Y` zojIrf8>CG4Q-K=Fv8mz31zPb$L((D)DT}n+gSxY7ZP!Y47uSn(5>VaTTDXo%+*H9% z^aZ;_d(dCojeXue%HG+J)2S&KKNRS+Eska9(pdMcf?3bfab0gu#&~8srjnQSxNl*{70^yPvNwjzjFRG=_HM;S~PsPS4)UnT7jFSaz7?b;n5G6V8RO7%s=f zb20f0*DfTAvrN`plu93UOF`R!QlaxW(nabgGYkq5YOC55Ly zT;rxVv#S>pxR@C$ZYqZ>*@*(V4DzmJ@Zj!M?iO6+R$eyu?p&q3R|y>L%<0yoJ_EeV z(bK_^%p-?+{o*+}7tUd3G~nWFg^PnNu8xl4OsWYKe6`|%3R22Rsqk|JDV12ZYug}! zR2@3Cu1%NLzHimFl|V?GPsf(NbZk|RPRaoZpgI)-N&!ph4WzoZt@~E*LdtZmfKWiy zUfK?FtYHHmT%Bz2ty2TflCHFE9f)6@iqx%MRtrII5a2`eCbhN97;dgs+`oV8ebgyS7nFN~^!Q4a2RSrhI<|`_SLoFW}mP z@#YR(@^?|<+GZT~juICYfWD!0@V-TH>`)W?P6DECrO~}Fpz2iyW8d=VyOu!TLvHsh zhh9Kt9N>d-khm(5?qh+@Akn~D($+>dxDNVHOb$yovMRa}l`#9H3dT<=V;JFsene$C zCdWs~x#I*@QyQS39gJaND-26IV_fwfX6rtn%(h{C96FcxzW4w$y&h**2Wke?#lpsg zBCeiT74gMbp#v6GyPz*xM>=g63}%*?TiavpR+_<+#xrW}cqT8K%BjRqJ{sPKDn8{& zh&{#X`7_zQd@;8!WRR1oGPTE&k`hl^N|Jyno}_q{5k8g#<(^_pfQpYiLri#tfa)}7 z!b3GkorzHMQin89MTH#}2PFzSBsxg6z$!+d6>(%2p$E1RA90xY@S~cmiVHs?u1h(t zLmJqwXQy#KHHPO;?sD@|raZstbZgz1(nVZ2vVJ9VhYh7gvC{Of(}?<>)vz)a!%Uo% zdGYQzRvwRig_&5DpMkqLsgRJ+KZKwEDWudP>5zYjO=Fa;8*}Z8GtJ=06mg=%4EExr zY~Z{=vwd9IEY^l24LZ%;3JmMX!6ob%e8&GzkE zu(q(kS^n$9jX(nK5-lPt}K{W{y$I`e#4FOINC5t#{3wql&t0~Z`Mf>K; zQHg_TUYE`-1Wuy%&FaujsYPAdw^RWZ3Qm3+tdx}ddKXZ?gOqYx>U;rQM{#8x1XAtf zSe=^Q@;uF{QMC${DwNW)VAQN$2EW=Bv^goIHsbK=i=(P1?&`&hC;tnnzw7n}QfdK} z3VcyRerm4j@rxH+1SLUjFUG5cJ97qEgjD<0U3giK9|Oe}*fYFw5Jft(`Kz zxf}ERz3A@>sP65@s6y4nS?LP4>wX31`4!GPKx%RVDM|6-phR+YpBNKI z!kJj&&%|g-jEWUFMQM;a6BZ^8>VyWVxF{2-l#@DnWH%AV_KNn1b`yDg57BC0Aa(M< zR$@;bBq8FMrnrcsZ>1o0O3qO(DkV0INB3`VB_p1Z?{}q=izA*6RvcKhjFv@9)5WU- zO}%|^w)VhiD2GLf9@teJCx8;?%I?28K^WuDCxg)OXTv`Na@yEGv z_B40JF+IM2nT+^TTFCpw%oxt4hLe#RM(&kV0n{ZP|Cq}U53XyDD!5@C+?_4?uuTiP z%lB#hm!HD3CtCE|q{$Pt&fneH8ar!qJe-{fZe*H=Qjlues1|{Km1z=Ki)M}dwFT!2 zQmujoQmqt>Y6^7xH88brTAOxn)T}mbo7d4GrPQ&t2~ck#)#lG3^=59Xo2Y}pslC9e zojiUuA5Wa@Eb%N+44=wnHRq%pRNWfh_}8nf4f&}#-8wbP)2n9}>C>M5FQopio8ps} z%}PP)wQ1oHt4fy!LDsr=mnrF~fB zAHeL^L2*>#s`Bq{L3m>julrlts#b)EpVOEnqu!&$7rdeWT~Q*tyT$J zM<yQj$O_p%7AW;-Umn0;+fcm4a15REz*6N&{4E*hzuV zApw?vDl$|cbxea)_|ZLt9~Mv@-lZw>&@Q4x3RI`n(BA1|B!-)$;1nZnN-19UBaZIk zw1DdRxg>HgB=cjzWipQ(pm{lOT=Y5ybnnc*>E9FV>Po|s#qn|~j=9boi{d@8sWcYz za^GR$JsAi0uQ?TV{cUvLUv299uD~Kr)-zlj(o})e6uldtO4~yq^`VzsiC3sZ zDLV(g9N3Sd;!;aHx-j|6;be&;%~*Gm)r}{zJ9sWT+K8G@XN~_DHU>`QV7rBEX)>L4 zfsIF6q3$>2&B$aiRjX^eFPLrID2+U`h<*L4nFX ze~!$waIRfU=HBfbetdXM1JtGLWPGcaqlCKy{oA#oT~SY--nt@h$}{=Rzv6=rdSGQ? zhP#V$QC5`mDoLY2Rp79;R@_iQDoB8!Amv}jNA?9u9h%TOxVFHj4lNt`&?dMBErY7k zs$n%+HLfnVYtp7^Ee%p_#c6eHtstcwl?hVHdHp*`b?;FBZHgoXb3>KyQD(HRhYO+e1nnj{fFWtOZndw+~^KzfT+b(}~N{asVpHzVXIx9B=Q(tzbVMH}~Ltc{6qw)?y#M z0LReT*d3UR?e4MI?iwTQcxfkMxBELBj?BdI)O;KgmSdN`2K%hF*k!K9?%ZmeGFRbp zcDW{7dEQnLQ?WVn4Gx<>!fr-eEJxJCFt8@N-sLgtQ3OMuV(5l>qx+%?R$q$?>*$T% z(SUcE@{}s>g;#NRt(1qAb15831!Cn<4>O0d=q(-4S?e(?VNbo!dsB1D2ZV&J;e(+) zDCJXzV+qGtw0tV3LiduHk-)WUIb^1%k)D=HT1v7&D$yi0+?Noi4gF=v-=iZ>3y8u= zjE*AZOeAq9j}seuRCGk3w3jF~A9YwjbZ{4E_KSnswS|O(yNTVulf)zYNI1Tq#L$Bz zpE|6yO0l5_h(5JnQ~U{a&OVamyfgcEa8E_RiJm{YOUSf|c-mNEZ*FAo@(@~9Do2wN zMXBRb7CZBrSQhV#b)_+wml=y=nF-k2_v2h<{@dZczuMIIT8$XImYus7E-~1nIOFsV zOxL;cmClL&=FW5}QG)ihsuNJ5B11a0qk?-8%DTC8Y{PmAVl&w)&S`bM>8$jf$mZrV zSsOT+ZB1vh%5Ng8{l1gq6IfPj49lyJW@XjUgp~b^W#v9+ZLM!v(`X#a+mGkK=gZkQ zVj%~=Ucj*li#a`O9dYaSarV?9&WjULr8#crW{OkF62}xpYWxW`?`ECPW~Rv#q%**nEQD-|JIEx?%%(n)tX9<3&*>J2erz3)6Bmf zGsk|*YjIJpUOeaNPe0@1d||0x3GFaB^*q$}~UK zxvdIg|C7K8ZEi}A5fcs`c;7Z4U3G$0;zde zsA7fUp!7K=N2N;9>n|+VlG5rE{smHM-s%rv`cEMxjwwzQUzkHMRsmA}t&Ei#q*Ur8 zRm{;mMciDPC~LVk^ryRQny1p;+Jar)ZmjafJ>5Hiv0$$WPReDy0hP!g@KVy>*op4C zfbGsc%x`SR>c$RDw)s1-zPTMcab))S$2HmAI)+v5Ap`Wx+8FHx6RBxnEqI z94in9C-AepC+FPRkBtD)BX=jorQL zK+4KMQRzgT9BlEgTY`oG#j$sD#=clJY}{*MVP6h|wX?_!VGoo7a-K zbt7kYY$bK)R^s<<7l7>~Mx0YjsOaQwfz)mi1XRi4he(t*{@6Yq-pJwcy*zDk><7)8 zU@LB_Lr{GNcWOZ`M>}e}RK(iCSDe%UY$^(*yhdYN>`O|@?OQj0{QVdC-)stDs;ko0 zbUyxx2?0TTYvs-)y}Jgfeg=CwigW81)PNpMn=-6ZTS`f1Sk=>$m>qk`S{ufks^74r z#sq0UXI{(^%hsEIU4#!O0n0NZ!AXba9!FZ{_eX?>u*}XK5ulaxW#3E^aC( zGn%BRLtM^0BQ7eMJJ&O%J;TF-91Uh!;=rz5O4R1Eu3Spy&Yepv?}$SwhpV%rICeK2?DXP9iV)nm##=}=396x`Lu%F3 zpJJ|d;;8Bfocw4m$D4_JYTBTp2B}s-;-G?R(z;0vliotAhUTU^wEiua;@u#MH;{S* zrg!yckSbo(gZlMrQ@y$uetwmyTekv%fz@dwj<0LimfAw!9J%m${px=q_0QVWkM~vK z54mt5)l<1TO?&x-{^|R03Hx54GY5kLPliC`++y_E0;d%JzRP>+*rQbnHMf4RqDFrI!q~g@vl-y1g2%TN04f*L)RIa~y7}Z*;L^)qY z|7EU_=dm8$B~uyt#y=%f|3LyyxdMzEg@AgaH=w$)%cQ)W=<`&uMb*9u#1Kbz%T&|K za7z_o+@rxx1K!PDn)IsFz1%k3-iu0ptD*~)lhZ=uZz@-}Mee%|3jv-s9GSaC;3hB> z5ZYhaipQmOxF^lT;phl#L%O4DQW?Fo1=dcER4(OCyMSWy1NX$Wm@`(cWwCLqiM3rh zER2p?M3zmxvUpADO|MPk7_(?JRoc{G%KB-X2;ISji%DF$nnmWhR8rFtIh&dyPAWx% zRH_10Qaou13Q{qoii66Gij;p&L@6_oV#ur$=lbGvUej%2R4&@a4Shi zc9I;b3Q-;+_QW32qEGOHbf!0R(%HIbE-u!VlyY`r@#OJTadM%yM-8m3#YGB~ti8U% z+H*7(4g;xNp)Wr_{?E!VsD&5mKkaP!q9?82L|>yl-&z*sQ=J2!I25C^jXhtrX~U4t zo%y(3D~efJ6Hvv6v>k_8(QX`zD}O6M8pxN$yYpF*j(q0ciXrxm_|&BtUlwh{Fz3d6 zA7IYlUx&dRD*I_90+m2wvz&V7@ zTTk4!Z4{)$aw$HXf}B(?q(*X4prn$>o=c73!L18qC5LOp5HF-fY4caNRX#s)Ue^Uo z`C3+rEbioA)XJh1HeM z2`xwEtCtE=uT1)fZ2#jSrBzuj95s0}{P*JZE3Sz?K6e<4Q&TWYR*({Rlc~}bDNwD@ zAZ2!SjaEoefyNN~4y68KPD(ACGsHKfOuGj9pl&A-{YTK(WWvzLB^by&4A8+MODV zG>L;UsF3!;)^J+@c6*0_Xh-3?tet-%^*0p?zeQlRS?*_&QEr=wvojNDndfc5{JN@$ zya6+Lp2kaS1*`(u^l3O}FQjbP0?LPcj$M0S94k4|yg@Pgeh|QWy?SEdToH4ps#x2X z6-YUtH|p`|P><4MyEEm`65PtW(tCV=#;+d3`3uopx|&XI-X+pAQUp@TTBy0^oXpRKoy+<8i;4Jl3@5()lEkS~i5)+Y*wJH1 z8Z(ZRQKN|*_z7{JeMQpbDV!Bpoft8K=&!yedE$5yzZ*}=j45O+n#0*;^GR8;kd(Dc z#c6FIX`i^Qlgd%;C;ZSh9^K0oM|FwR&?7YVt)+$GPZ&9ZmbLt-QM4Wwc1>|AJs9hf zUtm>q6qdGq81~Uz?W*dp2l`CgD_#hoo@*@Q3!eZ0AOJ~3K~!r5pGgPJlb;w`u^Jy6 zoETx|%5ZBB-nS5FH3?=|*RJ$!)>wm7L!WA#nzxZ9HNRk{`(Q@cx95}M?HT0Of}T#k z4EAcwfa3M(>rsz>CHxuT--@YS`tfD6PK@o?lSKnY^09YQ*7cjfq&5TCI${zFI}c=G zx8bbqGlm(BhA_S17aSh5io?_AleBdciQ-ms1uiN~{o=V;EuWtXHBXHfN0kx7k9RL? z>!Q?RUKI)W@a`o6RHjzCLrJZXx_v8~m%lvZ>o12=+owFW%9qElN)@^C{247b+OMjP z*b7n;lW}%%)Y2gpad#2-ZlqRCPl5zKjf1MuNF0=cR8V~%+BC1P&1==ER-Tr@ep=LD zuyQc<%G21dB2DF-=AxzzD$_DZ%}rGk$5mYbRa27|Jkd%)DWDKk{~l86xR%924G)Ui zwDi;JNR=vCRC7`d>eta6RNcDes8ORd4H{IXxmvr`p}9b|lBqM(0HrRt|C4XX-(&k9 z4Jierx2E{DX9qeTiKhj&Ob!?j02407re9fV*lfD0my!n3icIB_BD7Zso}sqg)+GVz|2>!yV2c`cUmo|&Crf<^XHEV342DX+;UX$Edl-{ZA*C_cji=`^e>?|=L$=Jw^$ zS*o(|r7&7MqO&)rX#Zw74r|J?qwA#ew&K&7pAfQV4!7=SbMbN}*REb7EhRmEv8xKcVe=aH?GoV!HGtu4_Nydk!FFz)(^K4JBdF5K@PKO6J#JlQm%+ z>65=BW!Cqkge)X+^)hi+n>l-8FA1UhIhPd8FZc5~v~Cpx-|vp4(Sjj;`qEz>-?vP2 ztQ^`>wEPI{N_>XVVJH?>odBpnSHHwv+}FOi|H*aoOM%o2fs~rNQaLPN%Xd0#?+s;SU?)DU+nU*(2J=mWuFUBAF{4|4z`7x0nANrq z3%U(qeurUf?KzFfHHQ<@b~Hyu&*$KjnWXRA%Y%!V#EVmk4BxAjjxP{5^=84ZR;l;S zrMC%ZRmqOqD&g$yH;__uQP+9&V*!;adh_vzJ;ebzGUL06^27f1Eu>xwq@p4sv9YwG zOzD!iySj*@vY~!Jc>?QKrjfX#;Kns+(X=l9b;{GVLog+aI8xTrO$$X24yr8eO*O5j_s0@bUR!cQRCxN$9-HS-t11{S76`d>)><2LQ1RR7aHS5vOP z7xJb4<<+k|l@I!+XmG|M+@n-pJ%NhIn6%JT?Kfi?X4$LxKkS`#d==N)ws+i#69p0o zB*fiA5+t}g6fYEt7HQE!ad!_1?(SOLf`pKSKoTT4g;F_f(ZcmTYj$XRdd_R#_x|&p z)5Y)p&FtB;XYUL%>zaG5XFbd8rkds>U7Sv7Us5To)O=J*hUfntPRa@CqXFf3ax4{U zuDi5EJ}AeDZ>YI#o}bwRl&bxZ{)rP=!Ezr5uFskNBi-HB_# zP6qHJ@y||U@x%U7K<(7SsZ<7kSXZ!8uMHomzNH=dz9p@HX}5r8r+`Y9Pxt<|)IF2+ zX9`UFFnYdUzZ4li+b@#sPj}0{3TOhiC)-M0oN6W%$5pak>jN{mvsP=K)D_C_7Qh?i z%lW#hGN(_#>dZLG9v+J4vQMbgwJsK39+=y^U{l5oa}82vxCx}Z1~(>s`5el)+puKA zO!A7(a`(|)9z48D;njSuXXlZBHIM5zi@98ImArynaZu;UJ9?Cyt-Huwy^;KROSv#) z4ClHI)^AI8i;i4w(w+kO{8fS0rM~^h9y*My(Id$cSY<7q%bAr+IJavf zXEv|r{j+aaKW{#3XUxUI(3F;S8!(~YD8jttZ!go7G9I0=cI}LVdlyn`4dUlF`fF4{ z>W}(oRnprJjeYMwjHPZ0;)cSwsbeJpskfH0~<7;Wr94pTUt=n#hu}` zI`WxSeFm9Ur-xZJx;UpYI;|7q+jnDRtB)Ajq8(qhYR|N;JsH=oGm|@XWBK6W% z+$+BPTgG!We(FQ!^A8gse#nBZ^7Z{O6Z-QfxB2P&hpbz(lyYtkv}jVFitcVay?a+G zZv`p!6sjL1nZ9r-X2zy?mG{8j)&@@xTVi5;h!aRD2bEScQtOwhEtP-0Ith4}cf!M^ z3^i3Yiue#x;{vE7j;elgxTXflVbn_&DAfq1ZuKC6RG0ut1y1PIpmumEtW@xX9#9HK z`V>jZJt;Uv(@421*ahgWHn-+u)uEmvRGJaI>hF}%1;jwg;NJ3|1q41;T{ zF;V?Ss!}hI%KWl`Of>@vpnijteAXy`Ifeo#UC~BN@9)7#KvnsNGdSN9z^EoXfrZgy zrEOY2l73w;gC`15+x|{!e`9e@#^RKWWNDM{@lrx+x=W8tH$4B6? z<8v%})iI<8Ed$Aylcb1Jqimm^zqqDnnZd)8vVT^vRA zdT_4AM||DvBThDI&R%&+&FR>I%b)b%QvKFsHSNghHXn1YW?N2GO681C3MVQik)I%- zs`nB3qN}xAliR#A`Q7_+=F{Gs9yE~b@ng9%Z3{zv$s|QbG zpwrQ`dM(EH8AT<>WXjmL#lpT5PUSjdY?a8m&4+pUUV-Po2PyGR+P~l5ls(_e^?dYe znpiv0%g%$ICN^|7vZh~bJlz`Bp{zKma;D<2g3{<|S(^b?seIwygnr(QncS=wU$*PW z7p+<|x>E;MkNb*cUyfkaxG`*-J%gW-%m^>F$#q!gf@Juc>3 z(WAT9G)TSv`KgwE%G1q}kJ>b&as>}@Wyg5`>ZRz_Z;*QX>Mesm8>~U9ijO#SGZV^{ zvnMgBDoNFXwEn2twNz>(aVXWJ1X^KQs-!ZO##+{iq^Lk@CWH#4B1nx1Bq_{~da_qsB?qP7M|?MGBz8XM8 z>--vC$|CRAH+0I|jqQ;s;;2;Mf>fxN1V}1D?X|^PIJiaea?A^sVx&N&jeDA|$xxv3 z8=%fB&Coa5ol&4tNp96Z;guNXiet(aAYIAC_~r(T?`;zJNF{zvfoq-~QsSNr6rfaA zitL4$TwjG@kyQ9an=!d7kb1ZqvnP8nyDu&v4-pO zGCA_`5KgxLjEmK~alyM07t7Y+jB^^7Y-^EaTb&cG)wr74oU0AQIn`~(<%UhT)Tkv{ zX-(K(K8Qor>vO(K4=&Vi$04UMj@SmX&(e=`KFNGtDS>0o z)2lYe){yUKRXXRJz;p;VP#!e)`z>LmTWwkk_I+ypMN8Q>)c~!*2 z(42Z+(R6pO&!;Y_4D@Qq=!DKpY1fOngZi^{+)$2h%;eOTm0a4tnG1V2P;hE5g=Y_t ze{vUhv&F3*k?p(JaqH9(j;vh4(h+0mpW29_sqOe8v<=gJK4zv@FGjg{WM!iX?C8IM zWAoN?=k$4=-MOv>L+D+TUf-1J>BAd(NIfc+b>*gR3aoBxb9#*#J{V6|dy?X#>D{9{ zZ(hIDGQn%3_f=q&7991SgxCbEEG)%Id17U5L8XeW0;sCgs1dF?DAm+gBSqG$g%ckm zZl!h-m1Q4y7i-)cEb%GlM0h|&!hF5(E$>2NL?E>VWOY-b#7)IezoxjN+Oaf9ixy`U zp@ps2ucd+~1VHsvvtG2`Whuv0H(Vg4fK}?G8mYm&(E`d)K5AE+s#QF3cCr_j7f5up z04g$ohzM`ZO~uFiX^?VtwPL}%xx9P(2SEJ=sej?3{%_@}>%IJk>r*J{uW=Rim;3AA z=y7c?CgP~fvsM3*xGAmKZ=txIWtbLaVlA?~wnRXsprYK4IG?QFA!U4CX$EF0q3r3& z+A&PdFT$!|9cBgVG0ag=QiF6ii_2J#?uNX+x>WX4;1ivjBajkMU7U?cUOL8wYcxn1 z-QJE#$sUaF?!xrmcFe`CnBG^-Ypc-R7Ke5#L%;m9?x^r}QOO^4cf~)dbU%YT0+SLo zHftqD_vClpUnA~qm4HU=Bfs^QEN`jr6yU06J#m6+Y?iUey0jyXqg!YYKbmSLQ>2;s105C>hNI2l}vs|`DGx$P%h={=AOUw+P6 zaU4%BUZN!HIt%+w#!6>SbkzU`b?A$qM;ewEO|f%qiIHh2*}3=iW201Pv3i(752ycW zc`KiMDUf>m`V|ug^e0wl%17p9sbgS8&-i4~8L4AL07BJzPJ(hZ`66aQ5J8a!>Ez_LYN_h;E$U&G#iaJSxuS zLD3a%Wu0Tim?<}!6>Kp%#R$xnDA~a?>>$@`%m$_xKQt;9;vybK7dL; zxuL$P?`ElP@$I8K{Pg@0m(Lx?!P=EGc7U zL*+`|8l=3`7^x&b4N|qLtHiQlT7ZN~iKG(D)~*$=Wk;{z>41~H1rD}ml$E8slTGQ; zmU1q3_>?b8Sk;OoircDNJ%)y93EJFHEgPlKqG7U@Jhox&7%kdQ1yX#70Q}JW*R)~0 zCN-G1(?@lgGU;<-BK=Fl&x47K^!*J|aq%j$FOd2T;&5`ZV(aFO`XI1BKDqphl)oVL zr=n-ndp)2;3Q*dhNLl|`DvA8JFX@uE2fIU4#1YNZPmZVt{^l>jRKJr4{d^#*O`WX0OBKsz9fBla?hySF{FQ-ZFj0 z2^Dg##!sD}i=nI=ii|Lqkbo*sgV$mAC=PhVayqsiS?$_!=Cc9Z+k2P?$FI^kpq@a= zl(@iPdbRF>i)Abh_RTP}jHGM#;r#MK0Sf&S^8W**)aPFP`YW&Bz2%pme`Mpd=`?cm zq>hCnEuFmSn_QR5HjY>s88c_>ICd_cMX!1_nAEo`OC}Fs)x42x&78uKZHve|y_KR1 zDs#jZp58pk-P}W5P@p=onS9w+ba@~7qQaunJb0YL{rfrGE6S$ez+q-J?#YPY7EJK& z!i>tj$fz}(6`jU%X!Y`N#d|#NywysU}S+?0Y;C2de|YHBD=npDHq)`D|qPHD68{?**~Z&Lo;e4<`!(|m~31d1wq zdav9)xExnac9XTxa}yPjmopdR>*-h)Do~|ko~Qbn6r`r32K^dcP!lbxd84Lbac&xp z0vh`hGg4aBZ^|0AW%r4&z#;))!@=D>jPtq4IOQ$C>CPG~@2|t~o`RLQuA2%>O3O7F-z;@( zMFQjOiI{Bfh4FZ)$QuTt^Oq;rHnH>^(iuB>ie0~BDK{S%Q1bWz*X4;l|NLRDY*|Cr zjF|$cF`VzzpL4a^kzcPXXJVRg+_yT%Tw^)slEi7pM2^dHw?`6toMKpK<;#JfL{?R( z!q>4W>FuHYvwCGcUGSR$-%MWwu#X zWwVhFTLoNutphpi5XK?LP!2c*a#TQdwsI1CD+Y0?Ll^FB*h0?MgCyGdW3O`{CLoYz z)mvd_l!(23B2KPV`S$yt^%vhC^}kTwsViGx_5C+bsP5-Synrgr$&1Dz(b$@pNyQq@ zkKf)SecCW43~b96J?k=ed`}KdC!A9^EhIen}xee)ouSM)j~XH^$k~p8VV# z)YS05outz9rDo!N_2LDM8Z^et*bH|UH*J4MTWc-vof^nnt9lqUl0&uW;!_fXwdlU2 zIDewUDiayzqvgq~7NFkKBg9Gl4PE;;mfq=>tNr_keQ8j4VAVolxO3joAQUyqUm7JL{W{{Qw z$*+n#DFV*$P;UWdRjt`CP9R#ndNhq2rD0`h!sCbHw8bH7K_Y)a>ff(ua6%uVuKw>| zym`y@w=d{%W1l#xiCARM)%u-O4nKpd;yenL3aFN0r6T|G1ym|yMJcF0Oh<2fb|$s{ zcAO}I%#&TuT?=h%36f2%{_WFuJT}{#E^HleHOu4K7Rh$p%c88& z*dFPF<<^fdUeN@@*)`Bj5Wq|fLpLcvR28F{VVEpT##Cg!ARgm|@fa?thT&4tl2nWr z)zW0LC{0uc(?vBgT_Av(8;9Y-B!QKHYkIUmuDYz(#4xiNrrWw>eQXr=g{vuZZ<98$ zqwZz~2I59dZ)E7lQ;DN9E}VmD_INDz_rs`P6uM}0jALvWwP-kH94(nP=SyxqDImWn zpL@l*0w}3KXD#OZfH53z-=E_t9XS`kWSENcG8P$lu~I?HCWOsa;?Bf3=C=AxDFSNZRhl!W!${5oxEe~*poSxK5c8#HZ6?isX?@>7e$Sz3WWKTA;Q;= zKrcID16)WB^B_6YgT^U=eAcB2gFkN0>@fq_nlX!#ywlvvJx5mhW@gms&6K2W%&Fg> zO#^0fI%7SL@-Fht-2%S7TPTn!?k%3ldebJg1yF{VnHqBN z;6C1|O8So{sPufj6NmBS@e^@Co|v1M6BZnbodBw=gN+878mVEVX^>JbN_s`mJ3UwK_g-V-;I6he)}P;p5iG^m|ST53EAG2!yrP!00oLH_tx zu7sC|o2K%vZg`Y+6~`q&b0~|0wK%adPV(AOKxRYb3eK{>8-c#=@?BNM?Ku!1>qlg` zxH0+Oh){uLY!ERq{?w|OK<%^?e5+L8^{bz?j2gdXoA@i^#J^Wjl|yN3fj+q6&3oR+ zvRKsR%67^gQB8m1NR(?*6DZ~`!1VeOECd!7h08Hjpi)zSsHQ)0Tn6HbY;%?p{BSoF z^Hx*q-eHpN9l|Pmxz-P5bZ3J&stkQXSv9sw%S@q~`{ecIbaZ(Fvl{}aqODS~t7g7E z7~S56fy#`2dzCn%W#Z}tPU6BeI9;A9aGZp}xrrE^n}ntOR@+k(u{-cN7OOuJMMergfNVzhGH@|5=()R$&wlvWz@$!vn7_9?J!%>PTW;{3|7ffsEUt8kUPa;puP?w{++FztLosURjjpneE>i@oE|xTJU7M3x2PwQ& z!0q#=xHx-0S^Y+lgyx4=J;1^L_w;ECy#FO&4ZgfR1WI-69Lo%h79bBxxh&UK;-6Lk>CG{-`hd{ z{-mg;`^;s_urM{p#>$2Ozp9k6w#3cZj<}dWft2cx3Kd8NYl&r5wuyvjKT;Bc1v*tV zXB8FUBd)5Frr5~J+WRU~dZM_uemn6103ZNKL_t&=)hrmVa_L0{Xw8BNu`0Ean&CH& zM)hh?zjmrXFp=bhXksD)@UJ4y%R_<6341GhSvuovYff2vGb(u43gmq8^0X88R8fvq zm6#|WS@)9PP=y%zuGp9W>eovlxmqkWswWAP0?xwXU^mtB20_yp{|8 z?c4YK{Q510Z(q_gXBT!yr=e4sDJ}`DE~{p=1>&eOuqaxAxoUR1ruvo^33No~=3$VN zE*0@A%(7*l%iTfFs;-F0L z?G-@n(whE^Zmq#ozRxgMDtG~uiGa=Y{47k)P8UtV;@o7c&rHBVX)9*%05oE_K^C zz|-B99oyG%v#5Y88#j_Y;wvt;@5!mO)*Onh$ALFB zw8kreh0gvgaQ0?tRX?`Zs>Zr%(X2_1Vx3ep>GJeD!^)1Sj^&u-;>A<}*n%nnOm%W& zw!I4r%eu43*-g^|c`{z?WdNSxG4n_!lD zN3gH+=hU~4qLPIxQB?y7trCf286WoUIxP;W^x^0~BozOwKK>sT)xY&CujSt_ywE`P zuovy9stiuXCb(Kz5Kz&Rif#_FZcJG-6LC&KOd39r9cvbGc*hEk?9AZk?hH0B zpTLHUv20#8p7o2rV*85e>{vO2nWOtMxO*$2tGM88Z%kl$TiP~AWci}0+`e*=gG*O2 zz4ZXbs`N@Drf~Vd9-cqE$@4p^xvz*v4^+m9J3N1SpT`gH(ydDetSn5Z>RW|^Yx(GJ z&=2~@lk&^YKNC>3s<O2Uv*`1RinGRmJ@Tk{BN(>;9rZO=>C-rL-DRT5v?u21ztX zix(wPr$z!b1!^^_MN%szhJY#_;-);btQT=nRfrTYhJ|=(po*7cR!a!cGEIbq1<EpR$E7Q6jJ zv0Tv+-N;xBdk7G^RYBJ~5QBl@W(EnIM#P~TE8v=(CV;Ak+0qu+uKgJMO`qYkV+4Lj zCK7vm91V|7ro-2>>2+cupPg9Bpu-F3ad-+%_6(u%uEDfAG=h5D`cr+!5JI;PBVgAs z+&A^denTHDHuS=H-NzWO>VWC8mY6JTfblH(zDd@DL2X5O59)!fI7loG(nl zI#&fx%)?BLCp$kEF1T#zg(6)y+TF$A4e``2Iiaqc*63c9omr1uyxDC*R+v=;k%9UcA6ZHS6IoFfLS@+SD z6s-qTd~_A9DKK7~Ph6}ovGO`0uBt5kWSc)p0yW5NVT(E_KK-=rpKOc0nU z(4{8oqYFPYFRG@-h%j%$1yt(yR8NVdQG*&(^!8xt)QP{lr@tWe?^*P>aw(+Vt6N$R zC~ZEfcWUtOdkV#Be|k+ob#M{}0tVgjNdlL-+88R8SI@Y369$Fr1t#ldeIqtEHeq&F zfO2{P#@E+k^kA#zqAV56v^;&vIjM{kv$Q@b!z=+>p6pY!Lmbl%bhq|laA%*msNGt0 zpHblk%&)J|JFtra$+HtNIW-Q)v*R&b`Vj`7MWO2%fUZYn%=(34+&3Jf0TCDsi4Z`= zh=Zz!;k1@mFYJQHhW-Q{9!tpf5!5|2jn@07GwehLbIz`2#@_i1PanbXCBqmoe+bP# ztw-+(ooU*?CM^foro*sC^q=0D(U~KeynQiW?N6uP+|Q`JVmQ&OhvL6$6lFK|!zQyc zCd=BQTha{OoQCM8)WYzqBn*bi@9FE0N$*e$I{Q=hzzD3wQCjB#D4B3Sp^Z zAhWG137i6$W>|rV=5DNuiREC^I?R)&=LJ#;%t;DkUX5^;)lXzaqiW1f3}8jwI1Y4f z!mI>;=2j13Wz%HVw64MSu1(n0y#+hFH)lt;W^C+GpA{`@h^n)xZC#dz`7%?$Hrvge zS>m#$TDq~qBbC|CiG1Wxl}a`aRB?30+1`x#MRsi7b9^|2FAD;>hLr(!da)o z%hZ@?PfJ=Sm8Va$2)^u5kIWf;STUzRJ64QgYsMJXE*Q?1^zrg~GV2zMX7R*9O#b3y zdbX`a|IQ6qIB76F81=g#oMy%G&h zDnMe#woR0Evd61@IW3y)?b|mRob=NFfgi>?zL%TzXK_zz-mTa2Ala$IN0=BHVP_-m zsX_&DVqI~twIw*nhxmj*k`hC-rnr=ZFjC@GlV6a4CqQ5#5?2%+SC#AsIJ*eL;syTPb^V2F7P*V5oBIseYw`40MG8qMPf* zF>S%Hcs&k$uR+Q{0ZSc2f$*A|(qj%L*)y>|Gakzw1JF&Xk8VITx?aH;^bW?bcMwK>1y22A zF&S1Jqfu#CPHlnzx}k)x|B~7VX3^}(eA*pd!1#;n8NPWMlebLg^ND?!HvMzfu9?Y% ziTzl#Xbkh`k7W4JP7E9R5d-=(Vd7Wane^2s4EUrG3#Sib{jxc%UB8%#ODEBwe=9;i zPNUkyE;L#*f);xw6R`R-td@U*;gXIR&uNY6ltvhRnIg_E64OrwR%#BacD}f*>VwO< zNf=+6fYGJ#7-fyc}m>mW}74=*+?|*){%CHVTCN z=8o-5=FHDnGUYSojQNZq-CEJQP8^L>f@xbfh6$f{XK3&C_?L6W*}|0Q06(UU8N-?V zN0`|6Gd53~$%D(;JiBw7gZp>UprN?TN?y#EHdS2JLtejnSsKOn!8Mis$?E=;^QZLo z?OP2}-+%u-o*wR6)^jsc6RP<5@WtRE#KlCBTrG+ufkd^WaB)%b0;q5eP)d4O=;5K< zlUjZNlv-D_<0`1AZ3;%Qkv9lNG{ zca6kg`QAVcNb0x>P+HTWYDN@S77^n+TPt#MbM%n@0H?no z^;c0vT@xS1392XFUthfwP`&4>)GTeTY{T~8WK30Vy{uXKNecxaMmJW7<5`KtosGo& znvL({!x*ZxMh~`VV~0#~<^9X)7^?hz7ger4H9kt8*UwBf=M`qg8m!h+Fd9)GtMP4dpVNz= z)uZ_Mw78_3Czz45pY^xSG4J>$zM31^7th%ZL=B)LH}HQJ^yZ|hw4 zTsuPV9W(J?)(?keJ+V&jhWUcF7)`Aw&Mpyy?!lOR5{YGpFmy={IL&U4+vRDPoc#*j zv0>Pp8H?$P?l^r`8>fJ>RErLl%Fcw{t20^Kz8CAmYjG~285tHKq>Fo+SH_2FX62Y^ z=fg~AUnV>HFxM}PnW5nhg_Eh9r|6&Wo`pm&2NbWBNLNSg+9 zuOG{h_O+NXqy^g-58}xBVPx-{$nA3r`0n~f?qp?hbloTxe%X?)^`%0sVoJ>@2hyU; zQ$IGCnqg8|`<0jNu7r6z5?RTW0bScLVc2K*dbr?VZcIv4Bs0g1W9__!%pEhD$-};& zN;wZa+?<&`YdWu9z2NN|6=wZTVEb0i)i3;Kka{CPRvI-*oVU3do^GyKSyfTVn6z}vM+^RzPSvFKRQ<*tyB%AlI;zIrr4t~9svCD>&GNw7c)0*PEpe5EBtua~B6y3yHm=8(CC%MjX`y3=R&#bk7&qZ0Lv6#8%AzdOcGnjG(f!16`UmCTGnWKKJq? z-7ALq0;vfWo=mgx(f~Ed)|=^Wek=)!VM%H-3!2noLAwSl?AeCtpR{6HpSGfQOz+>G zc|*IBKDIX*6Z)`xQeT#i>&4nRpR-Q1Xk1U$%^u3$%t@Tux`?b@%g8ynhWw-JxqfOh zx3hL}=fZAEvUYOkfcD;7-Xz~=N%Id^a@c_)`}>+&WZ z73|^pt;4e1P4L4$~x8Q*R>N#VIc%n zsz~jmYQzKwQ@NZwwNesUv2+pl@8059H8{7lY41NPzc=@(^k%2E1!~jBt0p@2enONU zULQcF+?0ZfKHATZgcyC_lmbr$~~bNSK%4)0mPg2_V}+qWZg$Mz-f%3e;M+{B54tJ%6`0Yj$sp#JbUQoc&2 z%Gea_XVk}NUUPI`%Cc_)h8+XYweS{I!k}7Nyf+TS^3ZT}J3hm3O;21FbfWX}kvw^R zmwsJ3;Av&S-sKBe-Mt5cjNBM+QiUn@-U25NraE{t&8-T{qpGncwI<73HzK`HYtjdI zX6cu`NuM&9WiyAfa?Y2moj->3se_sGRd1#Z@5a0_pOP_sh*ZquIkIIwM+Hph_pRW} zt_&_8TF2F|H*@8%%3-&S+ZXo(!f~qKRJ&6!m59_`&QF$DA>1uz;s8>C*(b6#W^);+>lczkMis57uvP=?%f-4PO2I7zYnPoZfen@ zMHm|yVQXV0fHK8KzRN`osFepAx3Xohx3LtJ!O_+V*Rtk#x>!@e-HuA-oba#gNkp&@ zVF5lQMu%up(25sGMThyy>mXvotLj6~qm}EaqBZ%&MCe1*)i|rzs47|@MN)zSo$Bif z(L&ml^HO2$sR?R&j}UQE{#rAkk^+)~R9H}XaZ%pdYYkMgElMC29afQAsZoRkS0Okg zh#!9ZeWvC=y6}IM@)xB3FN>-Nv=(ty87L=J`dY#27uoQ$+$gtyeZzp_BRCzLh+WQn zj80F+^vo22&@>Ek=3$sU4`YFrk(vr#C9BPvqvht)sl0wFaR1Bs=YoQxm7t6(6sIX%sRo0KB z^ZsV7iv(TO0V)-sQXf$#$WL6)sMXX=_x-v zE#aHn*SL4%s<@{!+$y@vt*d8wQgnrHOY(Ro`teCIKRhnt`^SYmQ%OQguJX4>0N?~+t$rv>xQ|UIIxb3r*^SUsqJ}r)#&O-$iym`sSrAufUnL?Y8I>cD}6K4`jl2HTNc=qPg z&@l{(pTQ?#qZytym4zQKWqJR_46f6I$z4Y>rsFUMwfKZisdf0gbqf}MIe>*zda-=Y zU=~m5$DXy5Ik;&8J63(p(QV_&Il7F(bDMaOt8(`p;%@Fy?#bVOH~$3BZeOG%?`x)x z>VvDbK*P`o8zX^&jT6!Sfeh`}pYtbA%KzaL-oBB)K|+3 z-&~Y9(_eo2nYfr(%2--qEpSp!%G}t9_N`jcQb1U{dJ>6IQFwcl!$W0=aJHeWqZQ8f z=Gd1p!O6}{w%f|m9QU$z0ylm3h>D(N39RBxKxHq2e7%T|4A7bg(^8^IO%5k6N#G^$ zNsJ8^cm)t2qniA}i3;}I>CJ_9-_N5$r`nxt4+ip0#RM=V!TZfB*Y`*S|{n3sV2fMFmty^`Pbdd;98F3f}$1 zz-tF^-98$Nug7D0bUY?1Z{Jyg)j6ff0;SURv(rQ~^y_L+@TGJN3&cI$-lBI-_qGb8 zw(4`<6=rCFvddpAfa-|USF)iVcplzX1nz7tSs5@sloVc| zAp2{M?p?>;Ez3B%E0be8XRv(9XN(%zoc04!@ar0kW1lo^CN;)rND2lW1XPU$P}R$! zt6Ltsg%6WdwO9ATx`Q4*F=|OOrGOQgsl=&aErJb8211nc# zP}R!JYT1aX-C8oae+QP#{DO^Zrm=0~9D(Ot4(-h3s5rlq`&M#l?+S5JLmBaD8@e`5 zp=*;Ass)szqOGB(O7_P1IO>S-HYdD-1>v4nR1b7#VB0k9*b7HWWj^jRHqRQxqS1Y+ z8y$#e8B^SBEf_qYA5R|1?|rE{|Mh^|}4-oos8nhP5r`GOzv!HjkXkmWi`iIc6MN=1gV#(#dRCHiA`) zh6t>tkach!xhHp0cxe}R@&wj}N96zHD7WPvC@DO|^9NbnE6SpKn^t7bUdT7MN_hW^ zIP;f3iCa=Y`jxlp;pn}J+*5;7Kj_0ap#K_D%9Scu@7lftYYR&p%h+LSX^BG_8ysw` z@u^Tjn+4gYoF{csQw1(+plu2@lH-Yq3?@t*N>J5`RIO4TA8$7eUY@QFxH^`>!^xhq zb^!2+ltc`dFg z!JlM-Q<5x`67-OY(?Zt;Rw|r5R)wxt(Ez2!M|}X5T8|2=M3Y9Tly$bJNB56;t)`6s z@b~{g|0?A#Nc}Gt^)#%)(zVdx7qWc&o(EF#kGXja*DWKlIXnS_BV#bmnx==72B^t; zDR60^nyzF|`T`(Kl6w7r(uwFX=lSQqu7!bqJZ?mO- zR!`b*c`J@$_569nR;oasMzz^HcM2Usefcacks1BF@zuc2EL|{`Rm-O_ML@D!VEO1) zHrFp4VdIi14D8r|dJz@zwlKlp!HDScw#0fn(IP2~NdrG&T)&T*Jh%r_hID7v@ScqB z*@n(4axKP}q{>e68ABr6EvX$?jvU6prL`S?^SBS#Hdw&IRmo$M$pvbD3)&dMZmc4K!Pxx6xNj3jwSP3m zrzQxXChC95P(`X(uG|dE0PN4ei2u1U!&l5K0mzvjyLar;pd;8awGSG zR3uy3vvDN{wyvQl=L}cR9p=dHH7uL=6>}#KX6fvaWX>POswLy~j$_wyE*#y;^-G7j zbNw8*3$wU;^AZp5UFF#~Mg07?d;IvrU8#tl^77TU{P5#LzWeSjCAV_8QzRAM#lz%Z zI>P1CdpUDtGb_}<&jq8HH|7&EXANND_^#B9uYz-g0~Q~}U_G!tMhz;WOK{a7W${HF z>{boIa%DeE7q^r5<5`)rORKQoz4=w&kM{C4Ip@zZZR`jl{VUNtHkgm9MbWQKL#B-E z&)#j~n2v7e>ZM~G+_jFO0-lz&s}bN)MjVqHEm8s*@o`g@j_c3t;XOF9bqN>tuH?+l z3{Gy7b#Yra&TOaX{2uOHIYdd$*P>(OpV`aN9V^J3Gmdr*Qwac+Kx@DE@g&I0gDGDQ zzX#fA`U9M_Cci(n)%+1me}L5+IminIxEJrZd$y1*pRQw7(keFit>KvO zJ}y-~$`!w3Np3hZ**5&#ax!C#)g=3F#ZT>+X9LnbQrEH3B zoa5@nEnGUYf^BQZvVHwH)?|Faj?Lq^a3+Jo{OvrxdyL1ok8%FkDrQgW&+Au@<=p%t zE=o1?>6SuX6y!Y-kpBEg!cFQmfPma?D+-mv& zsB;tbjw(y#=o1&Erj0KW-BV3{qFWo#6|1JcHCPv9;IMxnx&eV$ejbNmw;&ArCu26} zBOEe%le&2dnfa%=`slXYwAZ+DIg8vISNP?{cl`4DIR!T^v3S8`#t-es!A+~Vm!HMe zll$4XVHuk<=CWzo9Coc)$d(mz*|ByJs}@e7Z`T%dY>`Gobrbn`;A3Ngm9Y`FrpA;t zH^#-vhzgG81Xpw+E~FAI>!&hySbq-e*g#R<1%CPd5pQ2Uu5%kcZ%~T{$yKn9a>Be*66Rf##7TK#P|Fjm{xz{%+82|hpI|Vr z6-GTnS$JkWzsSSDJC&wh4PJY%y42Kk4KD>;e`ERlX#{(F;_F#fDjyHI`GaZJuqKr& zxKY8)0S|j?5+j0Gy<`R@1t&Qn5XzW2f^kE-P&>9V3I1+GdOH$Y-kz#1mIMpTQi3be zrdB+kwrxnijx8AUaXTU^x@)e<+0+nE8!P&BZp)K9H$VjeynUl((O1{R{}fW1LocnQ zU%&cA8}Ib`yVq>(vyqt|BiQ4;j-#FjxKQaNXI+nQrQB(9y)KgDeTl2S;(#h&Bs(~t z?4Vq(_~(%CpG#rLb@BrQNb$G06?20dVY%E8pxulu;EuqlF#0OjqjD&SxhkM4pg8_I zcan-IsaD9{8aFAad4qz)OXMe=BDdiQa=IPo&deOX-+z;DuV?eH_!I>fcX92~E-s#2 z%es}rnLfD#8}wTdf#m0Sr6^dZDw4Xjnc5dqXnj-x?ZDs}2sr+$O#v=H~6Rz04W1aZ*< zAO*b&a^1NKxa@4qDC=m0hl_l-QyFc#57p3K_!xn^wk_z zg34Br6hgJ+5OH5&gopTICimTj^=q~9VE@YH-vd&mH~I%EsS93v5|v8h(PfnIu$DfvWSARK=p@Qd2@D{?yOlt0LpHzzJ0W$s4NqT-8LX+3&6PPxbexxvAcB z^W7_k79GZJ`$&vWPsCJYpg@(S0HuI6RUdYKbp=K@#4+7kuLo3#xTs=rRX0{+aAPHw z7p7u1w=M>sR>h=OIEEjGVfj^atXK9XV*Qs)Kf9Cv!`^$xS5>v`y6KJd-g_?)Lhrrz zs)C}3qKHTlMFbH+l%jxCDbhOxNGPEs5K>6*y+i1TbfoIFpD_czzF#@}{I$=1-}6m= zH*>C)m9tX(XC z_>Qehzb5g(X3~!z;Lx_!d^3F#Bm4EDRY(9{a&mQU63p8}2k`CZQ`x?HDO;9($I)Fs zYcAyM>7&FSJIwJzyV?EAO1_)(IiG$sme&ULBuZS2i?tcHW<~`1c*qIdoh5TWB~hH( zrK(giQpHKF`HGnJ3puuQ5z*f+VDZNjc(-#DZ}kbrH^c(n2q&z@^+uW&l@PtX~w6Ykehj4T-_siBP!CBHX;yJg^ATL-9JI%t(fzjQ_1am7Ux^2A*U&roS!3vA3SG*Br(Ia%A{EK++1 ziw0-qY$MPTP}y5IWc;~0+TmzxiHn1cmNen&WUcKzo$Uzrb;r-kNrD}B8i)8086F_u z3ZO~2pE#~CI*598Z%g|w&FR^<6Mg%1VNkzr3=w$s=-N&o(?TE=Mp&>nzVbXCE;d@G zp_6KFc4pXF8_QlfFbi>94)Pc;f&&AH6eu=r>O+g>q2d%nu(4{y#`Wv849 zQjwTSfO@I`^k)Nz?g*@|J$^*py}K0Ox=CDh0sHb(*^qdQWih)&60J%H{G(`j>|Qn} zo#a?)4u!v7rsm#nT>t$64__!4DJY>`RzaOqYGV6^mbNVsz8Z>>*S#-qqFS&xsqhHK z;&?1i&&M!n0eUIotTI#*#gFLbti_;syLQE0U6nAQhWfOt^opx0*@}MA28_;pf%%e7 z7>oqwK9nZ9sU7W8iqO`J@_*2J8I&+HB!fe*BUCP1*Uod^@P<~!D zjWtWBv*_!I>|D2q1DlregTSRH{z5cq5?2C zHo)FQhc;2ZY~Hw#+>}^OZC=gJuV=Ai?pJL5W+wAK8pb<=LueIYhpDp#)&tw3H$-Ln z1)~2&4@{SiL4W0N^uFy!z-L3KlTTJbO3QA0*?To)z^SxlH3_d^^^6xUp78wH6JicU z)3tMF=^Pv}H!{LlPY+W=Jq&epFwxc3qHc9;VPdRJw%aypq{-S;Iu9EwZBD|+%~5k< zHfAal%!<%pU%vlt9@lSN=F#K((hlUi7x(rU!@7!Md8}+j;a%s{PDqpLmT;NmN?EP2{mZ}cb zREvYEYF9vA7Xeg{a>}|EQ`Nnci#^J?(7Bk}_W4{9cwOpNLP@7A(z?ZyHs&~WYqPkU zRKd-P0&>%ivf-z%m@(xwKKW>%w8J@k_1PF^PZ-Il-hJ?NvFGH8BNA*p(=zl_rm5=A$@gU@-<_FCn6Sd$N?qT{?k_o5T5H39 z%3ZyZjeOVEjZCpLGnVHt6dB>5U?#G+w8Bo@v6Fn)4&uCB9mRclI{u{)fmZX#K&?zt zRAYa_LOp5Ix`_ZOgf=Z3Ym0?@cW0!|x8k&||55lc%0oSg)!;>1*e+|9pi``D;3_n??JdXVP}vOxmoUNfYVRUny+) zCj>71fYwVU)8+fg^qxPN&v&iia7iYW4}RmmbViT<{9U4ERXyODc7mylzeghVv&Zt@ zD*t@Utdf(s?wgI}nFSaoFKD=cV#YG`^44Qmx&wpi{pc%5skta|QMzT@1yEZt&X=FZ zO~Y_OYxKtWpffrYqv>6-`Dq+|q8G8JB9-*wEcOXpqxbLTZ2WO{ZClUMMRVDAFTy;et`3xTM$CKOC zUnwV}JdMT6=Fz`LdweA_wba!i)Xjz2Z;zocdJm_6TFBz}M)LFQ57@9|9D_RhbSS*Z6(M16~|?&W&}~dAR2RcemZ)&ppycS3lw2y!+h!;sNzs%jpLv)iH zqTfc`eRkV5f{W=)psxBO0OC&cdw?rQ!d%v&vAbE z35vc?pz7dxcKx`T9Y6oXFU#lg@t8p@pZgKh-|5DpPsh`*O$6b7?i@RESlpCKsh7`E zNe$)xcaV~g^I-Hr>}+it9F$1|psWN?3R3n0Crxgv?IXNfe>$J z+BXlUS(Ap;^N#JBX;Lt1-#StpRdd?5YN7#Z$iSYOMhxxC8zToYWKd7~^zK0CPLVWk z7KXpC4{k0lxVegB7mzENo0+m<-Ot(@vVZ68-xpHKF-cVY^ofe1#WBhL=B?WlS5|W> zEtT~L_VV@GpO~|CHQ&WYq zV6klmmRqM|v2HR}KYfJN!m&8ce+R$s-lOZ1@qBk;17|N3aP!%H)OruK?n7nZ+?P{K z%}uGu@QIvw7v%ogxks?wHVunY^UzQHPAe#DP`DAp@|_q~@57*0l|2#%Rk~T+)fSBM zH({E&3WMm6(f?LD?a^M^IwG^V{qdFevmz&s{hQXY=*!P(5fZ@QeqH!{`gr0GY#{%{esW^B60`C%7EKt!?AM2|Y|_U>|FD?Y z70WoiVGRjeR+DjP3(3)&h~Kx4ll#{YAG4ms6I)0+xl>$QG)d=T$;(NitT>Y!m&7+d z{EY{9uJic!yIj2{QF?hg@kci?{r$dl2(lqiMXv&@FjspPd@`28!+@uwQbeafRJ;xO*t<<7l3+_`gGD_E)lLyPdGU8z##PanzeNhk3_ z4L1oWWv{XfA4_oZyMmOq2wDyJYQFA2Hq-xN`%fOB;ijs;_25s+Ru{6&X*q`+j&RyC zffVZ`GTf3m@0vuGXC?*y`ILqfQ5IHAQDh;d?Zx4Aui(buTB^HcQ_wP*%wR|=?#sqRrkMVEXkIu}ycshm1-ST%j*d3qO8+e2Je$7J&Q2~ek=pm<9X z>AMdz>z((RG^!s{#&=`nz!uE;d>rjtN~i4OK~_epoIA=OS=|B^!C1l9fp#?f2AzUj->84N%4= z0xJ{DltU9hDVZ7R)5uUs56eb|SXmfiY%I>(O5THsjy%6UZUQb(M=Ly>Rdov+eBB)g z@^K|R$VZ%3h?ef&tVt+sT0{!8q6B13G<9gxjBZ`p(z{oC`u1zf@Yi}UY*;S_4d_MR z-aY8jsV&{QcEr}kl9fL#)7tXC^Y(8Ass9*ZsbRMlqWb#FWM=1b;K*@~oH@&pdR6N7^@G2Ax;qdn6x*!?Mb zyQZSQYZ^xCc*`X8Hhze~`VY}x^)43Q^uc{w#D<97>1X-*B`vowTe}R;^mbrqw?Yvv((nr;iG#cCm2YEHafU0Sr@LUEq9rfK=SseCoAKXb>NZG%VoB4^{s3_)YRRxzT$|=jvq%0>( zlty`O29*VcloS?FARsC%$)KPhgWRknO7qW?nQ~Io`J_0`C&$ZuS>$J?QeBeI#mZvp zE*A0l@ioc{pm5^YXU5m88W zlM2eiOEpN9gcXwCB!}#%Gz#0KQQGl5HNCQ^>7GMji!?5@FXDQ~a;hRSxY9}-P^)4s z3otMIJcW%j1WW~#24_*-xRA1t961(nh3AkLmP%=>97@{dQ{A_UtFK+<<{LM-HTqXB z54%Kle{oMeN(ELWRCX>BCsjasmjdegRB^Fa75Qz?liu+(xo;#;I60AxgTCd9-b0!B zet!n{iK0b>H}AeVlGd%7iIa8V_N|**Q1|?~a>V}(DYdB~L3bBd4I~XieC8TxloWVg z0ZC=}DJNy4fb~)hPRrsYm`q=S$QV-tqraqXn;9u6DG2Hd1a-AxVFf1(V*!+jK6aKS z0zgxFP6K%!9RZfyXKjeJr9KwsdYGHaUgYLrtql=-i#%N&MD_w>H*xJQ;@;h95+0~2 zG9rj3qDXOIZNzD{Y~xSI&OvnU+C<>gT42?ikt2rS;o-)bRV)4)A^3Z{`FGm>Wg+$I zD^Y4#vER@IXi>CuGPm#Cr?9w;6Q>f$$jK82RYPJ%;!^oH~>tOFyF1-Z=#9{}Ouvlht7bsxL6#EADCUXISn24C`H=VXJ96w!5ce zzI8Ij>psAA<44%87=`;c{Ry7Zoyd2)(Q)EXRtg|4h>QDO-q+(lRF;+s+8ZX&A4-&6 z^ZX%Gb7QdCKO3{$HRzRXLbqz00BVO;Hc7vDJ^FcTG0IwlLF#gh6BePj^?mdfbw}sz zKmk<{_DkMm;;Bs(Uo2<4IG(R&OcF>f=E#A~Y~Q$owLdN7-O(cm@%3QMiXX^KJi(Hg zllbht5$sqqkNmR-$vqRx)v{cHQjS07S{s~U6q^-eGk-55Dg{o|zk}V!{^J+b3;*POt=_2Nh zf19_vcA#&VKi&L2=;rH4-yjzTgt;)fl`rpg3T1rva3&9H!;e!&vU%Qz>|8jRE#FOM z&&maCTRxwUM-8G!izwz!{g4B@x5?*K${&vv2;te&=RA>4_nClFP4+ir+^Atd{%JT+ z?Vl@W_lLa4hktVF+mozt{*fatF~r!!aLV&E)$K~T*g~M>n@O=(HhJ#p1` zscDu&jr#qFOsX2^YATD!qF8PhHkT~suBlR z*|ChGme~TyLV;Bo<(ei-~7lZFkVqkPvbnupM;mWz_JzR_2WaIuBfnF~7y4m63 zV2Pu(3GR-TxHwy4XKNy$GLhFa#>!OOmWd&D7N*#mo8oAr76{wpE{yAz&nPIwEf zRQkG?-1Zk}1*@tSApwMjdEysfM|fioTD6X#XU{GS8rT;%H)r2ozq^CvfN-KC(glq0bxNKQ`Y_oq*(zI=`ESFh!*Pp0tBv}wHc z@dREQC!PBA2~3*tJ`<+B$w$-2FyigLjGQ!rZi~j#VcQq9J-mPxv5ScmhZV4XI-W~E z#BuIuY^D#z`qLp;Oc{X1w1JpS9)Q`Dfi#*r5X))3@cOh5P31W9gI@TK>rB)K!BYa5i_e2F-u&EZslg}>b&}uTeXWO8WgU@Aa^xJ zK$>%Z001BWNklpo%15&162^zlK?}KV$5${)`_z zh&?-2uzuA%QW9cl9ubJCM1%3i4shV-r8IH35~s3}E9L3rWu2fv98hUlJ{6TEvM-{n zyhH<3Sy>qs<`Xat`L}2Qd?O~t=v~95PPK>>8vZu z3bgfE*=Z*^b#McZ)8pAQs1M0=r;&YlJC3&2+U311jC9%h{R~F745VwTU;^9w<2tQ7 z){9@qX5JfgT<|e>|4LVBh$jC#2c`bKQ2(nA|AjzJ0QLLB2ON!wX7uo(vr5 z{;s^)E{qAiTJm1!CQKdBj+w)|@YU-*m@%XSGX}R~&d5&8e4`U{C-!IKq6wTkxRi`z zYsowP3;AcZkb8C;2RD7g&@MrAiFCn6PZt{#UEUlpn8bZY`2F6)zb+%8Rr+`)`=<>~ z@IR%XqecW?@bu0zzH9yiyIeODW4@R4kYp|ntf8z!E~!qjWLn0NWtl*_^%+tf5;*U4 zmJElJRQjGJ%Q1#*x1*$c9wE*9D5>58D(~Z(&ijabkCEnkgp7b=WcbI)UY<+AOTbkk zfGY?~qB!h4W#X_(#Ca)5l{d|$vUwp@&5I~)BG3s_4om`@mU&$3bAc;^FH_gQj#_a~ z6`e~cXq8P#O97oYxXO;=vN{w|B}xxJO;&q(FGKb+ug!Q~@6cVVh92bUK}4u8F7CE$ z-?Tx|-%XBP1zxVswl8Hbz_Yxho_T(su{iebjMQwmzi~5`apHjAT7W6 zWqbcSsQ&eV^)CmhS10hR?&nqHeEaS_ip#4weKwigoI>tiyUpc`*Eo}$%I1TI_~hFK zeDKvA#(n+;)4ra=%=uq2?xS~@@zq3Te=~_0vnMcS+#p7b=|`(UZ3*bt0;ldxF>Mxr zep7#RLLAZYvqs;;0s~ib3|%cS@wCO%&j}+R2aNn2G4^-DxUn1hQLZ9qOj>(j*3loU z4#D{LYR{W9KIcq95%(p6{#_z?1*khBbp_VnpFZR4{TnzRS&V7fX7sDJU{EP4--y1d z@0a}(`l&ynpC~Ts)I9W}C!@Q1D8_TUqd&PLVJjxl`IFb^+O0LO4d~9YMRVA+?nizR zAO-oj5fOi zAf+Dj3RJIPRb5@#Ac0hMr8uw33N1N7sZQ>@SW~G%s;X4nT1mR>Q@M~G$Ax{1Ssdg< z!ldzRUHmmJww8p4d($GsjR~*!B&wM^-fg^bpV=SFxkE8{tu=>=GqvRd4NmFR3Ho14 z0ZK^$?1KDVUrm`zv!DRntjzJVx8<`jW0)~|G;6>4f>jHqvG~gmnK6DS^QVpB`{{49 z@bhM%0Wpjc51sewmc>L|(b*pz@Hr1%}xP7s|TFFcu& zrUI)L;?~-WV{Vy4L1Y@a5lK|E5qH%lQ+toUzIByLZ(XK#)CH=#7EsYDlae+W6t&Bu zpnVpl;;IUoBvaNRm9#cT*xGU>W4wpcqe%xMBZCO>7cfX?U0+w*Fj}HA9px+icHRO^ z)$pE$MMJWLv2rXHIN8|!15#>uPk~CDlLC~fu`$L5hT6ps4U}ur)7Ne*7o{AQouyi2 z>nv`@Rg=4`I#z&k(T3#w1V$dNwpvDKkgqGg9*%?udDA4wi^hSTG!6Hqaj4vueSo*K zz|2V;mIGdn0vmBoZnomU9PP1bB(6`(95lehP=`il2H03P(g5Wx0BnGi-CvM$mDls} zb;D0weL$cWfkD1Bjf%k6&yyQBu4tnHnnnL>Rpu)&{rw#H*WLc5Af<-!lxupm6h^IK z`c0zJ((-B&&LokXmdUkWZ}aPoJKVW)mxp(saQVh>oG&eB()Qge&_BaXWuON(0hAKvY@z3Q!7G>M{S&D^O{Vli#g+ zshXFnt$gXI5TCF@T95zt6G5d&oX_l9HTc&Lz*6ru6dhXW8eUaihM5Kc1_r$f|1~sW~wfjQ!Q<`Sb1{Ea|t9U7qVX8*+l$ zfhC#)i8I_yqV56W^>%YccNd8|dpNDLn>hVl0;z+fI>!=kx0|z0`#IsXhm&5>q(_}5 zHR>3N5r>EmJwSpeK6F24!US?nV@U`(z!C55oDSN@+2BJYDg_WEB#Dw1>6Ew5qNr&yMUmpXnw+Dyv$(2}7q~d)5|=03;>P%2sU1;8 zZl`ogx@J?{Ig8v@>6Et56i1jseyaqsTO@L<{eGr|zR#deUFp;=R62J{7R>%e`%Ioc zea_kVGdNh=3v^605GkjmrIlORO6PC-R|14eJ6AtbLwX8i26}qvifhu>71v~-hnb0y zb_owVYfJ6&9Ud-@8j$>b#T^BB6Cf_hOZo&)fstC+>#vd?LVW1dCW_G`2Ql&eG0ggO z5{u`3DedDMep>Pki@%%Af_XETJ8LSRPyK+mUK>cacFl;8zQW7FS{#)%e&Vbgtjxq! znPF|NvIY$WvIYWDQ(T-J1XS((V|NSd_sJ2ljy`ZJQyc$iBuwjah1m8-Nc{3@BsXLrJ)9g z;2IQ!l|(O=K?+RX*bAhbe7rS4nMvei7UqRXh$zUDMuBda2DxGo<}QHp6ldjysq9S} zyUVdNM&aVfe5`O3ch>c_L99Bmms`@%)S}?)(!gd^red1D2IHa)=;p0Pr$AkgN)?@5 zh|alh(M=FQCCo$T)EDS%AC1n^{#Yy=j_1@tH0j-#citJuu;IPgy?-bE^0)O9N4W0i zuUPcm$HX33#kKl8aZpKA7Uofylc^2w5@ zD*p$hYAee`3R0@zY_7mDTb?6{>-DF2T(XZnL)-F|n;WwS_QPIIUIzCs9`D|SEEgM(QW9Q}{ z*s}gR_HO@ygt#r_XT?yQbCim#qvXbIWz(z)9Qf)Jc7E~}%Z7Gj@$in!XcNwaKyOBQ zy78KuJL4jnFs@}w`UeLPyKWVa@7>puYF>fr)pjfQ8q=L8a6+6=O{XFfJ>!Tq+D?Lr zNPi2b^tKA9wvixG$4B%wam;iZM{GB9!g~ivO=8IIbe77Y1za0jN8OvnRE?DHe?Xe1 zf&nR%3`wJMWHtr;lgaCo#D$SXVp2qt1w%I!k%;^Heky5H%5J)hw0#&beF`M|E@3U2aaf!;Q(mQ8l)Tw7%(N z_sk)$T^hyBlBjBxMtR#ba@r=4)cYWxhmK=Jn?87on+cWk@cEPH zAvIjBe{vQFWnpN7m9d#XN@e#oWca-r($f<-DH(`+G80Fo))?8@SYa!GQfu6t?d|Y% zQFB4Agao_OIKqdB2w#GOJqQl+AWZs-;Y0c`f9@C37Jg*MrnMZ{xrw6(c5!6?c4GH! zBX<8bqW5lL&(00(+5WROlCf^Z_Y&O9WX9z8cynkUI<^d_Nw7EW;;!5zAafCDS(}^4 zdouV7oc6ZTMqQoo_Hz3hr0g_UDoFYHd(p04E5gEpc>dz?U+uj?{{yN2&o<4xzJ%28 zPo7a*f0+~UXGuy;r?#%1%&a_i?mfT_aZ=CJ!bf$ozi3#nd+FA1Y&;Olun#`LF*Fjl z&~Tb{Y)^1xIKIJtc=>x16drTkpG-~33X;U{$qTDc!lE*d|wG?1Q zxMLpfi*B$poyWb-f#ekGrE@y_>^>orCBT1vXEs*0Yp0G;@`=*G`O_q0Ij z6i0 z*7Vet6mzw%MgUPvzDjTqKvh*KHNd9fbmjEx^EllvRF`!d>UqV{= z^TyW`D7tfzr!Q1`x2ocx=Jy&F%D##uU#*Q&Qp0Gc5>Mk6=!s*12i@Nu#)vr|GiAq* zY)p+|Z&EB7xvyOG?K{p2R?At`nx2iJVT_BrpdVd4nZkL}O4v3>Y?Y+rtOtp^jE z1~babl|j-u_Oq~Ngu5?e!=vcw;=-b7)A{Y{HH1=HTUTrA)Zb9>%XxmZo5>m97?RwM zbHZdVC-t{;Oy_4#=&u#$w4P%IzYuG|nqDb`JV)B_?PGF~LeZNp2NKmOMVAbsX7kPLkdFB+25m zk{cf(Df|fW0sBb|X>eNSf({9|j*=E}T!U3^q&T9GvlN7;P#7i-sp)w#o2QW3DU+H} zwF0Rd)K9rc&D3hD-YunWXbClKvnU8pBBx0*`2x1IrYAYxVJ}mCKcKOFBv$5j6z3Jv zuSYMeEX>3msYSZB8i-VCxrM4OAuh^TAY}P+O_KtYk=#~uPDc9r+WILwYimso0s==l zzwK=;aJ03;RiL4U^IEnDp>^wEZGBSLF3suFryWxSFw1_JPqct2?&yAw#mMJ(U`NBU zjzhbNKenHfv3odsO5EnjgPe|w=FF*RP8``q%)U+R-L^_#^)2&fO=8M~(G2X>ftHb> zT4fR+>2I9GG1*u)!raV6gOs}dl|>^H0h5CUDR(!ux7Q%$=j%>DpbyQOHz6=kIjM$< z@qfWcv*9mKh5r&<|9acM45VIt)K8v0r@W??=opFOvU8}ct>bjU8CL(io}7YW9zTD< zgWsR=?2m?O66F^!^U3VFe79~B@67m$mi>kh)vYJN&70%u=Sx6%m;lKOPhUTxTC~L1 z&kq-gVmu^@addaVGsqujiCk<2O4e?6Si0F`>}-p%t3+}hPMCN)VI<%(6<1{%tmKMW zkRwjb{jm&l!75yUC6H2Z(r@mDZgUs(+InKxE(F82jWKQ;jE+FfylEuw%~?SG<0ovn zoJ-V!uW>ju2dji77zmW~PJE5=83EO?uP{sa9-Tv9VsKRM+ww7P^G4y`Gm3WITQX{N zA3p!`6O7G_h-x0n;>9zlEKjGjD3iD&hb0mfKvh*zQkYL!Ns)HZJ*A6vwOqPbM{RW_ zH8nL<*HntLs-UQ-Km*WU$q_HzQKf)Msl2pU+rPSB%|U5;<)F$HnADtRJvlb@BcNYC<;P^hSTy%lEQ*^4q*FuzbtYlh@r{ z8Dj6j01F?6*$TKk!g!}?d&YL^$+PRXw9ZDA>{5T**(v)u;J%tfpMxAR7N?}MP5`y0 zLAq->s<)Q?hHKbsv7WpZ;*xtvFcKO|yk9g4Ui&y~^9zTpH*&;rE2o`zlIXabGfulX z;l76>p3>oZ?cuoRK234ndpYX4lQ_TK#Q94nEQKlmTGS;d|xQJtN5J-8bxhH{us;TGa?M(B? zU^=voq<^3GbZpm{F>elH@}zf|^~FS%ES$sIl}lK&axp8GEnwriW$f6zhV7eHYx~{X z*05#6GFB~{&+6sh^7G2?Su0RFuxFjP?7d{A#F3G7ltX*hv25WNd^mPE1ABI)W1FUg z1^Egb-L-1zs?3qOoVUtRnVT77Y^bMIobYsS0F{^YH);s7v07WzwgpX^gloguYLWMU z4XJ;R?OzH~&tCk&rK{J8i%%foT#~lmb>IMtmMtUZ#0h@AbC*juf2F*(mW$VKl2caB zrh|uAwP!zzw(X+R>u=L);7Ho{A40F;BMFXbM!%s$@edXk6&y@xL^vMa61_{K@%8t? z!^a(04;O)$Gw#xmef{0>5$ELQEse%UK<4L)yT2FCq}J&FzOVDX_v;Bb!tkZ&S6+|4#Kd74+c$q==#B%q)CTa&r`Ns z%4byOUYrijrP2PGm>!vf>B)H*9RC{qnAsSK%wlI@wQQU`XDGo>2X&^Qo*V*M{;` z5`=O_>bShTR9sW3KuJkKs`M{Nz0xaCy>d|1Wo0j&R0E)DDkN&ID5R!RK}xBJi?#9^ zg_-i2$GCBEI~kkDGum304?9QUYbz0@$kEP(B}?Xsvv{mUkFT84-y!wtL{$|fRF#5+ zgwygJdf*!5O=#y9gbnUO+sUJ8K6MncV#Fn#+((Kyr~Jx7uGLlYYgQKb5>lyLxR&f; zpHS9!BBgD|QXD*-s(|4XJN2d3br3a<{i(9=O@%`b${o8=CMt30!X>|fT=45liDx$o z{CiRoIf%+muanqm0CA(=BK_OBEa=gVPy9mo(7Q3?T$?l2z8xRB^x}P|ZoK2%f!EyI z@P6BY+^f3AA8M@nIoh?Hj`#><>^E~jfE1&*ni$=mIIQ!N9Iqrse&R?zMnG|l zBMt%;fzlzX4cd@jto;@ZP)A*Na7+~ECh&3Jsln)oD8^glvzr*-T^#e@!|AYt92d9s zN~c5iaWY7N=Ch4szS}t}iuK+`e8>UPn#T#OQpjo z@lE1*-~K}aOoPSwIACRJiG#7YA(I9`nduwRNMxqgH3^*5+NK5vr7I4~Kyy&)QXO_y zme^aVtMFP9Am@4W$Z!Vs>(01$UgL|;K4$rk-}2Ma1(v4Oh)p3(Nmnto}cc`aiy@zDHZDCfoI2ZxbDRlvC%DS+?e97Oz~zlrO$y zm$<6C@h|3ISJ zwr9}Dku+}Bl;F@1!otFcjBHGBZ~!5p0fdDI5D^h5w|xkU@F6NHkTz`^)4Fv8Et&;u zSK3r-I-;5f<0}rz)z=XhFOj?0bG7zC+?ZN$r{wA>AoFp>Cd3n)=7HF>4#lQzI1Zg7 zF>UFENh@~@o4Sara;D4px46hNr1^7|C!BwLlTXj>C2-9&Y`0CvH2QNK&VGk!%uEap ze}Vh;7s_e{63Uc%0WGtqzu#obKQaL%ZI1_)0I)O-eSrN(S5(H%V8iW*(N{b6MNU7&j zw=3kit4d3>EWVoZ1}If4(3LANm1@ZuYCcMVsX>KYs1!(*7jv;VTVNYtxj^U8a2KF1R;E?HBVl4zx zHk*iZ-o|lffs`h3d#>UTgAR}wcz_e$+d1jCixYkguIXgpZW6){h;!Pk4ezOA1*y1z zUBq~8(cIK20oJLYeWLwx{~ivxZ{tkJA&NVta%qsb>#kXvqlyna#M#gzWVbv^a;Tce zI!=1y)7sBcnjIsf{YeV?r}OLhD_nc?BKd7n$!nTIc32X5ks0Jgq*3UXN^WpEr&=6k zxbYxNbZjv(w!>c690wg^%nbxedWM*aOqBEuwIM&{qSUcUnow(^l#6n-v5|J<#4&Ki29lWZd|G0%7tQid_DJntK;U?GHzTd;cB(G#N2b7iQCV<9cwj6 zCCA6Ga_P5pXx)@{Et_hUCj7nKv9dJNAZlr*)@T`Mi5_-V=2}9Bhr6>lDKDBthG~jy z+W4??~z%j%4{>LwIaIj$9+XA|a#hgny%!$+c1pwk+s>&!7$SB8@FOVuLRf}yKR9aFZ zD%JJ{1^HxTq{-t71YYUd+NbQSH2Iw@0iAM7Wt5dDN0zSvssU1^+Je<;*()Hug42cS z3hD)N%0X3?7YX2ssTM$03!tjSNvV_lvH&mlz!r*#6j6NYQ*xzi+EKNuR&_f)88TWYPbV#-@HNNFbM)|Ei|X=9^yuu7Qsv! z+?$_9zE1o{Q%D&-OZ5elLO5K!H_XE*bRVf__h~n)c(C$+z!Migh27Tt>0a{4Lg-PGEMUu}rjjjnTG!X=4(~@=w0ykGp@c zq1SSDJFH}<@sI4&UBXVCZ`r2%jp$o;87?N?Wh*J(dx$k(!(p@495r7@jQP(T6+j(# z*hY-)FB}yYcHB{5<+MrM)CLZ_ttZxRGsnVq5*N9flZ|(CJaju}BKMQrJchWSoy5L$ zOiIW7cM%`FPa6W%ASK5q1C_(t$8o%t%_rOx$1`BaZR^pr*DM+d65CSM;U43n>lO_R_g({7t${v}E^RltV&B;+f z;3bgi!h2&!GvWQQtXjTA8)10#;0`aIJ`z{-SOD`Y*Dqb<@`YNi)(e;}R0*KODc!on zo!ggraQ`|_AOEHmPk!>?hUgl<-@nRZ+28%OPJ`C<%O%{pR>7^yW!%16!EaZ~xhx>7 zEKDUM=@`cj@8FQYYRkq|jDCFxkrE7t2Ko>v5OQ&_!`8BqRvS>cDkFVeE!$76qf*Hf z5sgD=(j-g*=|G;S`i3em|JC+CkorHgDaZQKL8&q~k6-*rNp&qT$4;|u{Z__LnZokT zzwqUcKhkCR2;TU3B1z%~Ql*3ZVCD>#ZQaa`r%$PUaF3MgD$Yn$f3mQc-SOubKXWd_ z$9+Ipi626@P1U0v<5v$w#?T23Nc1FW2^ zuyl2xk+?4-4_kqj6Xw32n0dH!P@?@&(9SYiE}D0qD)j; zD$Xe{m-FXS$rLapCZ5rT0CTd^$<4{oq#&1mK1BnU0$6cjfq+LGl|WAoAr=+pQc{ph zk%F0kN2QyqeR;7wu1p|VQ7phK78n&#RV9wBI#-K8FO}wTwKSXh>U42hySY=kkojG` z_-t55JgiOSq;=pzUERwSU4Jq=zfDr)5ME4Q$%{EVxYO|?9uJvGZNzIlYcZZXou+UAoIqp0&1(eJ23-U^ zUAU;zmRqJhxMAF#Yr37euG57ZIvuIkX-SRfvSA16_1keluR%4st+`~_o(sC|xTMp8 ztMXX&_-k@}QMV13%-U0D)LI_fl4_miRO)r4w$TVm&Bt@dY#hssUgInCp?qM|n_iYJ zxKn1dwt$MfHL{`&)-iR$ZWxbxdB?dr*D*!aog`#idTMO;z2 zv`=wF`6*RY!dy?LF2ix>ED z-Flj}?TnqXH`aE}SX$Xiq-~G0lasch+}+hh_HyhjFme((I^pVMuR+DbTcQ|mwYQhO z13uo)+GMMrI3<5?Hv$A^0Rk33vFl3y-X7XysLI^&R={%qXILqzsK`UW;@mm1zvrIZSDvt*wy zzauAgVK$Ye1=^)TiVHHRtyYdMSAdx=PA-LuH5t^GrE{qyjhmI{s4v{W?=>s=qLVw5 zhY!Nq*i53$4LnhcUjM`Jo_1k8^?^KZ$bx$zoz0v%pW!U94-^O0S6uNN?@;1e_T}2- z@3=YPTW)`{i2Eyd@N`ow7rRcS-g*#M0>)70&_|q74=x#Z;f|xYE0Ye?>UR?0bmE$U zz)80wH+4I4Q>PO*)Ly?cH;uY-Qyza=;B-l^tvIVza@<_hRMQpFRhTmMFls0J4x)~tPSgnytM$5)XV{NpW`p@bw>NL-wPs1b$*gTVlQrhESg$*W zRr)hnV=$fddehmUJA)IR>p1JXjlE_|*=_U#dkuf!n9X|j8?PiK&W+32RP{?4$FN5v2GhU>b8{_=bgkjZR3#3FC27T&jGh}M0@r=l#4x|(dVGET5}_ZngjZDIa5(+Cby3wVvNvr*SPv(*aS z8XM~4pw@D^xeyi}qy=I&wpP>%R8`RSpN72~y3zkf{i|&M*Fs8jQL2QHD7&bX=o6>e zy!!w{ULQ@*p+o5U`f%R(bP6M+k8K>md~G9`Mizk7#!(t(xMUt!_ORfIQh zfssig`O=+mv~|JOLfn(3EiU!~3l|r>+|){PCy}!NM8QQ}jLaK9A8&0@UucMrMCBd? z2fGs(;3mbT*y8guqh47pPfOSoNzUjrE;95IUu#pDKk5hb18`w3REftQc1~) zWD0DuvN9+wDQEzcs7PR?9F#U(S5_>psX%jBYCh^xU9EPXa!#RKfC>P2TxKf_N^~%FsF8-eStm(|` z7fSoUKt>H6sTu+*_WMWKpjS2c)CZxAl*;6MPHI-Nd@s(lmLOo3YluYay|~c+Egrt{ zB~PX;=gJ4)P&M^CF8#QXKR2JCy3;3IvKz)lel_gEb;EAlXw;wU zjRtYqNDb)?5;rwiz%*FfUl-jl6d4R^kp3X98}#9-VH>XKH*b($Gp_136&Dr36`fFV zR)ONMdUBmM_1+s20we*VcMP^;;$C9Mi4!|cbDYGz(R&dBgbEVs z6(I>k?+8gqLIu&Au^o4VvHg7SK7!aO_pbk6|9|h7m6gMK*Ew_M%$dKDvC4 zPd~WIS6_TAVETejKe{S6Ty1RGu!^@|`5jqliTJzP6XowoK(H^V8Ohv#?=1fC;;+bE zvzF?ztsK}RCfIR+?oPQ$J0do^yOQGk<-Gga^E@_p5+kD{2=H_zB_@PF{QBqAmgRBf zVmDuYc8P0Oy6NoPE7#E;xt?~(4P*tmn-;O7p@6cIHH^=SlQPIr8!u1|6J}v%qILFF z9eqPW0|bVVSld|AvU}&9&g%bx)c@`x-=O-g6{J4@>KjU`>p6b9kGEH?!Vnfg=8Rd4 zo->CLGiUP5pZ~~#z8#;#2?>!8=v4XY{i&dJq-+Va>LQp11A*% zCs$=o-Uf`a_7#It5oGixA}ECLpfE!G0|hYtga`W*85Tf9sOr#X)O1_{Dm2JjwiSE^ zAvFjgWl(x8?t*IYxx1h;Xv3>$qaF+3f&(Rj3<5tN0hZXDwTGY-tVx-b z3S~}bHHdY|{yhSqBl5V7&ej8*lIZO`LdS`{vb~pqo}+Y~+%5a|aH0PI1KrJ>JW|5d zb35s8tLA*yYCafvgZw2K{Cq~5HtAsNk>lEE<#(sy_#RT<4nj(C>MNDq^er_7TX<9- zLS@`+F2+B_89M=o^LIG2yLn~?YDU@TyupHM==CqcAp-U)pUq_8^9;ISZ748BV z?|d3OHnKOkfE`i!G{zROJvyJ-hyrTE^95wtRQqq%Y^%&;4aF`iC=-w!%537~v;!QQ zwwH>S0*d9hy1-H@4BMzOlu+v{5Y!jTxoSA`U>EHVv{DgSEI=$|XJ|FM!)nN3wIOhVVGmf!0vVr3x_t7=^IQvI8b8J=<*)vz*rgOamDT^OK$^ttpHTt;| z(LupDSX=VckME^hZn(M+AEdFQn3jr4KEHNNYkxZ5-%r!_T8avC88tEmD=Tw6++3J7 zaRMt|f1L}bI{ENYFJF9imD?X(;8b%ZmFpI;W$AC(y7qT0dF5eNzxAYS|CZ*4Rh$`U zC8@o0IsdDzTl(2U7 zYm^J5_O+Dq*u%3KW@DqZrK$c?R_12tJ-rAFQo|y}U}rakb?eq>8GiqP)c^LP%3jfh zFTT1>dsiPjb|2u4Wy|r6iy?MQCS&fomnrif=64Ghutt7#)d%*mr{ff*&CNXX+uslv z5rMU}EpF~Ac_9#ICwDyD4S0BZj?Re4-fPsT;SyI?T4?y zTf$4LvQtnB3pNlPAx5QopNEQFg&4J=Qoe`K_mJ|_#`ziqm8uT>Vf?j<&|jU`oxlKt z7C`~-0xTCXD+j#&t?}>|^E0|Ia>6(|dQOWysX306F$`As4NgI>MpVC3?Ip6I%xm)vDPPXjCXj-Axt0+X45VMSrhKH9w_Q-)Z(FU{dW&3RcGt@8`&Y zz3i7eI0Y#MEM;rz`OdbZoasI(5IRQBsUuPVw9%)g@i?+m%Wmpv-$PexGZ%XI(SNFu z?$#Q3Q+yKk2)Bnn0~s&yb@J2PlK3L+X)XrNe3dDOq5o zfOIDmoODU_$acR;GG}$e=@KhDHEc3_tfsNmWIAhfKVqrL<191zDa&=w3ACOU$Sft# z<{ffOULORNiE6icM~ggDDNIclvfIBzENUBNj_av(*+7ZIY7JCXVp3(UYbkaT<8sZF z*hHz*Ix5`O3p}!^iO!`mdXvOP$|G~clyazv%%e(-snl-+6~4-_)==%efg-n+Z1c;e zVR#ws_Z((-W)&3zwNk%As{Kl+6!_H&tjatJDfZgP@yUDXpMR2Sm5dQo#?G*6>O2eC zA6U(iFx4Trg>&OOxiqJrLuq^25m(RN5pq+Lb%+iDVaE%H$@0q7s{0hAtV{-NO088D zpVN?`csLH_)!#kO#qM_Ma`V{m)&g3J%DHl)lP)P!vRAI;{(I&yJ~KnhrhD+g`)F$3 z&gY+gBtZI*?!5;nes2*k&zjDCkwIknIxsHCfoX{zJe1|jukMfI*Ygv3?deQjeMpY2xN*eSCB4y8L!Ok@Y9=^~Zem$rW}r6!WX+9w9v~lKBrz6>D0@K>rcC z#HuRF){(bmF>6-5LTzOZM-Mb`|J*q^46(&tfTTJkyE(hy>+3^8LL9^Fhw{fi{Xwhh z{tu-7x0gXV&>(fJt&3-#|25{0E{vEmnW)TkCfxrZ_doL-zZXa?%gbd~>j{4L(jOQi zHe{}PlKc4JG|WZrK)khSmwmi^adq+#cmzoJODH4qCn6-6@PGgfPQd~cUoTfYom59Z zR}C-sRhcO5# z)q`BGMOd&0k>Q?1M!1VvIT94=ikCpj!`qXR$}&E``IVT|H{4Q?lAqXD@+HarlLjfx zroK@FMXQO*zhm6EWJZth;^c|#TK^7}7OtXSjU4g+08%QOPgU_ERT;T^*V& zsp#%#<@)6dnxPFQN*vRS?VP;kTwez#kM0q0?dHVcW;%}V5;*PWV(&3}<&LqpeLGhN z_R`y0&A_P|F84NY>{PH4K{0#dizei$5zV~>=Q#N8oHWVk36;wTPu)SGiXyTVneQL zC=shF8n&K7*}o^Inv-Mpax`-Xd($dt8BxZev2`4pP|v~fb!-dGVMj_i+XY57$t8o< zUze?@p@Z`s3#) z3&@w(=d#_ahz75%>@kW_1=i7(dYI!Q_HlmR0J}%FP#06l(Ge}|jjN>Np~L)f%}Z2rcZXTD@OR{GUdX#|zCdGr5slj`NfK*Pkg~S4z{y_9 zCm%h@$%!~gS@qasj|^T9|720P`g<__2U7nZmLGnh>MOq~x6W7I|CsrYK834y08uHU z2um43^2CWudGKML_|5NmdEsJy`NuyIo{)sKodeD;Zut8L;Opayr@I&Ku3mV0>G3i8 z5fBhcSa>u6LBWJZgyJjq;_m6BjkfJRY$$qXJN)&IVpX0RFrp)Th>H%C2)Gk|#6~HD z@+LGuPpE(;RNxe%+M|3`cuRN@#tFDmcKC zkN__+IuC)ShitnN7No{1(-RUTh85(;E3f?-HQAe*Tj;jjHGQpSdsUA$NZpz4;X5@H zlp1OkTJ|+zYirJe1%Keu#WsP;NdZcaR^_K4b9Z!zAC4ZOMn2b~TeTml`JzM}Vah52c*oez^A>Q8Jmc};9;5LC*jHkqh)s^#pEg1=;_z{qaxU^om$xxi_g{VEDG@GjU0$Rq@vOTSw!_x&E0fPsy#MRDDU4iqMY-Ob!x*fRf$RM zjHsqTU&wB+GMWr!9F1?$2BN(%x1W8<%^XQ=qBXgRBcq#WxVMH-%V36>T4Qc*t&J>U zYi^3YxfutxmD0Q-hXoJ+hK(YuQ=7nw?cE zX{lMsp{5P&sb9nX#*OUWF5g$tVrolPaHyq-ivvfw{l!JTy>)|cz7lW=w01R?F?;%W zJY606<wSYe(goEsW(`)>~(D%$qnn4Yr`a2Sy*TzxqHgZxa!R7;_AfxKf0Hj zH~%wz{Qt`G_mKMZi<|uN#g`c3=tgWxI?*YkNuM^8%$c*9^1yuVfBG3xCr!rGatN-D zp7{9&;pX9qK~&gFwKe(rVKj;*g+~$_6Hij&a6&@D*5N_kT7>$0N~nEe zSgLAI02Sn`AmyPQ8|^zj>O=Qgfr&up??5$ZR%%3v zQ~v;|A9lvQ8-pW9d=Dv=Uf$QOIyRo*-04;>^qdedwV&deN&o;L07*naRC4)js~FS~ z2DTesAzvIhPPYFDnr+hP8Zt#7HG#91nOwA<#g!pH<}bm|^7VtS^I7_{ z^b8$Mk5wu?CL;tanY?c<7G#>G^iAv8+6gaG8o7+0_?3S?2Rc)`N#B()8UMnf~ zTuG^bYpcs*N(0x^oL&~+IbZ5B`ywv}^FcF>Yl z!B(H8lzXqD+L$A-%A?VD8@Y~)sgBNP;E^u&rq@yGzL{pPQg-RfXf~A6;$OwFq}`mK z)=k^!100Iq&XI)e>`$m;e^xDz51oUH*wiqc4Ynp00xT0AnlzPF&-{@;k9m|WkG{eB zhyF<6i|_H`-1~48fDJP-Wny4B2Xb;){If@yJ2IT-9-1m)xnnE4cNPgy^4ZmpNAvd0?5bC>k;aEMBubo z_OGSmNQE}|aCzY}inc7`_g zHp+Kjw1LC>n;Dfh983A^YMK#u4_9sMK4nt#=G`NZQu)9CzRSNHq?AFaY1LnQ^F3VL zy@^anC2sg=VkIWboXfZwvl%&I0v=*aLtH#?clW{HCs^z$0KK;ngTE1Pe;@n;0tpR? zAR;W1(BM!3KoEL&R~(1g;3IYu8Kx&CK0t#~N^~%Z5rHH|st6%2ELflwN=#UgK*LYW z$)Eu!D%7X}M@8_RQ1DS;3JV;Bi<FY*-8u8f29e<-Iv9VDcJ1%!&YFs*%YOU2l-yH^Ju-Z!vGWo5n zZ+)XG0&nxx7hkY><9fovyr`2qy0d4GXxVq?2L=J7hAHZoP@QvCCtNj3xN3*G8~;6| z)MI5=%A(phqXvrYJt6y#aZWC{3q4{`JqKlbKNtHBaIU+BOFer8OnbT7yH5bNi;HK< zxO{N~XM5kIX7i6ZP_vkuAK%cXEKz^U-#|)L_1&q})F7oAia+_Bim~%)b{fYCt8tvN z7$+v8I?qn!nuQpQm4L!^K7A^A!DcMy1WM<1lel3!d(e8MupbcPx@aarE=I^6rY&p4NSlf)*R2nu@Zz!ZhzmYBSK4pP9bUkp8BJUikJPK$ONHr>0 z87s8xzqaH(oF02jo6YxVY7>W(8rU6Q!Mcd|@YK0rZ)$^`r7i9@4m_SZjn~IN%bJ;g z(?)&v^bmk znnHonHkx-73#5u^YS>Cc?PkhLR?=9PLrvK#8miU_sB+j@mn|l?QUg-U_H|-gtF*^0 za)Y>|VwITJ3bC%W>~AWd^LRaNN9x$wkk5(3Rebt!FJIrfPW!P&o_%VbRKA5dC1su~F;XTtIXMcX=JNH|>h|s5clnoq)L$*?JJqi8 z4OMlG_y&ZMkUEN(;Ufu)P9!#IB$H;$(e~N9dSGMkh+e?tZ45-O_mePc1{Dw@CKMA% zR8*AwhX>>1<%73}9=)5Zz{W_tfG8s^n)H-lQewP`3-uy7%AcgjAmYP=WgSVFuRnqE zI)6_&pTQvDFlrVQEs#>6i3;_RZHX}NAA~`mW*{m=K&7hij0#jf+GJ=EcOj+r`zxbz z6TrC+LP{dwPAEtP`M7Gcd^uIyjP7D(fJQ?7g9eosJ{i&Af)H z?{0}9eyuVW6{K$c4WzUmhz2T^Eb@)gZ1(@+b;^dOa@2MT?N*cN5<|Hl=5ozsrdZJw zF_{^>Z#Ip~7Gt<1fVwD;`vg+w%%=!2CUezs3ZLkwOUSmp7>rdSeFBv}fl!|=fPS4n z{U*Mgk?1!GlywAWbP5ttTDrJ0o__O0&J9W81J99Mb4j7cI)<~uk~uRpfpbp7>9vce z-7=gL^7<};SeJ=PHyDgI{mkAJP`yE! z!wPo!6zcb_RvzcId9aGFe+VhsBqpu?vPcK zMi$chcsn&I+t}c`oHDOX0;4Sydu*cAkjtiFi?pi0)ANo}V%$QdM-kigr2?uF_V|`_ zBwPyj5qoJJbAbIL8rdIL%bu`ms*(zcu?@k^)&U1I8&ch(_<7hoUP*qE-y}Xr`5)Hv z^88=oD5m9W>A@2Tvnc-A+dP*sk*4B2xnVj@(bg@zyX-A;iq^1iUxPrQmfg)Y>`o?FW;A*T| ztv#+Sk-xie5skGuoR{n4)X7Tr?A*xvS5NZQrvn_=Q_izLnTMC_P_o7(Q&S<|vw*Xz zynyzTM@fv2)A~z!c(`e0oP(nS&p-dX+_e2*Sfqc$<^MXQRQaQ-_te*{?v~q5brTX5 zOIU1z*wYAY9U2`=RAMsT0YNyqd1CMEhL^z?Z@)l^Abk7-wNcE2!$OG&49fmj%o8bomGRq_NorK-G2p=)(-W9`0KcP0w^_t zgz7K#J)rJ{m{Y$P(iw?!y$3iipt>m5bg{pQ%V!$7)L+My{z|T#E$8Z`3XU9pkLKn# zxOU|Lw{LyScQ?Q0mKd})3ASo@`c6&c_K!`wT9Sx>_iFtC*4f3e*LE~VO~%k=F`aW3 z_i)i{7U#_sh_bkr~HSB*d7M$iLX51Y+(zbwvsWYIZfG~K!+i4kH)qi8qHq)R?y zpY?Q33qZTY>@GUZ1y;L;S94_2 zZYsT$ZLJqjt*6Xs4W({kOT#u&6uO1>dyla-Xfq|AVrK#=1*!tqbz1U7o%zp0e`ekIS}|1&&nd&J9Jj%lpjq(xKBMuF5?s<*As+NheV*KwdJS8Qs{ z9g|u?OMNztHLKQFfJ*UNr@4RjR_-J&Xby&$jJ0$Qd1)2u^$nB?)ZDy?lN40eTKT763&{#0IM{5F6r6OsKb<$4h`E zKnqkBB_Fc@lERTKIQf+|qL-&f4ZSIx?)rjT}!u*aZx#KTQs<>SSaN#ptWfu6R(=sU#O{+$vn0|7 z_|s3Qo&G3|b|cttn@*SY1TmYbQjpIOFo_kJWD1bRa$Yx0fHjYPF_vpCbGhy`kL!+d z+|W2WEL^3aA4;3Ko^~np+sz_4YoEdyyHt8?66v)`7Fdnu%&-i)EYrk{vc!_ca>`^J zZ6*^qB_?#rG+iP?>}D*jVn=P3!5Z>dW)ilNb;n_2^RR5?FPLjU6{n zVxL7CJH#N@i%Gqzo6kDKtCWVVVY&HBTEdE{F>a>Jb3KIuv#pv*tt3xil_%>`eGXgQ){^V6 zoI)pMTWd7q+N#>A#LzaoEaTXeT>_k3Ic^=rF6-qSYsq&I=n1Gc*)CyE>UMUIZepvu z0@+3Z*aq#mB8hDRwLqI7!EtR$BCR zH27|1x@!i$W_FBoNaC?!vw3>RR94J;oD}e|(t()%oOX zT1sxgIu0J)B`~UE$Ifbr8XCo-np(Egv}-%NS{m5Bce_BUmIM2$#jZ*y&Ra%pd5(Z6 zSBz<$Kx#ERDzd5Frfg~j^<~Q?#HwmmYSB>ree9@N!H!yqnzhu(u@xonv2({70o6f1 zzTU_C7kc^niw`NzU&Av`&Xb!|M^=jAspN^K#wxPcuf)Pkr77gF-(>lhfRrkdR9RJ3S5HJzBAK(M6FYhY5s7gGiW&I^1>!9KF;1>-xO;kG z@G;`$sXD+05*ZOie0)4nQQ-nCHFlmWMxzVqDbZx5MKd-zn2b0d(gZl^2`MBfGYbt8 zD0m5wT=90YBgEUCq=+EW5+WI!mPl&6nAgY{IY+oyS12h7Ap$Aizlm67P=N;GREm0l zn3QZQqZ$k^E#e{+sKlrO+yzW70w_lM{5Wy^sAf_(Z>m8hzwZ|@I1+;@;P1o_exP@gQNI4>CO5zNgwH;| zO7F1-{_wMz@vmJH3KX8=GT_+i6YvrtpQ|$tkc6xf+>F(>`bbq@RJ!d-T>1(IA z|Ky!GE^&-A{Rg^%N#*Z3XSl}ALA$=AdPLt`eN|!=>0_RPqb6Gc&Yhp|vS>4NZi+kyl zeOEl@aU*yZXNN`6YibnG_;J!Ih)$PCdb|?p^Bhi}R|0(=(VTV;rh8}fKQvfrTHtQ@t4t#=Jk)#cB!Ja(t1~GOo_)U{}O8HVs`sp8Zk^ zoCblE@34YgF|bWTmdO6)0=nfIz>1w$2&9%#C?Hxp^gZ@xHBcS8l|tur6iE~~uF;}I zU{o-41%)2#Ih?hZBCj0somOjr+IA<3<@j}`Z&4pw$mtoX+HVu}#;w$Q713-gW0$^y z-98PprXQkr(rFHdv{3Dn$7BBY5_H8`vTrlW7nB_5sbx-_` zvPEyxyQ`6+^`sH<8=y=<$f ztys?X%9Yg1{;JZ2G}YzEwR4Y8)>YeDy9w2Kz7~KAcGzBbbyqf-xf!NK1@lWKtBFsqq4-IC)(R zBgDjFBYf|KGAc2waHE)-FUev4B!>7B6D&XyKq-TYQR`q&&BT(T70~oV1gS&|C((Fk zf(2Ls{w^3L{KTLF4NhWJuG$!2etKto3~H)qXY?AR>;=dQYDOkcoWKVk+z<;qP_tK88RyvMumJk7S8*Jy3Z=hCT0E_F6@>GV!6 z5A5dR+1*?iXyN>sotz(N=EB)#fzVE_oEIpaY2eb??Og1uqQA9}wtX9D-H^iDwgwM(R8}`(CuX4^e_RHZ49R@BI&R-a!}{UVVxU?bgr`YlJJpjgMi3a z02IJ+U8rnF$^Hlqn?!KTEJ;9?LaTYItcP>dWVnECtN?E!Ck3Xh676D0r%cB)Fk}Yr zd(7t(md~!6L%-=9`pl+jk2`f@ZzdxIfWtXrI)Xg{zG8vX zLfs?$S*?ftlB%fHtQqnrR$Km=wdSv|-uyKI)f;46zR4PkKhqGgg##JYZ1Gsk2Ag*& z5a4XKSwODs0&;BLpu}S}IkxWzjNW65*p`Z|&SFc>s|8dm1vmmGWl64U1YRp8mQ(Dp zk}X4*uv%W%kWxl@V4m35YB~09sF5kwv(Z{$D96>uR8ksHNWMH*ZM7C<9vj8H zRg2Nz&T(FD0Iy%})2jWf1WIqd z`dhj?j?mOljibX*qM{>ka<Disf1v@cnE6|AEy1*rLARy$27II3|OniQ@@RNg_Nx z0WYIJPO6Q_%bn`PGU~ZTK}wI*&n0^u=R4l+shS$go2b_9(#E@3)Fmw3=3i9@@03b z_5KTs`Wr||!LH_C;^yFNw%@6#)(4Xh2G{C5U-I!6XQ(J#zqIP8*1yQ&5=8_iiM z^v~Kzfj;ye&N$BybGnyv`V88w{lsou#6*0#ICK;joX2y)CxbT6K)ON&Tw!{;<>umS zZ~&*JKs#mODByACh|W_$%PDl5P2!BjG)|jM76{Ge zyzK+@n#~kjN~c4OHer%Mk8T#dy1De4%%)omQGx1|fT~?UbwZcQe%*LVbU$W^0O}p{ zCwR^DQS$xYrYvGL3$1@kw*6Zgpj2$OeNTY(Dl4pCraDn>B&Ib};IW*IVn6D+&9?7K zyd$7`M_{y+4c70<_Iqs9;I&W-Wl~#)iFr9L7r=;#iFxHa%09cqm9bH*tYFyp$82-UrpRRtn;jO@lu$`ya_!&* zjS5meCF~TF+G(g|SB#q0{vi8fTWCtFVa4>t#LG>?L;8u#cOS>nXaA_p#OrEm%Ok_n zIa*xEiNi;E`sXjP=iFOgf4sar=5x7gr5X z1|MI1{euY%3L{t`6(-w31|vSI1FwfGS!uD%oS4YCk^YPxZeVO$1Y<@EY{ZNLg8Xpt zaFW-$GBVze8DpcEJ~4*z6C%a5hBIZ%D8{9w2%r*42oKXb;|2&Mf(`CsM1CYj2H$CC zG7uT+CE-qVsE34}SlJgJ<}I%?k`(SsQiPvu4?-$hj!lU0COuhY4f<#x_4afi!014b z(NW$<4S?y0(QBAi?H3r}CUCOXM!fLxau+*v#wb>$COk8E2^d3z$V?y2wJVqZUqDKY zPjORAPahobP939Oqqc9!U6`sWkmtVnnp;I)>G)P5~H)6K&R6Lx{Xum3%ZYPzq#}U-_Mn} zCpcx8&T-3895G21%gPXw8qZ0wy$D`|-v9t007*naR92IzVqw!|dpf;C=E>hOogSOH zoU@-NRyCat)6oJffv~CUHx)>k&J@EE0~6co(1}e6$PVhpQmOk9E5xK0m_Er{CQtB= z)lVspSxtFdHtU?;V58kzthahq;IcrhNleD!O_n(Pp6v;_Z1Wa4I4>4hyrV&ClSHoF zV$GiN?3ZcQq$1aLso0jXs^x64UrM$>ZIhjx&uOVZO3ig8g|Kl8O;KfR(yt^(z_r71Qr}mTJ9IG(Q6)5{Zl~T*K#f-++r^|* zreCvnHI1P)?2QvZ#Wb*eLN$;2&nIhGGE;35cxlvQOo-0F)zXec8&9^pypY5DPceP& zpP4;>A-{Nb11sMuC4ZxYz^h_M6+2Y5-u?z!_HWk!rJ}i|?jIo4(p*M$c@B-LgKzaF zfm60v(0XdhRED2wpIS+CZI0a3t@taX>Xc30fmBV!QVB6h0o9Hg0aN8VIc7ao+ZIzR z`>V>9a<+FLpMP?ZE9blAnj7Hp$L=S@-xD9XQGNNPU-Q`G58)|g)00m<&evc6pho3C zkouopzL5g+!RMYMHSf}59*7JfmY_y&d$7#u-pSPZdIaRhklF}e&T z&fl5IX<b}=soOlcM}Nd1ytTngsJI1436>xv?EfD zWFe+z@EC@V{O!IT?ikgS(FT?AXV4&Jkh`b&*ck2=n1A`@=UV38f1x3F7WG3_t$jbp zPWZv1{h&2!sRwuV{Wa7-^@G=jv_+dcaVM$c4wMEjsJqAg z%UDDM-xKgG(4ez%$N~*Q%9=Ddi8Cxuxnn$= z?FDLbY|hYk+2*;9osp$9gcNA$>#F@~vx69woMW>xE(z6srCz7blWn&Nhm?7Z%{}P~F#ZwJC%wt+AFbiO`&Mx8thadO&xNea ztz=Vi6&3YjQM*(|U+rLfl(MKhkZRsl&Fa)*cow<AAAa<~-(Da8H2yu7|Lc%a z1>41I*O>9d<4k<^N#ew6+@uh9*SllzRVn6y8lY5!hQ|>e7K@*^5B^>*#EZ?`KP8Eo zW5O7p62OGfkz{4Ykv1ZZh_DbT+&u{oa3?j&hkGU@^1$>Y=8TVFc2++mj@`aQ-PT)juLKLL!{Xs`5K`P5&LF)bc|k`Mc$ZPyGX={_Yij z|HR*&?w{<^mcPBz`%dlne*3?C)!pR>7o6C3`#VxVgiPb;uyldcD0+rW=B(@ebQ)%G zF?udN`cWJ=Gtg-p!x_&^E`*MuH6V=cq#$|{jP%5L(=9g?rvmgG8RE)b6L*11IK588 zIcXEfsUhKFMLA;=^V8l!*TO0 z+RP^ktfmOWrpmS$mZhvMCX4OJXRw;Y$sv=t67T?jiF}CD&T_nNBqwweIAlJPgI3e! zTv?nLHiveH`#EYRaC5kqZd=)}OAt_{3aAvIrqW>|Hl-Uaub0oEOJS#OBI|YYSz_`G zOH7|*f%$X1Y5p@7IsJ~7iRCn8l(50DgpKZEEiO`kI78`9Bv3cks zf!rc;9qz(u`C#>+*ph3ax*p)WFCOQ#*UHB(aZ=;=at8Pg}bFIUgnPc`JRVa z{L*?1p-+)MV>#pJtYqrkWsIBjDsz7H9zXxXT2^eVrnYfE4Lh4@(m*9}+Fd8xwKO)1 zwKZ4L++5E7{dLsVljBZnv9=H|lcwX6Qx73Ckn^dCt5|GuaqYTvOF%zFAMCOrOQe4|5f5aaOn_Z2XO zY8K@e7)Fr5Bq&5e?eoz~0q?@BadAw`3?(bo$fOKer$>`AJd}v20E|W-LIMno85zOU zaS=Q)Gl>~lQA`~xkeZmrI00E)h`+!|Pl&HKQ6WaslEauHhBbYZtVaYfencQ+M+%fi z2a0X^kdbU8J<-VMcop7^N%mpxlmt0f5dNOF0-j;SsLZ|~PtBqd!^OHHy%`zr&zMnh zI6Du)Q(2NgM^z^VsgAndo&*{7Vp0JDC9x%UXE7^*k^ss_@2Rc5#iIQEyfKPBdb+z2 zBcPf)Zw{Y)qGsj%_xUmYzm0yP#S#^QFKNJ&>kz+6z$2G7;oC;MqrQSFoBI;jt!>QBawdna5|iQ>2wVc znD}za+MiZ4e*unw$}CEt6Dx)j!BI;;j#&na{rC#7d^uto!%@>Xc`kut<_Q`&RXX|! zvn;VWfs^@o4(rCsaT7Q!AUkY6mewI-x#T@htSeKFN#&@8>~oyMG1n=a^qj$Qr>Ptp zI#u>hlh;qD!#acGCW*8ugHn*v%@jzD7f7WFq*B=>+v|1rvDoYx7MedVkopDhS^R=` zZJuY1*K4%R+(~(O4!Lei$#+o}rD7=sZi^+BQmkLeR<9N0doE{_+am3_4UPgLIc9?Z zDc@}+1+J=EP(Y%p@l^YhKq}X1F?mh`7RM!8KdF2TOiQ&;De4MNn}-SGBR=WNznEhXD}uv#!zK}wF#u~pfF%QR3`c;`_UT1Jt5HWgx1HNIP^*B8)e zte`HWoSj+qJmvqmHo&a6ts|pdW4JeI5b_}s26*A&?}afq82{i10zx7Q42>c(CW#O!bktlb0e+rh zJCV$r5>M6$BNH=%#h^mT$c!OcfZ!9Lro8YXIU$HiVU`nK>y{M@v_U5qkMNUIH0ozG%T2&86AM~xz3K6Z;#^oXy?z2FcLT><1YE9;oEV~~-6EJ)6F&)Gu_z-aObi^= zISX)X#cUkKvYZ8AZnWz{#I(XVZtBkgoxo4$rNQc$d76M~j2t&nKqJr*Gdg0Xz%xle zHICz!<7k)XRMN!}(^!t$iW%we;i%_$PWjH{u>C|%I?kfidb-%1YQsw7q%MtilPrPM zEP<3*n%L9{-3WnH7TKovu+Z`+ylW|tviv2Bt)6G8^$RSre35L|H^^~(iz1gLT2)`6 z%W`d`@oi$X~Zlk=SkVXMjt=LmRfxxqD1KaEK1y-A>ug#-D zfK^qlfVGxwTjg^Xub{j{+0*KOj8Xwqnd)b?TmUr~s_Jih)p8oD#k4AyvaR4<_O)!| zvyTVZzqf|Iz7x!xIGW)xVZ=v8YSsMzY~-1L=jC4mQrnw(;DzUj8k>Tfk1PHWVoRan z8lY5!M9hcGEq3@J01F(bSgpBcp1F~N)&6;7xC#V^E}s8}!3M~3j@ zc^S-|mco5A(&Rrog)!*~#KowI4gH7-_ZN`4Gk#nM(EWQ@dUF{?2n{FpX2N`sh>tA!Yu6Nyo3PAb(QSaswTPzj_) zrACMWdgAJ0kHOQOU~hvKeg=d5J>K|;A$bXmJiR=$tR1~PHi#{Hd3&K3kQv0dyj4}8 zY^%9sTwR?Bm+vTb#BjR1yZ-(w{r6mMeM9FYzVq)$xbjL$qy`;Mn4Vidi`7!F%` zaV2~d{eCf=ij1cb zF*+>pKWXPfhlM{!1ww~S?Ko^^E#broi$L0jgbIM7IB5|}@34_{TaKmAZUViI<2mO& znE}H%4qAkA*fN&m3RI>OC1!HeM8IP@ofFnGIAJrDBNpR0WS&8rbtdgrX&f|(6Ppr4 zbeT%4@qSKt+{-chIUE&89XC^btj2IimniFW+D)g>X(EOthIT?g)uJ26Mzh%hsV8{L zN(@Q>waDsMEVuj>OKgA3+cv*p`LI{m;PfuJu1f}u%6kQ+A=#9MuBRkCn_S-|Yz zmOz1p-(qrn7O>v)ZI;=;LUHgKHt83$S-*rrVv19(;ts2RY&D(e0Iq&U%1MTyZ`hSs~)X> zRRp6_G`KB7jq^MLR3#eSOHt>t0QK$*vBakgHLeR$P21}YJ=kU2h;IK{tPg0wa?vt$ z(s|Z~He-)!BX-8EM}0;KG>(Draup!h*#kMUNQ|>&q0k%;FGo+h7qvLH>jZR)4CD?g zhA4e5Bsm1Bk@ZL%+klkObx7`Ci9CYU#79cd(7XW~w(KA`wG}(|($66Gv}M~WY}nX? z&6|68pHvs0;(ZBLbk}0*=5}mc--0!(o3Ns*iUW#?!D`FKrM&5n>0U@x*+I36Kt-_P zpjyWO#rmk&xTz|vTwa0$dza$}2C0La(AHLqFjjH*^G0(co8a&op9$f2Nc{&F)-|zX z&k;*6n7br4{j-_?rIE{_k}IH8C?KbG83D^KIc@juWAGv{ zF>r~+ArOg!30ibMwH%?sK%TCj%_np9>W{F`Dbr{3|6M=NFLb$b6CX{h!s;L!-qhsa zgft!RnF{f~m0XoP5pN4)a7JyzdAdf<#zf$lMT@s%bhto}`Y2w4(^Nq?7ofmj^ocm^ zEyTN8Io=HQ!G*v;T=0|Oyte|UT}3$M5Qul(WjN&QhlB2+_>(XkZ+lq@2$8+A^(u-0 z#0J(T;hbwW-t^4J7y3!~I${FO_(kEUlYtyl93K~T%4sl%)CtFdI7yIUI?4w8x)$Lr zk34c{@i^*|iZ`_ruq%KdCK-w2!NYObYXFWp^e4#Wko6V*od{Ge&tM@rCgkKGzTq2b7%=x8keBcP>T*@9a_U` z(HdTjc1sO9NL{2(Gl9rhiDq>PYJ_vpBwvIjvPGyD&PRu~nyW=#jwR%}mIzBx?>Pr8 zfu&e)YA5)$VoS^_Y_)V_g`yFiq19;iAvn3uC(T2n#{wz|Os)$EQ1emaG7ojM-RM0=( zwx%E=Pzo;xXPDJ$oY=MtN#O-h=qAEH{5b^1%!D*;5p?~^5jmy~3FDd(MQ$p0^n5)2 zVj~um^kDnW1K6^CBQ|Ya!JGQF?_5VtY8CeFTi5HPny_+3J%?3yS2f>ekm_n9h_{!c zr^^njHQn@DPc_!Bti`$&HTFKM6$IU$3aslc!-kctZ;IA?N@$(+NwLVkax8C~hfV7n z2vQ%COIwZw^QZGJfQ+O#d-ngUm+orjyE7E!*S!3XLyA35-+JqPOn&T1$XFAcMni5W z7J8};YmF9|B5W{*v3a1#?uJJoE6R-isU|z5vdtKn8;+tJBeK$zh%rl%Y||kvR*S48 z14a!_#aPm~!O0juh%_Vx{R?d9R}hVUIX1)_Lr|Ec$M^y97+DyF5d^8hxe*vqXrEF~s16uM%g3N}LT1#2H^toc9XEd3Pz^bX5@qLg~Tp zha&<{yy>sS`Cv0ndTMae!AMYw!&#Q(RbatshcLSwV+c42IPIK<(@t3gCUQnWh4@HC z0P?dDn5;PCl!G(QLkL*IaFSqkl3;a`V0GH5Kf$YzgXgqU22Rj9&MU^^aL7;`3LT20 z;t@D18bywaK`Ni;C_L%N`nv|;96{==1B+tJ!G1w1)(i5{>M#mbE|XD0ZfX&8Qlx6P zsi-AL)qBoBbKrcg76QxK@MbJim7^=75gk@?Igtda$Qraql2fAfuE;uc+v?F1)qrj* ztc+4Z`G#_(9>JD^Co6rQiq11 zX0!#iU}bO<%0)AAWYGb9_Wob+*yE4D%f%Hz?p_eP_+V6O9t{2>_&7Tw*vkX!o0>3X z%##pF2SI9m86lRL2(-@3M^;l%vO>JX+5WM{c^B;G{=(k_B-VDRR?v7&j~lV}~SQ9BJ&(Sd1SQixC4O z$z3I&Fx?DIs56~Ui_t|f7*P;~kp!us*%2s8QlltEgMJw-l23uu1SxWo$VDZokdvq; zNU1rbk|U)^h!7)|&8W*JD`aVol))TQY!R?cJPMy#@^DgW!g;F+C#({@6{*6T1}V-*1mVLl zIZlPj@QE%O$2~)Fnts100(^1W%LnJ&gK^G9N-&WS2z+ta!5bfn&GHBQ^FBAEF6l%k`Xwr9E+pw{pe>D5GV_{qdM)}kAPQ1 z=U~AZnFOg=Y^HPc(9dXgorFsFCs4}zq@15eRWGFKJm0|5pm|ur6g(eYJ@HrOF&ezf&_LxgcG(=1pzbw6PWI2~t}(cW_{B-`s|c z1S&Sbcirknd)ZS@kgDUbTG?67L)6#N`g-QBR+Q7eh1jsVg4P+N7L(&D!%70x^7i>S zc6c>*ZSTO!o+d=w%;@fH!7Xx8H?Fc^2{tzBu3;U&L+U@XT)S}-(`FSztTw_Hl?0uM z1x)A>77+$pj18vnFs?YNA?GHlP>^Q8;2aYM<|zqKS`5yQKwgH10A)r&dNh)vG4wTs&bRn$fjd6;*>}yNX3MOA~sA&4o(cKUdZQyVv`!O3>AD@c@hhV zV3B!hm7D|04k-nR+>(+d%2jYUNu>-*OmaxXGDw63C?UBeA)BC)03`|`NRgY8gc7Jk z1SdK#fzUs|n>YIv&zptoMjV9{xB^uJ(j~08YCF;H(S1 z?kFXg1QP^2aL!MGuOsqs+$9vp9OzhwAbO3x&Qc(0aZn)PfV$urL+@pPVxS@zIi%w# zxuQdkX*lehO2A_K(nz@^7C14OKy=4RopkC)CAlGoOi~(-6QoWC48mF2Se#al!wCY^ z?!W;!?omiTy8!20$&HbVddrL6@6nG(3!ZRDAxK%UgTT0sV94f}s&Re@i=CgwV#nuD z>0*Zz(~^KWXbPH(Mp8p)F`8tHutZyiW>Y0Pq8qU~sTEz(byyxtaEh+wS{_f^@%89R zpnVBc#y60fNK4Tk-Gq*qrD(R)qLbEJsa#?xL!F$UBql(GGDwkI@-If6?;O$N?`L@h+6k~JfMO_6fSmu6`e)R_{X2V!{zU; z^MT^mZeGUqo7Zsp#~*uPp$z~4AOJ~3K~!;Y{{e*h_#;YZ!juUU;qUE=01pogNQlMp zojV|K2u6T(2!z_F5up7eeDtp&C~5{ds#(w#ErexQCE~}`A#r#K{_sQ{R-+FHil z6ob^t_A;#QDyRKrSi7>EhnufoMJ01nRIXc7fv%1P`1YH(v7);QFFrRJ6{QR8U}A&r zd(}4}{&z_I2NyPnP~DPdh?NG!#izk+jfBB$AQxpoRIG)-6@jSe2qea;k;4Kd2p~gp znS+vJaG{zY6^?v@R8d+aIjlHjuqL?_2|1=na!=7*V~59J!stXiGCmDYJ&=hfCZ}Wc z2rJTKC5SZzATc5ssWvff%a9r?MrOPW*|eS+B|>hj6q$5fMx2blo)RTNqE!rwK7bTR z3Pz+s#GCurth`1Ib5U}#>9V^ZWpC!Y2U6suWD2{B`Z-96gltmBP(Bmq-GCBFgglrc zDmt7%CxL|ARSf-}jvYTlcw*xxn49{QEjO;=+qX}lJ0TK($;rb9aT$0kAsMIi;rJqz z9^|1qoR_Kjn5YY}BAnIu;T;V@N+-f;{~&yqP=sS1Qi4!0j?2hR`2-PUg2_SA>kiCC z1(MuxL8!%F!?Jj^9tW4B9|6XXfI{oNx%=Mnh{LBL`8e;9Ot49y{YeC~R2*~6BuHft zq%!$BZ=z!^$ng%Ojx%66450NQQVs{yX$KbNmw{seLvd0*7N=C>a7-}<`~4Y^$UzBm z$?Xl}4(zN)5srEm;jn8yfiMO8$l2{AP;I8quu4#bCdUb=aC#hz9iFFhDr($cM-4%$ zF<>?a)Dq!5ER_%hgvBTinT1;AB5clBfmQL#xO?g$P<6+Udx|4aB@loTNlE0glF3CS z6Ht;G(UGteZSmxyl3UOm+kob{dMt~nLvutWmg*OyQ9d6{B7#jQ{hXj;RQt_B6&JZD zzxiBkvRbS%w4zl~OP_5qs>xkdlC!EJI8}PhAO|)ZRX#H@Gw5Y>j%&dWAO4dE46unm zuiRpgx?yadU&D_-T*AaL z+1P@u+d6r(Uq^ezJ&@{Zt3Xd@HNl5vkzj7B0UKB|AOVMoxv14$3|gfGr%J3^z8I^! z7n7r68PO}y(mWH}w-WqTRb%EGk7MZIOkDi>b6mS}iI{W+x7cK#tjX_pNd5m^ej3To zboKgm?AW^>`YMyJOc(55Uldd z7*=S;psYxY$&W+76b;hjf-$1M8RN-4jUP_18ezkPF)?^xOdKAWn1DwgNW{bUCt=cv zBow5OTZ$GVD^5y)Q_y?mhz$=!l*R*zdQTL@gdj6Uf+U*=X)#g)R4AguLtqUHhSeO3 z$Z!dP%07P-n;cQEW)6y^CP1m|QY#sxs8p$-BJrla-u@^CD2YUJTOyH&1Ii95Ha$rw zZ}MX>lJUN%06M1>wn!5qttJEodehI7;PJ;E;3*j+qF1|SJ6ig6dfbzo$Cr`XhPU z1BT#dzw8Pi&BQ|IS5fNrIu^RThGOT} zu-Ic3=6g;@sdyHi_MXi9pKg9Z596zM27xm`-5{~~#aIRR+6^|z=XG59@iLaTw?Ity zN04tI+#OsnZR!iyvwJ68T~r7ND1cafKm65G;H7#Vj_POOW19|DK^aUVYGE8&f#|#i zC>pW|)%6<*MjLo?SvEdu*Y4G5@2o**S1k{qXzw6kw~-5FYANUHY+){`4x865#RdY` z+U0Ck-bQR)OVH{e5G`k~ldED*Y6S~dFXQT2zK9&uJTxzP1N--MVEVL2@!Bg>@Wc0i zqucm;Z_^$tS=JVIU*F>D`+fWlssG2zPXT4uEqwgRr$|gqCrH@{QZcYbSr8o;#pi#D zjj&WXxukC8V`<*!$T8d@!$k1$0cF%*kn9JE^GV{8-^7aP?Rc1UYrnFQNie!EJIL5TzE zcCduqMg0q;?vhw6CP;k z>5S$`=Ar(*Hv4iTU6FDNMTswpu3JFpq9?{neDM5caAA?jOxvN+@XADlc6yb<_ z5e|h6#R1_^9F&Z}K`8;tuOALO#?$9YrO!p+6OF+c^?00CjKR;>i8)|7g*CO2|OF+g#}*KHO}R=i#P*0lqP>L6$!srlI7h`d1=8Z$2J$!HDD-5eDXjb4U%(w_qf#56{wL zOrahRk}G;-LI%c;h(y0!HIk$Kk)JNZhyh{brmUDWDvBU=N0Ua`FnUNBCJZxU>|i5C z571#ue+`CaiZLojNu>&fDN3Z+#7K)%BOyYHq)3+OT#QI#C_zewNQ(kSy#y**DAaNZ zbQ&deTID^EVwoZsq+}8?2hyD&3A=s@snF0+E(WMO`&j@*FzpLQP_PgE3>j2P5tQ^0 zj*qj#Xe6hm4nR5F3R*r>Egff(S;^+fM4JAdVEz9n~QM#bE&( z*HlQ`tUsxU&Od;QLF(vjNEK0;MaSphh~pp}@*9W~+R-?yWPlohqogCA**NHwgu~7` z*ymP2u5m05s7BzVb~sKehv7(YA%QFsdtBt$>Yyh`#S^58Q0p*(AoT>w9bQ0nAEW|j zquWqRU|NV~V+EQFWmuwLj23ehT1m~O3M@5Lq9v>bUA86;DAw219?^(ec?s61ccUw1 zDFKNOa7N zoRwef%h2~LLCmlkMC8sw)}U!v*0}@Qc5cPKgBz(_ft73M`!_GfmaWUNYSj{~WMEp+ zgf%@4ydP>MIV{!>#gfjhCr7oaqY~ZA7GXtODFLd2i#e$^Jr#CHt*FEba#hPaO3>4_ z7%x0C93Xqp+Lt>N=nMn!^$R_tx6p7)vCXDMxBDhQ>Cv@M)7(6sF4iAn` z#Na#)GUJ2D1qGrYO^lHPO_)S*dSF~E2h{ix7K|Mh&i9WPXvB#A1`N$pVL*x$L(n1FuO7HUpTNuwe#siD;}5u|iVXlS2ZYTgIM zpc5V*4zt<(Z(-H&I?*(V! zQ~dyZWgCWnCXU95AS?E}7zi*nIzFyf@dThGDpRRskjn1`RS}0%pX^P2gL*~o%4G;n z1&qZZ;V>LjkHmiYaO@Qg#lDb%IOvjzJ} zd~6wptu7L5a!`_@2vWJIbsSBAdXzx*3@Qi;RRoB-z&8jSkw^Nq$25|2BIwXDOU>1&BS+L^s6s<{85(UBSe96a)}*G}plT*aEsL(j zGHW%Obc<0Zo5!0IOMPBPng1JD=uM8v>ou8mQu#WS4eU${M zN~~U4g;k^;g4FKa9Vjh%8B<@Lj4wa`fW8KU6LV1P``Q7_D`>rt{;e(lC6Ky>E7w>% z^G!^jJ{>lj4Ov+kNKQ^bMrI0fGLtYMKLaBMXCO00hs;DZ!AF51`4JeDZ^Y0-C59JD zao-RNCXcY<(Xlp69-DxP!=o@TTSbrxLV8RHxgaqzVpYgWP$MTvgZxw-2IPdHFkOp$ zf)X1Kl^Lf%TBHt1VJajVB*+MpBh@5EqFIIb2pwY08bq1Z1Sut~1_dJYN`z@uFle;| zCng3d6_37CtNTEurgb~07;yTU^rQ?#ayAW#{B{6^QYI${#*Syve{v{P@_WvyR1vHc zc0e%~#mc|{PlVIwi;gxRD9{7e2qX6F+Q~6pMBM;sh**hzqcE-2_u+Ag57AHk>}j`r%XV}BfR8gN@DJqO{eXbeGV81_j<;-HM+BOi(F0sXMc zITzbp2jW1;5FAp`dt=ArXxtMq$~ufbksQxMbZ(-BKT*Jn7YY`OS4^K}=G}bmFH+>kqUGxa>Ooy*% z5In`h;UXCaH_c@DMZE}B?p&DiiebwxM)u&Ds9e&9{fD=shuqPU=0#Xe4ymW73G3Il zW7)ED-t@O_%`$X%)}pJmiX2-V)~{^DR)W{6u4?qOmyyF_!4hS>nQzSsa#1TQ2zFH@ z`u%n+#{2~@;I-Eu!}qKY>K1cQ*X+$px1Hnfkou1Qg+*Zjr8@`Un4FqfdG|= zq@)Do<)))3FAW0<(lLHy0Y(o=M@lp~2b&cA()AdSrNiJ{4Mz9ZVZtC29vU8oN5+_O z-v}cfoe+h`?u*4ACdFXVun6QOsS#}mfKBU*xUgVYG(J>@AkGwu7(*~(jiDq7NsUO2 z99EqaF%dFEMX+SCDh?=Hm>L#RI4`vXDO%TS5oXXsr_({Fw}VQpwnK_Z#hU%@f|RTm zOj0VDSPCSTU4o6BQqs1ck2j1)J!DE53}FVCBf?-Z8OWX4otjE5<8w?YWTBADLLij{ zLnaMEOmrBzDg~tCV2mC)41fOUeO$WyPh7ipt1s_RtAr0;Y2dmOD; zfX!Bd5&d2F8AI^CEtWu}z*$!zPB;hPlxrZ)c=+SArw2~Dvt~SBoN|)loRbl6^Za}g zf)opg@U;WV!I|Vhpc4>GNCFpfRGw7&5qv^uT}t~kRGMhpLW<#Ir0j|#C6QPYUOJAu zCS#W%nvT!M5e6rxemLk$StkHxXD(Kuijg?-wgIAt7*GwO*r5;PJ=eEQ*}Z!Y#ZMPQ3Sjm=I8XczQH zjo^NiIXsO84$q>*{Y5m$=3;q76*|Z@wOh-{L6xF2u?Ef2$5 zxfOd@2>QllSiNQ`x|fqy(C@0Pl7Q8K zww4NXv{hlvs-@`ZsfYhzKWdB>xpWu$j(jPi>1Su)0$j{G3W=0|k z@=`ElKqf{H%fX`$48eq98GI0Ksx5>-rNSU`OGEQjxPQ12J`Lq=hX)jR<-O zhDWLp8KHqSTn#HZENhqs;U;>`$kH%rVAhieQYO8=*YyyP$N{lDdkj(xPE36P6EY?S zDdwJ(Y~2nimRZ8z-v=zQY<_+Y!mVbzECe4Dom<1sA>~PD=TkSwh(`QXixkpqQIV;x6j2AQgMq>)z4W>zYCUAs{&q zATagEegYPo_I)2gYCl10pK}5BxfWuN#{is=Ou!N8I2_T8Cn!zAk?;xFs~?3!#xd9} z8HhvDVb~us07u2caEzSS5dzy$w|+S8T!iE9DcIv+z)mMC);MHgiC{b`1W$5E)rZVL z2RWfma|L<`CTnAxu!fC)imgI>Y$buFiX2q~cTsJr^zcn5U}ciK%Bn?cW-SL*Ybq1D ztz-r>a#_)JSQgc*=z7$d7Nc6d02Q+NXwWQ1B|&N-1C;-CEbwKZnvPPhmoeAnc`9dM zu`{_Sw`cK$+ekD`D8coM*KwWAi^(VFq-(WTwh7+`T5?kx*0o~Q$|iJo)nFCtry{q% zyt9(lYpE>fTDhVUYu46cY0Es!Tl5m@n@aHY#lPF{&!5&K**0eq`%bs++Wu`V{{xV^ z``^Dp>hh&4e5T%>?jA%$m@#(D5ESMoW9Wc%4DOdqZYl>)JUkfVhsGl>nT=iYM`5}M z{j(()lCQw1{%VXH7>1Dr76MfyCJfaRsI(YAOo9i-8t}*@3mzO7hRGAcF@87!ZI~Qm zhtlgq!!feRilS5#a+AaO#EEePs|br0W-9^6qCr@=3K8U}B25}Z83w$bbFu)7e2m_D$6NUOCIw~BePah*EMK#ph6CeImzntZ#wP!Xn>E&-e#e1d2=n(qh zbgTq#M+M_zTq@oQ)Zw6mH+M-V1c3x0PXdfH2hUM~E6JUpA|^mt@SaZsKJZV(`=qn( zCOSq)3I=mfhw1%?9LPyI1ae3nc9M}hlGA?uU4UXueJpSynSM?-@0ViE=Igl8I3wwY zy)Fef;KTqmpjWhy$%(0mYZqy!=KyT;?uYGx1F%Or6#KR0s5C=x#4sBBWP=G%j5ixQ6^mVt25fdtK!;!mY6XvBq2OsOa(NDw0k4yrnuB(I zDb_|cVHLTc&ZsJ^NMuoY1S=~WsJjH6sY}t8LHn|5(30J!I<#hyqsnYVdwLVvQ&}@# zZ$A{NnLt&gTZA%2G0J80P$HU*GSM6?2%L`j{%>HO50#{1=V!3U`xVS_n~FvLui*v1 zM^HXy0j~f3I>_+zracBJ4ye8qUwzpXdLhN05%)sM4xxXA)V*RM>uidP=U#jcPR>ra z|KW!)eB}KQIE2E>KLx=Ga#XU3aFsp+q2Z6v#La>(^9|TCpF;7xdQ_G##?qw~JdfYz zjjOS0Z3{MUTh4}-QRKL`j5DFodrm+i~O#S{X3-i@?Qa|KCuh1cbppO7X9Iyk(d~b{{2!g zYFHMA4@$?AevQuHWFGhujMB*yf!V%#7T zxhpl-*g*yYmDG`-DyeNB9QfVv%eP7X>=P*aeU1T~G0j?*e3 zxt;l3LdS}El%Pr>fiNV1i$N+h*q;CtXqV8xR{;F0M`2O3k zapStZf9j_<@4a`#FK@ZZqPf2R4qx=NV{2A4cFVkQP8*DmBf{{GM2j;XtnbOQx9`cj z7f!C^hMY(QEeAIoaSX&!XBp0UMdIDSRD2MUj`u@S@wQ(y&ih($)+Yi-ob)*6V!%mv z0}c_SPPm$T`=S!LgE}HeAxNc?GH5*m2c6?_Dkuwk-7~P;F`Had0XdyK>?WY>7UW=; zL-sw|PH@}ekb~X619+0zUF4W{%LZbzlHg_@gMHeO*drf`U7_@vZy|QO=aFm5BKYOg zXX%e~zJqYiKbIh-#}3CxtP$j4so(*W37$c@({m_udlBWne?)D_bo7K(k(+A7^5|-; zBxkfdwgC;w5^_L`uq=`uwy|aC$)|^F4!NgXQg%JsvU-)#U{@Nssw5WKSHlA=8q8&= z)-OVZVgX9U#aJ9V7bSr+vB2*Q%=LO5#a=Ajd@73FU&2g}7x04DWIP%12YmPGcVO`l zSLyP*!m3ie;JK5B^3L_y_x$dwp!B`Qn)LX4Zd}LBt5@v0&YZ~&T<@K|x-V<^)$3Pr z;?xN|_4Jd7iH<>j!9cnPW8oDL5AV?aa1I^?KiOlDMZOM0{ybRoreeyJc~~&F9Lt*P zv2Ir+8lwOJAOJ~3K~!A_wr^XD)vH^ueR~g&^jkqNYHzRNVo`sr&#Jp~3D&G?Avi6? zrcK>gPw#7JD8;mC&tlHpsn|gKzWx47{@Hm`W?z3>->rDJ*!R2}Qun;}>tFusAZ7nU z*(1}~^ymiCy=`k>uO*5}0_eDTU*YLc2ISFcHL@SXLu0X6w zjcC^YL{PF?RIo&<5FNp`HL#c&s5Be|MsiaoqYmL=2E<0$pw_T#5)y)x%szluMUG0T zhDym`W6v(ZnjBc)mCX&)N-?&CK(5oyw zm#&^G-{ZtHkD`-)FGmRY?`lkVCrF3$E-HeQlpw`2Mg)=b@g`^FPEN}i2OXVpz|n;u zA;1oT*Di8T`^aG(V)ID(gyR!sCO+2<#)W`fyi4$UJ1~Kc*J3X@sY3**LxKbn3q4Q3 zLB1VNWeSeCBxAQjH1@lsk*i`;YNTKfIjx-pG^VXw3G{OlX*-$9bZjS>ZE?!MR`-1D zAjh;r*bf_m30%s-*d-c-J>;Zz_~&Dn4}s1-7dstuu$Lv8bsvD^aetk;F2idrd`6z-w3{Bj;%2RW+t^aiwN^opERAEY|6o6wThfZFJCf>b4kRE>Ti%H+&R%|&U*94ri+jd^}E zZbNF0%S)K+_A+MsynrblbDB#KpQ&S#BQ*|Z&bp9-Uyv%alu?LEM92L{$AK83Nc4zj@m*3#2 zCmth6Y2e~1M35*60-t<%1WtgL;&J%LyaZFuD;PX@Dpq#x!19hRw6!*%sj&jfmQ`@~ z)ZI;xUtWhE0u^ia+qSI-yLPR^j;(94ch@GYThoQ|vUz-<{fswW#?d2taQ*uC49E1% zSjDmLnRiG33L^Hx?~wYBEv!Ct=ij}*Z`M!MHx}T=jXsNi4Hv)pj9(m+#uk!mB4;!z z7LQF%!9x=)cbVvpkm8WEoQu1g$4XumGC5fbBa$DrgOh$5MW*CeH=m~TN zqn=Ob$TFNOR8sDmlu{w&A|b>f1RnyCgdV;OQsR&h2!jJ5ldxt#KS;<)34=*NL8M@U zSRj25)<>p9aDXpzv(oVXyBF+9DzCF33U+hQ!=2wK_J8)j#xHMS6&ZeWvzwjOp1=DR zUvAlm!(&FFJwSwgeo}H68oU)0hx0y(IPGX8HzmS;K>)#oKqYXX2fYJ15JzlxaHI#n zJ2@XedeHk2u>1)u!SuSEAfv^82Xav^5j+*qegeS(K`beb6wlZ92?$CKG4xsjt;dmL zilX;NlVVBnJo)TSTHi+6LdR_-h;651b~&(p@dTDsY$8}~cFw{!uUza1%EMZpRBQ_% z*F^4Vt7i^268yG%W@49nHaRA8WR7__z#iu~bn^g;$N8Y% z0Kq^$ze)l`jr+@}_I?FT!ntT6*VGYFhW5xZEDK+RTIC#6NC{NN1!%UEpewNkEwPmZ zt2%V1H=#MX7R%D=(3P=-M+SB#H=;AU74=E=XpXMIGHVU$bc<0YEyf~(REe+{3xn>3 zREgg#%yEAn&k>;3Pp690kKiuqCfK#euE}e3zhC8pbNR58YxZXVzgAhrm3E#waTbe9 z7Guc3{#04iBgoGa?#>SIbQZwJ)dBvVF7R}7Mu4{`k`v?6+|)p1V@}InIph7a>^fo3 znH#tG*s8yM_BU+avK=q}aUSAR#v+g^BhH>la0|{wsG>hKiVQse)EhW_@EG2H`y4J@ zIE`~>kI`p2gmY((5_tAv|GpjMmU_r-)nQ3f4eD#l(9+z1jq6w8#IZy8^wW>&^Rv&< zI}cRfeb)p#y z81)1#Jqr_OE=mlYQU9l-+FAGs;fflS(t5{Ay z0u;F@xtL8QPY@y?G3O)-CdU-)0}(+g)ISg*1Rn;cKtDGG`Fjw!$VmmWV_6vhS!e*_ ztYJ{eg#=;BKcbr0-xa-|2qJ3aV42^^l->EJ=`%AMp*3M9bD z2wYl%jfrF-a7EptJq}C^N(@lZq)1XYy~jeX%0^`*oyUr84pwY&B)8+h(iue%m`vE_ z9EHtJaoFULM6ab`i$?~w2WDauL28qGIyO0FUXf>6h*}MpCwo-IO zSD+`h7M)QQJfLEELM@N_TTX!L%4kA+N<9ZurKT7q!33PpH_$|2s)?$_Qa0U(wE=aS za+Hb5Nrlcqsc_!Ckn)~}vfz1`=KVOHQI5r>Prl`md)IHVUKg`NdShONU8|AQ>sA6eSAdK!q zok{{<4|hB;aU8zA_%&}$xWPcd`x97Y7+lwH^bLc#$(!Cj{p53O+OQX$ZJSX%s{uv1 zqtGvZD8`JOfG3}P0@L4k6?10IK=B-c@q&d|P&^L}b@f=iaup8l+lQ~d_#CVP$f3vs zB6@!i4+61+_ooZH``F%peWiE#je+Uz<-ZP6eap{5>K0wFx2|5r58r-)PyT!wTQ}8X z+O$!a@`prB8gIhni3U9JkO|K|5{+jkC*!5ZvuHgPPdt!_M<>RT;_%S;7(Du5GRBUL zLjM6e3>+AS!Trq`RuGO6dEpqA8-e`HD5NEt5l^6si#8!9HVhU9C~{HEK^cr1=nV`? zR1$0qS{;l!7J9BC@F;0pNp6fvl@eNllu9avN+#t^fZ=8n0Wg{eS{Mm(EI5QiiuFw~ zNU_jz=Ae2ZB@GSXPD&I^4vGLJWe^Gsq8g+Z!O0!rbVJ5Q86gU>J1Q9qrU>;RPc1tnmkGl9h^ z23wpHvDrNl+XGUt#Wxk(eY3FJuMpeGDeVf#$Nr!q?DZ?cE{{AMC6{#~I1PJT5^>C{ z5C>e-@t$QYHn|cAT{F=k7*1~L5mdN5Nq~BR^hZ?sOhdhBHtOZG(QG1EMV9c8^o}TU zPPQ_160o|H>rfwGj^>ma0u;Haglbf2XOYt?L%C=c76ng7m5RBpS}ZZupxIc58U;D2 z(0RP6Z!wkkH1~PGj#=d7{@^hlpRRcqw=UZ&qBpOz{v!LZAlA@#jUaWM+|A8vxA4uo zU!vxfatw_afDC0atU?1~6;xrA%3+a&BAP0LgEJB^W%OV?JgNu}kL{0#Ck({H6NgYm zFcmhlnku0-f=mW7Gg9#5KytFu<1K9@w>ETt1acW*0^ z{pbATy5&FlcS!x*@?Q(7e_iYs4^fI%Sw>*i%){*SC0zW+2ROKYB~~pjMak?(@%)s2 zc;fynJUk{5lgGs4fl;xzZ-fmKhgmRYh#3P5Rmd-pAtOhE)O00M)3nG;CdHGpA}19a zsX#PKe-x!A^#)Ivbug2{2vT7t9l0kBL57JOkwyodTFa$Xs9?~j2_AY_%tnM8*?u`p zbgac>gn?kBl!#$8=(t!kAWNO3kVzmFh4MZq1}HfJip|K$6-tgOD2PPR3T6sKXdt<& z5FaGQhoc}TnchnP6Z-K|Mx~4eR0R4WD%{AM{*-ji!t6|Z@%O*d?~pb5v0CfhVd=k) ztDk)n*5k&X1MDGjjez{)k0koepMHvumoCHlyna|I(qfIL82kLSxDXPFbAA?j*hk}d zKnzZLMd6@p1ok@_u+K%0-OeiPbRiezroa|~0$Uw*1Q{KzYq7;aZI?hpV*89FHfAZD zq^D!mb`hwyIvB8-Mc+9Zv6&#XjYBI8XCygz-!hz>4}+8qTU_F?%`=gJl!6_8Y1rVN zjLlvd*yf#0u*%0t;YjRtEx=yKEF5r1z)^B#XXy3guDLkwmWelYLvSu^FgCc@u-+*X z&4RJ0a(Wn*PHbBFS5QIFs1BNeI@xSA>59=}u|ulWTE@pvb=nA4(Uqu;U5uLO5;VtG zpe?QvHKw_!ikOdu`k7qwmDA7?N&96B(WI(ErEmet$t{%y6=R;)49xSHNt%H<9(tL=3~O>{xIvMh_{)L zk{E;R^dwyT`tLlbqA$Hr-7Mg*{KAPNKUZ&UnN!QYMbjT05`r}1>{@MHZ?(2_n@$+|a?)Y}JFD=E3 z&rd*6e?1ENX^@>OLr$IodHEUwlpe`xM#Ll%j1n}6OJPlW3Pds42vRmZxgQoR@l!~t z2~bQL4k?3{fMke(Ej$ttEcH>Ci9ki~(UE&Hv8FvGB6RdVr9DL56lQ>y0LA*AWaONf zgHp=u&3t!5N-he5Ld2T;7^sAhhJ+Bb0?C<$@SK5BRxNB6Ex|2>yD4FiFFCG20$C6_ zu0T4M2&sv2REE%JGh)HqS+GNj#Zvv+A!RRFlhXBTRJpmqvQ>PGn-{;trBf$y;15&K zq|{)UgEzK0hG2uhpPZBmd&u?dqX+*1aw7-aEZFBBfj1Q?cw3!<^#T#rJBqR1K|+$= zB^kXY$3_CzMsi0R2~ZmeUYqEAn;f;+?4%<&>9B#ybxwLZUdNB!Bv9jkcN7lz#*>4J zq+?@A3E1YIihTju1koJq2_*QahZ3-Iu+u9KJKYPhlMM!TWmzoPl=4aVC~zRHXX3C+ z3Xb@v;p6lNu+86soo+Gca2SMY$B8I+cot<&ucE|tDoQ+FMy+@jYE|T>!WLm!LBV^MxlwfGm-(t_3NiGzDqWAw0Gx-TWjNQp;QYCN8Q<|$rrVPs+v#JvWG z!;0Slw?V-EV>98h=L?(ln$fFw_uaS5|4zO4(@){_JEZ>amtPR1>|KpFxY)nm=os#* zSPv`Hk$;=$!6phBp^O50*DuSS7R4QTO({xCnP{|-?dH4t(QU)THk59&2RES*!DJ7K}IYCUsf+d3Z45ZQ! z)&v*;xg-c$6*)4UoZc70;bfN|L_qgb;gE^_$r&2(=9@>sM)dcE z?%JDGe;rr9{XVw;>)Ym0qx|8(rgg9fkATf``p<80_03Z_HFG99t#N2}3C2ptAc9RO zwhF}LghT`p0upJHtCHM}8mk1sq#&$z2*w(RP?D%uVv>xc#9AgtIl)MYb&eYB_Kl@= z4eh6W4hlL(jnx7b)(T_C>j;gAE%woagO%EBpv)De2$L6=y3nlKLUSo&d`lL;%FQc>?Z5f$!F zpw?$97CJnKg-*|+%I`JQ3uj=7Vh-92CA^QS)l`Pourjoema;TR;pJ!yUyMrQBGiUa zshUUoD$o>GiB@9`>Ou)fA;nniHv{E?vr!Q+msE^4Sq;j3X7lt%EV1l#&*w18r9Zwq zeUU%&*{#X)@m=}x8ZLhD6_!q|#$@v-JfJ%6M?m6mBG4H03IY<7 zlHjC@kW5R8ze%46oJVep|gGWyNvj32e5O zqAg@DD#=Mzc}zu}->YZ}c>_yDv(Y4;gC)v^sFTk}jbc8QM>gv>os6+K-R+AHsoktFWx56x9o7 zpk~o@oIAP=e|wK$^uY;y{vn9~_1VX#@wbmo;IluU#9ux>gHJwq6aW17D|~(NBL48) z41{V%B4^k#WDM^?(ZlPJ`{V{>PFandhgKkGQa1)rIdEb#URbye?_9isf84l+E0g178J7WZbr_<8j$|MKj*{mM_4pHS(irtP=3{GuTBbIZ?N4F4xcvHdp) zR5x$_h;J@_j@tS%MA}TSMjFWhSqV}s%v{Tx_QFYeJ)0j&MsCRtDx*$`FatTLUa^!! z3~Etyj>LE?xi2+rRs$l#^)TyM9~Hq#C5K!lA}}#H*_-$n+<4x-+mH(2u8D~O%BYqh zI>N|7#8MuKg={PogA@Z#kUgT0WCxNRQsPiw-dDvV0co3okd2p;O8gNWt%F?dLyk~_ zo}Q(!H<{jPFaJ#~^mX}A2m1b(>ED(A#ol{>MS1M)|64$MXK73C9Rzz%Ofx4j)u@Py zVngg=@3AK)CYoZ}NwfD}KvBTnHI|rIqk@7+hh-PI|M$!;YI4r?d&~d#=KY-$pX<7> zd7gR7F1yQo=AQY^OhYB=OYhC7_||hCzVTXsuYKObGVU)Ww7v--hF zIE=O!g53*upXE&KbMWZsbuU3_6a)wBu+o{16fc9YjLk18CN3Kb}oJiuQeS(Q0Tm zqWi7F3!}cny6m4&BNkgUjWK*ubq%6LNBn;N5lVklObE8 z_!SE^j%I(Xn>VlGop;_qctkK^6)7iijgAVHZYrE03JDNP!uugCi0+CmO3c?M9FiD| z6cZVYgxD}dhZ9Kto(K=pB03^Kf-Jz_OTx$qB|0Zh0!l>g2?u4$6U}=PQZnGd0a}d+ zGq*wK_O0;R%g;-Q8E2OGM@V_NIvFGX1jwZ9pHSoM6yjccL`HfeKAuqZwn1yU#=?8I zVW<_&qWJ4o(Zt_dZ>-P=H&xpp=8j{rLx-))%f@Hg5UjP-V4c#EP_ZL)gqwN{sC5eG zKgdzac%Ad(Yo*)L$ZIwdN)k>4)@Fii3(sv=dl{P(m44WxjwFN{hY`UramaVbZ zygBmhpP|!AB&3?tO*JFLMA{DfRGn}@*%7(QHpo*FP^#8+SIuz3ED^`dVsYN5BMLht z(~*VKrFF(Pinp=Ud?*1m+61Zb_}GGu%62L~6tL3F#xnQ$_`qfgKCqjHFTCiU2&XT# zqTgvgmblKwR|M4O%wOz06CXOwz=x*vJ-aEGXEh1)ttQLG3>TWszyg~&Sl~Dt?^;ek zy5&F=pDspOK^fM)zY1^rbjA2C!?0k;M0`A9IyQX%H4g8|L`i-D^p%yUttf}Sq6`%e z?&D$pEfoItD=wZsgiWiy#^)c-!tBZEIKF=q?*4ib1y@d^>dq}x-M)+4SMK4_XE zf@=q!OV2{PZl7Y)o}XpppO~as3<7Q(6l`2{@hPPKjO{N9DbcmwC}U}*H;5*{N-S6~ z53VfyV`3xa>=9zECb9BuXrPc0Qjx*Nna`s_g9xi&+2<1$B$n*(K};ym(QOG>1qEo~ z>reM2`kF-49win&^pP?GO3H(fayLQB%?PO=9}l!nj74HhIQ)6Pn`pw5vr3pCC0vw? zlS%(Eq`W+x;qT{)__#p$`8y&a+#BOZrK7q`gylZ^u>Lxm9JkW&NRRs&_?xJHfTA6n z@V{Yw*_r?(z#|-$kbsi?6B5_h5=sJSJWtoOLFGvCm=gOEQglumC7|4}!^|JK z4spn85=Kb*W0yJvyOfdGt&BykErDa(lE6y9ZnFgAz+YukLM)CjYll_KKPnmupAmua*iyddsJOR^rO( z!zdw)YWTU=H&pY#QpZX|H4F_kFx1yTS6jyOxAEZi6`ai7g&&r^k0lGIV&3%eI7_#6 z{gw`^w%){`$$QW#brS-6tcQQnPJ|CUj-dW0;ot8hw0#ai(|r#D`s^d5vJlm851J0j zL`yoSX8m@eed-=`=%0no!|AS4cA;7CE$A|M8@dhn7I}xyqfSh@^axN-A@yf&e^E&N z@hdk54u~I~PFI7OGpECgPDhMr4=1ETg0%?t*CH}Bkf8E4K`PJ$sUSoURAQ1@(L5I} z!p)h6`FkUfkn$6o!bC#K%?Sa7R~W$< zEv8B$ur#8tNkGcY0-M5|DB-O`GHZjjJ`Cohw>0+;b=Z z03ZNKL_t)x427T84s8>naq8GnQ**{&h*Yu=;CC+pg~w6Gl&(f}kQ?NhRRuqMi%&G3 z_|e=EYgNuzt8~Oll>_$$h-?TYAtRiO`__b$t1;hlLL>|TcjY=nMA?P+sxnq}k zFb+EtELJgeIspWgkA&3@WiWOsL$O;OfnACS0xE`S4E7RUnd%s1s^ST$MC?iDsc9ZFtCSk7C z1kAA%kP@1JMb=aCfo47yYUW~!^L;(x&Y~=YbpG z{qhQg^*Ri{UMHX(coyC%M-e{w3?aqiK=+ePfgu6D#wJ03BcO!D za`GZHJb(%zltSDHED=WT>IQc@AUENF#FFrYjF=(9M`N5RLJZ0k(S1GwULImTJ{J?9 zoF$-y{unIb!{=xTI&tqmlLVH=j;ReI!#xol?16A^4Z6MjJPQi3*qSJ*{~}vsAz(Do zzmpudBNkT_(SIekv47%Nyl>@A1EkoCzvtV{@NEi=FB~0xA&Om0H3{tgtImKSC-HyTv#wMHnF^##2Sg z0mOSnaD^&XP9mGFjKhBB9Z*IghXBhVpmGQaeOA4q8OxcFUKLXCH`cRRrHx zCw!*rg+=NSSgc6LQpE^-q#T1qbU=&krV&K5u*7jDAvFyj6G%&)XGneGBJ!u>Bm1dX zU^O1^5l$btOvh~7@t8{&B{WBX$^@%<<|FaG%T&yC7>DUj=@{zz2KLVX4)X>M!=B}< zQT4D`_6yh4RLDWLI-#0!7-}m~ud70XnC{9@C%+5v(`?Wi@6xxqDPFIzSW`p*kDVZ+HVq6$PgXpLTuE-ENsxUuGx=M2NEn9uD?$MvAD=00|#A!bJc^0ICsES`G6gkRCVjJt;T3 zCLwo?m}FK2R)|^AolF51{|G4&At+rILFMP=f)34M5y!v9pU_&gU^4&an!nCJ-II{| z1FZDiuM!1NT`g*A9{?r4Vck1D@vVgyq2r7-bV_T~*4Uu7!#br6L1klfPqLX$1X#E} zg4AC75FFGb%KXQWa*z_i7@G;JtqKjciM}bN3wEeou|wr9LA8^h+N}sgrXrLIL#8rJ z&RQX*iZZ6m&*phCP16BIGfFi5wU&T7NQZThj_ZIb3|AuB;7-Cb*l!+y?Pjg;rRr@g zG8;lj4Z%l>G&-s=c+Z^Rv7CgD9cJKDmpM{Pos58Lgw)3l)9|6)6bY%1m?vhQm}QlL z**rD^Y8+-;W?+u>IDG6p74tnOVWRtByc;

(W2Krz5B0+Od=TfW<&5Jq(6w=m@1p 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 QXFDq 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[,,], + * enabling address to be processed in rules + * Fix plugin VType setting as SENSOR_TYPE_ULONG isn't needed here, only storing 0/1 state + * Make Interval optional, as with Event processing enabled, this state is not useful + * 2024-05 tonhuisman: Start changelog + */ + # include "src/Helpers/Dallas1WireHelper.h" # define PLUGIN_080 @@ -14,6 +23,8 @@ # define PLUGIN_NAME_080 "Input - iButton" # define PLUGIN_VALUENAME1_080 "iButton" +# define P080_ADDRESS_EVENT PCONFIG(0) +# define P080_EVENT_NAME "Address" boolean Plugin_080(uint8_t function, struct EventStruct *event, String& string) { @@ -25,7 +36,7 @@ boolean Plugin_080(uint8_t function, struct EventStruct *event, String& string) { Device[++deviceCount].Number = PLUGIN_ID_080; Device[deviceCount].Type = DEVICE_TYPE_SINGLE; - Device[deviceCount].VType = Sensor_VType::SENSOR_TYPE_ULONG; + Device[deviceCount].VType = Sensor_VType::SENSOR_TYPE_QUAD; Device[deviceCount].Ports = 0; Device[deviceCount].PullUpOption = false; Device[deviceCount].InverseLogicOption = false; @@ -33,6 +44,7 @@ boolean Plugin_080(uint8_t function, struct EventStruct *event, String& string) Device[deviceCount].ValueCount = 1; Device[deviceCount].SendDataOption = true; Device[deviceCount].TimerOption = true; + Device[deviceCount].TimerOptional = true; Device[deviceCount].GlobalSyncOption = true; break; } @@ -64,7 +76,11 @@ boolean Plugin_080(uint8_t function, struct EventStruct *event, String& string) if (validGpio(Plugin_080_DallasPin)) { Dallas_addr_selector_webform_load(event->TaskIndex, Plugin_080_DallasPin, Plugin_080_DallasPin); + + addFormCheckBox(F("Event with iButton address"), F("iaddr"), P080_ADDRESS_EVENT); + addFormNote(F("When checked, Device Address should be '- None -'")); } + success = true; break; } @@ -73,7 +89,8 @@ boolean Plugin_080(uint8_t function, struct EventStruct *event, String& string) { // save the address for selected device and store into extra tasksettings Dallas_addr_selector_webform_save(event->TaskIndex, CONFIG_PIN1, CONFIG_PIN1); - success = true; + P080_ADDRESS_EVENT = isFormItemChecked(F("iaddr")); + success = true; break; } @@ -85,6 +102,7 @@ boolean Plugin_080(uint8_t function, struct EventStruct *event, String& string) success = true; break; } + case PLUGIN_INIT: { const int8_t Plugin_080_DallasPin = CONFIG_PIN1; @@ -97,9 +115,17 @@ boolean Plugin_080(uint8_t function, struct EventStruct *event, String& string) pinMode(Plugin_080_DallasPin, INPUT); Dallas_plugin_get_addr(addr, event->TaskIndex); - Dallas_startConversion(addr, Plugin_080_DallasPin, Plugin_080_DallasPin); - delay(800); // give it time to do intial conversion + if (0 != addr[0]) { + Dallas_startConversion(addr, Plugin_080_DallasPin, Plugin_080_DallasPin); + + delay(800); // give it time to do intial conversion + } + + if (Settings.TaskDeviceTimer[event->TaskIndex] == 0) { // Trigger at least once a PLUGIN_READ + UserVar.setFloat(event->TaskIndex, 2, -1); + } + success = true; } break; @@ -107,20 +133,50 @@ boolean Plugin_080(uint8_t function, struct EventStruct *event, String& string) case PLUGIN_TEN_PER_SECOND: // PLUGIN_READ: { + const int8_t Plugin_080_DallasPin = CONFIG_PIN1; uint8_t addr[8]; Dallas_plugin_get_addr(addr, event->TaskIndex); - if (addr[0] != 0) { - const int8_t Plugin_080_DallasPin = CONFIG_PIN1; + if ((0x00 == addr[0]) && P080_ADDRESS_EVENT) { // Respond to any iButton presented? + uint32_t state = 0; + Dallas_reset(Plugin_080_DallasPin, Plugin_080_DallasPin); + Dallas_reset_search(); - if (Dallas_readiButton(addr, Plugin_080_DallasPin, Plugin_080_DallasPin)) - { - UserVar.setUint32(event->TaskIndex, 0, 1); - success = true; + while (Dallas_search(addr, Plugin_080_DallasPin, Plugin_080_DallasPin)) { + if (addr[0] == 0x01) { // Respond to first iButton device + state = 1; + break; + } } - else - { - UserVar.setUint32(event->TaskIndex, 0, 0); + + if (0x01 == addr[0]) { + if (state != UserVar.getFloat(event->TaskIndex, 0)) { + UserVar.setFloat(event->TaskIndex, 0, state); + eventQueue.add(event->TaskIndex, F(P080_EVENT_NAME), + strformat(F("%d,0x%s,0x%s"), // Address split in 2 hex parts + state, + formatToHex_array(addr, 4).c_str(), + formatToHex_array(&addr[4], 4).c_str())); + } + } else { + if (state != UserVar.getFloat(event->TaskIndex, 0)) { + UserVar.setFloat(event->TaskIndex, 0, state); + eventQueue.add(event->TaskIndex, F(P080_EVENT_NAME), + state); + } + } + + // No (debug) logging is added as the generated event is already logged at INFO level. + success = true; + addr[0] = 0; // Ignore other devices on the wire + } else + + if (0 != addr[0]) { + if (Dallas_readiButton(addr, Plugin_080_DallasPin, Plugin_080_DallasPin)) { + UserVar.setFloat(event->TaskIndex, 0, 1); + success = true; + } else { + UserVar.setFloat(event->TaskIndex, 0, 0); } Dallas_startConversion(addr, Plugin_080_DallasPin, Plugin_080_DallasPin); @@ -142,10 +198,10 @@ boolean Plugin_080(uint8_t function, struct EventStruct *event, String& string) } case PLUGIN_READ: { - success = UserVar.getUint32(event->TaskIndex, 0) != UserVar.getUint32(event->TaskIndex, 2); // Changed? + success = UserVar.getFloat(event->TaskIndex, 0) != UserVar.getFloat(event->TaskIndex, 2); // Changed? // Keep previous state - UserVar.setUint32(event->TaskIndex, 2, UserVar.getUint32(event->TaskIndex, 0)); + UserVar.setFloat(event->TaskIndex, 2, UserVar.getFloat(event->TaskIndex, 0)); break; } } From 16d7111eaed67cbbe2adfaf7588988dac9a8615c Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 11 May 2024 13:37:03 +0200 Subject: [PATCH 101/113] [DallasHelper] Reduce logging for Dallas_readiButton to on-change only --- src/src/Helpers/Dallas1WireHelper.cpp | 6 ++++-- src/src/Helpers/Dallas1WireHelper.h | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/src/Helpers/Dallas1WireHelper.cpp b/src/src/Helpers/Dallas1WireHelper.cpp index c8d55f7132..72211298d0 100644 --- a/src/src/Helpers/Dallas1WireHelper.cpp +++ b/src/src/Helpers/Dallas1WireHelper.cpp @@ -415,7 +415,7 @@ bool Dallas_readTemp(const uint8_t ROM[8], float *value, int8_t gpio_pin_rx, int } #ifdef USES_P080 -bool Dallas_readiButton(const uint8_t addr[8], int8_t gpio_pin_rx, int8_t gpio_pin_tx) +bool Dallas_readiButton(const uint8_t addr[8], int8_t gpio_pin_rx, int8_t gpio_pin_tx, int8_t lastState) { // maybe this is needed to trigger the reading // uint8_t ScratchPad[12]; @@ -461,7 +461,9 @@ bool Dallas_readiButton(const uint8_t addr[8], int8_t gpio_pin_rx, int8_t gpio_p found = true; } } - addLogMove(LOG_LEVEL_INFO, log); + if ((-1 == lastState) || (lastState != found)) { + addLogMove(LOG_LEVEL_INFO, log); + } return found; } diff --git a/src/src/Helpers/Dallas1WireHelper.h b/src/src/Helpers/Dallas1WireHelper.h index ac6e9ad48b..3771e73892 100644 --- a/src/src/Helpers/Dallas1WireHelper.h +++ b/src/src/Helpers/Dallas1WireHelper.h @@ -124,7 +124,8 @@ bool Dallas_readTemp(const uint8_t ROM[8], #ifdef USES_P080 bool Dallas_readiButton(const uint8_t addr[8], int8_t gpio_pin_rx, - int8_t gpio_pin_tx); + int8_t gpio_pin_tx, + int8_t lastState = -1); #endif #ifdef USES_P100 From 7047c73afb171d41cfad3d3b7125ef1ced40e9fd Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 11 May 2024 13:39:07 +0200 Subject: [PATCH 102/113] [P080] Remove unneeded Dallas_startConversion calls, reduce logging for Dallas_readiButton call --- src/_P080_DallasIButton.ino | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/_P080_DallasIButton.ino b/src/_P080_DallasIButton.ino index 49a3822af8..8039d90aad 100644 --- a/src/_P080_DallasIButton.ino +++ b/src/_P080_DallasIButton.ino @@ -8,6 +8,8 @@ // Maxim Integrated /** Changelog: + * 2024-05-11 tonhuisman: Dallas_StartConversion() call not needed for iButton. + * Reduce logging in Dallas_readiButton() function to on-change (only used for this plugin) * 2024-05-10 tonhuisman: Add support for Event with iButton address, * generating event: #Address=[,,], * enabling address to be processed in rules @@ -116,12 +118,6 @@ boolean Plugin_080(uint8_t function, struct EventStruct *event, String& string) Dallas_plugin_get_addr(addr, event->TaskIndex); - if (0 != addr[0]) { - Dallas_startConversion(addr, Plugin_080_DallasPin, Plugin_080_DallasPin); - - delay(800); // give it time to do intial conversion - } - if (Settings.TaskDeviceTimer[event->TaskIndex] == 0) { // Trigger at least once a PLUGIN_READ UserVar.setFloat(event->TaskIndex, 2, -1); } @@ -172,13 +168,12 @@ boolean Plugin_080(uint8_t function, struct EventStruct *event, String& string) } else if (0 != addr[0]) { - if (Dallas_readiButton(addr, Plugin_080_DallasPin, Plugin_080_DallasPin)) { + if (Dallas_readiButton(addr, Plugin_080_DallasPin, Plugin_080_DallasPin, UserVar.getFloat(event->TaskIndex, 0))) { UserVar.setFloat(event->TaskIndex, 0, 1); success = true; } else { UserVar.setFloat(event->TaskIndex, 0, 0); } - Dallas_startConversion(addr, Plugin_080_DallasPin, Plugin_080_DallasPin); # ifndef BUILD_NO_DEBUG From 2c8d5220ddaf89ff840d455a80f04ac96d3e9c7a Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 11 May 2024 13:39:56 +0200 Subject: [PATCH 103/113] [P080] Improved documentation --- docs/source/Plugin/P080.rst | 30 +++++++++++++++--- docs/source/Plugin/P080_events.repl | 6 ++-- docs/source/Plugin/P080_iButtonExamples.png | Bin 239573 -> 239388 bytes docs/source/Plugin/P080_iButtonReader1.png | Bin 0 -> 103648 bytes docs/source/Plugin/P080_iButtonReader2.png | Bin 0 -> 80423 bytes docs/source/Plugin/_plugin_sets_overview.repl | 27 ++++++---------- docs/source/Reference/Events.rst | 6 ++-- 7 files changed, 40 insertions(+), 29 deletions(-) create mode 100644 docs/source/Plugin/P080_iButtonReader1.png create mode 100644 docs/source/Plugin/P080_iButtonReader2.png diff --git a/docs/source/Plugin/P080.rst b/docs/source/Plugin/P080.rst index 52d86fffb1..279b294a5b 100644 --- a/docs/source/Plugin/P080.rst +++ b/docs/source/Plugin/P080.rst @@ -24,14 +24,34 @@ Used libraries: |P080_usedlibraries| Supported hardware ------------------ -|P080_usedby| +.. .. |P080_usedby| + +.. image:: P080_iButtonReader1.png + :width: 200px + +.. image:: P080_iButtonReader2.png + :width: 200px + +This type of reader is available at sites like Aliexpress and eBay. The wiring for these units can be somewhat confusing: + +.. code-block:: none + + ESP iButton (4 wires) + GPIO <--> 1-wire/D (green) with 1k..10k Pull-up to VCC + GND <--> GND (red) + + ESP Resistor LED + VCC <--> Anode (black) + GPIO <--> 470 ohm <--> Cathode (white) + +The value for the pull-up resistor on the 1-wire GPIO pin depends somewhat, sometimes, 10k doesn't work reliable, especially when using longer wires (> 2m), then lowering to 4k7 or 2k2 usually fixes that. For really problematic installations with high (electrical) noise levels, a 1k resistor may be needed to make it work reliable. Description ----------- -The iButton, developed by Dallas, now Maxim, is a uniquely coded key or button, that can be used for access control or similar identity checks. They use RFID technology to transfer their ID to the receiver, once in close proximity of the receiver. +The iButton, developed by Dallas, now Maxim, is a coded key or button, that can be used for access control or similar identity checks. They use RFID technology to transfer their ID to the receiver, once in close proximity of the receiver. -The iButtons often come in the shape shown in the image, and can be added to a keychain for easy access and use. +The iButtons often come in the shape shown in the image below, and can be attached to a keyring for easy access and use. These buttons are available as read-only iButtons, and as rewritable iButtons, where the user can change the ID of the button. Both types can be read by ESPEasy, but ESPEasy does not provide tools or features to write an ID to the rewritable buttons, separate tools for that can be obtained elsewhere. @@ -52,14 +72,14 @@ Configuration Sensor ^^^^^^ -* **GPIO 1-Wire**: The reader only needs a single GPIO pin and GND to be connected. The 1-Wire hardware configured in ESPEasy requires a ca. 4k7 ohm (range 1k..10k depending on wire-length) pull-up resistor between VCC (3.3V) and the GPIO pin. Internal pull-up of the ESP is not sufficient! +* **GPIO 1-Wire**: The reader only needs a single GPIO pin (and GND) to be connected. The 1-Wire hardware configured in ESPEasy requires a ca. 4k7 ohm (range 1k..10k depending on wire-length) pull-up resistor between VCC (3.3V) and the GPIO pin. Internal pull-up of the ESP is not sufficient! Any additional wires on the receiver unit mostly are used for indicator leds, that can optionally be controlled using standard GPIO commands from rules. (Don't forget a resistor to limit the current...) Device Settings ^^^^^^^^^^^^^^^ -* **Device Address**: Select the desired device ID that the plugin should respond to. This requires the device to be enabled, and an iButton in contact with the receiver when opening the device settings page! +* **Device Address**: Select the desired device ID that the plugin should respond to. This requires the device to be enabled, and an iButton in contact with the receiver when opening the device settings page! When set, the task will only respond to this iButton ID, when read. * **Event with iButton address**: With this option enabled, and the **Device Address** selection left to ``- None -``, instead of responding to a single iButton, an event is generated for any iButton that is recognized by the receiver. And once removed, the same event is generated without the iButton address. This can be processed in rules. See below in the **Events** chapter for a more detailed description. diff --git a/docs/source/Plugin/P080_events.repl b/docs/source/Plugin/P080_events.repl index 935be482d5..565fe6b420 100644 --- a/docs/source/Plugin/P080_events.repl +++ b/docs/source/Plugin/P080_events.repl @@ -3,17 +3,17 @@ :widths: 30, 30 " - ``#Address,[,,]`` + ``#Address=[,,]`` ````: 0 or 1, depending on wether the iButton is on the receiver (1) or not (0). - ````: The high 4 bytes of the (64 bit) iButton address in hexadecimal presentation. The address parts are only included if the iButton is on the receiver. + ````: The high 4 bytes of the (64 bit) iButton address in hexadecimal presentation, with a ``0x`` prefix. The address parts are only included if the iButton is detected by the receiver. ````: The low 4 bytes of the iButton address in hexadecimal presentation. "," - This event is generated when the **Event with iButton address** setting is enabled, and no **Device Address** is selected (None). + This event is generated when the **Event with iButton address** setting is *enabled*, and no **Device Address** is selected (*None*). As the iButton address is in fact a 64 bit value (though only 57 bits are used), this value is too large to be stored in a regular variable in the rules, so they are split into 2 parts, as often sets of iButtons are produced in the same batch, where the higher bits are the same, and only the lower bits vary. diff --git a/docs/source/Plugin/P080_iButtonExamples.png b/docs/source/Plugin/P080_iButtonExamples.png index 5ff56bf4967359cdf0cc1ac36d52f30b1fd72764..85d983e32fcae185d6d35b52b5381ee71116ed7d 100644 GIT binary patch delta 47 zcmcb5o^Q@Mz6su%FU9D?UIZj%(PvSk$KND0Fgls Ar~m)} delta 229 zcmbPpj_>Muz6sv-M@w8IN?cNllZ!G7N;32F7?LxJ@^e#pxeN_-4NP^742(<^bQBCN zt&GjBj4VOI3c)3%*~JP$<%X7)1_~ajMa4iZnOSK`iAAXjp4oZ%1q$AYiA5zk3L&`) zA&Kc=8Y~WE<(DRBDEOx4<`-2ec&AqC_$ve_mgXe_B~nX@Q}c8byz>h(Q&Wo+ToQ8< zi;ERpib1vj<#QplOHqD7aYklAL1tdMg06yletvpRs)94v&7wAMcpDR16Bt_)n6@S` H?^y-_+R025 diff --git a/docs/source/Plugin/P080_iButtonReader1.png b/docs/source/Plugin/P080_iButtonReader1.png new file mode 100644 index 0000000000000000000000000000000000000000..bcbf245f090c97ebdc9fa5037fe64fbce1c8474c GIT binary patch literal 103648 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

Upload settings file:

")); - sendHeadandTail_stdtemplate(_TAIL); - TXBuffer.endStream(); - printWebString = String(); - printToWeb = false; -} - -// ******************************************************************************** -// Web Interface upload page -// ******************************************************************************** -void handle_upload_post() { - # ifndef BUILD_NO_RAM_TRACKER - checkRAM(F("handle_upload_post")); - # endif // ifndef BUILD_NO_RAM_TRACKER - - if (!isLoggedIn()) { return; } - - navMenuIndex = MENU_INDEX_TOOLS; - TXBuffer.startStream(); - sendHeadandTail_stdtemplate(_HEAD); - - switch (uploadResult) { - case uploadResult_e::Success: - case uploadResult_e::SuccessReboot: - addHtml(F("Upload OK!
")); - - if (uploadResult_e::SuccessReboot == uploadResult) { // Enable string de-duplication - addHtml(F("")); - addHtml(F("You REALLY need to reboot to apply all settings...")); - addHtml(F(" ")); - addWideButton(F("/?cmd=reboot"), F("Reboot now")); - addHtml(F("
")); - } else { - addHtml(F("You may")); - addHtml(F(" need to reboot to apply all settings...")); - } - LoadSettings(); - break; - case uploadResult_e::InvalidFile: - addHtml(F("")); - addHtml(F("Upload file invalid!")); - addHtml(F("
")); - break; - case uploadResult_e::NoFilename: - addHtml(F("")); - addHtml(F("No filename!")); - addHtml(F("
")); - break; - case uploadResult_e::UploadStarted: - break; - } - - addHtml(F("Upload finished")); - sendHeadandTail_stdtemplate(_TAIL); - TXBuffer.endStream(); - printWebString = String(); - printToWeb = false; -} - -# ifdef WEBSERVER_NEW_UI -void handle_upload_json() { - # ifndef BUILD_NO_RAM_TRACKER - checkRAM(F("handle_upload_post")); - # endif // ifndef BUILD_NO_RAM_TRACKER - uint8_t result = static_cast(uploadResult); - - if (!isLoggedIn()) { result = 255; } - - TXBuffer.startJsonStream(); - addHtml('{'); - stream_next_json_object_value(F("status"), result); - addHtml('}'); - - TXBuffer.endStream(); -} - -# endif // WEBSERVER_NEW_UI - -// ******************************************************************************** -// Web Interface upload handler -// ******************************************************************************** -fs::File uploadFile; -# if FEATURE_TARSTREAM_SUPPORT -TarStream *tarStream = nullptr; -# endif // if FEATURE_TARSTREAM_SUPPORT -void handleFileUpload() { - # ifndef BUILD_NO_RAM_TRACKER - checkRAM(F("handleFileUpload")); - # endif // ifndef BUILD_NO_RAM_TRACKER - handleFileUploadBase(false); -} - -# if FEATURE_SD -void handleSDFileUpload() { - # ifndef BUILD_NO_RAM_TRACKER - checkRAM(F("handleSDFileUpload")); - # endif // ifndef BUILD_NO_RAM_TRACKER - handleFileUploadBase(true); -} - -# endif // if FEATURE_SD - -void handleFileUploadBase(bool toSDcard) { - if (!isLoggedIn()) { return; } - - static boolean valid = false; - bool receivedConfigDat = false; - const FileDestination_e destination = toSDcard ? FileDestination_e::SD : FileDestination_e::ANY; - - HTTPUpload& upload = web_server.upload(); - - if (upload.filename.isEmpty()) - { - uploadResult = uploadResult_e::NoFilename; - return; - } - - if (upload.status == UPLOAD_FILE_START) - { - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("Upload: START, filename: "), upload.filename)); - } - valid = false; - uploadResult = uploadResult_e::UploadStarted; - } - else if (upload.status == UPLOAD_FILE_WRITE) - { - // first data block, if this is the config file, check PID/Version - if (upload.totalSize == 0) - { - if (matchFileType(upload.filename, FileType::CONFIG_DAT)) { - valid = validateUploadConfigDat(upload.buf); - receivedConfigDat = true; - } else { - // other files are always assumed valid... - valid = true; - } - - if (valid) { - # if FEATURE_TARSTREAM_SUPPORT - - if ((upload.filename.length() > 3) && upload.filename.substring(upload.filename.length() - 4).equalsIgnoreCase(F(".tar"))) { - tarStream = new TarStream(upload.filename, destination); - # ifndef BUILD_NO_DEBUG - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("Upload: TAR Processing .tar file: "), upload.filename)); - } - # endif // ifndef BUILD_NO_DEBUG - } else - # endif // if FEATURE_TARSTREAM_SUPPORT - { - // once we're safe, remove file and create empty one... - tryDeleteFile(upload.filename, destination); - uploadFile = tryOpenFile(upload.filename, F("w"), destination); - } - - // dont count manual uploads: flashCount(); - } - } - - # if FEATURE_TARSTREAM_SUPPORT - - if (nullptr != tarStream) { - tarStream->write(upload.buf, upload.currentSize); - } else - # endif // if FEATURE_TARSTREAM_SUPPORT - { - if (uploadFile) { uploadFile.write(upload.buf, upload.currentSize); } - } - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("Upload: WRITE, Bytes: "), upload.currentSize)); - } - } - else if (upload.status == UPLOAD_FILE_END) - { - # if FEATURE_TARSTREAM_SUPPORT - - if (nullptr != tarStream) { - tarStream->flush(); - # if FEATURE_EXTENDED_CUSTOM_SETTINGS && FEATURE_UPLOAD_CLEANUP_CONFIG - - // If we have received a valid config.dat, and extended custom settings is available, - // delete all NOT included extcfg.dat files - // FIXME Keep this feature enabled ??? (the extra files _can't_ be deleted manually) - if (tarStream->isFileIncluded(getFileName(FileType::CONFIG_DAT))) { // Is config.dat included? - receivedConfigDat = true; - - for (uint8_t n = 0; n < TASKS_MAX; n++) { - const String extcfgFilename = SettingsType::getSettingsFileName(SettingsType::Enum::CustomTaskSettings_Type, n); - - if (!tarStream->isFileIncluded(extcfgFilename) && - tryDeleteFile(extcfgFilename) && loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("Upload: Removing not included extended settings: "), extcfgFilename)); - } - } - } - # endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS && FEATURE_UPLOAD_CLEANUP_CONFIG - delete tarStream; - } else - # endif // if FEATURE_TARSTREAM_SUPPORT - { - if (uploadFile) { uploadFile.close(); } - } - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("Upload: END, Size: "), upload.totalSize)); - } - } - - if (valid) { - if (receivedConfigDat) { - uploadResult = uploadResult_e::SuccessReboot; - } else { - uploadResult = uploadResult_e::Success; - } - } - else { - uploadResult = uploadResult_e::InvalidFile; - } -} - -#endif // ifdef WEBSERVER_UPLOAD +#include "../WebServer/UploadPage.h" + +#include "../WebServer/ESPEasy_WebServer.h" +#include "../WebServer/AccessControl.h" +#include "../WebServer/Markup_Buttons.h" +#include "../WebServer/HTML_wrappers.h" + +#include "../Globals/Cache.h" +#include "../Helpers/ESPEasy_Storage.h" +#if FEATURE_TARSTREAM_SUPPORT +# include "../Helpers/TarStream.h" +#endif // if FEATURE_TARSTREAM_SUPPORT +#include "../../ESPEasy-Globals.h" + + +#ifdef WEBSERVER_UPLOAD + +# ifndef FEATURE_UPLOAD_CLEANUP_CONFIG +# define FEATURE_UPLOAD_CLEANUP_CONFIG 1 // Enable/Disable removing of extcfg.dat files when a .tar file is uploaded having + // a valid config.dat included +# endif // ifndef FEATURE_UPLOAD_CLEANUP_CONFIG + +// ******************************************************************************** +// Web Interface upload page +// ******************************************************************************** +uploadResult_e uploadResult = uploadResult_e::UploadStarted; + +void handle_upload() { + if (!isLoggedIn()) { return; } + navMenuIndex = MENU_INDEX_TOOLS; + TXBuffer.startStream(); + sendHeadandTail_stdtemplate(_HEAD); + + addHtml(F( + "

Upload settings file:

")); + sendHeadandTail_stdtemplate(_TAIL); + TXBuffer.endStream(); + printWebString = String(); + printToWeb = false; +} + +// ******************************************************************************** +// Web Interface upload page +// ******************************************************************************** +void handle_upload_post() { + # ifndef BUILD_NO_RAM_TRACKER + checkRAM(F("handle_upload_post")); + # endif // ifndef BUILD_NO_RAM_TRACKER + + if (!isLoggedIn()) { return; } + + navMenuIndex = MENU_INDEX_TOOLS; + TXBuffer.startStream(); + sendHeadandTail_stdtemplate(_HEAD); + + switch (uploadResult) { + case uploadResult_e::Success: + case uploadResult_e::SuccessReboot: + addHtml(F("Upload OK!
")); + + if (uploadResult_e::SuccessReboot == uploadResult) { // Enable string de-duplication + addHtml(F("")); + addHtml(F("You REALLY need to reboot to apply all settings...")); + addHtml(F(" ")); + addWideButton(F("/?cmd=reboot"), F("Reboot now")); + addHtml(F("
")); + } else { + addHtml(F("You may")); + addHtml(F(" need to reboot to apply all settings...")); + } + LoadSettings(); + break; + case uploadResult_e::InvalidFile: + addHtml(F("")); + addHtml(F("Upload file invalid!")); + addHtml(F("
")); + break; + case uploadResult_e::NoFilename: + addHtml(F("")); + addHtml(F("No filename!")); + addHtml(F("
")); + break; + case uploadResult_e::UploadStarted: + break; + } + + addHtml(F("Upload finished")); + sendHeadandTail_stdtemplate(_TAIL); + TXBuffer.endStream(); + printWebString = String(); + printToWeb = false; +} + +# ifdef WEBSERVER_NEW_UI +void handle_upload_json() { + # ifndef BUILD_NO_RAM_TRACKER + checkRAM(F("handle_upload_post")); + # endif // ifndef BUILD_NO_RAM_TRACKER + uint8_t result = static_cast(uploadResult); + + if (!isLoggedIn()) { result = 255; } + + TXBuffer.startJsonStream(); + addHtml('{'); + stream_next_json_object_value(F("status"), result); + addHtml('}'); + + TXBuffer.endStream(); +} + +# endif // WEBSERVER_NEW_UI + +// ******************************************************************************** +// Web Interface upload handler +// ******************************************************************************** +fs::File uploadFile; +# if FEATURE_TARSTREAM_SUPPORT +TarStream *tarStream = nullptr; +# endif // if FEATURE_TARSTREAM_SUPPORT +void handleFileUpload() { + # ifndef BUILD_NO_RAM_TRACKER + checkRAM(F("handleFileUpload")); + # endif // ifndef BUILD_NO_RAM_TRACKER + handleFileUploadBase(false); +} + +# if FEATURE_SD +void handleSDFileUpload() { + # ifndef BUILD_NO_RAM_TRACKER + checkRAM(F("handleSDFileUpload")); + # endif // ifndef BUILD_NO_RAM_TRACKER + handleFileUploadBase(true); +} + +# endif // if FEATURE_SD + +void handleFileUploadBase(bool toSDcard) { + if (!isLoggedIn()) { return; } + + static boolean valid = false; + bool receivedConfigDat = false; + const FileDestination_e destination = toSDcard ? FileDestination_e::SD : FileDestination_e::ANY; + + HTTPUpload& upload = web_server.upload(); + + if (upload.filename.isEmpty()) + { + uploadResult = uploadResult_e::NoFilename; + return; + } + + if (upload.status == UPLOAD_FILE_START) + { + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, concat(F("Upload: START, filename: "), upload.filename)); + } + valid = false; + uploadResult = uploadResult_e::UploadStarted; + } + else if (upload.status == UPLOAD_FILE_WRITE) + { + // first data block, if this is the config file, check PID/Version + if (upload.totalSize == 0) + { + if (matchFileType(upload.filename, FileType::CONFIG_DAT)) { + valid = validateUploadConfigDat(upload.buf); + receivedConfigDat = true; + } else { + // other files are always assumed valid... + valid = true; + } + + if (valid) { + # if FEATURE_TARSTREAM_SUPPORT + + if ((upload.filename.length() > 3) && upload.filename.substring(upload.filename.length() - 4).equalsIgnoreCase(F(".tar"))) { + tarStream = new (std::nothrow) TarStream(upload.filename, destination); + # ifndef BUILD_NO_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, concat(F("Upload: TAR Processing .tar file: "), upload.filename)); + } + # endif // ifndef BUILD_NO_DEBUG + } else + # endif // if FEATURE_TARSTREAM_SUPPORT + { + // once we're safe, remove file and create empty one... + tryDeleteFile(upload.filename, destination); + uploadFile = tryOpenFile(upload.filename, F("w"), destination); + } + + // dont count manual uploads: flashCount(); + } + } + + # if FEATURE_TARSTREAM_SUPPORT + + if (nullptr != tarStream) { + tarStream->write(upload.buf, upload.currentSize); + } else + # endif // if FEATURE_TARSTREAM_SUPPORT + { + if (uploadFile) { uploadFile.write(upload.buf, upload.currentSize); } + } + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, concat(F("Upload: WRITE, Bytes: "), upload.currentSize)); + } + } + else if (upload.status == UPLOAD_FILE_END) + { + # if FEATURE_TARSTREAM_SUPPORT + + if (nullptr != tarStream) { + tarStream->flush(); + # if FEATURE_EXTENDED_CUSTOM_SETTINGS && FEATURE_UPLOAD_CLEANUP_CONFIG + + // If we have received a valid config.dat, and extended custom settings is available, + // delete all NOT included extcfg.dat files + // FIXME Keep this feature enabled ??? (the extra files _can't_ be deleted manually) + if (tarStream->isFileIncluded(getFileName(FileType::CONFIG_DAT))) { // Is config.dat included? + receivedConfigDat = true; + + for (uint8_t n = 0; n < TASKS_MAX; n++) { + const String extcfgFilename = SettingsType::getSettingsFileName(SettingsType::Enum::CustomTaskSettings_Type, n); + + if (!tarStream->isFileIncluded(extcfgFilename) && + tryDeleteFile(extcfgFilename) && loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, concat(F("Upload: Removing not included extended settings: "), extcfgFilename)); + } + } + } + # endif // if FEATURE_EXTENDED_CUSTOM_SETTINGS && FEATURE_UPLOAD_CLEANUP_CONFIG + delete tarStream; + } else + # endif // if FEATURE_TARSTREAM_SUPPORT + { + if (uploadFile) { uploadFile.close(); } + } + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, concat(F("Upload: END, Size: "), upload.totalSize)); + } + } + + if (valid) { + if (receivedConfigDat) { + uploadResult = uploadResult_e::SuccessReboot; + } else { + uploadResult = uploadResult_e::Success; + } + } + else { + uploadResult = uploadResult_e::InvalidFile; + } +} + +#endif // ifdef WEBSERVER_UPLOAD From 3d15092ea7a2d2f46232955210da11cb15f672c0 Mon Sep 17 00:00:00 2001 From: TD-er Date: Thu, 23 May 2024 23:11:10 +0200 Subject: [PATCH 112/113] [ESP-IDF5.1] Fix WiFi issues on ESP32-C6 --- platformio_core_defs.ini | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/platformio_core_defs.ini b/platformio_core_defs.ini index 355a71c694..380ce36824 100644 --- a/platformio_core_defs.ini +++ b/platformio_core_defs.ini @@ -208,8 +208,12 @@ lib_ignore = ; ESP_IDF 5.1 [core_esp32_IDF5_1__3_0_0] -platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.04.14/platform-espressif32.zip -platform_packages = framework-arduinoespressif32 @ https://github.com/Jason2866/esp32-arduino-lib-builder/releases/download/2334/framework-arduinoespressif32-all-release_v5.1-08e138f.zip +;platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.04.11/platform-espressif32.zip +;platform_packages = framework-arduinoespressif32 @ https://github.com/Jason2866/esp32-arduino-lib-builder/releases/download/2286/framework-arduinoespressif32-all-release_v5.1-11140aa.zip +;platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.04.14/platform-espressif32.zip +;platform_packages = framework-arduinoespressif32 @ https://github.com/Jason2866/esp32-arduino-lib-builder/releases/download/2386/framework-arduinoespressif32-all-release_v5.1-324fdc1.zip +platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.05.11/platform-espressif32-2024.05.11.zip +platform_packages = framework-arduinoespressif32 @ https://github.com/Jason2866/esp32-arduino-lib-builder/releases/download/2404/framework-arduinoespressif32-all-release_v5.1-fa138fe.zip build_flags = -DESP32_STAGE -DESP_IDF_VERSION_MAJOR=5 -DLIBRARIES_NO_LOG=1 From 1aa62eaf1ff8716d392dfd21ebf5c1bd56503d07 Mon Sep 17 00:00:00 2001 From: TD-er Date: Wed, 29 May 2024 23:19:01 +0200 Subject: [PATCH 113/113] [ESP-IDF5.1] Revert Arduino PR 9453 WiFiClient - rename flush() to clear() --- platformio_core_defs.ini | 8 ++++++-- platformio_esp32_solo1.ini | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/platformio_core_defs.ini b/platformio_core_defs.ini index 355a71c694..75e04c8086 100644 --- a/platformio_core_defs.ini +++ b/platformio_core_defs.ini @@ -208,8 +208,12 @@ lib_ignore = ; ESP_IDF 5.1 [core_esp32_IDF5_1__3_0_0] -platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.04.14/platform-espressif32.zip -platform_packages = framework-arduinoespressif32 @ https://github.com/Jason2866/esp32-arduino-lib-builder/releases/download/2334/framework-arduinoespressif32-all-release_v5.1-08e138f.zip +;platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.04.11/platform-espressif32.zip +;platform_packages = framework-arduinoespressif32 @ https://github.com/Jason2866/esp32-arduino-lib-builder/releases/download/2286/framework-arduinoespressif32-all-release_v5.1-11140aa.zip +;platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.04.14/platform-espressif32.zip +;platform_packages = framework-arduinoespressif32 @ https://github.com/Jason2866/esp32-arduino-lib-builder/releases/download/2386/framework-arduinoespressif32-all-release_v5.1-324fdc1.zip +platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.05.13/platform-espressif32.zip +platform_packages = build_flags = -DESP32_STAGE -DESP_IDF_VERSION_MAJOR=5 -DLIBRARIES_NO_LOG=1 diff --git a/platformio_esp32_solo1.ini b/platformio_esp32_solo1.ini index 0886c119dd..98870fed5c 100644 --- a/platformio_esp32_solo1.ini +++ b/platformio_esp32_solo1.ini @@ -18,7 +18,8 @@ build_unflags = ${esp32_base.build_unflags} ; IDF 5.1.2 [esp32_solo1_common_LittleFS] extends = esp32_base_idf5 -;platform_packages = framework-arduino-solo1 @ https://github.com/Jason2866/esp32-arduino-lib-builder/releases/download/2038/framework-arduinoespressif32-solo1-release_v5.1-246cad0.zip +platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.05.13/platform-espressif32.zip +platform_packages = build_flags = ${esp32_base_idf5.build_flags} -DFEATURE_ARDUINO_OTA=1 -DUSE_LITTLEFS

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 E0U^ZTCjbBd literal 0 HcmV?d00001 diff --git a/docs/source/Plugin/P080_iButtonReader2.png b/docs/source/Plugin/P080_iButtonReader2.png new file mode 100644 index 0000000000000000000000000000000000000000..767d409e2954285a58414a0433601331d34fe64e GIT binary patch literal 80423 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ξ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 Date: Sat, 11 May 2024 20:47:16 +0200 Subject: [PATCH 104/113] [P004] Add support for Get Config Value to retrieve sensor statistics --- docs/source/Plugin/P004.rst | 7 +++ docs/source/Plugin/P004_DS18b20.rst | 5 +- docs/source/Plugin/P004_config_values.repl | 46 ++++++++++++++ src/_P004_Dallas.ino | 71 +++++++++++++++++++--- src/src/PluginStructs/P004_data_struct.h | 6 ++ 5 files changed, 123 insertions(+), 12 deletions(-) create mode 100644 docs/source/Plugin/P004_config_values.repl diff --git a/docs/source/Plugin/P004.rst b/docs/source/Plugin/P004.rst index ba8ee3f3a5..732e2dfc83 100644 --- a/docs/source/Plugin/P004.rst +++ b/docs/source/Plugin/P004.rst @@ -39,6 +39,13 @@ Supported hardware .. .. include:: P004_events.repl +Get Config Values +----------------- + +Get Config Values retrieves values or settings from the sensor or plugin, and can be used in Rules, Display plugins, Formula's etc. The square brackets **are** part of the variable. Replace ```` by the **Name** of the task. + +.. include:: P004_config_values.repl + Change log ---------- diff --git a/docs/source/Plugin/P004_DS18b20.rst b/docs/source/Plugin/P004_DS18b20.rst index 9a10a7a7ac..9253470fef 100644 --- a/docs/source/Plugin/P004_DS18b20.rst +++ b/docs/source/Plugin/P004_DS18b20.rst @@ -227,7 +227,8 @@ Data Acquisition * **Interval**: How often should the task publish its value (5..15 seconds is normal). Values -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^ + * **Name**: Value Name of temperature indicator. * **Formula**: Optional math conversion. The measured temperature defaults to Celsius. It can be converted to Fahrenheit by entering this formula: @@ -251,7 +252,7 @@ Rules examples .. code-block:: none On Temperature1#Celsius Do - If [Temperature1#Celsius]>37 + If %eventvalue1%>37 NeoPixelAll,255,0,0 //Your body temperature is too high! Else NeoPixelAll,0,255,0 //Body temperature is OK. diff --git a/docs/source/Plugin/P004_config_values.repl b/docs/source/Plugin/P004_config_values.repl new file mode 100644 index 0000000000..9df718042f --- /dev/null +++ b/docs/source/Plugin/P004_config_values.repl @@ -0,0 +1,46 @@ +.. csv-table:: + :header: "Config value", "Information" + :widths: 20, 30 + + " + | ``[#SensorStats,,Success]`` + + | ````: The number of the device address within the configuration, range 1..4. + "," + | Returns the number of successful reads for this sensor as shown in the settings page. + " + " + | ``[#SensorStats,,Retry]`` + + | ````: The number of the device address within the configuration, range 1..4. + "," + | Returns the number of retries for this sensor as shown in the settings page. + " + " + | ``[#SensorStats,,Failed]`` + + | ````: The number of the device address within the configuration, range 1..4. + "," + | Returns the number of failed reads for this sensor as shown in the settings page. + " + " + | ``[#SensorStats,,InitFailed]`` + + | ````: The number of the device address within the configuration, range 1..4. + "," + | Returns the number of failed initialization-reads (second retry failed too) for this sensor as shown in the settings page. + " + " + | ``[#SensorStats,,Resolution]`` + + | ````: The number of the device address within the configuration, range 1..4. + "," + | Returns the resolution in bits for this sensor as shown in the settings page. + " + " + | ``[#SensorStats,,Address]`` + + | ````: The number of the device address within the configuration, range 1..4. + "," + | Returns the formatted Device Address, including the device type, for this sensor as shown in the settings page. + " diff --git a/src/_P004_Dallas.ino b/src/_P004_Dallas.ino index 6754f1acea..50f0e3ac04 100644 --- a/src/_P004_Dallas.ino +++ b/src/_P004_Dallas.ino @@ -8,6 +8,9 @@ // Maxim Integrated (ex Dallas) DS18B20 datasheet : https://datasheets.maximintegrated.com/en/ds/DS18B20.pdf /** Changelog: + * 2024-05-11 tonhuisman: Add Get Config Value support for sensor statistics: Read success, Read retry, Read failed, + * Read init failed, Resolution and Address (formatted) + * [#sensorstats,,success|retry|failed|initfailed|resolution|address] * 2023-04-18 tonhuisman: Add warning on statistics section for Parasite Powered sensors, as these are unsupported. * 2023-04-17 tonhuisman: Use actual sensor resolution, even when using multiple sensors with different resolutions * 2023-04-16 tonhuisman: Rename from DS18b20 to 1-Wire Temperature, as it supports several 1-Wire temperature sensors @@ -206,7 +209,7 @@ boolean Plugin_004(uint8_t function, struct EventStruct *event, String& string) static_cast(getPluginTaskData(event->TaskIndex)); int8_t Plugin_004_DallasPin_RX = CONFIG_PIN1; int8_t Plugin_004_DallasPin_TX = CONFIG_PIN2; - const int valueCount = P004_NR_OUTPUT_VALUES; + const int valueCount = P004_NR_OUTPUT_VALUES; if (Plugin_004_DallasPin_TX == -1) { Plugin_004_DallasPin_TX = Plugin_004_DallasPin_RX; @@ -243,7 +246,7 @@ boolean Plugin_004(uint8_t function, struct EventStruct *event, String& string) int8_t Plugin_004_DallasPin_RX = CONFIG_PIN1; int8_t Plugin_004_DallasPin_TX = CONFIG_PIN2; const uint8_t res = P004_RESOLUTION; - const int valueCount = P004_NR_OUTPUT_VALUES; + const int valueCount = P004_NR_OUTPUT_VALUES; if (Plugin_004_DallasPin_TX == -1) { Plugin_004_DallasPin_TX = Plugin_004_DallasPin_RX; @@ -255,11 +258,11 @@ boolean Plugin_004(uint8_t function, struct EventStruct *event, String& string) # endif // ifdef USE_SECOND_HEAP initPluginTaskData(event->TaskIndex, new (std::nothrow) P004_data_struct( - event->TaskIndex, - Plugin_004_DallasPin_RX, - Plugin_004_DallasPin_TX, - res, - valueCount == 1 && P004_SCAN_ON_INIT)); + event->TaskIndex, + Plugin_004_DallasPin_RX, + Plugin_004_DallasPin_TX, + res, + valueCount == 1 && P004_SCAN_ON_INIT)); } P004_data_struct *P004_data = static_cast(getPluginTaskData(event->TaskIndex)); @@ -284,6 +287,7 @@ boolean Plugin_004(uint8_t function, struct EventStruct *event, String& string) if (nullptr != P004_data) { const int valueCount = P004_NR_OUTPUT_VALUES; + if ((valueCount == 1) && P004_SCAN_ON_INIT) { if (!P004_data->sensorAddressSet()) { P004_data->init(); @@ -308,8 +312,8 @@ boolean Plugin_004(uint8_t function, struct EventStruct *event, String& string) if (P004_data->read_temp(value, i)) { - UserVar.setFloat(event->TaskIndex, i, value); - success = true; + UserVar.setFloat(event->TaskIndex, i, value); + success = true; } else { @@ -323,7 +327,7 @@ boolean Plugin_004(uint8_t function, struct EventStruct *event, String& string) default: break; } - UserVar.setFloat(event->TaskIndex, i, errorValue); + UserVar.setFloat(event->TaskIndex, i, errorValue); } } @@ -347,6 +351,53 @@ boolean Plugin_004(uint8_t function, struct EventStruct *event, String& string) } break; } + + # if P004_FEATURE_GET_CONFIG_VALUE + case PLUGIN_GET_CONFIG_VALUE: + { + P004_data_struct *P004_data = + static_cast(getPluginTaskData(event->TaskIndex)); + + if (nullptr != P004_data) { + const String cmd = parseString(string, 1); + + if (equals(cmd, F("sensorstats"))) { // To distinguish from 'DeviceStats' + const String par1 = parseString(string, 2); + int32_t nPar1; + + if (validIntFromString(par1, nPar1) && (nPar1 > 0) && (nPar1 <= P004_NR_OUTPUT_VALUES)) { + nPar1--; // From DeviceNr to array index + const String subcmd = parseString(string, 2); + Dallas_SensorData sensorData = P004_data->get_sensor_data(nPar1); + success = true; + + if (equals(subcmd, F("success"))) { + string = sensorData.read_success; + } else + if (equals(subcmd, F("retry"))) { + string = sensorData.read_retry; + } else + if (equals(subcmd, F("failed"))) { + string = sensorData.read_failed; + } else + if (equals(subcmd, F("initfailed"))) { + string = sensorData.start_read_failed; + } else + if (equals(subcmd, F("resolution"))) { + string = sensorData.actual_res; + } else + if (equals(subcmd, F("address"))) { + string = sensorData.get_formatted_address(); + } else + { // Unsupported stat + success = false; + } + } + } + } + break; + } + # endif // if P004_FEATURE_GET_CONFIG_VALUE } return success; } diff --git a/src/src/PluginStructs/P004_data_struct.h b/src/src/PluginStructs/P004_data_struct.h index 78f313aa59..3c70624745 100644 --- a/src/src/PluginStructs/P004_data_struct.h +++ b/src/src/PluginStructs/P004_data_struct.h @@ -6,6 +6,12 @@ # include "../Helpers/Dallas1WireHelper.h" +# ifndef P004_FEATURE_GET_CONFIG_VALUE +# define P004_FEATURE_GET_CONFIG_VALUE 1 // Enable by default, +// adds 468 bytes on ESP8266, 944 bytes on ESP32-C6, 490 bytes on ESP32-C3 +// 456 bytes on ESP32 Classic, 468 bytes on ESP32-S3 (ESP32 builds: IDF 5.1) +# endif // ifndef P004_FEATURE_GET_CONFIG_VALUE + struct P004_data_struct : public PluginTaskData_base { /*********************************************************************************************\ * Task data struct to simplify taking measurements of upto 4 Dallas DS18b20 (or compatible) From 1a0daf7d9a570ae614f3b4379c1164daac8af882 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Sat, 11 May 2024 21:30:41 +0200 Subject: [PATCH 105/113] [Commands] Add OWScan command to scan for 1-wire devices --- docs/source/Plugin/P000_commands.repl | 17 ++++++ docs/source/Plugin/_plugin_sets_overview.repl | 53 +++++++++++++++++++ src/src/Commands/InternalCommands.cpp | 6 +++ src/src/Commands/InternalCommands_decoder.cpp | 12 +++-- src/src/Commands/InternalCommands_decoder.h | 4 ++ src/src/Commands/OneWire.cpp | 49 +++++++++++++++++ src/src/Commands/OneWire.h | 10 ++++ src/src/CustomBuild/define_plugin_sets.h | 16 ++++++ src/src/Helpers/Dallas1WireHelper.cpp | 4 ++ src/src/Helpers/Dallas1WireHelper.h | 3 ++ 10 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 src/src/Commands/OneWire.cpp create mode 100644 src/src/Commands/OneWire.h diff --git a/docs/source/Plugin/P000_commands.repl b/docs/source/Plugin/P000_commands.repl index 24cf777b64..185cce3c22 100644 --- a/docs/source/Plugin/P000_commands.repl +++ b/docs/source/Plugin/P000_commands.repl @@ -383,6 +383,23 @@ ``NTPHost,""`` or ``NTPHost,`` -> Clear the NTP host so a default host from pool.ntp.org will be used. (As second argument *is* provided but empty.)" " + OWScan"," + :red:`Internal`"," + Scan for & list 1-wire (One Wire = OW) devices on a GPIO pin. + + Syntax: ``owscan,[,]`` + + If no separate TX pin is provided, the RX pin will be used as the TX pin too, as is the normal behavior for 1-wire communication. The separate TX pin is used on a Shelly device when adding the Shelly Temperature add-on (RX = GPIO-0 and TX = GPIO-3). + + Example: + + .. code-block:: none + + >owscan,5 + 01-6e-56-8c-01-00-00-84 [DS1990A] + + " + " Password"," :red:`Internal`"," Set the password of the unit. diff --git a/docs/source/Plugin/_plugin_sets_overview.repl b/docs/source/Plugin/_plugin_sets_overview.repl index 1364a0a3da..cf5ca6bca6 100644 --- a/docs/source/Plugin/_plugin_sets_overview.repl +++ b/docs/source/Plugin/_plugin_sets_overview.repl @@ -316,20 +316,26 @@ Build set: :yellow:`COLLECTION C` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" + ":ref:`P007_page`","P007" + ":ref:`P008_page`","P008" + ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" + ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" + ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" + ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -340,6 +346,9 @@ Build set: :yellow:`COLLECTION C` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" + ":ref:`P041_page`","P041" + ":ref:`P042_page`","P042" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P045_page`","P045" @@ -414,20 +423,26 @@ Build set: :yellow:`COLLECTION D` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" + ":ref:`P007_page`","P007" + ":ref:`P008_page`","P008" + ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" + ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" + ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" + ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -438,6 +453,9 @@ Build set: :yellow:`COLLECTION D` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" + ":ref:`P041_page`","P041" + ":ref:`P042_page`","P042" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P045_page`","P045" @@ -723,20 +741,26 @@ Build set: :yellow:`COLLECTION G` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" + ":ref:`P007_page`","P007" + ":ref:`P008_page`","P008" + ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" + ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" + ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" + ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -747,6 +771,9 @@ Build set: :yellow:`COLLECTION G` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" + ":ref:`P041_page`","P041" + ":ref:`P042_page`","P042" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P045_page`","P045" @@ -906,20 +933,26 @@ Build set: :yellow:`DISPLAY` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" + ":ref:`P007_page`","P007" + ":ref:`P008_page`","P008" + ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" + ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" + ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" + ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -930,6 +963,9 @@ Build set: :yellow:`DISPLAY` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" + ":ref:`P041_page`","P041" + ":ref:`P042_page`","P042" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P049_page`","P049" @@ -982,16 +1018,21 @@ Build set: :yellow:`ENERGY` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" + ":ref:`P007_page`","P007" + ":ref:`P008_page`","P008" + ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" + ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" + ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" @@ -1007,6 +1048,9 @@ Build set: :yellow:`ENERGY` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" + ":ref:`P041_page`","P041" + ":ref:`P042_page`","P042" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P049_page`","P049" @@ -1125,20 +1169,26 @@ Build set: :yellow:`IRext` ":ref:`P004_page`","P004" ":ref:`P005_page`","P005" ":ref:`P006_page`","P006" + ":ref:`P007_page`","P007" + ":ref:`P008_page`","P008" + ":ref:`P009_page`","P009" ":ref:`P010_page`","P010" ":ref:`P011_page`","P011" ":ref:`P012_page`","P012" ":ref:`P013_page`","P013" ":ref:`P014_page`","P014" ":ref:`P015_page`","P015" + ":ref:`P017_page`","P017" ":ref:`P018_page`","P018" ":ref:`P019_page`","P019" ":ref:`P020_page`","P020" ":ref:`P021_page`","P021" + ":ref:`P022_page`","P022" ":ref:`P023_page`","P023" ":ref:`P024_page`","P024" ":ref:`P025_page`","P025" ":ref:`P026_page`","P026" + ":ref:`P027_page`","P027" ":ref:`P028_page`","P028" ":ref:`P029_page`","P029" ":ref:`P031_page`","P031" @@ -1149,6 +1199,9 @@ Build set: :yellow:`IRext` ":ref:`P037_page`","P037" ":ref:`P038_page`","P038" ":ref:`P039_page`","P039" + ":ref:`P040_page`","P040" + ":ref:`P041_page`","P041" + ":ref:`P042_page`","P042" ":ref:`P043_page`","P043" ":ref:`P044_page`","P044" ":ref:`P049_page`","P049" diff --git a/src/src/Commands/InternalCommands.cpp b/src/src/Commands/InternalCommands.cpp index d5438bfdc0..6be25458de 100644 --- a/src/src/Commands/InternalCommands.cpp +++ b/src/src/Commands/InternalCommands.cpp @@ -26,6 +26,9 @@ #if FEATURE_NOTIFIER # include "../Commands/Notifications.h" #endif // if FEATURE_NOTIFIER +#if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN +#include "../Commands/OneWire.h" +#endif // if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN #include "../Commands/Provisioning.h" #include "../Commands/RTC.h" #include "../Commands/Rules.h" @@ -350,6 +353,9 @@ bool InternalCommands::executeInternalCommand() case ESPEasy_cmd_e::notify: COMMAND_CASE_R(Command_Notifications_Notify, -1); // Notifications.h #endif // if FEATURE_NOTIFIER case ESPEasy_cmd_e::ntphost: COMMAND_CASE_R(Command_NTPHost, 1); // Time.h +#if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN + case ESPEasy_cmd_e::owscan: COMMAND_CASE_R(Command_OneWire_Owscan, -1); // OneWire.h +#endif // if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN #ifdef USES_P019 case ESPEasy_cmd_e::pcfgpio: COMMAND_CASE_A(Command_GPIO, 2); // Gpio.h case ESPEasy_cmd_e::pcfgpiorange: COMMAND_CASE_A(Command_GPIO_PcfGPIORange, -1); // Gpio.h diff --git a/src/src/Commands/InternalCommands_decoder.cpp b/src/src/Commands/InternalCommands_decoder.cpp index 2523033955..bb98342f62 100644 --- a/src/src/Commands/InternalCommands_decoder.cpp +++ b/src/src/Commands/InternalCommands_decoder.cpp @@ -132,14 +132,17 @@ const char Internal_commands_m[] PROGMEM = #endif // ifndef BUILD_NO_DIAGNOSTIC_COMMANDS ; -#define Int_cmd_n_offset ESPEasy_cmd_e::name -const char Internal_commands_n[] PROGMEM = +#define Int_cmd_no_offset ESPEasy_cmd_e::name +const char Internal_commands_no[] PROGMEM = "name|" "nosleep|" #if FEATURE_NOTIFIER "notify|" #endif // #if FEATURE_NOTIFIER "ntphost|" +#if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN + "owscan|" +#endif // if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN ; #define Int_cmd_p_offset ESPEasy_cmd_e::password @@ -317,8 +320,9 @@ const char* getInternalCommand_Haystack_Offset(const char firstLetter, int& offs haystack = Internal_commands_m; break; case 'n': - offset = static_cast(Int_cmd_n_offset); - haystack = Internal_commands_n; + case 'o': + offset = static_cast(Int_cmd_no_offset); + haystack = Internal_commands_no; break; case 'p': offset = static_cast(Int_cmd_p_offset); diff --git a/src/src/Commands/InternalCommands_decoder.h b/src/src/Commands/InternalCommands_decoder.h index 0a0a452fb8..3cf2d7d227 100644 --- a/src/src/Commands/InternalCommands_decoder.h +++ b/src/src/Commands/InternalCommands_decoder.h @@ -112,6 +112,10 @@ enum class ESPEasy_cmd_e : uint8_t { #endif // #if FEATURE_NOTIFIER ntphost, +#if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN + owscan, +#endif // if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN + password, #ifdef USES_P019 pcfgpio, diff --git a/src/src/Commands/OneWire.cpp b/src/src/Commands/OneWire.cpp new file mode 100644 index 0000000000..d285e72f58 --- /dev/null +++ b/src/src/Commands/OneWire.cpp @@ -0,0 +1,49 @@ +#include "../Commands/OneWire.h" + +#if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN + +# include "../Commands/Common.h" + +# include "../Helpers/Dallas1WireHelper.h" +# include "../Helpers/Hardware_GPIO.h" +# include "../Helpers/StringConverter.h" + +String Command_OneWire_Owscan(struct EventStruct *event, + const char *Line) { + int pinnr; bool input; bool output1; bool output; bool warning; + + if (getGpioInfo(event->Par1, pinnr, input, output1, warning) && input) { // Input pin required + if (!parseString(Line, 3).isEmpty()) { + if (getGpioInfo(event->Par2, pinnr, input, output, warning) && !output) { // Output required if specified + return return_command_failed(); // Argument error + } + } else { + event->Par2 = event->Par1; // RX and TX on same pin + + if (!output1) { // Single pin must be input & output capable + return return_command_failed(); // Argument error + } + } + pinMode(event->Par1, INPUT); + + if (event->Par2 != event->Par1) { + pinMode(event->Par2, OUTPUT); + } + + uint8_t addr[8]{}; + + Dallas_reset(event->Par1, event->Par2); + String res; + res.reserve(80); + + while (Dallas_search(addr, event->Par1, event->Par2)) { // Scan the 1-wire + res += Dallas_format_address(addr); + res += '\n'; + } + return res; + } + + return return_command_failed(); +} + +#endif // if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN diff --git a/src/src/Commands/OneWire.h b/src/src/Commands/OneWire.h new file mode 100644 index 0000000000..d8c29a15a3 --- /dev/null +++ b/src/src/Commands/OneWire.h @@ -0,0 +1,10 @@ +#ifndef COMMANDS_ONE_WIRE_H +#include "../Helpers/Dallas1WireHelper.h" + +#if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN +String Command_OneWire_Owscan(struct EventStruct *event, + const char *Line); + +#endif // if FEATURE_DALLAS_HELPER && FEATURE_COMMAND_OWSCAN + +#endif // ifndef COMMANDS_ONE_WIRE_H diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index ab792fab42..ef492adaab 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -3388,6 +3388,22 @@ To create/register a plugin, you have to : #endif +#if defined(USES_P004) || defined(USES_P080) || defined(USES_P100) + #define FEATURE_DALLAS_HELPER 1 +#endif +#ifndef FEATURE_DALLAS_HELPER + #define FEATURE_DALLAS_HELPER 0 // Only when Dallas/Maxim 1-wire plugins are included +#endif +#if FEATURE_DALLAS_HELPER && !defined(FEATURE_COMMAND_OWSCAN) + #ifdef MINIMAL_OTA + #define FEATURE_COMMAND_OWSCAN 0 // Exclude owscan command for minimal-OTA builds + #else // ifdef MINIMAL_OTA + #define FEATURE_COMMAND_OWSCAN 1 + #endif // ifdef MINIMAL_OTA +#endif +#ifndef FEATURE_COMMAND_OWSCAN + #define FEATURE_COMMAND_OWSCAN 0 // Remaining cases: disable command +#endif // ifndef FEATURE_COMMAND_OWSCAN // TODO TD-er: Test feature, must remove /* diff --git a/src/src/Helpers/Dallas1WireHelper.cpp b/src/src/Helpers/Dallas1WireHelper.cpp index c8d55f7132..26863acfc6 100644 --- a/src/src/Helpers/Dallas1WireHelper.cpp +++ b/src/src/Helpers/Dallas1WireHelper.cpp @@ -1,5 +1,7 @@ #include "../Helpers/Dallas1WireHelper.h" +#if FEATURE_DALLAS_HELPER + #include "../../_Plugin_Helper.h" #include "../ESPEasyCore/ESPEasy_Log.h" #include "../Helpers/ESPEasy_Storage.h" @@ -1266,3 +1268,5 @@ bool Dallas_SensorData::check_sensor(int8_t gpio_rx, int8_t gpio_tx, int8_t res) parasitePowered = Dallas_is_parasite(tmpaddr, gpio_rx, gpio_tx); return true; } + +#endif // if FEATURE_DALLAS_HELPER \ No newline at end of file diff --git a/src/src/Helpers/Dallas1WireHelper.h b/src/src/Helpers/Dallas1WireHelper.h index ac6e9ad48b..17510739e0 100644 --- a/src/src/Helpers/Dallas1WireHelper.h +++ b/src/src/Helpers/Dallas1WireHelper.h @@ -3,6 +3,8 @@ #include "../../ESPEasy_common.h" +#if FEATURE_DALLAS_HELPER + #include "../DataTypes/TaskIndex.h" #include "../DataTypes/PluginID.h" @@ -226,5 +228,6 @@ uint16_t Dallas_crc16(const uint8_t *input, uint16_t crc); +#endif // if FEATURE_DALLAS_HELPER #endif // ifndef HELPERS_DALLAS1WIREHELPER_H From c27083234e95f9a722efdcb04ec318b7b1c6fa2c Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Tue, 14 May 2024 12:42:03 +0200 Subject: [PATCH 106/113] [P004] Correct typo --- src/_P004_Dallas.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_P004_Dallas.ino b/src/_P004_Dallas.ino index 50f0e3ac04..9771239a58 100644 --- a/src/_P004_Dallas.ino +++ b/src/_P004_Dallas.ino @@ -367,7 +367,7 @@ boolean Plugin_004(uint8_t function, struct EventStruct *event, String& string) if (validIntFromString(par1, nPar1) && (nPar1 > 0) && (nPar1 <= P004_NR_OUTPUT_VALUES)) { nPar1--; // From DeviceNr to array index - const String subcmd = parseString(string, 2); + const String subcmd = parseString(string, 3); Dallas_SensorData sensorData = P004_data->get_sensor_data(nPar1); success = true; From 7648885036c75354eadfc8b64953a9fad866e80e Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Wed, 15 May 2024 23:01:00 +0200 Subject: [PATCH 107/113] [P004] Change argument separator to dot for Get Config Value --- docs/source/Plugin/P004_config_values.repl | 12 ++++++------ src/_P004_Dallas.ino | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/source/Plugin/P004_config_values.repl b/docs/source/Plugin/P004_config_values.repl index 9df718042f..fbd371f166 100644 --- a/docs/source/Plugin/P004_config_values.repl +++ b/docs/source/Plugin/P004_config_values.repl @@ -3,42 +3,42 @@ :widths: 20, 30 " - | ``[#SensorStats,,Success]`` + | ``[#SensorStats..Success]`` | ````: The number of the device address within the configuration, range 1..4. "," | Returns the number of successful reads for this sensor as shown in the settings page. " " - | ``[#SensorStats,,Retry]`` + | ``[#SensorStats..Retry]`` | ````: The number of the device address within the configuration, range 1..4. "," | Returns the number of retries for this sensor as shown in the settings page. " " - | ``[#SensorStats,,Failed]`` + | ``[#SensorStats..Failed]`` | ````: The number of the device address within the configuration, range 1..4. "," | Returns the number of failed reads for this sensor as shown in the settings page. " " - | ``[#SensorStats,,InitFailed]`` + | ``[#SensorStats..InitFailed]`` | ````: The number of the device address within the configuration, range 1..4. "," | Returns the number of failed initialization-reads (second retry failed too) for this sensor as shown in the settings page. " " - | ``[#SensorStats,,Resolution]`` + | ``[#SensorStats..Resolution]`` | ````: The number of the device address within the configuration, range 1..4. "," | Returns the resolution in bits for this sensor as shown in the settings page. " " - | ``[#SensorStats,,Address]`` + | ``[#SensorStats..Address]`` | ````: The number of the device address within the configuration, range 1..4. "," diff --git a/src/_P004_Dallas.ino b/src/_P004_Dallas.ino index 9771239a58..6b5e2db889 100644 --- a/src/_P004_Dallas.ino +++ b/src/_P004_Dallas.ino @@ -359,15 +359,15 @@ boolean Plugin_004(uint8_t function, struct EventStruct *event, String& string) static_cast(getPluginTaskData(event->TaskIndex)); if (nullptr != P004_data) { - const String cmd = parseString(string, 1); + const String cmd = parseString(string, 1, '.'); if (equals(cmd, F("sensorstats"))) { // To distinguish from 'DeviceStats' - const String par1 = parseString(string, 2); + const String par1 = parseString(string, 2, '.'); int32_t nPar1; if (validIntFromString(par1, nPar1) && (nPar1 > 0) && (nPar1 <= P004_NR_OUTPUT_VALUES)) { nPar1--; // From DeviceNr to array index - const String subcmd = parseString(string, 3); + const String subcmd = parseString(string, 3, '.'); Dallas_SensorData sensorData = P004_data->get_sensor_data(nPar1); success = true; From a5e44671bd510419b5d7edf887fd84b94b132d7a Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Thu, 16 May 2024 22:21:13 +0200 Subject: [PATCH 108/113] [1WireHelper] Return Unknown for unrecognized devices --- src/src/Helpers/Dallas1WireHelper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/Helpers/Dallas1WireHelper.cpp b/src/src/Helpers/Dallas1WireHelper.cpp index 26863acfc6..3acd2a2331 100644 --- a/src/src/Helpers/Dallas1WireHelper.cpp +++ b/src/src/Helpers/Dallas1WireHelper.cpp @@ -56,7 +56,7 @@ const __FlashStringHelper* Dallas_getModel(uint8_t family, const bool hasFixedRe case 0x1D: return F("DS2423"); // 4k RAM with counter case 0x01: return F("DS1990A"); // Serial Number iButton } - return F(""); + return F("Unknown"); } String Dallas_format_address(const uint8_t addr[], const bool hasFixedResolution) { From abb062593fb2d83a73886c2fdb83f07165763107 Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 20 May 2024 16:32:44 +0200 Subject: [PATCH 109/113] [Build] Set ETH enabled for all IDF 5.1 builds --- platformio.ini | 2 +- platformio_esp32_envs.ini | 36 ++++++++++++++++++++---------------- platformio_esp32_solo1.ini | 13 +++++++++---- platformio_esp32c2_envs.ini | 13 +++++++++---- platformio_esp32c3_envs.ini | 28 +++++++++++++++++++--------- platformio_esp32c6_envs.ini | 7 +++++-- platformio_esp32s2_envs.ini | 11 ++++++++--- platformio_esp32s3_envs.ini | 3 ++- 8 files changed, 73 insertions(+), 40 deletions(-) diff --git a/platformio.ini b/platformio.ini index 0a26179810..07784dfedd 100644 --- a/platformio.ini +++ b/platformio.ini @@ -28,7 +28,7 @@ extra_configs = platformio_esp32c6_envs.ini ;default_envs = normal_ESP32_4M -default_envs = max_ESP32_16M8M_LittleFS +default_envs = max_ESP32_16M8M_LittleFS_ETH ;default_envs = normal_ESP32c6_4M316k_LittleFS_CDC ; default_envs = custom_ESP8266_4M1M diff --git a/platformio_esp32_envs.ini b/platformio_esp32_envs.ini index 6c24456eab..6930f5a3ff 100644 --- a/platformio_esp32_envs.ini +++ b/platformio_esp32_envs.ini @@ -150,14 +150,16 @@ extra_scripts = ${esp32_common_LittleFS.extra_scripts} extends = esp32_custom_base board = esp32_4M -[env:custom_ESP32_4M316k_LittleFS] -extends = esp32_custom_base_LittleFS -board = esp32_4M +; [env:custom_ESP32_4M316k_LittleFS] +; extends = esp32_custom_base_LittleFS +; board = esp32_4M -[env:custom_ESP32_16M8M_LittleFS] +[env:custom_ESP32_16M8M_LittleFS_ETH] extends = esp32_custom_base_LittleFS board = esp32_16M8M board_upload.flash_size = 16MB +build_flags = ${esp32_custom_base_LittleFS.build_flags} + -DFEATURE_ETHERNET=1 [env:custom_IR_ESP32_4M316k] extends = esp32_common @@ -171,11 +173,12 @@ extra_scripts = ${esp32_common.extra_scripts} pre:tools/pio/pre_custom_esp32_IR.py pre:tools/pio/ir_build_check.py -[env:custom_ESP32_4M2M_NO_OTA_LittleFS] +[env:custom_ESP32_4M2M_NO_OTA_LittleFS_ETH] extends = esp32_custom_base_LittleFS board = esp32_4M2M build_flags = ${esp32_custom_base_LittleFS.build_flags} -DNO_HTTP_UPDATER + -DFEATURE_ETHERNET=1 [env:normal_ESP32_4M316k] @@ -184,11 +187,11 @@ board = esp32_4M lib_ignore = ${esp32_common.lib_ignore} ${no_ir.lib_ignore} -[env:normal_ESP32_4M316k_LittleFS] -extends = esp32_common_LittleFS -board = esp32_4M -lib_ignore = ${esp32_common_LittleFS.lib_ignore} - ${no_ir.lib_ignore} +; [env:normal_ESP32_4M316k_LittleFS] +; extends = esp32_common_LittleFS +; board = esp32_4M +; lib_ignore = ${esp32_common_LittleFS.lib_ignore} +; ${no_ir.lib_ignore} [env:collection_A_ESP32_4M316k] extends = esp32_common @@ -536,7 +539,7 @@ build_flags = ${env:max_ESP32_16M1M.build_flags} ; A Lolin D32 PRO with 16MB Flash, allowing 4MB sketch size, and file storage using LittleFS filesystem -[env:max_ESP32_16M8M_LittleFS] +[env:max_ESP32_16M8M_LittleFS_ETH] extends = esp32_common_LittleFS board = esp32_16M8M board_upload.flash_size = 16MB @@ -547,15 +550,16 @@ build_flags = ${esp32_common_LittleFS.build_flags} -DFEATURE_ARDUINO_OTA=1 -DPLUGIN_BUILD_MAX_ESP32 -DPLUGIN_BUILD_IR_EXTENDED + -DFEATURE_ETHERNET=1 extra_scripts = ${esp32_common.extra_scripts} board_build.filesystem = littlefs ; If you have a board with Ethernet integrated and 16MB Flash, then this configuration could be enabled, it's based on the max_ESP32_16M8M_LittleFS definition -[env:max_ESP32_16M8M_LittleFS_ETH] -extends = env:max_ESP32_16M8M_LittleFS -board = ${env:max_ESP32_16M8M_LittleFS.board} -build_flags = ${env:max_ESP32_16M8M_LittleFS.build_flags} - -DFEATURE_ETHERNET=1 +; [env:max_ESP32_16M8M_LittleFS_ETH] +; extends = env:max_ESP32_16M8M_LittleFS +; board = ${env:max_ESP32_16M8M_LittleFS.board} +; build_flags = ${env:max_ESP32_16M8M_LittleFS.build_flags} +; -DFEATURE_ETHERNET=1 diff --git a/platformio_esp32_solo1.ini b/platformio_esp32_solo1.ini index 0886c119dd..19da771164 100644 --- a/platformio_esp32_solo1.ini +++ b/platformio_esp32_solo1.ini @@ -28,30 +28,35 @@ build_unflags = ${esp32_base_idf5.build_unflags} board_build.filesystem = littlefs -[env:custom_ESP32solo1_4M316k_LittleFS] +[env:custom_ESP32solo1_4M316k_LittleFS_ETH] extends = esp32_solo1_common_LittleFS board = esp32_solo1_4M build_flags = ${esp32_solo1_common_LittleFS.build_flags} -DPLUGIN_BUILD_CUSTOM + -DFEATURE_ETHERNET=1 extra_scripts = ${esp32_solo1_common_LittleFS.extra_scripts} pre:tools/pio/pre_custom_esp32.py -[env:normal_ESP32solo1_4M316k_LittleFS] +[env:normal_ESP32solo1_4M316k_LittleFS_ETH] extends = esp32_solo1_common_LittleFS board = esp32_solo1_4M +build_flags = ${esp32_solo1_common_LittleFS.build_flags} + -DFEATURE_ETHERNET=1 lib_ignore = ${esp32_solo1_common_LittleFS.lib_ignore} ${no_ir.lib_ignore} -[env:energy_ESP32solo1_4M316k_LittleFS] +[env:energy_ESP32solo1_4M316k_LittleFS_ETH] extends = esp32_solo1_common_LittleFS board = esp32_solo1_4M build_flags = ${esp32_solo1_common_LittleFS.build_flags} -D PLUGIN_ENERGY_COLLECTION + -DFEATURE_ETHERNET=1 -[env:climate_ESP32solo1_4M316k_LittleFS] +[env:climate_ESP32solo1_4M316k_LittleFS_ETH] extends = esp32_solo1_common_LittleFS board = esp32_solo1_4M build_flags = ${esp32_solo1_common_LittleFS.build_flags} -D PLUGIN_CLIMATE_COLLECTION + -DFEATURE_ETHERNET=1 diff --git a/platformio_esp32c2_envs.ini b/platformio_esp32c2_envs.ini index 732859392b..e5937ff2e0 100644 --- a/platformio_esp32c2_envs.ini +++ b/platformio_esp32c2_envs.ini @@ -16,35 +16,40 @@ lib_ignore = ${esp32_base_idf5.lib_ignore} Adafruit NeoMatrix via NeoPixelBus -[env:safeboot_ESP32c2_4M_LittleFS] +[env:safeboot_ESP32c2_4M_LittleFS_ETH] extends = esp32c2_common_LittleFS board = esp32c2 build_flags = ${esp32c2_common_LittleFS.build_flags} -DPLUGIN_BUILD_CUSTOM -DPLUGIN_BUILD_SAFEBOOT + -DFEATURE_ETHERNET=1 extra_scripts = ${esp32c2_common_LittleFS.extra_scripts} pre:tools/pio/pre_safeboot_esp32c2.py lib_ignore = ${esp32c2_common_LittleFS.lib_ignore} -[env:custom_ESP32c2_2M320k_LittleFS_noOTA] +[env:custom_ESP32c2_2M320k_LittleFS_noOTA_ETH] extends = esp32c2_common_LittleFS board = esp32c2_2M build_flags = ${esp32c2_common_LittleFS.build_flags} -DPLUGIN_BUILD_CUSTOM + -DFEATURE_ETHERNET=1 extra_scripts = ${esp32c2_common_LittleFS.extra_scripts} pre:tools/pio/pre_custom_esp32c2.py -[env:custom_ESP32c2_4M316k_LittleFS] +[env:custom_ESP32c2_4M316k_LittleFS_ETH] extends = esp32c2_common_LittleFS board = esp32c2 build_flags = ${esp32c2_common_LittleFS.build_flags} -DPLUGIN_BUILD_CUSTOM + -DFEATURE_ETHERNET=1 extra_scripts = ${esp32c2_common_LittleFS.extra_scripts} pre:tools/pio/pre_custom_esp32c2.py -[env:normal_ESP32c2_4M316k_LittleFS] +[env:normal_ESP32c2_4M316k_LittleFS_ETH] extends = esp32c2_common_LittleFS board = esp32c2 +build_flags = ${esp32c2_common_LittleFS.build_flags} + -DFEATURE_ETHERNET=1 lib_ignore = ${esp32c2_common_LittleFS.lib_ignore} ${no_ir.lib_ignore} diff --git a/platformio_esp32c3_envs.ini b/platformio_esp32c3_envs.ini index abffc558e6..2fb29ddb7b 100644 --- a/platformio_esp32c3_envs.ini +++ b/platformio_esp32c3_envs.ini @@ -47,13 +47,13 @@ extra_scripts = ${esp32c3_common.extra_scripts} pre:tools/pio/pre_custom_esp32_IR.py pre:tools/pio/ir_build_check.py -[env:custom_ESP32c3_4M316k_LittleFS_CDC] -extends = esp32c3_common_LittleFS -board = esp32c3cdc -build_flags = ${esp32c3_common_LittleFS.build_flags} - -DPLUGIN_BUILD_CUSTOM -extra_scripts = ${esp32c3_common_LittleFS.extra_scripts} - pre:tools/pio/pre_custom_esp32.py +; [env:custom_ESP32c3_4M316k_LittleFS_CDC] +; extends = esp32c3_common_LittleFS +; board = esp32c3cdc +; build_flags = ${esp32c3_common_LittleFS.build_flags} +; -DPLUGIN_BUILD_CUSTOM +; extra_scripts = ${esp32c3_common_LittleFS.extra_scripts} +; pre:tools/pio/pre_custom_esp32.py [env:custom_ESP32c3_4M316k_LittleFS_CDC_ETH] extends = esp32c3_common_LittleFS @@ -73,9 +73,11 @@ lib_ignore = ${esp32_common.lib_ignore} ${no_ir.lib_ignore} -[env:normal_ESP32c3_4M316k_LittleFS_CDC] +[env:normal_ESP32c3_4M316k_LittleFS_CDC_ETH] extends = esp32c3_common_LittleFS board = esp32c3cdc +build_flags = ${esp32c3_common_LittleFS.build_flags} + -DFEATURE_ETHERNET=1 lib_ignore = ${esp32c3_common_LittleFS.lib_ignore} ${no_ir.lib_ignore} @@ -135,6 +137,13 @@ board = esp32c3cdc build_flags = ${esp32c3_common.build_flags} -D PLUGIN_ENERGY_COLLECTION +[env:energy_ESP32c3_4M316k_LittleFS_CDC_ETH] +extends = esp32c3_common_LittleFS +board = esp32c3cdc +build_flags = ${esp32c3_common_LittleFS.build_flags} + -D PLUGIN_ENERGY_COLLECTION + -DFEATURE_ETHERNET=1 + [env:display_ESP32c3_4M316k_CDC] extends = esp32c3_common board = esp32c3cdc @@ -155,10 +164,11 @@ build_flags = ${esp32c3_common.build_flags} -DFEATURE_SD=1 -DPLUGIN_NEOPIXEL_COLLECTION -[env:neopixel_ESP32c3_4M316k_LittleFS_CDC] +[env:neopixel_ESP32c3_4M316k_LittleFS_CDC_ETH] extends = esp32c3_common_LittleFS board = esp32c3cdc build_flags = ${esp32c3_common_LittleFS.build_flags} -DFEATURE_ARDUINO_OTA=1 -DFEATURE_SD=1 + -DFEATURE_ETHERNET=1 -DPLUGIN_NEOPIXEL_COLLECTION diff --git a/platformio_esp32c6_envs.ini b/platformio_esp32c6_envs.ini index 2889771b92..461498bb1e 100644 --- a/platformio_esp32c6_envs.ini +++ b/platformio_esp32c6_envs.ini @@ -14,16 +14,19 @@ lib_ignore = ${esp32_base_idf5.lib_ignore} board = esp32c6cdc -[env:custom_ESP32c6_4M316k_LittleFS_CDC] +[env:custom_ESP32c6_4M316k_LittleFS_CDC_ETH] extends = esp32c6_common_LittleFS build_flags = ${esp32c6_common_LittleFS.build_flags} -DPLUGIN_BUILD_CUSTOM + -DFEATURE_ETHERNET=1 extra_scripts = ${esp32c6_common_LittleFS.extra_scripts} pre:tools/pio/pre_custom_esp32c6.py -[env:normal_ESP32c6_4M316k_LittleFS_CDC] +[env:normal_ESP32c6_4M316k_LittleFS_CDC_ETH] extends = esp32c6_common_LittleFS +build_flags = ${esp32c6_common_LittleFS.build_flags} + -DFEATURE_ETHERNET=1 lib_ignore = ${esp32c6_common_LittleFS.lib_ignore} ${no_ir.lib_ignore} diff --git a/platformio_esp32s2_envs.ini b/platformio_esp32s2_envs.ini index d97fd66415..f4b530945f 100644 --- a/platformio_esp32s2_envs.ini +++ b/platformio_esp32s2_envs.ini @@ -44,13 +44,14 @@ build_flags = ${esp32s2_common.build_flags} -DFEATURE_SD=1 -DPLUGIN_NEOPIXEL_COLLECTION -[env:neopixel_ESP32s2_4M316k_LittleFS_CDC] +[env:neopixel_ESP32s2_4M316k_LittleFS_CDC_ETH] extends = esp32s2_common_LittleFS board = esp32s2cdc build_flags = ${esp32s2_common_LittleFS.build_flags} -DFEATURE_ARDUINO_OTA=1 -DFEATURE_SD=1 -DPLUGIN_NEOPIXEL_COLLECTION + -DFEATURE_ETHERNET=1 [env:custom_IR_ESP32s2_4M316k_CDC] @@ -73,7 +74,7 @@ board = esp32s2cdc lib_ignore = ${esp32s2_common.lib_ignore} ${no_ir.lib_ignore} -[env:custom_ESP32s2_4M316k_LittleFS_CDC] +[env:custom_ESP32s2_4M316k_LittleFS_CDC_ETH] extends = esp32s2_common_LittleFS board = esp32s2cdc lib_ignore = ${esp32s2_common_LittleFS.lib_ignore} @@ -81,14 +82,18 @@ lib_ignore = ${esp32s2_common_LittleFS.lib_ignore} build_flags = ${esp32s2_common_LittleFS.build_flags} -DPLUGIN_BUILD_CUSTOM -DESP_CONSOLE_USB_CDC=y + -DFEATURE_ETHERNET=1 extra_scripts = ${esp32s2_common_LittleFS.extra_scripts} pre:tools/pio/pre_custom_esp32.py -[env:normal_ESP32s2_4M316k_LittleFS_CDC] +[env:normal_ESP32s2_4M316k_LittleFS_CDC_ETH] extends = esp32s2_common_LittleFS board = esp32s2cdc +build_flags = ${esp32s2_common_LittleFS.build_flags} + -DESP_CONSOLE_USB_CDC=y + -DFEATURE_ETHERNET=1 lib_ignore = ${esp32s2_common_LittleFS.lib_ignore} ${no_ir.lib_ignore} diff --git a/platformio_esp32s3_envs.ini b/platformio_esp32s3_envs.ini index 4d852a3483..a3d2a4e0b9 100644 --- a/platformio_esp32s3_envs.ini +++ b/platformio_esp32s3_envs.ini @@ -137,13 +137,14 @@ build_flags = ${esp32s3_common.build_flags} -DFEATURE_SD=1 -DPLUGIN_NEOPIXEL_COLLECTION -[env:neopixel_ESP32s3_4M316k_LittleFS_CDC] +[env:neopixel_ESP32s3_4M316k_LittleFS_CDC_ETH] extends = esp32s3_common_LittleFS board = esp32s3cdc-qio_qspi build_flags = ${esp32s3_common_LittleFS.build_flags} -DFEATURE_ARDUINO_OTA=1 -DFEATURE_SD=1 -DPLUGIN_NEOPIXEL_COLLECTION + -DFEATURE_ETHERNET=1 [env:custom_ESP32s3_8M1M_LittleFS_CDC_ETH] From 49005f30439af109af39cfb24afa95c7054c3a8e Mon Sep 17 00:00:00 2001 From: Ton Huisman Date: Mon, 20 May 2024 23:23:46 +0200 Subject: [PATCH 110/113] [P167] Correct interrupt function definitions --- src/src/PluginStructs/P167_data_struct.cpp | 4 ++-- src/src/PluginStructs/P167_data_struct.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/src/PluginStructs/P167_data_struct.cpp b/src/src/PluginStructs/P167_data_struct.cpp index 94eb882e09..e3b4f6c645 100644 --- a/src/src/PluginStructs/P167_data_struct.cpp +++ b/src/src/PluginStructs/P167_data_struct.cpp @@ -1217,7 +1217,7 @@ void P167_data_struct::clearSuccCount() { _readingsuccesscount = 0; } -void IRAM_ATTR P167_data_struct::checkPin_interrupt() { +void P167_data_struct::checkPin_interrupt() { monpinValue = monpinValue + 1; // volatile monpinLastTransitionTime = getMicros64(); @@ -1239,7 +1239,7 @@ void P167_data_struct::startCleaning() { // When using interrupts we have to call the library entry point // whenever an interrupt is triggered -void IRAM_ATTR P167_data_struct::Plugin_167_interrupt(P167_data_struct *self) { +void P167_data_struct::Plugin_167_interrupt(P167_data_struct *self) { // addLog(LOG_LEVEL_ERROR, F("********* SEN5X: interrupt apear!")); if (self) { self->checkPin_interrupt(); diff --git a/src/src/PluginStructs/P167_data_struct.h b/src/src/PluginStructs/P167_data_struct.h index 66d260330f..514f3320de 100644 --- a/src/src/PluginStructs/P167_data_struct.h +++ b/src/src/PluginStructs/P167_data_struct.h @@ -114,8 +114,8 @@ struct P167_data_struct : public PluginTaskData_base { ~P167_data_struct(); - void checkPin_interrupt(void); - static void Plugin_167_interrupt(P167_data_struct *self); + void IRAM_ATTR checkPin_interrupt(void); + static void IRAM_ATTR Plugin_167_interrupt(P167_data_struct *self); ///////////////////////////////////////////////////////// // This method runs the FSM step by step on each call From 06e458ea04fc68cad5f5a842ff778e189b04bdb1 Mon Sep 17 00:00:00 2001 From: TD-er Date: Thu, 23 May 2024 15:19:39 +0200 Subject: [PATCH 111/113] [Controller] Fail gracefully when almost out of memory One often reported issue is that the ESPEasy node (especially ESP8266 ones) will crash when WiFi is unstable. This could be due to memory suddenly filling up by the controller queue. When creating a new item for the controller queue, there was no `(std::nothrow)` included on the `new` calls, so if running out of memory this might have thrown a `std::bad_alloc` exception, which are not caught and thus ESPEasy might crash on it. --- src/_C001.cpp | 312 +- src/_C003.cpp | 312 +- src/_C004.cpp | 272 +- src/_C007.cpp | 262 +- src/_C008.cpp | 344 +- src/_C009.cpp | 434 +- src/_C010.cpp | 326 +- src/_C011.cpp | 740 +-- src/_C012.cpp | 250 +- src/_C015.cpp | 982 ++-- src/_C017.cpp | 304 +- src/_C018.cpp | 866 +-- .../DataStructs/ControllerSettingsStruct.h | 462 +- src/src/ESPEasyCore/Controller.cpp | 1282 ++-- src/src/PluginStructs/P104_data_struct.cpp | 5196 ++++++++--------- src/src/PluginStructs/P147_data_struct.cpp | 888 +-- src/src/WebServer/UploadPage.cpp | 516 +- 17 files changed, 6875 insertions(+), 6873 deletions(-) diff --git a/src/_C001.cpp b/src/_C001.cpp index ff5f4b1472..13e180aa09 100644 --- a/src/_C001.cpp +++ b/src/_C001.cpp @@ -1,156 +1,156 @@ -#include "src/Helpers/_CPlugin_Helper.h" -#ifdef USES_C001 - -# include "src/Helpers/_CPlugin_DomoticzHelper.h" - -// ####################################################################################################### -// ########################### Controller Plugin 001: Domoticz HTTP ###################################### -// ####################################################################################################### - -# define CPLUGIN_001 -# define CPLUGIN_ID_001 1 -# define CPLUGIN_NAME_001 "Domoticz HTTP" - - -bool CPlugin_001(CPlugin::Function function, struct EventStruct *event, String& string) -{ - bool success = false; - - switch (function) - { - case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: - { - ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_001; - proto.usesMQTT = false; - proto.usesAccount = true; - proto.usesPassword = true; - proto.usesExtCreds = true; - proto.defaultPort = 8080; - proto.usesID = true; - break; - } - - case CPlugin::Function::CPLUGIN_GET_DEVICENAME: - { - string = F(CPLUGIN_NAME_001); - break; - } - - case CPlugin::Function::CPLUGIN_INIT: - { - success = init_c001_delay_queue(event->ControllerIndex); - break; - } - - case CPlugin::Function::CPLUGIN_EXIT: - { - exit_c001_delay_queue(); - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: - { - if (C001_DelayHandler == nullptr || !validTaskIndex(event->TaskIndex)) { - break; - } - if (C001_DelayHandler->queueFull(event->ControllerIndex)) { - break; - } - - if (event->idx != 0) - { - // We now create a URI for the request - const Sensor_VType sensorType = event->getSensorType(); - String url; - const size_t expectedSize = sensorType == Sensor_VType::SENSOR_TYPE_STRING ? 64 + event->String2.length() : 128; - - if (reserve_special(url, expectedSize)) { - url = F("/json.htm?type=command¶m="); - - if (sensorType == Sensor_VType::SENSOR_TYPE_SWITCH || - sensorType == Sensor_VType::SENSOR_TYPE_DIMMER) - { - url += F("switchlight&idx="); - url += event->idx; - url += F("&switchcmd="); - - if (essentiallyZero(UserVar[event->BaseVarIndex])) { - url += F("Off"); - } else { - if (sensorType == Sensor_VType::SENSOR_TYPE_SWITCH) { - url += F("On"); - } else { - url += F("Set%20Level&level="); - url += UserVar[event->BaseVarIndex]; - } - } - } else { - url += F("udevice&idx="); - url += event->idx; - url += F("&nvalue=0"); - url += F("&svalue="); - url += formatDomoticzSensorType(event); - } - - // Add WiFi reception quality - url += F("&rssi="); - url += mapRSSItoDomoticz(); - # if FEATURE_ADC_VCC - url += F("&battery="); - url += mapVccToDomoticz(); - # endif // if FEATURE_ADC_VCC - - std::unique_ptr element(new C001_queue_element(event->ControllerIndex, event->TaskIndex, std::move(url))); - - success = C001_DelayHandler->addToQueue(std::move(element)); - Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C001_DELAY_QUEUE, - C001_DelayHandler->getNextScheduleTime()); - } - } // if ixd !=0 - else - { - addLog(LOG_LEVEL_ERROR, F("HTTP : IDX cannot be zero!")); - } - break; - } - - case CPlugin::Function::CPLUGIN_FLUSH: - { - process_c001_delay_queue(); - delay(0); - break; - } - - default: - break; - } - return success; -} - -// Uncrustify may change this into multi line, which will result in failed builds -// *INDENT-OFF* -bool do_process_c001_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { - const C001_queue_element& element = static_cast(element_base); - -// *INDENT-ON* - # ifndef BUILD_NO_DEBUG - - if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - addLog(LOG_LEVEL_DEBUG, element.txt); - } - # endif // ifndef BUILD_NO_DEBUG - - int httpCode = -1; - send_via_http( - controller_number, - ControllerSettings, - element._controller_idx, - element.txt, - F("GET"), - EMPTY_STRING, - EMPTY_STRING, - httpCode); - return (httpCode >= 100) && (httpCode < 300); -} - -#endif // ifdef USES_C001 +#include "src/Helpers/_CPlugin_Helper.h" +#ifdef USES_C001 + +# include "src/Helpers/_CPlugin_DomoticzHelper.h" + +// ####################################################################################################### +// ########################### Controller Plugin 001: Domoticz HTTP ###################################### +// ####################################################################################################### + +# define CPLUGIN_001 +# define CPLUGIN_ID_001 1 +# define CPLUGIN_NAME_001 "Domoticz HTTP" + + +bool CPlugin_001(CPlugin::Function function, struct EventStruct *event, String& string) +{ + bool success = false; + + switch (function) + { + case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: + { + ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_001; + proto.usesMQTT = false; + proto.usesAccount = true; + proto.usesPassword = true; + proto.usesExtCreds = true; + proto.defaultPort = 8080; + proto.usesID = true; + break; + } + + case CPlugin::Function::CPLUGIN_GET_DEVICENAME: + { + string = F(CPLUGIN_NAME_001); + break; + } + + case CPlugin::Function::CPLUGIN_INIT: + { + success = init_c001_delay_queue(event->ControllerIndex); + break; + } + + case CPlugin::Function::CPLUGIN_EXIT: + { + exit_c001_delay_queue(); + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: + { + if (C001_DelayHandler == nullptr || !validTaskIndex(event->TaskIndex)) { + break; + } + if (C001_DelayHandler->queueFull(event->ControllerIndex)) { + break; + } + + if (event->idx != 0) + { + // We now create a URI for the request + const Sensor_VType sensorType = event->getSensorType(); + String url; + const size_t expectedSize = sensorType == Sensor_VType::SENSOR_TYPE_STRING ? 64 + event->String2.length() : 128; + + if (reserve_special(url, expectedSize)) { + url = F("/json.htm?type=command¶m="); + + if (sensorType == Sensor_VType::SENSOR_TYPE_SWITCH || + sensorType == Sensor_VType::SENSOR_TYPE_DIMMER) + { + url += F("switchlight&idx="); + url += event->idx; + url += F("&switchcmd="); + + if (essentiallyZero(UserVar[event->BaseVarIndex])) { + url += F("Off"); + } else { + if (sensorType == Sensor_VType::SENSOR_TYPE_SWITCH) { + url += F("On"); + } else { + url += F("Set%20Level&level="); + url += UserVar[event->BaseVarIndex]; + } + } + } else { + url += F("udevice&idx="); + url += event->idx; + url += F("&nvalue=0"); + url += F("&svalue="); + url += formatDomoticzSensorType(event); + } + + // Add WiFi reception quality + url += F("&rssi="); + url += mapRSSItoDomoticz(); + # if FEATURE_ADC_VCC + url += F("&battery="); + url += mapVccToDomoticz(); + # endif // if FEATURE_ADC_VCC + + std::unique_ptr element(new (std::nothrow) C001_queue_element(event->ControllerIndex, event->TaskIndex, std::move(url))); + + success = C001_DelayHandler->addToQueue(std::move(element)); + Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C001_DELAY_QUEUE, + C001_DelayHandler->getNextScheduleTime()); + } + } // if ixd !=0 + else + { + addLog(LOG_LEVEL_ERROR, F("HTTP : IDX cannot be zero!")); + } + break; + } + + case CPlugin::Function::CPLUGIN_FLUSH: + { + process_c001_delay_queue(); + delay(0); + break; + } + + default: + break; + } + return success; +} + +// Uncrustify may change this into multi line, which will result in failed builds +// *INDENT-OFF* +bool do_process_c001_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { + const C001_queue_element& element = static_cast(element_base); + +// *INDENT-ON* + # ifndef BUILD_NO_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + addLog(LOG_LEVEL_DEBUG, element.txt); + } + # endif // ifndef BUILD_NO_DEBUG + + int httpCode = -1; + send_via_http( + controller_number, + ControllerSettings, + element._controller_idx, + element.txt, + F("GET"), + EMPTY_STRING, + EMPTY_STRING, + httpCode); + return (httpCode >= 100) && (httpCode < 300); +} + +#endif // ifdef USES_C001 diff --git a/src/_C003.cpp b/src/_C003.cpp index 56781de8bc..ca58458271 100644 --- a/src/_C003.cpp +++ b/src/_C003.cpp @@ -1,156 +1,156 @@ -#include "src/Helpers/_CPlugin_Helper.h" -#ifdef USES_C003 - -// ####################################################################################################### -// ########################### Controller Plugin 003: Nodo Telnet ####################################### -// ####################################################################################################### - -# define CPLUGIN_003 -# define CPLUGIN_ID_003 3 -# define CPLUGIN_NAME_003 "Nodo Telnet" - -bool CPlugin_003(CPlugin::Function function, struct EventStruct *event, String& string) -{ - bool success = false; - - switch (function) - { - case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: - { - ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_003; - proto.usesMQTT = false; - proto.usesAccount = false; - proto.usesPassword = true; - proto.defaultPort = 23; - proto.usesID = true; - break; - } - - case CPlugin::Function::CPLUGIN_GET_DEVICENAME: - { - string = F(CPLUGIN_NAME_003); - break; - } - - case CPlugin::Function::CPLUGIN_INIT: - { - success = init_c003_delay_queue(event->ControllerIndex); - break; - } - - case CPlugin::Function::CPLUGIN_EXIT: - { - exit_c003_delay_queue(); - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: - { - if (C003_DelayHandler == nullptr) { - break; - } - if (C003_DelayHandler->queueFull(event->ControllerIndex)) { - break; - } - - // We now create a URI for the request - String url = strformat( - F("variableset %d,%s\n"), - event->idx, - formatUserVarNoCheck(event, 0).c_str()); - std::unique_ptr element( - new C003_queue_element( - event->ControllerIndex, - event->TaskIndex, - std::move(url))); - - success = C003_DelayHandler->addToQueue(std::move(element)); - Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C003_DELAY_QUEUE, C003_DelayHandler->getNextScheduleTime()); - - break; - } - - case CPlugin::Function::CPLUGIN_FLUSH: - { - process_c003_delay_queue(); - delay(0); - break; - } - - default: - break; - } - return success; -} - -// Uncrustify may change this into multi line, which will result in failed builds -// *INDENT-OFF* -bool do_process_c003_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { - const C003_queue_element& element = static_cast(element_base); -// *INDENT-ON* - bool success = false; - - // Use WiFiClient class to create TCP connections - WiFiClient client; - - if (!try_connect_host(controller_number, client, ControllerSettings, F("TELNT: "))) - { - return success; - } - - // strcpy_P(log, PSTR("TELNT: Sending enter")); - // addLog(LOG_LEVEL_ERROR, log); - client.print(" \n"); - - unsigned long timer = millis() + 200; - - while (!client_available(client) && !timeOutReached(timer)) { - delay(1); - } - - timer = millis() + 1000; - - while (client_available(client) && !timeOutReached(timer) && !success) - { - // String line = client.readStringUntil('\n'); - String line; - safeReadStringUntil(client, line, '\n'); - - if (line.startsWith(F("Enter your password:"))) - { - success = true; - #ifndef BUILD_NO_DEBUG - addLog(LOG_LEVEL_DEBUG, F("TELNT: Password request ok")); - #endif - } - delay(1); - } - #ifndef BUILD_NO_DEBUG - addLog(LOG_LEVEL_DEBUG, F("TELNT: Sending pw")); - #endif - client.println(getControllerPass(element._controller_idx, ControllerSettings)); - delay(100); - - while (client_available(client)) { - client.read(); - } - - #ifndef BUILD_NO_DEBUG - addLog(LOG_LEVEL_DEBUG, F("TELNT: Sending cmd")); - #endif - client.print(element.txt); - delay(10); - - while (client_available(client)) { - client.read(); - } - - #ifndef BUILD_NO_DEBUG - addLog(LOG_LEVEL_DEBUG, F("TELNT: closing connection")); - #endif - - client.stop(); - return success; -} - -#endif // ifdef USES_C003 +#include "src/Helpers/_CPlugin_Helper.h" +#ifdef USES_C003 + +// ####################################################################################################### +// ########################### Controller Plugin 003: Nodo Telnet ####################################### +// ####################################################################################################### + +# define CPLUGIN_003 +# define CPLUGIN_ID_003 3 +# define CPLUGIN_NAME_003 "Nodo Telnet" + +bool CPlugin_003(CPlugin::Function function, struct EventStruct *event, String& string) +{ + bool success = false; + + switch (function) + { + case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: + { + ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_003; + proto.usesMQTT = false; + proto.usesAccount = false; + proto.usesPassword = true; + proto.defaultPort = 23; + proto.usesID = true; + break; + } + + case CPlugin::Function::CPLUGIN_GET_DEVICENAME: + { + string = F(CPLUGIN_NAME_003); + break; + } + + case CPlugin::Function::CPLUGIN_INIT: + { + success = init_c003_delay_queue(event->ControllerIndex); + break; + } + + case CPlugin::Function::CPLUGIN_EXIT: + { + exit_c003_delay_queue(); + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: + { + if (C003_DelayHandler == nullptr) { + break; + } + if (C003_DelayHandler->queueFull(event->ControllerIndex)) { + break; + } + + // We now create a URI for the request + String url = strformat( + F("variableset %d,%s\n"), + event->idx, + formatUserVarNoCheck(event, 0).c_str()); + std::unique_ptr element( + new (std::nothrow) C003_queue_element( + event->ControllerIndex, + event->TaskIndex, + std::move(url))); + + success = C003_DelayHandler->addToQueue(std::move(element)); + Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C003_DELAY_QUEUE, C003_DelayHandler->getNextScheduleTime()); + + break; + } + + case CPlugin::Function::CPLUGIN_FLUSH: + { + process_c003_delay_queue(); + delay(0); + break; + } + + default: + break; + } + return success; +} + +// Uncrustify may change this into multi line, which will result in failed builds +// *INDENT-OFF* +bool do_process_c003_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { + const C003_queue_element& element = static_cast(element_base); +// *INDENT-ON* + bool success = false; + + // Use WiFiClient class to create TCP connections + WiFiClient client; + + if (!try_connect_host(controller_number, client, ControllerSettings, F("TELNT: "))) + { + return success; + } + + // strcpy_P(log, PSTR("TELNT: Sending enter")); + // addLog(LOG_LEVEL_ERROR, log); + client.print(" \n"); + + unsigned long timer = millis() + 200; + + while (!client_available(client) && !timeOutReached(timer)) { + delay(1); + } + + timer = millis() + 1000; + + while (client_available(client) && !timeOutReached(timer) && !success) + { + // String line = client.readStringUntil('\n'); + String line; + safeReadStringUntil(client, line, '\n'); + + if (line.startsWith(F("Enter your password:"))) + { + success = true; + #ifndef BUILD_NO_DEBUG + addLog(LOG_LEVEL_DEBUG, F("TELNT: Password request ok")); + #endif + } + delay(1); + } + #ifndef BUILD_NO_DEBUG + addLog(LOG_LEVEL_DEBUG, F("TELNT: Sending pw")); + #endif + client.println(getControllerPass(element._controller_idx, ControllerSettings)); + delay(100); + + while (client_available(client)) { + client.read(); + } + + #ifndef BUILD_NO_DEBUG + addLog(LOG_LEVEL_DEBUG, F("TELNT: Sending cmd")); + #endif + client.print(element.txt); + delay(10); + + while (client_available(client)) { + client.read(); + } + + #ifndef BUILD_NO_DEBUG + addLog(LOG_LEVEL_DEBUG, F("TELNT: closing connection")); + #endif + + client.stop(); + return success; +} + +#endif // ifdef USES_C003 diff --git a/src/_C004.cpp b/src/_C004.cpp index 0c2f5328ed..aa63b2dad1 100644 --- a/src/_C004.cpp +++ b/src/_C004.cpp @@ -1,136 +1,136 @@ -#include "src/Helpers/_CPlugin_Helper.h" -#ifdef USES_C004 - -// ####################################################################################################### -// ########################### Controller Plugin 004: ThingSpeak ######################################### -// ####################################################################################################### - -# define CPLUGIN_004 -# define CPLUGIN_ID_004 4 -# define CPLUGIN_NAME_004 "ThingSpeak" - -bool CPlugin_004(CPlugin::Function function, struct EventStruct *event, String& string) -{ - bool success = false; - - switch (function) - { - case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: - { - ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_004; - proto.usesMQTT = false; - proto.usesAccount = true; - proto.usesPassword = true; - proto.defaultPort = 80; - proto.usesID = true; - break; - } - - case CPlugin::Function::CPLUGIN_GET_DEVICENAME: - { - string = F(CPLUGIN_NAME_004); - break; - } - - case CPlugin::Function::CPLUGIN_INIT: - { - success = init_c004_delay_queue(event->ControllerIndex); - break; - } - - case CPlugin::Function::CPLUGIN_EXIT: - { - exit_c004_delay_queue(); - break; - } - - case CPlugin::Function::CPLUGIN_GET_PROTOCOL_DISPLAY_NAME: - { - success = true; - - switch (event->idx) { - case ControllerSettingsStruct::CONTROLLER_USER: - string = F("ThingHTTP Name"); - break; - case ControllerSettingsStruct::CONTROLLER_PASS: - string = F("API Key"); - break; - default: - success = false; - break; - } - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: - { - if (C004_DelayHandler == nullptr) { - break; - } - if (C004_DelayHandler->queueFull(event->ControllerIndex)) { - break; - } - - std::unique_ptr element(new C004_queue_element(event)); - - success = C004_DelayHandler->addToQueue(std::move(element)); - Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C004_DELAY_QUEUE, C004_DelayHandler->getNextScheduleTime()); - - break; - } - - case CPlugin::Function::CPLUGIN_FLUSH: - { - process_c004_delay_queue(); - delay(0); - break; - } - - default: - break; - } - return success; -} - -// Uncrustify may change this into multi line, which will result in failed builds -// *INDENT-OFF* -bool do_process_c004_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { - const C004_queue_element& element = static_cast(element_base); -// *INDENT-ON* - String postDataStr = F("api_key="); - - postDataStr += getControllerPass(element._controller_idx, ControllerSettings); // used for API key - - if (element.sensorType == Sensor_VType::SENSOR_TYPE_STRING) { - postDataStr += F("&status="); - postDataStr += element.txt[0]; // FIXME TD-er: Is this correct? - // See: https://nl.mathworks.com/help/thingspeak/writedata.html - } else { - for (uint8_t x = 0; x < element.valueCount; x++) - { - postDataStr += F("&field"); - postDataStr += element.idx + x; - postDataStr += '='; - postDataStr += element.txt[x]; - } - } - if (!ControllerSettings.UseDNS) { - // Patch the ControllerSettings to make sure we're using a hostname instead of an IP address - ControllerSettings.setHostname(F("api.thingspeak.com")); // PM_CZ: HTTP requests must contain host headers. - ControllerSettings.UseDNS = true; - } - - int httpCode = -1; - send_via_http( - controller_number, - ControllerSettings, - element._controller_idx, - F("/update"), // uri - F("POST"), - F("Content-Type: application/x-www-form-urlencoded\r\n"), - postDataStr, - httpCode); - return (httpCode >= 100) && (httpCode < 300); -} - -#endif // ifdef USES_C004 +#include "src/Helpers/_CPlugin_Helper.h" +#ifdef USES_C004 + +// ####################################################################################################### +// ########################### Controller Plugin 004: ThingSpeak ######################################### +// ####################################################################################################### + +# define CPLUGIN_004 +# define CPLUGIN_ID_004 4 +# define CPLUGIN_NAME_004 "ThingSpeak" + +bool CPlugin_004(CPlugin::Function function, struct EventStruct *event, String& string) +{ + bool success = false; + + switch (function) + { + case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: + { + ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_004; + proto.usesMQTT = false; + proto.usesAccount = true; + proto.usesPassword = true; + proto.defaultPort = 80; + proto.usesID = true; + break; + } + + case CPlugin::Function::CPLUGIN_GET_DEVICENAME: + { + string = F(CPLUGIN_NAME_004); + break; + } + + case CPlugin::Function::CPLUGIN_INIT: + { + success = init_c004_delay_queue(event->ControllerIndex); + break; + } + + case CPlugin::Function::CPLUGIN_EXIT: + { + exit_c004_delay_queue(); + break; + } + + case CPlugin::Function::CPLUGIN_GET_PROTOCOL_DISPLAY_NAME: + { + success = true; + + switch (event->idx) { + case ControllerSettingsStruct::CONTROLLER_USER: + string = F("ThingHTTP Name"); + break; + case ControllerSettingsStruct::CONTROLLER_PASS: + string = F("API Key"); + break; + default: + success = false; + break; + } + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: + { + if (C004_DelayHandler == nullptr) { + break; + } + if (C004_DelayHandler->queueFull(event->ControllerIndex)) { + break; + } + + std::unique_ptr element(new (std::nothrow) C004_queue_element(event)); + + success = C004_DelayHandler->addToQueue(std::move(element)); + Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C004_DELAY_QUEUE, C004_DelayHandler->getNextScheduleTime()); + + break; + } + + case CPlugin::Function::CPLUGIN_FLUSH: + { + process_c004_delay_queue(); + delay(0); + break; + } + + default: + break; + } + return success; +} + +// Uncrustify may change this into multi line, which will result in failed builds +// *INDENT-OFF* +bool do_process_c004_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { + const C004_queue_element& element = static_cast(element_base); +// *INDENT-ON* + String postDataStr = F("api_key="); + + postDataStr += getControllerPass(element._controller_idx, ControllerSettings); // used for API key + + if (element.sensorType == Sensor_VType::SENSOR_TYPE_STRING) { + postDataStr += F("&status="); + postDataStr += element.txt[0]; // FIXME TD-er: Is this correct? + // See: https://nl.mathworks.com/help/thingspeak/writedata.html + } else { + for (uint8_t x = 0; x < element.valueCount; x++) + { + postDataStr += F("&field"); + postDataStr += element.idx + x; + postDataStr += '='; + postDataStr += element.txt[x]; + } + } + if (!ControllerSettings.UseDNS) { + // Patch the ControllerSettings to make sure we're using a hostname instead of an IP address + ControllerSettings.setHostname(F("api.thingspeak.com")); // PM_CZ: HTTP requests must contain host headers. + ControllerSettings.UseDNS = true; + } + + int httpCode = -1; + send_via_http( + controller_number, + ControllerSettings, + element._controller_idx, + F("/update"), // uri + F("POST"), + F("Content-Type: application/x-www-form-urlencoded\r\n"), + postDataStr, + httpCode); + return (httpCode >= 100) && (httpCode < 300); +} + +#endif // ifdef USES_C004 diff --git a/src/_C007.cpp b/src/_C007.cpp index f5dce1f02f..dcab924655 100644 --- a/src/_C007.cpp +++ b/src/_C007.cpp @@ -1,131 +1,131 @@ -#include "src/Helpers/_CPlugin_Helper.h" -#ifdef USES_C007 - -# include "src/ESPEasyCore/Serial.h" - -// ####################################################################################################### -// ########################### Controller Plugin 007: Emoncms ############################################ -// ####################################################################################################### - -# define CPLUGIN_007 -# define CPLUGIN_ID_007 7 -# define CPLUGIN_NAME_007 "Emoncms" - - -bool CPlugin_007(CPlugin::Function function, struct EventStruct *event, String& string) -{ - bool success = false; - - switch (function) - { - case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: - { - ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_007; - proto.usesMQTT = false; - proto.usesAccount = false; - proto.usesPassword = true; - proto.defaultPort = 80; - proto.usesID = true; - break; - } - - case CPlugin::Function::CPLUGIN_GET_DEVICENAME: - { - string = F(CPLUGIN_NAME_007); - break; - } - - case CPlugin::Function::CPLUGIN_INIT: - { - success = init_c007_delay_queue(event->ControllerIndex); - break; - } - - case CPlugin::Function::CPLUGIN_EXIT: - { - exit_c007_delay_queue(); - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: - { - if (C007_DelayHandler == nullptr) { - break; - } - if (C007_DelayHandler->queueFull(event->ControllerIndex)) { - break; - } - - - if (event->getSensorType() == Sensor_VType::SENSOR_TYPE_STRING) { - addLog(LOG_LEVEL_ERROR, F("emoncms : No support for Sensor_VType::SENSOR_TYPE_STRING")); - break; - } - const uint8_t valueCount = getValueCountForTask(event->TaskIndex); - - if ((valueCount == 0) || (valueCount > VARS_PER_TASK)) { - addLog(LOG_LEVEL_ERROR, F("emoncms : Unknown sensortype or too many sensor values")); - break; - } - - std::unique_ptr element(new C007_queue_element(event)); - success = C007_DelayHandler->addToQueue(std::move(element)); - - Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C007_DELAY_QUEUE, C007_DelayHandler->getNextScheduleTime()); - break; - } - - case CPlugin::Function::CPLUGIN_FLUSH: - { - process_c007_delay_queue(); - delay(0); - break; - } - - default: - break; - } - return success; -} - -// Uncrustify may change this into multi line, which will result in failed builds -// *INDENT-OFF* -bool do_process_c007_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { - const C007_queue_element& element = static_cast(element_base); -// *INDENT-ON* - String url = F("/emoncms/input/post.json?node="); - - url += Settings.Unit; - url += F("&json="); - - for (uint8_t i = 0; i < element.valueCount; ++i) { - url += (i == 0) ? '{' : ','; - url += F("field"); - url += element.idx + i; - url += ':'; - url += element.txt[i]; - } - url += '}'; - url += F("&apikey="); - url += getControllerPass(element._controller_idx, ControllerSettings); // "0UDNN17RW6XAS2E5" // api key - -#ifndef BUILD_NO_DEBUG - if (Settings.SerialLogLevel >= LOG_LEVEL_DEBUG_MORE) { - serialPrintln(url); - } -#endif - - int httpCode = -1; - send_via_http( - controller_number, - ControllerSettings, - element._controller_idx, - url, - F("GET"), - EMPTY_STRING, - EMPTY_STRING, - httpCode); - return (httpCode >= 100) && (httpCode < 300); -} - -#endif // ifdef USES_C007 +#include "src/Helpers/_CPlugin_Helper.h" +#ifdef USES_C007 + +# include "src/ESPEasyCore/Serial.h" + +// ####################################################################################################### +// ########################### Controller Plugin 007: Emoncms ############################################ +// ####################################################################################################### + +# define CPLUGIN_007 +# define CPLUGIN_ID_007 7 +# define CPLUGIN_NAME_007 "Emoncms" + + +bool CPlugin_007(CPlugin::Function function, struct EventStruct *event, String& string) +{ + bool success = false; + + switch (function) + { + case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: + { + ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_007; + proto.usesMQTT = false; + proto.usesAccount = false; + proto.usesPassword = true; + proto.defaultPort = 80; + proto.usesID = true; + break; + } + + case CPlugin::Function::CPLUGIN_GET_DEVICENAME: + { + string = F(CPLUGIN_NAME_007); + break; + } + + case CPlugin::Function::CPLUGIN_INIT: + { + success = init_c007_delay_queue(event->ControllerIndex); + break; + } + + case CPlugin::Function::CPLUGIN_EXIT: + { + exit_c007_delay_queue(); + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: + { + if (C007_DelayHandler == nullptr) { + break; + } + if (C007_DelayHandler->queueFull(event->ControllerIndex)) { + break; + } + + + if (event->getSensorType() == Sensor_VType::SENSOR_TYPE_STRING) { + addLog(LOG_LEVEL_ERROR, F("emoncms : No support for Sensor_VType::SENSOR_TYPE_STRING")); + break; + } + const uint8_t valueCount = getValueCountForTask(event->TaskIndex); + + if ((valueCount == 0) || (valueCount > VARS_PER_TASK)) { + addLog(LOG_LEVEL_ERROR, F("emoncms : Unknown sensortype or too many sensor values")); + break; + } + + std::unique_ptr element(new (std::nothrow) C007_queue_element(event)); + success = C007_DelayHandler->addToQueue(std::move(element)); + + Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C007_DELAY_QUEUE, C007_DelayHandler->getNextScheduleTime()); + break; + } + + case CPlugin::Function::CPLUGIN_FLUSH: + { + process_c007_delay_queue(); + delay(0); + break; + } + + default: + break; + } + return success; +} + +// Uncrustify may change this into multi line, which will result in failed builds +// *INDENT-OFF* +bool do_process_c007_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { + const C007_queue_element& element = static_cast(element_base); +// *INDENT-ON* + String url = F("/emoncms/input/post.json?node="); + + url += Settings.Unit; + url += F("&json="); + + for (uint8_t i = 0; i < element.valueCount; ++i) { + url += (i == 0) ? '{' : ','; + url += F("field"); + url += element.idx + i; + url += ':'; + url += element.txt[i]; + } + url += '}'; + url += F("&apikey="); + url += getControllerPass(element._controller_idx, ControllerSettings); // "0UDNN17RW6XAS2E5" // api key + +#ifndef BUILD_NO_DEBUG + if (Settings.SerialLogLevel >= LOG_LEVEL_DEBUG_MORE) { + serialPrintln(url); + } +#endif + + int httpCode = -1; + send_via_http( + controller_number, + ControllerSettings, + element._controller_idx, + url, + F("GET"), + EMPTY_STRING, + EMPTY_STRING, + httpCode); + return (httpCode >= 100) && (httpCode < 300); +} + +#endif // ifdef USES_C007 diff --git a/src/_C008.cpp b/src/_C008.cpp index 7101a10c1d..f9a3129882 100644 --- a/src/_C008.cpp +++ b/src/_C008.cpp @@ -1,172 +1,172 @@ -#include "src/Helpers/_CPlugin_Helper.h" - -#ifdef USES_C008 - -// ####################################################################################################### -// ########################### Controller Plugin 008: Generic HTTP ####################################### -// ####################################################################################################### - -# define CPLUGIN_008 -# define CPLUGIN_ID_008 8 -# define CPLUGIN_NAME_008 "Generic HTTP" - -bool CPlugin_008(CPlugin::Function function, struct EventStruct *event, String& string) -{ - bool success = false; - - switch (function) - { - case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: - { - ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_008; - proto.usesMQTT = false; - proto.usesTemplate = true; - proto.usesAccount = true; - proto.usesPassword = true; - proto.usesExtCreds = true; - proto.defaultPort = 80; - proto.usesID = true; - break; - } - - case CPlugin::Function::CPLUGIN_GET_DEVICENAME: - { - string = F(CPLUGIN_NAME_008); - break; - } - - case CPlugin::Function::CPLUGIN_INIT: - { - success = init_c008_delay_queue(event->ControllerIndex); - break; - } - - case CPlugin::Function::CPLUGIN_EXIT: - { - exit_c008_delay_queue(); - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_TEMPLATE: - { - event->String1 = String(); - event->String2 = F("demo.php?name=%sysname%&task=%tskname%&valuename=%valname%&value=%value%"); - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: - { - if (C008_DelayHandler == nullptr) { - break; - } - if (C008_DelayHandler->queueFull(event->ControllerIndex)) { - break; - } - - - String pubname; - { - // Place the ControllerSettings in a scope to free the memory as soon as we got all relevant information. - MakeControllerSettings(ControllerSettings); //-V522 - - if (!AllocatedControllerSettings()) { - addLog(LOG_LEVEL_ERROR, F("C008 : Generic HTTP - Cannot send, out of RAM")); - break; - } - LoadControllerSettings(event->ControllerIndex, *ControllerSettings); - pubname = ControllerSettings->Publish; - } - - uint8_t valueCount = getValueCountForTask(event->TaskIndex); - std::unique_ptr element(new C008_queue_element(event, valueCount)); - success = C008_DelayHandler->addToQueue(std::move(element)); - - if (success) { - // Element was added. - // Now we try to append to the existing element - // and thus preventing the need to create a long string only to copy it to a queue element. - C008_queue_element& element = static_cast(*(C008_DelayHandler->sendQueue.back())); - - // Collect the values at the same run, to make sure all are from the same sample - //LoadTaskSettings(event->TaskIndex); // FIXME TD-er: This can probably be removed - parseControllerVariables(pubname, event, true); - - for (uint8_t x = 0; x < valueCount; x++) - { - bool isvalid; - const String formattedValue = formatUserVar(event, x , isvalid); - - if (isvalid) { - // First store in a temporary string, so we can use move_special to allocate on the best heap - String txt; - txt += '/'; - txt += pubname; - parseSingleControllerVariable(txt, event, x, true); - -# ifndef BUILD_NO_DEBUG - if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - addLog(LOG_LEVEL_DEBUG, strformat( - F("C008 : pubname: %s value: %s"), - pubname.c_str(), - formattedValue.c_str() - )); - } -#endif - txt.replace(F("%value%"), formattedValue); - move_special(element.txt[x], std::move(txt)); -# ifndef BUILD_NO_DEBUG - if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) { - addLog(LOG_LEVEL_DEBUG_MORE, concat(F("C008 : "), element.txt[x])); - } -# endif // ifndef BUILD_NO_DEBUG - } - } - } - Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C008_DELAY_QUEUE, C008_DelayHandler->getNextScheduleTime()); - break; - } - - case CPlugin::Function::CPLUGIN_FLUSH: - { - process_c008_delay_queue(); - delay(0); - break; - } - - default: - break; - } - return success; -} - -// ******************************************************************************** -// Generic HTTP get request -// ******************************************************************************** - -// Uncrustify may change this into multi line, which will result in failed builds -// *INDENT-OFF* -bool do_process_c008_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { - const C008_queue_element& element = static_cast(element_base); -// *INDENT-ON* - while (element.txt[element.valuesSent].isEmpty()) { - // A non valid value, which we are not going to send. - // Increase sent counter until a valid value is found. - if (element.checkDone(true)) { - return true; - } - } - - int httpCode = -1; - send_via_http( - controller_number, - ControllerSettings, - element._controller_idx, - element.txt[element.valuesSent], - F("GET"), - EMPTY_STRING, - EMPTY_STRING, - httpCode); - return element.checkDone((httpCode >= 100) && (httpCode < 300)); -} - -#endif // ifdef USES_C008 +#include "src/Helpers/_CPlugin_Helper.h" + +#ifdef USES_C008 + +// ####################################################################################################### +// ########################### Controller Plugin 008: Generic HTTP ####################################### +// ####################################################################################################### + +# define CPLUGIN_008 +# define CPLUGIN_ID_008 8 +# define CPLUGIN_NAME_008 "Generic HTTP" + +bool CPlugin_008(CPlugin::Function function, struct EventStruct *event, String& string) +{ + bool success = false; + + switch (function) + { + case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: + { + ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_008; + proto.usesMQTT = false; + proto.usesTemplate = true; + proto.usesAccount = true; + proto.usesPassword = true; + proto.usesExtCreds = true; + proto.defaultPort = 80; + proto.usesID = true; + break; + } + + case CPlugin::Function::CPLUGIN_GET_DEVICENAME: + { + string = F(CPLUGIN_NAME_008); + break; + } + + case CPlugin::Function::CPLUGIN_INIT: + { + success = init_c008_delay_queue(event->ControllerIndex); + break; + } + + case CPlugin::Function::CPLUGIN_EXIT: + { + exit_c008_delay_queue(); + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_TEMPLATE: + { + event->String1 = String(); + event->String2 = F("demo.php?name=%sysname%&task=%tskname%&valuename=%valname%&value=%value%"); + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: + { + if (C008_DelayHandler == nullptr) { + break; + } + if (C008_DelayHandler->queueFull(event->ControllerIndex)) { + break; + } + + + String pubname; + { + // Place the ControllerSettings in a scope to free the memory as soon as we got all relevant information. + MakeControllerSettings(ControllerSettings); //-V522 + + if (!AllocatedControllerSettings()) { + addLog(LOG_LEVEL_ERROR, F("C008 : Generic HTTP - Cannot send, out of RAM")); + break; + } + LoadControllerSettings(event->ControllerIndex, *ControllerSettings); + pubname = ControllerSettings->Publish; + } + + uint8_t valueCount = getValueCountForTask(event->TaskIndex); + std::unique_ptr element(new (std::nothrow) C008_queue_element(event, valueCount)); + success = C008_DelayHandler->addToQueue(std::move(element)); + + if (success) { + // Element was added. + // Now we try to append to the existing element + // and thus preventing the need to create a long string only to copy it to a queue element. + C008_queue_element& element = static_cast(*(C008_DelayHandler->sendQueue.back())); + + // Collect the values at the same run, to make sure all are from the same sample + //LoadTaskSettings(event->TaskIndex); // FIXME TD-er: This can probably be removed + parseControllerVariables(pubname, event, true); + + for (uint8_t x = 0; x < valueCount; x++) + { + bool isvalid; + const String formattedValue = formatUserVar(event, x , isvalid); + + if (isvalid) { + // First store in a temporary string, so we can use move_special to allocate on the best heap + String txt; + txt += '/'; + txt += pubname; + parseSingleControllerVariable(txt, event, x, true); + +# ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + addLog(LOG_LEVEL_DEBUG, strformat( + F("C008 : pubname: %s value: %s"), + pubname.c_str(), + formattedValue.c_str() + )); + } +#endif + txt.replace(F("%value%"), formattedValue); + move_special(element.txt[x], std::move(txt)); +# ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) { + addLog(LOG_LEVEL_DEBUG_MORE, concat(F("C008 : "), element.txt[x])); + } +# endif // ifndef BUILD_NO_DEBUG + } + } + } + Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C008_DELAY_QUEUE, C008_DelayHandler->getNextScheduleTime()); + break; + } + + case CPlugin::Function::CPLUGIN_FLUSH: + { + process_c008_delay_queue(); + delay(0); + break; + } + + default: + break; + } + return success; +} + +// ******************************************************************************** +// Generic HTTP get request +// ******************************************************************************** + +// Uncrustify may change this into multi line, which will result in failed builds +// *INDENT-OFF* +bool do_process_c008_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { + const C008_queue_element& element = static_cast(element_base); +// *INDENT-ON* + while (element.txt[element.valuesSent].isEmpty()) { + // A non valid value, which we are not going to send. + // Increase sent counter until a valid value is found. + if (element.checkDone(true)) { + return true; + } + } + + int httpCode = -1; + send_via_http( + controller_number, + ControllerSettings, + element._controller_idx, + element.txt[element.valuesSent], + F("GET"), + EMPTY_STRING, + EMPTY_STRING, + httpCode); + return element.checkDone((httpCode >= 100) && (httpCode < 300)); +} + +#endif // ifdef USES_C008 diff --git a/src/_C009.cpp b/src/_C009.cpp index 5fe1bbcd2c..47f7a4cadb 100644 --- a/src/_C009.cpp +++ b/src/_C009.cpp @@ -1,217 +1,217 @@ -#include "src/Helpers/_CPlugin_Helper.h" -#ifdef USES_C009 - -#include "src/DataTypes/NodeTypeID.h" -#include "src/Helpers/StringProvider.h" -#include "src/CustomBuild/ESPEasy_buildinfo.h" - -// ####################################################################################################### -// ########################### Controller Plugin 009: FHEM HTTP ########################################## -// ####################################################################################################### - -/******************************************************************************* - * Copyright 2016-2017 dev0 - * Contact: https://forum.fhem.de/index.php?action=profile;u=7465 - * https://github.com/ddtlabs/ - * - * Release notes: - - v1.0 - - changed switch and dimmer setreading cmds - - v1.01 - - added json content to http requests - - v1.02 - - some optimizations as requested by mvdbro - - fixed JSON TaskDeviceValueDecimals handling - - ArduinoJson Library v5.6.4 required (as used by stable R120) - - parse for HTTP errors 400, 401 - - moved on/off translation for Sensor_VType::SENSOR_TYPE_SWITCH/DIMMER to FHEM module - - v1.03 - - changed http request from GET to POST (RFC conform) - - removed obsolete http get url code - - v1.04 - - added build options and node_type_id to JSON/device - ******************************************************************************/ - -# define CPLUGIN_009 -# define CPLUGIN_ID_009 9 -# define CPLUGIN_NAME_009 "FHEM HTTP" - -bool CPlugin_009(CPlugin::Function function, struct EventStruct *event, String& string) -{ - bool success = false; - - switch (function) - { - case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: - { - ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_009; - proto.usesMQTT = false; - proto.usesTemplate = false; - proto.usesAccount = true; - proto.usesPassword = true; - proto.usesExtCreds = true; - proto.usesID = false; - proto.defaultPort = 8383; - break; - } - - case CPlugin::Function::CPLUGIN_GET_DEVICENAME: - { - string = F(CPLUGIN_NAME_009); - break; - } - - case CPlugin::Function::CPLUGIN_INIT: - { - success = init_c009_delay_queue(event->ControllerIndex); - break; - } - - case CPlugin::Function::CPLUGIN_EXIT: - { - exit_c009_delay_queue(); - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: - { - if (C009_DelayHandler != nullptr) { - if (C009_DelayHandler->queueFull(event->ControllerIndex)) { - break; - } - - std::unique_ptr element(new C009_queue_element(event)); - success = C009_DelayHandler->addToQueue(std::move(element)); - Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C009_DELAY_QUEUE, C009_DelayHandler->getNextScheduleTime()); - } - break; - } - - case CPlugin::Function::CPLUGIN_FLUSH: - { - process_c009_delay_queue(); - delay(0); - break; - } - - default: - break; - } - return success; -} - -/*********************************************************************************************\ -* FHEM HTTP request -\*********************************************************************************************/ - -// Uncrustify may change this into multi line, which will result in failed builds -// *INDENT-OFF* -bool do_process_c009_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { - const C009_queue_element& element = static_cast(element_base); -// *INDENT-ON* - String jsonString; - // Make an educated guess on the actual length, based on earlier requests. - static size_t expectedJsonLength = 100; - { - // Reserve on the heap with most space - if (!reserve_special(jsonString, expectedJsonLength)) { - // Not enough free memory - return false; - } - } - { - jsonString += '{'; - { - jsonString += to_json_object_value(F("module"), F("ESPEasy")); - jsonString += ','; - jsonString += to_json_object_value(F("version"), F("1.04")); - - // Create nested object "ESP" inside "data" - jsonString += ','; - jsonString += F("\"data\":{"); - { - jsonString += F("\"ESP\":{"); - { - // Create nested objects in "ESP": - jsonString += to_json_object_value(F("name"), Settings.getName()); - jsonString += ','; - jsonString += to_json_object_value(F("unit"), static_cast(Settings.Unit)); - jsonString += ','; - jsonString += to_json_object_value(F("version"), static_cast(Settings.Version)); - jsonString += ','; - jsonString += to_json_object_value(F("build"), static_cast(Settings.Build)); - jsonString += ','; - jsonString += to_json_object_value(F("build_notes"), F(BUILD_NOTES)); - jsonString += ','; - jsonString += to_json_object_value(F("build_git"), getValue(LabelType::GIT_BUILD)); - jsonString += ','; - jsonString += to_json_object_value(F("node_type_id"), static_cast(NODE_TYPE_ID)); - jsonString += ','; - jsonString += to_json_object_value(F("sleep"), static_cast(Settings.deepSleep_wakeTime)); - - // embed IP, important if there is NAT/PAT - // char ipStr[20]; - // IPAddress ip = NetworkLocalIP(); - // sprintf_P(ipStr, PSTR("%u.%u.%u.%u"), ip[0], ip[1], ip[2], ip[3]); - jsonString += ','; - jsonString += to_json_object_value(F("ip"), formatIP(NetworkLocalIP())); - } - jsonString += '}'; // End "ESP" - - jsonString += ','; - - // Create nested object "SENSOR" json object inside "data" - jsonString += F("\"SENSOR\":{"); - { - // char itemNames[valueCount][2]; - for (uint8_t x = 0; x < element.valueCount; x++) - { - // Each sensor value get an own object (0..n) - // sprintf(itemNames[x],"%d",x); - if (x != 0) { - jsonString += ','; - } - - jsonString += '"'; - jsonString += x; - jsonString += F("\":{"); - { - jsonString += to_json_object_value(F("deviceName"), getTaskDeviceName(element._taskIndex)); - jsonString += ','; - jsonString += to_json_object_value(F("valueName"), getTaskValueName(element._taskIndex, x)); - jsonString += ','; - jsonString += to_json_object_value(F("type"), static_cast(element.sensorType)); - jsonString += ','; - jsonString += to_json_object_value(F("value"), element.txt[x]); - } - jsonString += '}'; // End "sensor value N" - } - } - jsonString += '}'; // End "SENSOR" - } - jsonString += '}'; // End "data" - } - jsonString += '}'; // End JSON structure - } - - if (expectedJsonLength < jsonString.length()) { - expectedJsonLength = jsonString.length(); - } - - // addLog(LOG_LEVEL_INFO, F("C009 Test JSON:")); - // addLog(LOG_LEVEL_INFO, jsonString); - - int httpCode = -1; - send_via_http( - controller_number, - ControllerSettings, - element._controller_idx, - F("/ESPEasy"), - F("POST"), - EMPTY_STRING, - jsonString, - httpCode); - return (httpCode >= 100) && (httpCode < 300); -} - -#endif // ifdef USES_C009 +#include "src/Helpers/_CPlugin_Helper.h" +#ifdef USES_C009 + +#include "src/DataTypes/NodeTypeID.h" +#include "src/Helpers/StringProvider.h" +#include "src/CustomBuild/ESPEasy_buildinfo.h" + +// ####################################################################################################### +// ########################### Controller Plugin 009: FHEM HTTP ########################################## +// ####################################################################################################### + +/******************************************************************************* + * Copyright 2016-2017 dev0 + * Contact: https://forum.fhem.de/index.php?action=profile;u=7465 + * https://github.com/ddtlabs/ + * + * Release notes: + - v1.0 + - changed switch and dimmer setreading cmds + - v1.01 + - added json content to http requests + - v1.02 + - some optimizations as requested by mvdbro + - fixed JSON TaskDeviceValueDecimals handling + - ArduinoJson Library v5.6.4 required (as used by stable R120) + - parse for HTTP errors 400, 401 + - moved on/off translation for Sensor_VType::SENSOR_TYPE_SWITCH/DIMMER to FHEM module + - v1.03 + - changed http request from GET to POST (RFC conform) + - removed obsolete http get url code + - v1.04 + - added build options and node_type_id to JSON/device + ******************************************************************************/ + +# define CPLUGIN_009 +# define CPLUGIN_ID_009 9 +# define CPLUGIN_NAME_009 "FHEM HTTP" + +bool CPlugin_009(CPlugin::Function function, struct EventStruct *event, String& string) +{ + bool success = false; + + switch (function) + { + case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: + { + ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_009; + proto.usesMQTT = false; + proto.usesTemplate = false; + proto.usesAccount = true; + proto.usesPassword = true; + proto.usesExtCreds = true; + proto.usesID = false; + proto.defaultPort = 8383; + break; + } + + case CPlugin::Function::CPLUGIN_GET_DEVICENAME: + { + string = F(CPLUGIN_NAME_009); + break; + } + + case CPlugin::Function::CPLUGIN_INIT: + { + success = init_c009_delay_queue(event->ControllerIndex); + break; + } + + case CPlugin::Function::CPLUGIN_EXIT: + { + exit_c009_delay_queue(); + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: + { + if (C009_DelayHandler != nullptr) { + if (C009_DelayHandler->queueFull(event->ControllerIndex)) { + break; + } + + std::unique_ptr element(new (std::nothrow) C009_queue_element(event)); + success = C009_DelayHandler->addToQueue(std::move(element)); + Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C009_DELAY_QUEUE, C009_DelayHandler->getNextScheduleTime()); + } + break; + } + + case CPlugin::Function::CPLUGIN_FLUSH: + { + process_c009_delay_queue(); + delay(0); + break; + } + + default: + break; + } + return success; +} + +/*********************************************************************************************\ +* FHEM HTTP request +\*********************************************************************************************/ + +// Uncrustify may change this into multi line, which will result in failed builds +// *INDENT-OFF* +bool do_process_c009_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { + const C009_queue_element& element = static_cast(element_base); +// *INDENT-ON* + String jsonString; + // Make an educated guess on the actual length, based on earlier requests. + static size_t expectedJsonLength = 100; + { + // Reserve on the heap with most space + if (!reserve_special(jsonString, expectedJsonLength)) { + // Not enough free memory + return false; + } + } + { + jsonString += '{'; + { + jsonString += to_json_object_value(F("module"), F("ESPEasy")); + jsonString += ','; + jsonString += to_json_object_value(F("version"), F("1.04")); + + // Create nested object "ESP" inside "data" + jsonString += ','; + jsonString += F("\"data\":{"); + { + jsonString += F("\"ESP\":{"); + { + // Create nested objects in "ESP": + jsonString += to_json_object_value(F("name"), Settings.getName()); + jsonString += ','; + jsonString += to_json_object_value(F("unit"), static_cast(Settings.Unit)); + jsonString += ','; + jsonString += to_json_object_value(F("version"), static_cast(Settings.Version)); + jsonString += ','; + jsonString += to_json_object_value(F("build"), static_cast(Settings.Build)); + jsonString += ','; + jsonString += to_json_object_value(F("build_notes"), F(BUILD_NOTES)); + jsonString += ','; + jsonString += to_json_object_value(F("build_git"), getValue(LabelType::GIT_BUILD)); + jsonString += ','; + jsonString += to_json_object_value(F("node_type_id"), static_cast(NODE_TYPE_ID)); + jsonString += ','; + jsonString += to_json_object_value(F("sleep"), static_cast(Settings.deepSleep_wakeTime)); + + // embed IP, important if there is NAT/PAT + // char ipStr[20]; + // IPAddress ip = NetworkLocalIP(); + // sprintf_P(ipStr, PSTR("%u.%u.%u.%u"), ip[0], ip[1], ip[2], ip[3]); + jsonString += ','; + jsonString += to_json_object_value(F("ip"), formatIP(NetworkLocalIP())); + } + jsonString += '}'; // End "ESP" + + jsonString += ','; + + // Create nested object "SENSOR" json object inside "data" + jsonString += F("\"SENSOR\":{"); + { + // char itemNames[valueCount][2]; + for (uint8_t x = 0; x < element.valueCount; x++) + { + // Each sensor value get an own object (0..n) + // sprintf(itemNames[x],"%d",x); + if (x != 0) { + jsonString += ','; + } + + jsonString += '"'; + jsonString += x; + jsonString += F("\":{"); + { + jsonString += to_json_object_value(F("deviceName"), getTaskDeviceName(element._taskIndex)); + jsonString += ','; + jsonString += to_json_object_value(F("valueName"), getTaskValueName(element._taskIndex, x)); + jsonString += ','; + jsonString += to_json_object_value(F("type"), static_cast(element.sensorType)); + jsonString += ','; + jsonString += to_json_object_value(F("value"), element.txt[x]); + } + jsonString += '}'; // End "sensor value N" + } + } + jsonString += '}'; // End "SENSOR" + } + jsonString += '}'; // End "data" + } + jsonString += '}'; // End JSON structure + } + + if (expectedJsonLength < jsonString.length()) { + expectedJsonLength = jsonString.length(); + } + + // addLog(LOG_LEVEL_INFO, F("C009 Test JSON:")); + // addLog(LOG_LEVEL_INFO, jsonString); + + int httpCode = -1; + send_via_http( + controller_number, + ControllerSettings, + element._controller_idx, + F("/ESPEasy"), + F("POST"), + EMPTY_STRING, + jsonString, + httpCode); + return (httpCode >= 100) && (httpCode < 300); +} + +#endif // ifdef USES_C009 diff --git a/src/_C010.cpp b/src/_C010.cpp index 8b361fe4a1..c4c1904907 100644 --- a/src/_C010.cpp +++ b/src/_C010.cpp @@ -1,163 +1,163 @@ -#include "src/Helpers/_CPlugin_Helper.h" -#ifdef USES_C010 - -// ####################################################################################################### -// ########################### Controller Plugin 010: Generic UDP ######################################## -// ####################################################################################################### - -# define CPLUGIN_010 -# define CPLUGIN_ID_010 10 -# define CPLUGIN_NAME_010 "Generic UDP" - -bool CPlugin_010(CPlugin::Function function, struct EventStruct *event, String& string) -{ - bool success = false; - - switch (function) - { - case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: - { - ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_010; - proto.usesMQTT = false; - proto.usesTemplate = true; - proto.usesAccount = false; - proto.usesPassword = false; - proto.defaultPort = 514; - proto.usesID = false; - break; - } - - case CPlugin::Function::CPLUGIN_GET_DEVICENAME: - { - string = F(CPLUGIN_NAME_010); - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_TEMPLATE: - { - event->String1 = String(); - event->String2 = F("%sysname%_%tskname%_%valname%=%value%"); - break; - } - - case CPlugin::Function::CPLUGIN_INIT: - { - success = init_c010_delay_queue(event->ControllerIndex); - break; - } - - case CPlugin::Function::CPLUGIN_EXIT: - { - exit_c010_delay_queue(); - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: - { - if (C010_DelayHandler == nullptr) { - break; - } - if (C010_DelayHandler->queueFull(event->ControllerIndex)) { - break; - } - - const uint8_t valueCount = getValueCountForTask(event->TaskIndex); - - if (valueCount == 0) { - break; - } - - //LoadTaskSettings(event->TaskIndex); // FIXME TD-er: This can probably be removed - - std::unique_ptr element(new C010_queue_element(event, valueCount)); - - - { - String pubname; - { - MakeControllerSettings(ControllerSettings); //-V522 - - if (!AllocatedControllerSettings()) { - break; - } - LoadControllerSettings(event->ControllerIndex, *ControllerSettings); - pubname = ControllerSettings->Publish; - } - - parseControllerVariables(pubname, event, false); - - for (uint8_t x = 0; x < valueCount; x++) - { - bool isvalid; - const String formattedValue = formatUserVar(event, x, isvalid); - - if (isvalid) { - String txt; - txt = pubname; - parseSingleControllerVariable(txt, event, x, false); - txt.replace(F("%value%"), formattedValue); - move_special(element->txt[x], std::move(txt)); -#ifndef BUILD_NO_DEBUG - if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) - addLog(LOG_LEVEL_DEBUG_MORE, element->txt[x]); -#endif - } - } - } - - success = C010_DelayHandler->addToQueue(std::move(element)); - Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C010_DELAY_QUEUE, C010_DelayHandler->getNextScheduleTime()); - break; - } - - case CPlugin::Function::CPLUGIN_FLUSH: - { - process_c010_delay_queue(); - delay(0); - break; - } - - default: - break; - } - return success; -} - -// ******************************************************************************** -// Generic UDP message -// ******************************************************************************** - -// Uncrustify may change this into multi line, which will result in failed builds -// *INDENT-OFF* -bool do_process_c010_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { - const C010_queue_element& element = static_cast(element_base); -// *INDENT-ON* - while (element.txt[element.valuesSent].isEmpty()) { - // A non valid value, which we are not going to send. - // Increase sent counter until a valid value is found. - if (element.checkDone(true)) { - return true; - } - } - WiFiUDP C010_portUDP; - - if (!beginWiFiUDP_randomPort(C010_portUDP)) { return false; } - - if (!try_connect_host(controller_number, C010_portUDP, ControllerSettings)) { - return false; - } - - C010_portUDP.write( - reinterpret_cast(element.txt[element.valuesSent].c_str()), - element.txt[element.valuesSent].length()); - bool reply = C010_portUDP.endPacket(); - - C010_portUDP.stop(); - - if (ControllerSettings.MustCheckReply) { - return element.checkDone(reply); - } - return element.checkDone(true); -} - -#endif // ifdef USES_C010 +#include "src/Helpers/_CPlugin_Helper.h" +#ifdef USES_C010 + +// ####################################################################################################### +// ########################### Controller Plugin 010: Generic UDP ######################################## +// ####################################################################################################### + +# define CPLUGIN_010 +# define CPLUGIN_ID_010 10 +# define CPLUGIN_NAME_010 "Generic UDP" + +bool CPlugin_010(CPlugin::Function function, struct EventStruct *event, String& string) +{ + bool success = false; + + switch (function) + { + case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: + { + ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_010; + proto.usesMQTT = false; + proto.usesTemplate = true; + proto.usesAccount = false; + proto.usesPassword = false; + proto.defaultPort = 514; + proto.usesID = false; + break; + } + + case CPlugin::Function::CPLUGIN_GET_DEVICENAME: + { + string = F(CPLUGIN_NAME_010); + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_TEMPLATE: + { + event->String1 = String(); + event->String2 = F("%sysname%_%tskname%_%valname%=%value%"); + break; + } + + case CPlugin::Function::CPLUGIN_INIT: + { + success = init_c010_delay_queue(event->ControllerIndex); + break; + } + + case CPlugin::Function::CPLUGIN_EXIT: + { + exit_c010_delay_queue(); + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: + { + if (C010_DelayHandler == nullptr) { + break; + } + if (C010_DelayHandler->queueFull(event->ControllerIndex)) { + break; + } + + const uint8_t valueCount = getValueCountForTask(event->TaskIndex); + + if (valueCount == 0) { + break; + } + + //LoadTaskSettings(event->TaskIndex); // FIXME TD-er: This can probably be removed + + std::unique_ptr element(new (std::nothrow) C010_queue_element(event, valueCount)); + + + { + String pubname; + { + MakeControllerSettings(ControllerSettings); //-V522 + + if (!AllocatedControllerSettings()) { + break; + } + LoadControllerSettings(event->ControllerIndex, *ControllerSettings); + pubname = ControllerSettings->Publish; + } + + parseControllerVariables(pubname, event, false); + + for (uint8_t x = 0; x < valueCount; x++) + { + bool isvalid; + const String formattedValue = formatUserVar(event, x, isvalid); + + if (isvalid) { + String txt; + txt = pubname; + parseSingleControllerVariable(txt, event, x, false); + txt.replace(F("%value%"), formattedValue); + move_special(element->txt[x], std::move(txt)); +#ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) + addLog(LOG_LEVEL_DEBUG_MORE, element->txt[x]); +#endif + } + } + } + + success = C010_DelayHandler->addToQueue(std::move(element)); + Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C010_DELAY_QUEUE, C010_DelayHandler->getNextScheduleTime()); + break; + } + + case CPlugin::Function::CPLUGIN_FLUSH: + { + process_c010_delay_queue(); + delay(0); + break; + } + + default: + break; + } + return success; +} + +// ******************************************************************************** +// Generic UDP message +// ******************************************************************************** + +// Uncrustify may change this into multi line, which will result in failed builds +// *INDENT-OFF* +bool do_process_c010_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { + const C010_queue_element& element = static_cast(element_base); +// *INDENT-ON* + while (element.txt[element.valuesSent].isEmpty()) { + // A non valid value, which we are not going to send. + // Increase sent counter until a valid value is found. + if (element.checkDone(true)) { + return true; + } + } + WiFiUDP C010_portUDP; + + if (!beginWiFiUDP_randomPort(C010_portUDP)) { return false; } + + if (!try_connect_host(controller_number, C010_portUDP, ControllerSettings)) { + return false; + } + + C010_portUDP.write( + reinterpret_cast(element.txt[element.valuesSent].c_str()), + element.txt[element.valuesSent].length()); + bool reply = C010_portUDP.endPacket(); + + C010_portUDP.stop(); + + if (ControllerSettings.MustCheckReply) { + return element.checkDone(reply); + } + return element.checkDone(true); +} + +#endif // ifdef USES_C010 diff --git a/src/_C011.cpp b/src/_C011.cpp index 60effe70d2..8c091d5ea8 100644 --- a/src/_C011.cpp +++ b/src/_C011.cpp @@ -1,370 +1,370 @@ -#include "src/Helpers/_CPlugin_Helper.h" -#ifdef USES_C011 - -// ####################################################################################################### -// ########################### Controller Plugin 011: Generic HTTP Advanced ############################## -// ####################################################################################################### - -# define CPLUGIN_011 -# define CPLUGIN_ID_011 11 -# define CPLUGIN_NAME_011 "Generic HTTP Advanced" - -# define C011_HTTP_METHOD_MAX_LEN 16 -# define C011_HTTP_URI_MAX_LEN 240 -# define C011_HTTP_HEADER_MAX_LEN 256 -# define C011_HTTP_BODY_MAX_LEN 512 - - -bool C011_sendBinary = false; - -struct C011_ConfigStruct -{ - void zero_last() { - HttpMethod[C011_HTTP_METHOD_MAX_LEN - 1] = 0; - HttpUri[C011_HTTP_URI_MAX_LEN - 1] = 0; - HttpHeader[C011_HTTP_HEADER_MAX_LEN - 1] = 0; - HttpBody[C011_HTTP_BODY_MAX_LEN - 1] = 0; - } - - char HttpMethod[C011_HTTP_METHOD_MAX_LEN] = { 0 }; - char HttpUri[C011_HTTP_URI_MAX_LEN] = { 0 }; - char HttpHeader[C011_HTTP_HEADER_MAX_LEN] = { 0 }; - char HttpBody[C011_HTTP_BODY_MAX_LEN] = { 0 }; -}; - - -// Forward declarations -bool load_C011_ConfigStruct(controllerIndex_t ControllerIndex, String& HttpMethod, String& HttpUri, String& HttpHeader, String& HttpBody); -boolean Create_schedule_HTTP_C011(struct EventStruct *event); -void DeleteNotNeededValues(String& s, uint8_t numberOfValuesWanted); -void ReplaceTokenByValue(String& s, struct EventStruct *event, bool sendBinary); - - - -bool CPlugin_011(CPlugin::Function function, struct EventStruct *event, String& string) -{ - bool success = false; - - switch (function) - { - case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: - { - ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_011; - proto.usesMQTT = false; - proto.usesAccount = true; - proto.usesPassword = true; - proto.usesExtCreds = true; - proto.defaultPort = 80; - proto.usesID = false; - break; - } - - case CPlugin::Function::CPLUGIN_GET_DEVICENAME: - { - string = F(CPLUGIN_NAME_011); - break; - } - - case CPlugin::Function::CPLUGIN_INIT: - { - { - MakeControllerSettings(ControllerSettings); //-V522 - - if (AllocatedControllerSettings()) { - LoadControllerSettings(event->ControllerIndex, *ControllerSettings); - C011_sendBinary = ControllerSettings->sendBinary(); - } - } - success = init_c011_delay_queue(event->ControllerIndex); - break; - } - - case CPlugin::Function::CPLUGIN_EXIT: - { - exit_c011_delay_queue(); - break; - } - - case CPlugin::Function::CPLUGIN_WEBFORM_LOAD: - { - { - String HttpMethod; - String HttpUri; - String HttpHeader; - String HttpBody; - - if (!load_C011_ConfigStruct(event->ControllerIndex, HttpMethod, HttpUri, HttpHeader, HttpBody)) - { - return false; - } - addTableSeparator(F("HTTP Config"), 2, 3); - { - uint8_t choice = 0; - const __FlashStringHelper * methods[] = { F("GET"), F("POST"), F("PUT"), F("HEAD"), F("PATCH") }; - - for (uint8_t i = 0; i < 5; i++) - { - if (HttpMethod.equals(methods[i])) { - choice = i; - } - } - addFormSelector(F("Method"), F("P011httpmethod"), 5, methods, nullptr, choice); - } - - addFormTextBox(F("URI"), F("P011httpuri"), HttpUri, C011_HTTP_URI_MAX_LEN - 1); - { - htmlEscape(HttpHeader); - addFormTextArea(F("Header"), F("P011httpheader"), HttpHeader, C011_HTTP_HEADER_MAX_LEN - 1, 4, 50); - } - { - htmlEscape(HttpBody); - addFormTextArea(F("Body"), F("P011httpbody"), HttpBody, C011_HTTP_BODY_MAX_LEN - 1, 8, 50); - } - } - { - // Place in scope to delete ControllerSettings as soon as it is no longer needed - MakeControllerSettings(ControllerSettings); //-V522 - - if (!AllocatedControllerSettings()) { - addHtmlError(F("Out of memory, cannot load page")); - } else { - LoadControllerSettings(event->ControllerIndex, *ControllerSettings); - addControllerParameterForm(*ControllerSettings, event->ControllerIndex, ControllerSettingsStruct::CONTROLLER_SEND_BINARY); - addFormNote(F("Do not 'percent escape' body when send binary checked")); - } - } - break; - } - - case CPlugin::Function::CPLUGIN_WEBFORM_SAVE: - { - std::shared_ptr customConfig(new (std::nothrow) C011_ConfigStruct); - - if (customConfig) { - uint8_t choice = 0; - String methods[] = { F("GET"), F("POST"), F("PUT"), F("HEAD"), F("PATCH") }; - - for (uint8_t i = 0; i < 5; i++) - { - if (methods[i].equals(customConfig->HttpMethod)) { - choice = i; - } - } - - int httpmethod = getFormItemInt(F("P011httpmethod"), choice); - String httpuri = webArg(F("P011httpuri")); - String httpheader = webArg(F("P011httpheader")); - String httpbody = webArg(F("P011httpbody")); - - strlcpy(customConfig->HttpMethod, methods[httpmethod].c_str(), sizeof(customConfig->HttpMethod)); - strlcpy(customConfig->HttpUri, httpuri.c_str(), sizeof(customConfig->HttpUri)); - strlcpy(customConfig->HttpHeader, httpheader.c_str(), sizeof(customConfig->HttpHeader)); - strlcpy(customConfig->HttpBody, httpbody.c_str(), sizeof(customConfig->HttpBody)); - customConfig->zero_last(); - SaveCustomControllerSettings(event->ControllerIndex, reinterpret_cast(customConfig.get()), sizeof(C011_ConfigStruct)); - } - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: - { - if (C011_DelayHandler->queueFull(event->ControllerIndex)) { - break; - } - success = Create_schedule_HTTP_C011(event); - break; - } - - case CPlugin::Function::CPLUGIN_FLUSH: - { - process_c011_delay_queue(); - delay(0); - break; - } - - default: - break; - } - return success; -} - -// ******************************************************************************** -// Generic HTTP request -// ******************************************************************************** - -// Uncrustify may change this into multi line, which will result in failed builds -// *INDENT-OFF* -bool do_process_c011_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { - const C011_queue_element& element = static_cast(element_base); -// *INDENT-ON* - - if (!NetworkConnected()) { return false; } - - int httpCode = -1; - - send_via_http( - controller_number, - ControllerSettings, - element._controller_idx, - element.uri, - element.HttpMethod, - element.header, - element.postStr, - httpCode); - - // HTTP codes: - // 1xx Informational response - // 2xx Success - return httpCode >= 100 && httpCode < 300; -} - -bool load_C011_ConfigStruct(controllerIndex_t ControllerIndex, String& HttpMethod, String& HttpUri, String& HttpHeader, String& HttpBody) { - // Just copy the needed strings and destruct the C011_ConfigStruct as soon as possible - std::shared_ptr customConfig(new (std::nothrow) C011_ConfigStruct); - - if (!customConfig) { - return false; - } - LoadCustomControllerSettings(ControllerIndex, reinterpret_cast(customConfig.get()), sizeof(C011_ConfigStruct)); - customConfig->zero_last(); - move_special(HttpMethod, String(customConfig->HttpMethod)); - move_special(HttpUri , String(customConfig->HttpUri)); - move_special(HttpHeader, String(customConfig->HttpHeader)); - move_special(HttpBody , String(customConfig->HttpBody)); - return true; -} - -// ******************************************************************************** -// Create request -// ******************************************************************************** -boolean Create_schedule_HTTP_C011(struct EventStruct *event) -{ - if (C011_DelayHandler == nullptr) { - addLog(LOG_LEVEL_ERROR, F("No C011_DelayHandler")); - return false; - } - //LoadTaskSettings(event->TaskIndex); // FIXME TD-er: This can probably be removed - - // Add a new element to the queue with the minimal payload - std::unique_ptr element(new C011_queue_element(event)); - bool success = C011_DelayHandler->addToQueue(std::move(element)); - - if (success) { - // Element was added. - // Now we try to append to the existing element - // and thus preventing the need to create a long string only to copy it to a queue element. - C011_queue_element& element = static_cast(*(C011_DelayHandler->sendQueue.back())); - - - if (!load_C011_ConfigStruct(event->ControllerIndex, element.HttpMethod, element.uri, element.header, element.postStr)) - { - if (loglevelActiveFor(LOG_LEVEL_ERROR)) { - addLogMove(LOG_LEVEL_ERROR, strformat( - F("C011 : %s %s %s %s"), - element.HttpMethod.c_str(), - element.uri.c_str(), - element.header.c_str(), - element.postStr.c_str())); - } - C011_DelayHandler->sendQueue.pop_back(); - return false; - } - - ReplaceTokenByValue(element.uri, event, false); - ReplaceTokenByValue(element.header, event, false); - - if (element.postStr.length() > 0) - { - ReplaceTokenByValue(element.postStr, event, C011_sendBinary); - } - } else { - addLog(LOG_LEVEL_ERROR, F("C011 : Could not add to delay handler")); - } - - Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C011_DELAY_QUEUE, C011_DelayHandler->getNextScheduleTime()); - return success; -} - -// parses the string and returns only the the number of name/values we want -// according to the parameter numberOfValuesWanted -void DeleteNotNeededValues(String& s, uint8_t numberOfValuesWanted) -{ - numberOfValuesWanted++; - - for (uint8_t i = 1; i < 5; i++) - { - const String startToken(strformat(F("%%%d%%"), i)); - const String endToken(strformat(F("%%/%d%%"), i)); - - // do we want to keep this one? - if (i < numberOfValuesWanted) - { - // yes, so just remove the tokens - s.replace(startToken, EMPTY_STRING); - s.replace(endToken, EMPTY_STRING); - } - else - { - // remove all the whole strings including tokes - int startIndex = s.indexOf(startToken); - int endIndex = s.indexOf(endToken); - - while (startIndex != -1 && endIndex != -1 && endIndex > startIndex) - { - String p = s.substring(startIndex, endIndex + 4); - - // remove the whole string including tokens - s.replace(p, EMPTY_STRING); - - // find next ones - startIndex = s.indexOf(startToken); - endIndex = s.indexOf(endToken); - } - } - } -} - -// ******************************************************************************** -// Replace the token in a string by real value. -// -// Example: -// %1%%vname1%____%tskname%____%val1%%/1%%2%%__%%vname2%____%tskname%____%val2%%/2% -// will become in case of a sensor with 1 value: -// SENSORVALUENAME1____TASKNAME1____VALUE1 <- everything not between %1% and %/1% will be discarded -// in case of a sensor with 2 values: -// SENSORVALUENAME1____TASKNAME1____VALUE1__SENSORVALUENAME2____TASKNAME2____VALUE2 -// ******************************************************************************** -void ReplaceTokenByValue(String& s, struct EventStruct *event, bool sendBinary) -{ - // example string: - // write?db=testdb&type=%1%%vname1%%/1%%2%;%vname2%%/2%%3%;%vname3%%/3%%4%;%vname4%%/4%&value=%1%%val1%%/1%%2%;%val2%%/2%%3%;%val3%%/3%%4%;%val4%%/4% - // %1%%vname1%,Standort=%tskname% Wert=%val1%%/1%%2%%LF%%vname2%,Standort=%tskname% Wert=%val2%%/2%%3%%LF%%vname3%,Standort=%tskname% - // Wert=%val3%%/3%%4%%LF%%vname4%,Standort=%tskname% Wert=%val4%%/4% - #ifndef BUILD_NO_DEBUG - if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) { - addLog(LOG_LEVEL_DEBUG_MORE, F("HTTP before parsing: ")); - addLog(LOG_LEVEL_DEBUG_MORE, s); - } - #endif - const uint8_t valueCount = getValueCountForTask(event->TaskIndex); - - DeleteNotNeededValues(s, valueCount); - - #ifndef BUILD_NO_DEBUG - if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) { - addLog(LOG_LEVEL_DEBUG_MORE, F("HTTP after parsing: ")); - addLog(LOG_LEVEL_DEBUG_MORE, s); - } - #endif - - parseControllerVariables(s, event, !sendBinary); - - #ifndef BUILD_NO_DEBUG - if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) { - addLog(LOG_LEVEL_DEBUG_MORE, F("HTTP after replacements: ")); - addLog(LOG_LEVEL_DEBUG_MORE, s); - } - #endif -} - -#endif // ifdef USES_C011 +#include "src/Helpers/_CPlugin_Helper.h" +#ifdef USES_C011 + +// ####################################################################################################### +// ########################### Controller Plugin 011: Generic HTTP Advanced ############################## +// ####################################################################################################### + +# define CPLUGIN_011 +# define CPLUGIN_ID_011 11 +# define CPLUGIN_NAME_011 "Generic HTTP Advanced" + +# define C011_HTTP_METHOD_MAX_LEN 16 +# define C011_HTTP_URI_MAX_LEN 240 +# define C011_HTTP_HEADER_MAX_LEN 256 +# define C011_HTTP_BODY_MAX_LEN 512 + + +bool C011_sendBinary = false; + +struct C011_ConfigStruct +{ + void zero_last() { + HttpMethod[C011_HTTP_METHOD_MAX_LEN - 1] = 0; + HttpUri[C011_HTTP_URI_MAX_LEN - 1] = 0; + HttpHeader[C011_HTTP_HEADER_MAX_LEN - 1] = 0; + HttpBody[C011_HTTP_BODY_MAX_LEN - 1] = 0; + } + + char HttpMethod[C011_HTTP_METHOD_MAX_LEN] = { 0 }; + char HttpUri[C011_HTTP_URI_MAX_LEN] = { 0 }; + char HttpHeader[C011_HTTP_HEADER_MAX_LEN] = { 0 }; + char HttpBody[C011_HTTP_BODY_MAX_LEN] = { 0 }; +}; + + +// Forward declarations +bool load_C011_ConfigStruct(controllerIndex_t ControllerIndex, String& HttpMethod, String& HttpUri, String& HttpHeader, String& HttpBody); +boolean Create_schedule_HTTP_C011(struct EventStruct *event); +void DeleteNotNeededValues(String& s, uint8_t numberOfValuesWanted); +void ReplaceTokenByValue(String& s, struct EventStruct *event, bool sendBinary); + + + +bool CPlugin_011(CPlugin::Function function, struct EventStruct *event, String& string) +{ + bool success = false; + + switch (function) + { + case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: + { + ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_011; + proto.usesMQTT = false; + proto.usesAccount = true; + proto.usesPassword = true; + proto.usesExtCreds = true; + proto.defaultPort = 80; + proto.usesID = false; + break; + } + + case CPlugin::Function::CPLUGIN_GET_DEVICENAME: + { + string = F(CPLUGIN_NAME_011); + break; + } + + case CPlugin::Function::CPLUGIN_INIT: + { + { + MakeControllerSettings(ControllerSettings); //-V522 + + if (AllocatedControllerSettings()) { + LoadControllerSettings(event->ControllerIndex, *ControllerSettings); + C011_sendBinary = ControllerSettings->sendBinary(); + } + } + success = init_c011_delay_queue(event->ControllerIndex); + break; + } + + case CPlugin::Function::CPLUGIN_EXIT: + { + exit_c011_delay_queue(); + break; + } + + case CPlugin::Function::CPLUGIN_WEBFORM_LOAD: + { + { + String HttpMethod; + String HttpUri; + String HttpHeader; + String HttpBody; + + if (!load_C011_ConfigStruct(event->ControllerIndex, HttpMethod, HttpUri, HttpHeader, HttpBody)) + { + return false; + } + addTableSeparator(F("HTTP Config"), 2, 3); + { + uint8_t choice = 0; + const __FlashStringHelper * methods[] = { F("GET"), F("POST"), F("PUT"), F("HEAD"), F("PATCH") }; + + for (uint8_t i = 0; i < 5; i++) + { + if (HttpMethod.equals(methods[i])) { + choice = i; + } + } + addFormSelector(F("Method"), F("P011httpmethod"), 5, methods, nullptr, choice); + } + + addFormTextBox(F("URI"), F("P011httpuri"), HttpUri, C011_HTTP_URI_MAX_LEN - 1); + { + htmlEscape(HttpHeader); + addFormTextArea(F("Header"), F("P011httpheader"), HttpHeader, C011_HTTP_HEADER_MAX_LEN - 1, 4, 50); + } + { + htmlEscape(HttpBody); + addFormTextArea(F("Body"), F("P011httpbody"), HttpBody, C011_HTTP_BODY_MAX_LEN - 1, 8, 50); + } + } + { + // Place in scope to delete ControllerSettings as soon as it is no longer needed + MakeControllerSettings(ControllerSettings); //-V522 + + if (!AllocatedControllerSettings()) { + addHtmlError(F("Out of memory, cannot load page")); + } else { + LoadControllerSettings(event->ControllerIndex, *ControllerSettings); + addControllerParameterForm(*ControllerSettings, event->ControllerIndex, ControllerSettingsStruct::CONTROLLER_SEND_BINARY); + addFormNote(F("Do not 'percent escape' body when send binary checked")); + } + } + break; + } + + case CPlugin::Function::CPLUGIN_WEBFORM_SAVE: + { + std::shared_ptr customConfig(new (std::nothrow) C011_ConfigStruct); + + if (customConfig) { + uint8_t choice = 0; + String methods[] = { F("GET"), F("POST"), F("PUT"), F("HEAD"), F("PATCH") }; + + for (uint8_t i = 0; i < 5; i++) + { + if (methods[i].equals(customConfig->HttpMethod)) { + choice = i; + } + } + + int httpmethod = getFormItemInt(F("P011httpmethod"), choice); + String httpuri = webArg(F("P011httpuri")); + String httpheader = webArg(F("P011httpheader")); + String httpbody = webArg(F("P011httpbody")); + + strlcpy(customConfig->HttpMethod, methods[httpmethod].c_str(), sizeof(customConfig->HttpMethod)); + strlcpy(customConfig->HttpUri, httpuri.c_str(), sizeof(customConfig->HttpUri)); + strlcpy(customConfig->HttpHeader, httpheader.c_str(), sizeof(customConfig->HttpHeader)); + strlcpy(customConfig->HttpBody, httpbody.c_str(), sizeof(customConfig->HttpBody)); + customConfig->zero_last(); + SaveCustomControllerSettings(event->ControllerIndex, reinterpret_cast(customConfig.get()), sizeof(C011_ConfigStruct)); + } + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: + { + if (C011_DelayHandler->queueFull(event->ControllerIndex)) { + break; + } + success = Create_schedule_HTTP_C011(event); + break; + } + + case CPlugin::Function::CPLUGIN_FLUSH: + { + process_c011_delay_queue(); + delay(0); + break; + } + + default: + break; + } + return success; +} + +// ******************************************************************************** +// Generic HTTP request +// ******************************************************************************** + +// Uncrustify may change this into multi line, which will result in failed builds +// *INDENT-OFF* +bool do_process_c011_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { + const C011_queue_element& element = static_cast(element_base); +// *INDENT-ON* + + if (!NetworkConnected()) { return false; } + + int httpCode = -1; + + send_via_http( + controller_number, + ControllerSettings, + element._controller_idx, + element.uri, + element.HttpMethod, + element.header, + element.postStr, + httpCode); + + // HTTP codes: + // 1xx Informational response + // 2xx Success + return httpCode >= 100 && httpCode < 300; +} + +bool load_C011_ConfigStruct(controllerIndex_t ControllerIndex, String& HttpMethod, String& HttpUri, String& HttpHeader, String& HttpBody) { + // Just copy the needed strings and destruct the C011_ConfigStruct as soon as possible + std::shared_ptr customConfig(new (std::nothrow) C011_ConfigStruct); + + if (!customConfig) { + return false; + } + LoadCustomControllerSettings(ControllerIndex, reinterpret_cast(customConfig.get()), sizeof(C011_ConfigStruct)); + customConfig->zero_last(); + move_special(HttpMethod, String(customConfig->HttpMethod)); + move_special(HttpUri , String(customConfig->HttpUri)); + move_special(HttpHeader, String(customConfig->HttpHeader)); + move_special(HttpBody , String(customConfig->HttpBody)); + return true; +} + +// ******************************************************************************** +// Create request +// ******************************************************************************** +boolean Create_schedule_HTTP_C011(struct EventStruct *event) +{ + if (C011_DelayHandler == nullptr) { + addLog(LOG_LEVEL_ERROR, F("No C011_DelayHandler")); + return false; + } + //LoadTaskSettings(event->TaskIndex); // FIXME TD-er: This can probably be removed + + // Add a new element to the queue with the minimal payload + std::unique_ptr element(new (std::nothrow) C011_queue_element(event)); + bool success = C011_DelayHandler->addToQueue(std::move(element)); + + if (success) { + // Element was added. + // Now we try to append to the existing element + // and thus preventing the need to create a long string only to copy it to a queue element. + C011_queue_element& element = static_cast(*(C011_DelayHandler->sendQueue.back())); + + + if (!load_C011_ConfigStruct(event->ControllerIndex, element.HttpMethod, element.uri, element.header, element.postStr)) + { + if (loglevelActiveFor(LOG_LEVEL_ERROR)) { + addLogMove(LOG_LEVEL_ERROR, strformat( + F("C011 : %s %s %s %s"), + element.HttpMethod.c_str(), + element.uri.c_str(), + element.header.c_str(), + element.postStr.c_str())); + } + C011_DelayHandler->sendQueue.pop_back(); + return false; + } + + ReplaceTokenByValue(element.uri, event, false); + ReplaceTokenByValue(element.header, event, false); + + if (element.postStr.length() > 0) + { + ReplaceTokenByValue(element.postStr, event, C011_sendBinary); + } + } else { + addLog(LOG_LEVEL_ERROR, F("C011 : Could not add to delay handler")); + } + + Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C011_DELAY_QUEUE, C011_DelayHandler->getNextScheduleTime()); + return success; +} + +// parses the string and returns only the the number of name/values we want +// according to the parameter numberOfValuesWanted +void DeleteNotNeededValues(String& s, uint8_t numberOfValuesWanted) +{ + numberOfValuesWanted++; + + for (uint8_t i = 1; i < 5; i++) + { + const String startToken(strformat(F("%%%d%%"), i)); + const String endToken(strformat(F("%%/%d%%"), i)); + + // do we want to keep this one? + if (i < numberOfValuesWanted) + { + // yes, so just remove the tokens + s.replace(startToken, EMPTY_STRING); + s.replace(endToken, EMPTY_STRING); + } + else + { + // remove all the whole strings including tokes + int startIndex = s.indexOf(startToken); + int endIndex = s.indexOf(endToken); + + while (startIndex != -1 && endIndex != -1 && endIndex > startIndex) + { + String p = s.substring(startIndex, endIndex + 4); + + // remove the whole string including tokens + s.replace(p, EMPTY_STRING); + + // find next ones + startIndex = s.indexOf(startToken); + endIndex = s.indexOf(endToken); + } + } + } +} + +// ******************************************************************************** +// Replace the token in a string by real value. +// +// Example: +// %1%%vname1%____%tskname%____%val1%%/1%%2%%__%%vname2%____%tskname%____%val2%%/2% +// will become in case of a sensor with 1 value: +// SENSORVALUENAME1____TASKNAME1____VALUE1 <- everything not between %1% and %/1% will be discarded +// in case of a sensor with 2 values: +// SENSORVALUENAME1____TASKNAME1____VALUE1__SENSORVALUENAME2____TASKNAME2____VALUE2 +// ******************************************************************************** +void ReplaceTokenByValue(String& s, struct EventStruct *event, bool sendBinary) +{ + // example string: + // write?db=testdb&type=%1%%vname1%%/1%%2%;%vname2%%/2%%3%;%vname3%%/3%%4%;%vname4%%/4%&value=%1%%val1%%/1%%2%;%val2%%/2%%3%;%val3%%/3%%4%;%val4%%/4% + // %1%%vname1%,Standort=%tskname% Wert=%val1%%/1%%2%%LF%%vname2%,Standort=%tskname% Wert=%val2%%/2%%3%%LF%%vname3%,Standort=%tskname% + // Wert=%val3%%/3%%4%%LF%%vname4%,Standort=%tskname% Wert=%val4%%/4% + #ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) { + addLog(LOG_LEVEL_DEBUG_MORE, F("HTTP before parsing: ")); + addLog(LOG_LEVEL_DEBUG_MORE, s); + } + #endif + const uint8_t valueCount = getValueCountForTask(event->TaskIndex); + + DeleteNotNeededValues(s, valueCount); + + #ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) { + addLog(LOG_LEVEL_DEBUG_MORE, F("HTTP after parsing: ")); + addLog(LOG_LEVEL_DEBUG_MORE, s); + } + #endif + + parseControllerVariables(s, event, !sendBinary); + + #ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) { + addLog(LOG_LEVEL_DEBUG_MORE, F("HTTP after replacements: ")); + addLog(LOG_LEVEL_DEBUG_MORE, s); + } + #endif +} + +#endif // ifdef USES_C011 diff --git a/src/_C012.cpp b/src/_C012.cpp index 09387ef501..6eccb2aee1 100644 --- a/src/_C012.cpp +++ b/src/_C012.cpp @@ -1,125 +1,125 @@ -#include "src/Helpers/_CPlugin_Helper.h" -#ifdef USES_C012 - -// ####################################################################################################### -// ########################### Controller Plugin 012: Blynk ############################################# -// ####################################################################################################### - -# include "src/Commands/Blynk.h" - -# define CPLUGIN_012 -# define CPLUGIN_ID_012 12 -# define CPLUGIN_NAME_012 "Blynk HTTP" - -bool CPlugin_012(CPlugin::Function function, struct EventStruct *event, String& string) -{ - bool success = false; - - switch (function) - { - case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: - { - ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_012; - proto.usesMQTT = false; - proto.usesAccount = false; - proto.usesPassword = true; - proto.usesExtCreds = true; - proto.defaultPort = 80; - proto.usesID = true; - break; - } - - case CPlugin::Function::CPLUGIN_GET_DEVICENAME: - { - string = F(CPLUGIN_NAME_012); - break; - } - - case CPlugin::Function::CPLUGIN_INIT: - { - success = init_c012_delay_queue(event->ControllerIndex); - break; - } - - case CPlugin::Function::CPLUGIN_EXIT: - { - exit_c012_delay_queue(); - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: - { - if (C012_DelayHandler == nullptr) { - break; - } - if (C012_DelayHandler->queueFull(event->ControllerIndex)) { - break; - } - //LoadTaskSettings(event->TaskIndex); // FIXME TD-er: This can probably be removed - - // Collect the values at the same run, to make sure all are from the same sample - uint8_t valueCount = getValueCountForTask(event->TaskIndex); - std::unique_ptr element(new C012_queue_element(event, valueCount)); - - for (uint8_t x = 0; x < valueCount; x++) - { - bool isvalid; - const String formattedValue = formatUserVar(event, x, isvalid); - - if (isvalid) { - move_special(element->txt[x], strformat( - F("update/V%d?value=%s"), - event->idx + x, - formattedValue.c_str())); - - #ifndef BUILD_NO_DEBUG - if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) { - addLog(LOG_LEVEL_DEBUG_MORE, element->txt[x]); - } - #endif - } - } - - - success = C012_DelayHandler->addToQueue(std::move(element)); - Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C012_DELAY_QUEUE, C012_DelayHandler->getNextScheduleTime()); - break; - } - - case CPlugin::Function::CPLUGIN_FLUSH: - { - process_c012_delay_queue(); - delay(0); - break; - } - - default: - break; - } - return success; -} - -// ******************************************************************************** -// Process Queued Blynk request, with data set to NULL -// ******************************************************************************** - -// Uncrustify may change this into multi line, which will result in failed builds -// *INDENT-OFF* -bool do_process_c012_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { - const C012_queue_element& element = static_cast(element_base); -// *INDENT-ON* - while (element.txt[element.valuesSent].isEmpty()) { - // A non valid value, which we are not going to send. - // Increase sent counter until a valid value is found. - if (element.checkDone(true)) { - return true; - } - } - - if (!NetworkConnected()) { - return false; - } - return element.checkDone(Blynk_get(element.txt[element.valuesSent], element._controller_idx)); -} - -#endif // ifdef USES_C012 +#include "src/Helpers/_CPlugin_Helper.h" +#ifdef USES_C012 + +// ####################################################################################################### +// ########################### Controller Plugin 012: Blynk ############################################# +// ####################################################################################################### + +# include "src/Commands/Blynk.h" + +# define CPLUGIN_012 +# define CPLUGIN_ID_012 12 +# define CPLUGIN_NAME_012 "Blynk HTTP" + +bool CPlugin_012(CPlugin::Function function, struct EventStruct *event, String& string) +{ + bool success = false; + + switch (function) + { + case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: + { + ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_012; + proto.usesMQTT = false; + proto.usesAccount = false; + proto.usesPassword = true; + proto.usesExtCreds = true; + proto.defaultPort = 80; + proto.usesID = true; + break; + } + + case CPlugin::Function::CPLUGIN_GET_DEVICENAME: + { + string = F(CPLUGIN_NAME_012); + break; + } + + case CPlugin::Function::CPLUGIN_INIT: + { + success = init_c012_delay_queue(event->ControllerIndex); + break; + } + + case CPlugin::Function::CPLUGIN_EXIT: + { + exit_c012_delay_queue(); + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: + { + if (C012_DelayHandler == nullptr) { + break; + } + if (C012_DelayHandler->queueFull(event->ControllerIndex)) { + break; + } + //LoadTaskSettings(event->TaskIndex); // FIXME TD-er: This can probably be removed + + // Collect the values at the same run, to make sure all are from the same sample + uint8_t valueCount = getValueCountForTask(event->TaskIndex); + std::unique_ptr element(new (std::nothrow) C012_queue_element(event, valueCount)); + + for (uint8_t x = 0; x < valueCount; x++) + { + bool isvalid; + const String formattedValue = formatUserVar(event, x, isvalid); + + if (isvalid) { + move_special(element->txt[x], strformat( + F("update/V%d?value=%s"), + event->idx + x, + formattedValue.c_str())); + + #ifndef BUILD_NO_DEBUG + if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) { + addLog(LOG_LEVEL_DEBUG_MORE, element->txt[x]); + } + #endif + } + } + + + success = C012_DelayHandler->addToQueue(std::move(element)); + Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C012_DELAY_QUEUE, C012_DelayHandler->getNextScheduleTime()); + break; + } + + case CPlugin::Function::CPLUGIN_FLUSH: + { + process_c012_delay_queue(); + delay(0); + break; + } + + default: + break; + } + return success; +} + +// ******************************************************************************** +// Process Queued Blynk request, with data set to NULL +// ******************************************************************************** + +// Uncrustify may change this into multi line, which will result in failed builds +// *INDENT-OFF* +bool do_process_c012_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { + const C012_queue_element& element = static_cast(element_base); +// *INDENT-ON* + while (element.txt[element.valuesSent].isEmpty()) { + // A non valid value, which we are not going to send. + // Increase sent counter until a valid value is found. + if (element.checkDone(true)) { + return true; + } + } + + if (!NetworkConnected()) { + return false; + } + return element.checkDone(Blynk_get(element.txt[element.valuesSent], element._controller_idx)); +} + +#endif // ifdef USES_C012 diff --git a/src/_C015.cpp b/src/_C015.cpp index 6443a24391..8356993e32 100644 --- a/src/_C015.cpp +++ b/src/_C015.cpp @@ -1,492 +1,492 @@ -#include "src/Helpers/_CPlugin_Helper.h" -#ifdef USES_C015 - -# include "src/Globals/CPlugins.h" -# include "src/Commands/Common.h" -# include "src/ESPEasyCore/ESPEasy_backgroundtasks.h" - -// ####################################################################################################### -// ########################### Controller Plugin 015: Blynk ############################################# -// ####################################################################################################### - -// This plugin provides blynk native protocol. This makes possible receive callbacks from user -// like button press, slider move etc. -// This require much more ESP resources, than use of blynk http API. -// So, use C012 Blynk HTTP plugin when you don't need blynk calbacks. -// -// Only one blynk controller instance is supported. -// -// https://www.youtube.com/watch?v=5_V_DibOypE - -// Uncomment this to use ssl connection. This requires more device resources than unencrypted one. -// Also it requires valid server thumbprint string to be entered in plugin settings. -// #define CPLUGIN_015_SSL - -# define CPLUGIN_015 -# define CPLUGIN_ID_015 15 -# define _BLYNK_USE_DEFAULT_FREE_RAM -# define BLYNK_TIMEOUT_MS 2000UL -# define BLYNK_HEARTBEAT 30 -# define CPLUGIN_015_RECONNECT_INTERVAL 60000 - -# ifdef CPLUGIN_015_SSL - #ifdef ESP8266 - # include - #endif - #ifdef ESP32 - # include - #endif - # define CPLUGIN_NAME_015 "Blynk SSL" - -// Current official blynk server thumbprint - # define CPLUGIN_015_DEFAULT_THUMBPRINT "FD C0 7D 8D 47 97 F7 E3 07 05 D3 4E E3 BB 8E 3D C0 EA BE 1C" - # define C015_LOG_PREFIX "BL (ssl): " -# else // ifdef CPLUGIN_015_SSL - #ifdef ESP8266 - # include - #endif - #ifdef ESP32 - # include - #endif - # define CPLUGIN_NAME_015 "Blynk" - # define C015_LOG_PREFIX "BL: " -# endif // ifdef CPLUGIN_015_SSL - - -// Forward declarations: -boolean Blynk_send_c015(const String& value, int vPin, unsigned int clientTimeout); -boolean Blynk_keep_connection_c015(int controllerIndex, ControllerSettingsStruct& ControllerSettings); - - -static unsigned long _C015_LastConnectAttempt[CONTROLLER_MAX] = { 0, 0, 0 }; - -void CPlugin_015_handleInterrupt() { - // This cplugin uses modified blynk library. - // It includes support of calling this during time-wait operations - // like blynk connection process to keep espeasy stability. - backgroundtasks(); -} - -void Blynk_Run_c015() { - // user callbacks processing. Called from run10TimesPerSecond. - if (Blynk.connected()) { - Blynk.run(); - } -} - -bool CPlugin_015(CPlugin::Function function, struct EventStruct *event, String& string) -{ - bool success = false; - - switch (function) - { - case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: - { - ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_015; - proto.usesMQTT = false; - proto.usesAccount = false; - proto.usesPassword = true; - proto.usesExtCreds = true; - proto.defaultPort = 80; - proto.usesID = false; - break; - } - - case CPlugin::Function::CPLUGIN_GET_DEVICENAME: - { - string = F(CPLUGIN_NAME_015); - break; - } - - case CPlugin::Function::CPLUGIN_INIT: - { - success = init_c015_delay_queue(event->ControllerIndex); - - // when connected to another server and user has changed settings - if (success && Blynk.connected()) { - addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "disconnect from server")); - Blynk.disconnect(); - } - break; - } - - case CPlugin::Function::CPLUGIN_EXIT: - { - exit_c015_delay_queue(); - break; - } - - # ifdef CPLUGIN_015_SSL - case CPlugin::Function::CPLUGIN_WEBFORM_LOAD: - { - char thumbprint[60] = {0}; - LoadCustomControllerSettings(event->ControllerIndex, reinterpret_cast(&thumbprint), sizeof(thumbprint)); - - if (strlen(thumbprint) != 59) { - strcpy(thumbprint, CPLUGIN_015_DEFAULT_THUMBPRINT); - } - addFormTextBox(F("Server thumbprint string"), F("c015_thumbprint"), thumbprint, 60); - success = true; - break; - } - # endif // ifdef CPLUGIN_015_SSL - - case CPlugin::Function::CPLUGIN_WEBFORM_SAVE: - { - success = true; - - if (isFormItemChecked(F("controllerenabled"))) { - for (controllerIndex_t i = 0; i < CONTROLLER_MAX; ++i) { - const protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(i); - - if (validProtocolIndex(ProtocolIndex)) { - const cpluginID_t number = getCPluginID_from_ProtocolIndex(ProtocolIndex); - if ((i != event->ControllerIndex) && (number == 15) && Settings.ControllerEnabled[i]) { - success = false; - - // FIXME: this will only show a warning message and not uncheck "enabled" in webform. - // Webserver object is not checking result of "success" var :( - addHtmlError(F("Only one enabled instance of blynk controller is supported")); - break; - } - } - } - - // force to connect without delay when webform saved - _C015_LastConnectAttempt[event->ControllerIndex] = 0; - - # ifdef CPLUGIN_015_SSL - char thumbprint[60] = {0}; - String error = F("Specify server thumbprint with exactly 59 symbols string like " CPLUGIN_015_DEFAULT_THUMBPRINT); - - if (!safe_strncpy(thumbprint, webArg("c015_thumbprint"), 60) || (strlen(thumbprint) != 59)) { - addHtmlError(error); - } - SaveCustomControllerSettings(event->ControllerIndex, reinterpret_cast(&thumbprint), sizeof(thumbprint)); - # endif // ifdef CPLUGIN_015_SSL - } - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: - { - if (C015_DelayHandler == nullptr) { - break; - } - if (C015_DelayHandler->queueFull(event->ControllerIndex)) { - break; - } - - if (!Settings.ControllerEnabled[event->ControllerIndex]) { - break; - } - - // Collect the values at the same run, to make sure all are from the same sample - uint8_t valueCount = getValueCountForTask(event->TaskIndex); - - std::unique_ptr element(new C015_queue_element(event, valueCount)); - success = C015_DelayHandler->addToQueue(std::move(element)); - - if (success) { - // Element was added. - // Now we try to append to the existing element - // and thus preventing the need to create a long string only to copy it to a queue element. - C015_queue_element& element = static_cast(*(C015_DelayHandler->sendQueue.back())); - - for (uint8_t x = 0; x < valueCount; x++) - { - bool isvalid; - String formattedValue = formatUserVar(event, x, isvalid); - - if (!isvalid) { - // send empty string to Blynk in case of error - formattedValue = String(); - } - - const String valueName = getTaskValueName(event->TaskIndex, x); - const String valueFullName = strformat( - F("%s.%s"), - getTaskDeviceName(event->TaskIndex).c_str(), - valueName.c_str()); - const String vPinNumberStr = valueName.substring(1, 4); - int vPinNumber = vPinNumberStr.toInt(); - - if ((vPinNumber < 0) || (vPinNumber > 255)) { - vPinNumber = -1; - } - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F(C015_LOG_PREFIX); - log += Blynk.connected() ? F("(online): ") : F("(offline): "); - - if ((vPinNumber > 0) && (vPinNumber < 256)) { - log += strformat( - F("send %s = %s to blynk pin v%d"), - valueFullName.c_str(), - formattedValue.c_str(), - vPinNumber); - } else { - log += strformat( - F("error got vPin number for %s, got not valid value: %s"), - valueFullName.c_str(), - vPinNumberStr.c_str()); - } - addLogMove(LOG_LEVEL_INFO, log); - } - element.vPin[x] = vPinNumber; - move_special(element.txt[x], std::move(formattedValue)); - } - } - Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C015_DELAY_QUEUE, C015_DelayHandler->getNextScheduleTime()); - break; - } - - default: - break; - } - return success; -} - -// ******************************************************************************** -// Process Queued Blynk request, with data set to NULL -// ******************************************************************************** -// controller_plugin_number = 015 because of C015 - -// Uncrustify may change this into multi line, which will result in failed builds -// *INDENT-OFF* -bool do_process_c015_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { - const C015_queue_element& element = static_cast(element_base); -// *INDENT-ON* - if (!Settings.ControllerEnabled[element._controller_idx]) { - // controller has been disabled. Answer true to flush queue. - return true; - } - - if (!NetworkConnected()) { - return false; - } - - if (!Blynk_keep_connection_c015(element._controller_idx, ControllerSettings)) { - return false; - } - - while (element.vPin[element.valuesSent] == -1) { - // A non valid value, which we are not going to send. - // answer ok and skip real sending - if (element.checkDone(true)) { - return true; - } - } - - bool sendSuccess = Blynk_send_c015( - element.txt[element.valuesSent], - element.vPin[element.valuesSent], - ControllerSettings.ClientTimeout); - - return element.checkDone(sendSuccess); -} - -boolean Blynk_keep_connection_c015(int controllerIndex, ControllerSettingsStruct& ControllerSettings) { - if (!NetworkConnected()) { - return false; - } - - if (!Blynk.connected()) { - String auth = getControllerPass(controllerIndex, ControllerSettings); - boolean connectDefault = false; - - if (timePassedSince(_C015_LastConnectAttempt[controllerIndex]) < CPLUGIN_015_RECONNECT_INTERVAL) { - // "skip connect to blynk server too often. Wait a little..."; - return false; - } - _C015_LastConnectAttempt[controllerIndex] = millis(); - - # ifdef CPLUGIN_015_SSL - char thumbprint[60] = {0}; - LoadCustomControllerSettings(controllerIndex, reinterpret_cast(&thumbprint), sizeof(thumbprint)); - - if (strlen(thumbprint) != 59) { - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "Saved thumprint value is not correct:")); - addLog(LOG_LEVEL_INFO, thumbprint); - } - strcpy(thumbprint, CPLUGIN_015_DEFAULT_THUMBPRINT); - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "using default one:")); - addLog(LOG_LEVEL_INFO, thumbprint); - } - } - # endif // ifdef CPLUGIN_015_SSL - - String log = F(C015_LOG_PREFIX); - - if (ControllerSettings.UseDNS) { - String hostName = ControllerSettings.getHost(); - - if (!hostName.isEmpty()) { - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log += F("Connecting to custom blynk server "); - log += ControllerSettings.getHostPortString(); - } - Blynk.config(auth.c_str(), - CPlugin_015_handleInterrupt, - hostName.c_str(), - ControllerSettings.Port - # ifdef CPLUGIN_015_SSL - , thumbprint - # endif // ifdef CPLUGIN_015_SSL - ); - } - else { - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log += F("Custom blynk server name not specified. "); - } - connectDefault = true; - } - } - else { - IPAddress ip = ControllerSettings.getIP(); - - if ((ip[0] + ip[1] + ip[2] + ip[3]) > 0) { - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log += F("Connecting to custom blynk server "); - log += ControllerSettings.getHostPortString(); - } - Blynk.config(auth.c_str(), - CPlugin_015_handleInterrupt, - ip, - ControllerSettings.Port - # ifdef CPLUGIN_015_SSL - , thumbprint - # endif // ifdef CPLUGIN_015_SSL - ); - } - else { - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log += F("Custom blynk server ip not specified. "); - } - connectDefault = true; - } - } - addLogMove(LOG_LEVEL_INFO, log); - - if (connectDefault) { - addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "Connecting to default server")); - Blynk.config(auth.c_str(), - CPlugin_015_handleInterrupt, - BLYNK_DEFAULT_DOMAIN - # ifdef CPLUGIN_015_SSL - , BLYNK_DEFAULT_PORT_SSL - , thumbprint - # else // ifdef CPLUGIN_015_SSL - , BLYNK_DEFAULT_PORT - # endif // ifdef CPLUGIN_015_SSL - ); - } - - # ifdef CPLUGIN_015_SSL - - if (!Blynk.connect()) { - if (!_blynkWifiClient.verify(thumbprint, BLYNK_DEFAULT_DOMAIN)) { - addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "thumbprint check FAILED! Check thumbprint in device settings and server thumbprint")); - addLog(LOG_LEVEL_INFO, thumbprint); - } - } - # else // ifdef CPLUGIN_015_SSL - Blynk.connect(); - # endif // ifdef CPLUGIN_015_SSL - } - - return Blynk.connected(); -} - -String Command_Blynk_Set_c015(struct EventStruct *event, const char *Line) { - // todo add multicontroller support and chek it is connected and enabled - if (!Blynk.connected()) { - return F("Not connected to blynk server"); - } - - int vPin = event->Par1; - - if ((vPin < 0) || (vPin > 255)) { - return concat(F("Not correct blynk vPin number "), vPin); - } - - String data = parseString(Line, 3); - - if (data.isEmpty()) { - return concat(F("Skip sending empty data to blynk vPin "), vPin); - } - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, strformat( - F(C015_LOG_PREFIX "(online): send blynk pin v%d = %s"), - vPin, - data.c_str())); - } - - Blynk.virtualWrite(vPin, data); - return return_command_success(); -} - -boolean Blynk_send_c015(const String& value, int vPin, unsigned int clientTimeout) -{ - Blynk.virtualWrite(vPin, value); - - unsigned long timer = millis() + clientTimeout; - - while (!timeOutReached(timer)) { - backgroundtasks(); - } - return true; -} - -// This is called for all virtual pins, that don't have BLYNK_WRITE handler -BLYNK_WRITE_DEFAULT() { - const unsigned int vPin = request.pin; - const float pinValue = param.asFloat(); - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, strformat( - F(C015_LOG_PREFIX "server set v%u to %f"), - vPin, - pinValue)); - } - - if (Settings.UseRules) { - eventQueue.addMove(strformat( - F("blynkv%d=%f"), - vPin, - pinValue)); - } -} - -BLYNK_CONNECTED() { - // Your code here when hardware connects to Blynk Cloud or private server. - // It’s common to call sync functions inside of this function. - // Requests all stored on the server latest values for all widgets. - if (Settings.UseRules) { - eventQueue.add(F("blynk_connected")); - } - - // addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "connected handler")); -} - -// This is called when Smartphone App is opened -BLYNK_APP_CONNECTED() { - if (Settings.UseRules) { - eventQueue.add(F("blynk_app_connected")); - } - - // addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "app connected handler")); -} - -// This is called when Smartphone App is closed -BLYNK_APP_DISCONNECTED() { - if (Settings.UseRules) { - eventQueue.add(F("blynk_app_disconnected")); - } - - // addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "app disconnected handler")); -} - +#include "src/Helpers/_CPlugin_Helper.h" +#ifdef USES_C015 + +# include "src/Globals/CPlugins.h" +# include "src/Commands/Common.h" +# include "src/ESPEasyCore/ESPEasy_backgroundtasks.h" + +// ####################################################################################################### +// ########################### Controller Plugin 015: Blynk ############################################# +// ####################################################################################################### + +// This plugin provides blynk native protocol. This makes possible receive callbacks from user +// like button press, slider move etc. +// This require much more ESP resources, than use of blynk http API. +// So, use C012 Blynk HTTP plugin when you don't need blynk calbacks. +// +// Only one blynk controller instance is supported. +// +// https://www.youtube.com/watch?v=5_V_DibOypE + +// Uncomment this to use ssl connection. This requires more device resources than unencrypted one. +// Also it requires valid server thumbprint string to be entered in plugin settings. +// #define CPLUGIN_015_SSL + +# define CPLUGIN_015 +# define CPLUGIN_ID_015 15 +# define _BLYNK_USE_DEFAULT_FREE_RAM +# define BLYNK_TIMEOUT_MS 2000UL +# define BLYNK_HEARTBEAT 30 +# define CPLUGIN_015_RECONNECT_INTERVAL 60000 + +# ifdef CPLUGIN_015_SSL + #ifdef ESP8266 + # include + #endif + #ifdef ESP32 + # include + #endif + # define CPLUGIN_NAME_015 "Blynk SSL" + +// Current official blynk server thumbprint + # define CPLUGIN_015_DEFAULT_THUMBPRINT "FD C0 7D 8D 47 97 F7 E3 07 05 D3 4E E3 BB 8E 3D C0 EA BE 1C" + # define C015_LOG_PREFIX "BL (ssl): " +# else // ifdef CPLUGIN_015_SSL + #ifdef ESP8266 + # include + #endif + #ifdef ESP32 + # include + #endif + # define CPLUGIN_NAME_015 "Blynk" + # define C015_LOG_PREFIX "BL: " +# endif // ifdef CPLUGIN_015_SSL + + +// Forward declarations: +boolean Blynk_send_c015(const String& value, int vPin, unsigned int clientTimeout); +boolean Blynk_keep_connection_c015(int controllerIndex, ControllerSettingsStruct& ControllerSettings); + + +static unsigned long _C015_LastConnectAttempt[CONTROLLER_MAX] = { 0, 0, 0 }; + +void CPlugin_015_handleInterrupt() { + // This cplugin uses modified blynk library. + // It includes support of calling this during time-wait operations + // like blynk connection process to keep espeasy stability. + backgroundtasks(); +} + +void Blynk_Run_c015() { + // user callbacks processing. Called from run10TimesPerSecond. + if (Blynk.connected()) { + Blynk.run(); + } +} + +bool CPlugin_015(CPlugin::Function function, struct EventStruct *event, String& string) +{ + bool success = false; + + switch (function) + { + case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: + { + ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_015; + proto.usesMQTT = false; + proto.usesAccount = false; + proto.usesPassword = true; + proto.usesExtCreds = true; + proto.defaultPort = 80; + proto.usesID = false; + break; + } + + case CPlugin::Function::CPLUGIN_GET_DEVICENAME: + { + string = F(CPLUGIN_NAME_015); + break; + } + + case CPlugin::Function::CPLUGIN_INIT: + { + success = init_c015_delay_queue(event->ControllerIndex); + + // when connected to another server and user has changed settings + if (success && Blynk.connected()) { + addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "disconnect from server")); + Blynk.disconnect(); + } + break; + } + + case CPlugin::Function::CPLUGIN_EXIT: + { + exit_c015_delay_queue(); + break; + } + + # ifdef CPLUGIN_015_SSL + case CPlugin::Function::CPLUGIN_WEBFORM_LOAD: + { + char thumbprint[60] = {0}; + LoadCustomControllerSettings(event->ControllerIndex, reinterpret_cast(&thumbprint), sizeof(thumbprint)); + + if (strlen(thumbprint) != 59) { + strcpy(thumbprint, CPLUGIN_015_DEFAULT_THUMBPRINT); + } + addFormTextBox(F("Server thumbprint string"), F("c015_thumbprint"), thumbprint, 60); + success = true; + break; + } + # endif // ifdef CPLUGIN_015_SSL + + case CPlugin::Function::CPLUGIN_WEBFORM_SAVE: + { + success = true; + + if (isFormItemChecked(F("controllerenabled"))) { + for (controllerIndex_t i = 0; i < CONTROLLER_MAX; ++i) { + const protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(i); + + if (validProtocolIndex(ProtocolIndex)) { + const cpluginID_t number = getCPluginID_from_ProtocolIndex(ProtocolIndex); + if ((i != event->ControllerIndex) && (number == 15) && Settings.ControllerEnabled[i]) { + success = false; + + // FIXME: this will only show a warning message and not uncheck "enabled" in webform. + // Webserver object is not checking result of "success" var :( + addHtmlError(F("Only one enabled instance of blynk controller is supported")); + break; + } + } + } + + // force to connect without delay when webform saved + _C015_LastConnectAttempt[event->ControllerIndex] = 0; + + # ifdef CPLUGIN_015_SSL + char thumbprint[60] = {0}; + String error = F("Specify server thumbprint with exactly 59 symbols string like " CPLUGIN_015_DEFAULT_THUMBPRINT); + + if (!safe_strncpy(thumbprint, webArg("c015_thumbprint"), 60) || (strlen(thumbprint) != 59)) { + addHtmlError(error); + } + SaveCustomControllerSettings(event->ControllerIndex, reinterpret_cast(&thumbprint), sizeof(thumbprint)); + # endif // ifdef CPLUGIN_015_SSL + } + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: + { + if (C015_DelayHandler == nullptr) { + break; + } + if (C015_DelayHandler->queueFull(event->ControllerIndex)) { + break; + } + + if (!Settings.ControllerEnabled[event->ControllerIndex]) { + break; + } + + // Collect the values at the same run, to make sure all are from the same sample + uint8_t valueCount = getValueCountForTask(event->TaskIndex); + + std::unique_ptr element(new (std::nothrow) C015_queue_element(event, valueCount)); + success = C015_DelayHandler->addToQueue(std::move(element)); + + if (success) { + // Element was added. + // Now we try to append to the existing element + // and thus preventing the need to create a long string only to copy it to a queue element. + C015_queue_element& element = static_cast(*(C015_DelayHandler->sendQueue.back())); + + for (uint8_t x = 0; x < valueCount; x++) + { + bool isvalid; + String formattedValue = formatUserVar(event, x, isvalid); + + if (!isvalid) { + // send empty string to Blynk in case of error + formattedValue = String(); + } + + const String valueName = getTaskValueName(event->TaskIndex, x); + const String valueFullName = strformat( + F("%s.%s"), + getTaskDeviceName(event->TaskIndex).c_str(), + valueName.c_str()); + const String vPinNumberStr = valueName.substring(1, 4); + int vPinNumber = vPinNumberStr.toInt(); + + if ((vPinNumber < 0) || (vPinNumber > 255)) { + vPinNumber = -1; + } + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F(C015_LOG_PREFIX); + log += Blynk.connected() ? F("(online): ") : F("(offline): "); + + if ((vPinNumber > 0) && (vPinNumber < 256)) { + log += strformat( + F("send %s = %s to blynk pin v%d"), + valueFullName.c_str(), + formattedValue.c_str(), + vPinNumber); + } else { + log += strformat( + F("error got vPin number for %s, got not valid value: %s"), + valueFullName.c_str(), + vPinNumberStr.c_str()); + } + addLogMove(LOG_LEVEL_INFO, log); + } + element.vPin[x] = vPinNumber; + move_special(element.txt[x], std::move(formattedValue)); + } + } + Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C015_DELAY_QUEUE, C015_DelayHandler->getNextScheduleTime()); + break; + } + + default: + break; + } + return success; +} + +// ******************************************************************************** +// Process Queued Blynk request, with data set to NULL +// ******************************************************************************** +// controller_plugin_number = 015 because of C015 + +// Uncrustify may change this into multi line, which will result in failed builds +// *INDENT-OFF* +bool do_process_c015_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { + const C015_queue_element& element = static_cast(element_base); +// *INDENT-ON* + if (!Settings.ControllerEnabled[element._controller_idx]) { + // controller has been disabled. Answer true to flush queue. + return true; + } + + if (!NetworkConnected()) { + return false; + } + + if (!Blynk_keep_connection_c015(element._controller_idx, ControllerSettings)) { + return false; + } + + while (element.vPin[element.valuesSent] == -1) { + // A non valid value, which we are not going to send. + // answer ok and skip real sending + if (element.checkDone(true)) { + return true; + } + } + + bool sendSuccess = Blynk_send_c015( + element.txt[element.valuesSent], + element.vPin[element.valuesSent], + ControllerSettings.ClientTimeout); + + return element.checkDone(sendSuccess); +} + +boolean Blynk_keep_connection_c015(int controllerIndex, ControllerSettingsStruct& ControllerSettings) { + if (!NetworkConnected()) { + return false; + } + + if (!Blynk.connected()) { + String auth = getControllerPass(controllerIndex, ControllerSettings); + boolean connectDefault = false; + + if (timePassedSince(_C015_LastConnectAttempt[controllerIndex]) < CPLUGIN_015_RECONNECT_INTERVAL) { + // "skip connect to blynk server too often. Wait a little..."; + return false; + } + _C015_LastConnectAttempt[controllerIndex] = millis(); + + # ifdef CPLUGIN_015_SSL + char thumbprint[60] = {0}; + LoadCustomControllerSettings(controllerIndex, reinterpret_cast(&thumbprint), sizeof(thumbprint)); + + if (strlen(thumbprint) != 59) { + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "Saved thumprint value is not correct:")); + addLog(LOG_LEVEL_INFO, thumbprint); + } + strcpy(thumbprint, CPLUGIN_015_DEFAULT_THUMBPRINT); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "using default one:")); + addLog(LOG_LEVEL_INFO, thumbprint); + } + } + # endif // ifdef CPLUGIN_015_SSL + + String log = F(C015_LOG_PREFIX); + + if (ControllerSettings.UseDNS) { + String hostName = ControllerSettings.getHost(); + + if (!hostName.isEmpty()) { + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + log += F("Connecting to custom blynk server "); + log += ControllerSettings.getHostPortString(); + } + Blynk.config(auth.c_str(), + CPlugin_015_handleInterrupt, + hostName.c_str(), + ControllerSettings.Port + # ifdef CPLUGIN_015_SSL + , thumbprint + # endif // ifdef CPLUGIN_015_SSL + ); + } + else { + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + log += F("Custom blynk server name not specified. "); + } + connectDefault = true; + } + } + else { + IPAddress ip = ControllerSettings.getIP(); + + if ((ip[0] + ip[1] + ip[2] + ip[3]) > 0) { + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + log += F("Connecting to custom blynk server "); + log += ControllerSettings.getHostPortString(); + } + Blynk.config(auth.c_str(), + CPlugin_015_handleInterrupt, + ip, + ControllerSettings.Port + # ifdef CPLUGIN_015_SSL + , thumbprint + # endif // ifdef CPLUGIN_015_SSL + ); + } + else { + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + log += F("Custom blynk server ip not specified. "); + } + connectDefault = true; + } + } + addLogMove(LOG_LEVEL_INFO, log); + + if (connectDefault) { + addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "Connecting to default server")); + Blynk.config(auth.c_str(), + CPlugin_015_handleInterrupt, + BLYNK_DEFAULT_DOMAIN + # ifdef CPLUGIN_015_SSL + , BLYNK_DEFAULT_PORT_SSL + , thumbprint + # else // ifdef CPLUGIN_015_SSL + , BLYNK_DEFAULT_PORT + # endif // ifdef CPLUGIN_015_SSL + ); + } + + # ifdef CPLUGIN_015_SSL + + if (!Blynk.connect()) { + if (!_blynkWifiClient.verify(thumbprint, BLYNK_DEFAULT_DOMAIN)) { + addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "thumbprint check FAILED! Check thumbprint in device settings and server thumbprint")); + addLog(LOG_LEVEL_INFO, thumbprint); + } + } + # else // ifdef CPLUGIN_015_SSL + Blynk.connect(); + # endif // ifdef CPLUGIN_015_SSL + } + + return Blynk.connected(); +} + +String Command_Blynk_Set_c015(struct EventStruct *event, const char *Line) { + // todo add multicontroller support and chek it is connected and enabled + if (!Blynk.connected()) { + return F("Not connected to blynk server"); + } + + int vPin = event->Par1; + + if ((vPin < 0) || (vPin > 255)) { + return concat(F("Not correct blynk vPin number "), vPin); + } + + String data = parseString(Line, 3); + + if (data.isEmpty()) { + return concat(F("Skip sending empty data to blynk vPin "), vPin); + } + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, strformat( + F(C015_LOG_PREFIX "(online): send blynk pin v%d = %s"), + vPin, + data.c_str())); + } + + Blynk.virtualWrite(vPin, data); + return return_command_success(); +} + +boolean Blynk_send_c015(const String& value, int vPin, unsigned int clientTimeout) +{ + Blynk.virtualWrite(vPin, value); + + unsigned long timer = millis() + clientTimeout; + + while (!timeOutReached(timer)) { + backgroundtasks(); + } + return true; +} + +// This is called for all virtual pins, that don't have BLYNK_WRITE handler +BLYNK_WRITE_DEFAULT() { + const unsigned int vPin = request.pin; + const float pinValue = param.asFloat(); + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, strformat( + F(C015_LOG_PREFIX "server set v%u to %f"), + vPin, + pinValue)); + } + + if (Settings.UseRules) { + eventQueue.addMove(strformat( + F("blynkv%d=%f"), + vPin, + pinValue)); + } +} + +BLYNK_CONNECTED() { + // Your code here when hardware connects to Blynk Cloud or private server. + // It’s common to call sync functions inside of this function. + // Requests all stored on the server latest values for all widgets. + if (Settings.UseRules) { + eventQueue.add(F("blynk_connected")); + } + + // addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "connected handler")); +} + +// This is called when Smartphone App is opened +BLYNK_APP_CONNECTED() { + if (Settings.UseRules) { + eventQueue.add(F("blynk_app_connected")); + } + + // addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "app connected handler")); +} + +// This is called when Smartphone App is closed +BLYNK_APP_DISCONNECTED() { + if (Settings.UseRules) { + eventQueue.add(F("blynk_app_disconnected")); + } + + // addLog(LOG_LEVEL_INFO, F(C015_LOG_PREFIX "app disconnected handler")); +} + #endif // ifdef USES_C015 \ No newline at end of file diff --git a/src/_C017.cpp b/src/_C017.cpp index c3408d1fe3..d10cee5227 100644 --- a/src/_C017.cpp +++ b/src/_C017.cpp @@ -1,152 +1,152 @@ -#include "src/Helpers/_CPlugin_Helper.h" -#ifdef USES_C017 - -// ####################################################################################################### -// ########################### Controller Plugin 017: ZABBIX ########################################## -// ####################################################################################################### -// Based on https://www.zabbix.com/documentation/current/manual/appendix/items/trapper -// and https://www.zabbix.com/documentation/4.2/manual/appendix/protocols/header_datalen - -// USAGE: at Zabbix server you go at Configuration -> Hosts -> Create host -// The "Host name" should match exactly the EspEasy name (Config -> Unit Name) -// Add a group (mandatory) and hit add. No need to set up IP address or agent. -// Go to the newly created host ->Items ->Create Item -// Name the item something descriptive -// For Key add the EspEasy task Value name (case sensitive) -// Type of information select "Numeric (float)" and press add. -// Aslo make sure that you enable send to controller (under Data Acquisition in tasks) -// and set an interval because you need to actively send the data to Zabbix - -# define CPLUGIN_017 -# define CPLUGIN_ID_017 17 -# define CPLUGIN_NAME_017 "Zabbix" -# include - -bool CPlugin_017(CPlugin::Function function, struct EventStruct *event, String& string) -{ - bool success = false; - - switch (function) - { - case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: - { - ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_017; - proto.usesMQTT = false; - proto.usesTemplate = false; - proto.usesAccount = false; - proto.usesPassword = false; - proto.usesID = false; - proto.defaultPort = 10051; - break; - } - - case CPlugin::Function::CPLUGIN_GET_DEVICENAME: - { - string = F(CPLUGIN_NAME_017); - break; - } - - case CPlugin::Function::CPLUGIN_INIT: - { - success = init_c017_delay_queue(event->ControllerIndex); - break; - } - - case CPlugin::Function::CPLUGIN_EXIT: - { - exit_c017_delay_queue(); - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: - { - if (C017_DelayHandler == nullptr) { - break; - } - if (C017_DelayHandler->queueFull(event->ControllerIndex)) { - break; - } - - std::unique_ptr element(new C017_queue_element(event)); - success = C017_DelayHandler->addToQueue(std::move(element)); - Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C017_DELAY_QUEUE, C017_DelayHandler->getNextScheduleTime()); - break; - } - - case CPlugin::Function::CPLUGIN_FLUSH: - { - process_c017_delay_queue(); - delay(0); - break; - } - - default: - break; - } - return success; -} - -// Uncrustify may change this into multi line, which will result in failed builds -// *INDENT-OFF* -bool do_process_c017_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { - const C017_queue_element& element = static_cast(element_base); -// *INDENT-ON* - if (element.valueCount == 0) { - return true; // exit if we don't have anything to send. - } - - if (!NetworkConnected(10)) - { - return false; - } - - WiFiClient client; - - if (!try_connect_host(controller_number, client, ControllerSettings, F("ZBX : "))) - { - return false; - } - - const size_t capacity = JSON_ARRAY_SIZE(VARS_PER_TASK) + JSON_OBJECT_SIZE(2) + VARS_PER_TASK * JSON_OBJECT_SIZE(3) + VARS_PER_TASK * 50; //Size for esp8266 with 4 variables per task: 288+200 - String JSON_packet_content; - { - // Place the JSON document in a separate scope to have it destructed as soon as it is no longer needed. - DynamicJsonDocument root(capacity); - - // Create the schafolding - root[F("request")] = F("sender data"); - JsonArray data = root.createNestedArray(F("data")); - - // Populate JSON with the data - for (uint8_t i = 0; i < element.valueCount; i++) - { - const String taskValueName = getTaskValueName(element._taskIndex, i); - if (taskValueName.isEmpty()) { - continue; // Zabbix will ignore an empty key anyway - } - JsonObject block = data.createNestedObject(); - block[F("host")] = Settings.getName(); // Zabbix hostname, Unit Name for the ESP easy - block[F("key")] = taskValueName; // Zabbix item key // Value Name for the ESP easy - float value = 0.0f; - validFloatFromString(element.txt[i], value); - block[F("value")] = value; // ESPeasy supports only floats - } - serializeJson(root, JSON_packet_content); - } - - // Assemble packet - char packet_header[] = "ZBXD\1"; - - uint64_t payload_len = JSON_packet_content.length(); - - // addLog(LOG_LEVEL_INFO, concat(F("ZBX: "), JSON_packet_content)); - // Send the packet - client.write(packet_header, sizeof(packet_header) - 1); - client.write(reinterpret_cast(&payload_len), sizeof(payload_len)); - client.write(JSON_packet_content.c_str(), payload_len); - - client.stop(); - return true; -} - -#endif // ifdef USES_C017 +#include "src/Helpers/_CPlugin_Helper.h" +#ifdef USES_C017 + +// ####################################################################################################### +// ########################### Controller Plugin 017: ZABBIX ########################################## +// ####################################################################################################### +// Based on https://www.zabbix.com/documentation/current/manual/appendix/items/trapper +// and https://www.zabbix.com/documentation/4.2/manual/appendix/protocols/header_datalen + +// USAGE: at Zabbix server you go at Configuration -> Hosts -> Create host +// The "Host name" should match exactly the EspEasy name (Config -> Unit Name) +// Add a group (mandatory) and hit add. No need to set up IP address or agent. +// Go to the newly created host ->Items ->Create Item +// Name the item something descriptive +// For Key add the EspEasy task Value name (case sensitive) +// Type of information select "Numeric (float)" and press add. +// Aslo make sure that you enable send to controller (under Data Acquisition in tasks) +// and set an interval because you need to actively send the data to Zabbix + +# define CPLUGIN_017 +# define CPLUGIN_ID_017 17 +# define CPLUGIN_NAME_017 "Zabbix" +# include + +bool CPlugin_017(CPlugin::Function function, struct EventStruct *event, String& string) +{ + bool success = false; + + switch (function) + { + case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: + { + ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_017; + proto.usesMQTT = false; + proto.usesTemplate = false; + proto.usesAccount = false; + proto.usesPassword = false; + proto.usesID = false; + proto.defaultPort = 10051; + break; + } + + case CPlugin::Function::CPLUGIN_GET_DEVICENAME: + { + string = F(CPLUGIN_NAME_017); + break; + } + + case CPlugin::Function::CPLUGIN_INIT: + { + success = init_c017_delay_queue(event->ControllerIndex); + break; + } + + case CPlugin::Function::CPLUGIN_EXIT: + { + exit_c017_delay_queue(); + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: + { + if (C017_DelayHandler == nullptr) { + break; + } + if (C017_DelayHandler->queueFull(event->ControllerIndex)) { + break; + } + + std::unique_ptr element(new (std::nothrow) C017_queue_element(event)); + success = C017_DelayHandler->addToQueue(std::move(element)); + Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C017_DELAY_QUEUE, C017_DelayHandler->getNextScheduleTime()); + break; + } + + case CPlugin::Function::CPLUGIN_FLUSH: + { + process_c017_delay_queue(); + delay(0); + break; + } + + default: + break; + } + return success; +} + +// Uncrustify may change this into multi line, which will result in failed builds +// *INDENT-OFF* +bool do_process_c017_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { + const C017_queue_element& element = static_cast(element_base); +// *INDENT-ON* + if (element.valueCount == 0) { + return true; // exit if we don't have anything to send. + } + + if (!NetworkConnected(10)) + { + return false; + } + + WiFiClient client; + + if (!try_connect_host(controller_number, client, ControllerSettings, F("ZBX : "))) + { + return false; + } + + const size_t capacity = JSON_ARRAY_SIZE(VARS_PER_TASK) + JSON_OBJECT_SIZE(2) + VARS_PER_TASK * JSON_OBJECT_SIZE(3) + VARS_PER_TASK * 50; //Size for esp8266 with 4 variables per task: 288+200 + String JSON_packet_content; + { + // Place the JSON document in a separate scope to have it destructed as soon as it is no longer needed. + DynamicJsonDocument root(capacity); + + // Create the schafolding + root[F("request")] = F("sender data"); + JsonArray data = root.createNestedArray(F("data")); + + // Populate JSON with the data + for (uint8_t i = 0; i < element.valueCount; i++) + { + const String taskValueName = getTaskValueName(element._taskIndex, i); + if (taskValueName.isEmpty()) { + continue; // Zabbix will ignore an empty key anyway + } + JsonObject block = data.createNestedObject(); + block[F("host")] = Settings.getName(); // Zabbix hostname, Unit Name for the ESP easy + block[F("key")] = taskValueName; // Zabbix item key // Value Name for the ESP easy + float value = 0.0f; + validFloatFromString(element.txt[i], value); + block[F("value")] = value; // ESPeasy supports only floats + } + serializeJson(root, JSON_packet_content); + } + + // Assemble packet + char packet_header[] = "ZBXD\1"; + + uint64_t payload_len = JSON_packet_content.length(); + + // addLog(LOG_LEVEL_INFO, concat(F("ZBX: "), JSON_packet_content)); + // Send the packet + client.write(packet_header, sizeof(packet_header) - 1); + client.write(reinterpret_cast(&payload_len), sizeof(payload_len)); + client.write(JSON_packet_content.c_str(), payload_len); + + client.stop(); + return true; +} + +#endif // ifdef USES_C017 diff --git a/src/_C018.cpp b/src/_C018.cpp index 17006dcd1c..f4032e4a67 100644 --- a/src/_C018.cpp +++ b/src/_C018.cpp @@ -1,433 +1,433 @@ -#include "src/Helpers/_CPlugin_Helper.h" - -#ifdef USES_C018 - -// ####################################################################################################### -// ########################### Controller Plugin 018: LoRa TTN - RN2483/RN2903 ########################### -// ####################################################################################################### - -# define CPLUGIN_018 -# define CPLUGIN_ID_018 18 -# define CPLUGIN_NAME_018 "LoRa TTN - RN2483/RN2903" - - -# include - -# include "src/ControllerQueue/C018_queue_element.h" -# include "src/Controller_config/C018_config.h" -# include "src/Controller_struct/C018_data_struct.h" -# include "src/DataTypes/ESPEasy_plugin_functions.h" -# include "src/Globals/CPlugins.h" -# include "src/Helpers/_Plugin_Helper_serial.h" -# include "src/Helpers/StringGenerator_GPIO.h" -# include "src/WebServer/Markup.h" -# include "src/WebServer/Markup_Forms.h" -# include "src/WebServer/HTML_wrappers.h" - - -// Have this define after the includes, so we can set it in Custom.h -# ifndef C018_FORCE_SW_SERIAL -# define C018_FORCE_SW_SERIAL false -# endif // ifndef C018_FORCE_SW_SERIAL - - -// FIXME TD-er: Must add a controller data struct vector, like with plugins. -C018_data_struct *C018_data = nullptr; - - -// Forward declarations -bool C018_init(struct EventStruct *event); -String c018_add_joinChanged_script_element_line(const String& id, - bool forOTAA); - - -bool CPlugin_018(CPlugin::Function function, struct EventStruct *event, String& string) -{ - bool success = false; - - switch (function) - { - case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: - { - ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_018; - proto.usesMQTT = false; - proto.usesAccount = true; - proto.usesPassword = true; - proto.defaultPort = 1; - proto.usesID = true; - proto.usesHost = false; - proto.usesCheckReply = false; - proto.usesTimeout = false; - proto.usesSampleSets = true; - proto.needsNetwork = false; - break; - } - - case CPlugin::Function::CPLUGIN_GET_DEVICENAME: - { - string = F(CPLUGIN_NAME_018); - break; - } - - case CPlugin::Function::CPLUGIN_WEBFORM_SHOW_HOST_CONFIG: - { - if ((C018_data != nullptr) && C018_data->isInitialized()) { - string = F("Dev addr: "); - string += C018_data->getDevaddr(); - string += C018_data->useOTAA() ? F(" (OTAA)") : F(" (ABP)"); - } else { - string = F("-"); - } - break; - } - - case CPlugin::Function::CPLUGIN_INIT: - { - success = init_c018_delay_queue(event->ControllerIndex); - - if (success) { - C018_init(event); - } - break; - } - - case CPlugin::Function::CPLUGIN_EXIT: - { - if (C018_data != nullptr) { - C018_data->reset(); - delete C018_data; - C018_data = nullptr; - } - exit_c018_delay_queue(); - break; - } - - case CPlugin::Function::CPLUGIN_WEBFORM_LOAD: - { - { - // Script to toggle visibility of OTAA/ABP field, based on the activation method selector. - protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(event->ControllerIndex); - html_add_script(false); - addHtml(F("function joinChanged(elem){ var styleOTAA = elem.value == 0 ? '' : 'none'; var styleABP = elem.value == 1 ? '' : 'none';")); - addHtml(c018_add_joinChanged_script_element_line(getControllerParameterInternalName(ProtocolIndex, - ControllerSettingsStruct::CONTROLLER_USER), - true)); - addHtml(c018_add_joinChanged_script_element_line(getControllerParameterInternalName(ProtocolIndex, - ControllerSettingsStruct::CONTROLLER_PASS), - true)); - addHtml(c018_add_joinChanged_script_element_line(F("deveui"), true)); - addHtml(c018_add_joinChanged_script_element_line(F("deveui_note"), true)); - - addHtml(c018_add_joinChanged_script_element_line(F("devaddr"), false)); - addHtml(c018_add_joinChanged_script_element_line(F("nskey"), false)); - addHtml(c018_add_joinChanged_script_element_line(F("appskey"), false)); - addHtml('}'); - html_add_script_end(); - } - - { - // Keep this object in a small scope so we can destruct it as soon as possible again. - std::shared_ptr customConfig(new (std::nothrow) C018_ConfigStruct); - - if (!customConfig) { - break; - } - LoadCustomControllerSettings(event->ControllerIndex, reinterpret_cast(customConfig.get()), sizeof(C018_ConfigStruct)); - customConfig->webform_load(C018_data); - } - - break; - } - case CPlugin::Function::CPLUGIN_WEBFORM_SAVE: - { - std::shared_ptr customConfig(new (std::nothrow) C018_ConfigStruct); - - if (customConfig) { - customConfig->webform_save(); - SaveCustomControllerSettings(event->ControllerIndex, reinterpret_cast(customConfig.get()), - sizeof(C018_ConfigStruct)); - } - break; - } - - case CPlugin::Function::CPLUGIN_GET_PROTOCOL_DISPLAY_NAME: - { - success = true; - - switch (event->idx) { - case ControllerSettingsStruct::CONTROLLER_USER: - string = F("AppEUI"); - break; - case ControllerSettingsStruct::CONTROLLER_PASS: - string = F("AppKey"); - break; - case ControllerSettingsStruct::CONTROLLER_TIMEOUT: - string = F("Module Timeout"); - break; - case ControllerSettingsStruct::CONTROLLER_PORT: - string = F("Port"); - break; - default: - success = false; - break; - } - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: - { - if (C018_DelayHandler == nullptr) { - break; - } - - if (C018_DelayHandler->queueFull(event->ControllerIndex)) { - break; - } - - if (C018_data != nullptr) { - { - std::unique_ptr element(new C018_queue_element(event, C018_data->getSampleSetCount(event->TaskIndex))); - success = C018_DelayHandler->addToQueue(std::move(element)); - Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C018_DELAY_QUEUE, - C018_DelayHandler->getNextScheduleTime()); - } - - if (!C018_data->isInitialized()) { - // Sometimes the module does need some time after power on to respond. - // So it may not be initialized well at the call of CPLUGIN_INIT - // We try to trigger its init again when sending data. - C018_init(event); - } - } - break; - } - - case CPlugin::Function::CPLUGIN_PROTOCOL_RECV: - { - // FIXME TD-er: WHen should this be scheduled? - // protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(event->ControllerIndex); - // schedule_controller_event_timer(ProtocolIndex, CPlugin::Function::CPLUGIN_PROTOCOL_RECV, event); - break; - } - - case CPlugin::Function::CPLUGIN_WRITE: - { - if (C018_data != nullptr) { - if (C018_data->isInitialized()) - { - const String command = parseString(string, 1); - - if (equals(command, F("lorawan"))) { - const String subcommand = parseString(string, 2); - - if (equals(subcommand, F("write"))) { - const String loraWriteCommand = parseStringToEnd(string, 3); - const String res = C018_data->sendRawCommand(loraWriteCommand); - String logstr = F("LoRaWAN cmd: "); - logstr += loraWriteCommand; - logstr += F(" -> "); - logstr += res; - addLog(LOG_LEVEL_INFO, logstr); - SendStatus(event, logstr); - success = true; - } - } - } - } - break; - } - - case CPlugin::Function::CPLUGIN_FIFTY_PER_SECOND: - { - if (C018_data != nullptr) { - C018_data->async_loop(); - } - - // FIXME TD-er: Handle reading error state or return values. - break; - } - - case CPlugin::Function::CPLUGIN_FLUSH: - { - process_c018_delay_queue(); - delay(0); - break; - } - - default: - break; - } - return success; -} - -bool C018_init(struct EventStruct *event) { - String AppEUI; - String AppKey; - taskIndex_t SampleSetInitiator = INVALID_TASK_INDEX; - unsigned int Port = 0; - - // Check if the object is already created. - // If so, delete it to make sure the module is initialized according to the full set parameters. - if (C018_data != nullptr) { - C018_data->reset(); - delete C018_data; - C018_data = nullptr; - } - - - C018_data = new (std::nothrow) C018_data_struct; - - if (C018_data == nullptr) { - return false; - } - { - // Allocate ControllerSettings object in a scope, so we can destruct it as soon as possible. - MakeControllerSettings(ControllerSettings); // -V522 - - if (!AllocatedControllerSettings()) { - return false; - } - - LoadControllerSettings(event->ControllerIndex, *ControllerSettings); - C018_DelayHandler->cacheControllerSettings(*ControllerSettings); - AppEUI = getControllerUser(event->ControllerIndex, *ControllerSettings); - AppKey = getControllerPass(event->ControllerIndex, *ControllerSettings); - SampleSetInitiator = ControllerSettings->SampleSetInitiator; - Port = ControllerSettings->Port; - } - - std::shared_ptr customConfig(new (std::nothrow) C018_ConfigStruct); - - if (!customConfig) { - return false; - } - LoadCustomControllerSettings(event->ControllerIndex, reinterpret_cast(customConfig.get()), sizeof(C018_ConfigStruct)); - customConfig->validate(); - - if (!C018_data->init(customConfig->serialPort, customConfig->rxpin, customConfig->txpin, customConfig->baudrate, - (customConfig->joinmethod == C018_USE_OTAA), - SampleSetInitiator, customConfig->resetpin)) - { - return false; - } - - C018_data->setFrequencyPlan(static_cast(customConfig->frequencyplan), customConfig->rx2_freq); - - if (!C018_data->setSF(customConfig->sf)) { - return false; - } - - if (!C018_data->setAdaptiveDataRate(customConfig->adr != 0)) { - return false; - } - - if (!C018_data->setTTNstack(static_cast(customConfig->stackVersion))) { - return false; - } - - if (customConfig->joinmethod == C018_USE_OTAA) { - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("OTAA: AppEUI: "); - log += AppEUI; - log += F(" AppKey: "); - log += AppKey; - log += F(" DevEUI: "); - log += customConfig->DeviceEUI; - - addLogMove(LOG_LEVEL_INFO, log); - } - - if (!C018_data->initOTAA(AppEUI, AppKey, customConfig->DeviceEUI)) { - return false; - } - } - else { - if (!C018_data->initABP(customConfig->DeviceAddr, customConfig->AppSessionKey, customConfig->NetworkSessionKey)) { - return false; - } - } - - - if (!C018_data->txUncnf(F("ESPeasy (TTN)"), Port)) { - return false; - } - return true; -} - -// Uncrustify may change this into multi line, which will result in failed builds -// *INDENT-OFF* -bool do_process_c018_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { - const C018_queue_element& element = static_cast(element_base); -// *INDENT-ON* - uint8_t pl = (element.packed.length() / 2); - float airtime_ms = C018_data->getLoRaAirTime(pl); - bool mustSetDelay = false; - bool success = false; - - if (!C018_data->command_finished()) { - mustSetDelay = true; - } else { - success = C018_data->txHexBytes(element.packed, ControllerSettings.Port); - - if (success) { - if (airtime_ms > 0.0f) { - ADD_TIMER_STAT(C018_AIR_TIME, static_cast(airtime_ms * 1000)); - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("LoRaWAN : Payload Length: "); - log += pl + 13; // We have a LoRaWAN header of 13 bytes. - log += F(" Air Time: "); - log += toString(airtime_ms, 3); - log += F(" ms"); - addLogMove(LOG_LEVEL_INFO, log); - } - } - } - } - String error = C018_data->getLastError(); // Clear the error string. - - if (error.indexOf(F("no_free_ch")) != -1) { - mustSetDelay = true; - } - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("C018 : Sent: "); - log += element.packed; - log += F(" length: "); - log += String(element.packed.length()); - - if (success) { - log += F(" (success) "); - } - log += error; - addLogMove(LOG_LEVEL_INFO, log); - } - - if (mustSetDelay) { - // Module is still sending, delay for 10x expected air time, which is equivalent of 10% air time duty cycle. - // This can be retried a few times, so at most 10 retries like these are needed to get below 1% air time again. - // Very likely only 2 - 3 of these delays are needed, as we have 8 channels to send from and messages are likely sent in bursts. - C018_DelayHandler->setAdditionalDelay(10 * airtime_ms); - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("LoRaWAN : Unable to send. Delay for "); - log += 10 * airtime_ms; - log += F(" ms"); - addLogMove(LOG_LEVEL_INFO, log); - } - } - - return success; -} - -String c018_add_joinChanged_script_element_line(const String& id, bool forOTAA) { - String result = F("document.getElementById('tr_"); - - result += id; - result += F("').style.display = style"); - result += forOTAA ? F("OTAA") : F("ABP"); - result += ';'; - return result; -} - -#endif // ifdef USES_C018 +#include "src/Helpers/_CPlugin_Helper.h" + +#ifdef USES_C018 + +// ####################################################################################################### +// ########################### Controller Plugin 018: LoRa TTN - RN2483/RN2903 ########################### +// ####################################################################################################### + +# define CPLUGIN_018 +# define CPLUGIN_ID_018 18 +# define CPLUGIN_NAME_018 "LoRa TTN - RN2483/RN2903" + + +# include + +# include "src/ControllerQueue/C018_queue_element.h" +# include "src/Controller_config/C018_config.h" +# include "src/Controller_struct/C018_data_struct.h" +# include "src/DataTypes/ESPEasy_plugin_functions.h" +# include "src/Globals/CPlugins.h" +# include "src/Helpers/_Plugin_Helper_serial.h" +# include "src/Helpers/StringGenerator_GPIO.h" +# include "src/WebServer/Markup.h" +# include "src/WebServer/Markup_Forms.h" +# include "src/WebServer/HTML_wrappers.h" + + +// Have this define after the includes, so we can set it in Custom.h +# ifndef C018_FORCE_SW_SERIAL +# define C018_FORCE_SW_SERIAL false +# endif // ifndef C018_FORCE_SW_SERIAL + + +// FIXME TD-er: Must add a controller data struct vector, like with plugins. +C018_data_struct *C018_data = nullptr; + + +// Forward declarations +bool C018_init(struct EventStruct *event); +String c018_add_joinChanged_script_element_line(const String& id, + bool forOTAA); + + +bool CPlugin_018(CPlugin::Function function, struct EventStruct *event, String& string) +{ + bool success = false; + + switch (function) + { + case CPlugin::Function::CPLUGIN_PROTOCOL_ADD: + { + ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_018; + proto.usesMQTT = false; + proto.usesAccount = true; + proto.usesPassword = true; + proto.defaultPort = 1; + proto.usesID = true; + proto.usesHost = false; + proto.usesCheckReply = false; + proto.usesTimeout = false; + proto.usesSampleSets = true; + proto.needsNetwork = false; + break; + } + + case CPlugin::Function::CPLUGIN_GET_DEVICENAME: + { + string = F(CPLUGIN_NAME_018); + break; + } + + case CPlugin::Function::CPLUGIN_WEBFORM_SHOW_HOST_CONFIG: + { + if ((C018_data != nullptr) && C018_data->isInitialized()) { + string = F("Dev addr: "); + string += C018_data->getDevaddr(); + string += C018_data->useOTAA() ? F(" (OTAA)") : F(" (ABP)"); + } else { + string = F("-"); + } + break; + } + + case CPlugin::Function::CPLUGIN_INIT: + { + success = init_c018_delay_queue(event->ControllerIndex); + + if (success) { + C018_init(event); + } + break; + } + + case CPlugin::Function::CPLUGIN_EXIT: + { + if (C018_data != nullptr) { + C018_data->reset(); + delete C018_data; + C018_data = nullptr; + } + exit_c018_delay_queue(); + break; + } + + case CPlugin::Function::CPLUGIN_WEBFORM_LOAD: + { + { + // Script to toggle visibility of OTAA/ABP field, based on the activation method selector. + protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(event->ControllerIndex); + html_add_script(false); + addHtml(F("function joinChanged(elem){ var styleOTAA = elem.value == 0 ? '' : 'none'; var styleABP = elem.value == 1 ? '' : 'none';")); + addHtml(c018_add_joinChanged_script_element_line(getControllerParameterInternalName(ProtocolIndex, + ControllerSettingsStruct::CONTROLLER_USER), + true)); + addHtml(c018_add_joinChanged_script_element_line(getControllerParameterInternalName(ProtocolIndex, + ControllerSettingsStruct::CONTROLLER_PASS), + true)); + addHtml(c018_add_joinChanged_script_element_line(F("deveui"), true)); + addHtml(c018_add_joinChanged_script_element_line(F("deveui_note"), true)); + + addHtml(c018_add_joinChanged_script_element_line(F("devaddr"), false)); + addHtml(c018_add_joinChanged_script_element_line(F("nskey"), false)); + addHtml(c018_add_joinChanged_script_element_line(F("appskey"), false)); + addHtml('}'); + html_add_script_end(); + } + + { + // Keep this object in a small scope so we can destruct it as soon as possible again. + std::shared_ptr customConfig(new (std::nothrow) C018_ConfigStruct); + + if (!customConfig) { + break; + } + LoadCustomControllerSettings(event->ControllerIndex, reinterpret_cast(customConfig.get()), sizeof(C018_ConfigStruct)); + customConfig->webform_load(C018_data); + } + + break; + } + case CPlugin::Function::CPLUGIN_WEBFORM_SAVE: + { + std::shared_ptr customConfig(new (std::nothrow) C018_ConfigStruct); + + if (customConfig) { + customConfig->webform_save(); + SaveCustomControllerSettings(event->ControllerIndex, reinterpret_cast(customConfig.get()), + sizeof(C018_ConfigStruct)); + } + break; + } + + case CPlugin::Function::CPLUGIN_GET_PROTOCOL_DISPLAY_NAME: + { + success = true; + + switch (event->idx) { + case ControllerSettingsStruct::CONTROLLER_USER: + string = F("AppEUI"); + break; + case ControllerSettingsStruct::CONTROLLER_PASS: + string = F("AppKey"); + break; + case ControllerSettingsStruct::CONTROLLER_TIMEOUT: + string = F("Module Timeout"); + break; + case ControllerSettingsStruct::CONTROLLER_PORT: + string = F("Port"); + break; + default: + success = false; + break; + } + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_SEND: + { + if (C018_DelayHandler == nullptr) { + break; + } + + if (C018_DelayHandler->queueFull(event->ControllerIndex)) { + break; + } + + if (C018_data != nullptr) { + { + std::unique_ptr element(new (std::nothrow) C018_queue_element(event, C018_data->getSampleSetCount(event->TaskIndex))); + success = C018_DelayHandler->addToQueue(std::move(element)); + Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C018_DELAY_QUEUE, + C018_DelayHandler->getNextScheduleTime()); + } + + if (!C018_data->isInitialized()) { + // Sometimes the module does need some time after power on to respond. + // So it may not be initialized well at the call of CPLUGIN_INIT + // We try to trigger its init again when sending data. + C018_init(event); + } + } + break; + } + + case CPlugin::Function::CPLUGIN_PROTOCOL_RECV: + { + // FIXME TD-er: WHen should this be scheduled? + // protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(event->ControllerIndex); + // schedule_controller_event_timer(ProtocolIndex, CPlugin::Function::CPLUGIN_PROTOCOL_RECV, event); + break; + } + + case CPlugin::Function::CPLUGIN_WRITE: + { + if (C018_data != nullptr) { + if (C018_data->isInitialized()) + { + const String command = parseString(string, 1); + + if (equals(command, F("lorawan"))) { + const String subcommand = parseString(string, 2); + + if (equals(subcommand, F("write"))) { + const String loraWriteCommand = parseStringToEnd(string, 3); + const String res = C018_data->sendRawCommand(loraWriteCommand); + String logstr = F("LoRaWAN cmd: "); + logstr += loraWriteCommand; + logstr += F(" -> "); + logstr += res; + addLog(LOG_LEVEL_INFO, logstr); + SendStatus(event, logstr); + success = true; + } + } + } + } + break; + } + + case CPlugin::Function::CPLUGIN_FIFTY_PER_SECOND: + { + if (C018_data != nullptr) { + C018_data->async_loop(); + } + + // FIXME TD-er: Handle reading error state or return values. + break; + } + + case CPlugin::Function::CPLUGIN_FLUSH: + { + process_c018_delay_queue(); + delay(0); + break; + } + + default: + break; + } + return success; +} + +bool C018_init(struct EventStruct *event) { + String AppEUI; + String AppKey; + taskIndex_t SampleSetInitiator = INVALID_TASK_INDEX; + unsigned int Port = 0; + + // Check if the object is already created. + // If so, delete it to make sure the module is initialized according to the full set parameters. + if (C018_data != nullptr) { + C018_data->reset(); + delete C018_data; + C018_data = nullptr; + } + + + C018_data = new (std::nothrow) C018_data_struct; + + if (C018_data == nullptr) { + return false; + } + { + // Allocate ControllerSettings object in a scope, so we can destruct it as soon as possible. + MakeControllerSettings(ControllerSettings); // -V522 + + if (!AllocatedControllerSettings()) { + return false; + } + + LoadControllerSettings(event->ControllerIndex, *ControllerSettings); + C018_DelayHandler->cacheControllerSettings(*ControllerSettings); + AppEUI = getControllerUser(event->ControllerIndex, *ControllerSettings); + AppKey = getControllerPass(event->ControllerIndex, *ControllerSettings); + SampleSetInitiator = ControllerSettings->SampleSetInitiator; + Port = ControllerSettings->Port; + } + + std::shared_ptr customConfig(new (std::nothrow) C018_ConfigStruct); + + if (!customConfig) { + return false; + } + LoadCustomControllerSettings(event->ControllerIndex, reinterpret_cast(customConfig.get()), sizeof(C018_ConfigStruct)); + customConfig->validate(); + + if (!C018_data->init(customConfig->serialPort, customConfig->rxpin, customConfig->txpin, customConfig->baudrate, + (customConfig->joinmethod == C018_USE_OTAA), + SampleSetInitiator, customConfig->resetpin)) + { + return false; + } + + C018_data->setFrequencyPlan(static_cast(customConfig->frequencyplan), customConfig->rx2_freq); + + if (!C018_data->setSF(customConfig->sf)) { + return false; + } + + if (!C018_data->setAdaptiveDataRate(customConfig->adr != 0)) { + return false; + } + + if (!C018_data->setTTNstack(static_cast(customConfig->stackVersion))) { + return false; + } + + if (customConfig->joinmethod == C018_USE_OTAA) { + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("OTAA: AppEUI: "); + log += AppEUI; + log += F(" AppKey: "); + log += AppKey; + log += F(" DevEUI: "); + log += customConfig->DeviceEUI; + + addLogMove(LOG_LEVEL_INFO, log); + } + + if (!C018_data->initOTAA(AppEUI, AppKey, customConfig->DeviceEUI)) { + return false; + } + } + else { + if (!C018_data->initABP(customConfig->DeviceAddr, customConfig->AppSessionKey, customConfig->NetworkSessionKey)) { + return false; + } + } + + + if (!C018_data->txUncnf(F("ESPeasy (TTN)"), Port)) { + return false; + } + return true; +} + +// Uncrustify may change this into multi line, which will result in failed builds +// *INDENT-OFF* +bool do_process_c018_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) { + const C018_queue_element& element = static_cast(element_base); +// *INDENT-ON* + uint8_t pl = (element.packed.length() / 2); + float airtime_ms = C018_data->getLoRaAirTime(pl); + bool mustSetDelay = false; + bool success = false; + + if (!C018_data->command_finished()) { + mustSetDelay = true; + } else { + success = C018_data->txHexBytes(element.packed, ControllerSettings.Port); + + if (success) { + if (airtime_ms > 0.0f) { + ADD_TIMER_STAT(C018_AIR_TIME, static_cast(airtime_ms * 1000)); + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("LoRaWAN : Payload Length: "); + log += pl + 13; // We have a LoRaWAN header of 13 bytes. + log += F(" Air Time: "); + log += toString(airtime_ms, 3); + log += F(" ms"); + addLogMove(LOG_LEVEL_INFO, log); + } + } + } + } + String error = C018_data->getLastError(); // Clear the error string. + + if (error.indexOf(F("no_free_ch")) != -1) { + mustSetDelay = true; + } + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("C018 : Sent: "); + log += element.packed; + log += F(" length: "); + log += String(element.packed.length()); + + if (success) { + log += F(" (success) "); + } + log += error; + addLogMove(LOG_LEVEL_INFO, log); + } + + if (mustSetDelay) { + // Module is still sending, delay for 10x expected air time, which is equivalent of 10% air time duty cycle. + // This can be retried a few times, so at most 10 retries like these are needed to get below 1% air time again. + // Very likely only 2 - 3 of these delays are needed, as we have 8 channels to send from and messages are likely sent in bursts. + C018_DelayHandler->setAdditionalDelay(10 * airtime_ms); + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("LoRaWAN : Unable to send. Delay for "); + log += 10 * airtime_ms; + log += F(" ms"); + addLogMove(LOG_LEVEL_INFO, log); + } + } + + return success; +} + +String c018_add_joinChanged_script_element_line(const String& id, bool forOTAA) { + String result = F("document.getElementById('tr_"); + + result += id; + result += F("').style.display = style"); + result += forOTAA ? F("OTAA") : F("ABP"); + result += ';'; + return result; +} + +#endif // ifdef USES_C018 diff --git a/src/src/DataStructs/ControllerSettingsStruct.h b/src/src/DataStructs/ControllerSettingsStruct.h index ff45d4fd21..d9a91c7455 100644 --- a/src/src/DataStructs/ControllerSettingsStruct.h +++ b/src/src/DataStructs/ControllerSettingsStruct.h @@ -1,230 +1,232 @@ -#ifndef DATASTRUCTS_CONTROLLERSETTINGSSTRUCT_H -#define DATASTRUCTS_CONTROLLERSETTINGSSTRUCT_H - -/*********************************************************************************************\ -* ControllerSettingsStruct definition -\*********************************************************************************************/ -#include "../../ESPEasy_common.h" - -#include // For std::shared_ptr -#include // for std::nothrow - -#include -#include -#include - -#include "../DataStructs/ChecksumType.h" -#include "../Globals/Plugins.h" - -// Minimum delay between messages for a controller to send in msec. -#ifndef CONTROLLER_DELAY_QUEUE_DELAY_MAX -# define CONTROLLER_DELAY_QUEUE_DELAY_MAX 3600000 -#endif // ifndef CONTROLLER_DELAY_QUEUE_DELAY_MAX -#ifndef CONTROLLER_DELAY_QUEUE_DELAY_DFLT -# define CONTROLLER_DELAY_QUEUE_DELAY_DFLT 100 -#endif // ifndef CONTROLLER_DELAY_QUEUE_DELAY_DFLT - -// Queue length for controller messages not yet sent. -#ifndef CONTROLLER_DELAY_QUEUE_DEPTH_MAX -# define CONTROLLER_DELAY_QUEUE_DEPTH_MAX 50 -#endif // ifndef CONTROLLER_DELAY_QUEUE_DEPTH_MAX -#ifndef CONTROLLER_DELAY_QUEUE_DEPTH_DFLT -# define CONTROLLER_DELAY_QUEUE_DEPTH_DFLT 10 -#endif // ifndef CONTROLLER_DELAY_QUEUE_DEPTH_DFLT - -// Number of retries to send a message by a controller. -// N.B. Retries without a connection to wifi do not count as retry. -#ifndef CONTROLLER_DELAY_QUEUE_RETRY_MAX -# define CONTROLLER_DELAY_QUEUE_RETRY_MAX 10 -#endif // ifndef CONTROLLER_DELAY_QUEUE_RETRY_MAX -#ifndef CONTROLLER_DELAY_QUEUE_RETRY_DFLT -# define CONTROLLER_DELAY_QUEUE_RETRY_DFLT 10 -#endif // ifndef CONTROLLER_DELAY_QUEUE_RETRY_DFLT - -// Timeout of the client in msec. -#ifndef CONTROLLER_CLIENTTIMEOUT_MAX -# define CONTROLLER_CLIENTTIMEOUT_MAX 4000 // Not sure if this may trigger SW watchdog. -#endif // ifndef CONTROLLER_CLIENTTIMEOUT_MAX -#ifndef CONTROLLER_CLIENTTIMEOUT_DFLT -# define CONTROLLER_CLIENTTIMEOUT_DFLT 100 -#endif // ifndef CONTROLLER_CLIENTTIMEOUT_DFLT - -#ifndef CONTROLLER_DEFAULT_CLIENTID -# define CONTROLLER_DEFAULT_CLIENTID "%sysname%_%unit%" -#endif // ifndef CONTROLLER_DEFAULT_CLIENTID - -struct ControllerSettingsStruct -{ - // ******************************************************************************** - // IDs of controller settings, used to generate web forms - // ******************************************************************************** - enum VarType { - CONTROLLER_USE_DNS = 0, // PLace this before HOSTNAME/IP - CONTROLLER_USE_EXTENDED_CREDENTIALS = 1, // Place this before USER/PASS - CONTROLLER_HOSTNAME, - CONTROLLER_IP, - CONTROLLER_PORT, - CONTROLLER_USER, - CONTROLLER_PASS, - CONTROLLER_MIN_SEND_INTERVAL, - CONTROLLER_MAX_QUEUE_DEPTH, - CONTROLLER_MAX_RETRIES, - CONTROLLER_FULL_QUEUE_ACTION, - CONTROLLER_ALLOW_EXPIRE, - CONTROLLER_DEDUPLICATE, - CONTROLLER_USE_LOCAL_SYSTEM_TIME, - CONTROLLER_CHECK_REPLY, - CONTROLLER_CLIENT_ID, - CONTROLLER_UNIQUE_CLIENT_ID_RECONNECT, - CONTROLLER_RETAINFLAG, - CONTROLLER_SUBSCRIBE, - CONTROLLER_PUBLISH, - CONTROLLER_LWT_TOPIC, - CONTROLLER_LWT_CONNECT_MESSAGE, - CONTROLLER_LWT_DISCONNECT_MESSAGE, - CONTROLLER_SEND_LWT, - CONTROLLER_WILL_RETAIN, - CONTROLLER_CLEAN_SESSION, - CONTROLLER_TIMEOUT, - CONTROLLER_SAMPLE_SET_INITIATOR, - CONTROLLER_SEND_BINARY, - - // Keep this as last, is used to loop over all parameters - CONTROLLER_ENABLED - }; - - - ControllerSettingsStruct(); - - void reset(); - - bool isSet() const; - - void validate(); - - ChecksumType computeChecksum() const { - return ChecksumType(reinterpret_cast(this), sizeof(ControllerSettingsStruct)); - } - - IPAddress getIP() const { - return IPAddress(IP[0], IP[1], IP[2], IP[3]); - } - - String getHost() const; - - void setHostname(const String& controllerhostname); - - bool checkHostReachable(bool quick); - - #if FEATURE_HTTP_CLIENT - bool connectToHost(WiFiClient& client); - #endif // FEATURE_HTTP_CLIENT - - bool beginPacket(WiFiUDP& client); - - String getHostPortString() const; - - // VariousBits1 defaults to 0, keep in mind when adding bit lookups. - bool mqtt_cleanSession() const { return VariousBits1.mqtt_cleanSession; } - void mqtt_cleanSession(bool value) { VariousBits1.mqtt_cleanSession = value; } - - bool mqtt_sendLWT() const { return !VariousBits1.mqtt_not_sendLWT; } - void mqtt_sendLWT(bool value) { VariousBits1.mqtt_not_sendLWT = !value; } - - bool mqtt_willRetain() const { return !VariousBits1.mqtt_not_willRetain; } - void mqtt_willRetain(bool value) { VariousBits1.mqtt_not_willRetain = !value; } - - bool mqtt_uniqueMQTTclientIdReconnect() const { return VariousBits1.mqtt_uniqueMQTTclientIdReconnect; } - void mqtt_uniqueMQTTclientIdReconnect(bool value) { VariousBits1.mqtt_uniqueMQTTclientIdReconnect = value; } - - bool mqtt_retainFlag() const { return VariousBits1.mqtt_retainFlag; } - void mqtt_retainFlag(bool value) { VariousBits1.mqtt_retainFlag = value; } - - bool useExtendedCredentials() const { return VariousBits1.useExtendedCredentials; } - void useExtendedCredentials(bool value) { VariousBits1.useExtendedCredentials = value; } - - bool sendBinary() const { return VariousBits1.sendBinary; } - void sendBinary(bool value) { VariousBits1.sendBinary = value; } - - bool allowExpire() const { return VariousBits1.allowExpire; } - void allowExpire(bool value) { VariousBits1.allowExpire = value; } - - bool deduplicate() const { return VariousBits1.deduplicate; } - void deduplicate(bool value) { VariousBits1.deduplicate = value; } - - bool useLocalSystemTime() const { return VariousBits1.useLocalSystemTime; } - void useLocalSystemTime(bool value) { VariousBits1.useLocalSystemTime = value; } - - bool UseDNS; - uint8_t IP[4]; - unsigned int Port; - char HostName[65]; - char Publish[129]; - char Subscribe[129]; - char MQTTLwtTopic[129]; - char LWTMessageConnect[129]; - char LWTMessageDisconnect[129]; - unsigned int MinimalTimeBetweenMessages; - unsigned int MaxQueueDepth; - unsigned int MaxRetry; - bool DeleteOldest; // Action to perform when buffer full, delete oldest, or ignore newest. - unsigned int ClientTimeout; - bool MustCheckReply; // When set to false, a sent message is considered always successful. - taskIndex_t SampleSetInitiator; // The first task to start a sample set. - - struct { - uint32_t unused_00 : 1; // Bit 00 - uint32_t mqtt_cleanSession : 1; // Bit 01 - uint32_t mqtt_not_sendLWT : 1; // Bit 02, !value, default enabled - uint32_t mqtt_not_willRetain : 1; // Bit 03, !value, default enabled - uint32_t mqtt_uniqueMQTTclientIdReconnect : 1; // Bit 04 - uint32_t mqtt_retainFlag : 1; // Bit 05 - uint32_t useExtendedCredentials : 1; // Bit 06 - uint32_t sendBinary : 1; // Bit 07 - uint32_t unused_08 : 1; // Bit 08 - uint32_t allowExpire : 1; // Bit 09 - uint32_t deduplicate : 1; // Bit 10 - uint32_t useLocalSystemTime : 1; // Bit 11 - uint32_t unused_12 : 1; // Bit 12 - uint32_t unused_13 : 1; // Bit 13 - uint32_t unused_14 : 1; // Bit 14 - uint32_t unused_15 : 1; // Bit 15 - uint32_t unused_16 : 1; // Bit 16 - uint32_t unused_17 : 1; // Bit 17 - uint32_t unused_18 : 1; // Bit 18 - uint32_t unused_19 : 1; // Bit 19 - uint32_t unused_20 : 1; // Bit 20 - uint32_t unused_21 : 1; // Bit 21 - uint32_t unused_22 : 1; // Bit 22 - uint32_t unused_23 : 1; // Bit 23 - uint32_t unused_24 : 1; // Bit 24 - uint32_t unused_25 : 1; // Bit 25 - uint32_t unused_26 : 1; // Bit 26 - uint32_t unused_27 : 1; // Bit 27 - uint32_t unused_28 : 1; // Bit 28 - uint32_t unused_29 : 1; // Bit 29 - uint32_t unused_30 : 1; // Bit 30 - uint32_t unused_31 : 1; // Bit 31 - } VariousBits1; - char ClientID[65]; // Used to define the Client ID used by the controller - -private: - - bool ipSet() const; - - bool updateIPcache(); -}; - -typedef std::shared_ptr ControllerSettingsStruct_ptr_type; -/* -# ifdef USE_SECOND_HEAP -#define MakeControllerSettings(T) HeapSelectIram ephemeral; ControllerSettingsStruct_ptr_type T(new (std::nothrow) ControllerSettingsStruct()); -#else -*/ -#define MakeControllerSettings(T) ControllerSettingsStruct_ptr_type T(new (std::nothrow) ControllerSettingsStruct()); -//#endif - -// Check to see if MakeControllerSettings was successful -#define AllocatedControllerSettings() (ControllerSettings.get() != nullptr) - -#endif // DATASTRUCTS_CONTROLLERSETTINGSSTRUCT_H +#ifndef DATASTRUCTS_CONTROLLERSETTINGSSTRUCT_H +#define DATASTRUCTS_CONTROLLERSETTINGSSTRUCT_H + +/*********************************************************************************************\ +* ControllerSettingsStruct definition +\*********************************************************************************************/ +#include "../../ESPEasy_common.h" + +#include // For std::shared_ptr +#include // for std::nothrow + +#include +#include +#include + +#include "../DataStructs/ChecksumType.h" +#include "../Globals/Plugins.h" + +// Minimum delay between messages for a controller to send in msec. +#ifndef CONTROLLER_DELAY_QUEUE_DELAY_MAX +# define CONTROLLER_DELAY_QUEUE_DELAY_MAX 3600000 +#endif // ifndef CONTROLLER_DELAY_QUEUE_DELAY_MAX +#ifndef CONTROLLER_DELAY_QUEUE_DELAY_DFLT +# define CONTROLLER_DELAY_QUEUE_DELAY_DFLT 100 +#endif // ifndef CONTROLLER_DELAY_QUEUE_DELAY_DFLT + +// Queue length for controller messages not yet sent. +#ifndef CONTROLLER_DELAY_QUEUE_DEPTH_MAX +# define CONTROLLER_DELAY_QUEUE_DEPTH_MAX 50 +#endif // ifndef CONTROLLER_DELAY_QUEUE_DEPTH_MAX +#ifndef CONTROLLER_DELAY_QUEUE_DEPTH_DFLT +# define CONTROLLER_DELAY_QUEUE_DEPTH_DFLT 10 +#endif // ifndef CONTROLLER_DELAY_QUEUE_DEPTH_DFLT + +// Number of retries to send a message by a controller. +// N.B. Retries without a connection to wifi do not count as retry. +#ifndef CONTROLLER_DELAY_QUEUE_RETRY_MAX +# define CONTROLLER_DELAY_QUEUE_RETRY_MAX 10 +#endif // ifndef CONTROLLER_DELAY_QUEUE_RETRY_MAX +#ifndef CONTROLLER_DELAY_QUEUE_RETRY_DFLT +# define CONTROLLER_DELAY_QUEUE_RETRY_DFLT 10 +#endif // ifndef CONTROLLER_DELAY_QUEUE_RETRY_DFLT + +// Timeout of the client in msec. +#ifndef CONTROLLER_CLIENTTIMEOUT_MAX +# define CONTROLLER_CLIENTTIMEOUT_MAX 4000 // Not sure if this may trigger SW watchdog. +#endif // ifndef CONTROLLER_CLIENTTIMEOUT_MAX +#ifndef CONTROLLER_CLIENTTIMEOUT_DFLT +# define CONTROLLER_CLIENTTIMEOUT_DFLT 100 +#endif // ifndef CONTROLLER_CLIENTTIMEOUT_DFLT + +#ifndef CONTROLLER_DEFAULT_CLIENTID +# define CONTROLLER_DEFAULT_CLIENTID "%sysname%_%unit%" +#endif // ifndef CONTROLLER_DEFAULT_CLIENTID + +struct ControllerSettingsStruct +{ + // ******************************************************************************** + // IDs of controller settings, used to generate web forms + // ******************************************************************************** + enum VarType { + CONTROLLER_USE_DNS = 0, // PLace this before HOSTNAME/IP + CONTROLLER_USE_EXTENDED_CREDENTIALS = 1, // Place this before USER/PASS + CONTROLLER_HOSTNAME, + CONTROLLER_IP, + CONTROLLER_PORT, + CONTROLLER_USER, + CONTROLLER_PASS, + CONTROLLER_MIN_SEND_INTERVAL, + CONTROLLER_MAX_QUEUE_DEPTH, + CONTROLLER_MAX_RETRIES, + CONTROLLER_FULL_QUEUE_ACTION, + CONTROLLER_ALLOW_EXPIRE, + CONTROLLER_DEDUPLICATE, + CONTROLLER_USE_LOCAL_SYSTEM_TIME, + CONTROLLER_CHECK_REPLY, + CONTROLLER_CLIENT_ID, + CONTROLLER_UNIQUE_CLIENT_ID_RECONNECT, + CONTROLLER_RETAINFLAG, + CONTROLLER_SUBSCRIBE, + CONTROLLER_PUBLISH, + CONTROLLER_LWT_TOPIC, + CONTROLLER_LWT_CONNECT_MESSAGE, + CONTROLLER_LWT_DISCONNECT_MESSAGE, + CONTROLLER_SEND_LWT, + CONTROLLER_WILL_RETAIN, + CONTROLLER_CLEAN_SESSION, + CONTROLLER_TIMEOUT, + CONTROLLER_SAMPLE_SET_INITIATOR, + CONTROLLER_SEND_BINARY, + + // Keep this as last, is used to loop over all parameters + CONTROLLER_ENABLED + }; + + + ControllerSettingsStruct(); + + void reset(); + + bool isSet() const; + + void validate(); + + ChecksumType computeChecksum() const { + return ChecksumType(reinterpret_cast(this), sizeof(ControllerSettingsStruct)); + } + + IPAddress getIP() const { + return IPAddress(IP[0], IP[1], IP[2], IP[3]); + } + + String getHost() const; + + void setHostname(const String& controllerhostname); + + bool checkHostReachable(bool quick); + + #if FEATURE_HTTP_CLIENT + bool connectToHost(WiFiClient& client); + #endif // FEATURE_HTTP_CLIENT + + bool beginPacket(WiFiUDP& client); + + String getHostPortString() const; + + // VariousBits1 defaults to 0, keep in mind when adding bit lookups. + bool mqtt_cleanSession() const { return VariousBits1.mqtt_cleanSession; } + void mqtt_cleanSession(bool value) { VariousBits1.mqtt_cleanSession = value; } + + bool mqtt_sendLWT() const { return !VariousBits1.mqtt_not_sendLWT; } + void mqtt_sendLWT(bool value) { VariousBits1.mqtt_not_sendLWT = !value; } + + bool mqtt_willRetain() const { return !VariousBits1.mqtt_not_willRetain; } + void mqtt_willRetain(bool value) { VariousBits1.mqtt_not_willRetain = !value; } + + bool mqtt_uniqueMQTTclientIdReconnect() const { return VariousBits1.mqtt_uniqueMQTTclientIdReconnect; } + void mqtt_uniqueMQTTclientIdReconnect(bool value) { VariousBits1.mqtt_uniqueMQTTclientIdReconnect = value; } + + bool mqtt_retainFlag() const { return VariousBits1.mqtt_retainFlag; } + void mqtt_retainFlag(bool value) { VariousBits1.mqtt_retainFlag = value; } + + bool useExtendedCredentials() const { return VariousBits1.useExtendedCredentials; } + void useExtendedCredentials(bool value) { VariousBits1.useExtendedCredentials = value; } + + bool sendBinary() const { return VariousBits1.sendBinary; } + void sendBinary(bool value) { VariousBits1.sendBinary = value; } + + bool allowExpire() const { return VariousBits1.allowExpire; } + void allowExpire(bool value) { VariousBits1.allowExpire = value; } + + bool deduplicate() const { return VariousBits1.deduplicate; } + void deduplicate(bool value) { VariousBits1.deduplicate = value; } + + bool useLocalSystemTime() const { return VariousBits1.useLocalSystemTime; } + void useLocalSystemTime(bool value) { VariousBits1.useLocalSystemTime = value; } + + bool UseDNS; + uint8_t IP[4]; + unsigned int Port; + char HostName[65]; + char Publish[129]; + char Subscribe[129]; + char MQTTLwtTopic[129]; + char LWTMessageConnect[129]; + char LWTMessageDisconnect[129]; + unsigned int MinimalTimeBetweenMessages; + unsigned int MaxQueueDepth; + unsigned int MaxRetry; + bool DeleteOldest; // Action to perform when buffer full, delete oldest, or ignore newest. + unsigned int ClientTimeout; + bool MustCheckReply; // When set to false, a sent message is considered always successful. + taskIndex_t SampleSetInitiator; // The first task to start a sample set. + + struct { + uint32_t unused_00 : 1; // Bit 00 + uint32_t mqtt_cleanSession : 1; // Bit 01 + uint32_t mqtt_not_sendLWT : 1; // Bit 02, !value, default enabled + uint32_t mqtt_not_willRetain : 1; // Bit 03, !value, default enabled + uint32_t mqtt_uniqueMQTTclientIdReconnect : 1; // Bit 04 + uint32_t mqtt_retainFlag : 1; // Bit 05 + uint32_t useExtendedCredentials : 1; // Bit 06 + uint32_t sendBinary : 1; // Bit 07 + uint32_t unused_08 : 1; // Bit 08 + uint32_t allowExpire : 1; // Bit 09 + uint32_t deduplicate : 1; // Bit 10 + uint32_t useLocalSystemTime : 1; // Bit 11 + uint32_t unused_12 : 1; // Bit 12 + uint32_t unused_13 : 1; // Bit 13 + uint32_t unused_14 : 1; // Bit 14 + uint32_t unused_15 : 1; // Bit 15 + uint32_t unused_16 : 1; // Bit 16 + uint32_t unused_17 : 1; // Bit 17 + uint32_t unused_18 : 1; // Bit 18 + uint32_t unused_19 : 1; // Bit 19 + uint32_t unused_20 : 1; // Bit 20 + uint32_t unused_21 : 1; // Bit 21 + uint32_t unused_22 : 1; // Bit 22 + uint32_t unused_23 : 1; // Bit 23 + uint32_t unused_24 : 1; // Bit 24 + uint32_t unused_25 : 1; // Bit 25 + uint32_t unused_26 : 1; // Bit 26 + uint32_t unused_27 : 1; // Bit 27 + uint32_t unused_28 : 1; // Bit 28 + uint32_t unused_29 : 1; // Bit 29 + uint32_t unused_30 : 1; // Bit 30 + uint32_t unused_31 : 1; // Bit 31 + } VariousBits1; + char ClientID[65]; // Used to define the Client ID used by the controller + +private: + + bool ipSet() const; + + bool updateIPcache(); +}; + +#include "../Helpers/Memory.h" + +typedef std::shared_ptr ControllerSettingsStruct_ptr_type; +/* +# ifdef USE_SECOND_HEAP +#define MakeControllerSettings(T) HeapSelectIram ephemeral; ControllerSettingsStruct_ptr_type T(new (std::nothrow) ControllerSettingsStruct()); +#else +*/ +#define MakeControllerSettings(T) void * calloc_ptr = special_calloc(1,sizeof(ControllerSettingsStruct)); ControllerSettingsStruct_ptr_type T(new (calloc_ptr) ControllerSettingsStruct()); +//#endif + +// Check to see if MakeControllerSettings was successful +#define AllocatedControllerSettings() (ControllerSettings.get() != nullptr) + +#endif // DATASTRUCTS_CONTROLLERSETTINGSSTRUCT_H diff --git a/src/src/ESPEasyCore/Controller.cpp b/src/src/ESPEasyCore/Controller.cpp index 063b0cd8b2..cd68f36485 100644 --- a/src/src/ESPEasyCore/Controller.cpp +++ b/src/src/ESPEasyCore/Controller.cpp @@ -1,641 +1,641 @@ -#include "../ESPEasyCore/Controller.h" - -#include "../../ESPEasy_common.h" -#include "../../ESPEasy-Globals.h" - -#include "../../_Plugin_Helper.h" - -#include "../ControllerQueue/MQTT_queue_element.h" - -#include "../DataStructs/ControllerSettingsStruct.h" -#include "../DataStructs/ESPEasy_EventStruct.h" - -#include "../DataTypes/ESPEasy_plugin_functions.h" -#include "../DataTypes/SPI_options.h" - -#include "../ESPEasyCore/ESPEasyRules.h" -#include "../ESPEasyCore/Serial.h" - -#include "../Globals/CPlugins.h" -#include "../Globals/Device.h" -#include "../Globals/ESPEasyWiFiEvent.h" -#include "../Globals/ESPEasy_Scheduler.h" -#include "../Globals/MQTT.h" -#include "../Globals/Plugins.h" -#include "../Globals/RulesCalculate.h" - -#include "../Helpers/_CPlugin_Helper.h" -#include "../Helpers/Misc.h" -#include "../Helpers/Network.h" -#include "../Helpers/PeriodicalActions.h" -#include "../Helpers/PortStatus.h" - - -constexpr pluginID_t PLUGIN_ID_MQTT_IMPORT(37); - -// ******************************************************************************** -// Interface for Sending to Controllers -// ******************************************************************************** -void sendData(struct EventStruct *event, bool sendEvents) -{ - START_TIMER; - #ifndef BUILD_NO_RAM_TRACKER - checkRAM(F("sendData")); - #endif // ifndef BUILD_NO_RAM_TRACKER -// LoadTaskSettings(event->TaskIndex); - - if (Settings.UseRules && sendEvents) { - createRuleEvents(event); - } - - if (Settings.UseValueLogger && (Settings.InitSPI > static_cast(SPI_Options_e::None)) && (Settings.Pin_sd_cs >= 0)) { - SendValueLogger(event->TaskIndex); - } - -// LoadTaskSettings(event->TaskIndex); // could have changed during background tasks. - - for (controllerIndex_t x = 0; x < CONTROLLER_MAX; x++) - { - event->ControllerIndex = x; - event->idx = Settings.TaskDeviceID[x][event->TaskIndex]; - - if (Settings.TaskDeviceSendData[event->ControllerIndex][event->TaskIndex] && - Settings.ControllerEnabled[event->ControllerIndex] && - Settings.Protocol[event->ControllerIndex]) - { - protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(event->ControllerIndex); - - if (validUserVar(event)) { - String dummy; - CPluginCall(ProtocolIndex, CPlugin::Function::CPLUGIN_PROTOCOL_SEND, event, dummy); - } -#ifndef BUILD_NO_DEBUG - else { - if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { - String log = F("Invalid value detected for controller "); - log += getCPluginNameFromProtocolIndex(ProtocolIndex); - addLogMove(LOG_LEVEL_DEBUG, log); - } - } -#endif // ifndef BUILD_NO_DEBUG - } - } - - lastSend = millis(); - STOP_TIMER(SEND_DATA_STATS); -} - -bool validUserVar(struct EventStruct *event) { - if (!validTaskIndex(event->TaskIndex)) return false; - const Sensor_VType vtype = event->getSensorType(); - if (isIntegerOutputDataType(vtype) || - vtype == Sensor_VType::SENSOR_TYPE_STRING) // FIXME TD-er: Must look at length of event->String2 ? - { - return true; - } - const uint8_t valueCount = getValueCountForTask(event->TaskIndex); - - for (int i = 0; i < valueCount; ++i) { - if (!UserVar.isValid(event->TaskIndex, i, vtype)) { - return false; - } - } - return true; -} - -#if FEATURE_MQTT - -/*********************************************************************************************\ -* Handle incoming MQTT messages -\*********************************************************************************************/ - -// handle MQTT messages -void incoming_mqtt_callback(char *c_topic, uint8_t *b_payload, unsigned int length) { - statusLED(true); - controllerIndex_t enabledMqttController = firstEnabledMQTT_ControllerIndex(); - - if (!validControllerIndex(enabledMqttController)) { - addLog(LOG_LEVEL_ERROR, F("MQTT : No enabled MQTT controller")); - return; - } - - if (length > MQTT_MAX_PACKET_SIZE) - { - addLog(LOG_LEVEL_ERROR, F("MQTT : Ignored too big message")); - return; - } - - // TD-er: This one cannot set the TaskIndex, but that may seem to work out.... hopefully. - protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(enabledMqttController); - - Scheduler.schedule_mqtt_controller_event_timer( - ProtocolIndex, - CPlugin::Function::CPLUGIN_PROTOCOL_RECV, - c_topic, b_payload, length); - - deviceIndex_t DeviceIndex = getDeviceIndex(PLUGIN_ID_MQTT_IMPORT); // Check if P037_MQTTimport is present in the build - - if (validDeviceIndex(DeviceIndex)) { - // Here we loop over all tasks and call each 037 plugin with function PLUGIN_MQTT_IMPORT - for (taskIndex_t taskIndex = 0; taskIndex < TASKS_MAX; taskIndex++) - { - if (Settings.TaskDeviceEnabled[taskIndex] && (Settings.getPluginID_for_task(taskIndex) == PLUGIN_ID_MQTT_IMPORT)) - { - Scheduler.schedule_mqtt_plugin_import_event_timer( - DeviceIndex, taskIndex, PLUGIN_MQTT_IMPORT, - c_topic, b_payload, length); - } - } - } -} - -/*********************************************************************************************\ -* Disconnect from MQTT message broker -\*********************************************************************************************/ -void MQTTDisconnect() -{ - if (MQTTclient.connected()) { - MQTTclient.disconnect(); - addLog(LOG_LEVEL_INFO, F("MQTT : Disconnected from broker")); - } - updateMQTTclient_connected(); -} - -/*********************************************************************************************\ -* Connect to MQTT message broker -\*********************************************************************************************/ -bool MQTTConnect(controllerIndex_t controller_idx) -{ - if (MQTTclient_next_connect_attempt.isSet() && !MQTTclient_next_connect_attempt.timeoutReached(timermqtt_interval)) { - return false; - } - MQTTclient_next_connect_attempt.setNow(); - ++mqtt_reconnect_count; - - MakeControllerSettings(ControllerSettings); //-V522 - - if (!AllocatedControllerSettings()) { - addLog(LOG_LEVEL_ERROR, F("MQTT : Cannot connect, out of RAM")); - return false; - } - LoadControllerSettings(controller_idx, *ControllerSettings); - - if (!ControllerSettings->checkHostReachable(true)) { - return false; - } - - if (MQTTclient.connected()) { - MQTTclient.disconnect(); - } - - updateMQTTclient_connected(); - - // mqtt = WiFiClient(); // workaround see: https://github.com/esp8266/Arduino/issues/4497#issuecomment-373023864 - delay(0); - - // Ignoring the ACK from the server is probably set for a reason. - // For example because the server does not give an acknowledgement. - // This way, we always need the set amount of timeout to handle the request. - // Thus we should not make the timeout dynamic here if set to ignore ack. - const uint32_t timeout = ControllerSettings->MustCheckReply - ? WiFiEventData.getSuggestedTimeout(Settings.Protocol[controller_idx], ControllerSettings->ClientTimeout) - : ControllerSettings->ClientTimeout; - - #ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS - // See: https://github.com/espressif/arduino-esp32/pull/6676 - mqtt.setTimeout((timeout + 500) / 1000); // in seconds!!!! - Client *pClient = &mqtt; - pClient->setTimeout(timeout); - #else - mqtt.setTimeout(timeout); // in msec as it should be! - #endif - - MQTTclient.setClient(mqtt); - - if (ControllerSettings->UseDNS) { - MQTTclient.setServer(ControllerSettings->getHost().c_str(), ControllerSettings->Port); - } else { - MQTTclient.setServer(ControllerSettings->getIP(), ControllerSettings->Port); - } - MQTTclient.setCallback(incoming_mqtt_callback); - - // MQTT needs a unique clientname to subscribe to broker - const String clientid = getMQTTclientID(*ControllerSettings); - - const String LWTTopic = getLWT_topic(*ControllerSettings); - const String LWTMessageDisconnect = getLWT_messageDisconnect(*ControllerSettings); - bool MQTTresult = false; - const uint8_t willQos = 0; - const bool willRetain = ControllerSettings->mqtt_willRetain() && ControllerSettings->mqtt_sendLWT(); - const bool cleanSession = ControllerSettings->mqtt_cleanSession(); // As suggested here: - - if (MQTTclient_should_reconnect) { - addLog(LOG_LEVEL_ERROR, F("MQTT : Intentional reconnect")); - } - - const unsigned long connect_start_time = millis(); - - // https://github.com/knolleary/pubsubclient/issues/458#issuecomment-493875150 - if (hasControllerCredentialsSet(controller_idx, *ControllerSettings)) { - MQTTresult = - MQTTclient.connect(clientid.c_str(), - getControllerUser(controller_idx, *ControllerSettings).c_str(), - getControllerPass(controller_idx, *ControllerSettings).c_str(), - ControllerSettings->mqtt_sendLWT() ? LWTTopic.c_str() : nullptr, - willQos, - willRetain, - ControllerSettings->mqtt_sendLWT() ? LWTMessageDisconnect.c_str() : nullptr, - cleanSession); - } else { - MQTTresult = MQTTclient.connect(clientid.c_str(), - nullptr, - nullptr, - ControllerSettings->mqtt_sendLWT() ? LWTTopic.c_str() : nullptr, - willQos, - willRetain, - ControllerSettings->mqtt_sendLWT() ? LWTMessageDisconnect.c_str() : nullptr, - cleanSession); - } - delay(0); - - count_connection_results(MQTTresult, F("MQTT : Broker "), Settings.Protocol[controller_idx], connect_start_time); - - if (!MQTTresult) { - MQTTclient.disconnect(); - updateMQTTclient_connected(); - - return false; - } - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log; - log += F("MQTT : Connected to broker with client ID: "); - log += clientid; - addLogMove(LOG_LEVEL_INFO, log); - } - String subscribeTo = ControllerSettings->Subscribe; - - parseSystemVariables(subscribeTo, false); - MQTTclient.subscribe(subscribeTo.c_str()); - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log = F("Subscribed to: "); - log += subscribeTo; - addLogMove(LOG_LEVEL_INFO, log); - } - - updateMQTTclient_connected(); - statusLED(true); - mqtt_reconnect_count = 0; - - // call all installed controller to publish autodiscover data - if (MQTTclient_should_reconnect) { CPluginCall(CPlugin::Function::CPLUGIN_GOT_CONNECTED, 0); } - MQTTclient_should_reconnect = false; - - if (ControllerSettings->mqtt_sendLWT()) { - String LWTMessageConnect = getLWT_messageConnect(*ControllerSettings); - - if (!MQTTclient.publish(LWTTopic.c_str(), LWTMessageConnect.c_str(), willRetain)) { - MQTTclient_must_send_LWT_connected = true; - } - } - - return true; -} - -String getMQTTclientID(const ControllerSettingsStruct& ControllerSettings) { - String clientid = ControllerSettings.ClientID; - - if (clientid.isEmpty()) { - // Try to generate some default - clientid = F(CONTROLLER_DEFAULT_CLIENTID); - } - parseSystemVariables(clientid, false); - clientid.replace(' ', '_'); // Make sure no spaces are present in the client ID - - if ((WiFiEventData.wifi_reconnects >= 1) && ControllerSettings.mqtt_uniqueMQTTclientIdReconnect()) { - // Work-around for 'lost connections' to the MQTT broker. - // If the broker thinks the connection is still alive, a reconnect from the - // client will be refused. - // To overcome this issue, append the number of reconnects to the client ID to - // make it different from the previous one. - clientid += '_'; - clientid += WiFiEventData.wifi_reconnects; - } - return clientid; -} - -/*********************************************************************************************\ -* Check connection MQTT message broker -\*********************************************************************************************/ -bool MQTTCheck(controllerIndex_t controller_idx) -{ - if (!NetworkConnected(10)) { - return false; - } - protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(controller_idx); - - if (!validProtocolIndex(ProtocolIndex)) { - return false; - } - - if (getProtocolStruct(ProtocolIndex).usesMQTT) - { - bool mqtt_sendLWT = false; - String LWTTopic, LWTMessageConnect; - bool willRetain = false; - { - MakeControllerSettings(ControllerSettings); //-V522 - - if (!AllocatedControllerSettings()) { - addLog(LOG_LEVEL_ERROR, F("MQTT : Cannot check, out of RAM")); - return false; - } - - LoadControllerSettings(controller_idx, *ControllerSettings); - - // FIXME TD-er: Is this still needed? - - /* - #ifdef USES_ESPEASY_NOW - if (!MQTTclient.connected()) { - if (ControllerSettings->enableESPEasyNowFallback()) { - return true; - } - } - #endif - */ - - if (!ControllerSettings->isSet()) { - return true; - } - - if (ControllerSettings->mqtt_sendLWT()) { - mqtt_sendLWT = true; - LWTTopic = getLWT_topic(*ControllerSettings); - LWTMessageConnect = getLWT_messageConnect(*ControllerSettings); - willRetain = ControllerSettings->mqtt_willRetain(); - } - } - - if (MQTTclient_should_reconnect || !MQTTclient.connected()) - { - return MQTTConnect(controller_idx); - } - - if (MQTTclient_must_send_LWT_connected) { - if (mqtt_sendLWT) { - if (MQTTclient.publish(LWTTopic.c_str(), LWTMessageConnect.c_str(), willRetain)) { - MQTTclient_must_send_LWT_connected = false; - } - } else { - MQTTclient_must_send_LWT_connected = false; - } - } - } - - // When no MQTT protocol is enabled, all is fine. - return true; -} - -String getLWT_topic(const ControllerSettingsStruct& ControllerSettings) { - String LWTTopic; - - if (ControllerSettings.mqtt_sendLWT()) { - LWTTopic = ControllerSettings.MQTTLwtTopic; - - if (LWTTopic.isEmpty()) - { - LWTTopic = ControllerSettings.Subscribe; - LWTTopic += F("/LWT"); - } - LWTTopic.replace(F("/#"), F("/status")); - parseSystemVariables(LWTTopic, false); - } - return LWTTopic; -} - -String getLWT_messageConnect(const ControllerSettingsStruct& ControllerSettings) { - String LWTMessageConnect; - - if (ControllerSettings.mqtt_sendLWT()) { - LWTMessageConnect = ControllerSettings.LWTMessageConnect; - - if (LWTMessageConnect.isEmpty()) { - LWTMessageConnect = F(DEFAULT_MQTT_LWT_CONNECT_MESSAGE); - } - parseSystemVariables(LWTMessageConnect, false); - } - return LWTMessageConnect; -} - -String getLWT_messageDisconnect(const ControllerSettingsStruct& ControllerSettings) { - String LWTMessageDisconnect; - - if (ControllerSettings.mqtt_sendLWT()) { - LWTMessageDisconnect = ControllerSettings.LWTMessageDisconnect; - - if (LWTMessageDisconnect.isEmpty()) { - LWTMessageDisconnect = F(DEFAULT_MQTT_LWT_DISCONNECT_MESSAGE); - } - parseSystemVariables(LWTMessageDisconnect, false); - } - return LWTMessageDisconnect; -} - -#endif // if FEATURE_MQTT - -/*********************************************************************************************\ -* Send status info to request source -\*********************************************************************************************/ -void SendStatusOnlyIfNeeded(struct EventStruct *event, bool param1, uint32_t key, const String& param2, int16_t param3) { - if (SourceNeedsStatusUpdate(event->Source)) { - SendStatus(event, getPinStateJSON(param1, key, param2, param3)); - printToWeb = false; // SP: 2020-06-12: to avoid to add more info to a JSON structure - } -} - -bool SourceNeedsStatusUpdate(EventValueSource::Enum eventSource) -{ - switch (eventSource) { - case EventValueSource::Enum::VALUE_SOURCE_HTTP: - case EventValueSource::Enum::VALUE_SOURCE_SERIAL: - case EventValueSource::Enum::VALUE_SOURCE_MQTT: - case EventValueSource::Enum::VALUE_SOURCE_WEB_FRONTEND: - return true; - - default: - break; - } - return false; -} - -void SendStatus(struct EventStruct *event, const __FlashStringHelper * status) -{ - SendStatus(event, String(status)); -} - -void SendStatus(struct EventStruct *event, const String& status) -{ - if (status.isEmpty()) { return; } - - switch (event->Source) - { - case EventValueSource::Enum::VALUE_SOURCE_HTTP: - case EventValueSource::Enum::VALUE_SOURCE_WEB_FRONTEND: - - if (printToWeb) { - printWebString += status; - } - break; -#if FEATURE_MQTT - case EventValueSource::Enum::VALUE_SOURCE_MQTT: - MQTTStatus(event, status); - break; -#endif // if FEATURE_MQTT - case EventValueSource::Enum::VALUE_SOURCE_SERIAL: - serialPrintln(status); - break; - - default: - break; - } -} - -#if FEATURE_MQTT -controllerIndex_t firstEnabledMQTT_ControllerIndex() { - for (controllerIndex_t i = 0; i < CONTROLLER_MAX; ++i) { - protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(i); - if (validProtocolIndex(ProtocolIndex)) { - if (getProtocolStruct(ProtocolIndex).usesMQTT && Settings.ControllerEnabled[i]) { - return i; - } - } - } - return INVALID_CONTROLLER_INDEX; -} - -bool MQTT_queueFull(controllerIndex_t controller_idx) { - if (MQTTDelayHandler == nullptr) { - return true; - } - - if (MQTTDelayHandler->queueFull(controller_idx)) { - // The queue is full, try to make some room first. - processMQTTdelayQueue(); - return MQTTDelayHandler->queueFull(controller_idx); - } - return false; -} - -bool MQTTpublish(controllerIndex_t controller_idx, taskIndex_t taskIndex, const char *topic, const char *payload, bool retained, bool callbackTask) -{ - if (MQTTDelayHandler == nullptr) { - return false; - } - - if (MQTT_queueFull(controller_idx)) { - return false; - } - const bool success = MQTTDelayHandler->addToQueue(std::unique_ptr(new MQTT_queue_element(controller_idx, taskIndex, topic, payload, retained, callbackTask))); - - scheduleNextMQTTdelayQueue(); - return success; -} - -bool MQTTpublish(controllerIndex_t controller_idx, taskIndex_t taskIndex, String&& topic, String&& payload, bool retained, bool callbackTask) { - if (MQTTDelayHandler == nullptr) { - return false; - } - - if (MQTT_queueFull(controller_idx)) { - return false; - } - - const bool success = MQTTDelayHandler->addToQueue(std::unique_ptr(new MQTT_queue_element(controller_idx, taskIndex, std::move(topic), std::move(payload), retained, callbackTask))); - - scheduleNextMQTTdelayQueue(); - return success; -} - -/*********************************************************************************************\ -* Send status info back to channel where request came from -\*********************************************************************************************/ -void MQTTStatus(struct EventStruct *event, const String& status) -{ - controllerIndex_t enabledMqttController = firstEnabledMQTT_ControllerIndex(); - - if (validControllerIndex(enabledMqttController)) { - controllerIndex_t DomoticzMQTT_controllerIndex = findFirstEnabledControllerWithId(2); - - if (DomoticzMQTT_controllerIndex == enabledMqttController) { - // Do not send MQTT status updates to Domoticz - return; - } - String pubname; - bool mqtt_retainFlag; - { - // Place the ControllerSettings in a scope to free the memory as soon as we got all relevant information. - MakeControllerSettings(ControllerSettings); //-V522 - - if (!AllocatedControllerSettings()) { - addLog(LOG_LEVEL_ERROR, F("MQTT : Cannot send status, out of RAM")); - return; - } - - LoadControllerSettings(enabledMqttController, *ControllerSettings); - pubname = ControllerSettings->Publish; - mqtt_retainFlag = ControllerSettings->mqtt_retainFlag(); - } - - // FIXME TD-er: Why check for "/#" suffix on a publish topic? - // It makes no sense to have a subscribe wildcard on a publish topic. - pubname.replace(F("/#"), F("/status")); - - parseSingleControllerVariable(pubname, event, 0, false); - parseControllerVariables(pubname, event, false); - - - if (!pubname.endsWith(F("/status"))) { - pubname += F("/status"); - } - - MQTTpublish(enabledMqttController, event->TaskIndex, pubname.c_str(), status.c_str(), mqtt_retainFlag); - } -} - -#endif // if FEATURE_MQTT - - -/*********************************************************************************************\ -* send specific sensor task data, effectively calling PluginCall(PLUGIN_READ...) -\*********************************************************************************************/ -void SensorSendTask(struct EventStruct *event, unsigned long timestampUnixTime) -{ - SensorSendTask(event, timestampUnixTime, millis()); -} - -void SensorSendTask(struct EventStruct *event, unsigned long timestampUnixTime, unsigned long lasttimer) -{ - if (!validTaskIndex(event->TaskIndex)) { return; } - Scheduler.reschedule_task_device_timer(event->TaskIndex, lasttimer); - - #ifndef BUILD_NO_RAM_TRACKER - checkRAM(F("SensorSendTask")); - #endif // ifndef BUILD_NO_RAM_TRACKER - - if (Settings.TaskDeviceEnabled[event->TaskIndex]) - { - const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex); - - if (!validDeviceIndex(DeviceIndex)) { return; } - - struct EventStruct TempEvent(event->TaskIndex); - TempEvent.Source = event->Source; - TempEvent.timestamp = timestampUnixTime; - checkDeviceVTypeForTask(&TempEvent); - - String dummy; - if (PluginCall(PLUGIN_READ, &TempEvent, dummy)) { - sendData(&TempEvent); - } - } -} +#include "../ESPEasyCore/Controller.h" + +#include "../../ESPEasy_common.h" +#include "../../ESPEasy-Globals.h" + +#include "../../_Plugin_Helper.h" + +#include "../ControllerQueue/MQTT_queue_element.h" + +#include "../DataStructs/ControllerSettingsStruct.h" +#include "../DataStructs/ESPEasy_EventStruct.h" + +#include "../DataTypes/ESPEasy_plugin_functions.h" +#include "../DataTypes/SPI_options.h" + +#include "../ESPEasyCore/ESPEasyRules.h" +#include "../ESPEasyCore/Serial.h" + +#include "../Globals/CPlugins.h" +#include "../Globals/Device.h" +#include "../Globals/ESPEasyWiFiEvent.h" +#include "../Globals/ESPEasy_Scheduler.h" +#include "../Globals/MQTT.h" +#include "../Globals/Plugins.h" +#include "../Globals/RulesCalculate.h" + +#include "../Helpers/_CPlugin_Helper.h" +#include "../Helpers/Misc.h" +#include "../Helpers/Network.h" +#include "../Helpers/PeriodicalActions.h" +#include "../Helpers/PortStatus.h" + + +constexpr pluginID_t PLUGIN_ID_MQTT_IMPORT(37); + +// ******************************************************************************** +// Interface for Sending to Controllers +// ******************************************************************************** +void sendData(struct EventStruct *event, bool sendEvents) +{ + START_TIMER; + #ifndef BUILD_NO_RAM_TRACKER + checkRAM(F("sendData")); + #endif // ifndef BUILD_NO_RAM_TRACKER +// LoadTaskSettings(event->TaskIndex); + + if (Settings.UseRules && sendEvents) { + createRuleEvents(event); + } + + if (Settings.UseValueLogger && (Settings.InitSPI > static_cast(SPI_Options_e::None)) && (Settings.Pin_sd_cs >= 0)) { + SendValueLogger(event->TaskIndex); + } + +// LoadTaskSettings(event->TaskIndex); // could have changed during background tasks. + + for (controllerIndex_t x = 0; x < CONTROLLER_MAX; x++) + { + event->ControllerIndex = x; + event->idx = Settings.TaskDeviceID[x][event->TaskIndex]; + + if (Settings.TaskDeviceSendData[event->ControllerIndex][event->TaskIndex] && + Settings.ControllerEnabled[event->ControllerIndex] && + Settings.Protocol[event->ControllerIndex]) + { + protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(event->ControllerIndex); + + if (validUserVar(event)) { + String dummy; + CPluginCall(ProtocolIndex, CPlugin::Function::CPLUGIN_PROTOCOL_SEND, event, dummy); + } +#ifndef BUILD_NO_DEBUG + else { + if (loglevelActiveFor(LOG_LEVEL_DEBUG)) { + String log = F("Invalid value detected for controller "); + log += getCPluginNameFromProtocolIndex(ProtocolIndex); + addLogMove(LOG_LEVEL_DEBUG, log); + } + } +#endif // ifndef BUILD_NO_DEBUG + } + } + + lastSend = millis(); + STOP_TIMER(SEND_DATA_STATS); +} + +bool validUserVar(struct EventStruct *event) { + if (!validTaskIndex(event->TaskIndex)) return false; + const Sensor_VType vtype = event->getSensorType(); + if (isIntegerOutputDataType(vtype) || + vtype == Sensor_VType::SENSOR_TYPE_STRING) // FIXME TD-er: Must look at length of event->String2 ? + { + return true; + } + const uint8_t valueCount = getValueCountForTask(event->TaskIndex); + + for (int i = 0; i < valueCount; ++i) { + if (!UserVar.isValid(event->TaskIndex, i, vtype)) { + return false; + } + } + return true; +} + +#if FEATURE_MQTT + +/*********************************************************************************************\ +* Handle incoming MQTT messages +\*********************************************************************************************/ + +// handle MQTT messages +void incoming_mqtt_callback(char *c_topic, uint8_t *b_payload, unsigned int length) { + statusLED(true); + controllerIndex_t enabledMqttController = firstEnabledMQTT_ControllerIndex(); + + if (!validControllerIndex(enabledMqttController)) { + addLog(LOG_LEVEL_ERROR, F("MQTT : No enabled MQTT controller")); + return; + } + + if (length > MQTT_MAX_PACKET_SIZE) + { + addLog(LOG_LEVEL_ERROR, F("MQTT : Ignored too big message")); + return; + } + + // TD-er: This one cannot set the TaskIndex, but that may seem to work out.... hopefully. + protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(enabledMqttController); + + Scheduler.schedule_mqtt_controller_event_timer( + ProtocolIndex, + CPlugin::Function::CPLUGIN_PROTOCOL_RECV, + c_topic, b_payload, length); + + deviceIndex_t DeviceIndex = getDeviceIndex(PLUGIN_ID_MQTT_IMPORT); // Check if P037_MQTTimport is present in the build + + if (validDeviceIndex(DeviceIndex)) { + // Here we loop over all tasks and call each 037 plugin with function PLUGIN_MQTT_IMPORT + for (taskIndex_t taskIndex = 0; taskIndex < TASKS_MAX; taskIndex++) + { + if (Settings.TaskDeviceEnabled[taskIndex] && (Settings.getPluginID_for_task(taskIndex) == PLUGIN_ID_MQTT_IMPORT)) + { + Scheduler.schedule_mqtt_plugin_import_event_timer( + DeviceIndex, taskIndex, PLUGIN_MQTT_IMPORT, + c_topic, b_payload, length); + } + } + } +} + +/*********************************************************************************************\ +* Disconnect from MQTT message broker +\*********************************************************************************************/ +void MQTTDisconnect() +{ + if (MQTTclient.connected()) { + MQTTclient.disconnect(); + addLog(LOG_LEVEL_INFO, F("MQTT : Disconnected from broker")); + } + updateMQTTclient_connected(); +} + +/*********************************************************************************************\ +* Connect to MQTT message broker +\*********************************************************************************************/ +bool MQTTConnect(controllerIndex_t controller_idx) +{ + if (MQTTclient_next_connect_attempt.isSet() && !MQTTclient_next_connect_attempt.timeoutReached(timermqtt_interval)) { + return false; + } + MQTTclient_next_connect_attempt.setNow(); + ++mqtt_reconnect_count; + + MakeControllerSettings(ControllerSettings); //-V522 + + if (!AllocatedControllerSettings()) { + addLog(LOG_LEVEL_ERROR, F("MQTT : Cannot connect, out of RAM")); + return false; + } + LoadControllerSettings(controller_idx, *ControllerSettings); + + if (!ControllerSettings->checkHostReachable(true)) { + return false; + } + + if (MQTTclient.connected()) { + MQTTclient.disconnect(); + } + + updateMQTTclient_connected(); + + // mqtt = WiFiClient(); // workaround see: https://github.com/esp8266/Arduino/issues/4497#issuecomment-373023864 + delay(0); + + // Ignoring the ACK from the server is probably set for a reason. + // For example because the server does not give an acknowledgement. + // This way, we always need the set amount of timeout to handle the request. + // Thus we should not make the timeout dynamic here if set to ignore ack. + const uint32_t timeout = ControllerSettings->MustCheckReply + ? WiFiEventData.getSuggestedTimeout(Settings.Protocol[controller_idx], ControllerSettings->ClientTimeout) + : ControllerSettings->ClientTimeout; + + #ifdef MUSTFIX_CLIENT_TIMEOUT_IN_SECONDS + // See: https://github.com/espressif/arduino-esp32/pull/6676 + mqtt.setTimeout((timeout + 500) / 1000); // in seconds!!!! + Client *pClient = &mqtt; + pClient->setTimeout(timeout); + #else + mqtt.setTimeout(timeout); // in msec as it should be! + #endif + + MQTTclient.setClient(mqtt); + + if (ControllerSettings->UseDNS) { + MQTTclient.setServer(ControllerSettings->getHost().c_str(), ControllerSettings->Port); + } else { + MQTTclient.setServer(ControllerSettings->getIP(), ControllerSettings->Port); + } + MQTTclient.setCallback(incoming_mqtt_callback); + + // MQTT needs a unique clientname to subscribe to broker + const String clientid = getMQTTclientID(*ControllerSettings); + + const String LWTTopic = getLWT_topic(*ControllerSettings); + const String LWTMessageDisconnect = getLWT_messageDisconnect(*ControllerSettings); + bool MQTTresult = false; + const uint8_t willQos = 0; + const bool willRetain = ControllerSettings->mqtt_willRetain() && ControllerSettings->mqtt_sendLWT(); + const bool cleanSession = ControllerSettings->mqtt_cleanSession(); // As suggested here: + + if (MQTTclient_should_reconnect) { + addLog(LOG_LEVEL_ERROR, F("MQTT : Intentional reconnect")); + } + + const unsigned long connect_start_time = millis(); + + // https://github.com/knolleary/pubsubclient/issues/458#issuecomment-493875150 + if (hasControllerCredentialsSet(controller_idx, *ControllerSettings)) { + MQTTresult = + MQTTclient.connect(clientid.c_str(), + getControllerUser(controller_idx, *ControllerSettings).c_str(), + getControllerPass(controller_idx, *ControllerSettings).c_str(), + ControllerSettings->mqtt_sendLWT() ? LWTTopic.c_str() : nullptr, + willQos, + willRetain, + ControllerSettings->mqtt_sendLWT() ? LWTMessageDisconnect.c_str() : nullptr, + cleanSession); + } else { + MQTTresult = MQTTclient.connect(clientid.c_str(), + nullptr, + nullptr, + ControllerSettings->mqtt_sendLWT() ? LWTTopic.c_str() : nullptr, + willQos, + willRetain, + ControllerSettings->mqtt_sendLWT() ? LWTMessageDisconnect.c_str() : nullptr, + cleanSession); + } + delay(0); + + count_connection_results(MQTTresult, F("MQTT : Broker "), Settings.Protocol[controller_idx], connect_start_time); + + if (!MQTTresult) { + MQTTclient.disconnect(); + updateMQTTclient_connected(); + + return false; + } + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log; + log += F("MQTT : Connected to broker with client ID: "); + log += clientid; + addLogMove(LOG_LEVEL_INFO, log); + } + String subscribeTo = ControllerSettings->Subscribe; + + parseSystemVariables(subscribeTo, false); + MQTTclient.subscribe(subscribeTo.c_str()); + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log = F("Subscribed to: "); + log += subscribeTo; + addLogMove(LOG_LEVEL_INFO, log); + } + + updateMQTTclient_connected(); + statusLED(true); + mqtt_reconnect_count = 0; + + // call all installed controller to publish autodiscover data + if (MQTTclient_should_reconnect) { CPluginCall(CPlugin::Function::CPLUGIN_GOT_CONNECTED, 0); } + MQTTclient_should_reconnect = false; + + if (ControllerSettings->mqtt_sendLWT()) { + String LWTMessageConnect = getLWT_messageConnect(*ControllerSettings); + + if (!MQTTclient.publish(LWTTopic.c_str(), LWTMessageConnect.c_str(), willRetain)) { + MQTTclient_must_send_LWT_connected = true; + } + } + + return true; +} + +String getMQTTclientID(const ControllerSettingsStruct& ControllerSettings) { + String clientid = ControllerSettings.ClientID; + + if (clientid.isEmpty()) { + // Try to generate some default + clientid = F(CONTROLLER_DEFAULT_CLIENTID); + } + parseSystemVariables(clientid, false); + clientid.replace(' ', '_'); // Make sure no spaces are present in the client ID + + if ((WiFiEventData.wifi_reconnects >= 1) && ControllerSettings.mqtt_uniqueMQTTclientIdReconnect()) { + // Work-around for 'lost connections' to the MQTT broker. + // If the broker thinks the connection is still alive, a reconnect from the + // client will be refused. + // To overcome this issue, append the number of reconnects to the client ID to + // make it different from the previous one. + clientid += '_'; + clientid += WiFiEventData.wifi_reconnects; + } + return clientid; +} + +/*********************************************************************************************\ +* Check connection MQTT message broker +\*********************************************************************************************/ +bool MQTTCheck(controllerIndex_t controller_idx) +{ + if (!NetworkConnected(10)) { + return false; + } + protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(controller_idx); + + if (!validProtocolIndex(ProtocolIndex)) { + return false; + } + + if (getProtocolStruct(ProtocolIndex).usesMQTT) + { + bool mqtt_sendLWT = false; + String LWTTopic, LWTMessageConnect; + bool willRetain = false; + { + MakeControllerSettings(ControllerSettings); //-V522 + + if (!AllocatedControllerSettings()) { + addLog(LOG_LEVEL_ERROR, F("MQTT : Cannot check, out of RAM")); + return false; + } + + LoadControllerSettings(controller_idx, *ControllerSettings); + + // FIXME TD-er: Is this still needed? + + /* + #ifdef USES_ESPEASY_NOW + if (!MQTTclient.connected()) { + if (ControllerSettings->enableESPEasyNowFallback()) { + return true; + } + } + #endif + */ + + if (!ControllerSettings->isSet()) { + return true; + } + + if (ControllerSettings->mqtt_sendLWT()) { + mqtt_sendLWT = true; + LWTTopic = getLWT_topic(*ControllerSettings); + LWTMessageConnect = getLWT_messageConnect(*ControllerSettings); + willRetain = ControllerSettings->mqtt_willRetain(); + } + } + + if (MQTTclient_should_reconnect || !MQTTclient.connected()) + { + return MQTTConnect(controller_idx); + } + + if (MQTTclient_must_send_LWT_connected) { + if (mqtt_sendLWT) { + if (MQTTclient.publish(LWTTopic.c_str(), LWTMessageConnect.c_str(), willRetain)) { + MQTTclient_must_send_LWT_connected = false; + } + } else { + MQTTclient_must_send_LWT_connected = false; + } + } + } + + // When no MQTT protocol is enabled, all is fine. + return true; +} + +String getLWT_topic(const ControllerSettingsStruct& ControllerSettings) { + String LWTTopic; + + if (ControllerSettings.mqtt_sendLWT()) { + LWTTopic = ControllerSettings.MQTTLwtTopic; + + if (LWTTopic.isEmpty()) + { + LWTTopic = ControllerSettings.Subscribe; + LWTTopic += F("/LWT"); + } + LWTTopic.replace(F("/#"), F("/status")); + parseSystemVariables(LWTTopic, false); + } + return LWTTopic; +} + +String getLWT_messageConnect(const ControllerSettingsStruct& ControllerSettings) { + String LWTMessageConnect; + + if (ControllerSettings.mqtt_sendLWT()) { + LWTMessageConnect = ControllerSettings.LWTMessageConnect; + + if (LWTMessageConnect.isEmpty()) { + LWTMessageConnect = F(DEFAULT_MQTT_LWT_CONNECT_MESSAGE); + } + parseSystemVariables(LWTMessageConnect, false); + } + return LWTMessageConnect; +} + +String getLWT_messageDisconnect(const ControllerSettingsStruct& ControllerSettings) { + String LWTMessageDisconnect; + + if (ControllerSettings.mqtt_sendLWT()) { + LWTMessageDisconnect = ControllerSettings.LWTMessageDisconnect; + + if (LWTMessageDisconnect.isEmpty()) { + LWTMessageDisconnect = F(DEFAULT_MQTT_LWT_DISCONNECT_MESSAGE); + } + parseSystemVariables(LWTMessageDisconnect, false); + } + return LWTMessageDisconnect; +} + +#endif // if FEATURE_MQTT + +/*********************************************************************************************\ +* Send status info to request source +\*********************************************************************************************/ +void SendStatusOnlyIfNeeded(struct EventStruct *event, bool param1, uint32_t key, const String& param2, int16_t param3) { + if (SourceNeedsStatusUpdate(event->Source)) { + SendStatus(event, getPinStateJSON(param1, key, param2, param3)); + printToWeb = false; // SP: 2020-06-12: to avoid to add more info to a JSON structure + } +} + +bool SourceNeedsStatusUpdate(EventValueSource::Enum eventSource) +{ + switch (eventSource) { + case EventValueSource::Enum::VALUE_SOURCE_HTTP: + case EventValueSource::Enum::VALUE_SOURCE_SERIAL: + case EventValueSource::Enum::VALUE_SOURCE_MQTT: + case EventValueSource::Enum::VALUE_SOURCE_WEB_FRONTEND: + return true; + + default: + break; + } + return false; +} + +void SendStatus(struct EventStruct *event, const __FlashStringHelper * status) +{ + SendStatus(event, String(status)); +} + +void SendStatus(struct EventStruct *event, const String& status) +{ + if (status.isEmpty()) { return; } + + switch (event->Source) + { + case EventValueSource::Enum::VALUE_SOURCE_HTTP: + case EventValueSource::Enum::VALUE_SOURCE_WEB_FRONTEND: + + if (printToWeb) { + printWebString += status; + } + break; +#if FEATURE_MQTT + case EventValueSource::Enum::VALUE_SOURCE_MQTT: + MQTTStatus(event, status); + break; +#endif // if FEATURE_MQTT + case EventValueSource::Enum::VALUE_SOURCE_SERIAL: + serialPrintln(status); + break; + + default: + break; + } +} + +#if FEATURE_MQTT +controllerIndex_t firstEnabledMQTT_ControllerIndex() { + for (controllerIndex_t i = 0; i < CONTROLLER_MAX; ++i) { + protocolIndex_t ProtocolIndex = getProtocolIndex_from_ControllerIndex(i); + if (validProtocolIndex(ProtocolIndex)) { + if (getProtocolStruct(ProtocolIndex).usesMQTT && Settings.ControllerEnabled[i]) { + return i; + } + } + } + return INVALID_CONTROLLER_INDEX; +} + +bool MQTT_queueFull(controllerIndex_t controller_idx) { + if (MQTTDelayHandler == nullptr) { + return true; + } + + if (MQTTDelayHandler->queueFull(controller_idx)) { + // The queue is full, try to make some room first. + processMQTTdelayQueue(); + return MQTTDelayHandler->queueFull(controller_idx); + } + return false; +} + +bool MQTTpublish(controllerIndex_t controller_idx, taskIndex_t taskIndex, const char *topic, const char *payload, bool retained, bool callbackTask) +{ + if (MQTTDelayHandler == nullptr) { + return false; + } + + if (MQTT_queueFull(controller_idx)) { + return false; + } + const bool success = MQTTDelayHandler->addToQueue(std::unique_ptr(new (std::nothrow) MQTT_queue_element(controller_idx, taskIndex, topic, payload, retained, callbackTask))); + + scheduleNextMQTTdelayQueue(); + return success; +} + +bool MQTTpublish(controllerIndex_t controller_idx, taskIndex_t taskIndex, String&& topic, String&& payload, bool retained, bool callbackTask) { + if (MQTTDelayHandler == nullptr) { + return false; + } + + if (MQTT_queueFull(controller_idx)) { + return false; + } + + const bool success = MQTTDelayHandler->addToQueue(std::unique_ptr(new (std::nothrow) MQTT_queue_element(controller_idx, taskIndex, std::move(topic), std::move(payload), retained, callbackTask))); + + scheduleNextMQTTdelayQueue(); + return success; +} + +/*********************************************************************************************\ +* Send status info back to channel where request came from +\*********************************************************************************************/ +void MQTTStatus(struct EventStruct *event, const String& status) +{ + controllerIndex_t enabledMqttController = firstEnabledMQTT_ControllerIndex(); + + if (validControllerIndex(enabledMqttController)) { + controllerIndex_t DomoticzMQTT_controllerIndex = findFirstEnabledControllerWithId(2); + + if (DomoticzMQTT_controllerIndex == enabledMqttController) { + // Do not send MQTT status updates to Domoticz + return; + } + String pubname; + bool mqtt_retainFlag; + { + // Place the ControllerSettings in a scope to free the memory as soon as we got all relevant information. + MakeControllerSettings(ControllerSettings); //-V522 + + if (!AllocatedControllerSettings()) { + addLog(LOG_LEVEL_ERROR, F("MQTT : Cannot send status, out of RAM")); + return; + } + + LoadControllerSettings(enabledMqttController, *ControllerSettings); + pubname = ControllerSettings->Publish; + mqtt_retainFlag = ControllerSettings->mqtt_retainFlag(); + } + + // FIXME TD-er: Why check for "/#" suffix on a publish topic? + // It makes no sense to have a subscribe wildcard on a publish topic. + pubname.replace(F("/#"), F("/status")); + + parseSingleControllerVariable(pubname, event, 0, false); + parseControllerVariables(pubname, event, false); + + + if (!pubname.endsWith(F("/status"))) { + pubname += F("/status"); + } + + MQTTpublish(enabledMqttController, event->TaskIndex, pubname.c_str(), status.c_str(), mqtt_retainFlag); + } +} + +#endif // if FEATURE_MQTT + + +/*********************************************************************************************\ +* send specific sensor task data, effectively calling PluginCall(PLUGIN_READ...) +\*********************************************************************************************/ +void SensorSendTask(struct EventStruct *event, unsigned long timestampUnixTime) +{ + SensorSendTask(event, timestampUnixTime, millis()); +} + +void SensorSendTask(struct EventStruct *event, unsigned long timestampUnixTime, unsigned long lasttimer) +{ + if (!validTaskIndex(event->TaskIndex)) { return; } + Scheduler.reschedule_task_device_timer(event->TaskIndex, lasttimer); + + #ifndef BUILD_NO_RAM_TRACKER + checkRAM(F("SensorSendTask")); + #endif // ifndef BUILD_NO_RAM_TRACKER + + if (Settings.TaskDeviceEnabled[event->TaskIndex]) + { + const deviceIndex_t DeviceIndex = getDeviceIndex_from_TaskIndex(event->TaskIndex); + + if (!validDeviceIndex(DeviceIndex)) { return; } + + struct EventStruct TempEvent(event->TaskIndex); + TempEvent.Source = event->Source; + TempEvent.timestamp = timestampUnixTime; + checkDeviceVTypeForTask(&TempEvent); + + String dummy; + if (PluginCall(PLUGIN_READ, &TempEvent, dummy)) { + sendData(&TempEvent); + } + } +} diff --git a/src/src/PluginStructs/P104_data_struct.cpp b/src/src/PluginStructs/P104_data_struct.cpp index 23c3ccfd54..8badf5550f 100644 --- a/src/src/PluginStructs/P104_data_struct.cpp +++ b/src/src/PluginStructs/P104_data_struct.cpp @@ -1,2598 +1,2598 @@ -#include "../PluginStructs/P104_data_struct.h" - -#ifdef USES_P104 - -# include "../Helpers/ESPEasy_Storage.h" -# include "../Helpers/Numerical.h" -# include "../WebServer/Markup_Forms.h" -# include "../WebServer/ESPEasy_WebServer.h" -# include "../WebServer/Markup.h" -# include "../WebServer/HTML_wrappers.h" -# include "../ESPEasyCore/ESPEasyRules.h" -# include "../Globals/ESPEasy_time.h" -# include "../Globals/RTC.h" - -# include -# include -# include - -// Needed also here for PlatformIO's library finder as the .h file -// is in a directory which is excluded in the src_filter - -# if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) -void createHString(String& string); // Forward definition -# endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) -void reverseStr(String& str); // Forward definition - -/**************************************************************** - * Constructor - ***************************************************************/ -P104_data_struct::P104_data_struct(MD_MAX72XX::moduleType_t _mod, - taskIndex_t _taskIndex, - int8_t _cs_pin, - uint8_t _modules, - uint8_t _zonesCount) - : mod(_mod), taskIndex(_taskIndex), cs_pin(_cs_pin), modules(_modules), expectedZones(_zonesCount) { - if (Settings.isSPI_valid()) { - P = new (std::nothrow) MD_Parola(mod, cs_pin, modules); - } else { - addLog(LOG_LEVEL_ERROR, F("DOTMATRIX: Required SPI not enabled. Initialization aborted!")); - } -} - -/******************************* - * Destructor - ******************************/ -P104_data_struct::~P104_data_struct() { - # if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) - - if (nullptr != pM) { - pM = nullptr; // Not created here, only reset - } - # endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) - - if (nullptr != P) { - // P->~MD_Parola(); // Call destructor directly, as delete of the object fails miserably - // do not: delete P; // Warning: the MD_Parola object doesn't have a virtual destructor, and when changed, - // a reboot uccurs when the object is deleted here! - P = nullptr; // Reset only - } -} - -/******************************* - * Initializer/starter - ******************************/ -bool P104_data_struct::begin() { - if (!initialized) { - loadSettings(); - initialized = true; - } - - if ((P != nullptr) && validGpio(cs_pin)) { - # ifdef P104_DEBUG - addLog(LOG_LEVEL_INFO, F("dotmatrix: begin() called")); - # endif // ifdef P104_DEBUG - P->begin(expectedZones); - # if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) - pM = P->getGraphicObject(); - # endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) - return true; - } - return false; -} - -# define P104_ZONE_SEP '\x02' -# define P104_FIELD_SEP '\x01' -# define P104_ZONE_DISP ';' -# define P104_FIELD_DISP ',' - -# define P104_CONFIG_VERSION_V2 0xF000 // Marker in first uint16_t to to indicate second version config settings, anything else if first - // version. - // Any third version or later could use 0xE000, etc. The 'version' is stored in the first uint16_t - // stored in the custom settings - -/* - Settings layout: - Version 1: - - uint16_t : size of the next blob holding all settings - - char[x] : Blob with settings, with csv-like strings, using P104_FIELD_SEP and P104_ZONE_SEP separators - Version 2: - - uint16_t : marker with content P104_CONFIG_VERSION_V2 - - uint16_t : size of next blob holding 1 zone settings string - - char[y] : Blob holding 1 zone settings string, with csv like string, using P104_FIELD_SEP separators - - uint16_t : next size, if 0 then no more blobs - - char[x] : Blob - - ... - - Max. allowed total custom settings size = 1024 - */ -/************************************** - * loadSettings - *************************************/ -void P104_data_struct::loadSettings() { - uint16_t bufferSize; - char *settingsBuffer; - - if (taskIndex < TASKS_MAX) { - int loadOffset = 0; - - // Read size of the used buffer, could be the settings-version marker - LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)&bufferSize, sizeof(bufferSize), loadOffset); - bool settingsVersionV2 = (bufferSize == P104_CONFIG_VERSION_V2) || (bufferSize == 0u); - uint16_t structDataSize = 0; - uint16_t reservedBuffer = 0; - - if (!settingsVersionV2) { - reservedBuffer = bufferSize + 1; // just add 1 for storing a string-terminator - addLog(LOG_LEVEL_INFO, F("dotmatrix: Reading Settings V1, will be stored as Settings V2.")); - } else { - reservedBuffer = P104_SETTINGS_BUFFER_V2 + 1; // just add 1 for storing a string-terminator - } - reservedBuffer++; // Add 1 for 0..size use - settingsBuffer = new char[reservedBuffer](); // Allocate buffer and reset to all zeroes - loadOffset += sizeof(bufferSize); - - if (settingsVersionV2) { - LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)&bufferSize, sizeof(bufferSize), loadOffset); - loadOffset += sizeof(bufferSize); // Skip the size - } - structDataSize = bufferSize; - # ifdef P104_DEBUG_DEV - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, strformat(F("P104: loadSettings stored Size: %d taskindex: %d"), structDataSize, taskIndex)); - } - # endif // ifdef P104_DEBUG_DEV - - // Read actual data - if (structDataSize > 0) { // Reading 0 bytes logs an error, so lets avoid that - LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)settingsBuffer, structDataSize, loadOffset); - } - settingsBuffer[bufferSize + 1] = '\0'; // Terminate string - - uint8_t zoneIndex = 0; - - { - String buffer(settingsBuffer); - # ifdef P104_DEBUG_DEV - - String log; - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log = F("P104: loadSettings bufferSize: "); - log += bufferSize; - log += F(" untrimmed: "); - log += buffer.length(); - } - # endif // ifdef P104_DEBUG_DEV - buffer.trim(); - # ifdef P104_DEBUG_DEV - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log += F(" trimmed: "); - log += buffer.length(); - addLogMove(LOG_LEVEL_INFO, log); - } - # endif // ifdef P104_DEBUG_DEV - - if (zones.size() > 0) { - zones.clear(); - } - zones.reserve(P104_MAX_ZONES); - numDevices = 0; - - String tmp; - String fld; - int32_t tmp_int; - uint16_t prev2 = 0; - int16_t offset2 = buffer.indexOf(P104_ZONE_SEP); - - if ((offset2 == -1) && (buffer.length() > 0)) { - offset2 = buffer.length(); - } - - while (offset2 > -1) { - tmp = buffer.substring(prev2, offset2); - # ifdef P104_DEBUG_DEV - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - log = F("P104: reading string: "); - log += tmp; - log.replace(P104_FIELD_SEP, P104_FIELD_DISP); - addLogMove(LOG_LEVEL_INFO, log); - } - # endif // ifdef P104_DEBUG_DEV - - zones.push_back(P104_zone_struct(zoneIndex + 1)); - - tmp_int = 0; - - // WARNING: Order of parsing these values should match the numeric order of P104_OFFSET_* values - for (uint8_t i = 0; i < P104_OFFSET_COUNT; ++i) { - if (i == P104_OFFSET_TEXT) { - zones[zoneIndex].text = parseStringKeepCaseNoTrim(tmp, 1 + P104_OFFSET_TEXT, P104_FIELD_SEP); - } else { - if (validIntFromString(parseString(tmp, 1 + i, P104_FIELD_SEP), tmp_int)) { - zones[zoneIndex].setIntValue(i, tmp_int); - } - } - } - - delay(0); - - numDevices += zones[zoneIndex].size + zones[zoneIndex].offset; - - if (!settingsVersionV2) { - prev2 = offset2 + 1; - offset2 = buffer.indexOf(P104_ZONE_SEP, prev2); - } else { - loadOffset += bufferSize; - structDataSize = sizeof(bufferSize); - LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)&bufferSize, structDataSize, loadOffset); - offset2 = bufferSize; // Length - - if (bufferSize == 0) { // End of zones reached - offset2 = -1; // fall out of while loop - } else { - structDataSize = bufferSize; - loadOffset += sizeof(bufferSize); - LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)settingsBuffer, structDataSize, loadOffset); - settingsBuffer[bufferSize + 1] = '\0'; // Terminate string - buffer = String(settingsBuffer); - } - } - zoneIndex++; - - # ifdef P104_DEBUG - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("dotmatrix: parsed zone: "), zoneIndex)); - } - # endif // ifdef P104_DEBUG - } - - buffer = String(); // Free some memory - } - - delete[] settingsBuffer; // Release allocated buffer - # ifdef P104_DEBUG_DEV - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("P104: read zones from config: "), zoneIndex)); - } - # endif // ifdef P104_DEBUG_DEV - - if (expectedZones == -1) { expectedZones = zoneIndex; } - - if (expectedZones == 0) { expectedZones++; } // Guarantee at least 1 zone to be displayed - - while (zoneIndex < expectedZones) { - zones.push_back(P104_zone_struct(zoneIndex + 1)); - - if (equals(zones[zoneIndex].text, F("\"\""))) { // Special case - zones[zoneIndex].text.clear(); - } - - zoneIndex++; - delay(0); - } - # ifdef P104_DEBUG_DEV - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, strformat(F("P104: total zones initialized: %d expected: %d"), zoneIndex, expectedZones)); - } - # endif // ifdef P104_DEBUG_DEV - } -} - -/**************************************************** - * configureZones: initialize Zones setup - ***************************************************/ -void P104_data_struct::configureZones() { - if (!initialized) { - loadSettings(); - initialized = true; - } - - uint8_t currentZone = 0; - uint8_t zoneOffset = 0; - - # ifdef P104_DEBUG_DEV - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("P104: configureZones to do: "), zones.size())); - } - # endif // ifdef P104_DEBUG_DEV - - if (nullptr == P) { return; } - - P->displayClear(); - - for (auto it = zones.begin(); it != zones.end(); ++it) { - if (it->zone <= expectedZones) { - zoneOffset += it->offset; - P->setZone(currentZone, zoneOffset, zoneOffset + it->size - 1); - # if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) - it->_startModule = zoneOffset; - P->getDisplayExtent(currentZone, it->_lower, it->_upper); - # endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) - zoneOffset += it->size; - - switch (it->font) { - # ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT - case P104_DOUBLE_HEIGHT_FONT_ID: { - P->setFont(currentZone, numeric7SegDouble); - P->setCharSpacing(currentZone, P->getCharSpacing() * 2); // double spacing as well - break; - } - # endif // ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT - # ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT - case P104_FULL_DOUBLEHEIGHT_FONT_ID: { - P->setFont(currentZone, BigFont); - P->setCharSpacing(currentZone, P->getCharSpacing() * 2); // double spacing as well - break; - } - # endif // ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT - # ifdef P104_USE_VERTICAL_FONT - case P104_VERTICAL_FONT_ID: { - P->setFont(currentZone, _fontVertical); - break; - } - # endif // ifdef P104_USE_VERTICAL_FONT - # ifdef P104_USE_EXT_ASCII_FONT - case P104_EXT_ASCII_FONT_ID: { - P->setFont(currentZone, ExtASCII); - break; - } - # endif // ifdef P104_USE_EXT_ASCII_FONT - # ifdef P104_USE_ARABIC_FONT - case P104_ARABIC_FONT_ID: { - P->setFont(currentZone, fontArabic); - break; - } - # endif // ifdef P104_USE_ARABIC_FONT - # ifdef P104_USE_GREEK_FONT - case P104_GREEK_FONT_ID: { - P->setFont(currentZone, fontGreek); - break; - } - # endif // ifdef P104_USE_GREEK_FONT - # ifdef P104_USE_KATAKANA_FONT - case P104_KATAKANA_FONT_ID: { - P->setFont(currentZone, fontKatakana); - break; - } - # endif // ifdef P104_USE_KATAKANA_FONT - - // Extend above this comment with more fonts if/when available, - // case P104_DEFAULT_FONT_ID: and default: clauses should be the last options. - // This should also make sure the default font is set if a no longer available font was selected - case P104_DEFAULT_FONT_ID: - default: { - P->setFont(currentZone, nullptr); // default font - break; - } - } - - // Inverted - P->setInvert(currentZone, it->inverted); - - // Special Effects - P->setZoneEffect(currentZone, (it->specialEffect & P104_SPECIAL_EFFECT_UP_DOWN) == P104_SPECIAL_EFFECT_UP_DOWN, PA_FLIP_UD); - P->setZoneEffect(currentZone, (it->specialEffect & P104_SPECIAL_EFFECT_LEFT_RIGHT) == P104_SPECIAL_EFFECT_LEFT_RIGHT, PA_FLIP_LR); - - // Brightness - P->setIntensity(currentZone, it->brightness); - - # ifdef P104_DEBUG_DEV - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, strformat(F("P104: configureZones #%d/%d offset: %d"), currentZone + 1, expectedZones, zoneOffset)); - } - # endif // ifdef P104_DEBUG_DEV - - delay(0); - - // Content == text && text != "" - if (((it->content == P104_CONTENT_TEXT) || - (it->content == P104_CONTENT_TEXT_REV)) - && (!it->text.isEmpty())) { - displayOneZoneText(currentZone, *it, it->text); - } - - # ifdef P104_USE_BAR_GRAPH - - // Content == Bar-graph && text != "" - if ((it->content == P104_CONTENT_BAR_GRAPH) - && (!it->text.isEmpty())) { - displayBarGraph(currentZone, *it, it->text); - } - # endif // ifdef P104_USE_BAR_GRAPH - - if (it->repeatDelay > -1) { - it->_repeatTimer = millis(); - } - currentZone++; - delay(0); - } - } - - // Synchronize the start - P->synchZoneStart(); -} - -/********************************************************** - * Display the text with attributes for a specific zone - *********************************************************/ -void P104_data_struct::displayOneZoneText(uint8_t zone, - const P104_zone_struct& zstruct, - const String & text) { - if ((nullptr == P) || (zone >= P104_MAX_ZONES)) { return; } // double check - sZoneInitial[zone].reserve(text.length()); - sZoneInitial[zone] = text; // Keep the original string for future use - sZoneBuffers[zone].reserve(text.length()); - sZoneBuffers[zone] = text; // We explicitly want a copy here so it can be modified by parseTemplate() - - sZoneBuffers[zone] = parseTemplate(sZoneBuffers[zone]); - - # if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) - - if (zstruct.layout == P104_LAYOUT_DOUBLE_UPPER) { - createHString(sZoneBuffers[zone]); - } - # endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) - - if (zstruct.content == P104_CONTENT_TEXT_REV) { - reverseStr(sZoneBuffers[zone]); - } - - String log; - - if (loglevelActiveFor(LOG_LEVEL_INFO) && - logAllText && - log.reserve(28 + text.length() + sZoneBuffers[zone].length())) { - log = strformat(F("dotmatrix: ZoneText: %d, '"), zone + 1); // UI-number - log += text; - log += F("' -> '"); - log += sZoneBuffers[zone]; - log += '\''; - addLogMove(LOG_LEVEL_INFO, log); - } - - P->displayZoneText(zone, - sZoneBuffers[zone].c_str(), - static_cast(zstruct.alignment), - zstruct.speed, - zstruct.pause, - static_cast(zstruct.animationIn), - static_cast(zstruct.animationOut)); -} - -/********************************************* - * Update all or the specified zone - ********************************************/ -void P104_data_struct::updateZone(uint8_t zone, - const P104_zone_struct& zstruct) { - if (nullptr == P) { return; } - - if (zone == 0) { - for (auto it = zones.begin(); it != zones.end(); ++it) { - if ((it->zone > 0) && - ((it->content == P104_CONTENT_TEXT) || - (it->content == P104_CONTENT_TEXT_REV))) { - displayOneZoneText(it->zone - 1, *it, sZoneInitial[it->zone - 1]); // Re-send last displayed text - P->displayReset(it->zone - 1); - } - # ifdef P104_USE_BAR_GRAPH - - if ((it->zone > 0) && - (it->content == P104_CONTENT_BAR_GRAPH)) { - displayBarGraph(it->zone - 1, *it, sZoneInitial[it->zone - 1]); // Re-send last displayed bar graph - } - # endif // ifdef P104_USE_BAR_GRAPH - - if ((zstruct.content == P104_CONTENT_TEXT) - || zstruct.content == P104_CONTENT_TEXT_REV - # ifdef P104_USE_BAR_GRAPH - || zstruct.content == P104_CONTENT_BAR_GRAPH - # endif // ifdef P104_USE_BAR_GRAPH - ) { - if (it->repeatDelay > -1) { // Restart repeat timer - it->_repeatTimer = millis(); - } - } - } - } else { - if ((zstruct.zone > 0) && - ((zstruct.content == P104_CONTENT_TEXT) || - (zstruct.content == P104_CONTENT_TEXT_REV))) { - displayOneZoneText(zstruct.zone - 1, zstruct, sZoneInitial[zstruct.zone - 1]); // Re-send last displayed text - P->displayReset(zstruct.zone - 1); - } - # ifdef P104_USE_BAR_GRAPH - - if ((zstruct.zone > 0) && - (zstruct.content == P104_CONTENT_BAR_GRAPH)) { - displayBarGraph(zstruct.zone - 1, zstruct, sZoneInitial[zstruct.zone - 1]); // Re-send last displayed bar graph - } - # endif // ifdef P104_USE_BAR_GRAPH - - // Repeat timer is/should be started elsewhere - } -} - -# if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) - -/*********************************************** - * Enable/Disable updating a range of modules - **********************************************/ -void P104_data_struct::modulesOnOff(uint8_t start, uint8_t end, MD_MAX72XX::controlValue_t on_off) { - for (uint8_t m = start; m <= end; m++) { - pM->control(m, MD_MAX72XX::UPDATE, on_off); - } -} - -# endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) - -# ifdef P104_USE_BAR_GRAPH - -/******************************************************** - * draw a single bar-graph, arguments already adjusted for direction - *******************************************************/ -void P104_data_struct::drawOneBarGraph(uint16_t lower, - uint16_t upper, - int16_t pixBottom, - int16_t pixTop, - uint16_t zeroPoint, - uint8_t barWidth, - uint8_t barType, - uint8_t row) { - bool on_off; - - for (uint8_t r = 0; r < barWidth; r++) { - for (uint8_t col = lower; col <= upper; col++) { - on_off = (col >= pixBottom && col <= pixTop); // valid area - - if ((zeroPoint != 0) && - (barType == P104_BARTYPE_STANDARD) && - (barWidth > 2) && - ((r == 0) || (r == barWidth - 1)) && - (col == lower + zeroPoint)) { - on_off = false; // when bar wider than 2, turn off zeropoint top and bottom led - } - - if ((barType == P104_BARTYPE_SINGLE) && (r > 0)) { - on_off = false; // barType 1 = only a single line is drawn, independent of the width - } - - if ((barType == P104_BARTYPE_ALT_DOT) && (barWidth > 1) && on_off) { - on_off = ((r % 2) == (col % 2)); // barType 2 = dotted line when bar is wider than 1 pixel - } - pM->setPoint(row + r, col, on_off); - - if (col % 16 == 0) { delay(0); } - } - delay(0); // Leave some breathingroom - } -} - -/******************************************************************** - * Process a graph-string to display in a zone, format: - * value,max-value,min-value,direction,bartype|... - *******************************************************************/ -void P104_data_struct::displayBarGraph(uint8_t zone, - const P104_zone_struct& zstruct, - const String & graph) { - if ((nullptr == P) || (nullptr == pM) || graph.isEmpty()) { return; } - sZoneInitial[zone] = graph; // Keep the original string for future use - - # define NOT_A_COMMA 0x02 // Something else than a comma, or the parseString function will get confused - String parsedGraph(graph); // Extra copy created so we don't mess up the incoming String - parsedGraph = parseTemplate(parsedGraph); - parsedGraph.replace(',', NOT_A_COMMA); - - std::vector barGraphs; - uint8_t currentBar = 0; - bool loop = true; - - // Parse the graph-string - while (loop && currentBar < 8) { // Maximum 8 valuesets possible - String graphpart = parseString(parsedGraph, currentBar + 1, '|'); - graphpart.trim(); - graphpart.replace(NOT_A_COMMA, ','); - - if (graphpart.isEmpty()) { - loop = false; - } else { - barGraphs.push_back(P104_bargraph_struct(currentBar)); - } - - if (loop && validDoubleFromString(parseString(graphpart, 1), barGraphs[currentBar].value)) { // value - String datapart = parseString(graphpart, 2); // max (default: 100.0) - - if (datapart.isEmpty()) { - barGraphs[currentBar].max = 100.0; - } else { - validDoubleFromString(datapart, barGraphs[currentBar].max); - } - datapart = parseString(graphpart, 3); // min (default: 0.0) - - if (datapart.isEmpty()) { - barGraphs[currentBar].min = 0.0; - } else { - validDoubleFromString(datapart, barGraphs[currentBar].min); - } - datapart = parseString(graphpart, 4); // direction - - if (datapart.isEmpty()) { - barGraphs[currentBar].direction = 0; - } else { - int32_t value = 0; - validIntFromString(datapart, value); - barGraphs[currentBar].direction = value; - } - datapart = parseString(graphpart, 5); // barType - - if (datapart.isEmpty()) { - barGraphs[currentBar].barType = 0; - } else { - int32_t value = 0; - validIntFromString(datapart, value); - barGraphs[currentBar].barType = value; - } - - if (definitelyGreaterThan(barGraphs[currentBar].min, barGraphs[currentBar].max)) { - std::swap(barGraphs[currentBar].min, barGraphs[currentBar].max); - } - } - # ifdef P104_DEBUG - - if (logAllText && loglevelActiveFor(LOG_LEVEL_INFO)) { - String log; - - if (log.reserve(70)) { - log = F("dotmatrix: Bar-graph: "); - - if (loop) { - log += currentBar; - log += F(" in: "); - log += graphpart; - log += F(" value: "); - log += barGraphs[currentBar].value; - log += F(" max: "); - log += barGraphs[currentBar].max; - log += F(" min: "); - log += barGraphs[currentBar].min; - log += F(" dir: "); - log += barGraphs[currentBar].direction; - log += F(" typ: "); - log += barGraphs[currentBar].barType; - } else { - log += F(" bsize: "); - log += barGraphs.size(); - } - addLogMove(LOG_LEVEL_INFO, log); - } - } - # endif // ifdef P104_DEBUG - currentBar++; // next - delay(0); // Leave some breathingroom - } - # undef NOT_A_COMMA - - if (barGraphs.size() > 0) { - uint8_t barWidth = 8 / barGraphs.size(); // Divide the 8 pixel width per number of bars to show - int16_t pixTop, pixBottom; - uint16_t zeroPoint; - # ifdef P104_DEBUG - String log; - - if (logAllText && - loglevelActiveFor(LOG_LEVEL_INFO) && - log.reserve(64)) { - log = F("dotmatrix: bar Width: "); - log += barWidth; - log += F(" low: "); - log += zstruct._lower; - log += F(" high: "); - log += zstruct._upper; - } - # endif // ifdef P104_DEBUG - modulesOnOff(zstruct._startModule, zstruct._startModule + zstruct.size - 1, MD_MAX72XX::MD_OFF); // Stop updates on modules - P->setIntensity(zstruct.zone - 1, zstruct.brightness); // don't forget to set the brightness - uint8_t row = 0; - - if ((barGraphs.size() == 3) || (barGraphs.size() == 5) || (barGraphs.size() == 6)) { // Center within the rows a bit - for (; row < (barGraphs.size() == 5 ? 2 : 1); row++) { - for (uint8_t col = zstruct._lower; col <= zstruct._upper; col++) { - pM->setPoint(row, col, false); // all off - - if (col % 16 == 0) { delay(0); } - } - delay(0); // Leave some breathingroom - } - } - - for (auto it = barGraphs.begin(); it != barGraphs.end(); ++it) { - if (essentiallyZero(it->min)) { - pixTop = zstruct._lower - 1 + (((zstruct._upper + 1) - zstruct._lower) / it->max) * it->value; - pixBottom = zstruct._lower - 1; - zeroPoint = 0; - } else { - if (definitelyLessThan(it->min, 0.0) && - definitelyGreaterThan(it->max, 0.0) && - definitelyGreaterThan(it->max - it->min, 0.01)) { // Zero-point is used - zeroPoint = (it->min * -1.0) / ((it->max - it->min) / (1.0 * ((zstruct._upper + 1) - zstruct._lower))); - } else { - zeroPoint = 0; - } - pixTop = zstruct._lower + zeroPoint + (((zstruct._upper + 1) - zstruct._lower) / (it->max - it->min)) * it->value; - pixBottom = zstruct._lower + zeroPoint; - - if (definitelyLessThan(it->value, 0.0)) { - std::swap(pixTop, pixBottom); - } - } - - if (it->direction == 1) { // Left to right display: Flip values within the lower/upper range - pixBottom = zstruct._upper - (pixBottom - zstruct._lower); - pixTop = zstruct._lower + (zstruct._upper - pixTop); - std::swap(pixBottom, pixTop); - zeroPoint = zstruct._upper - zstruct._lower - zeroPoint + (zeroPoint == 0 ? 1 : 0); - } - # ifdef P104_DEBUG_DEV - - if (logAllText && loglevelActiveFor(LOG_LEVEL_INFO)) { - log += F(" B: "); - log += pixBottom; - log += F(" T: "); - log += pixTop; - log += F(" Z: "); - log += zeroPoint; - } - # endif // ifdef P104_DEBUG_DEV - drawOneBarGraph(zstruct._lower, zstruct._upper, pixBottom, pixTop, zeroPoint, barWidth, it->barType, row); - row += barWidth; // Next set of rows - delay(0); // Leave some breathingroom - } - - for (; row < 8; row++) { // Clear unused rows - for (uint8_t col = zstruct._lower; col <= zstruct._upper; col++) { - pM->setPoint(row, col, false); // all off - - if (col % 16 == 0) { delay(0); } - } - delay(0); // Leave some breathingroom - } - # ifdef P104_DEBUG - - if (logAllText && loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, log); - } - # endif // ifdef P104_DEBUG - modulesOnOff(zstruct._startModule, zstruct._startModule + zstruct.size - 1, MD_MAX72XX::MD_ON); // Continue updates on modules - } -} - -# endif // ifdef P104_USE_BAR_GRAPH - -# ifdef P104_USE_DOT_SET -void P104_data_struct::displayDots(uint8_t zone, - const P104_zone_struct& zstruct, - const String & dots) { - if ((nullptr == P) || (nullptr == pM) || dots.isEmpty()) { return; } - { - uint8_t idx = 0; - String sRow; - String sCol; - String sOn_off; - bool on_off = true; - modulesOnOff(zstruct._startModule, zstruct._startModule + zstruct.size - 1, MD_MAX72XX::MD_OFF); // Stop updates on modules - P->setIntensity(zstruct.zone - 1, zstruct.brightness); // don't forget to set the brightness - sRow = parseString(dots, idx + 1); - sCol = parseString(dots, idx + 2); - sOn_off = parseString(dots, idx + 3); - - while (!sRow.isEmpty() && !sCol.isEmpty()) { - on_off = true; // Default On - - int32_t row; - int32_t col; - if (validIntFromString(sRow, row) && - validIntFromString(sCol, col) && - (row > 0) && ((row - 1) < 8) && - (col > 0) && ((col - 1) <= (zstruct._upper - zstruct._lower))) { // Valid coordinates? - if (equals(sOn_off, F("0"))) { // Dot On is the default - on_off = false; - idx++; // 3rd argument used - } - pM->setPoint(row - 1, zstruct._upper - (col - 1), on_off); // Reverse layout - } - idx += 2; // Skip to next argument set - - if (idx % 16 == 0) { delay(0); } - sRow = parseString(dots, idx + 1); - sCol = parseString(dots, idx + 2); - sOn_off = parseString(dots, idx + 3); - } - - modulesOnOff(zstruct._startModule, zstruct._startModule + zstruct.size - 1, MD_MAX72XX::MD_ON); // Continue updates on modules - } -} - -# endif // ifdef P104_USE_DOT_SET - -/************************************************** - * Check if an animation is available in the current build - *************************************************/ -bool isAnimationAvailable(uint8_t animation, bool noneIsAllowed = false) { - textEffect_t selection = static_cast(animation); - - switch (selection) { - case PA_NO_EFFECT: - { - return noneIsAllowed; - } - case PA_PRINT: - case PA_SCROLL_UP: - case PA_SCROLL_DOWN: - case PA_SCROLL_LEFT: - case PA_SCROLL_RIGHT: - { - return true; - } - # if ENA_SPRITE - case PA_SPRITE: - { - return true; - } - # endif // ENA_SPRITE - # if ENA_MISC - case PA_SLICE: - case PA_MESH: - case PA_FADE: - case PA_DISSOLVE: - case PA_BLINDS: - case PA_RANDOM: - { - return true; - } - # endif // ENA_MISC - # if ENA_WIPE - case PA_WIPE: - case PA_WIPE_CURSOR: - { - return true; - } - # endif // ENA_WIPE - # if ENA_SCAN - case PA_SCAN_HORIZ: - case PA_SCAN_HORIZX: - case PA_SCAN_VERT: - case PA_SCAN_VERTX: - { - return true; - } - # endif // ENA_SCAN - # if ENA_OPNCLS - case PA_OPENING: - case PA_OPENING_CURSOR: - case PA_CLOSING: - case PA_CLOSING_CURSOR: - { - return true; - } - # endif // ENA_OPNCLS - # if ENA_SCR_DIA - case PA_SCROLL_UP_LEFT: - case PA_SCROLL_UP_RIGHT: - case PA_SCROLL_DOWN_LEFT: - case PA_SCROLL_DOWN_RIGHT: - { - return true; - } - # endif // ENA_SCR_DIA - # if ENA_GROW - case PA_GROW_UP: - case PA_GROW_DOWN: - { - return true; - } - # endif // ENA_GROW - default: - return false; - } -} - -const char p104_subcommands[] PROGMEM = - "clear" - "|update" - - "|txt" - "|settxt" - -# ifdef P104_USE_BAR_GRAPH - "|bar" - "|setbar" -# endif // ifdef P104_USE_BAR_GRAPH - -# ifdef P104_USE_DOT_SET - "|dot" -# endif // ifdef P104_USE_DOT_SET - -# ifdef P104_USE_COMMANDS - "|alignment" - "|anim.in" - "|anim.out" - "|brightness" - "|content" - "|font" - "|inverted" -# if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) - "|layout" -# endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) - "|offset" - "|pause" - "|repeat" - "|size" - "|specialeffect" - "|speed" -# endif // ifdef P104_USE_COMMANDS -; - -// Subcommands prefixed by "dotmatrix," -enum class p104_subcommands_e { - clear, // subcommand: clear, / clear[,all] - update, // subcommand: update, / update[,all] - - txt, // subcommand: [set]txt,, (only - settxt, // subcommand: settxt,, (stores - -# ifdef P104_USE_BAR_GRAPH - bar, // subcommand: [set]bar,, (only allowed for zones - setbar, // subcommand: setbar,, (stores the graph-string -# endif // ifdef P104_USE_BAR_GRAPH - -# ifdef P104_USE_DOT_SET - dot, // subcommand: dot,,,[,0][,,[,0]...] to draw -# endif // ifdef P104_USE_DOT_SET - -# ifdef P104_USE_COMMANDS - alignment, // subcommand: alignment,, (0..3) - anim_in, // subcommand: anim.in,, (1..) - anim_out, // subcommand: anim.out,, (0..) - brightness, // subcommand: brightness,, (0..15) - content, // subcommand: content,, (0..-1) - font, // subcommand: font,, (only for incuded font id's) - inverted, // subcommand: inverted,, (disable/enable) -# if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) - layout, // subcommand: layout,, (0..2), only when double-height font is available -# endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) - offset, // subcommand: offset,, (0..-1) - pause, // subcommand: pause,, (0..P104_MAX_SPEED_PAUSE_VALUE) - repeat, // subcommand: repeat,, (-1..86400 = 24h) - size, // subcommand: size,, (1..) - specialeffect, // subcommand: specialeffect,, (0..3) - speed, // subcommand: speed,, (0..P104_MAX_SPEED_PAUSE_VALUE) -# endif // ifdef P104_USE_COMMANDS -}; - -/******************************************************* - * handlePluginWrite : process commands - ******************************************************/ -bool P104_data_struct::handlePluginWrite(taskIndex_t taskIndex, - const String& string) { - # ifdef P104_USE_COMMANDS - bool reconfigure = false; - # endif // ifdef P104_USE_COMMANDS - bool success = false; - const String command = parseString(string, 1); - - if ((nullptr != P) && equals(command, F("dotmatrix"))) { // main command: dotmatrix - const String subCommand = parseString(string, 2); - const int subCommand_i = GetCommandCode(subCommand.c_str(), p104_subcommands); - - if (subCommand_i != -1) { - const p104_subcommands_e subcommands_e = static_cast(subCommand_i); - - int32_t zoneIndex{}; - const String string4 = parseStringKeepCaseNoTrim(string, 4); - # ifdef P104_USE_COMMANDS - int32_t value4{}; - validIntFromString(string4, value4); - # endif // ifdef P104_USE_COMMANDS - - // Global subcommands - - if ((subcommands_e == p104_subcommands_e::clear) && // subcommand: clear[,all] - (string4.isEmpty() || - string4.equalsIgnoreCase(F("all")))) { - P->displayClear(); - success = true; - } else - - if ((subcommands_e == p104_subcommands_e::update) && // subcommand: update[,all] - (string4.isEmpty() || - string4.equalsIgnoreCase(F("all")))) { - updateZone(0, P104_zone_struct(0)); - success = true; - } - - // Zone-specific subcommands - if (validIntFromString(parseString(string, 3), zoneIndex) && - (zoneIndex > 0) && - (static_cast(zoneIndex) <= zones.size())) { - // subcommands are processed in the same order as they are presented in the UI - for (auto it = zones.begin(); it != zones.end() && !success; ++it) { - if ((it->zone == zoneIndex)) { // This zone - switch (subcommands_e) { - case p104_subcommands_e::clear: - // subcommand: clear, - { - P->displayClear(zoneIndex - 1); - success = true; - break; - } - - case p104_subcommands_e::update: - // subcommand: update, - { - updateZone(zoneIndex, *it); - success = true; - break; - } - - # ifdef P104_USE_COMMANDS - - case p104_subcommands_e::size: - // subcommand: size,, (1..) - { - if ((value4 > 0) && - (value4 <= P104_MAX_MODULES_PER_ZONE)) - { - reconfigure = (it->size != value4); - it->size = value4; - success = true; - } - break; - } - # endif // ifdef P104_USE_COMMANDS - - case p104_subcommands_e::txt: // subcommand: [set]txt,, (only - case p104_subcommands_e::settxt: // allowed for zones with Text content) - { - if ((it->content == P104_CONTENT_TEXT) || - (it->content == P104_CONTENT_TEXT_REV)) { // no length check, so longer than the UI allows is made - // possible - if ((subcommands_e == p104_subcommands_e::settxt) && // subcommand: settxt,, (stores - (string4.length() <= P104_MAX_TEXT_LENGTH_PER_ZONE)) { // the text in the settings, is not saved) - it->text = string4; // Only if not too long, could 'blow up' the - } // settings when saved - displayOneZoneText(zoneIndex - 1, *it, string4); - success = true; - } - - break; - } - - # ifdef P104_USE_COMMANDS - - case p104_subcommands_e::content: - // subcommand: content,, (0..-1) - { - if ((value4 >= 0) && - (value4 < P104_CONTENT_count)) - { - reconfigure = (it->content != value4); - it->content = value4; - success = true; - } - break; - } - - case p104_subcommands_e::alignment: - // subcommand: alignment,, (0..3) - { - if ((value4 >= 0) && - (value4 <= static_cast(textPosition_t::PA_RIGHT))) // last item in the enum - { - it->alignment = value4; - success = true; - } - break; - } - - case p104_subcommands_e::anim_in: - // subcommand: anim.in,, (1..) - { - if (isAnimationAvailable(value4)) { - it->animationIn = value4; - success = true; - } - break; - } - - case p104_subcommands_e::speed: - // subcommand: speed,, (0..P104_MAX_SPEED_PAUSE_VALUE) - { - if ((value4 >= 0) && - (value4 <= P104_MAX_SPEED_PAUSE_VALUE)) - { - it->speed = value4; - success = true; - } - break; - } - - case p104_subcommands_e::anim_out: - // subcommand: anim.out,, (0..) - { - if (isAnimationAvailable(value4, true)) - { - it->animationOut = value4; - success = true; - } - break; - } - - case p104_subcommands_e::pause: - // subcommand: pause,, (0..P104_MAX_SPEED_PAUSE_VALUE) - { - if ((value4 >= 0) && - (value4 <= P104_MAX_SPEED_PAUSE_VALUE)) - { - it->pause = value4; - success = true; - } - break; - } - - case p104_subcommands_e::font: - // subcommand: font,, (only for incuded font id's) - { - if ( - (value4 == 0) - # ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT - || (value4 == P104_DOUBLE_HEIGHT_FONT_ID) - # endif // ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT - # ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT - || (value4 == P104_FULL_DOUBLEHEIGHT_FONT_ID) - # endif // ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT - # ifdef P104_USE_VERTICAL_FONT - || (value4 == P104_VERTICAL_FONT_ID) - # endif // ifdef P104_USE_VERTICAL_FONT - # ifdef P104_USE_EXT_ASCII_FONT - || (value4 == P104_EXT_ASCII_FONT_ID) - # endif // ifdef P104_USE_EXT_ASCII_FONT - # ifdef P104_USE_ARABIC_FONT - || (value4 == P104_ARABIC_FONT_ID) - # endif // ifdef P104_USE_ARABIC_FONT - # ifdef P104_USE_GREEK_FONT - || (value4 == P104_GREEK_FONT_ID) - # endif // ifdef P104_USE_GREEK_FONT - # ifdef P104_USE_KATAKANA_FONT - || (value4 == P104_KATAKANA_FONT_ID) - # endif // ifdef P104_USE_KATAKANA_FONT - ) - { - reconfigure = (it->font != value4); - it->font = value4; - success = true; - } - break; - } - - case p104_subcommands_e::inverted: - // subcommand: inverted,, (disable/enable) - { - if ((value4 >= 0) && - (value4 <= 1)) - { - reconfigure = (it->inverted != value4); - it->inverted = value4; - success = true; - } - break; - } - - # if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) - - case p104_subcommands_e::layout: - // subcommand: layout,, (0..2), only when double-height font is available - { - if ((value4 >= 0) && - (value4 <= P104_LAYOUT_DOUBLE_LOWER)) - { - reconfigure = (it->layout != value4); - it->layout = value4; - success = true; - } - break; - } - # endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) - - case p104_subcommands_e::specialeffect: - // subcommand: specialeffect,, (0..3) - { - if ((value4 >= 0) && - (value4 <= P104_SPECIAL_EFFECT_BOTH)) - { - reconfigure = (it->specialEffect != value4); - it->specialEffect = value4; - success = true; - } - break; - } - - case p104_subcommands_e::offset: - // subcommand: offset,, (0..-1) - { - if ((value4 >= 0) && - (value4 < P104_MAX_MODULES_PER_ZONE) && - (value4 < it->size)) - { - reconfigure = (it->offset != value4); - it->offset = value4; - success = true; - } - break; - } - - case p104_subcommands_e::brightness: - // subcommand: brightness,, (0..15) - { - if ((value4 >= 0) && - (value4 <= P104_BRIGHTNESS_MAX)) - { - it->brightness = value4; - P->setIntensity(zoneIndex - 1, it->brightness); // Change brightness immediately - success = true; - } - break; - } - - case p104_subcommands_e::repeat: - // subcommand: repeat,, (-1..86400 = 24h) - { - if ((value4 >= -1) && - (value4 <= P104_MAX_REPEATDELAY_VALUE)) - { - it->repeatDelay = value4; - success = true; - - if (it->repeatDelay > -1) { - it->_repeatTimer = millis(); - } - } - break; - } - # endif // ifdef P104_USE_COMMANDS - - # ifdef P104_USE_BAR_GRAPH - - case p104_subcommands_e::bar: // subcommand: [set]bar,, (only allowed for - // zones - case p104_subcommands_e::setbar: // with Bargraph content) no length check, so longer than the - // UI allows is made possible - { - if (it->content == P104_CONTENT_BAR_GRAPH) { - if ((subcommands_e == p104_subcommands_e::setbar) && // subcommand: setbar,, (stores the - // graph-string - (string4.length() <= P104_MAX_TEXT_LENGTH_PER_ZONE)) { // in the settings, is not saved) - it->text = string4; // Only if not too long, could 'blow up' the settings when - // saved - } - displayBarGraph(zoneIndex - 1, *it, string4); - success = true; - } - break; - } - # endif // ifdef P104_USE_BAR_GRAPH - - # ifdef P104_USE_DOT_SET - - case p104_subcommands_e::dot: - // subcommand: dot,,,[,0][,,[,0]...] to draw - { - displayDots(zoneIndex - 1, *it, parseStringToEnd(string, 4)); // dots at row/column, add ,0 to turn a dot off - success = true; - break; - } - # endif // ifdef P104_USE_DOT_SET - } - - // FIXME TD-er: success is always false here. Maybe this must be done outside the for-loop? - if (success) { // Reset the repeat timer - if (it->repeatDelay > -1) { - it->_repeatTimer = millis(); - } - } - } - } - } - } - } - - # ifdef P104_USE_COMMANDS - - if (reconfigure) { - configureZones(); // Re-initialize - success = true; // Successful - } - # endif // ifdef P104_USE_COMMANDS - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - String log; - - if (log.reserve(34 + string.length())) { - log = F("dotmatrix: command "); - - if (!success) { log += F("NOT "); } - log += F("succesful: "); - log += string; - addLogMove(LOG_LEVEL_INFO, log); - } - } - - return success; // Default: unknown command -} - -int8_t P104_data_struct::getTime(char *psz, - bool seconds, - bool colon, - bool time12h, - bool timeAmpm) { - uint16_t h, M, s; - String ampm; - - # ifdef P104_USE_DATETIME_OPTIONS - - if (time12h) { - if (timeAmpm) { - ampm = (node_time.hour() >= 12 ? F("p") : F("a")); - } - h = node_time.hour() % 12; - - if (h == 0) { h = 12; } - } else - # endif // ifdef P104_USE_DATETIME_OPTIONS - { - h = node_time.hour(); - } - M = node_time.minute(); - - if (!seconds) { - sprintf_P(psz, PSTR("%02d%c%02d%s"), h, (colon ? ':' : ' '), M, ampm.c_str()); - } else { - s = node_time.second(); - sprintf_P(psz, PSTR("%02d%c%02d %02d%s"), h, (colon ? ':' : ' '), M, s, ampm.c_str()); - } - return M; -} - -void P104_data_struct::getDate(char *psz, - bool showYear, - bool fourDgt - # ifdef P104_USE_DATETIME_OPTIONS - , const uint8_t dateFmt - , const uint8_t dateSep - # endif // ifdef P104_USE_DATETIME_OPTIONS - ) { - uint16_t d, m, y; - const uint16_t year = node_time.year() - (fourDgt ? 0 : 2000); - - # ifdef P104_USE_DATETIME_OPTIONS - const String separators = F(" /-."); - const char sep = separators[dateSep]; - # else // ifdef P104_USE_DATETIME_OPTIONS - const char sep = ' '; - # endif // ifdef P104_USE_DATETIME_OPTIONS - - d = node_time.day(); - m = node_time.month(); - y = year; - # ifdef P104_USE_DATETIME_OPTIONS - - if (showYear) { - switch (dateFmt) { - case P104_DATE_FORMAT_US: - d = node_time.month(); - m = node_time.day(); - y = year; - break; - case P104_DATE_FORMAT_JP: - d = year; - m = node_time.month(); - y = node_time.day(); - break; - } - } else { - if ((dateFmt == P104_DATE_FORMAT_US) || - (dateFmt == P104_DATE_FORMAT_JP)) { - std::swap(d, m); - } - } - # endif // ifdef P104_USE_DATETIME_OPTIONS - - if (showYear) { - sprintf_P(psz, PSTR("%02d%c%02d%c%02d"), d, sep, m, sep, y); // %02d will expand to 04 when needed - } else { - sprintf_P(psz, PSTR("%02d%c%02d"), d, sep, m); - } -} - -uint8_t P104_data_struct::getDateTime(char *psz, - bool colon, - bool time12h, - bool timeAmpm, - bool fourDgt - # ifdef P104_USE_DATETIME_OPTIONS - , const uint8_t dateFmt - , const uint8_t dateSep - # endif // ifdef P104_USE_DATETIME_OPTIONS - ) { - String ampm; - uint16_t d, M, y; - uint8_t h, m; - const uint16_t year = node_time.year() - (fourDgt ? 0 : 2000); - - # ifdef P104_USE_DATETIME_OPTIONS - const String separators = F(" /-."); - const char sep = separators[dateSep]; - # else // ifdef P104_USE_DATETIME_OPTIONS - const char sep = ' '; - # endif // ifdef P104_USE_DATETIME_OPTIONS - - # ifdef P104_USE_DATETIME_OPTIONS - - if (time12h) { - if (timeAmpm) { - ampm = (node_time.hour() >= 12 ? F("p") : F("a")); - } - h = node_time.hour() % 12; - - if (h == 0) { h = 12; } - } else - # endif // ifdef P104_USE_DATETIME_OPTIONS - { - h = node_time.hour(); - } - M = node_time.minute(); - - # ifdef P104_USE_DATETIME_OPTIONS - - switch (dateFmt) { - case P104_DATE_FORMAT_US: - d = node_time.month(); - m = node_time.day(); - y = year; - break; - case P104_DATE_FORMAT_JP: - d = year; - m = node_time.month(); - y = node_time.day(); - break; - default: - # endif // ifdef P104_USE_DATETIME_OPTIONS - d = node_time.day(); - m = node_time.month(); - y = year; - # ifdef P104_USE_DATETIME_OPTIONS -} - - # endif // ifdef P104_USE_DATETIME_OPTIONS - sprintf_P(psz, PSTR("%02d%c%02d%c%02d %02d%c%02d%s"), d, sep, m, sep, y, h, (colon ? ':' : ' '), M, ampm.c_str()); // %02d will expand to - // 04 when needed - return M; -} - -# if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) -void P104_data_struct::createHString(String& string) { - const uint16_t stringLen = string.length(); - - for (uint16_t i = 0; i < stringLen; i++) { - string[i] |= 0x80; // use 'high' part of the font, by adding 0x80 - } -} - -# endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) - -void P104_data_struct::reverseStr(String& str) { - const uint16_t n = str.length(); - - // Swap characters starting from two corners - for (uint16_t i = 0; i < n / 2; i++) { - std::swap(str[i], str[n - i - 1]); - } -} - -/************************************************************************ - * execute all PLUGIN_ONE_PER_SECOND tasks - ***********************************************************************/ -bool P104_data_struct::handlePluginOncePerSecond(struct EventStruct *event) { - if (nullptr == P) { return false; } - bool redisplay = false; - bool success = false; - - # ifdef P104_USE_DATETIME_OPTIONS - bool useFlasher = !bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FLASH); - bool time12h = bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_12H); - bool timeAmpm = bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_AMPM); - bool year4dgt = bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_YEAR4DGT); - # else // ifdef P104_USE_DATETIME_OPTIONS - bool useFlasher = true; - bool time12h = false; - bool timeAmpm = false; - bool year4dgt = false; - # endif // ifdef P104_USE_DATETIME_OPTIONS - bool newFlasher = !flasher && useFlasher; - - for (auto it = zones.begin(); it != zones.end(); ++it) { - redisplay = false; - - if (P->getZoneStatus(it->zone - 1)) { // Animations done? - switch (it->content) { - case P104_CONTENT_TIME: // time - case P104_CONTENT_TIME_SEC: // time sec - { - bool useSeconds = (it->content == P104_CONTENT_TIME_SEC); - int8_t m = getTime(szTimeL, useSeconds, flasher || !useFlasher, time12h, timeAmpm); - flasher = newFlasher; - redisplay = useFlasher || useSeconds || (it->_lastChecked != m); - it->_lastChecked = m; - break; - } - case P104_CONTENT_DATE4: // date/4 - case P104_CONTENT_DATE6: // date/6 - { - if (it->_lastChecked != node_time.day()) { - getDate(szTimeL, - it->content != P104_CONTENT_DATE4, - year4dgt - # ifdef P104_USE_DATETIME_OPTIONS - , get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FORMAT) - , get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_SEP_CHAR) - # endif // ifdef P104_USE_DATETIME_OPTIONS - ); - redisplay = true; - it->_lastChecked = node_time.day(); - } - break; - } - case P104_CONTENT_DATE_TIME: // date-time/9 - { - int8_t m = getDateTime(szTimeL, - flasher || !useFlasher, - time12h, - timeAmpm, - year4dgt - # ifdef P104_USE_DATETIME_OPTIONS - , get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FORMAT) - , get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_SEP_CHAR) - # endif // ifdef P104_USE_DATETIME_OPTIONS - ); - flasher = newFlasher; - redisplay = useFlasher || (it->_lastChecked != m); - it->_lastChecked = m; - break; - } - default: - break; - } - - if (redisplay) { - displayOneZoneText(it->zone - 1, *it, String(szTimeL)); - P->displayReset(it->zone - 1); - - if (it->repeatDelay > -1) { - it->_repeatTimer = millis(); - } - } - } - delay(0); // Leave some breathingroom - } - - if (redisplay) { - // synchronise the start - P->synchZoneStart(); - } - return redisplay || success; -} - -/*************************************************** - * restart a zone if the repeat delay (if any) has passed - **************************************************/ -void P104_data_struct::checkRepeatTimer(uint8_t z) { - if (nullptr == P) { return; } - bool handled = false; - - for (auto it = zones.begin(); it != zones.end() && !handled; ++it) { - if (it->zone == z + 1) { - handled = true; - - if ((it->repeatDelay > -1) && (timePassedSince(it->_repeatTimer) >= (it->repeatDelay - 1) * 1000)) { // Compensated for the '1' in - // PLUGIN_ONE_PER_SECOND - # ifdef P104_DEBUG - - if (logAllText && loglevelActiveFor(LOG_LEVEL_INFO)) { - String log; - log.reserve(51); - log = F("dotmatrix: Repeat zone: "); - log += it->zone; - log += F(" delay: "); - log += it->repeatDelay; - log += F(" ("); - log += (timePassedSince(it->_repeatTimer) / 1000.0f); // Decimals can be useful here - log += ')'; - addLogMove(LOG_LEVEL_INFO, log); - } - # endif // ifdef P104_DEBUG - - if ((it->content == P104_CONTENT_TEXT) || - (it->content == P104_CONTENT_TEXT_REV)) { - displayOneZoneText(it->zone - 1, *it, sZoneInitial[it->zone - 1]); // Re-send last displayed text - P->displayReset(it->zone - 1); - } - - if ((it->content == P104_CONTENT_TIME) || - (it->content == P104_CONTENT_TIME_SEC) || - (it->content == P104_CONTENT_DATE4) || - (it->content == P104_CONTENT_DATE6) || - (it->content == P104_CONTENT_DATE_TIME)) { - it->_lastChecked = -1; // Invalidate so next run will re-display the date/time - } - # ifdef P104_USE_BAR_GRAPH - - if (it->content == P104_CONTENT_BAR_GRAPH) { - displayBarGraph(it->zone - 1, *it, sZoneInitial[it->zone - 1]); // Re-send last displayed bar graph - } - # endif // ifdef P104_USE_BAR_GRAPH - it->_repeatTimer = millis(); - } - } - delay(0); // Leave some breathingroom - } -} - -/*************************************** - * saveSettings gather the zones data from the UI and store in customsettings - **************************************/ -bool P104_data_struct::saveSettings() { - error = String(); // Clear - - # ifdef P104_DEBUG_DEV - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("P104: saving zones, count: "), expectedZones)); - } - # endif // ifdef P104_DEBUG_DEV - - uint8_t index = 0; - uint8_t action = P104_ACTION_NONE; - uint8_t zoneIndex = 0; - int8_t zoneOffset = 0; - - zones.clear(); // Start afresh - - for (uint8_t zCounter = 0; zCounter < expectedZones; zCounter++) { - # ifdef P104_USE_ZONE_ACTIONS - action = getFormItemIntCustomArgName(index + P104_OFFSET_ACTION); - - if (((action == P104_ACTION_ADD_ABOVE) && (zoneOrder == 0)) || - ((action == P104_ACTION_ADD_BELOW) && (zoneOrder == 1))) { - zones.push_back(P104_zone_struct(0)); - zoneOffset++; - # ifdef P104_DEBUG_DEV - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("P104: insert before zone: "), zoneIndex + 1)); - } - # endif // ifdef P104_DEBUG_DEV - } - # endif // ifdef P104_USE_ZONE_ACTIONS - zoneIndex = zCounter + zoneOffset; - - if (action == P104_ACTION_DELETE) { - zoneOffset--; - } else { - # ifdef P104_DEBUG_DEV - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("P104: read zone: "), zoneIndex + 1)); - } - # endif // ifdef P104_DEBUG_DEV - zones.push_back(P104_zone_struct(zoneIndex + 1)); - - for (uint8_t i = 0; i < P104_OFFSET_COUNT; ++i) { - // for newly added zone, use defaults - const bool mustCheckSize = - (i == P104_OFFSET_BRIGHTNESS) || - (i == P104_OFFSET_REPEATDELAY); - if (!mustCheckSize || zones[zoneIndex].size != 0) { - if (i == P104_OFFSET_TEXT) { - zones[zoneIndex].text = wrapWithQuotes(webArg(getPluginCustomArgName(index + P104_OFFSET_TEXT))); - } else { - zones[zoneIndex].setIntValue(i, getFormItemIntCustomArgName(index + i)); - } - } - } - } - # ifdef P104_DEBUG_DEV - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("P104: add zone: "), zoneIndex + 1)); - } - # endif // ifdef P104_DEBUG_DEV - - # ifdef P104_USE_ZONE_ACTIONS - - if (((action == P104_ACTION_ADD_BELOW) && (zoneOrder == 0)) || - ((action == P104_ACTION_ADD_ABOVE) && (zoneOrder == 1))) { - zones.push_back(P104_zone_struct(0)); - zoneOffset++; - # ifdef P104_DEBUG_DEV - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, concat(F("P104: insert after zone: "), zoneIndex + 2)); - } - # endif // ifdef P104_DEBUG_DEV - } - # endif // ifdef P104_USE_ZONE_ACTIONS - - index += P104_OFFSET_COUNT; - delay(0); - } - - uint16_t bufferSize; - int saveOffset = 0; - - numDevices = 0; // Count the number of connected display units - - bufferSize = P104_CONFIG_VERSION_V2; // Save special marker that we're using V2 settings - // This write is counting - error += SaveToFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)&bufferSize, sizeof(bufferSize), saveOffset); - saveOffset += sizeof(bufferSize); - - String zbuffer; - - // 47 total + (max) 100 characters for it->text requires a buffer of ~150 (P104_SETTINGS_BUFFER_V2), but only the required length is - // stored with the length prefixed - if (zbuffer.reserve(P104_SETTINGS_BUFFER_V2 + 2)) { - for (auto it = zones.begin(); it != zones.end() && error.length() == 0; ++it) { - // WARNING: Order of values should match the numeric order of P104_OFFSET_* values - zbuffer.clear(); - for (uint8_t i = 0; i < P104_OFFSET_COUNT; ++i) { - if (i == P104_OFFSET_TEXT) { - zbuffer += it->text; - zbuffer += '\x01'; - } else { - int32_t value{}; - if (it->getIntValue(i, value)) { - zbuffer += value; - zbuffer += '\x01'; - } - } - } - - numDevices += (it->size != 0 ? it->size : 1) + it->offset; // Count corrected for newly added zones - - if (saveOffset + zbuffer.length() + (sizeof(bufferSize) * 2) > (DAT_TASKS_CUSTOM_SIZE)) { // Detect ourselves if we've reached the - error.reserve(55); // high-water mark - error += F("Total combination of Zones & text too long to store.\n"); - addLogMove(LOG_LEVEL_ERROR, error); - } else { - // Store length of buffer - bufferSize = zbuffer.length(); - - // As we write in parts, only count as single write. - if (RTC.flashDayCounter > 0) { - RTC.flashDayCounter--; - } - error += SaveToFile(SettingsType::Enum::CustomTaskSettings_Type, - taskIndex, - (uint8_t *)&bufferSize, - sizeof(bufferSize), - saveOffset); - saveOffset += sizeof(bufferSize); - - // As we write in parts, only count as single write. - if (RTC.flashDayCounter > 0) { - RTC.flashDayCounter--; - } - error += SaveToFile(SettingsType::Enum::CustomTaskSettings_Type, - taskIndex, - (uint8_t *)zbuffer.c_str(), - bufferSize, - saveOffset); - saveOffset += bufferSize; - - # ifdef P104_DEBUG_DEV - - if (loglevelActiveFor(LOG_LEVEL_INFO)) { - addLogMove(LOG_LEVEL_INFO, strformat(F("P104: saveSettings zone: %d bufferSize: %d offset: %d"), - it->zone, bufferSize, saveOffset)); - zbuffer.replace(P104_FIELD_SEP, P104_FIELD_DISP); - addLog(LOG_LEVEL_INFO, zbuffer); - } - # endif // ifdef P104_DEBUG_DEV - } - - delay(0); - } - - // Store an End-of-settings marker == 0 - bufferSize = 0u; - - // This write is counting - SaveToFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)&bufferSize, sizeof(bufferSize), saveOffset); - - if (numDevices > 255) { - error += strformat(F("More than 255 modules configured (%u)\n"), numDevices); - } - } else { - addLog(LOG_LEVEL_ERROR, F("DOTMATRIX: Can't allocate string for saving settings, insufficient memory!")); - return false; // Don't continue - } - - return error.isEmpty(); -} - -/************************************************************** -* webform_load -**************************************************************/ -bool P104_data_struct::webform_load(struct EventStruct *event) { - { // Hardware types - # define P104_hardwareTypeCount 8 - const __FlashStringHelper *hardwareTypes[P104_hardwareTypeCount] = { - F("Generic (DR:0, CR:1, RR:0)"), // 010 - F("Parola (DR:1, CR:1, RR:0)"), // 110 - F("FC16 (DR:1, CR:0, RR:0)"), // 100 - F("IC Station (DR:1, CR:1, RR:1)"), // 111 - F("Other 1 (DR:0, CR:0, RR:0)"), // 000 - F("Other 2 (DR:0, CR:0, RR:1)"), // 001 - F("Other 3 (DR:0, CR:1, RR:1)"), // 011 - F("Other 4 (DR:1, CR:0, RR:1)") // 101 - }; - constexpr int hardwareOptions[P104_hardwareTypeCount] = { - static_cast(MD_MAX72XX::moduleType_t::GENERIC_HW), - static_cast(MD_MAX72XX::moduleType_t::PAROLA_HW), - static_cast(MD_MAX72XX::moduleType_t::FC16_HW), - static_cast(MD_MAX72XX::moduleType_t::ICSTATION_HW), - static_cast(MD_MAX72XX::moduleType_t::DR0CR0RR0_HW), - static_cast(MD_MAX72XX::moduleType_t::DR0CR0RR1_HW), - static_cast(MD_MAX72XX::moduleType_t::DR0CR1RR1_HW), - static_cast(MD_MAX72XX::moduleType_t::DR1CR0RR1_HW) - }; - addFormSelector(F("Hardware type"), - F("hardware"), - P104_hardwareTypeCount, - hardwareTypes, - hardwareOptions, - P104_CONFIG_HARDWARETYPE); - # ifdef P104_ADD_SETTINGS_NOTES - addFormNote(F("DR = Digits as Rows, CR = Column Reversed, RR = Row Reversed; 0 = no, 1 = yes.")); - # endif // ifdef P104_ADD_SETTINGS_NOTES - } - - { - addFormCheckBox(F("Clear display on disable"), F("clrdsp"), - bitRead(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_CLEAR_DISABLE)); - - addFormCheckBox(F("Log all displayed text (info)"), - F("logtxt"), - bitRead(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_LOG_ALL_TEXT)); - } - - # ifdef P104_USE_DATETIME_OPTIONS - { - addFormSubHeader(F("Content options")); - - addFormCheckBox(F("Clock with flashing colon"), F("clkflash"), !bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FLASH)); - addFormCheckBox(F("Clock 12h display"), F("clk12h"), bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_12H)); - addFormCheckBox(F("Clock 12h AM/PM indicator"), F("clkampm"), bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_AMPM)); - } - { // Date format - const __FlashStringHelper *dateFormats[] = { - F("Day Month [Year]"), - F("Month Day [Year] (US-style)"), - F("[Year] Month Day (Japanese-style)") - }; - constexpr int dateFormatOptions[] = { - P104_DATE_FORMAT_EU, - P104_DATE_FORMAT_US, - P104_DATE_FORMAT_JP - }; - addFormSelector(F("Date format"), F("datefmt"), - 3, - dateFormats, dateFormatOptions, - get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FORMAT)); - } - { // Date separator - const __FlashStringHelper *dateSeparators[] = { - F("Space"), - F("Slash /"), - F("Dash -"), - F("Dot .") - }; - constexpr int dateSeparatorOptions[] = { - P104_DATE_SEPARATOR_SPACE, - P104_DATE_SEPARATOR_SLASH, - P104_DATE_SEPARATOR_DASH, - P104_DATE_SEPARATOR_DOT - }; - addFormSelector(F("Date separator"), F("datesep"), - 4, - dateSeparators, dateSeparatorOptions, - get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_SEP_CHAR)); - - addFormCheckBox(F("Year uses 4 digits"), F("year4dgt"), bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_YEAR4DGT)); - } - # endif // ifdef P104_USE_DATETIME_OPTIONS - - addFormSubHeader(F("Zones")); - - { // Zones - String zonesList[P104_MAX_ZONES]; - int zonesOptions[P104_MAX_ZONES]; - - for (uint8_t i = 0; i < P104_MAX_ZONES; i++) { - zonesList[i] = i + 1; - zonesOptions[i] = i + 1; // No 0 needed or wanted - } - # if defined(P104_USE_TOOLTIPS) || defined(P104_ADD_SETTINGS_NOTES) - - const String zonetip = F("Select between 1 and " STRINGIFY(P104_MAX_ZONES) " zones, changing" - # ifdef P104_USE_ZONE_ORDERING - " Zones or Zone order" - # endif // ifdef P104_USE_ZONE_ORDERING - " will save and reload the page."); - # endif // if defined(P104_USE_TOOLTIPS) || defined(P104_ADD_SETTINGS_NOTES) - addFormSelector(F("Zones"), F("zonecnt"), P104_MAX_ZONES, zonesList, zonesOptions, nullptr, P104_CONFIG_ZONE_COUNT, true - # ifdef P104_USE_TOOLTIPS - , zonetip - # endif // ifdef P104_USE_TOOLTIPS - ); - - # ifdef P104_USE_ZONE_ORDERING - const String orderTypes[] = { - F("Numeric order (1..n)"), - F("Display order (n..1)") - }; - const int orderOptions[] = { 0, 1 }; - addFormSelector(F("Zone order"), F("zoneorder"), 2, orderTypes, orderOptions, nullptr, - bitRead(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_ZONE_ORDER) ? 1 : 0, true - # ifdef P104_USE_TOOLTIPS - , zonetip - # endif // ifdef P104_USE_TOOLTIPS - ); - # endif // ifdef P104_USE_ZONE_ORDERING - # ifdef P104_ADD_SETTINGS_NOTES - addFormNote(zonetip); - # endif // ifdef P104_ADD_SETTINGS_NOTES - } - expectedZones = P104_CONFIG_ZONE_COUNT; - - if (expectedZones == 0) { expectedZones++; } // Minimum of 1 zone - - { // Optionlists and zones table - const __FlashStringHelper *alignmentTypes[3] = { - F("Left"), - F("Center"), - F("Right") - }; - const int alignmentOptions[3] = { - static_cast(textPosition_t::PA_LEFT), - static_cast(textPosition_t::PA_CENTER), - static_cast(textPosition_t::PA_RIGHT) - }; - - - // Append the numeric value as a reference for the 'anim.in' and 'anim.out' subcommands - const __FlashStringHelper *animationTypes[] { - F("None (0)") - , F("Print (1)") - , F("Scroll up (2)") - , F("Scroll down (3)") - , F("Scroll left * (4)") - , F("Scroll right * (5)") - # if ENA_SPRITE - , F("Sprite (6)") - # endif // ENA_SPRITE - # if ENA_MISC - , F("Slice * (7)") - , F("Mesh (8)") - , F("Fade (9)") - , F("Dissolve (10)") - , F("Blinds (11)") - , F("Random (12)") - # endif // ENA_MISC - # if ENA_WIPE - , F("Wipe (13)") - , F("Wipe w. cursor (14)") - # endif // ENA_WIPE - # if ENA_SCAN - , F("Scan horiz. (15)") - , F("Scan horiz. cursor (16)") - , F("Scan vert. (17)") - , F("Scan vert. cursor (18)") - # endif // ENA_SCAN - # if ENA_OPNCLS - , F("Opening (19)") - , F("Opening w. cursor (20)") - , F("Closing (21)") - , F("Closing w. cursor (22)") - # endif // ENA_OPNCLS - # if ENA_SCR_DIA - , F("Scroll up left * (23)") - , F("Scroll up right * (24)") - , F("Scroll down left * (25)") - , F("Scroll down right * (26)") - # endif // ENA_SCR_DIA - # if ENA_GROW - , F("Grow up (27)") - , F("Grow down (28)") - # endif // ENA_GROW - }; - - const int animationOptions[] = { - static_cast(textEffect_t::PA_NO_EFFECT) - , static_cast(textEffect_t::PA_PRINT) - , static_cast(textEffect_t::PA_SCROLL_UP) - , static_cast(textEffect_t::PA_SCROLL_DOWN) - , static_cast(textEffect_t::PA_SCROLL_LEFT) - , static_cast(textEffect_t::PA_SCROLL_RIGHT) - # if ENA_SPRITE - , static_cast(textEffect_t::PA_SPRITE) - # endif // ENA_SPRITE - # if ENA_MISC - , static_cast(textEffect_t::PA_SLICE) - , static_cast(textEffect_t::PA_MESH) - , static_cast(textEffect_t::PA_FADE) - , static_cast(textEffect_t::PA_DISSOLVE) - , static_cast(textEffect_t::PA_BLINDS) - , static_cast(textEffect_t::PA_RANDOM) - # endif // ENA_MISC - # if ENA_WIPE - , static_cast(textEffect_t::PA_WIPE) - , static_cast(textEffect_t::PA_WIPE_CURSOR) - # endif // ENA_WIPE - # if ENA_SCAN - , static_cast(textEffect_t::PA_SCAN_HORIZ) - , static_cast(textEffect_t::PA_SCAN_HORIZX) - , static_cast(textEffect_t::PA_SCAN_VERT) - , static_cast(textEffect_t::PA_SCAN_VERTX) - # endif // ENA_SCAN - # if ENA_OPNCLS - , static_cast(textEffect_t::PA_OPENING) - , static_cast(textEffect_t::PA_OPENING_CURSOR) - , static_cast(textEffect_t::PA_CLOSING) - , static_cast(textEffect_t::PA_CLOSING_CURSOR) - # endif // ENA_OPNCLS - # if ENA_SCR_DIA - , static_cast(textEffect_t::PA_SCROLL_UP_LEFT) - , static_cast(textEffect_t::PA_SCROLL_UP_RIGHT) - , static_cast(textEffect_t::PA_SCROLL_DOWN_LEFT) - , static_cast(textEffect_t::PA_SCROLL_DOWN_RIGHT) - # endif // ENA_SCR_DIA - # if ENA_GROW - , static_cast(textEffect_t::PA_GROW_UP) - , static_cast(textEffect_t::PA_GROW_DOWN) - # endif // ENA_GROW - }; - - constexpr int animationCount = NR_ELEMENTS(animationOptions); - - delay(0); - - const __FlashStringHelper *fontTypes[] = { - F("Default (0)") - # ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT - , F("Numeric, double height (1)") - # endif // ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT - # ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT - , F("Full, double height (2)") - # endif // ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT - # ifdef P104_USE_VERTICAL_FONT - , F("Vertical (3)") - # endif // ifdef P104_USE_VERTICAL_FONT - # ifdef P104_USE_EXT_ASCII_FONT - , F("Extended ASCII (4)") - # endif // ifdef P104_USE_EXT_ASCII_FONT - # ifdef P104_USE_ARABIC_FONT - , F("Arabic (5)") - # endif // ifdef P104_USE_ARABIC_FONT - # ifdef P104_USE_GREEK_FONT - , F("Greek (6)") - # endif // ifdef P104_USE_GREEK_FONT - # ifdef P104_USE_KATAKANA_FONT - , F("Katakana (7)") - # endif // ifdef P104_USE_KATAKANA_FONT - }; - const int fontOptions[] = { - P104_DEFAULT_FONT_ID - # ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT - , P104_DOUBLE_HEIGHT_FONT_ID - # endif // ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT - # ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT - , P104_FULL_DOUBLEHEIGHT_FONT_ID - # endif // ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT - # ifdef P104_USE_VERTICAL_FONT - , P104_VERTICAL_FONT_ID - # endif // ifdef P104_USE_VERTICAL_FONT - # ifdef P104_USE_EXT_ASCII_FONT - , P104_EXT_ASCII_FONT_ID - # endif // ifdef P104_USE_EXT_ASCII_FONT - # ifdef P104_USE_ARABIC_FONT - , P104_ARABIC_FONT_ID - # endif // ifdef P104_USE_ARABIC_FONT - # ifdef P104_USE_GREEK_FONT - , P104_GREEK_FONT_ID - # endif // ifdef P104_USE_GREEK_FONT - # ifdef P104_USE_KATAKANA_FONT - , P104_KATAKANA_FONT_ID - # endif // ifdef P104_USE_KATAKANA_FONT - }; - - const __FlashStringHelper *layoutTypes[] = { - F("Standard") - # if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) - , F("Double, upper") - , F("Double, lower") - # endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) - }; - const int layoutOptions[] = { - P104_LAYOUT_STANDARD - # if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) - , P104_LAYOUT_DOUBLE_UPPER - , P104_LAYOUT_DOUBLE_LOWER - # endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) - }; - - const __FlashStringHelper *specialEffectTypes[] = { - F("None"), - F("Flip up/down"), - F("Flip left/right *"), - F("Flip u/d & l/r *") - }; - const int specialEffectOptions[] = { - P104_SPECIAL_EFFECT_NONE, - P104_SPECIAL_EFFECT_UP_DOWN, - P104_SPECIAL_EFFECT_LEFT_RIGHT, - P104_SPECIAL_EFFECT_BOTH - }; - - const __FlashStringHelper *contentTypes[] = { - F("Text"), - F("Text reverse"), - F("Clock (4 mod)"), - F("Clock sec (6 mod)"), - F("Date (4 mod)"), - F("Date yr (6/7 mod)"), - F("Date/time (9/13 mod)"), - # ifdef P104_USE_BAR_GRAPH - F("Bar graph"), - # endif // ifdef P104_USE_BAR_GRAPH - }; - const int contentOptions[] { - P104_CONTENT_TEXT, - P104_CONTENT_TEXT_REV, - P104_CONTENT_TIME, - P104_CONTENT_TIME_SEC, - P104_CONTENT_DATE4, - P104_CONTENT_DATE6, - P104_CONTENT_DATE_TIME, - # ifdef P104_USE_BAR_GRAPH - P104_CONTENT_BAR_GRAPH, - # endif // ifdef P104_USE_BAR_GRAPH - }; - const __FlashStringHelper *invertedTypes[3] = { - F("Normal"), - F("Inverted") - }; - const int invertedOptions[] = { - 0, - 1 - }; - # ifdef P104_USE_ZONE_ACTIONS - uint8_t actionCount = 0; - const __FlashStringHelper *actionTypes[4]; - int actionOptions[4]; - actionTypes[actionCount] = F("None"); - actionOptions[actionCount] = P104_ACTION_NONE; - actionCount++; - - if (zones.size() < P104_MAX_ZONES) { - actionTypes[actionCount] = F("New above"); - actionOptions[actionCount] = P104_ACTION_ADD_ABOVE; - actionCount++; - actionTypes[actionCount] = F("New below"); - actionOptions[actionCount] = P104_ACTION_ADD_BELOW; - actionCount++; - } - actionTypes[actionCount] = F("Delete"); - actionOptions[actionCount] = P104_ACTION_DELETE; - actionCount++; - # endif // ifdef P104_USE_ZONE_ACTIONS - - delay(0); - - addFormSubHeader(F("Zone configuration")); - - { - html_table(EMPTY_STRING); // Sub-table - - const __FlashStringHelper *headers[] = { - F("Zone # "), - F("Modules"), - F("Text"), - F("Content"), - F("Alignment"), - F("Animation In/Out"), // 1st and 2nd row title - F("Speed/Pause"), // 1st and 2nd row title - F("Font/Layout"), // 1st and 2nd row title - F("Inverted/ Special Effects"), // 1st and 2nd row title - F("Offset"), - F("Brightness"), - F("Repeat (sec)") - }; - - constexpr unsigned nrHeaders = NR_ELEMENTS(headers); - for (unsigned i = 0; i < nrHeaders; ++i) { - int width = 0; - if (i == 2) { - // "Text" needs a width - width = 180; - } - html_table_header(headers[i], width); - } - # ifdef P104_USE_ZONE_ACTIONS - html_table_header(F(""), 15); // Spacer - html_table_header(F("Action"), 45); - # endif // ifdef P104_USE_ZONE_ACTIONS - } - - uint16_t index; - int16_t startZone, endZone; - int8_t incrZone = 1; - # ifdef P104_USE_ZONE_ACTIONS - uint8_t currentRow = 0; - # endif // ifdef P104_USE_ZONE_ACTIONS - - # ifdef P104_USE_ZONE_ORDERING - - if (bitRead(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_ZONE_ORDER)) { - startZone = zones.size() - 1; - endZone = -1; - incrZone = -1; - } else - # endif // ifdef P104_USE_ZONE_ORDERING - { - startZone = 0; - endZone = zones.size(); - } - - for (int8_t zone = startZone; zone != endZone; zone += incrZone) { - if (zones[zone].zone <= expectedZones) { - index = (zones[zone].zone - 1) * P104_OFFSET_COUNT; - - html_TR_TD(); // All columns use max. width available - addHtml(F(" ")); - addHtmlInt(zones[zone].zone); - - html_TD(); // Modules - addNumericBox(getPluginCustomArgName(index + P104_OFFSET_SIZE), zones[zone].size, 1, P104_MAX_MODULES_PER_ZONE); - - html_TD(); // Text - addTextBox(getPluginCustomArgName(index + P104_OFFSET_TEXT), - zones[zone].text, - P104_MAX_TEXT_LENGTH_PER_ZONE, - false, - false, - EMPTY_STRING, - F("")); - - html_TD(); // Content - addSelector(getPluginCustomArgName(index + P104_OFFSET_CONTENT), - P104_CONTENT_count, - contentTypes, - contentOptions, - nullptr, - zones[zone].content, - false, - true, - F("")); - - html_TD(); // Alignment - addSelector(getPluginCustomArgName(index + P104_OFFSET_ALIGNMENT), - 3, - alignmentTypes, - alignmentOptions, - nullptr, - zones[zone].alignment, - false, - true, - F("")); - - { - html_TD(); // Animation In (without None by passing the second element index) - addSelector(getPluginCustomArgName(index + P104_OFFSET_ANIM_IN), - animationCount - 1, - &animationTypes[1], - &animationOptions[1], - nullptr, - zones[zone].animationIn, - false, - true, - F("") - # ifdef P104_USE_TOOLTIPS - , F("Animation In") - # endif // ifdef P104_USE_TOOLTIPS - ); - } - - html_TD(); // Speed In - addNumericBox(getPluginCustomArgName(index + P104_OFFSET_SPEED), zones[zone].speed, 0, P104_MAX_SPEED_PAUSE_VALUE - # ifdef P104_USE_TOOLTIPS - , F("") // classname - , F("Speed") // title - # endif // ifdef P104_USE_TOOLTIPS - ); - - html_TD(); // Font - addSelector(getPluginCustomArgName(index + P104_OFFSET_FONT), - NR_ELEMENTS(fontOptions), - fontTypes, - fontOptions, - nullptr, - zones[zone].font, - false, - true, - F("") - # ifdef P104_USE_TOOLTIPS - , F("Font") // title - # endif // ifdef P104_USE_TOOLTIPS - ); - - html_TD(); // Inverted - addSelector(getPluginCustomArgName(index + P104_OFFSET_INVERTED), - NR_ELEMENTS(invertedOptions), - invertedTypes, - invertedOptions, - nullptr, - zones[zone].inverted, - false, - true, - F("") - # ifdef P104_USE_TOOLTIPS - , F("Inverted") // title - # endif // ifdef P104_USE_TOOLTIPS - ); - - html_TD(3); // Fill columns - # ifdef P104_USE_ZONE_ACTIONS - - html_TD(); // Spacer - addHtml('|'); - - if (currentRow < 2) { - addHtml(F("")); // Action column, text centered and font-size 90% - } else { - html_TD(); - } - - if (currentRow == 0) { - addHtml(F("(applied immediately!)")); - } else if (currentRow == 1) { - addHtml(F("(Delete can't be undone!)")); - } - currentRow++; - # endif // ifdef P104_USE_ZONE_ACTIONS - - // Split here - html_TR_TD(); // Start new row - html_TD(4); // Start with some blank columns - - { - html_TD(); // Animation Out - addSelector(getPluginCustomArgName(index + P104_OFFSET_ANIM_OUT), - animationCount, - animationTypes, - animationOptions, - nullptr, - zones[zone].animationOut, - false, - true, - F("") - # ifdef P104_USE_TOOLTIPS - , F("Animation Out") - # endif // ifdef P104_USE_TOOLTIPS - ); - } - - html_TD(); // Pause after Animation In - addNumericBox(getPluginCustomArgName(index + P104_OFFSET_PAUSE), zones[zone].pause, 0, P104_MAX_SPEED_PAUSE_VALUE - # ifdef P104_USE_TOOLTIPS - , F("") // classname - , F("Pause") // title - # endif // ifdef P104_USE_TOOLTIPS - ); - - html_TD(); // Layout - addSelector(getPluginCustomArgName(index + P104_OFFSET_LAYOUT), - NR_ELEMENTS(layoutOptions), - layoutTypes, - layoutOptions, - nullptr, - zones[zone].layout, - false, - true, - F("") - # ifdef P104_USE_TOOLTIPS - , F("Layout") // title - # endif // ifdef P104_USE_TOOLTIPS - ); - - html_TD(); // Special effects - addSelector(getPluginCustomArgName(index + P104_OFFSET_SPEC_EFFECT), - NR_ELEMENTS(specialEffectOptions), - specialEffectTypes, - specialEffectOptions, - nullptr, - zones[zone].specialEffect, - false, - true, - F("") - # ifdef P104_USE_TOOLTIPS - , F("Special Effects") // title - # endif // ifdef P104_USE_TOOLTIPS - ); - - html_TD(); // Offset - addNumericBox(getPluginCustomArgName(index + P104_OFFSET_OFFSET), zones[zone].offset, 0, 254); - - html_TD(); // Brightness - - if (zones[zone].brightness == -1) { zones[zone].brightness = P104_BRIGHTNESS_DEFAULT; } - addNumericBox(getPluginCustomArgName(index + P104_OFFSET_BRIGHTNESS), zones[zone].brightness, 0, P104_BRIGHTNESS_MAX); - - html_TD(); // Repeat (sec) - addNumericBox(getPluginCustomArgName(index + P104_OFFSET_REPEATDELAY), - zones[zone].repeatDelay, - -1, - P104_MAX_REPEATDELAY_VALUE // max delay 86400 sec. = 24 hours - # ifdef P104_USE_TOOLTIPS - , F("") // classname - , F("Repeat after this delay (sec), -1 = off") // tooltip - # endif // ifdef P104_USE_TOOLTIPS - ); - - # ifdef P104_USE_ZONE_ACTIONS - html_TD(); // Spacer - addHtml('|'); - - html_TD(); // Action - addSelector(getPluginCustomArgName(index + P104_OFFSET_ACTION), - actionCount, - actionTypes, - actionOptions, - nullptr, - P104_ACTION_NONE, // Always start with None - true, - true, - F("")); - # endif // ifdef P104_USE_ZONE_ACTIONS - - delay(0); - } - } - html_end_table(); - } - - # ifdef P104_ADD_SETTINGS_NOTES - addFormNote(concat(F("- Maximum nr. of modules possible (Zones * Size + Offset) = 255. Last saved: "), numDevices)); - addFormNote(F("- 'Animation In' or 'Animation Out' and 'Special Effects' marked with * should not be combined in a Zone.")); - # if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) && !defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) - addFormNote(F("- 'Layout' 'Double upper' and 'Double lower' are only supported for numeric 'Content' types like 'Clock' and 'Date'.")); - # endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) && !defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) - # endif // ifdef P104_ADD_SETTINGS_NOTES - - return true; -} - -/************************************************************** -* webform_save -**************************************************************/ -bool P104_data_struct::webform_save(struct EventStruct *event) { - P104_CONFIG_ZONE_COUNT = getFormItemInt(F("zonecnt")); - P104_CONFIG_HARDWARETYPE = getFormItemInt(F("hardware")); - - bitWrite(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_CLEAR_DISABLE, isFormItemChecked(F("clrdsp"))); - bitWrite(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_LOG_ALL_TEXT, isFormItemChecked(F("logtxt"))); - - # ifdef P104_USE_ZONE_ORDERING - zoneOrder = getFormItemInt(F("zoneorder")); // Is used in saveSettings() - bitWrite(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_ZONE_ORDER, zoneOrder == 1); - # endif // ifdef P104_USE_ZONE_ORDERING - - # ifdef P104_USE_DATETIME_OPTIONS - uint32_t ulDateTime = 0; - bitWrite(ulDateTime, P104_CONFIG_DATETIME_FLASH, !isFormItemChecked(F("clkflash"))); // Inverted flag - bitWrite(ulDateTime, P104_CONFIG_DATETIME_12H, isFormItemChecked(F("clk12h"))); - bitWrite(ulDateTime, P104_CONFIG_DATETIME_AMPM, isFormItemChecked(F("clkampm"))); - bitWrite(ulDateTime, P104_CONFIG_DATETIME_YEAR4DGT, isFormItemChecked(F("year4dgt"))); - set4BitToUL(ulDateTime, P104_CONFIG_DATETIME_FORMAT, getFormItemInt(F("datefmt"))); - set4BitToUL(ulDateTime, P104_CONFIG_DATETIME_SEP_CHAR, getFormItemInt(F("datesep"))); - P104_CONFIG_DATETIME = ulDateTime; - # endif // ifdef P104_USE_DATETIME_OPTIONS - - previousZones = expectedZones; - expectedZones = P104_CONFIG_ZONE_COUNT; - - bool result = saveSettings(); // Determines numDevices and re-fills zones list - - P104_CONFIG_ZONE_COUNT = zones.size(); - P104_CONFIG_TOTAL_UNITS = numDevices; // Store counted number of devices - - zones.clear(); // Free some memory (temporarily) - - return result; -} - - - - -P104_zone_struct::P104_zone_struct(uint8_t _zone) - : text(F("\"\"")), zone(_zone) {} - - -bool P104_zone_struct::getIntValue(uint8_t offset, int32_t& value) const -{ - switch (offset) { - case P104_OFFSET_SIZE: value = size; break; - case P104_OFFSET_TEXT: return false; - case P104_OFFSET_CONTENT: value = content; break; - case P104_OFFSET_ALIGNMENT: value = alignment; break; - case P104_OFFSET_ANIM_IN: value = animationIn; break; - case P104_OFFSET_SPEED: value = speed; break; - case P104_OFFSET_ANIM_OUT: value = animationOut; break; - case P104_OFFSET_PAUSE: value = pause; break; - case P104_OFFSET_FONT: value = font; break; - case P104_OFFSET_LAYOUT: value = layout; break; - case P104_OFFSET_SPEC_EFFECT: value = specialEffect; break; - case P104_OFFSET_OFFSET: value = offset; break; - case P104_OFFSET_BRIGHTNESS: value = brightness; break; - case P104_OFFSET_REPEATDELAY: value = repeatDelay; break; - case P104_OFFSET_INVERTED: value = inverted; break; - - default: - return false; - } - return true; -} - -bool P104_zone_struct::setIntValue(uint8_t offset, int32_t value) -{ - switch (offset) { - case P104_OFFSET_SIZE: size = value; break; - case P104_OFFSET_TEXT: return false; - case P104_OFFSET_CONTENT: content = value; break; - case P104_OFFSET_ALIGNMENT: alignment = value; break; - case P104_OFFSET_ANIM_IN: animationIn = value; break; - case P104_OFFSET_SPEED: speed = value; break; - case P104_OFFSET_ANIM_OUT: animationOut = value; break; - case P104_OFFSET_PAUSE: pause = value; break; - case P104_OFFSET_FONT: font = value; break; - case P104_OFFSET_LAYOUT: layout = value; break; - case P104_OFFSET_SPEC_EFFECT: specialEffect = value; break; - case P104_OFFSET_OFFSET: offset = value; break; - case P104_OFFSET_BRIGHTNESS: brightness = value; break; - case P104_OFFSET_REPEATDELAY: repeatDelay = value; break; - case P104_OFFSET_INVERTED: inverted = value; break; - - default: - return false; - } - return true; -} - - -#endif // ifdef USES_P104 +#include "../PluginStructs/P104_data_struct.h" + +#ifdef USES_P104 + +# include "../Helpers/ESPEasy_Storage.h" +# include "../Helpers/Numerical.h" +# include "../WebServer/Markup_Forms.h" +# include "../WebServer/ESPEasy_WebServer.h" +# include "../WebServer/Markup.h" +# include "../WebServer/HTML_wrappers.h" +# include "../ESPEasyCore/ESPEasyRules.h" +# include "../Globals/ESPEasy_time.h" +# include "../Globals/RTC.h" + +# include +# include +# include + +// Needed also here for PlatformIO's library finder as the .h file +// is in a directory which is excluded in the src_filter + +# if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) +void createHString(String& string); // Forward definition +# endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) +void reverseStr(String& str); // Forward definition + +/**************************************************************** + * Constructor + ***************************************************************/ +P104_data_struct::P104_data_struct(MD_MAX72XX::moduleType_t _mod, + taskIndex_t _taskIndex, + int8_t _cs_pin, + uint8_t _modules, + uint8_t _zonesCount) + : mod(_mod), taskIndex(_taskIndex), cs_pin(_cs_pin), modules(_modules), expectedZones(_zonesCount) { + if (Settings.isSPI_valid()) { + P = new (std::nothrow) MD_Parola(mod, cs_pin, modules); + } else { + addLog(LOG_LEVEL_ERROR, F("DOTMATRIX: Required SPI not enabled. Initialization aborted!")); + } +} + +/******************************* + * Destructor + ******************************/ +P104_data_struct::~P104_data_struct() { + # if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) + + if (nullptr != pM) { + pM = nullptr; // Not created here, only reset + } + # endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) + + if (nullptr != P) { + // P->~MD_Parola(); // Call destructor directly, as delete of the object fails miserably + // do not: delete P; // Warning: the MD_Parola object doesn't have a virtual destructor, and when changed, + // a reboot uccurs when the object is deleted here! + P = nullptr; // Reset only + } +} + +/******************************* + * Initializer/starter + ******************************/ +bool P104_data_struct::begin() { + if (!initialized) { + loadSettings(); + initialized = true; + } + + if ((P != nullptr) && validGpio(cs_pin)) { + # ifdef P104_DEBUG + addLog(LOG_LEVEL_INFO, F("dotmatrix: begin() called")); + # endif // ifdef P104_DEBUG + P->begin(expectedZones); + # if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) + pM = P->getGraphicObject(); + # endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) + return true; + } + return false; +} + +# define P104_ZONE_SEP '\x02' +# define P104_FIELD_SEP '\x01' +# define P104_ZONE_DISP ';' +# define P104_FIELD_DISP ',' + +# define P104_CONFIG_VERSION_V2 0xF000 // Marker in first uint16_t to to indicate second version config settings, anything else if first + // version. + // Any third version or later could use 0xE000, etc. The 'version' is stored in the first uint16_t + // stored in the custom settings + +/* + Settings layout: + Version 1: + - uint16_t : size of the next blob holding all settings + - char[x] : Blob with settings, with csv-like strings, using P104_FIELD_SEP and P104_ZONE_SEP separators + Version 2: + - uint16_t : marker with content P104_CONFIG_VERSION_V2 + - uint16_t : size of next blob holding 1 zone settings string + - char[y] : Blob holding 1 zone settings string, with csv like string, using P104_FIELD_SEP separators + - uint16_t : next size, if 0 then no more blobs + - char[x] : Blob + - ... + - Max. allowed total custom settings size = 1024 + */ +/************************************** + * loadSettings + *************************************/ +void P104_data_struct::loadSettings() { + uint16_t bufferSize; + char *settingsBuffer; + + if (taskIndex < TASKS_MAX) { + int loadOffset = 0; + + // Read size of the used buffer, could be the settings-version marker + LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)&bufferSize, sizeof(bufferSize), loadOffset); + bool settingsVersionV2 = (bufferSize == P104_CONFIG_VERSION_V2) || (bufferSize == 0u); + uint16_t structDataSize = 0; + uint16_t reservedBuffer = 0; + + if (!settingsVersionV2) { + reservedBuffer = bufferSize + 1; // just add 1 for storing a string-terminator + addLog(LOG_LEVEL_INFO, F("dotmatrix: Reading Settings V1, will be stored as Settings V2.")); + } else { + reservedBuffer = P104_SETTINGS_BUFFER_V2 + 1; // just add 1 for storing a string-terminator + } + reservedBuffer++; // Add 1 for 0..size use + settingsBuffer = new (std::nothrow) char[reservedBuffer](); // Allocate buffer and reset to all zeroes + loadOffset += sizeof(bufferSize); + + if (settingsVersionV2) { + LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)&bufferSize, sizeof(bufferSize), loadOffset); + loadOffset += sizeof(bufferSize); // Skip the size + } + structDataSize = bufferSize; + # ifdef P104_DEBUG_DEV + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, strformat(F("P104: loadSettings stored Size: %d taskindex: %d"), structDataSize, taskIndex)); + } + # endif // ifdef P104_DEBUG_DEV + + // Read actual data + if (structDataSize > 0) { // Reading 0 bytes logs an error, so lets avoid that + LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)settingsBuffer, structDataSize, loadOffset); + } + settingsBuffer[bufferSize + 1] = '\0'; // Terminate string + + uint8_t zoneIndex = 0; + + { + String buffer(settingsBuffer); + # ifdef P104_DEBUG_DEV + + String log; + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + log = F("P104: loadSettings bufferSize: "); + log += bufferSize; + log += F(" untrimmed: "); + log += buffer.length(); + } + # endif // ifdef P104_DEBUG_DEV + buffer.trim(); + # ifdef P104_DEBUG_DEV + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + log += F(" trimmed: "); + log += buffer.length(); + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // ifdef P104_DEBUG_DEV + + if (zones.size() > 0) { + zones.clear(); + } + zones.reserve(P104_MAX_ZONES); + numDevices = 0; + + String tmp; + String fld; + int32_t tmp_int; + uint16_t prev2 = 0; + int16_t offset2 = buffer.indexOf(P104_ZONE_SEP); + + if ((offset2 == -1) && (buffer.length() > 0)) { + offset2 = buffer.length(); + } + + while (offset2 > -1) { + tmp = buffer.substring(prev2, offset2); + # ifdef P104_DEBUG_DEV + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + log = F("P104: reading string: "); + log += tmp; + log.replace(P104_FIELD_SEP, P104_FIELD_DISP); + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // ifdef P104_DEBUG_DEV + + zones.push_back(P104_zone_struct(zoneIndex + 1)); + + tmp_int = 0; + + // WARNING: Order of parsing these values should match the numeric order of P104_OFFSET_* values + for (uint8_t i = 0; i < P104_OFFSET_COUNT; ++i) { + if (i == P104_OFFSET_TEXT) { + zones[zoneIndex].text = parseStringKeepCaseNoTrim(tmp, 1 + P104_OFFSET_TEXT, P104_FIELD_SEP); + } else { + if (validIntFromString(parseString(tmp, 1 + i, P104_FIELD_SEP), tmp_int)) { + zones[zoneIndex].setIntValue(i, tmp_int); + } + } + } + + delay(0); + + numDevices += zones[zoneIndex].size + zones[zoneIndex].offset; + + if (!settingsVersionV2) { + prev2 = offset2 + 1; + offset2 = buffer.indexOf(P104_ZONE_SEP, prev2); + } else { + loadOffset += bufferSize; + structDataSize = sizeof(bufferSize); + LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)&bufferSize, structDataSize, loadOffset); + offset2 = bufferSize; // Length + + if (bufferSize == 0) { // End of zones reached + offset2 = -1; // fall out of while loop + } else { + structDataSize = bufferSize; + loadOffset += sizeof(bufferSize); + LoadFromFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)settingsBuffer, structDataSize, loadOffset); + settingsBuffer[bufferSize + 1] = '\0'; // Terminate string + buffer = String(settingsBuffer); + } + } + zoneIndex++; + + # ifdef P104_DEBUG + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, concat(F("dotmatrix: parsed zone: "), zoneIndex)); + } + # endif // ifdef P104_DEBUG + } + + buffer = String(); // Free some memory + } + + delete[] settingsBuffer; // Release allocated buffer + # ifdef P104_DEBUG_DEV + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, concat(F("P104: read zones from config: "), zoneIndex)); + } + # endif // ifdef P104_DEBUG_DEV + + if (expectedZones == -1) { expectedZones = zoneIndex; } + + if (expectedZones == 0) { expectedZones++; } // Guarantee at least 1 zone to be displayed + + while (zoneIndex < expectedZones) { + zones.push_back(P104_zone_struct(zoneIndex + 1)); + + if (equals(zones[zoneIndex].text, F("\"\""))) { // Special case + zones[zoneIndex].text.clear(); + } + + zoneIndex++; + delay(0); + } + # ifdef P104_DEBUG_DEV + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, strformat(F("P104: total zones initialized: %d expected: %d"), zoneIndex, expectedZones)); + } + # endif // ifdef P104_DEBUG_DEV + } +} + +/**************************************************** + * configureZones: initialize Zones setup + ***************************************************/ +void P104_data_struct::configureZones() { + if (!initialized) { + loadSettings(); + initialized = true; + } + + uint8_t currentZone = 0; + uint8_t zoneOffset = 0; + + # ifdef P104_DEBUG_DEV + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, concat(F("P104: configureZones to do: "), zones.size())); + } + # endif // ifdef P104_DEBUG_DEV + + if (nullptr == P) { return; } + + P->displayClear(); + + for (auto it = zones.begin(); it != zones.end(); ++it) { + if (it->zone <= expectedZones) { + zoneOffset += it->offset; + P->setZone(currentZone, zoneOffset, zoneOffset + it->size - 1); + # if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) + it->_startModule = zoneOffset; + P->getDisplayExtent(currentZone, it->_lower, it->_upper); + # endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) + zoneOffset += it->size; + + switch (it->font) { + # ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT + case P104_DOUBLE_HEIGHT_FONT_ID: { + P->setFont(currentZone, numeric7SegDouble); + P->setCharSpacing(currentZone, P->getCharSpacing() * 2); // double spacing as well + break; + } + # endif // ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT + # ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT + case P104_FULL_DOUBLEHEIGHT_FONT_ID: { + P->setFont(currentZone, BigFont); + P->setCharSpacing(currentZone, P->getCharSpacing() * 2); // double spacing as well + break; + } + # endif // ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT + # ifdef P104_USE_VERTICAL_FONT + case P104_VERTICAL_FONT_ID: { + P->setFont(currentZone, _fontVertical); + break; + } + # endif // ifdef P104_USE_VERTICAL_FONT + # ifdef P104_USE_EXT_ASCII_FONT + case P104_EXT_ASCII_FONT_ID: { + P->setFont(currentZone, ExtASCII); + break; + } + # endif // ifdef P104_USE_EXT_ASCII_FONT + # ifdef P104_USE_ARABIC_FONT + case P104_ARABIC_FONT_ID: { + P->setFont(currentZone, fontArabic); + break; + } + # endif // ifdef P104_USE_ARABIC_FONT + # ifdef P104_USE_GREEK_FONT + case P104_GREEK_FONT_ID: { + P->setFont(currentZone, fontGreek); + break; + } + # endif // ifdef P104_USE_GREEK_FONT + # ifdef P104_USE_KATAKANA_FONT + case P104_KATAKANA_FONT_ID: { + P->setFont(currentZone, fontKatakana); + break; + } + # endif // ifdef P104_USE_KATAKANA_FONT + + // Extend above this comment with more fonts if/when available, + // case P104_DEFAULT_FONT_ID: and default: clauses should be the last options. + // This should also make sure the default font is set if a no longer available font was selected + case P104_DEFAULT_FONT_ID: + default: { + P->setFont(currentZone, nullptr); // default font + break; + } + } + + // Inverted + P->setInvert(currentZone, it->inverted); + + // Special Effects + P->setZoneEffect(currentZone, (it->specialEffect & P104_SPECIAL_EFFECT_UP_DOWN) == P104_SPECIAL_EFFECT_UP_DOWN, PA_FLIP_UD); + P->setZoneEffect(currentZone, (it->specialEffect & P104_SPECIAL_EFFECT_LEFT_RIGHT) == P104_SPECIAL_EFFECT_LEFT_RIGHT, PA_FLIP_LR); + + // Brightness + P->setIntensity(currentZone, it->brightness); + + # ifdef P104_DEBUG_DEV + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, strformat(F("P104: configureZones #%d/%d offset: %d"), currentZone + 1, expectedZones, zoneOffset)); + } + # endif // ifdef P104_DEBUG_DEV + + delay(0); + + // Content == text && text != "" + if (((it->content == P104_CONTENT_TEXT) || + (it->content == P104_CONTENT_TEXT_REV)) + && (!it->text.isEmpty())) { + displayOneZoneText(currentZone, *it, it->text); + } + + # ifdef P104_USE_BAR_GRAPH + + // Content == Bar-graph && text != "" + if ((it->content == P104_CONTENT_BAR_GRAPH) + && (!it->text.isEmpty())) { + displayBarGraph(currentZone, *it, it->text); + } + # endif // ifdef P104_USE_BAR_GRAPH + + if (it->repeatDelay > -1) { + it->_repeatTimer = millis(); + } + currentZone++; + delay(0); + } + } + + // Synchronize the start + P->synchZoneStart(); +} + +/********************************************************** + * Display the text with attributes for a specific zone + *********************************************************/ +void P104_data_struct::displayOneZoneText(uint8_t zone, + const P104_zone_struct& zstruct, + const String & text) { + if ((nullptr == P) || (zone >= P104_MAX_ZONES)) { return; } // double check + sZoneInitial[zone].reserve(text.length()); + sZoneInitial[zone] = text; // Keep the original string for future use + sZoneBuffers[zone].reserve(text.length()); + sZoneBuffers[zone] = text; // We explicitly want a copy here so it can be modified by parseTemplate() + + sZoneBuffers[zone] = parseTemplate(sZoneBuffers[zone]); + + # if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) + + if (zstruct.layout == P104_LAYOUT_DOUBLE_UPPER) { + createHString(sZoneBuffers[zone]); + } + # endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) + + if (zstruct.content == P104_CONTENT_TEXT_REV) { + reverseStr(sZoneBuffers[zone]); + } + + String log; + + if (loglevelActiveFor(LOG_LEVEL_INFO) && + logAllText && + log.reserve(28 + text.length() + sZoneBuffers[zone].length())) { + log = strformat(F("dotmatrix: ZoneText: %d, '"), zone + 1); // UI-number + log += text; + log += F("' -> '"); + log += sZoneBuffers[zone]; + log += '\''; + addLogMove(LOG_LEVEL_INFO, log); + } + + P->displayZoneText(zone, + sZoneBuffers[zone].c_str(), + static_cast(zstruct.alignment), + zstruct.speed, + zstruct.pause, + static_cast(zstruct.animationIn), + static_cast(zstruct.animationOut)); +} + +/********************************************* + * Update all or the specified zone + ********************************************/ +void P104_data_struct::updateZone(uint8_t zone, + const P104_zone_struct& zstruct) { + if (nullptr == P) { return; } + + if (zone == 0) { + for (auto it = zones.begin(); it != zones.end(); ++it) { + if ((it->zone > 0) && + ((it->content == P104_CONTENT_TEXT) || + (it->content == P104_CONTENT_TEXT_REV))) { + displayOneZoneText(it->zone - 1, *it, sZoneInitial[it->zone - 1]); // Re-send last displayed text + P->displayReset(it->zone - 1); + } + # ifdef P104_USE_BAR_GRAPH + + if ((it->zone > 0) && + (it->content == P104_CONTENT_BAR_GRAPH)) { + displayBarGraph(it->zone - 1, *it, sZoneInitial[it->zone - 1]); // Re-send last displayed bar graph + } + # endif // ifdef P104_USE_BAR_GRAPH + + if ((zstruct.content == P104_CONTENT_TEXT) + || zstruct.content == P104_CONTENT_TEXT_REV + # ifdef P104_USE_BAR_GRAPH + || zstruct.content == P104_CONTENT_BAR_GRAPH + # endif // ifdef P104_USE_BAR_GRAPH + ) { + if (it->repeatDelay > -1) { // Restart repeat timer + it->_repeatTimer = millis(); + } + } + } + } else { + if ((zstruct.zone > 0) && + ((zstruct.content == P104_CONTENT_TEXT) || + (zstruct.content == P104_CONTENT_TEXT_REV))) { + displayOneZoneText(zstruct.zone - 1, zstruct, sZoneInitial[zstruct.zone - 1]); // Re-send last displayed text + P->displayReset(zstruct.zone - 1); + } + # ifdef P104_USE_BAR_GRAPH + + if ((zstruct.zone > 0) && + (zstruct.content == P104_CONTENT_BAR_GRAPH)) { + displayBarGraph(zstruct.zone - 1, zstruct, sZoneInitial[zstruct.zone - 1]); // Re-send last displayed bar graph + } + # endif // ifdef P104_USE_BAR_GRAPH + + // Repeat timer is/should be started elsewhere + } +} + +# if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) + +/*********************************************** + * Enable/Disable updating a range of modules + **********************************************/ +void P104_data_struct::modulesOnOff(uint8_t start, uint8_t end, MD_MAX72XX::controlValue_t on_off) { + for (uint8_t m = start; m <= end; m++) { + pM->control(m, MD_MAX72XX::UPDATE, on_off); + } +} + +# endif // if defined(P104_USE_BAR_GRAPH) || defined(P104_USE_DOT_SET) + +# ifdef P104_USE_BAR_GRAPH + +/******************************************************** + * draw a single bar-graph, arguments already adjusted for direction + *******************************************************/ +void P104_data_struct::drawOneBarGraph(uint16_t lower, + uint16_t upper, + int16_t pixBottom, + int16_t pixTop, + uint16_t zeroPoint, + uint8_t barWidth, + uint8_t barType, + uint8_t row) { + bool on_off; + + for (uint8_t r = 0; r < barWidth; r++) { + for (uint8_t col = lower; col <= upper; col++) { + on_off = (col >= pixBottom && col <= pixTop); // valid area + + if ((zeroPoint != 0) && + (barType == P104_BARTYPE_STANDARD) && + (barWidth > 2) && + ((r == 0) || (r == barWidth - 1)) && + (col == lower + zeroPoint)) { + on_off = false; // when bar wider than 2, turn off zeropoint top and bottom led + } + + if ((barType == P104_BARTYPE_SINGLE) && (r > 0)) { + on_off = false; // barType 1 = only a single line is drawn, independent of the width + } + + if ((barType == P104_BARTYPE_ALT_DOT) && (barWidth > 1) && on_off) { + on_off = ((r % 2) == (col % 2)); // barType 2 = dotted line when bar is wider than 1 pixel + } + pM->setPoint(row + r, col, on_off); + + if (col % 16 == 0) { delay(0); } + } + delay(0); // Leave some breathingroom + } +} + +/******************************************************************** + * Process a graph-string to display in a zone, format: + * value,max-value,min-value,direction,bartype|... + *******************************************************************/ +void P104_data_struct::displayBarGraph(uint8_t zone, + const P104_zone_struct& zstruct, + const String & graph) { + if ((nullptr == P) || (nullptr == pM) || graph.isEmpty()) { return; } + sZoneInitial[zone] = graph; // Keep the original string for future use + + # define NOT_A_COMMA 0x02 // Something else than a comma, or the parseString function will get confused + String parsedGraph(graph); // Extra copy created so we don't mess up the incoming String + parsedGraph = parseTemplate(parsedGraph); + parsedGraph.replace(',', NOT_A_COMMA); + + std::vector barGraphs; + uint8_t currentBar = 0; + bool loop = true; + + // Parse the graph-string + while (loop && currentBar < 8) { // Maximum 8 valuesets possible + String graphpart = parseString(parsedGraph, currentBar + 1, '|'); + graphpart.trim(); + graphpart.replace(NOT_A_COMMA, ','); + + if (graphpart.isEmpty()) { + loop = false; + } else { + barGraphs.push_back(P104_bargraph_struct(currentBar)); + } + + if (loop && validDoubleFromString(parseString(graphpart, 1), barGraphs[currentBar].value)) { // value + String datapart = parseString(graphpart, 2); // max (default: 100.0) + + if (datapart.isEmpty()) { + barGraphs[currentBar].max = 100.0; + } else { + validDoubleFromString(datapart, barGraphs[currentBar].max); + } + datapart = parseString(graphpart, 3); // min (default: 0.0) + + if (datapart.isEmpty()) { + barGraphs[currentBar].min = 0.0; + } else { + validDoubleFromString(datapart, barGraphs[currentBar].min); + } + datapart = parseString(graphpart, 4); // direction + + if (datapart.isEmpty()) { + barGraphs[currentBar].direction = 0; + } else { + int32_t value = 0; + validIntFromString(datapart, value); + barGraphs[currentBar].direction = value; + } + datapart = parseString(graphpart, 5); // barType + + if (datapart.isEmpty()) { + barGraphs[currentBar].barType = 0; + } else { + int32_t value = 0; + validIntFromString(datapart, value); + barGraphs[currentBar].barType = value; + } + + if (definitelyGreaterThan(barGraphs[currentBar].min, barGraphs[currentBar].max)) { + std::swap(barGraphs[currentBar].min, barGraphs[currentBar].max); + } + } + # ifdef P104_DEBUG + + if (logAllText && loglevelActiveFor(LOG_LEVEL_INFO)) { + String log; + + if (log.reserve(70)) { + log = F("dotmatrix: Bar-graph: "); + + if (loop) { + log += currentBar; + log += F(" in: "); + log += graphpart; + log += F(" value: "); + log += barGraphs[currentBar].value; + log += F(" max: "); + log += barGraphs[currentBar].max; + log += F(" min: "); + log += barGraphs[currentBar].min; + log += F(" dir: "); + log += barGraphs[currentBar].direction; + log += F(" typ: "); + log += barGraphs[currentBar].barType; + } else { + log += F(" bsize: "); + log += barGraphs.size(); + } + addLogMove(LOG_LEVEL_INFO, log); + } + } + # endif // ifdef P104_DEBUG + currentBar++; // next + delay(0); // Leave some breathingroom + } + # undef NOT_A_COMMA + + if (barGraphs.size() > 0) { + uint8_t barWidth = 8 / barGraphs.size(); // Divide the 8 pixel width per number of bars to show + int16_t pixTop, pixBottom; + uint16_t zeroPoint; + # ifdef P104_DEBUG + String log; + + if (logAllText && + loglevelActiveFor(LOG_LEVEL_INFO) && + log.reserve(64)) { + log = F("dotmatrix: bar Width: "); + log += barWidth; + log += F(" low: "); + log += zstruct._lower; + log += F(" high: "); + log += zstruct._upper; + } + # endif // ifdef P104_DEBUG + modulesOnOff(zstruct._startModule, zstruct._startModule + zstruct.size - 1, MD_MAX72XX::MD_OFF); // Stop updates on modules + P->setIntensity(zstruct.zone - 1, zstruct.brightness); // don't forget to set the brightness + uint8_t row = 0; + + if ((barGraphs.size() == 3) || (barGraphs.size() == 5) || (barGraphs.size() == 6)) { // Center within the rows a bit + for (; row < (barGraphs.size() == 5 ? 2 : 1); row++) { + for (uint8_t col = zstruct._lower; col <= zstruct._upper; col++) { + pM->setPoint(row, col, false); // all off + + if (col % 16 == 0) { delay(0); } + } + delay(0); // Leave some breathingroom + } + } + + for (auto it = barGraphs.begin(); it != barGraphs.end(); ++it) { + if (essentiallyZero(it->min)) { + pixTop = zstruct._lower - 1 + (((zstruct._upper + 1) - zstruct._lower) / it->max) * it->value; + pixBottom = zstruct._lower - 1; + zeroPoint = 0; + } else { + if (definitelyLessThan(it->min, 0.0) && + definitelyGreaterThan(it->max, 0.0) && + definitelyGreaterThan(it->max - it->min, 0.01)) { // Zero-point is used + zeroPoint = (it->min * -1.0) / ((it->max - it->min) / (1.0 * ((zstruct._upper + 1) - zstruct._lower))); + } else { + zeroPoint = 0; + } + pixTop = zstruct._lower + zeroPoint + (((zstruct._upper + 1) - zstruct._lower) / (it->max - it->min)) * it->value; + pixBottom = zstruct._lower + zeroPoint; + + if (definitelyLessThan(it->value, 0.0)) { + std::swap(pixTop, pixBottom); + } + } + + if (it->direction == 1) { // Left to right display: Flip values within the lower/upper range + pixBottom = zstruct._upper - (pixBottom - zstruct._lower); + pixTop = zstruct._lower + (zstruct._upper - pixTop); + std::swap(pixBottom, pixTop); + zeroPoint = zstruct._upper - zstruct._lower - zeroPoint + (zeroPoint == 0 ? 1 : 0); + } + # ifdef P104_DEBUG_DEV + + if (logAllText && loglevelActiveFor(LOG_LEVEL_INFO)) { + log += F(" B: "); + log += pixBottom; + log += F(" T: "); + log += pixTop; + log += F(" Z: "); + log += zeroPoint; + } + # endif // ifdef P104_DEBUG_DEV + drawOneBarGraph(zstruct._lower, zstruct._upper, pixBottom, pixTop, zeroPoint, barWidth, it->barType, row); + row += barWidth; // Next set of rows + delay(0); // Leave some breathingroom + } + + for (; row < 8; row++) { // Clear unused rows + for (uint8_t col = zstruct._lower; col <= zstruct._upper; col++) { + pM->setPoint(row, col, false); // all off + + if (col % 16 == 0) { delay(0); } + } + delay(0); // Leave some breathingroom + } + # ifdef P104_DEBUG + + if (logAllText && loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // ifdef P104_DEBUG + modulesOnOff(zstruct._startModule, zstruct._startModule + zstruct.size - 1, MD_MAX72XX::MD_ON); // Continue updates on modules + } +} + +# endif // ifdef P104_USE_BAR_GRAPH + +# ifdef P104_USE_DOT_SET +void P104_data_struct::displayDots(uint8_t zone, + const P104_zone_struct& zstruct, + const String & dots) { + if ((nullptr == P) || (nullptr == pM) || dots.isEmpty()) { return; } + { + uint8_t idx = 0; + String sRow; + String sCol; + String sOn_off; + bool on_off = true; + modulesOnOff(zstruct._startModule, zstruct._startModule + zstruct.size - 1, MD_MAX72XX::MD_OFF); // Stop updates on modules + P->setIntensity(zstruct.zone - 1, zstruct.brightness); // don't forget to set the brightness + sRow = parseString(dots, idx + 1); + sCol = parseString(dots, idx + 2); + sOn_off = parseString(dots, idx + 3); + + while (!sRow.isEmpty() && !sCol.isEmpty()) { + on_off = true; // Default On + + int32_t row; + int32_t col; + if (validIntFromString(sRow, row) && + validIntFromString(sCol, col) && + (row > 0) && ((row - 1) < 8) && + (col > 0) && ((col - 1) <= (zstruct._upper - zstruct._lower))) { // Valid coordinates? + if (equals(sOn_off, F("0"))) { // Dot On is the default + on_off = false; + idx++; // 3rd argument used + } + pM->setPoint(row - 1, zstruct._upper - (col - 1), on_off); // Reverse layout + } + idx += 2; // Skip to next argument set + + if (idx % 16 == 0) { delay(0); } + sRow = parseString(dots, idx + 1); + sCol = parseString(dots, idx + 2); + sOn_off = parseString(dots, idx + 3); + } + + modulesOnOff(zstruct._startModule, zstruct._startModule + zstruct.size - 1, MD_MAX72XX::MD_ON); // Continue updates on modules + } +} + +# endif // ifdef P104_USE_DOT_SET + +/************************************************** + * Check if an animation is available in the current build + *************************************************/ +bool isAnimationAvailable(uint8_t animation, bool noneIsAllowed = false) { + textEffect_t selection = static_cast(animation); + + switch (selection) { + case PA_NO_EFFECT: + { + return noneIsAllowed; + } + case PA_PRINT: + case PA_SCROLL_UP: + case PA_SCROLL_DOWN: + case PA_SCROLL_LEFT: + case PA_SCROLL_RIGHT: + { + return true; + } + # if ENA_SPRITE + case PA_SPRITE: + { + return true; + } + # endif // ENA_SPRITE + # if ENA_MISC + case PA_SLICE: + case PA_MESH: + case PA_FADE: + case PA_DISSOLVE: + case PA_BLINDS: + case PA_RANDOM: + { + return true; + } + # endif // ENA_MISC + # if ENA_WIPE + case PA_WIPE: + case PA_WIPE_CURSOR: + { + return true; + } + # endif // ENA_WIPE + # if ENA_SCAN + case PA_SCAN_HORIZ: + case PA_SCAN_HORIZX: + case PA_SCAN_VERT: + case PA_SCAN_VERTX: + { + return true; + } + # endif // ENA_SCAN + # if ENA_OPNCLS + case PA_OPENING: + case PA_OPENING_CURSOR: + case PA_CLOSING: + case PA_CLOSING_CURSOR: + { + return true; + } + # endif // ENA_OPNCLS + # if ENA_SCR_DIA + case PA_SCROLL_UP_LEFT: + case PA_SCROLL_UP_RIGHT: + case PA_SCROLL_DOWN_LEFT: + case PA_SCROLL_DOWN_RIGHT: + { + return true; + } + # endif // ENA_SCR_DIA + # if ENA_GROW + case PA_GROW_UP: + case PA_GROW_DOWN: + { + return true; + } + # endif // ENA_GROW + default: + return false; + } +} + +const char p104_subcommands[] PROGMEM = + "clear" + "|update" + + "|txt" + "|settxt" + +# ifdef P104_USE_BAR_GRAPH + "|bar" + "|setbar" +# endif // ifdef P104_USE_BAR_GRAPH + +# ifdef P104_USE_DOT_SET + "|dot" +# endif // ifdef P104_USE_DOT_SET + +# ifdef P104_USE_COMMANDS + "|alignment" + "|anim.in" + "|anim.out" + "|brightness" + "|content" + "|font" + "|inverted" +# if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) + "|layout" +# endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) + "|offset" + "|pause" + "|repeat" + "|size" + "|specialeffect" + "|speed" +# endif // ifdef P104_USE_COMMANDS +; + +// Subcommands prefixed by "dotmatrix," +enum class p104_subcommands_e { + clear, // subcommand: clear, / clear[,all] + update, // subcommand: update, / update[,all] + + txt, // subcommand: [set]txt,, (only + settxt, // subcommand: settxt,, (stores + +# ifdef P104_USE_BAR_GRAPH + bar, // subcommand: [set]bar,, (only allowed for zones + setbar, // subcommand: setbar,, (stores the graph-string +# endif // ifdef P104_USE_BAR_GRAPH + +# ifdef P104_USE_DOT_SET + dot, // subcommand: dot,,,[,0][,,[,0]...] to draw +# endif // ifdef P104_USE_DOT_SET + +# ifdef P104_USE_COMMANDS + alignment, // subcommand: alignment,, (0..3) + anim_in, // subcommand: anim.in,, (1..) + anim_out, // subcommand: anim.out,, (0..) + brightness, // subcommand: brightness,, (0..15) + content, // subcommand: content,, (0..-1) + font, // subcommand: font,, (only for incuded font id's) + inverted, // subcommand: inverted,, (disable/enable) +# if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) + layout, // subcommand: layout,, (0..2), only when double-height font is available +# endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) + offset, // subcommand: offset,, (0..-1) + pause, // subcommand: pause,, (0..P104_MAX_SPEED_PAUSE_VALUE) + repeat, // subcommand: repeat,, (-1..86400 = 24h) + size, // subcommand: size,, (1..) + specialeffect, // subcommand: specialeffect,, (0..3) + speed, // subcommand: speed,, (0..P104_MAX_SPEED_PAUSE_VALUE) +# endif // ifdef P104_USE_COMMANDS +}; + +/******************************************************* + * handlePluginWrite : process commands + ******************************************************/ +bool P104_data_struct::handlePluginWrite(taskIndex_t taskIndex, + const String& string) { + # ifdef P104_USE_COMMANDS + bool reconfigure = false; + # endif // ifdef P104_USE_COMMANDS + bool success = false; + const String command = parseString(string, 1); + + if ((nullptr != P) && equals(command, F("dotmatrix"))) { // main command: dotmatrix + const String subCommand = parseString(string, 2); + const int subCommand_i = GetCommandCode(subCommand.c_str(), p104_subcommands); + + if (subCommand_i != -1) { + const p104_subcommands_e subcommands_e = static_cast(subCommand_i); + + int32_t zoneIndex{}; + const String string4 = parseStringKeepCaseNoTrim(string, 4); + # ifdef P104_USE_COMMANDS + int32_t value4{}; + validIntFromString(string4, value4); + # endif // ifdef P104_USE_COMMANDS + + // Global subcommands + + if ((subcommands_e == p104_subcommands_e::clear) && // subcommand: clear[,all] + (string4.isEmpty() || + string4.equalsIgnoreCase(F("all")))) { + P->displayClear(); + success = true; + } else + + if ((subcommands_e == p104_subcommands_e::update) && // subcommand: update[,all] + (string4.isEmpty() || + string4.equalsIgnoreCase(F("all")))) { + updateZone(0, P104_zone_struct(0)); + success = true; + } + + // Zone-specific subcommands + if (validIntFromString(parseString(string, 3), zoneIndex) && + (zoneIndex > 0) && + (static_cast(zoneIndex) <= zones.size())) { + // subcommands are processed in the same order as they are presented in the UI + for (auto it = zones.begin(); it != zones.end() && !success; ++it) { + if ((it->zone == zoneIndex)) { // This zone + switch (subcommands_e) { + case p104_subcommands_e::clear: + // subcommand: clear, + { + P->displayClear(zoneIndex - 1); + success = true; + break; + } + + case p104_subcommands_e::update: + // subcommand: update, + { + updateZone(zoneIndex, *it); + success = true; + break; + } + + # ifdef P104_USE_COMMANDS + + case p104_subcommands_e::size: + // subcommand: size,, (1..) + { + if ((value4 > 0) && + (value4 <= P104_MAX_MODULES_PER_ZONE)) + { + reconfigure = (it->size != value4); + it->size = value4; + success = true; + } + break; + } + # endif // ifdef P104_USE_COMMANDS + + case p104_subcommands_e::txt: // subcommand: [set]txt,, (only + case p104_subcommands_e::settxt: // allowed for zones with Text content) + { + if ((it->content == P104_CONTENT_TEXT) || + (it->content == P104_CONTENT_TEXT_REV)) { // no length check, so longer than the UI allows is made + // possible + if ((subcommands_e == p104_subcommands_e::settxt) && // subcommand: settxt,, (stores + (string4.length() <= P104_MAX_TEXT_LENGTH_PER_ZONE)) { // the text in the settings, is not saved) + it->text = string4; // Only if not too long, could 'blow up' the + } // settings when saved + displayOneZoneText(zoneIndex - 1, *it, string4); + success = true; + } + + break; + } + + # ifdef P104_USE_COMMANDS + + case p104_subcommands_e::content: + // subcommand: content,, (0..-1) + { + if ((value4 >= 0) && + (value4 < P104_CONTENT_count)) + { + reconfigure = (it->content != value4); + it->content = value4; + success = true; + } + break; + } + + case p104_subcommands_e::alignment: + // subcommand: alignment,, (0..3) + { + if ((value4 >= 0) && + (value4 <= static_cast(textPosition_t::PA_RIGHT))) // last item in the enum + { + it->alignment = value4; + success = true; + } + break; + } + + case p104_subcommands_e::anim_in: + // subcommand: anim.in,, (1..) + { + if (isAnimationAvailable(value4)) { + it->animationIn = value4; + success = true; + } + break; + } + + case p104_subcommands_e::speed: + // subcommand: speed,, (0..P104_MAX_SPEED_PAUSE_VALUE) + { + if ((value4 >= 0) && + (value4 <= P104_MAX_SPEED_PAUSE_VALUE)) + { + it->speed = value4; + success = true; + } + break; + } + + case p104_subcommands_e::anim_out: + // subcommand: anim.out,, (0..) + { + if (isAnimationAvailable(value4, true)) + { + it->animationOut = value4; + success = true; + } + break; + } + + case p104_subcommands_e::pause: + // subcommand: pause,, (0..P104_MAX_SPEED_PAUSE_VALUE) + { + if ((value4 >= 0) && + (value4 <= P104_MAX_SPEED_PAUSE_VALUE)) + { + it->pause = value4; + success = true; + } + break; + } + + case p104_subcommands_e::font: + // subcommand: font,, (only for incuded font id's) + { + if ( + (value4 == 0) + # ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT + || (value4 == P104_DOUBLE_HEIGHT_FONT_ID) + # endif // ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT + # ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT + || (value4 == P104_FULL_DOUBLEHEIGHT_FONT_ID) + # endif // ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT + # ifdef P104_USE_VERTICAL_FONT + || (value4 == P104_VERTICAL_FONT_ID) + # endif // ifdef P104_USE_VERTICAL_FONT + # ifdef P104_USE_EXT_ASCII_FONT + || (value4 == P104_EXT_ASCII_FONT_ID) + # endif // ifdef P104_USE_EXT_ASCII_FONT + # ifdef P104_USE_ARABIC_FONT + || (value4 == P104_ARABIC_FONT_ID) + # endif // ifdef P104_USE_ARABIC_FONT + # ifdef P104_USE_GREEK_FONT + || (value4 == P104_GREEK_FONT_ID) + # endif // ifdef P104_USE_GREEK_FONT + # ifdef P104_USE_KATAKANA_FONT + || (value4 == P104_KATAKANA_FONT_ID) + # endif // ifdef P104_USE_KATAKANA_FONT + ) + { + reconfigure = (it->font != value4); + it->font = value4; + success = true; + } + break; + } + + case p104_subcommands_e::inverted: + // subcommand: inverted,, (disable/enable) + { + if ((value4 >= 0) && + (value4 <= 1)) + { + reconfigure = (it->inverted != value4); + it->inverted = value4; + success = true; + } + break; + } + + # if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) + + case p104_subcommands_e::layout: + // subcommand: layout,, (0..2), only when double-height font is available + { + if ((value4 >= 0) && + (value4 <= P104_LAYOUT_DOUBLE_LOWER)) + { + reconfigure = (it->layout != value4); + it->layout = value4; + success = true; + } + break; + } + # endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) + + case p104_subcommands_e::specialeffect: + // subcommand: specialeffect,, (0..3) + { + if ((value4 >= 0) && + (value4 <= P104_SPECIAL_EFFECT_BOTH)) + { + reconfigure = (it->specialEffect != value4); + it->specialEffect = value4; + success = true; + } + break; + } + + case p104_subcommands_e::offset: + // subcommand: offset,, (0..-1) + { + if ((value4 >= 0) && + (value4 < P104_MAX_MODULES_PER_ZONE) && + (value4 < it->size)) + { + reconfigure = (it->offset != value4); + it->offset = value4; + success = true; + } + break; + } + + case p104_subcommands_e::brightness: + // subcommand: brightness,, (0..15) + { + if ((value4 >= 0) && + (value4 <= P104_BRIGHTNESS_MAX)) + { + it->brightness = value4; + P->setIntensity(zoneIndex - 1, it->brightness); // Change brightness immediately + success = true; + } + break; + } + + case p104_subcommands_e::repeat: + // subcommand: repeat,, (-1..86400 = 24h) + { + if ((value4 >= -1) && + (value4 <= P104_MAX_REPEATDELAY_VALUE)) + { + it->repeatDelay = value4; + success = true; + + if (it->repeatDelay > -1) { + it->_repeatTimer = millis(); + } + } + break; + } + # endif // ifdef P104_USE_COMMANDS + + # ifdef P104_USE_BAR_GRAPH + + case p104_subcommands_e::bar: // subcommand: [set]bar,, (only allowed for + // zones + case p104_subcommands_e::setbar: // with Bargraph content) no length check, so longer than the + // UI allows is made possible + { + if (it->content == P104_CONTENT_BAR_GRAPH) { + if ((subcommands_e == p104_subcommands_e::setbar) && // subcommand: setbar,, (stores the + // graph-string + (string4.length() <= P104_MAX_TEXT_LENGTH_PER_ZONE)) { // in the settings, is not saved) + it->text = string4; // Only if not too long, could 'blow up' the settings when + // saved + } + displayBarGraph(zoneIndex - 1, *it, string4); + success = true; + } + break; + } + # endif // ifdef P104_USE_BAR_GRAPH + + # ifdef P104_USE_DOT_SET + + case p104_subcommands_e::dot: + // subcommand: dot,,,[,0][,,[,0]...] to draw + { + displayDots(zoneIndex - 1, *it, parseStringToEnd(string, 4)); // dots at row/column, add ,0 to turn a dot off + success = true; + break; + } + # endif // ifdef P104_USE_DOT_SET + } + + // FIXME TD-er: success is always false here. Maybe this must be done outside the for-loop? + if (success) { // Reset the repeat timer + if (it->repeatDelay > -1) { + it->_repeatTimer = millis(); + } + } + } + } + } + } + } + + # ifdef P104_USE_COMMANDS + + if (reconfigure) { + configureZones(); // Re-initialize + success = true; // Successful + } + # endif // ifdef P104_USE_COMMANDS + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + String log; + + if (log.reserve(34 + string.length())) { + log = F("dotmatrix: command "); + + if (!success) { log += F("NOT "); } + log += F("succesful: "); + log += string; + addLogMove(LOG_LEVEL_INFO, log); + } + } + + return success; // Default: unknown command +} + +int8_t P104_data_struct::getTime(char *psz, + bool seconds, + bool colon, + bool time12h, + bool timeAmpm) { + uint16_t h, M, s; + String ampm; + + # ifdef P104_USE_DATETIME_OPTIONS + + if (time12h) { + if (timeAmpm) { + ampm = (node_time.hour() >= 12 ? F("p") : F("a")); + } + h = node_time.hour() % 12; + + if (h == 0) { h = 12; } + } else + # endif // ifdef P104_USE_DATETIME_OPTIONS + { + h = node_time.hour(); + } + M = node_time.minute(); + + if (!seconds) { + sprintf_P(psz, PSTR("%02d%c%02d%s"), h, (colon ? ':' : ' '), M, ampm.c_str()); + } else { + s = node_time.second(); + sprintf_P(psz, PSTR("%02d%c%02d %02d%s"), h, (colon ? ':' : ' '), M, s, ampm.c_str()); + } + return M; +} + +void P104_data_struct::getDate(char *psz, + bool showYear, + bool fourDgt + # ifdef P104_USE_DATETIME_OPTIONS + , const uint8_t dateFmt + , const uint8_t dateSep + # endif // ifdef P104_USE_DATETIME_OPTIONS + ) { + uint16_t d, m, y; + const uint16_t year = node_time.year() - (fourDgt ? 0 : 2000); + + # ifdef P104_USE_DATETIME_OPTIONS + const String separators = F(" /-."); + const char sep = separators[dateSep]; + # else // ifdef P104_USE_DATETIME_OPTIONS + const char sep = ' '; + # endif // ifdef P104_USE_DATETIME_OPTIONS + + d = node_time.day(); + m = node_time.month(); + y = year; + # ifdef P104_USE_DATETIME_OPTIONS + + if (showYear) { + switch (dateFmt) { + case P104_DATE_FORMAT_US: + d = node_time.month(); + m = node_time.day(); + y = year; + break; + case P104_DATE_FORMAT_JP: + d = year; + m = node_time.month(); + y = node_time.day(); + break; + } + } else { + if ((dateFmt == P104_DATE_FORMAT_US) || + (dateFmt == P104_DATE_FORMAT_JP)) { + std::swap(d, m); + } + } + # endif // ifdef P104_USE_DATETIME_OPTIONS + + if (showYear) { + sprintf_P(psz, PSTR("%02d%c%02d%c%02d"), d, sep, m, sep, y); // %02d will expand to 04 when needed + } else { + sprintf_P(psz, PSTR("%02d%c%02d"), d, sep, m); + } +} + +uint8_t P104_data_struct::getDateTime(char *psz, + bool colon, + bool time12h, + bool timeAmpm, + bool fourDgt + # ifdef P104_USE_DATETIME_OPTIONS + , const uint8_t dateFmt + , const uint8_t dateSep + # endif // ifdef P104_USE_DATETIME_OPTIONS + ) { + String ampm; + uint16_t d, M, y; + uint8_t h, m; + const uint16_t year = node_time.year() - (fourDgt ? 0 : 2000); + + # ifdef P104_USE_DATETIME_OPTIONS + const String separators = F(" /-."); + const char sep = separators[dateSep]; + # else // ifdef P104_USE_DATETIME_OPTIONS + const char sep = ' '; + # endif // ifdef P104_USE_DATETIME_OPTIONS + + # ifdef P104_USE_DATETIME_OPTIONS + + if (time12h) { + if (timeAmpm) { + ampm = (node_time.hour() >= 12 ? F("p") : F("a")); + } + h = node_time.hour() % 12; + + if (h == 0) { h = 12; } + } else + # endif // ifdef P104_USE_DATETIME_OPTIONS + { + h = node_time.hour(); + } + M = node_time.minute(); + + # ifdef P104_USE_DATETIME_OPTIONS + + switch (dateFmt) { + case P104_DATE_FORMAT_US: + d = node_time.month(); + m = node_time.day(); + y = year; + break; + case P104_DATE_FORMAT_JP: + d = year; + m = node_time.month(); + y = node_time.day(); + break; + default: + # endif // ifdef P104_USE_DATETIME_OPTIONS + d = node_time.day(); + m = node_time.month(); + y = year; + # ifdef P104_USE_DATETIME_OPTIONS +} + + # endif // ifdef P104_USE_DATETIME_OPTIONS + sprintf_P(psz, PSTR("%02d%c%02d%c%02d %02d%c%02d%s"), d, sep, m, sep, y, h, (colon ? ':' : ' '), M, ampm.c_str()); // %02d will expand to + // 04 when needed + return M; +} + +# if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) +void P104_data_struct::createHString(String& string) { + const uint16_t stringLen = string.length(); + + for (uint16_t i = 0; i < stringLen; i++) { + string[i] |= 0x80; // use 'high' part of the font, by adding 0x80 + } +} + +# endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) + +void P104_data_struct::reverseStr(String& str) { + const uint16_t n = str.length(); + + // Swap characters starting from two corners + for (uint16_t i = 0; i < n / 2; i++) { + std::swap(str[i], str[n - i - 1]); + } +} + +/************************************************************************ + * execute all PLUGIN_ONE_PER_SECOND tasks + ***********************************************************************/ +bool P104_data_struct::handlePluginOncePerSecond(struct EventStruct *event) { + if (nullptr == P) { return false; } + bool redisplay = false; + bool success = false; + + # ifdef P104_USE_DATETIME_OPTIONS + bool useFlasher = !bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FLASH); + bool time12h = bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_12H); + bool timeAmpm = bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_AMPM); + bool year4dgt = bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_YEAR4DGT); + # else // ifdef P104_USE_DATETIME_OPTIONS + bool useFlasher = true; + bool time12h = false; + bool timeAmpm = false; + bool year4dgt = false; + # endif // ifdef P104_USE_DATETIME_OPTIONS + bool newFlasher = !flasher && useFlasher; + + for (auto it = zones.begin(); it != zones.end(); ++it) { + redisplay = false; + + if (P->getZoneStatus(it->zone - 1)) { // Animations done? + switch (it->content) { + case P104_CONTENT_TIME: // time + case P104_CONTENT_TIME_SEC: // time sec + { + bool useSeconds = (it->content == P104_CONTENT_TIME_SEC); + int8_t m = getTime(szTimeL, useSeconds, flasher || !useFlasher, time12h, timeAmpm); + flasher = newFlasher; + redisplay = useFlasher || useSeconds || (it->_lastChecked != m); + it->_lastChecked = m; + break; + } + case P104_CONTENT_DATE4: // date/4 + case P104_CONTENT_DATE6: // date/6 + { + if (it->_lastChecked != node_time.day()) { + getDate(szTimeL, + it->content != P104_CONTENT_DATE4, + year4dgt + # ifdef P104_USE_DATETIME_OPTIONS + , get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FORMAT) + , get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_SEP_CHAR) + # endif // ifdef P104_USE_DATETIME_OPTIONS + ); + redisplay = true; + it->_lastChecked = node_time.day(); + } + break; + } + case P104_CONTENT_DATE_TIME: // date-time/9 + { + int8_t m = getDateTime(szTimeL, + flasher || !useFlasher, + time12h, + timeAmpm, + year4dgt + # ifdef P104_USE_DATETIME_OPTIONS + , get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FORMAT) + , get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_SEP_CHAR) + # endif // ifdef P104_USE_DATETIME_OPTIONS + ); + flasher = newFlasher; + redisplay = useFlasher || (it->_lastChecked != m); + it->_lastChecked = m; + break; + } + default: + break; + } + + if (redisplay) { + displayOneZoneText(it->zone - 1, *it, String(szTimeL)); + P->displayReset(it->zone - 1); + + if (it->repeatDelay > -1) { + it->_repeatTimer = millis(); + } + } + } + delay(0); // Leave some breathingroom + } + + if (redisplay) { + // synchronise the start + P->synchZoneStart(); + } + return redisplay || success; +} + +/*************************************************** + * restart a zone if the repeat delay (if any) has passed + **************************************************/ +void P104_data_struct::checkRepeatTimer(uint8_t z) { + if (nullptr == P) { return; } + bool handled = false; + + for (auto it = zones.begin(); it != zones.end() && !handled; ++it) { + if (it->zone == z + 1) { + handled = true; + + if ((it->repeatDelay > -1) && (timePassedSince(it->_repeatTimer) >= (it->repeatDelay - 1) * 1000)) { // Compensated for the '1' in + // PLUGIN_ONE_PER_SECOND + # ifdef P104_DEBUG + + if (logAllText && loglevelActiveFor(LOG_LEVEL_INFO)) { + String log; + log.reserve(51); + log = F("dotmatrix: Repeat zone: "); + log += it->zone; + log += F(" delay: "); + log += it->repeatDelay; + log += F(" ("); + log += (timePassedSince(it->_repeatTimer) / 1000.0f); // Decimals can be useful here + log += ')'; + addLogMove(LOG_LEVEL_INFO, log); + } + # endif // ifdef P104_DEBUG + + if ((it->content == P104_CONTENT_TEXT) || + (it->content == P104_CONTENT_TEXT_REV)) { + displayOneZoneText(it->zone - 1, *it, sZoneInitial[it->zone - 1]); // Re-send last displayed text + P->displayReset(it->zone - 1); + } + + if ((it->content == P104_CONTENT_TIME) || + (it->content == P104_CONTENT_TIME_SEC) || + (it->content == P104_CONTENT_DATE4) || + (it->content == P104_CONTENT_DATE6) || + (it->content == P104_CONTENT_DATE_TIME)) { + it->_lastChecked = -1; // Invalidate so next run will re-display the date/time + } + # ifdef P104_USE_BAR_GRAPH + + if (it->content == P104_CONTENT_BAR_GRAPH) { + displayBarGraph(it->zone - 1, *it, sZoneInitial[it->zone - 1]); // Re-send last displayed bar graph + } + # endif // ifdef P104_USE_BAR_GRAPH + it->_repeatTimer = millis(); + } + } + delay(0); // Leave some breathingroom + } +} + +/*************************************** + * saveSettings gather the zones data from the UI and store in customsettings + **************************************/ +bool P104_data_struct::saveSettings() { + error = String(); // Clear + + # ifdef P104_DEBUG_DEV + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, concat(F("P104: saving zones, count: "), expectedZones)); + } + # endif // ifdef P104_DEBUG_DEV + + uint8_t index = 0; + uint8_t action = P104_ACTION_NONE; + uint8_t zoneIndex = 0; + int8_t zoneOffset = 0; + + zones.clear(); // Start afresh + + for (uint8_t zCounter = 0; zCounter < expectedZones; zCounter++) { + # ifdef P104_USE_ZONE_ACTIONS + action = getFormItemIntCustomArgName(index + P104_OFFSET_ACTION); + + if (((action == P104_ACTION_ADD_ABOVE) && (zoneOrder == 0)) || + ((action == P104_ACTION_ADD_BELOW) && (zoneOrder == 1))) { + zones.push_back(P104_zone_struct(0)); + zoneOffset++; + # ifdef P104_DEBUG_DEV + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, concat(F("P104: insert before zone: "), zoneIndex + 1)); + } + # endif // ifdef P104_DEBUG_DEV + } + # endif // ifdef P104_USE_ZONE_ACTIONS + zoneIndex = zCounter + zoneOffset; + + if (action == P104_ACTION_DELETE) { + zoneOffset--; + } else { + # ifdef P104_DEBUG_DEV + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, concat(F("P104: read zone: "), zoneIndex + 1)); + } + # endif // ifdef P104_DEBUG_DEV + zones.push_back(P104_zone_struct(zoneIndex + 1)); + + for (uint8_t i = 0; i < P104_OFFSET_COUNT; ++i) { + // for newly added zone, use defaults + const bool mustCheckSize = + (i == P104_OFFSET_BRIGHTNESS) || + (i == P104_OFFSET_REPEATDELAY); + if (!mustCheckSize || zones[zoneIndex].size != 0) { + if (i == P104_OFFSET_TEXT) { + zones[zoneIndex].text = wrapWithQuotes(webArg(getPluginCustomArgName(index + P104_OFFSET_TEXT))); + } else { + zones[zoneIndex].setIntValue(i, getFormItemIntCustomArgName(index + i)); + } + } + } + } + # ifdef P104_DEBUG_DEV + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, concat(F("P104: add zone: "), zoneIndex + 1)); + } + # endif // ifdef P104_DEBUG_DEV + + # ifdef P104_USE_ZONE_ACTIONS + + if (((action == P104_ACTION_ADD_BELOW) && (zoneOrder == 0)) || + ((action == P104_ACTION_ADD_ABOVE) && (zoneOrder == 1))) { + zones.push_back(P104_zone_struct(0)); + zoneOffset++; + # ifdef P104_DEBUG_DEV + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, concat(F("P104: insert after zone: "), zoneIndex + 2)); + } + # endif // ifdef P104_DEBUG_DEV + } + # endif // ifdef P104_USE_ZONE_ACTIONS + + index += P104_OFFSET_COUNT; + delay(0); + } + + uint16_t bufferSize; + int saveOffset = 0; + + numDevices = 0; // Count the number of connected display units + + bufferSize = P104_CONFIG_VERSION_V2; // Save special marker that we're using V2 settings + // This write is counting + error += SaveToFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)&bufferSize, sizeof(bufferSize), saveOffset); + saveOffset += sizeof(bufferSize); + + String zbuffer; + + // 47 total + (max) 100 characters for it->text requires a buffer of ~150 (P104_SETTINGS_BUFFER_V2), but only the required length is + // stored with the length prefixed + if (zbuffer.reserve(P104_SETTINGS_BUFFER_V2 + 2)) { + for (auto it = zones.begin(); it != zones.end() && error.length() == 0; ++it) { + // WARNING: Order of values should match the numeric order of P104_OFFSET_* values + zbuffer.clear(); + for (uint8_t i = 0; i < P104_OFFSET_COUNT; ++i) { + if (i == P104_OFFSET_TEXT) { + zbuffer += it->text; + zbuffer += '\x01'; + } else { + int32_t value{}; + if (it->getIntValue(i, value)) { + zbuffer += value; + zbuffer += '\x01'; + } + } + } + + numDevices += (it->size != 0 ? it->size : 1) + it->offset; // Count corrected for newly added zones + + if (saveOffset + zbuffer.length() + (sizeof(bufferSize) * 2) > (DAT_TASKS_CUSTOM_SIZE)) { // Detect ourselves if we've reached the + error.reserve(55); // high-water mark + error += F("Total combination of Zones & text too long to store.\n"); + addLogMove(LOG_LEVEL_ERROR, error); + } else { + // Store length of buffer + bufferSize = zbuffer.length(); + + // As we write in parts, only count as single write. + if (RTC.flashDayCounter > 0) { + RTC.flashDayCounter--; + } + error += SaveToFile(SettingsType::Enum::CustomTaskSettings_Type, + taskIndex, + (uint8_t *)&bufferSize, + sizeof(bufferSize), + saveOffset); + saveOffset += sizeof(bufferSize); + + // As we write in parts, only count as single write. + if (RTC.flashDayCounter > 0) { + RTC.flashDayCounter--; + } + error += SaveToFile(SettingsType::Enum::CustomTaskSettings_Type, + taskIndex, + (uint8_t *)zbuffer.c_str(), + bufferSize, + saveOffset); + saveOffset += bufferSize; + + # ifdef P104_DEBUG_DEV + + if (loglevelActiveFor(LOG_LEVEL_INFO)) { + addLogMove(LOG_LEVEL_INFO, strformat(F("P104: saveSettings zone: %d bufferSize: %d offset: %d"), + it->zone, bufferSize, saveOffset)); + zbuffer.replace(P104_FIELD_SEP, P104_FIELD_DISP); + addLog(LOG_LEVEL_INFO, zbuffer); + } + # endif // ifdef P104_DEBUG_DEV + } + + delay(0); + } + + // Store an End-of-settings marker == 0 + bufferSize = 0u; + + // This write is counting + SaveToFile(SettingsType::Enum::CustomTaskSettings_Type, taskIndex, (uint8_t *)&bufferSize, sizeof(bufferSize), saveOffset); + + if (numDevices > 255) { + error += strformat(F("More than 255 modules configured (%u)\n"), numDevices); + } + } else { + addLog(LOG_LEVEL_ERROR, F("DOTMATRIX: Can't allocate string for saving settings, insufficient memory!")); + return false; // Don't continue + } + + return error.isEmpty(); +} + +/************************************************************** +* webform_load +**************************************************************/ +bool P104_data_struct::webform_load(struct EventStruct *event) { + { // Hardware types + # define P104_hardwareTypeCount 8 + const __FlashStringHelper *hardwareTypes[P104_hardwareTypeCount] = { + F("Generic (DR:0, CR:1, RR:0)"), // 010 + F("Parola (DR:1, CR:1, RR:0)"), // 110 + F("FC16 (DR:1, CR:0, RR:0)"), // 100 + F("IC Station (DR:1, CR:1, RR:1)"), // 111 + F("Other 1 (DR:0, CR:0, RR:0)"), // 000 + F("Other 2 (DR:0, CR:0, RR:1)"), // 001 + F("Other 3 (DR:0, CR:1, RR:1)"), // 011 + F("Other 4 (DR:1, CR:0, RR:1)") // 101 + }; + constexpr int hardwareOptions[P104_hardwareTypeCount] = { + static_cast(MD_MAX72XX::moduleType_t::GENERIC_HW), + static_cast(MD_MAX72XX::moduleType_t::PAROLA_HW), + static_cast(MD_MAX72XX::moduleType_t::FC16_HW), + static_cast(MD_MAX72XX::moduleType_t::ICSTATION_HW), + static_cast(MD_MAX72XX::moduleType_t::DR0CR0RR0_HW), + static_cast(MD_MAX72XX::moduleType_t::DR0CR0RR1_HW), + static_cast(MD_MAX72XX::moduleType_t::DR0CR1RR1_HW), + static_cast(MD_MAX72XX::moduleType_t::DR1CR0RR1_HW) + }; + addFormSelector(F("Hardware type"), + F("hardware"), + P104_hardwareTypeCount, + hardwareTypes, + hardwareOptions, + P104_CONFIG_HARDWARETYPE); + # ifdef P104_ADD_SETTINGS_NOTES + addFormNote(F("DR = Digits as Rows, CR = Column Reversed, RR = Row Reversed; 0 = no, 1 = yes.")); + # endif // ifdef P104_ADD_SETTINGS_NOTES + } + + { + addFormCheckBox(F("Clear display on disable"), F("clrdsp"), + bitRead(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_CLEAR_DISABLE)); + + addFormCheckBox(F("Log all displayed text (info)"), + F("logtxt"), + bitRead(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_LOG_ALL_TEXT)); + } + + # ifdef P104_USE_DATETIME_OPTIONS + { + addFormSubHeader(F("Content options")); + + addFormCheckBox(F("Clock with flashing colon"), F("clkflash"), !bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FLASH)); + addFormCheckBox(F("Clock 12h display"), F("clk12h"), bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_12H)); + addFormCheckBox(F("Clock 12h AM/PM indicator"), F("clkampm"), bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_AMPM)); + } + { // Date format + const __FlashStringHelper *dateFormats[] = { + F("Day Month [Year]"), + F("Month Day [Year] (US-style)"), + F("[Year] Month Day (Japanese-style)") + }; + constexpr int dateFormatOptions[] = { + P104_DATE_FORMAT_EU, + P104_DATE_FORMAT_US, + P104_DATE_FORMAT_JP + }; + addFormSelector(F("Date format"), F("datefmt"), + 3, + dateFormats, dateFormatOptions, + get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_FORMAT)); + } + { // Date separator + const __FlashStringHelper *dateSeparators[] = { + F("Space"), + F("Slash /"), + F("Dash -"), + F("Dot .") + }; + constexpr int dateSeparatorOptions[] = { + P104_DATE_SEPARATOR_SPACE, + P104_DATE_SEPARATOR_SLASH, + P104_DATE_SEPARATOR_DASH, + P104_DATE_SEPARATOR_DOT + }; + addFormSelector(F("Date separator"), F("datesep"), + 4, + dateSeparators, dateSeparatorOptions, + get4BitFromUL(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_SEP_CHAR)); + + addFormCheckBox(F("Year uses 4 digits"), F("year4dgt"), bitRead(P104_CONFIG_DATETIME, P104_CONFIG_DATETIME_YEAR4DGT)); + } + # endif // ifdef P104_USE_DATETIME_OPTIONS + + addFormSubHeader(F("Zones")); + + { // Zones + String zonesList[P104_MAX_ZONES]; + int zonesOptions[P104_MAX_ZONES]; + + for (uint8_t i = 0; i < P104_MAX_ZONES; i++) { + zonesList[i] = i + 1; + zonesOptions[i] = i + 1; // No 0 needed or wanted + } + # if defined(P104_USE_TOOLTIPS) || defined(P104_ADD_SETTINGS_NOTES) + + const String zonetip = F("Select between 1 and " STRINGIFY(P104_MAX_ZONES) " zones, changing" + # ifdef P104_USE_ZONE_ORDERING + " Zones or Zone order" + # endif // ifdef P104_USE_ZONE_ORDERING + " will save and reload the page."); + # endif // if defined(P104_USE_TOOLTIPS) || defined(P104_ADD_SETTINGS_NOTES) + addFormSelector(F("Zones"), F("zonecnt"), P104_MAX_ZONES, zonesList, zonesOptions, nullptr, P104_CONFIG_ZONE_COUNT, true + # ifdef P104_USE_TOOLTIPS + , zonetip + # endif // ifdef P104_USE_TOOLTIPS + ); + + # ifdef P104_USE_ZONE_ORDERING + const String orderTypes[] = { + F("Numeric order (1..n)"), + F("Display order (n..1)") + }; + const int orderOptions[] = { 0, 1 }; + addFormSelector(F("Zone order"), F("zoneorder"), 2, orderTypes, orderOptions, nullptr, + bitRead(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_ZONE_ORDER) ? 1 : 0, true + # ifdef P104_USE_TOOLTIPS + , zonetip + # endif // ifdef P104_USE_TOOLTIPS + ); + # endif // ifdef P104_USE_ZONE_ORDERING + # ifdef P104_ADD_SETTINGS_NOTES + addFormNote(zonetip); + # endif // ifdef P104_ADD_SETTINGS_NOTES + } + expectedZones = P104_CONFIG_ZONE_COUNT; + + if (expectedZones == 0) { expectedZones++; } // Minimum of 1 zone + + { // Optionlists and zones table + const __FlashStringHelper *alignmentTypes[3] = { + F("Left"), + F("Center"), + F("Right") + }; + const int alignmentOptions[3] = { + static_cast(textPosition_t::PA_LEFT), + static_cast(textPosition_t::PA_CENTER), + static_cast(textPosition_t::PA_RIGHT) + }; + + + // Append the numeric value as a reference for the 'anim.in' and 'anim.out' subcommands + const __FlashStringHelper *animationTypes[] { + F("None (0)") + , F("Print (1)") + , F("Scroll up (2)") + , F("Scroll down (3)") + , F("Scroll left * (4)") + , F("Scroll right * (5)") + # if ENA_SPRITE + , F("Sprite (6)") + # endif // ENA_SPRITE + # if ENA_MISC + , F("Slice * (7)") + , F("Mesh (8)") + , F("Fade (9)") + , F("Dissolve (10)") + , F("Blinds (11)") + , F("Random (12)") + # endif // ENA_MISC + # if ENA_WIPE + , F("Wipe (13)") + , F("Wipe w. cursor (14)") + # endif // ENA_WIPE + # if ENA_SCAN + , F("Scan horiz. (15)") + , F("Scan horiz. cursor (16)") + , F("Scan vert. (17)") + , F("Scan vert. cursor (18)") + # endif // ENA_SCAN + # if ENA_OPNCLS + , F("Opening (19)") + , F("Opening w. cursor (20)") + , F("Closing (21)") + , F("Closing w. cursor (22)") + # endif // ENA_OPNCLS + # if ENA_SCR_DIA + , F("Scroll up left * (23)") + , F("Scroll up right * (24)") + , F("Scroll down left * (25)") + , F("Scroll down right * (26)") + # endif // ENA_SCR_DIA + # if ENA_GROW + , F("Grow up (27)") + , F("Grow down (28)") + # endif // ENA_GROW + }; + + const int animationOptions[] = { + static_cast(textEffect_t::PA_NO_EFFECT) + , static_cast(textEffect_t::PA_PRINT) + , static_cast(textEffect_t::PA_SCROLL_UP) + , static_cast(textEffect_t::PA_SCROLL_DOWN) + , static_cast(textEffect_t::PA_SCROLL_LEFT) + , static_cast(textEffect_t::PA_SCROLL_RIGHT) + # if ENA_SPRITE + , static_cast(textEffect_t::PA_SPRITE) + # endif // ENA_SPRITE + # if ENA_MISC + , static_cast(textEffect_t::PA_SLICE) + , static_cast(textEffect_t::PA_MESH) + , static_cast(textEffect_t::PA_FADE) + , static_cast(textEffect_t::PA_DISSOLVE) + , static_cast(textEffect_t::PA_BLINDS) + , static_cast(textEffect_t::PA_RANDOM) + # endif // ENA_MISC + # if ENA_WIPE + , static_cast(textEffect_t::PA_WIPE) + , static_cast(textEffect_t::PA_WIPE_CURSOR) + # endif // ENA_WIPE + # if ENA_SCAN + , static_cast(textEffect_t::PA_SCAN_HORIZ) + , static_cast(textEffect_t::PA_SCAN_HORIZX) + , static_cast(textEffect_t::PA_SCAN_VERT) + , static_cast(textEffect_t::PA_SCAN_VERTX) + # endif // ENA_SCAN + # if ENA_OPNCLS + , static_cast(textEffect_t::PA_OPENING) + , static_cast(textEffect_t::PA_OPENING_CURSOR) + , static_cast(textEffect_t::PA_CLOSING) + , static_cast(textEffect_t::PA_CLOSING_CURSOR) + # endif // ENA_OPNCLS + # if ENA_SCR_DIA + , static_cast(textEffect_t::PA_SCROLL_UP_LEFT) + , static_cast(textEffect_t::PA_SCROLL_UP_RIGHT) + , static_cast(textEffect_t::PA_SCROLL_DOWN_LEFT) + , static_cast(textEffect_t::PA_SCROLL_DOWN_RIGHT) + # endif // ENA_SCR_DIA + # if ENA_GROW + , static_cast(textEffect_t::PA_GROW_UP) + , static_cast(textEffect_t::PA_GROW_DOWN) + # endif // ENA_GROW + }; + + constexpr int animationCount = NR_ELEMENTS(animationOptions); + + delay(0); + + const __FlashStringHelper *fontTypes[] = { + F("Default (0)") + # ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT + , F("Numeric, double height (1)") + # endif // ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT + # ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT + , F("Full, double height (2)") + # endif // ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT + # ifdef P104_USE_VERTICAL_FONT + , F("Vertical (3)") + # endif // ifdef P104_USE_VERTICAL_FONT + # ifdef P104_USE_EXT_ASCII_FONT + , F("Extended ASCII (4)") + # endif // ifdef P104_USE_EXT_ASCII_FONT + # ifdef P104_USE_ARABIC_FONT + , F("Arabic (5)") + # endif // ifdef P104_USE_ARABIC_FONT + # ifdef P104_USE_GREEK_FONT + , F("Greek (6)") + # endif // ifdef P104_USE_GREEK_FONT + # ifdef P104_USE_KATAKANA_FONT + , F("Katakana (7)") + # endif // ifdef P104_USE_KATAKANA_FONT + }; + const int fontOptions[] = { + P104_DEFAULT_FONT_ID + # ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT + , P104_DOUBLE_HEIGHT_FONT_ID + # endif // ifdef P104_USE_NUMERIC_DOUBLEHEIGHT_FONT + # ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT + , P104_FULL_DOUBLEHEIGHT_FONT_ID + # endif // ifdef P104_USE_FULL_DOUBLEHEIGHT_FONT + # ifdef P104_USE_VERTICAL_FONT + , P104_VERTICAL_FONT_ID + # endif // ifdef P104_USE_VERTICAL_FONT + # ifdef P104_USE_EXT_ASCII_FONT + , P104_EXT_ASCII_FONT_ID + # endif // ifdef P104_USE_EXT_ASCII_FONT + # ifdef P104_USE_ARABIC_FONT + , P104_ARABIC_FONT_ID + # endif // ifdef P104_USE_ARABIC_FONT + # ifdef P104_USE_GREEK_FONT + , P104_GREEK_FONT_ID + # endif // ifdef P104_USE_GREEK_FONT + # ifdef P104_USE_KATAKANA_FONT + , P104_KATAKANA_FONT_ID + # endif // ifdef P104_USE_KATAKANA_FONT + }; + + const __FlashStringHelper *layoutTypes[] = { + F("Standard") + # if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) + , F("Double, upper") + , F("Double, lower") + # endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) + }; + const int layoutOptions[] = { + P104_LAYOUT_STANDARD + # if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) + , P104_LAYOUT_DOUBLE_UPPER + , P104_LAYOUT_DOUBLE_LOWER + # endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) || defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) + }; + + const __FlashStringHelper *specialEffectTypes[] = { + F("None"), + F("Flip up/down"), + F("Flip left/right *"), + F("Flip u/d & l/r *") + }; + const int specialEffectOptions[] = { + P104_SPECIAL_EFFECT_NONE, + P104_SPECIAL_EFFECT_UP_DOWN, + P104_SPECIAL_EFFECT_LEFT_RIGHT, + P104_SPECIAL_EFFECT_BOTH + }; + + const __FlashStringHelper *contentTypes[] = { + F("Text"), + F("Text reverse"), + F("Clock (4 mod)"), + F("Clock sec (6 mod)"), + F("Date (4 mod)"), + F("Date yr (6/7 mod)"), + F("Date/time (9/13 mod)"), + # ifdef P104_USE_BAR_GRAPH + F("Bar graph"), + # endif // ifdef P104_USE_BAR_GRAPH + }; + const int contentOptions[] { + P104_CONTENT_TEXT, + P104_CONTENT_TEXT_REV, + P104_CONTENT_TIME, + P104_CONTENT_TIME_SEC, + P104_CONTENT_DATE4, + P104_CONTENT_DATE6, + P104_CONTENT_DATE_TIME, + # ifdef P104_USE_BAR_GRAPH + P104_CONTENT_BAR_GRAPH, + # endif // ifdef P104_USE_BAR_GRAPH + }; + const __FlashStringHelper *invertedTypes[3] = { + F("Normal"), + F("Inverted") + }; + const int invertedOptions[] = { + 0, + 1 + }; + # ifdef P104_USE_ZONE_ACTIONS + uint8_t actionCount = 0; + const __FlashStringHelper *actionTypes[4]; + int actionOptions[4]; + actionTypes[actionCount] = F("None"); + actionOptions[actionCount] = P104_ACTION_NONE; + actionCount++; + + if (zones.size() < P104_MAX_ZONES) { + actionTypes[actionCount] = F("New above"); + actionOptions[actionCount] = P104_ACTION_ADD_ABOVE; + actionCount++; + actionTypes[actionCount] = F("New below"); + actionOptions[actionCount] = P104_ACTION_ADD_BELOW; + actionCount++; + } + actionTypes[actionCount] = F("Delete"); + actionOptions[actionCount] = P104_ACTION_DELETE; + actionCount++; + # endif // ifdef P104_USE_ZONE_ACTIONS + + delay(0); + + addFormSubHeader(F("Zone configuration")); + + { + html_table(EMPTY_STRING); // Sub-table + + const __FlashStringHelper *headers[] = { + F("Zone # "), + F("Modules"), + F("Text"), + F("Content"), + F("Alignment"), + F("Animation In/Out"), // 1st and 2nd row title + F("Speed/Pause"), // 1st and 2nd row title + F("Font/Layout"), // 1st and 2nd row title + F("Inverted/ Special Effects"), // 1st and 2nd row title + F("Offset"), + F("Brightness"), + F("Repeat (sec)") + }; + + constexpr unsigned nrHeaders = NR_ELEMENTS(headers); + for (unsigned i = 0; i < nrHeaders; ++i) { + int width = 0; + if (i == 2) { + // "Text" needs a width + width = 180; + } + html_table_header(headers[i], width); + } + # ifdef P104_USE_ZONE_ACTIONS + html_table_header(F(""), 15); // Spacer + html_table_header(F("Action"), 45); + # endif // ifdef P104_USE_ZONE_ACTIONS + } + + uint16_t index; + int16_t startZone, endZone; + int8_t incrZone = 1; + # ifdef P104_USE_ZONE_ACTIONS + uint8_t currentRow = 0; + # endif // ifdef P104_USE_ZONE_ACTIONS + + # ifdef P104_USE_ZONE_ORDERING + + if (bitRead(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_ZONE_ORDER)) { + startZone = zones.size() - 1; + endZone = -1; + incrZone = -1; + } else + # endif // ifdef P104_USE_ZONE_ORDERING + { + startZone = 0; + endZone = zones.size(); + } + + for (int8_t zone = startZone; zone != endZone; zone += incrZone) { + if (zones[zone].zone <= expectedZones) { + index = (zones[zone].zone - 1) * P104_OFFSET_COUNT; + + html_TR_TD(); // All columns use max. width available + addHtml(F(" ")); + addHtmlInt(zones[zone].zone); + + html_TD(); // Modules + addNumericBox(getPluginCustomArgName(index + P104_OFFSET_SIZE), zones[zone].size, 1, P104_MAX_MODULES_PER_ZONE); + + html_TD(); // Text + addTextBox(getPluginCustomArgName(index + P104_OFFSET_TEXT), + zones[zone].text, + P104_MAX_TEXT_LENGTH_PER_ZONE, + false, + false, + EMPTY_STRING, + F("")); + + html_TD(); // Content + addSelector(getPluginCustomArgName(index + P104_OFFSET_CONTENT), + P104_CONTENT_count, + contentTypes, + contentOptions, + nullptr, + zones[zone].content, + false, + true, + F("")); + + html_TD(); // Alignment + addSelector(getPluginCustomArgName(index + P104_OFFSET_ALIGNMENT), + 3, + alignmentTypes, + alignmentOptions, + nullptr, + zones[zone].alignment, + false, + true, + F("")); + + { + html_TD(); // Animation In (without None by passing the second element index) + addSelector(getPluginCustomArgName(index + P104_OFFSET_ANIM_IN), + animationCount - 1, + &animationTypes[1], + &animationOptions[1], + nullptr, + zones[zone].animationIn, + false, + true, + F("") + # ifdef P104_USE_TOOLTIPS + , F("Animation In") + # endif // ifdef P104_USE_TOOLTIPS + ); + } + + html_TD(); // Speed In + addNumericBox(getPluginCustomArgName(index + P104_OFFSET_SPEED), zones[zone].speed, 0, P104_MAX_SPEED_PAUSE_VALUE + # ifdef P104_USE_TOOLTIPS + , F("") // classname + , F("Speed") // title + # endif // ifdef P104_USE_TOOLTIPS + ); + + html_TD(); // Font + addSelector(getPluginCustomArgName(index + P104_OFFSET_FONT), + NR_ELEMENTS(fontOptions), + fontTypes, + fontOptions, + nullptr, + zones[zone].font, + false, + true, + F("") + # ifdef P104_USE_TOOLTIPS + , F("Font") // title + # endif // ifdef P104_USE_TOOLTIPS + ); + + html_TD(); // Inverted + addSelector(getPluginCustomArgName(index + P104_OFFSET_INVERTED), + NR_ELEMENTS(invertedOptions), + invertedTypes, + invertedOptions, + nullptr, + zones[zone].inverted, + false, + true, + F("") + # ifdef P104_USE_TOOLTIPS + , F("Inverted") // title + # endif // ifdef P104_USE_TOOLTIPS + ); + + html_TD(3); // Fill columns + # ifdef P104_USE_ZONE_ACTIONS + + html_TD(); // Spacer + addHtml('|'); + + if (currentRow < 2) { + addHtml(F("")); // Action column, text centered and font-size 90% + } else { + html_TD(); + } + + if (currentRow == 0) { + addHtml(F("(applied immediately!)")); + } else if (currentRow == 1) { + addHtml(F("(Delete can't be undone!)")); + } + currentRow++; + # endif // ifdef P104_USE_ZONE_ACTIONS + + // Split here + html_TR_TD(); // Start new row + html_TD(4); // Start with some blank columns + + { + html_TD(); // Animation Out + addSelector(getPluginCustomArgName(index + P104_OFFSET_ANIM_OUT), + animationCount, + animationTypes, + animationOptions, + nullptr, + zones[zone].animationOut, + false, + true, + F("") + # ifdef P104_USE_TOOLTIPS + , F("Animation Out") + # endif // ifdef P104_USE_TOOLTIPS + ); + } + + html_TD(); // Pause after Animation In + addNumericBox(getPluginCustomArgName(index + P104_OFFSET_PAUSE), zones[zone].pause, 0, P104_MAX_SPEED_PAUSE_VALUE + # ifdef P104_USE_TOOLTIPS + , F("") // classname + , F("Pause") // title + # endif // ifdef P104_USE_TOOLTIPS + ); + + html_TD(); // Layout + addSelector(getPluginCustomArgName(index + P104_OFFSET_LAYOUT), + NR_ELEMENTS(layoutOptions), + layoutTypes, + layoutOptions, + nullptr, + zones[zone].layout, + false, + true, + F("") + # ifdef P104_USE_TOOLTIPS + , F("Layout") // title + # endif // ifdef P104_USE_TOOLTIPS + ); + + html_TD(); // Special effects + addSelector(getPluginCustomArgName(index + P104_OFFSET_SPEC_EFFECT), + NR_ELEMENTS(specialEffectOptions), + specialEffectTypes, + specialEffectOptions, + nullptr, + zones[zone].specialEffect, + false, + true, + F("") + # ifdef P104_USE_TOOLTIPS + , F("Special Effects") // title + # endif // ifdef P104_USE_TOOLTIPS + ); + + html_TD(); // Offset + addNumericBox(getPluginCustomArgName(index + P104_OFFSET_OFFSET), zones[zone].offset, 0, 254); + + html_TD(); // Brightness + + if (zones[zone].brightness == -1) { zones[zone].brightness = P104_BRIGHTNESS_DEFAULT; } + addNumericBox(getPluginCustomArgName(index + P104_OFFSET_BRIGHTNESS), zones[zone].brightness, 0, P104_BRIGHTNESS_MAX); + + html_TD(); // Repeat (sec) + addNumericBox(getPluginCustomArgName(index + P104_OFFSET_REPEATDELAY), + zones[zone].repeatDelay, + -1, + P104_MAX_REPEATDELAY_VALUE // max delay 86400 sec. = 24 hours + # ifdef P104_USE_TOOLTIPS + , F("") // classname + , F("Repeat after this delay (sec), -1 = off") // tooltip + # endif // ifdef P104_USE_TOOLTIPS + ); + + # ifdef P104_USE_ZONE_ACTIONS + html_TD(); // Spacer + addHtml('|'); + + html_TD(); // Action + addSelector(getPluginCustomArgName(index + P104_OFFSET_ACTION), + actionCount, + actionTypes, + actionOptions, + nullptr, + P104_ACTION_NONE, // Always start with None + true, + true, + F("")); + # endif // ifdef P104_USE_ZONE_ACTIONS + + delay(0); + } + } + html_end_table(); + } + + # ifdef P104_ADD_SETTINGS_NOTES + addFormNote(concat(F("- Maximum nr. of modules possible (Zones * Size + Offset) = 255. Last saved: "), numDevices)); + addFormNote(F("- 'Animation In' or 'Animation Out' and 'Special Effects' marked with * should not be combined in a Zone.")); + # if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) && !defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) + addFormNote(F("- 'Layout' 'Double upper' and 'Double lower' are only supported for numeric 'Content' types like 'Clock' and 'Date'.")); + # endif // if defined(P104_USE_NUMERIC_DOUBLEHEIGHT_FONT) && !defined(P104_USE_FULL_DOUBLEHEIGHT_FONT) + # endif // ifdef P104_ADD_SETTINGS_NOTES + + return true; +} + +/************************************************************** +* webform_save +**************************************************************/ +bool P104_data_struct::webform_save(struct EventStruct *event) { + P104_CONFIG_ZONE_COUNT = getFormItemInt(F("zonecnt")); + P104_CONFIG_HARDWARETYPE = getFormItemInt(F("hardware")); + + bitWrite(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_CLEAR_DISABLE, isFormItemChecked(F("clrdsp"))); + bitWrite(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_LOG_ALL_TEXT, isFormItemChecked(F("logtxt"))); + + # ifdef P104_USE_ZONE_ORDERING + zoneOrder = getFormItemInt(F("zoneorder")); // Is used in saveSettings() + bitWrite(P104_CONFIG_FLAGS, P104_CONFIG_FLAG_ZONE_ORDER, zoneOrder == 1); + # endif // ifdef P104_USE_ZONE_ORDERING + + # ifdef P104_USE_DATETIME_OPTIONS + uint32_t ulDateTime = 0; + bitWrite(ulDateTime, P104_CONFIG_DATETIME_FLASH, !isFormItemChecked(F("clkflash"))); // Inverted flag + bitWrite(ulDateTime, P104_CONFIG_DATETIME_12H, isFormItemChecked(F("clk12h"))); + bitWrite(ulDateTime, P104_CONFIG_DATETIME_AMPM, isFormItemChecked(F("clkampm"))); + bitWrite(ulDateTime, P104_CONFIG_DATETIME_YEAR4DGT, isFormItemChecked(F("year4dgt"))); + set4BitToUL(ulDateTime, P104_CONFIG_DATETIME_FORMAT, getFormItemInt(F("datefmt"))); + set4BitToUL(ulDateTime, P104_CONFIG_DATETIME_SEP_CHAR, getFormItemInt(F("datesep"))); + P104_CONFIG_DATETIME = ulDateTime; + # endif // ifdef P104_USE_DATETIME_OPTIONS + + previousZones = expectedZones; + expectedZones = P104_CONFIG_ZONE_COUNT; + + bool result = saveSettings(); // Determines numDevices and re-fills zones list + + P104_CONFIG_ZONE_COUNT = zones.size(); + P104_CONFIG_TOTAL_UNITS = numDevices; // Store counted number of devices + + zones.clear(); // Free some memory (temporarily) + + return result; +} + + + + +P104_zone_struct::P104_zone_struct(uint8_t _zone) + : text(F("\"\"")), zone(_zone) {} + + +bool P104_zone_struct::getIntValue(uint8_t offset, int32_t& value) const +{ + switch (offset) { + case P104_OFFSET_SIZE: value = size; break; + case P104_OFFSET_TEXT: return false; + case P104_OFFSET_CONTENT: value = content; break; + case P104_OFFSET_ALIGNMENT: value = alignment; break; + case P104_OFFSET_ANIM_IN: value = animationIn; break; + case P104_OFFSET_SPEED: value = speed; break; + case P104_OFFSET_ANIM_OUT: value = animationOut; break; + case P104_OFFSET_PAUSE: value = pause; break; + case P104_OFFSET_FONT: value = font; break; + case P104_OFFSET_LAYOUT: value = layout; break; + case P104_OFFSET_SPEC_EFFECT: value = specialEffect; break; + case P104_OFFSET_OFFSET: value = offset; break; + case P104_OFFSET_BRIGHTNESS: value = brightness; break; + case P104_OFFSET_REPEATDELAY: value = repeatDelay; break; + case P104_OFFSET_INVERTED: value = inverted; break; + + default: + return false; + } + return true; +} + +bool P104_zone_struct::setIntValue(uint8_t offset, int32_t value) +{ + switch (offset) { + case P104_OFFSET_SIZE: size = value; break; + case P104_OFFSET_TEXT: return false; + case P104_OFFSET_CONTENT: content = value; break; + case P104_OFFSET_ALIGNMENT: alignment = value; break; + case P104_OFFSET_ANIM_IN: animationIn = value; break; + case P104_OFFSET_SPEED: speed = value; break; + case P104_OFFSET_ANIM_OUT: animationOut = value; break; + case P104_OFFSET_PAUSE: pause = value; break; + case P104_OFFSET_FONT: font = value; break; + case P104_OFFSET_LAYOUT: layout = value; break; + case P104_OFFSET_SPEC_EFFECT: specialEffect = value; break; + case P104_OFFSET_OFFSET: offset = value; break; + case P104_OFFSET_BRIGHTNESS: brightness = value; break; + case P104_OFFSET_REPEATDELAY: repeatDelay = value; break; + case P104_OFFSET_INVERTED: inverted = value; break; + + default: + return false; + } + return true; +} + + +#endif // ifdef USES_P104 diff --git a/src/src/PluginStructs/P147_data_struct.cpp b/src/src/PluginStructs/P147_data_struct.cpp index 5fb5c8e245..2ac3d39aa3 100644 --- a/src/src/PluginStructs/P147_data_struct.cpp +++ b/src/src/PluginStructs/P147_data_struct.cpp @@ -1,444 +1,444 @@ -#include "../PluginStructs/P147_data_struct.h" - -#ifdef USES_P147 - -# include "../Helpers/CRC_functions.h" - -/************************************************************************** -* Constructor -**************************************************************************/ -P147_data_struct::P147_data_struct(struct EventStruct *event) -{ - _sensorType = static_cast(P147_SENSOR_TYPE); - _initialCounter = P147_LOW_POWER_MEASURE == 0 ? P147_SHORT_COUNTER : P147_LONG_COUNTER; - _secondsCounter = _initialCounter; - _ignoreFirstRead = P147_LOW_POWER_MEASURE == 1; - _useCompensation = P147_GET_USE_COMPENSATION; - # if P147_FEATURE_GASINDEXALGORITHM - _rawOnly = P147_GET_RAW_DATA_ONLY; - # endif // if P147_FEATURE_GASINDEXALGORITHM - - if (validTaskIndex(P147_TEMPERATURE_TASK) && validTaskVarIndex(P147_TEMPERATURE_VALUE)) { - _temperatureValueIndex = P147_TEMPERATURE_TASK * VARS_PER_TASK + P147_TEMPERATURE_VALUE; - } - - if (validTaskIndex(P147_HUMIDITY_TASK) && validTaskVarIndex(P147_HUMIDITY_VALUE)) { - _humidityValueIndex = P147_HUMIDITY_TASK * VARS_PER_TASK + P147_HUMIDITY_VALUE; - } - _initialized = false; -} - -/***************************************************** - * Init sensor and supporting objects - ****************************************************/ -bool P147_data_struct::init(struct EventStruct *event) { - if (I2C_wakeup(P147_I2C_ADDRESS) == 0) { - _initialized = true; - - // TODO Add initialization of IndexAlgorithm objects - # if P147_FEATURE_GASINDEXALGORITHM - vocGasIndexAlgorithm = new VOCGasIndexAlgorithm((P147_LOW_POWER_MEASURE == 0 ? P147_SHORT_COUNTER : P147_LONG_COUNTER) * 1.0f); - - if (nullptr == vocGasIndexAlgorithm) { - _initialized = false; - } - - if (_initialized && (_sensorType == P147_sensor_e::SGP41)) { - noxGasIndexAlgorithm = new NOxGasIndexAlgorithm(); // Algorithm doesn't support a sampling interval - - if (nullptr == noxGasIndexAlgorithm) { - _initialized = false; - } - } - # endif // if P147_FEATURE_GASINDEXALGORITHM - - // Read serial number - if (_initialized && I2C_write8_reg(P147_I2C_ADDRESS, P147_CMD_READ_SERIALNR_A, P147_CMD_READ_SERIALNR_B)) { - uint16_t serialNumber[3] = { 0 }; - bool is_ok = true; - delay(1); - - for (uint8_t d = 0; d < 3 && is_ok; d++) { - serialNumber[d] = readCheckedWord(is_ok); - } - - if (is_ok) { - _serial = static_cast(serialNumber[0]); - _serial <<= 16; - _serial |= static_cast(serialNumber[1]); - _serial <<= 16; - _serial |= static_cast(serialNumber[2]); - } else { - _initialized = false; - } - addLog(LOG_LEVEL_INFO, concat(F("SGP4x: Serial number: 0x"), ull2String(_serial, 16u))); - # if P147_FEATURE_GASINDEXALGORITHM - - if (!_rawOnly) { - addLog(LOG_LEVEL_INFO, F("SGP4x: Attention: First values will be available after initial indexing!")); - } - # endif // if P147_FEATURE_GASINDEXALGORITHM - } - - if (_initialized && I2C_write8_reg(P147_I2C_ADDRESS, P147_CMD_SELF_TEST_A, P147_CMD_SELF_TEST_B)) { - _state = P147_state_e::MeasureTest; - Scheduler.setPluginTaskTimer(P147_DELAY_SELFTEST, event->TaskIndex, 0); // Retrieve selftest result after 320 msec - } - - // addLog(LOG_LEVEL_INFO, - // concat(F("P147 : INIT State: "), static_cast(_state)) + - // boolToString(_initialized)); - } - return isInitialized(); -} - -/***************************************************** -* Destructor -*****************************************************/ -P147_data_struct::~P147_data_struct() { - # if P147_FEATURE_GASINDEXALGORITHM - delete vocGasIndexAlgorithm; - delete noxGasIndexAlgorithm; - # endif // if P147_FEATURE_GASINDEXALGORITHM -} - -/***************************************************** -* plugin_tasktimer_in : Handle several delay related tasks -*****************************************************/ -bool P147_data_struct::plugin_tasktimer_in(struct EventStruct *event) { - bool success = false; - bool is_ok; - - if (isInitialized()) { - switch (_state) - { - case P147_state_e::MeasureTest: - { - const uint16_t result = readCheckedWord(is_ok); - - // addLog(LOG_LEVEL_INFO, concat(F("P147 : Selftest result: "), formatToHex(result)) + (is_ok ? F(" ok") : F(" error"))); - bool checkOk = false; - - if (_sensorType == P147_sensor_e::SGP40) { - checkOk = ((result >> 8) & 0xFF) == 0xD4; // 0xD4xx = OK, 0x4Bxx = Error - } else { - checkOk = (result & 0xFF) == 0x00; // 0xxx00 = OK 01..03 = Error - } - - if (is_ok && checkOk) { - success = true; - _state = P147_state_e::MeasureStart; // Start sequence - } else { - _state = P147_state_e::Uninitialized; - } - _initialized = success; // Failing selftest will disable the plugin - break; - } - - case P147_state_e::MeasureStart: - case P147_state_e::MeasureTrigger: - { - uint16_t compensationT = 0x6666; // Default values - uint16_t compensationRh = 0x8000; - float temperature = 25.0f; // Default values - float humidity = 50.0f; - - // addLog(LOG_LEVEL_INFO, F("P147 : MeasureStart")); - - if (_useCompensation && ((_temperatureValueIndex > -1) || (_humidityValueIndex > -1))) { - if (_temperatureValueIndex > -1) { - temperature = UserVar[_temperatureValueIndex]; - } - - if (_humidityValueIndex > -1) { - humidity = UserVar[_humidityValueIndex]; - } - - // Sanity checks - if (definitelyLessThan(temperature, -45.0f)) { temperature = -45.0f; } - - if (definitelyGreaterThan(temperature, 130.0f)) { temperature = 130.0f; } - - if (definitelyLessThan(humidity, 0.0f)) { humidity = 0.0f; } - - if (definitelyGreaterThan(humidity, 100.0f)) { humidity = 100.0f; } - - // Calculate ticks - compensationT = static_cast((temperature + 45) * 65535 / 175); - compensationRh = static_cast(humidity * 65535 / 100); - } - - if (startSensorRead(compensationRh, compensationT)) { - if ((_readLoop == 0) && (P147_LOW_POWER_MEASURE == 1) && (P147_state_e::MeasureTrigger != _state)) { - _readLoop = 1; // Skip first read after waking up - } - _state = P147_state_e::MeasureReading; // Starting a measurement also turns the heater on, just needs extra time - - Scheduler.setPluginTaskTimer((P147_LOW_POWER_MEASURE == 0 || _readLoop == 0) - ? (_sensorType == P147_sensor_e::SGP40 ? P147_DELAY_REGULAR : P147_DELAY_REGULAR_SGP41) - : P147_DELAY_LOW_POWER, - event->TaskIndex, - 0); - } - break; - } - - case P147_state_e::MeasureReading: // Get raw data - { - _rawVOC = readCheckedWord(is_ok); - - if (is_ok && (_lastCommand == P147_CMD_SGP41_READ_B) && (_sensorType == P147_sensor_e::SGP41)) { - _rawNOx = readCheckedWord(is_ok); - } - - // addLog(LOG_LEVEL_INFO, - // concat(F("P147 : MeasureReading raw VOC: "), _rawVOC) + - // concat(F(", raw NOx: "), _rawNOx) + - // concat(F(", loop: "), _readLoop) + - // (is_ok ? F(" ok") : F(" error"))); - - if (is_ok && (_readLoop == 0)) { - success = true; - _state = P147_state_e::Ready; - - # if P147_FEATURE_GASINDEXALGORITHM - - // Feed to normalizers - _vocIndex = vocGasIndexAlgorithm->process(_rawVOC); - - if (_vocIndex == 0) { - _skipCount++; - } - - if ((_lastCommand == P147_CMD_SGP41_READ_B) && (_sensorType == P147_sensor_e::SGP41)) { - _noxIndex = noxGasIndexAlgorithm->process(_rawNOx); - } - # endif // if P147_FEATURE_GASINDEXALGORITHM - - // Startup delay check for NOx measurement/normalizer - if ((_startupNOxCounter == 0) || (_sensorType == P147_sensor_e::SGP40)) { - _dataAvailable = true; // Data can be read - } - - if (P147_LOW_POWER_MEASURE == 1) { // Turn off heater - I2C_write8_reg(P147_I2C_ADDRESS, P147_CMD_HEATER_OFF_A, P147_CMD_HEATER_OFF_B); - } - Scheduler.setPluginTaskTimer(P147_DELAY_MINIMAL, event->TaskIndex, 0); // Next step - } else { - _state = P147_state_e::MeasureStart; // Restart from once_a_second - - if (_readLoop > 0) { - _state = P147_state_e::MeasureTrigger; // Trigger only - Scheduler.setPluginTaskTimer(_sensorType == P147_sensor_e::SGP40 ? P147_DELAY_REGULAR : P147_DELAY_REGULAR_SGP41, - event->TaskIndex, 0); // Trigger actual read after heating up - } - } - - if (_readLoop > 0) { _readLoop--; } - break; - } - - case P147_state_e::Ready: - _state = P147_state_e::MeasureStart; // When ready, start a new sequence from plugin_once_a_second - break; - - case P147_state_e::Uninitialized: // Keep compiler happy - break; - } - } - return success; -} - -/***************************************************** -* plugin_once_a_second -*****************************************************/ -bool P147_data_struct::plugin_once_a_second(struct EventStruct *event) { - bool success = false; - - // addLog(LOG_LEVEL_INFO, - // concat(F("P147 : State: "), static_cast(_state)) + - // concat(F(", Last _rawVOC: "), _rawVOC) + - // concat(F(", Last _rawNOx: "), _rawNOx) + - // concat(F(", count: "), _secondsCounter)); - - if (isInitialized()) { - _secondsCounter--; - - if (_startupNOxCounter > 0) { _startupNOxCounter--; } - - if (_secondsCounter == 0) { - // Execute a measurement cycle - if (_state == P147_state_e::MeasureStart) { - // Trigger a cycle - Scheduler.setPluginTaskTimer(P147_DELAY_MINIMAL, event->TaskIndex, 0); // Next step - success = true; - } - - // Reset counter - _secondsCounter = _initialCounter; - } - } - - return success; -} - -/***************************************************** -* plugin_read -*****************************************************/ -bool P147_data_struct::plugin_read(struct EventStruct *event) { - bool success = false; - - if (isInitialized()) { - if (_dataAvailable) { - if (_ignoreFirstRead) { - _ignoreFirstRead = false; - } else { - # if P147_FEATURE_GASINDEXALGORITHM - - if (_rawOnly) - # endif // if P147_FEATURE_GASINDEXALGORITHM - { - UserVar.setFloat(event->TaskIndex, 0, _rawVOC); - } - # if P147_FEATURE_GASINDEXALGORITHM - else { - UserVar.setFloat(event->TaskIndex, 0, _vocIndex); // Use normalized VOC index - } - # endif // if P147_FEATURE_GASINDEXALGORITHM - - if (_sensorType == P147_sensor_e::SGP41) { - # if P147_FEATURE_GASINDEXALGORITHM - - if (_rawOnly) - # endif // if P147_FEATURE_GASINDEXALGORITHM - { - UserVar.setFloat(event->TaskIndex, 1, _rawNOx); - } - # if P147_FEATURE_GASINDEXALGORITHM - else { - UserVar.setFloat(event->TaskIndex, 1, _noxIndex); // Use normalized NOx index - } - # endif // if P147_FEATURE_GASINDEXALGORITHM - } - # if P147_FEATURE_GASINDEXALGORITHM - success = (_rawOnly || _vocIndex != 0); // Accepted if the VOC index is no longer 0 (NOx index ignored for now) - - if (success && !_rawOnly && (_skipCount > 0)) { - addLog(LOG_LEVEL_INFO, concat(F("SGP4x: Valid values found, skipped samples: "), _skipCount)); - _skipCount = 0; - } - # else // if P147_FEATURE_GASINDEXALGORITHM - success = true; - # endif // if P147_FEATURE_GASINDEXALGORITHM - } - } - } - return success; -} - -/***************************************************** -* plugin_write -*****************************************************/ -bool P147_data_struct::plugin_write(struct EventStruct *event, - String & string) { - bool success = false; - - // const String command = parseString(string, 1); - - // if (equals(command, F("sgp4x"))) { - // // const String sub = parseString(string, 2); - // } - return success; -} - -/***************************************************** -* plugin_get_config_value -*****************************************************/ -bool P147_data_struct::plugin_get_config_value(struct EventStruct *event, - String & string) { - bool success = false; - - const String var = parseString(string, 1); - - if (equals(var, F("serialnumber"))) { // [#serialnumber] = the devices electronic serial number - string = ull2String(_serial); - success = true; - } else - if (equals(var, F("rawvoc"))) { // [#rawVOC] = the last raw VOC value retrieved from the sensor - string = _rawVOC; - success = true; - } else - if (equals(var, F("rawnox")) && - (_sensorType == P147_sensor_e::SGP41)) { // [#rawNOx] = the last raw NOx value retrieved from the sensor - string = _rawNOx; - success = true; - } - return success; -} - -// Private - -/***************************************************** - * readCheckedWord : Read 2 data bytes from I2C and validate checksum (3rd byte) - ****************************************************/ -uint16_t P147_data_struct::readCheckedWord(bool& is_ok, long extraDelay) { - uint16_t result = 0; - uint8_t data[3] = { 0 }; - const uint32_t timeOut = millis(); - - is_ok = false; - Wire.requestFrom(P147_I2C_ADDRESS, 3); - - while (Wire.available() != 3 && timePassedSince(timeOut) < extraDelay) { // Wait extra 5 msec. - delay(1); - } - - if (Wire.available() == 3) { - for (uint8_t d = 0; d < 3; d++) { - data[d] = Wire.read(); - } - - if (calc_CRC8(data, 2) == data[2]) { // valid checksum? - result = (data[0] << 8) | data[1]; - is_ok = true; - } - } - - return result; -} - -bool P147_data_struct::startSensorRead(uint16_t compensationRh, uint16_t compensationT) { - uint8_t data[2] = { 0 }; - - Wire.beginTransmission(P147_I2C_ADDRESS); // Start - - if (_sensorType == P147_sensor_e::SGP40) { - Wire.write((uint8_t)P147_CMD_SGP40_READ_A); // SGP40 Command - Wire.write((uint8_t)P147_CMD_SGP40_READ_B); - _lastCommand = P147_CMD_SGP40_READ_B; // Read only VOC - } else { - if (_startupNOxCounter == 0) { - Wire.write((uint8_t)P147_CMD_SGP41_READ_A); // SGP41 regular read Command - Wire.write((uint8_t)P147_CMD_SGP41_READ_B); - _lastCommand = P147_CMD_SGP41_READ_B; // Read VOC and NOx - } else { - Wire.write((uint8_t)P147_CMD_SGP41_COND_A); // SGP41 NOx Conditioning Command - Wire.write((uint8_t)P147_CMD_SGP41_COND_B); // Only raw VOC is returned - _lastCommand = P147_CMD_SGP41_COND_B; // Conditioning, read only VOC - } - } - data[0] = (compensationRh >> 8); - data[1] = (compensationRh & 0xFF); - Wire.write(data[0]); // Rel. humidity compensation - Wire.write(data[1]); - Wire.write(calc_CRC8(data, 2)); // crc - data[0] = (compensationT >> 8); - data[1] = (compensationT & 0xFF); - Wire.write(data[0]); // Temperature compensation - Wire.write(data[1]); - Wire.write(calc_CRC8(data, 2)); // crc - - return Wire.endTransmission() == 0; -} - -#endif // ifdef USES_P147 +#include "../PluginStructs/P147_data_struct.h" + +#ifdef USES_P147 + +# include "../Helpers/CRC_functions.h" + +/************************************************************************** +* Constructor +**************************************************************************/ +P147_data_struct::P147_data_struct(struct EventStruct *event) +{ + _sensorType = static_cast(P147_SENSOR_TYPE); + _initialCounter = P147_LOW_POWER_MEASURE == 0 ? P147_SHORT_COUNTER : P147_LONG_COUNTER; + _secondsCounter = _initialCounter; + _ignoreFirstRead = P147_LOW_POWER_MEASURE == 1; + _useCompensation = P147_GET_USE_COMPENSATION; + # if P147_FEATURE_GASINDEXALGORITHM + _rawOnly = P147_GET_RAW_DATA_ONLY; + # endif // if P147_FEATURE_GASINDEXALGORITHM + + if (validTaskIndex(P147_TEMPERATURE_TASK) && validTaskVarIndex(P147_TEMPERATURE_VALUE)) { + _temperatureValueIndex = P147_TEMPERATURE_TASK * VARS_PER_TASK + P147_TEMPERATURE_VALUE; + } + + if (validTaskIndex(P147_HUMIDITY_TASK) && validTaskVarIndex(P147_HUMIDITY_VALUE)) { + _humidityValueIndex = P147_HUMIDITY_TASK * VARS_PER_TASK + P147_HUMIDITY_VALUE; + } + _initialized = false; +} + +/***************************************************** + * Init sensor and supporting objects + ****************************************************/ +bool P147_data_struct::init(struct EventStruct *event) { + if (I2C_wakeup(P147_I2C_ADDRESS) == 0) { + _initialized = true; + + // TODO Add initialization of IndexAlgorithm objects + # if P147_FEATURE_GASINDEXALGORITHM + vocGasIndexAlgorithm = new (std::nothrow) VOCGasIndexAlgorithm((P147_LOW_POWER_MEASURE == 0 ? P147_SHORT_COUNTER : P147_LONG_COUNTER) * 1.0f); + + if (nullptr == vocGasIndexAlgorithm) { + _initialized = false; + } + + if (_initialized && (_sensorType == P147_sensor_e::SGP41)) { + noxGasIndexAlgorithm = new (std::nothrow) NOxGasIndexAlgorithm(); // Algorithm doesn't support a sampling interval + + if (nullptr == noxGasIndexAlgorithm) { + _initialized = false; + } + } + # endif // if P147_FEATURE_GASINDEXALGORITHM + + // Read serial number + if (_initialized && I2C_write8_reg(P147_I2C_ADDRESS, P147_CMD_READ_SERIALNR_A, P147_CMD_READ_SERIALNR_B)) { + uint16_t serialNumber[3] = { 0 }; + bool is_ok = true; + delay(1); + + for (uint8_t d = 0; d < 3 && is_ok; d++) { + serialNumber[d] = readCheckedWord(is_ok); + } + + if (is_ok) { + _serial = static_cast(serialNumber[0]); + _serial <<= 16; + _serial |= static_cast(serialNumber[1]); + _serial <<= 16; + _serial |= static_cast(serialNumber[2]); + } else { + _initialized = false; + } + addLog(LOG_LEVEL_INFO, concat(F("SGP4x: Serial number: 0x"), ull2String(_serial, 16u))); + # if P147_FEATURE_GASINDEXALGORITHM + + if (!_rawOnly) { + addLog(LOG_LEVEL_INFO, F("SGP4x: Attention: First values will be available after initial indexing!")); + } + # endif // if P147_FEATURE_GASINDEXALGORITHM + } + + if (_initialized && I2C_write8_reg(P147_I2C_ADDRESS, P147_CMD_SELF_TEST_A, P147_CMD_SELF_TEST_B)) { + _state = P147_state_e::MeasureTest; + Scheduler.setPluginTaskTimer(P147_DELAY_SELFTEST, event->TaskIndex, 0); // Retrieve selftest result after 320 msec + } + + // addLog(LOG_LEVEL_INFO, + // concat(F("P147 : INIT State: "), static_cast(_state)) + + // boolToString(_initialized)); + } + return isInitialized(); +} + +/***************************************************** +* Destructor +*****************************************************/ +P147_data_struct::~P147_data_struct() { + # if P147_FEATURE_GASINDEXALGORITHM + delete vocGasIndexAlgorithm; + delete noxGasIndexAlgorithm; + # endif // if P147_FEATURE_GASINDEXALGORITHM +} + +/***************************************************** +* plugin_tasktimer_in : Handle several delay related tasks +*****************************************************/ +bool P147_data_struct::plugin_tasktimer_in(struct EventStruct *event) { + bool success = false; + bool is_ok; + + if (isInitialized()) { + switch (_state) + { + case P147_state_e::MeasureTest: + { + const uint16_t result = readCheckedWord(is_ok); + + // addLog(LOG_LEVEL_INFO, concat(F("P147 : Selftest result: "), formatToHex(result)) + (is_ok ? F(" ok") : F(" error"))); + bool checkOk = false; + + if (_sensorType == P147_sensor_e::SGP40) { + checkOk = ((result >> 8) & 0xFF) == 0xD4; // 0xD4xx = OK, 0x4Bxx = Error + } else { + checkOk = (result & 0xFF) == 0x00; // 0xxx00 = OK 01..03 = Error + } + + if (is_ok && checkOk) { + success = true; + _state = P147_state_e::MeasureStart; // Start sequence + } else { + _state = P147_state_e::Uninitialized; + } + _initialized = success; // Failing selftest will disable the plugin + break; + } + + case P147_state_e::MeasureStart: + case P147_state_e::MeasureTrigger: + { + uint16_t compensationT = 0x6666; // Default values + uint16_t compensationRh = 0x8000; + float temperature = 25.0f; // Default values + float humidity = 50.0f; + + // addLog(LOG_LEVEL_INFO, F("P147 : MeasureStart")); + + if (_useCompensation && ((_temperatureValueIndex > -1) || (_humidityValueIndex > -1))) { + if (_temperatureValueIndex > -1) { + temperature = UserVar[_temperatureValueIndex]; + } + + if (_humidityValueIndex > -1) { + humidity = UserVar[_humidityValueIndex]; + } + + // Sanity checks + if (definitelyLessThan(temperature, -45.0f)) { temperature = -45.0f; } + + if (definitelyGreaterThan(temperature, 130.0f)) { temperature = 130.0f; } + + if (definitelyLessThan(humidity, 0.0f)) { humidity = 0.0f; } + + if (definitelyGreaterThan(humidity, 100.0f)) { humidity = 100.0f; } + + // Calculate ticks + compensationT = static_cast((temperature + 45) * 65535 / 175); + compensationRh = static_cast(humidity * 65535 / 100); + } + + if (startSensorRead(compensationRh, compensationT)) { + if ((_readLoop == 0) && (P147_LOW_POWER_MEASURE == 1) && (P147_state_e::MeasureTrigger != _state)) { + _readLoop = 1; // Skip first read after waking up + } + _state = P147_state_e::MeasureReading; // Starting a measurement also turns the heater on, just needs extra time + + Scheduler.setPluginTaskTimer((P147_LOW_POWER_MEASURE == 0 || _readLoop == 0) + ? (_sensorType == P147_sensor_e::SGP40 ? P147_DELAY_REGULAR : P147_DELAY_REGULAR_SGP41) + : P147_DELAY_LOW_POWER, + event->TaskIndex, + 0); + } + break; + } + + case P147_state_e::MeasureReading: // Get raw data + { + _rawVOC = readCheckedWord(is_ok); + + if (is_ok && (_lastCommand == P147_CMD_SGP41_READ_B) && (_sensorType == P147_sensor_e::SGP41)) { + _rawNOx = readCheckedWord(is_ok); + } + + // addLog(LOG_LEVEL_INFO, + // concat(F("P147 : MeasureReading raw VOC: "), _rawVOC) + + // concat(F(", raw NOx: "), _rawNOx) + + // concat(F(", loop: "), _readLoop) + + // (is_ok ? F(" ok") : F(" error"))); + + if (is_ok && (_readLoop == 0)) { + success = true; + _state = P147_state_e::Ready; + + # if P147_FEATURE_GASINDEXALGORITHM + + // Feed to normalizers + _vocIndex = vocGasIndexAlgorithm->process(_rawVOC); + + if (_vocIndex == 0) { + _skipCount++; + } + + if ((_lastCommand == P147_CMD_SGP41_READ_B) && (_sensorType == P147_sensor_e::SGP41)) { + _noxIndex = noxGasIndexAlgorithm->process(_rawNOx); + } + # endif // if P147_FEATURE_GASINDEXALGORITHM + + // Startup delay check for NOx measurement/normalizer + if ((_startupNOxCounter == 0) || (_sensorType == P147_sensor_e::SGP40)) { + _dataAvailable = true; // Data can be read + } + + if (P147_LOW_POWER_MEASURE == 1) { // Turn off heater + I2C_write8_reg(P147_I2C_ADDRESS, P147_CMD_HEATER_OFF_A, P147_CMD_HEATER_OFF_B); + } + Scheduler.setPluginTaskTimer(P147_DELAY_MINIMAL, event->TaskIndex, 0); // Next step + } else { + _state = P147_state_e::MeasureStart; // Restart from once_a_second + + if (_readLoop > 0) { + _state = P147_state_e::MeasureTrigger; // Trigger only + Scheduler.setPluginTaskTimer(_sensorType == P147_sensor_e::SGP40 ? P147_DELAY_REGULAR : P147_DELAY_REGULAR_SGP41, + event->TaskIndex, 0); // Trigger actual read after heating up + } + } + + if (_readLoop > 0) { _readLoop--; } + break; + } + + case P147_state_e::Ready: + _state = P147_state_e::MeasureStart; // When ready, start a new sequence from plugin_once_a_second + break; + + case P147_state_e::Uninitialized: // Keep compiler happy + break; + } + } + return success; +} + +/***************************************************** +* plugin_once_a_second +*****************************************************/ +bool P147_data_struct::plugin_once_a_second(struct EventStruct *event) { + bool success = false; + + // addLog(LOG_LEVEL_INFO, + // concat(F("P147 : State: "), static_cast(_state)) + + // concat(F(", Last _rawVOC: "), _rawVOC) + + // concat(F(", Last _rawNOx: "), _rawNOx) + + // concat(F(", count: "), _secondsCounter)); + + if (isInitialized()) { + _secondsCounter--; + + if (_startupNOxCounter > 0) { _startupNOxCounter--; } + + if (_secondsCounter == 0) { + // Execute a measurement cycle + if (_state == P147_state_e::MeasureStart) { + // Trigger a cycle + Scheduler.setPluginTaskTimer(P147_DELAY_MINIMAL, event->TaskIndex, 0); // Next step + success = true; + } + + // Reset counter + _secondsCounter = _initialCounter; + } + } + + return success; +} + +/***************************************************** +* plugin_read +*****************************************************/ +bool P147_data_struct::plugin_read(struct EventStruct *event) { + bool success = false; + + if (isInitialized()) { + if (_dataAvailable) { + if (_ignoreFirstRead) { + _ignoreFirstRead = false; + } else { + # if P147_FEATURE_GASINDEXALGORITHM + + if (_rawOnly) + # endif // if P147_FEATURE_GASINDEXALGORITHM + { + UserVar.setFloat(event->TaskIndex, 0, _rawVOC); + } + # if P147_FEATURE_GASINDEXALGORITHM + else { + UserVar.setFloat(event->TaskIndex, 0, _vocIndex); // Use normalized VOC index + } + # endif // if P147_FEATURE_GASINDEXALGORITHM + + if (_sensorType == P147_sensor_e::SGP41) { + # if P147_FEATURE_GASINDEXALGORITHM + + if (_rawOnly) + # endif // if P147_FEATURE_GASINDEXALGORITHM + { + UserVar.setFloat(event->TaskIndex, 1, _rawNOx); + } + # if P147_FEATURE_GASINDEXALGORITHM + else { + UserVar.setFloat(event->TaskIndex, 1, _noxIndex); // Use normalized NOx index + } + # endif // if P147_FEATURE_GASINDEXALGORITHM + } + # if P147_FEATURE_GASINDEXALGORITHM + success = (_rawOnly || _vocIndex != 0); // Accepted if the VOC index is no longer 0 (NOx index ignored for now) + + if (success && !_rawOnly && (_skipCount > 0)) { + addLog(LOG_LEVEL_INFO, concat(F("SGP4x: Valid values found, skipped samples: "), _skipCount)); + _skipCount = 0; + } + # else // if P147_FEATURE_GASINDEXALGORITHM + success = true; + # endif // if P147_FEATURE_GASINDEXALGORITHM + } + } + } + return success; +} + +/***************************************************** +* plugin_write +*****************************************************/ +bool P147_data_struct::plugin_write(struct EventStruct *event, + String & string) { + bool success = false; + + // const String command = parseString(string, 1); + + // if (equals(command, F("sgp4x"))) { + // // const String sub = parseString(string, 2); + // } + return success; +} + +/***************************************************** +* plugin_get_config_value +*****************************************************/ +bool P147_data_struct::plugin_get_config_value(struct EventStruct *event, + String & string) { + bool success = false; + + const String var = parseString(string, 1); + + if (equals(var, F("serialnumber"))) { // [#serialnumber] = the devices electronic serial number + string = ull2String(_serial); + success = true; + } else + if (equals(var, F("rawvoc"))) { // [#rawVOC] = the last raw VOC value retrieved from the sensor + string = _rawVOC; + success = true; + } else + if (equals(var, F("rawnox")) && + (_sensorType == P147_sensor_e::SGP41)) { // [#rawNOx] = the last raw NOx value retrieved from the sensor + string = _rawNOx; + success = true; + } + return success; +} + +// Private + +/***************************************************** + * readCheckedWord : Read 2 data bytes from I2C and validate checksum (3rd byte) + ****************************************************/ +uint16_t P147_data_struct::readCheckedWord(bool& is_ok, long extraDelay) { + uint16_t result = 0; + uint8_t data[3] = { 0 }; + const uint32_t timeOut = millis(); + + is_ok = false; + Wire.requestFrom(P147_I2C_ADDRESS, 3); + + while (Wire.available() != 3 && timePassedSince(timeOut) < extraDelay) { // Wait extra 5 msec. + delay(1); + } + + if (Wire.available() == 3) { + for (uint8_t d = 0; d < 3; d++) { + data[d] = Wire.read(); + } + + if (calc_CRC8(data, 2) == data[2]) { // valid checksum? + result = (data[0] << 8) | data[1]; + is_ok = true; + } + } + + return result; +} + +bool P147_data_struct::startSensorRead(uint16_t compensationRh, uint16_t compensationT) { + uint8_t data[2] = { 0 }; + + Wire.beginTransmission(P147_I2C_ADDRESS); // Start + + if (_sensorType == P147_sensor_e::SGP40) { + Wire.write((uint8_t)P147_CMD_SGP40_READ_A); // SGP40 Command + Wire.write((uint8_t)P147_CMD_SGP40_READ_B); + _lastCommand = P147_CMD_SGP40_READ_B; // Read only VOC + } else { + if (_startupNOxCounter == 0) { + Wire.write((uint8_t)P147_CMD_SGP41_READ_A); // SGP41 regular read Command + Wire.write((uint8_t)P147_CMD_SGP41_READ_B); + _lastCommand = P147_CMD_SGP41_READ_B; // Read VOC and NOx + } else { + Wire.write((uint8_t)P147_CMD_SGP41_COND_A); // SGP41 NOx Conditioning Command + Wire.write((uint8_t)P147_CMD_SGP41_COND_B); // Only raw VOC is returned + _lastCommand = P147_CMD_SGP41_COND_B; // Conditioning, read only VOC + } + } + data[0] = (compensationRh >> 8); + data[1] = (compensationRh & 0xFF); + Wire.write(data[0]); // Rel. humidity compensation + Wire.write(data[1]); + Wire.write(calc_CRC8(data, 2)); // crc + data[0] = (compensationT >> 8); + data[1] = (compensationT & 0xFF); + Wire.write(data[0]); // Temperature compensation + Wire.write(data[1]); + Wire.write(calc_CRC8(data, 2)); // crc + + return Wire.endTransmission() == 0; +} + +#endif // ifdef USES_P147 diff --git a/src/src/WebServer/UploadPage.cpp b/src/src/WebServer/UploadPage.cpp index 4256d42824..072ba1a468 100644 --- a/src/src/WebServer/UploadPage.cpp +++ b/src/src/WebServer/UploadPage.cpp @@ -1,258 +1,258 @@ -#include "../WebServer/UploadPage.h" - -#include "../WebServer/ESPEasy_WebServer.h" -#include "../WebServer/AccessControl.h" -#include "../WebServer/Markup_Buttons.h" -#include "../WebServer/HTML_wrappers.h" - -#include "../Globals/Cache.h" -#include "../Helpers/ESPEasy_Storage.h" -#if FEATURE_TARSTREAM_SUPPORT -# include "../Helpers/TarStream.h" -#endif // if FEATURE_TARSTREAM_SUPPORT -#include "../../ESPEasy-Globals.h" - - -#ifdef WEBSERVER_UPLOAD - -# ifndef FEATURE_UPLOAD_CLEANUP_CONFIG -# define FEATURE_UPLOAD_CLEANUP_CONFIG 1 // Enable/Disable removing of extcfg.dat files when a .tar file is uploaded having - // a valid config.dat included -# endif // ifndef FEATURE_UPLOAD_CLEANUP_CONFIG - -// ******************************************************************************** -// Web Interface upload page -// ******************************************************************************** -uploadResult_e uploadResult = uploadResult_e::UploadStarted; - -void handle_upload() { - if (!isLoggedIn()) { return; } - navMenuIndex = MENU_INDEX_TOOLS; - TXBuffer.startStream(); - sendHeadandTail_stdtemplate(_HEAD); - - addHtml(F( - "