From 8dd17451495fa96cc87fe6f9a145d7c041b35e86 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Thu, 5 Jan 2023 19:48:53 +0100 Subject: [PATCH 001/148] =?UTF-8?q?Add=20base=20battery=20=F0=9F=94=8B=20c?= =?UTF-8?q?lass,=20Add=20Lipo,=20Lion=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- usermods/Battery/battery.h | 149 +++++++++++++++++ usermods/Battery/battery_defaults.h | 11 +- usermods/Battery/lion.h | 37 +++++ usermods/Battery/lipo.h | 51 ++++++ usermods/Battery/usermod_v2_Battery.h | 221 +++++--------------------- 5 files changed, 290 insertions(+), 179 deletions(-) create mode 100644 usermods/Battery/battery.h create mode 100644 usermods/Battery/lion.h create mode 100644 usermods/Battery/lipo.h diff --git a/usermods/Battery/battery.h b/usermods/Battery/battery.h new file mode 100644 index 000000000..f3daf05c4 --- /dev/null +++ b/usermods/Battery/battery.h @@ -0,0 +1,149 @@ +#ifndef UMBBattery_h +#define UMBBattery_h + +#include "battery_defaults.h" + +/** + * Battery base class + * all other battery classes should inherit from this + */ +class Battery +{ + private: + + protected: + float minVoltage = USERMOD_BATTERY_MIN_VOLTAGE; + float maxVoltage = USERMOD_BATTERY_MAX_VOLTAGE; + unsigned int capacity = USERMOD_BATTERY_TOTAL_CAPACITY; // current capacity + float voltage = this->maxVoltage; // current voltage + int8_t level = 100; // current level + float calibration = USERMOD_BATTERY_CALIBRATION; // offset or calibration value to fine tune the calculated voltage + + float linearMapping(float v, float min, float max, float oMin = 0.0f, float oMax = 100.0f) + { + return (v-min) * (oMax-oMin) / (max-min) + oMin; + } + + public: + Battery() + { + + } + + /** + * Corresponding battery curves + * calculates the capacity in % (0-100) with given voltage and possible voltage range + */ + virtual float mapVoltage(float v, float min, float max) = 0; + // { + // example implementation, linear mapping + // return (v-min) * 100 / (max-min); + // }; + + virtual void calculateAndSetLevel(float voltage) = 0; + + + + /* + * + * Getter and Setter + * + */ + + /* + * Get lowest configured battery voltage + */ + virtual float getMinVoltage() + { + return this->minVoltage; + } + + /* + * Set lowest battery voltage + * can't be below 0 volt + */ + virtual void setMinVoltage(float voltage) + { + this->minVoltage = max(0.0f, voltage); + } + + /* + * Get highest configured battery voltage + */ + virtual float getMaxVoltage() + { + return this->maxVoltage; + } + + /* + * Set highest battery voltage + * can't be below minVoltage + */ + virtual void setMaxVoltage(float voltage) + { + #ifdef USERMOD_BATTERY_USE_LIPO + this->maxVoltage = max(getMinVoltage()+0.7f, voltage); + #else + this->maxVoltage = max(getMinVoltage()+1.0f, voltage); + #endif + } + + /* + * Get the capacity of all cells in parralel sumed up + * unit: mAh + */ + unsigned int getCapacity() + { + return this->capacity; + } + + void setCapacity(unsigned int capacity) + { + this->capacity = capacity; + } + + float getVoltage() + { + return this->voltage; + } + + /** + * check if voltage is within specified voltage range, allow 10% over/under voltage + */ + void setVoltage(float voltage) + { + this->voltage = ( (voltage < this->getMinVoltage() * 0.85f) || (voltage > this->getMaxVoltage() * 1.1f) ) + ? -1.0f + : voltage; + } + + float getLevel() + { + return this->level; + } + + void setLevel(float level) + { + this->level = constrain(level, 0.0f, 110.0f);; + } + + /* + * Get the configured calibration value + * a offset value to fine-tune the calculated voltage. + */ + virtual float getCalibration() + { + return calibration; + } + + /* + * Set the voltage calibration offset value + * a offset value to fine-tune the calculated voltage. + */ + virtual void setCalibration(float offset) + { + calibration = offset; + } +}; + +#endif \ No newline at end of file diff --git a/usermods/Battery/battery_defaults.h b/usermods/Battery/battery_defaults.h index c682cb45d..73f14f62a 100644 --- a/usermods/Battery/battery_defaults.h +++ b/usermods/Battery/battery_defaults.h @@ -14,6 +14,15 @@ #define USERMOD_BATTERY_MEASUREMENT_INTERVAL 30000 #endif + +/* Default Battery Type + * 1 = Lipo + * 2 = Lion + */ +#ifndef USERMOB_BATTERY_DEFAULT_TYPE + #define USERMOB_BATTERY_DEFAULT_TYPE 1 +#endif + // default for 18650 battery // https://batterybro.com/blogs/18650-wholesale-battery-reviews/18852515-when-to-recycle-18650-batteries-and-how-to-start-a-collection-center-in-your-vape-shop // Discharge voltage: 2.5 volt + .1 for personal safety @@ -69,4 +78,4 @@ #ifndef USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION #define USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION 5 -#endif \ No newline at end of file +#endif diff --git a/usermods/Battery/lion.h b/usermods/Battery/lion.h new file mode 100644 index 000000000..e8d78cc72 --- /dev/null +++ b/usermods/Battery/lion.h @@ -0,0 +1,37 @@ +#ifndef UMBLion_h +#define UMBLion_h + +#include "battery_defaults.h" +#include "battery.h" + +/** + * Lion Battery + * + */ +class Lion : public Battery +{ + private: + + public: + Lion() : Battery() + { + + } + + float mapVoltage(float v, float min, float max) override + { + return 0.0f; + }; + + void calculateAndSetLevel(float voltage) override + { + + }; + + virtual void setMaxVoltage(float voltage) override + { + this->maxVoltage = max(getMinVoltage()+1.0f, voltage); + } +}; + +#endif \ No newline at end of file diff --git a/usermods/Battery/lipo.h b/usermods/Battery/lipo.h new file mode 100644 index 000000000..4e9b0be7c --- /dev/null +++ b/usermods/Battery/lipo.h @@ -0,0 +1,51 @@ +#ifndef UMBLipo_h +#define UMBLipo_h + +#include "battery_defaults.h" +#include "battery.h" + +/** + * Lipo Battery + * + */ +class Lipo : public Battery +{ + private: + + public: + Lipo() : Battery() + { + + } + + /** + * LiPo batteries have a differnt dischargin curve, see + * https://blog.ampow.com/lipo-voltage-chart/ + */ + float mapVoltage(float v, float min, float max) override + { + float lvl = 0.0f; + lvl = this->linearMapping(v, min, max); // basic mapping + + if (lvl < 40.0f) + lvl = this->linearMapping(lvl, 0, 40, 0, 12); // last 45% -> drops very quickly + else { + if (lvl < 90.0f) + lvl = this->linearMapping(lvl, 40, 90, 12, 95); // 90% ... 40% -> almost linear drop + else // level > 90% + lvl = this->linearMapping(lvl, 90, 105, 95, 100); // highest 15% -> drop slowly + } + }; + + void calculateAndSetLevel(float voltage) override + { + this->setLevel(this->mapVoltage(voltage, this->getMinVoltage(), this->getMaxVoltage())); + }; + + virtual void setMaxVoltage(float voltage) override + { + this->maxVoltage = max(getMinVoltage()+0.7f, voltage); + } +}; + +#endif \ No newline at end of file diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index ac34a7e4d..4c77ca5dd 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -2,6 +2,9 @@ #include "wled.h" #include "battery_defaults.h" +#include "battery.h" +#include "lion.h" +#include "lipo.h" /* * Usermod by Maximilian Mewes @@ -15,28 +18,12 @@ class UsermodBattery : public Usermod private: // battery pin can be defined in my_config.h int8_t batteryPin = USERMOD_BATTERY_MEASUREMENT_PIN; + // Battery object + Battery* bat; // how often to read the battery voltage unsigned long readingInterval = USERMOD_BATTERY_MEASUREMENT_INTERVAL; unsigned long nextReadTime = 0; unsigned long lastReadTime = 0; - // battery min. voltage - float minBatteryVoltage = USERMOD_BATTERY_MIN_VOLTAGE; - // battery max. voltage - float maxBatteryVoltage = USERMOD_BATTERY_MAX_VOLTAGE; - // all battery cells summed up - unsigned int totalBatteryCapacity = USERMOD_BATTERY_TOTAL_CAPACITY; - // raw analog reading - float rawValue = 0.0f; - // calculated voltage - float voltage = maxBatteryVoltage; - // mapped battery level based on voltage - int8_t batteryLevel = 100; - // offset or calibration value to fine tune the calculated voltage - float calibration = USERMOD_BATTERY_CALIBRATION; - - // time left estimation feature - // bool calculateTimeLeftEnabled = USERMOD_BATTERY_CALCULATE_TIME_LEFT_ENABLED; - // float estimatedTimeLeft = 0.0; // auto shutdown/shutoff/master off feature bool autoOffEnabled = USERMOD_BATTERY_AUTO_OFF_ENABLED; @@ -63,14 +50,6 @@ class UsermodBattery : public Usermod static const char _preset[]; static const char _duration[]; static const char _init[]; - - - // custom map function - // https://forum.arduino.cc/t/floating-point-using-map-function/348113/2 - double mapf(double x, double in_min, double in_max, double out_min, double out_max) - { - return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; - } float dot2round(float x) { @@ -94,8 +73,8 @@ class UsermodBattery : public Usermod { if (!lowPowerIndicatorEnabled) return; if (batteryPin < 0) return; // no measurement - if (lowPowerIndicationDone && lowPowerIndicatorReactivationThreshold <= batteryLevel) lowPowerIndicationDone = false; - if (lowPowerIndicatorThreshold <= batteryLevel) return; + if (lowPowerIndicationDone && lowPowerIndicatorReactivationThreshold <= bat->getLevel()) lowPowerIndicationDone = false; + if (lowPowerIndicatorThreshold <= bat->getLevel()) return; if (lowPowerIndicationDone) return; if (lowPowerActivationTime <= 1) { lowPowerActivationTime = millis(); @@ -139,6 +118,16 @@ class UsermodBattery : public Usermod pinMode(batteryPin, INPUT); #endif + // this could also be handled with a factory class but for only 2 types now it should be sufficient + if(USERMOB_BATTERY_DEFAULT_TYPE == 1) { + bat = new Lipo(); + } else + if(USERMOB_BATTERY_DEFAULT_TYPE == 2) { + bat = new Lion(); + } else { + bat = new Lipo(); + } + nextReadTime = millis() + readingInterval; lastReadTime = millis(); @@ -174,8 +163,9 @@ class UsermodBattery : public Usermod if (batteryPin < 0) return; // nothing to read - initializing = false; - + initializing = false; + float voltage = -1.0f; + float rawValue = 0.0f; #ifdef ARDUINO_ARCH_ESP32 // use calibrated millivolts analogread on esp32 (150 mV ~ 2450 mV) rawValue = analogReadMilliVolts(batteryPin); @@ -188,40 +178,15 @@ class UsermodBattery : public Usermod rawValue = analogRead(batteryPin); // calculate the voltage - voltage = ((rawValue / getAdcPrecision()) * maxBatteryVoltage) + calibration; + voltage = ((rawValue / getAdcPrecision()) * bat->getMaxVoltage()) + bat->getCalibration(); #endif - // check if voltage is within specified voltage range, allow 10% over/under voltage - voltage = ((voltage < minBatteryVoltage * 0.85f) || (voltage > maxBatteryVoltage * 1.1f)) ? -1.0f : voltage; + bat->setVoltage(voltage); // translate battery voltage into percentage - /* - the standard "map" function doesn't work - https://www.arduino.cc/reference/en/language/functions/math/map/ notes and warnings at the bottom - */ - #ifdef USERMOD_BATTERY_USE_LIPO - batteryLevel = mapf(voltage, minBatteryVoltage, maxBatteryVoltage, 0, 100); // basic mapping - // LiPo batteries have a differnt dischargin curve, see - // https://blog.ampow.com/lipo-voltage-chart/ - if (batteryLevel < 40.0f) - batteryLevel = mapf(batteryLevel, 0, 40, 0, 12); // last 45% -> drops very quickly - else { - if (batteryLevel < 90.0f) - batteryLevel = mapf(batteryLevel, 40, 90, 12, 95); // 90% ... 40% -> almost linear drop - else // level > 90% - batteryLevel = mapf(batteryLevel, 90, 105, 95, 100); // highest 15% -> drop slowly - } - #else - batteryLevel = mapf(voltage, minBatteryVoltage, maxBatteryVoltage, 0, 100); - #endif - if (voltage > -1.0f) batteryLevel = constrain(batteryLevel, 0.0f, 110.0f); - - // if (calculateTimeLeftEnabled) { - // float currentBatteryCapacity = totalBatteryCapacity; - // estimatedTimeLeft = (currentBatteryCapacity/strip.currentMilliamps)*60; - // } + bat->calculateAndSetLevel(voltage); // Auto off -- Master power off - if (autoOffEnabled && (autoOffThreshold >= batteryLevel)) + if (autoOffEnabled && (autoOffThreshold >= bat->getLevel())) turnOff(); // SmartHome stuff @@ -254,16 +219,6 @@ class UsermodBattery : public Usermod // info modal display names JsonArray infoPercentage = user.createNestedArray(F("Battery level")); JsonArray infoVoltage = user.createNestedArray(F("Battery voltage")); - // if (calculateTimeLeftEnabled) - // { - // JsonArray infoEstimatedTimeLeft = user.createNestedArray(F("Estimated time left")); - // if (initializing) { - // infoEstimatedTimeLeft.add(FPSTR(_init)); - // } else { - // infoEstimatedTimeLeft.add(estimatedTimeLeft); - // infoEstimatedTimeLeft.add(F(" min")); - // } - // } JsonArray infoNextUpdate = user.createNestedArray(F("Next update")); infoNextUpdate.add((nextReadTime - millis()) / 1000); @@ -275,17 +230,17 @@ class UsermodBattery : public Usermod return; } - if (batteryLevel < 0) { + if (bat->getLevel() < 0) { infoPercentage.add(F("invalid")); } else { - infoPercentage.add(batteryLevel); + infoPercentage.add(bat->getLevel()); } infoPercentage.add(F(" %")); - if (voltage < 0) { + if (bat->getVoltage() < 0) { infoVoltage.add(F("invalid")); } else { - infoVoltage.add(dot2round(voltage)); + infoVoltage.add(dot2round(bat->getVoltage())); } infoVoltage.add(F(" V")); } @@ -298,7 +253,7 @@ class UsermodBattery : public Usermod /* void addToJsonState(JsonObject& root) { - + // TBD } */ @@ -310,6 +265,7 @@ class UsermodBattery : public Usermod /* void readFromJsonState(JsonObject& root) { + // TBD } */ @@ -356,18 +312,17 @@ class UsermodBattery : public Usermod battery[F("pin")] = batteryPin; #endif - // battery[F("time-left")] = calculateTimeLeftEnabled; - battery[F("min-voltage")] = minBatteryVoltage; - battery[F("max-voltage")] = maxBatteryVoltage; - battery[F("capacity")] = totalBatteryCapacity; - battery[F("calibration")] = calibration; + battery[F("min-voltage")] = bat->getMinVoltage(); + battery[F("max-voltage")] = bat->getMaxVoltage(); + battery[F("capacity")] = bat->getCapacity(); + battery[F("calibration")] = bat->getCalibration(); battery[FPSTR(_readInterval)] = readingInterval; - JsonObject ao = battery.createNestedObject(F("auto-off")); // auto off section + JsonObject ao = battery.createNestedObject(F("auto-off")); // auto off section ao[FPSTR(_enabled)] = autoOffEnabled; ao[FPSTR(_threshold)] = autoOffThreshold; - JsonObject lp = battery.createNestedObject(F("indicator")); // low power section + JsonObject lp = battery.createNestedObject(F("indicator")); // low power section lp[FPSTR(_enabled)] = lowPowerIndicatorEnabled; lp[FPSTR(_preset)] = lowPowerIndicatorPreset; // dropdown trickery (String)lowPowerIndicatorPreset; lp[FPSTR(_threshold)] = lowPowerIndicatorThreshold; @@ -432,11 +387,11 @@ class UsermodBattery : public Usermod #ifdef ARDUINO_ARCH_ESP32 newBatteryPin = battery[F("pin")] | newBatteryPin; #endif - // calculateTimeLeftEnabled = battery[F("time-left")] | calculateTimeLeftEnabled; - setMinBatteryVoltage(battery[F("min-voltage")] | minBatteryVoltage); - setMaxBatteryVoltage(battery[F("max-voltage")] | maxBatteryVoltage); - setTotalBatteryCapacity(battery[F("capacity")] | totalBatteryCapacity); - setCalibration(battery[F("calibration")] | calibration); + + bat->setMinVoltage(battery[F("min-voltage")] | bat->getMinVoltage()); + bat->setMaxVoltage(battery[F("max-voltage")] | bat->getMaxVoltage()); + bat->setCapacity(battery[F("capacity")] | bat->getCapacity()); + bat->setCalibration(battery[F("calibration")] | bat->getCalibration()); setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); JsonObject ao = battery[F("auto-off")]; @@ -479,7 +434,8 @@ class UsermodBattery : public Usermod } /* - * Generate a preset sample for low power indication + * TBD: Generate a preset sample for low power indication + * a button on the config page would be cool, currently not possible */ void generateExamplePreset() { @@ -539,60 +495,6 @@ class UsermodBattery : public Usermod readingInterval = max((unsigned long)3000, newReadingInterval); } - - /* - * Get lowest configured battery voltage - */ - float getMinBatteryVoltage() - { - return minBatteryVoltage; - } - - /* - * Set lowest battery voltage - * can't be below 0 volt - */ - void setMinBatteryVoltage(float voltage) - { - minBatteryVoltage = max(0.0f, voltage); - } - - /* - * Get highest configured battery voltage - */ - float getMaxBatteryVoltage() - { - return maxBatteryVoltage; - } - - /* - * Set highest battery voltage - * can't be below minBatteryVoltage - */ - void setMaxBatteryVoltage(float voltage) - { - #ifdef USERMOD_BATTERY_USE_LIPO - maxBatteryVoltage = max(getMinBatteryVoltage()+0.7f, voltage); - #else - maxBatteryVoltage = max(getMinBatteryVoltage()+1.0f, voltage); - #endif - } - - - /* - * Get the capacity of all cells in parralel sumed up - * unit: mAh - */ - unsigned int getTotalBatteryCapacity() - { - return totalBatteryCapacity; - } - - void setTotalBatteryCapacity(unsigned int capacity) - { - totalBatteryCapacity = capacity; - } - /* * Get the choosen adc precision * esp8266 = 10bit resolution = 1024.0f @@ -609,43 +511,6 @@ class UsermodBattery : public Usermod #endif } - /* - * Get the calculated voltage - * formula: (adc pin value / adc precision * max voltage) + calibration - */ - float getVoltage() - { - return voltage; - } - - /* - * Get the mapped battery level (0 - 100) based on voltage - * important: voltage can drop when a load is applied, so its only an estimate - */ - int8_t getBatteryLevel() - { - return batteryLevel; - } - - /* - * Get the configured calibration value - * a offset value to fine-tune the calculated voltage. - */ - float getCalibration() - { - return calibration; - } - - /* - * Set the voltage calibration offset value - * a offset value to fine-tune the calculated voltage. - */ - void setCalibration(float offset) - { - calibration = offset; - } - - /* * Get auto-off feature enabled status * is auto-off enabled, true/false From 4c8b490c89635a647b9b7e30a519b8a358a1fc47 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Thu, 5 Jan 2023 20:38:55 +0100 Subject: [PATCH 002/148] minor changes --- usermods/Battery/battery.h | 6 +----- usermods/Battery/battery_defaults.h | 7 +------ usermods/Battery/lion.h | 4 ++-- usermods/Battery/lipo.h | 2 ++ usermods/Battery/usermod_v2_Battery.h | 18 ++++++++++++++---- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/usermods/Battery/battery.h b/usermods/Battery/battery.h index f3daf05c4..c678f775d 100644 --- a/usermods/Battery/battery.h +++ b/usermods/Battery/battery.h @@ -81,11 +81,7 @@ class Battery */ virtual void setMaxVoltage(float voltage) { - #ifdef USERMOD_BATTERY_USE_LIPO - this->maxVoltage = max(getMinVoltage()+0.7f, voltage); - #else - this->maxVoltage = max(getMinVoltage()+1.0f, voltage); - #endif + this->maxVoltage = max(getMinVoltage()+.5f, voltage); } /* diff --git a/usermods/Battery/battery_defaults.h b/usermods/Battery/battery_defaults.h index 73f14f62a..4a04ac352 100644 --- a/usermods/Battery/battery_defaults.h +++ b/usermods/Battery/battery_defaults.h @@ -27,7 +27,7 @@ // https://batterybro.com/blogs/18650-wholesale-battery-reviews/18852515-when-to-recycle-18650-batteries-and-how-to-start-a-collection-center-in-your-vape-shop // Discharge voltage: 2.5 volt + .1 for personal safety #ifndef USERMOD_BATTERY_MIN_VOLTAGE - #ifdef USERMOD_BATTERY_USE_LIPO + #if USERMOB_BATTERY_DEFAULT_TYPE == 1 // LiPo "1S" Batteries should not be dischared below 3V !! #define USERMOD_BATTERY_MIN_VOLTAGE 3.2f #else @@ -49,11 +49,6 @@ #define USERMOD_BATTERY_CALIBRATION 0 #endif -// calculate remaining time / the time that is left before the battery runs out of power -// #ifndef USERMOD_BATTERY_CALCULATE_TIME_LEFT_ENABLED -// #define USERMOD_BATTERY_CALCULATE_TIME_LEFT_ENABLED false -// #endif - // auto-off feature #ifndef USERMOD_BATTERY_AUTO_OFF_ENABLED #define USERMOD_BATTERY_AUTO_OFF_ENABLED true diff --git a/usermods/Battery/lion.h b/usermods/Battery/lion.h index e8d78cc72..69095ac09 100644 --- a/usermods/Battery/lion.h +++ b/usermods/Battery/lion.h @@ -20,12 +20,12 @@ class Lion : public Battery float mapVoltage(float v, float min, float max) override { - return 0.0f; + return this->linearMapping(v, min, max); // basic mapping }; void calculateAndSetLevel(float voltage) override { - + this->setLevel(this->mapVoltage(voltage, this->getMinVoltage(), this->getMaxVoltage())); }; virtual void setMaxVoltage(float voltage) override diff --git a/usermods/Battery/lipo.h b/usermods/Battery/lipo.h index 4e9b0be7c..92ede8f81 100644 --- a/usermods/Battery/lipo.h +++ b/usermods/Battery/lipo.h @@ -35,6 +35,8 @@ class Lipo : public Battery else // level > 90% lvl = this->linearMapping(lvl, 90, 105, 95, 100); // highest 15% -> drop slowly } + + return lvl; }; void calculateAndSetLevel(float voltage) override diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index 4c77ca5dd..87b73ec6d 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -18,8 +18,11 @@ class UsermodBattery : public Usermod private: // battery pin can be defined in my_config.h int8_t batteryPin = USERMOD_BATTERY_MEASUREMENT_PIN; + + int8_t batteryType = USERMOB_BATTERY_DEFAULT_TYPE; // Battery object Battery* bat; + // how often to read the battery voltage unsigned long readingInterval = USERMOD_BATTERY_MEASUREMENT_INTERVAL; unsigned long nextReadTime = 0; @@ -118,14 +121,14 @@ class UsermodBattery : public Usermod pinMode(batteryPin, INPUT); #endif - // this could also be handled with a factory class but for only 2 types now it should be sufficient - if(USERMOB_BATTERY_DEFAULT_TYPE == 1) { + // this could also be handled with a factory class but for only 2 types it should be sufficient for now + if(batteryType == 1) { bat = new Lipo(); } else - if(USERMOB_BATTERY_DEFAULT_TYPE == 2) { + if(batteryType == 2) { bat = new Lion(); } else { - bat = new Lipo(); + bat = new Lipo(); // in the future one could create a nullObject } nextReadTime = millis() + readingInterval; @@ -317,6 +320,10 @@ class UsermodBattery : public Usermod battery[F("capacity")] = bat->getCapacity(); battery[F("calibration")] = bat->getCalibration(); battery[FPSTR(_readInterval)] = readingInterval; + + // JsonArray type = battery[F("Type")]; + // type[0] = 1; + // type[1] = 2; JsonObject ao = battery.createNestedObject(F("auto-off")); // auto off section ao[FPSTR(_enabled)] = autoOffEnabled; @@ -393,6 +400,9 @@ class UsermodBattery : public Usermod bat->setCapacity(battery[F("capacity")] | bat->getCapacity()); bat->setCalibration(battery[F("calibration")] | bat->getCalibration()); setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); + + // JsonArray type = battery[F("Type")]; + // batteryType = type["bt"] | batteryType; JsonObject ao = battery[F("auto-off")]; setAutoOffEnabled(ao[FPSTR(_enabled)] | autoOffEnabled); From 85d59945a0ffaaa8907ce69cc1cb3f7548de2775 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Fri, 6 Jan 2023 00:19:16 +0100 Subject: [PATCH 003/148] =?UTF-8?q?runtime=20exception=20fix=20?= =?UTF-8?q?=F0=9F=90=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- usermods/Battery/usermod_v2_Battery.h | 38 ++++++++++++++++----------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index 87b73ec6d..f9bfc96fb 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -20,9 +20,15 @@ class UsermodBattery : public Usermod int8_t batteryPin = USERMOD_BATTERY_MEASUREMENT_PIN; int8_t batteryType = USERMOB_BATTERY_DEFAULT_TYPE; - // Battery object - Battery* bat; - + + float minVoltage = USERMOD_BATTERY_MIN_VOLTAGE; + float maxVoltage = USERMOD_BATTERY_MAX_VOLTAGE; + unsigned int capacity = USERMOD_BATTERY_TOTAL_CAPACITY; // current capacity + float voltage = this->maxVoltage; // current voltage + int8_t level = 100; // current level + float calibration = USERMOD_BATTERY_CALIBRATION; // offset or calibration value to fine tune the calculated voltage + Battery* bat = nullptr; + // how often to read the battery voltage unsigned long readingInterval = USERMOD_BATTERY_MEASUREMENT_INTERVAL; unsigned long nextReadTime = 0; @@ -121,7 +127,7 @@ class UsermodBattery : public Usermod pinMode(batteryPin, INPUT); #endif - // this could also be handled with a factory class but for only 2 types it should be sufficient for now + //this could also be handled with a factory class but for only 2 types it should be sufficient for now if(batteryType == 1) { bat = new Lipo(); } else @@ -315,15 +321,13 @@ class UsermodBattery : public Usermod battery[F("pin")] = batteryPin; #endif - battery[F("min-voltage")] = bat->getMinVoltage(); - battery[F("max-voltage")] = bat->getMaxVoltage(); - battery[F("capacity")] = bat->getCapacity(); - battery[F("calibration")] = bat->getCalibration(); + if(bat) { + battery[F("min-voltage")] = bat->getMinVoltage(); + battery[F("max-voltage")] = bat->getMaxVoltage(); + battery[F("capacity")] = bat->getCapacity(); + battery[F("calibration")] = bat->getCalibration(); + } battery[FPSTR(_readInterval)] = readingInterval; - - // JsonArray type = battery[F("Type")]; - // type[0] = 1; - // type[1] = 2; JsonObject ao = battery.createNestedObject(F("auto-off")); // auto off section ao[FPSTR(_enabled)] = autoOffEnabled; @@ -395,10 +399,12 @@ class UsermodBattery : public Usermod newBatteryPin = battery[F("pin")] | newBatteryPin; #endif - bat->setMinVoltage(battery[F("min-voltage")] | bat->getMinVoltage()); - bat->setMaxVoltage(battery[F("max-voltage")] | bat->getMaxVoltage()); - bat->setCapacity(battery[F("capacity")] | bat->getCapacity()); - bat->setCalibration(battery[F("calibration")] | bat->getCalibration()); + if(bat) { + bat->setMinVoltage(battery[F("min-voltage")] | bat->getMinVoltage()); + bat->setMaxVoltage(battery[F("max-voltage")] | bat->getMaxVoltage()); + bat->setCapacity(battery[F("capacity")] | bat->getCapacity()); + bat->setCalibration(battery[F("calibration")] | bat->getCalibration()); + } setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); // JsonArray type = battery[F("Type")]; From 375907144966e37e9a8dad3efc92343fec892052 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Fri, 6 Jan 2023 17:00:29 +0100 Subject: [PATCH 004/148] =?UTF-8?q?Fix=20previous=20bug=20again=20?= =?UTF-8?q?=F0=9F=90=9B,=20Add=20Type=20Dropdown=20to=20config=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- usermods/Battery/battery.h | 21 +++-- usermods/Battery/battery_defaults.h | 111 +++++++++++++++++++------- usermods/Battery/lion.h | 8 +- usermods/Battery/lipo.h | 10 ++- usermods/Battery/unkown.h | 36 +++++++++ usermods/Battery/usermod_v2_Battery.h | 62 +++++++------- 6 files changed, 178 insertions(+), 70 deletions(-) create mode 100644 usermods/Battery/unkown.h diff --git a/usermods/Battery/battery.h b/usermods/Battery/battery.h index c678f775d..ad3fb2703 100644 --- a/usermods/Battery/battery.h +++ b/usermods/Battery/battery.h @@ -12,12 +12,12 @@ class Battery private: protected: - float minVoltage = USERMOD_BATTERY_MIN_VOLTAGE; - float maxVoltage = USERMOD_BATTERY_MAX_VOLTAGE; - unsigned int capacity = USERMOD_BATTERY_TOTAL_CAPACITY; // current capacity - float voltage = this->maxVoltage; // current voltage - int8_t level = 100; // current level - float calibration = USERMOD_BATTERY_CALIBRATION; // offset or calibration value to fine tune the calculated voltage + float minVoltage; + float maxVoltage; + unsigned int capacity; + float voltage; + int8_t level = 100; + float calibration; // offset or calibration value to fine tune the calculated voltage float linearMapping(float v, float min, float max, float oMin = 0.0f, float oMax = 100.0f) { @@ -30,6 +30,15 @@ class Battery } + virtual void update(batteryConfig cfg) + { + if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); + if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); + if(cfg.calibration) this->setCapacity(cfg.calibration); + if(cfg.level) this->setLevel(cfg.level); + if(cfg.calibration) this->setCalibration(cfg.calibration); + } + /** * Corresponding battery curves * calculates the capacity in % (0-100) with given voltage and possible voltage range diff --git a/usermods/Battery/battery_defaults.h b/usermods/Battery/battery_defaults.h index 4a04ac352..f4060ca60 100644 --- a/usermods/Battery/battery_defaults.h +++ b/usermods/Battery/battery_defaults.h @@ -1,3 +1,6 @@ +#ifndef UMBDefaults_h +#define UMBDefaults_h + // pin defaults // for the esp32 it is best to use the ADC1: GPIO32 - GPIO39 // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/adc.html @@ -16,38 +19,70 @@ /* Default Battery Type + * 0 = unkown * 1 = Lipo * 2 = Lion */ -#ifndef USERMOB_BATTERY_DEFAULT_TYPE - #define USERMOB_BATTERY_DEFAULT_TYPE 1 +#ifndef USERMOD_BATTERY_DEFAULT_TYPE + #define USERMOD_BATTERY_DEFAULT_TYPE 0 +#endif +/* + * + * Unkown 'Battery' defaults + * + */ +#ifndef USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE + #define USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE 3.3f +#endif +#ifndef USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE + #define USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE 4.2f +#endif +#ifndef USERMOD_BATTERY_UNKOWN_CAPACITY + #define USERMOD_BATTERY_UNKOWN_CAPACITY 2500 +#endif +#ifndef USERMOD_BATTERY_UNKOWN_CALIBRATION + // offset or calibration value to fine tune the calculated voltage + #define USERMOD_BATTERY_UNKOWN_CALIBRATION 0 +#endif +/* + * + * Lithium polymer (Li-Po) defaults + * + */ +#ifndef USERMOD_BATTERY_LIPO_MIN_VOLTAGE + // LiPo "1S" Batteries should not be dischared below 3V !! + #define USERMOD_BATTERY_LIPO_MIN_VOLTAGE 3.2f +#endif +#ifndef USERMOD_BATTERY_LIPO_MAX_VOLTAGE + #define USERMOD_BATTERY_LIPO_MAX_VOLTAGE 4.2f +#endif +#ifndef USERMOD_BATTERY_LIPO_CAPACITY + #define USERMOD_BATTERY_LIPO_CAPACITY 5000 +#endif +#ifndef USERMOD_BATTERY_LIPO_CALIBRATION + #define USERMOD_BATTERY_LIPO_CALIBRATION 0 +#endif +/* + * + * Lithium-ion (Li-Ion) defaults + * + */ +#ifndef USERMOD_BATTERY_LION_MIN_VOLTAGE + // default for 18650 battery + #define USERMOD_BATTERY_LION_MIN_VOLTAGE 2.6f +#endif +#ifndef USERMOD_BATTERY_LION_MAX_VOLTAGE + #define USERMOD_BATTERY_LION_MAX_VOLTAGE 4.2f +#endif +#ifndef USERMOD_BATTERY_LION_CAPACITY + // a common capacity for single 18650 battery cells is between 2500 and 3600 mAh + #define USERMOD_BATTERY_LION_CAPACITY 3100 +#endif +#ifndef USERMOD_BATTERY_LION_CALIBRATION + // offset or calibration value to fine tune the calculated voltage + #define USERMOD_BATTERY_LION_CALIBRATION 0 #endif -// default for 18650 battery -// https://batterybro.com/blogs/18650-wholesale-battery-reviews/18852515-when-to-recycle-18650-batteries-and-how-to-start-a-collection-center-in-your-vape-shop -// Discharge voltage: 2.5 volt + .1 for personal safety -#ifndef USERMOD_BATTERY_MIN_VOLTAGE - #if USERMOB_BATTERY_DEFAULT_TYPE == 1 - // LiPo "1S" Batteries should not be dischared below 3V !! - #define USERMOD_BATTERY_MIN_VOLTAGE 3.2f - #else - #define USERMOD_BATTERY_MIN_VOLTAGE 2.6f - #endif -#endif - -#ifndef USERMOD_BATTERY_MAX_VOLTAGE - #define USERMOD_BATTERY_MAX_VOLTAGE 4.2f -#endif - -// a common capacity for single 18650 battery cells is between 2500 and 3600 mAh -#ifndef USERMOD_BATTERY_TOTAL_CAPACITY - #define USERMOD_BATTERY_TOTAL_CAPACITY 3100 -#endif - -// offset or calibration value to fine tune the calculated voltage -#ifndef USERMOD_BATTERY_CALIBRATION - #define USERMOD_BATTERY_CALIBRATION 0 -#endif // auto-off feature #ifndef USERMOD_BATTERY_AUTO_OFF_ENABLED @@ -74,3 +109,25 @@ #ifndef USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION #define USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION 5 #endif + +typedef enum +{ + unknown=0, + lipo=1, + lion=2 +} batteryType; + +// used for initial configuration after boot +typedef struct bconfig_t +{ + batteryType type; + float minVoltage; + float maxVoltage; + unsigned int capacity; // current capacity + float voltage; // current voltage + int8_t level; // current level + float calibration; // offset or calibration value to fine tune the calculated voltage +} batteryConfig; + + +#endif \ No newline at end of file diff --git a/usermods/Battery/lion.h b/usermods/Battery/lion.h index 69095ac09..4016af7e3 100644 --- a/usermods/Battery/lion.h +++ b/usermods/Battery/lion.h @@ -13,9 +13,13 @@ class Lion : public Battery private: public: - Lion() : Battery() + Lion() { - + this->setMinVoltage(USERMOD_BATTERY_LION_MIN_VOLTAGE); + this->setMaxVoltage(USERMOD_BATTERY_LION_MAX_VOLTAGE); + this->setCapacity(USERMOD_BATTERY_LION_CAPACITY); + this->setVoltage(this->getVoltage()); + this->setCalibration(USERMOD_BATTERY_LION_CALIBRATION); } float mapVoltage(float v, float min, float max) override diff --git a/usermods/Battery/lipo.h b/usermods/Battery/lipo.h index 92ede8f81..03eed7b86 100644 --- a/usermods/Battery/lipo.h +++ b/usermods/Battery/lipo.h @@ -5,7 +5,7 @@ #include "battery.h" /** - * Lipo Battery + * Lipo Battery * */ class Lipo : public Battery @@ -13,9 +13,13 @@ class Lipo : public Battery private: public: - Lipo() : Battery() + Lipo() { - + this->setMinVoltage(USERMOD_BATTERY_LIPO_MIN_VOLTAGE); + this->setMaxVoltage(USERMOD_BATTERY_LIPO_MAX_VOLTAGE); + this->setCapacity(USERMOD_BATTERY_LIPO_CAPACITY); + this->setVoltage(this->getVoltage()); + this->setCalibration(USERMOD_BATTERY_LIPO_CALIBRATION); } /** diff --git a/usermods/Battery/unkown.h b/usermods/Battery/unkown.h new file mode 100644 index 000000000..63b2674f3 --- /dev/null +++ b/usermods/Battery/unkown.h @@ -0,0 +1,36 @@ +#ifndef UMBUnkown_h +#define UMBUnkown_h + +#include "battery_defaults.h" +#include "battery.h" + +/** + * Lion Battery + * + */ +class Unkown : public Battery +{ + private: + + public: + Unkown() + { + this->setMinVoltage(USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE); + this->setMaxVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE); + this->setCapacity(USERMOD_BATTERY_UNKOWN_CAPACITY); + this->setVoltage(this->getVoltage()); + this->setCalibration(USERMOD_BATTERY_UNKOWN_CALIBRATION); + } + + float mapVoltage(float v, float min, float max) override + { + return this->linearMapping(v, min, max); // basic mapping + }; + + void calculateAndSetLevel(float voltage) override + { + this->setLevel(this->mapVoltage(voltage, this->getMinVoltage(), this->getMaxVoltage())); + }; +}; + +#endif \ No newline at end of file diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index f9bfc96fb..ab2ab908d 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -3,6 +3,7 @@ #include "wled.h" #include "battery_defaults.h" #include "battery.h" +#include "unkown.h" #include "lion.h" #include "lipo.h" @@ -18,16 +19,9 @@ class UsermodBattery : public Usermod private: // battery pin can be defined in my_config.h int8_t batteryPin = USERMOD_BATTERY_MEASUREMENT_PIN; - - int8_t batteryType = USERMOB_BATTERY_DEFAULT_TYPE; - - float minVoltage = USERMOD_BATTERY_MIN_VOLTAGE; - float maxVoltage = USERMOD_BATTERY_MAX_VOLTAGE; - unsigned int capacity = USERMOD_BATTERY_TOTAL_CAPACITY; // current capacity - float voltage = this->maxVoltage; // current voltage - int8_t level = 100; // current level - float calibration = USERMOD_BATTERY_CALIBRATION; // offset or calibration value to fine tune the calculated voltage + Battery* bat = nullptr; + batteryConfig bcfg; // how often to read the battery voltage unsigned long readingInterval = USERMOD_BATTERY_MEASUREMENT_INTERVAL; @@ -127,16 +121,17 @@ class UsermodBattery : public Usermod pinMode(batteryPin, INPUT); #endif - //this could also be handled with a factory class but for only 2 types it should be sufficient for now - if(batteryType == 1) { - bat = new Lipo(); - } else - if(batteryType == 2) { - bat = new Lion(); - } else { - bat = new Lipo(); // in the future one could create a nullObject - } + //this could also be handled with a factory class but for only 2 types it should be sufficient for now + if(bcfg.type == (batteryType)lipo) { + bat = new Lipo(); + } else + if(bcfg.type == (batteryType)lion) { + bat = new Lion(); + } else { + bat = new Unkown(); // nullObject + } + bat->update(bcfg); nextReadTime = millis() + readingInterval; lastReadTime = millis(); @@ -321,12 +316,11 @@ class UsermodBattery : public Usermod battery[F("pin")] = batteryPin; #endif - if(bat) { - battery[F("min-voltage")] = bat->getMinVoltage(); - battery[F("max-voltage")] = bat->getMaxVoltage(); - battery[F("capacity")] = bat->getCapacity(); - battery[F("calibration")] = bat->getCalibration(); - } + battery[F("type")] = (String)bcfg.type; + battery[F("min-voltage")] = bat->getMinVoltage(); + battery[F("max-voltage")] = bat->getMaxVoltage(); + battery[F("capacity")] = bat->getCapacity(); + battery[F("calibration")] = bat->getCalibration(); battery[FPSTR(_readInterval)] = readingInterval; JsonObject ao = battery.createNestedObject(F("auto-off")); // auto off section @@ -344,6 +338,11 @@ class UsermodBattery : public Usermod void appendConfigData() { + oappend(SET_F("td=addDropdown('Battery', 'type');")); + oappend(SET_F("addOption(td, 'Unkown', '0');")); + oappend(SET_F("addOption(td, 'LiPo', '1');")); + oappend(SET_F("addOption(td, 'LiOn', '2');")); + oappend(SET_F("addInfo('Battery:type',1,'requires reboot');")); oappend(SET_F("addInfo('Battery:min-voltage', 1, 'v');")); oappend(SET_F("addInfo('Battery:max-voltage', 1, 'v');")); oappend(SET_F("addInfo('Battery:capacity', 1, 'mAh');")); @@ -399,16 +398,15 @@ class UsermodBattery : public Usermod newBatteryPin = battery[F("pin")] | newBatteryPin; #endif - if(bat) { - bat->setMinVoltage(battery[F("min-voltage")] | bat->getMinVoltage()); - bat->setMaxVoltage(battery[F("max-voltage")] | bat->getMaxVoltage()); - bat->setCapacity(battery[F("capacity")] | bat->getCapacity()); - bat->setCalibration(battery[F("calibration")] | bat->getCalibration()); - } + getJsonValue(battery[F("type")], bcfg.type); + getJsonValue(battery[F("min-voltage")], bcfg.minVoltage); + getJsonValue(battery[F("max-voltage")], bcfg.maxVoltage); + getJsonValue(battery[F("capacity")], bcfg.capacity); + getJsonValue(battery[F("calibration")], bcfg.calibration); setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); // JsonArray type = battery[F("Type")]; - // batteryType = type["bt"] | batteryType; + // batteryType = type["bt"] | btype; JsonObject ao = battery[F("auto-off")]; setAutoOffEnabled(ao[FPSTR(_enabled)] | autoOffEnabled); @@ -446,7 +444,7 @@ class UsermodBattery : public Usermod } #endif - return !battery[FPSTR(_readInterval)].isNull(); + return !battery[F("min-voltage")].isNull(); } /* From 8ba5dfe447ac73111ff2a244be44a6ec08389417 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Fri, 6 Jan 2023 19:07:39 +0100 Subject: [PATCH 005/148] =?UTF-8?q?Another=20Bugfx=20=F0=9F=A7=91=E2=80=8D?= =?UTF-8?q?=F0=9F=94=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- usermods/Battery/battery.h | 9 +-------- usermods/Battery/lion.h | 9 +++++++++ usermods/Battery/lipo.h | 9 +++++++++ usermods/Battery/usermod_v2_Battery.h | 11 ++++++----- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/usermods/Battery/battery.h b/usermods/Battery/battery.h index ad3fb2703..3a792aadb 100644 --- a/usermods/Battery/battery.h +++ b/usermods/Battery/battery.h @@ -30,14 +30,7 @@ class Battery } - virtual void update(batteryConfig cfg) - { - if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); - if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); - if(cfg.calibration) this->setCapacity(cfg.calibration); - if(cfg.level) this->setLevel(cfg.level); - if(cfg.calibration) this->setCalibration(cfg.calibration); - } + virtual void update(batteryConfig cfg) = 0; /** * Corresponding battery curves diff --git a/usermods/Battery/lion.h b/usermods/Battery/lion.h index 4016af7e3..17a4b3593 100644 --- a/usermods/Battery/lion.h +++ b/usermods/Battery/lion.h @@ -22,6 +22,15 @@ class Lion : public Battery this->setCalibration(USERMOD_BATTERY_LION_CALIBRATION); } + void update(batteryConfig cfg) + { + if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); + if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); + if(cfg.calibration) this->setCapacity(cfg.calibration); + if(cfg.level) this->setLevel(cfg.level); + if(cfg.calibration) this->setCalibration(cfg.calibration); + } + float mapVoltage(float v, float min, float max) override { return this->linearMapping(v, min, max); // basic mapping diff --git a/usermods/Battery/lipo.h b/usermods/Battery/lipo.h index 03eed7b86..dcd44567f 100644 --- a/usermods/Battery/lipo.h +++ b/usermods/Battery/lipo.h @@ -22,6 +22,15 @@ class Lipo : public Battery this->setCalibration(USERMOD_BATTERY_LIPO_CALIBRATION); } + void update(batteryConfig cfg) + { + if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); + if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); + if(cfg.calibration) this->setCapacity(cfg.calibration); + if(cfg.level) this->setLevel(cfg.level); + if(cfg.calibration) this->setCalibration(cfg.calibration); + } + /** * LiPo batteries have a differnt dischargin curve, see * https://blog.ampow.com/lipo-voltage-chart/ diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index ab2ab908d..5cf6ac792 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -132,6 +132,7 @@ class UsermodBattery : public Usermod } bat->update(bcfg); + nextReadTime = millis() + readingInterval; lastReadTime = millis(); @@ -316,7 +317,7 @@ class UsermodBattery : public Usermod battery[F("pin")] = batteryPin; #endif - battery[F("type")] = (String)bcfg.type; + battery[F("type")] = (String)bcfg.type; // has to be a String otherwise it won't get converted to a Dropdown battery[F("min-voltage")] = bat->getMinVoltage(); battery[F("max-voltage")] = bat->getMaxVoltage(); battery[F("capacity")] = bat->getCapacity(); @@ -404,9 +405,6 @@ class UsermodBattery : public Usermod getJsonValue(battery[F("capacity")], bcfg.capacity); getJsonValue(battery[F("calibration")], bcfg.calibration); setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); - - // JsonArray type = battery[F("Type")]; - // batteryType = type["bt"] | btype; JsonObject ao = battery[F("auto-off")]; setAutoOffEnabled(ao[FPSTR(_enabled)] | autoOffEnabled); @@ -444,7 +442,10 @@ class UsermodBattery : public Usermod } #endif - return !battery[F("min-voltage")].isNull(); + if(initDone) + bat->update(bcfg); + + return !battery[FPSTR(_readInterval)].isNull(); } /* From d16f9efeecd9f182a150354479f885eb9e2d9bee Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Fri, 6 Jan 2023 19:09:12 +0100 Subject: [PATCH 006/148] =?UTF-8?q?Added=20forgotten=20file=20=F0=9F=98=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- usermods/Battery/unkown.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/usermods/Battery/unkown.h b/usermods/Battery/unkown.h index 63b2674f3..f36c3195e 100644 --- a/usermods/Battery/unkown.h +++ b/usermods/Battery/unkown.h @@ -22,6 +22,14 @@ class Unkown : public Battery this->setCalibration(USERMOD_BATTERY_UNKOWN_CALIBRATION); } + void update(batteryConfig cfg) + { + if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); else this->setMinVoltage(USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE); + if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); else this->setMaxVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE); + if(cfg.calibration) this->setCapacity(cfg.calibration); else this->setCapacity(USERMOD_BATTERY_UNKOWN_CAPACITY); + if(cfg.calibration) this->setCalibration(cfg.calibration); else this->setCalibration(USERMOD_BATTERY_UNKOWN_CALIBRATION); + } + float mapVoltage(float v, float min, float max) override { return this->linearMapping(v, min, max); // basic mapping From bb82bf762fd4becdf6dc1ed92b4e97eb03e18aed Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Thu, 12 Jan 2023 21:50:46 +0100 Subject: [PATCH 007/148] Update Readme, my_config type config options with examples --- usermods/Battery/battery_defaults.h | 2 ++ usermods/Battery/readme.md | 16 +++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/usermods/Battery/battery_defaults.h b/usermods/Battery/battery_defaults.h index f4060ca60..fbdaf4877 100644 --- a/usermods/Battery/battery_defaults.h +++ b/usermods/Battery/battery_defaults.h @@ -1,6 +1,8 @@ #ifndef UMBDefaults_h #define UMBDefaults_h +#include "wled.h" + // pin defaults // for the esp32 it is best to use the ADC1: GPIO32 - GPIO39 // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/adc.html diff --git a/usermods/Battery/readme.md b/usermods/Battery/readme.md index d55573abe..1ca229763 100644 --- a/usermods/Battery/readme.md +++ b/usermods/Battery/readme.md @@ -36,13 +36,12 @@ define `USERMOD_BATTERY` in `wled00/my_config.h` | Name | Unit | Description | | ----------------------------------------------- | ----------- |-------------------------------------------------------------------------------------- | | `USERMOD_BATTERY` | | define this (in `my_config.h`) to have this usermod included wled00\usermods_list.cpp | -| `USERMOD_BATTERY_USE_LIPO` | | define this (in `my_config.h`) if you use LiPo rechargeables (1S) | | `USERMOD_BATTERY_MEASUREMENT_PIN` | | defaults to A0 on ESP8266 and GPIO35 on ESP32 | | `USERMOD_BATTERY_MEASUREMENT_INTERVAL` | ms | battery check interval. defaults to 30 seconds | -| `USERMOD_BATTERY_MIN_VOLTAGE` | v | minimum battery voltage. default is 2.6 (18650 battery standard) | -| `USERMOD_BATTERY_MAX_VOLTAGE` | v | maximum battery voltage. default is 4.2 (18650 battery standard) | -| `USERMOD_BATTERY_TOTAL_CAPACITY` | mAh | the capacity of all cells in parralel sumed up | -| `USERMOD_BATTERY_CALIBRATION` | | offset / calibration number, fine tune the measured voltage by the microcontroller | +| `USERMOD_BATTERY_{TYPE}_MIN_VOLTAGE` | v | minimum battery voltage. default is 2.6 (18650 battery standard) | +| `USERMOD_BATTERY_{TYPE}_MAX_VOLTAGE` | v | maximum battery voltage. default is 4.2 (18650 battery standard) | +| `USERMOD_BATTERY_{TYPE}_TOTAL_CAPACITY` | mAh | the capacity of all cells in parralel sumed up | +| `USERMOD_BATTERY_{TYPE}_CALIBRATION` | | offset / calibration number, fine tune the measured voltage by the microcontroller | | Auto-Off | --- | --- | | `USERMOD_BATTERY_AUTO_OFF_ENABLED` | true/false | enables auto-off | | `USERMOD_BATTERY_AUTO_OFF_THRESHOLD` | % (0-100) | when this threshold is reached master power turns off | @@ -54,6 +53,13 @@ define `USERMOD_BATTERY` in `wled00/my_config.h` All parameters can be configured at runtime via the Usermods settings page. +**NOTICE:** Each Battery type can be pre-configured individualy (in `my_config.h`) + +| Name | Alias | `my_config.h` example | +| --------------- | ------------- | ------------------------------------- | +| Lithium Polymer | lipo (Li-Po) | `USERMOD_BATTERY_lipo_MIN_VOLTAGE` | +| Lithium Ionen | lion (Li-Ion) | `USERMOD_BATTERY_lion_TOTAL_CAPACITY` | + ## ⚠️ Important - Make sure you know your battery specifications! All batteries are **NOT** the same! From f97b79bc16a5027d35219e015162c7b58e154ef9 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Sat, 21 Jan 2023 00:39:51 +0100 Subject: [PATCH 008/148] Exposing the Battery state to JSON API - Part 1 --- usermods/Battery/battery_defaults.h | 9 +-- usermods/Battery/usermod_v2_Battery.h | 99 +++++++++++++++------------ 2 files changed, 62 insertions(+), 46 deletions(-) diff --git a/usermods/Battery/battery_defaults.h b/usermods/Battery/battery_defaults.h index fbdaf4877..092e3dd36 100644 --- a/usermods/Battery/battery_defaults.h +++ b/usermods/Battery/battery_defaults.h @@ -34,6 +34,7 @@ * */ #ifndef USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE + // Extra save defaults #define USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE 3.3f #endif #ifndef USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE @@ -125,10 +126,10 @@ typedef struct bconfig_t batteryType type; float minVoltage; float maxVoltage; - unsigned int capacity; // current capacity - float voltage; // current voltage - int8_t level; // current level - float calibration; // offset or calibration value to fine tune the calculated voltage + unsigned int capacity; // current capacity + float voltage; // current voltage + int8_t level; // current level + float calibration; // offset or calibration value to fine tune the calculated voltage } batteryConfig; diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index bf123a79d..390bd96b4 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -30,17 +30,17 @@ class UsermodBattery : public Usermod // auto shutdown/shutoff/master off feature bool autoOffEnabled = USERMOD_BATTERY_AUTO_OFF_ENABLED; - int8_t autoOffThreshold = USERMOD_BATTERY_AUTO_OFF_THRESHOLD; + uint8_t autoOffThreshold = USERMOD_BATTERY_AUTO_OFF_THRESHOLD; // low power indicator feature bool lowPowerIndicatorEnabled = USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED; - int8_t lowPowerIndicatorPreset = USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET; - int8_t lowPowerIndicatorThreshold = USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD; - int8_t lowPowerIndicatorReactivationThreshold = lowPowerIndicatorThreshold+10; - int8_t lowPowerIndicatorDuration = USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION; + uint8_t lowPowerIndicatorPreset = USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET; + uint8_t lowPowerIndicatorThreshold = USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD; + uint8_t lowPowerIndicatorReactivationThreshold = lowPowerIndicatorThreshold+10; + uint8_t lowPowerIndicatorDuration = USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION; bool lowPowerIndicationDone = false; unsigned long lowPowerActivationTime = 0; // used temporary during active time - int8_t lastPreset = 0; + uint8_t lastPreset = 0; bool initDone = false; bool initializing = true; @@ -128,7 +128,7 @@ class UsermodBattery : public Usermod if(bcfg.type == (batteryType)lion) { bat = new Lion(); } else { - bat = new Unkown(); // nullObject + bat = new Unkown(); // nullObject pattern } bat->update(bcfg); @@ -181,7 +181,6 @@ class UsermodBattery : public Usermod #else // read battery raw input rawValue = analogRead(batteryPin); - // calculate the voltage voltage = ((rawValue / getAdcPrecision()) * bat->getMaxVoltage()) + bat->getCalibration(); #endif @@ -252,17 +251,42 @@ class UsermodBattery : public Usermod infoVoltage.add(F(" V")); } + void addBatteryToJsonObject(JsonObject& battery, bool forJsonState) + { + if(forJsonState) { battery[F("type")] = bcfg.type; } else {battery[F("type")] = (String)bcfg.type; } // has to be a String otherwise it won't get converted to a Dropdown + battery[F("min-voltage")] = bat->getMinVoltage(); + battery[F("max-voltage")] = bat->getMaxVoltage(); + battery[F("capacity")] = bat->getCapacity(); + battery[F("calibration")] = bat->getCalibration(); + battery[FPSTR(_readInterval)] = readingInterval; + + JsonObject ao = battery.createNestedObject(F("auto-off")); // auto off section + ao[FPSTR(_enabled)] = autoOffEnabled; + ao[FPSTR(_threshold)] = autoOffThreshold; + + JsonObject lp = battery.createNestedObject(F("indicator")); // low power section + lp[FPSTR(_enabled)] = lowPowerIndicatorEnabled; + lp[FPSTR(_preset)] = lowPowerIndicatorPreset; // dropdown trickery (String)lowPowerIndicatorPreset; + lp[FPSTR(_threshold)] = lowPowerIndicatorThreshold; + lp[FPSTR(_duration)] = lowPowerIndicatorDuration; + } /* * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ - /* void addToJsonState(JsonObject& root) { - // TBD + JsonObject battery = root.createNestedObject(FPSTR(_name)); + + if (battery.isNull()) { + battery = root.createNestedObject(FPSTR(_name)); + } + + addBatteryToJsonObject(battery, true); + + DEBUG_PRINTLN(F("Battery state exposed in JSON API.")); } - */ /* @@ -314,48 +338,39 @@ class UsermodBattery : public Usermod */ void addToConfig(JsonObject& root) { - JsonObject battery = root.createNestedObject(FPSTR(_name)); // usermodname + JsonObject battery = root.createNestedObject(FPSTR(_name)); + + if (battery.isNull()) { + battery = root.createNestedObject(FPSTR(_name)); + } + #ifdef ARDUINO_ARCH_ESP32 battery[F("pin")] = batteryPin; #endif - - battery[F("type")] = (String)bcfg.type; // has to be a String otherwise it won't get converted to a Dropdown - battery[F("min-voltage")] = bat->getMinVoltage(); - battery[F("max-voltage")] = bat->getMaxVoltage(); - battery[F("capacity")] = bat->getCapacity(); - battery[F("calibration")] = bat->getCalibration(); - battery[FPSTR(_readInterval)] = readingInterval; - JsonObject ao = battery.createNestedObject(F("auto-off")); // auto off section - ao[FPSTR(_enabled)] = autoOffEnabled; - ao[FPSTR(_threshold)] = autoOffThreshold; - - JsonObject lp = battery.createNestedObject(F("indicator")); // low power section - lp[FPSTR(_enabled)] = lowPowerIndicatorEnabled; - lp[FPSTR(_preset)] = lowPowerIndicatorPreset; // dropdown trickery (String)lowPowerIndicatorPreset; - lp[FPSTR(_threshold)] = lowPowerIndicatorThreshold; - lp[FPSTR(_duration)] = lowPowerIndicatorDuration; + addBatteryToJsonObject(battery, false); DEBUG_PRINTLN(F("Battery config saved.")); } void appendConfigData() { - oappend(SET_F("td=addDropdown('Battery', 'type');")); - oappend(SET_F("addOption(td, 'Unkown', '0');")); - oappend(SET_F("addOption(td, 'LiPo', '1');")); - oappend(SET_F("addOption(td, 'LiOn', '2');")); - oappend(SET_F("addInfo('Battery:type',1,'requires reboot');")); - oappend(SET_F("addInfo('Battery:min-voltage', 1, 'v');")); - oappend(SET_F("addInfo('Battery:max-voltage', 1, 'v');")); - oappend(SET_F("addInfo('Battery:capacity', 1, 'mAh');")); - oappend(SET_F("addInfo('Battery:interval', 1, 'ms');")); - oappend(SET_F("addInfo('Battery:auto-off:threshold', 1, '%');")); - oappend(SET_F("addInfo('Battery:indicator:threshold', 1, '%');")); - oappend(SET_F("addInfo('Battery:indicator:duration', 1, 's');")); + // Total: 501 Bytes + oappend(SET_F("td=addDropdown('Battery', 'type');")); // 35 Bytes + oappend(SET_F("addOption(td, 'Unkown', '0');")); // 30 Bytes + oappend(SET_F("addOption(td, 'LiPo', '1');")); // 28 Bytes + oappend(SET_F("addOption(td, 'LiOn', '2');")); // 28 Bytes + oappend(SET_F("addInfo('Battery:type',1,'requires reboot');")); // 81 Bytes + oappend(SET_F("addInfo('Battery:min-voltage', 1, 'v');")); // 40 Bytes + oappend(SET_F("addInfo('Battery:max-voltage', 1, 'v');")); // 40 Bytes + oappend(SET_F("addInfo('Battery:capacity', 1, 'mAh');")); // 39 Bytes + oappend(SET_F("addInfo('Battery:interval', 1, 'ms');")); // 38 Bytes + oappend(SET_F("addInfo('Battery:auto-off:threshold', 1, '%');")); // 47 Bytes + oappend(SET_F("addInfo('Battery:indicator:threshold', 1, '%');")); // 48 Bytes + oappend(SET_F("addInfo('Battery:indicator:duration', 1, 's');")); // 47 Bytes - // cannot quite get this mf to work. its exeeding some buffer limit i think - // what i wanted is a list of all presets to select one from + // this option list would exeed the oappend() buffer + // a list of all presets to select one from // oappend(SET_F("bd=addDropdown('Battery:low-power-indicator', 'preset');")); // the loop generates: oappend(SET_F("addOption(bd, 'preset name', preset id);")); // for(int8_t i=1; i < 42; i++) { From f78f8b6b127203295cc0e22965c51a1f71415915 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Sat, 21 Jan 2023 01:44:50 +0100 Subject: [PATCH 009/148] Exposing the Battery state to JSON API - Part 2 --- usermods/Battery/usermod_v2_Battery.h | 59 +++++++++++++++------------ 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index 390bd96b4..bd4d7778c 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -271,6 +271,30 @@ class UsermodBattery : public Usermod lp[FPSTR(_duration)] = lowPowerIndicatorDuration; } + void getUsermodConfigFromJsonObject(JsonObject& battery) + { + getJsonValue(battery[F("type")], bcfg.type); + getJsonValue(battery[F("min-voltage")], bcfg.minVoltage); + getJsonValue(battery[F("max-voltage")], bcfg.maxVoltage); + getJsonValue(battery[F("capacity")], bcfg.capacity); + getJsonValue(battery[F("calibration")], bcfg.calibration); + setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); + + JsonObject ao = battery[F("auto-off")]; + setAutoOffEnabled(ao[FPSTR(_enabled)] | autoOffEnabled); + setAutoOffThreshold(ao[FPSTR(_threshold)] | autoOffThreshold); + + JsonObject lp = battery[F("indicator")]; + setLowPowerIndicatorEnabled(lp[FPSTR(_enabled)] | lowPowerIndicatorEnabled); + setLowPowerIndicatorPreset(lp[FPSTR(_preset)] | lowPowerIndicatorPreset); + setLowPowerIndicatorThreshold(lp[FPSTR(_threshold)] | lowPowerIndicatorThreshold); + lowPowerIndicatorReactivationThreshold = lowPowerIndicatorThreshold+10; + setLowPowerIndicatorDuration(lp[FPSTR(_duration)] | lowPowerIndicatorDuration); + + if(initDone) + bat->update(bcfg); + } + /* * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients @@ -296,7 +320,15 @@ class UsermodBattery : public Usermod /* void readFromJsonState(JsonObject& root) { - // TBD + if (!initDone) return; // prevent crash on boot applyPreset() + + JsonObject battery = root[FPSTR(_name)]; + + if (!battery.isNull()) { + getUsermodConfigFromJsonObject(battery); + + DEBUG_PRINTLN(F("Battery state read from JSON API.")); + } } */ @@ -416,25 +448,7 @@ class UsermodBattery : public Usermod newBatteryPin = battery[F("pin")] | newBatteryPin; #endif - getJsonValue(battery[F("type")], bcfg.type); - getJsonValue(battery[F("min-voltage")], bcfg.minVoltage); - getJsonValue(battery[F("max-voltage")], bcfg.maxVoltage); - getJsonValue(battery[F("capacity")], bcfg.capacity); - getJsonValue(battery[F("calibration")], bcfg.calibration); - setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); - - JsonObject ao = battery[F("auto-off")]; - setAutoOffEnabled(ao[FPSTR(_enabled)] | autoOffEnabled); - setAutoOffThreshold(ao[FPSTR(_threshold)] | autoOffThreshold); - - JsonObject lp = battery[F("indicator")]; - setLowPowerIndicatorEnabled(lp[FPSTR(_enabled)] | lowPowerIndicatorEnabled); - setLowPowerIndicatorPreset(lp[FPSTR(_preset)] | lowPowerIndicatorPreset); // dropdown trickery (int)lp["preset"] - setLowPowerIndicatorThreshold(lp[FPSTR(_threshold)] | lowPowerIndicatorThreshold); - lowPowerIndicatorReactivationThreshold = lowPowerIndicatorThreshold+10; - setLowPowerIndicatorDuration(lp[FPSTR(_duration)] | lowPowerIndicatorDuration); - - DEBUG_PRINT(FPSTR(_name)); + getUsermodConfigFromJsonObject(battery); #ifdef ARDUINO_ARCH_ESP32 if (!initDone) @@ -459,9 +473,6 @@ class UsermodBattery : public Usermod } #endif - if(initDone) - bat->update(bcfg); - return !battery[FPSTR(_readInterval)].isNull(); } @@ -578,7 +589,6 @@ class UsermodBattery : public Usermod autoOffThreshold = lowPowerIndicatorEnabled /*&& autoOffEnabled*/ ? min(lowPowerIndicatorThreshold-1, (int)autoOffThreshold) : autoOffThreshold; } - /* * Get low-power-indicator feature enabled status * is the low-power-indicator enabled, true/false @@ -648,7 +658,6 @@ class UsermodBattery : public Usermod lowPowerIndicatorDuration = duration; } - /* * Get low-power-indicator status when the indication is done thsi returns true */ From b8c61b52366a567d57860ada080c30ced9725777 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Sat, 9 Sep 2023 21:01:55 +0200 Subject: [PATCH 010/148] =?UTF-8?q?Move=20battery=20types=20to=20a=20separ?= =?UTF-8?q?ate=20folder=20=F0=9F=93=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- usermods/Battery/battery.h | 17 +---- usermods/Battery/battery_defaults.h | 16 +--- usermods/Battery/{ => types}/lion.h | 6 +- usermods/Battery/{ => types}/lipo.h | 14 ++-- usermods/Battery/{ => types}/unkown.h | 6 +- usermods/Battery/usermod_v2_Battery.h | 105 ++++++-------------------- 6 files changed, 37 insertions(+), 127 deletions(-) rename usermods/Battery/{ => types}/lion.h (87%) rename usermods/Battery/{ => types}/lipo.h (82%) rename usermods/Battery/{ => types}/unkown.h (83%) diff --git a/usermods/Battery/battery.h b/usermods/Battery/battery.h index 3a792aadb..8a8042ff3 100644 --- a/usermods/Battery/battery.h +++ b/usermods/Battery/battery.h @@ -14,7 +14,6 @@ class Battery protected: float minVoltage; float maxVoltage; - unsigned int capacity; float voltage; int8_t level = 100; float calibration; // offset or calibration value to fine tune the calculated voltage @@ -34,7 +33,7 @@ class Battery /** * Corresponding battery curves - * calculates the capacity in % (0-100) with given voltage and possible voltage range + * calculates the level in % (0-100) with given voltage and possible voltage range */ virtual float mapVoltage(float v, float min, float max) = 0; // { @@ -86,20 +85,6 @@ class Battery this->maxVoltage = max(getMinVoltage()+.5f, voltage); } - /* - * Get the capacity of all cells in parralel sumed up - * unit: mAh - */ - unsigned int getCapacity() - { - return this->capacity; - } - - void setCapacity(unsigned int capacity) - { - this->capacity = capacity; - } - float getVoltage() { return this->voltage; diff --git a/usermods/Battery/battery_defaults.h b/usermods/Battery/battery_defaults.h index 668009680..199ee3432 100644 --- a/usermods/Battery/battery_defaults.h +++ b/usermods/Battery/battery_defaults.h @@ -40,9 +40,6 @@ #ifndef USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE #define USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE 4.2f #endif -#ifndef USERMOD_BATTERY_UNKOWN_CAPACITY - #define USERMOD_BATTERY_UNKOWN_CAPACITY 2500 -#endif #ifndef USERMOD_BATTERY_UNKOWN_CALIBRATION // offset or calibration value to fine tune the calculated voltage #define USERMOD_BATTERY_UNKOWN_CALIBRATION 0 @@ -59,9 +56,6 @@ #ifndef USERMOD_BATTERY_LIPO_MAX_VOLTAGE #define USERMOD_BATTERY_LIPO_MAX_VOLTAGE 4.2f #endif -#ifndef USERMOD_BATTERY_LIPO_CAPACITY - #define USERMOD_BATTERY_LIPO_CAPACITY 5000 -#endif #ifndef USERMOD_BATTERY_LIPO_CALIBRATION #define USERMOD_BATTERY_LIPO_CALIBRATION 0 #endif @@ -77,10 +71,6 @@ #ifndef USERMOD_BATTERY_LION_MAX_VOLTAGE #define USERMOD_BATTERY_LION_MAX_VOLTAGE 4.2f #endif -#ifndef USERMOD_BATTERY_LION_CAPACITY - // a common capacity for single 18650 battery cells is between 2500 and 3600 mAh - #define USERMOD_BATTERY_LION_CAPACITY 3100 -#endif #ifndef USERMOD_BATTERY_LION_CALIBRATION // offset or calibration value to fine tune the calculated voltage #define USERMOD_BATTERY_LION_CALIBRATION 0 @@ -109,11 +99,6 @@ #define USERMOD_BATTERY_CALIBRATION 0 #endif -// calculate remaining time / the time that is left before the battery runs out of power -// #ifndef USERMOD_BATTERY_CALCULATE_TIME_LEFT_ENABLED -// #define USERMOD_BATTERY_CALCULATE_TIME_LEFT_ENABLED false -// #endif - // auto-off feature #ifndef USERMOD_BATTERY_AUTO_OFF_ENABLED #define USERMOD_BATTERY_AUTO_OFF_ENABLED true @@ -157,6 +142,7 @@ typedef struct bconfig_t float voltage; // current voltage int8_t level; // current level float calibration; // offset or calibration value to fine tune the calculated voltage + float voltageMultiplier; } batteryConfig; diff --git a/usermods/Battery/lion.h b/usermods/Battery/types/lion.h similarity index 87% rename from usermods/Battery/lion.h rename to usermods/Battery/types/lion.h index 17a4b3593..2ff54a1ea 100644 --- a/usermods/Battery/lion.h +++ b/usermods/Battery/types/lion.h @@ -1,8 +1,8 @@ #ifndef UMBLion_h #define UMBLion_h -#include "battery_defaults.h" -#include "battery.h" +#include "../battery_defaults.h" +#include "../battery.h" /** * Lion Battery @@ -17,7 +17,6 @@ class Lion : public Battery { this->setMinVoltage(USERMOD_BATTERY_LION_MIN_VOLTAGE); this->setMaxVoltage(USERMOD_BATTERY_LION_MAX_VOLTAGE); - this->setCapacity(USERMOD_BATTERY_LION_CAPACITY); this->setVoltage(this->getVoltage()); this->setCalibration(USERMOD_BATTERY_LION_CALIBRATION); } @@ -26,7 +25,6 @@ class Lion : public Battery { if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); - if(cfg.calibration) this->setCapacity(cfg.calibration); if(cfg.level) this->setLevel(cfg.level); if(cfg.calibration) this->setCalibration(cfg.calibration); } diff --git a/usermods/Battery/lipo.h b/usermods/Battery/types/lipo.h similarity index 82% rename from usermods/Battery/lipo.h rename to usermods/Battery/types/lipo.h index dcd44567f..264d3251e 100644 --- a/usermods/Battery/lipo.h +++ b/usermods/Battery/types/lipo.h @@ -1,8 +1,8 @@ #ifndef UMBLipo_h #define UMBLipo_h -#include "battery_defaults.h" -#include "battery.h" +#include "../battery_defaults.h" +#include "../battery.h" /** * Lipo Battery @@ -17,7 +17,6 @@ class Lipo : public Battery { this->setMinVoltage(USERMOD_BATTERY_LIPO_MIN_VOLTAGE); this->setMaxVoltage(USERMOD_BATTERY_LIPO_MAX_VOLTAGE); - this->setCapacity(USERMOD_BATTERY_LIPO_CAPACITY); this->setVoltage(this->getVoltage()); this->setCalibration(USERMOD_BATTERY_LIPO_CALIBRATION); } @@ -26,13 +25,12 @@ class Lipo : public Battery { if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); - if(cfg.calibration) this->setCapacity(cfg.calibration); if(cfg.level) this->setLevel(cfg.level); if(cfg.calibration) this->setCalibration(cfg.calibration); } /** - * LiPo batteries have a differnt dischargin curve, see + * LiPo batteries have a differnt discharge curve, see * https://blog.ampow.com/lipo-voltage-chart/ */ float mapVoltage(float v, float min, float max) override @@ -41,12 +39,12 @@ class Lipo : public Battery lvl = this->linearMapping(v, min, max); // basic mapping if (lvl < 40.0f) - lvl = this->linearMapping(lvl, 0, 40, 0, 12); // last 45% -> drops very quickly + lvl = this->linearMapping(lvl, 0, 40, 0, 12); // last 45% -> drops very quickly else { if (lvl < 90.0f) - lvl = this->linearMapping(lvl, 40, 90, 12, 95); // 90% ... 40% -> almost linear drop + lvl = this->linearMapping(lvl, 40, 90, 12, 95); // 90% ... 40% -> almost linear drop else // level > 90% - lvl = this->linearMapping(lvl, 90, 105, 95, 100); // highest 15% -> drop slowly + lvl = this->linearMapping(lvl, 90, 105, 95, 100); // highest 15% -> drop slowly } return lvl; diff --git a/usermods/Battery/unkown.h b/usermods/Battery/types/unkown.h similarity index 83% rename from usermods/Battery/unkown.h rename to usermods/Battery/types/unkown.h index f36c3195e..2b38da96c 100644 --- a/usermods/Battery/unkown.h +++ b/usermods/Battery/types/unkown.h @@ -1,8 +1,8 @@ #ifndef UMBUnkown_h #define UMBUnkown_h -#include "battery_defaults.h" -#include "battery.h" +#include "../battery_defaults.h" +#include "../battery.h" /** * Lion Battery @@ -17,7 +17,6 @@ class Unkown : public Battery { this->setMinVoltage(USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE); this->setMaxVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE); - this->setCapacity(USERMOD_BATTERY_UNKOWN_CAPACITY); this->setVoltage(this->getVoltage()); this->setCalibration(USERMOD_BATTERY_UNKOWN_CALIBRATION); } @@ -26,7 +25,6 @@ class Unkown : public Battery { if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); else this->setMinVoltage(USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE); if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); else this->setMaxVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE); - if(cfg.calibration) this->setCapacity(cfg.calibration); else this->setCapacity(USERMOD_BATTERY_UNKOWN_CAPACITY); if(cfg.calibration) this->setCalibration(cfg.calibration); else this->setCalibration(USERMOD_BATTERY_UNKOWN_CALIBRATION); } diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index 7f6738e10..a91331cb7 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -3,9 +3,9 @@ #include "wled.h" #include "battery_defaults.h" #include "battery.h" -#include "unkown.h" -#include "lion.h" -#include "lipo.h" +#include "types/unkown.h" +#include "types/lion.h" +#include "types/lipo.h" /* * Usermod by Maximilian Mewes @@ -28,7 +28,7 @@ class UsermodBattery : public Usermod unsigned long nextReadTime = 0; unsigned long lastReadTime = 0; // battery min. voltage - float minBatteryVoltage = USERMOD_BATTERY_MIN_VOLTAGE; + float minBatteryVoltage = 3.3f; // battery max. voltage float maxBatteryVoltage = USERMOD_BATTERY_MAX_VOLTAGE; // all battery cells summed up @@ -39,16 +39,10 @@ class UsermodBattery : public Usermod float voltage = maxBatteryVoltage; // between 0 and 1, to control strength of voltage smoothing filter float alpha = 0.05f; - // multiplier for the voltage divider that is in place between ADC pin and battery, default will be 2 but might be adapted to readout voltages over ~5v ESP32 or ~6.6v ESP8266 - float voltageMultiplier = USERMOD_BATTERY_VOLTAGE_MULTIPLIER; // mapped battery level based on voltage int8_t batteryLevel = 100; // offset or calibration value to fine tune the calculated voltage float calibration = USERMOD_BATTERY_CALIBRATION; - - // time left estimation feature - // bool calculateTimeLeftEnabled = USERMOD_BATTERY_CALCULATE_TIME_LEFT_ENABLED; - // float estimatedTimeLeft = 0.0; // auto shutdown/shutoff/master off feature bool autoOffEnabled = USERMOD_BATTERY_AUTO_OFF_ENABLED; @@ -114,18 +108,22 @@ class UsermodBattery : public Usermod } } - float readVoltage() - { - #ifdef ARDUINO_ARCH_ESP32 - // use calibrated millivolts analogread on esp32 (150 mV ~ 2450 mV default attentuation) and divide by 1000 to get from milivolts to volts and multiply by voltage multiplier and apply calibration value - return (analogReadMilliVolts(batteryPin) / 1000.0f) * voltageMultiplier + calibration; - #else - // use analog read on esp8266 ( 0V ~ 1V no attenuation options) and divide by ADC precision 1023 and multiply by voltage multiplier and apply calibration value - return (analogRead(batteryPin) / 1023.0f) * voltageMultiplier + calibration; - #endif - } + // float readVoltage() + // { + // #ifdef ARDUINO_ARCH_ESP32 + // // use calibrated millivolts analogread on esp32 (150 mV ~ 2450 mV default attentuation) and divide by 1000 to get from milivolts to volts and multiply by voltage multiplier and apply calibration value + // return (analogReadMilliVolts(batteryPin) / 1000.0f) * voltageMultiplier + calibration; + // #else + // // use analog read on esp8266 ( 0V ~ 1V no attenuation options) and divide by ADC precision 1023 and multiply by voltage multiplier and apply calibration value + // return (analogRead(batteryPin) / 1023.0f) * voltageMultiplier + calibration; + // #endif + // } public: + UsermodBattery() + { + bat = new Unkown(); + } //Functions called by WLED /* @@ -152,14 +150,13 @@ class UsermodBattery : public Usermod } #else //ESP8266 boards have only one analog input pin A0 pinMode(batteryPin, INPUT); - voltage = readVoltage(); + // voltage = readVoltage(); #endif //this could also be handled with a factory class but for only 2 types it should be sufficient for now if(bcfg.type == (batteryType)lipo) { bat = new Lipo(); - } else - if(bcfg.type == (batteryType)lion) { + } else if(bcfg.type == (batteryType)lion) { bat = new Lion(); } else { bat = new Unkown(); // nullObject pattern @@ -218,14 +215,8 @@ class UsermodBattery : public Usermod // calculate the voltage voltage = ((rawValue / getAdcPrecision()) * bat->getMaxVoltage()) + bat->getCalibration(); #endif - // initializing = false; - - // rawValue = readVoltage(); - // // filter with exponential smoothing because ADC in esp32 is fluctuating too much for a good single readout - // voltage = voltage + alpha * (rawValue - voltage); - - // check if voltage is within specified voltage range, allow 10% over/under voltage - removed cause this just makes it hard for people to troubleshoot as the voltage in the web gui will say invalid instead of displaying a voltage - //voltage = ((voltage < minBatteryVoltage * 0.85f) || (voltage > maxBatteryVoltage * 1.1f)) ? -1.0f : voltage; + // filter with exponential smoothing because ADC in esp32 is fluctuating too much for a good single readout + voltage = voltage + alpha * (rawValue - voltage); bat->setVoltage(voltage); // translate battery voltage into percentage @@ -298,7 +289,6 @@ class UsermodBattery : public Usermod if(forJsonState) { battery[F("type")] = bcfg.type; } else {battery[F("type")] = (String)bcfg.type; } // has to be a String otherwise it won't get converted to a Dropdown battery[F("min-voltage")] = bat->getMinVoltage(); battery[F("max-voltage")] = bat->getMaxVoltage(); - battery[F("capacity")] = bat->getCapacity(); battery[F("calibration")] = bat->getCalibration(); battery[FPSTR(_readInterval)] = readingInterval; @@ -318,8 +308,8 @@ class UsermodBattery : public Usermod getJsonValue(battery[F("type")], bcfg.type); getJsonValue(battery[F("min-voltage")], bcfg.minVoltage); getJsonValue(battery[F("max-voltage")], bcfg.maxVoltage); - getJsonValue(battery[F("capacity")], bcfg.capacity); getJsonValue(battery[F("calibration")], bcfg.calibration); + setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); JsonObject ao = battery[F("auto-off")]; @@ -421,26 +411,18 @@ class UsermodBattery : public Usermod #ifdef ARDUINO_ARCH_ESP32 battery[F("pin")] = batteryPin; #endif - - // battery[F("time-left")] = calculateTimeLeftEnabled; - battery[F("min-voltage")] = minBatteryVoltage; - battery[F("max-voltage")] = maxBatteryVoltage; - battery[F("capacity")] = totalBatteryCapacity; - battery[F("calibration")] = calibration; - battery[F("voltage-multiplier")] = voltageMultiplier; - battery[FPSTR(_readInterval)] = readingInterval; addBatteryToJsonObject(battery, false); // read voltage in case calibration or voltage multiplier changed to see immediate effect - voltage = readVoltage(); + // voltage = readVoltage(); DEBUG_PRINTLN(F("Battery config saved.")); } void appendConfigData() { - // Total: 501 Bytes + // Total: 462 Bytes oappend(SET_F("td=addDropdown('Battery', 'type');")); // 35 Bytes oappend(SET_F("addOption(td, 'Unkown', '0');")); // 30 Bytes oappend(SET_F("addOption(td, 'LiPo', '1');")); // 28 Bytes @@ -448,7 +430,6 @@ class UsermodBattery : public Usermod oappend(SET_F("addInfo('Battery:type',1,'requires reboot');")); // 81 Bytes oappend(SET_F("addInfo('Battery:min-voltage', 1, 'v');")); // 40 Bytes oappend(SET_F("addInfo('Battery:max-voltage', 1, 'v');")); // 40 Bytes - oappend(SET_F("addInfo('Battery:capacity', 1, 'mAh');")); // 39 Bytes oappend(SET_F("addInfo('Battery:interval', 1, 'ms');")); // 38 Bytes oappend(SET_F("addInfo('Battery:auto-off:threshold', 1, '%');")); // 47 Bytes oappend(SET_F("addInfo('Battery:indicator:threshold', 1, '%');")); // 48 Bytes @@ -503,9 +484,7 @@ class UsermodBattery : public Usermod // calculateTimeLeftEnabled = battery[F("time-left")] | calculateTimeLeftEnabled; setMinBatteryVoltage(battery[F("min-voltage")] | minBatteryVoltage); setMaxBatteryVoltage(battery[F("max-voltage")] | maxBatteryVoltage); - setTotalBatteryCapacity(battery[F("capacity")] | totalBatteryCapacity); setCalibration(battery[F("calibration")] | calibration); - setVoltageMultiplier(battery[F("voltage-multiplier")] | voltageMultiplier); setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); getUsermodConfigFromJsonObject(battery); @@ -655,22 +634,6 @@ class UsermodBattery : public Usermod } - /* - * Get the capacity of all cells in parralel sumed up - * unit: mAh - */ - unsigned int getTotalBatteryCapacity() - { - return totalBatteryCapacity; - } - - void setTotalBatteryCapacity(unsigned int capacity) - { - totalBatteryCapacity = capacity; - } - - - /* * Get the calculated voltage * formula: (adc pin value / adc precision * max voltage) + calibration @@ -707,24 +670,6 @@ class UsermodBattery : public Usermod calibration = offset; } - /* - * Set the voltage multiplier value - * A multiplier that may need adjusting for different voltage divider setups - */ - void setVoltageMultiplier(float multiplier) - { - voltageMultiplier = multiplier; - } - - /* - * Get the voltage multiplier value - * A multiplier that may need adjusting for different voltage divider setups - */ - float getVoltageMultiplier() - { - return voltageMultiplier; - } - /* * Get auto-off feature enabled status * is auto-off enabled, true/false From a9d6a1592412a6f901afecc1e9e4e8934ef46b13 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Sat, 9 Sep 2023 21:50:30 +0200 Subject: [PATCH 011/148] Update Classes --- usermods/Battery/battery.h | 23 ++- usermods/Battery/battery_defaults.h | 25 +--- usermods/Battery/types/lion.h | 3 +- usermods/Battery/types/lipo.h | 3 +- usermods/Battery/types/unkown.h | 4 +- usermods/Battery/usermod_v2_Battery.h | 208 ++++++++++---------------- 6 files changed, 111 insertions(+), 155 deletions(-) diff --git a/usermods/Battery/battery.h b/usermods/Battery/battery.h index 8a8042ff3..4cdfb035f 100644 --- a/usermods/Battery/battery.h +++ b/usermods/Battery/battery.h @@ -17,6 +17,7 @@ class Battery float voltage; int8_t level = 100; float calibration; // offset or calibration value to fine tune the calculated voltage + float voltageMultiplier; // ratio for the voltage divider float linearMapping(float v, float min, float max, float oMin = 0.0f, float oMax = 100.0f) { @@ -26,7 +27,9 @@ class Battery public: Battery() { - + this->setVoltage(this->getVoltage()); + this->setVoltageMultiplier(USERMOD_BATTERY_VOLTAGE_MULTIPLIER); + this->setCalibration(USERMOD_BATTERY_CALIBRATION); } virtual void update(batteryConfig cfg) = 0; @@ -127,6 +130,24 @@ class Battery { calibration = offset; } + + /* + * Get the configured calibration value + * a value to set the voltage divider ratio + */ + virtual float getVoltageMultiplier() + { + return voltageMultiplier; + } + + /* + * Set the voltage multiplier value + * a value to set the voltage divider ratio. + */ + virtual void setVoltageMultiplier(float multiplier) + { + voltageMultiplier = voltageMultiplier; + } }; #endif \ No newline at end of file diff --git a/usermods/Battery/battery_defaults.h b/usermods/Battery/battery_defaults.h index 199ee3432..6d0a95dc4 100644 --- a/usermods/Battery/battery_defaults.h +++ b/usermods/Battery/battery_defaults.h @@ -40,10 +40,7 @@ #ifndef USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE #define USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE 4.2f #endif -#ifndef USERMOD_BATTERY_UNKOWN_CALIBRATION - // offset or calibration value to fine tune the calculated voltage - #define USERMOD_BATTERY_UNKOWN_CALIBRATION 0 -#endif + /* * * Lithium polymer (Li-Po) defaults @@ -56,9 +53,7 @@ #ifndef USERMOD_BATTERY_LIPO_MAX_VOLTAGE #define USERMOD_BATTERY_LIPO_MAX_VOLTAGE 4.2f #endif -#ifndef USERMOD_BATTERY_LIPO_CALIBRATION - #define USERMOD_BATTERY_LIPO_CALIBRATION 0 -#endif + /* * * Lithium-ion (Li-Ion) defaults @@ -71,12 +66,8 @@ #ifndef USERMOD_BATTERY_LION_MAX_VOLTAGE #define USERMOD_BATTERY_LION_MAX_VOLTAGE 4.2f #endif -#ifndef USERMOD_BATTERY_LION_CALIBRATION - // offset or calibration value to fine tune the calculated voltage - #define USERMOD_BATTERY_LION_CALIBRATION 0 -#endif -//the default ratio for the voltage divider +// the default ratio for the voltage divider #ifndef USERMOD_BATTERY_VOLTAGE_MULTIPLIER #ifdef ARDUINO_ARCH_ESP32 #define USERMOD_BATTERY_VOLTAGE_MULTIPLIER 2.0f @@ -85,13 +76,8 @@ #endif #endif -#ifndef USERMOD_BATTERY_MAX_VOLTAGE - #define USERMOD_BATTERY_MAX_VOLTAGE 4.2f -#endif - -// a common capacity for single 18650 battery cells is between 2500 and 3600 mAh -#ifndef USERMOD_BATTERY_TOTAL_CAPACITY - #define USERMOD_BATTERY_TOTAL_CAPACITY 3100 +#ifndef USERMOD_BATTERY_AVERAGING_ALPHA + #define USERMOD_BATTERY_AVERAGING_ALPHA 0.1f #endif // offset or calibration value to fine tune the calculated voltage @@ -138,7 +124,6 @@ typedef struct bconfig_t batteryType type; float minVoltage; float maxVoltage; - unsigned int capacity; // current capacity float voltage; // current voltage int8_t level; // current level float calibration; // offset or calibration value to fine tune the calculated voltage diff --git a/usermods/Battery/types/lion.h b/usermods/Battery/types/lion.h index 2ff54a1ea..0d2325386 100644 --- a/usermods/Battery/types/lion.h +++ b/usermods/Battery/types/lion.h @@ -14,11 +14,10 @@ class Lion : public Battery public: Lion() + : Battery() { this->setMinVoltage(USERMOD_BATTERY_LION_MIN_VOLTAGE); this->setMaxVoltage(USERMOD_BATTERY_LION_MAX_VOLTAGE); - this->setVoltage(this->getVoltage()); - this->setCalibration(USERMOD_BATTERY_LION_CALIBRATION); } void update(batteryConfig cfg) diff --git a/usermods/Battery/types/lipo.h b/usermods/Battery/types/lipo.h index 264d3251e..f65ab12c5 100644 --- a/usermods/Battery/types/lipo.h +++ b/usermods/Battery/types/lipo.h @@ -14,11 +14,10 @@ class Lipo : public Battery public: Lipo() + : Battery() { this->setMinVoltage(USERMOD_BATTERY_LIPO_MIN_VOLTAGE); this->setMaxVoltage(USERMOD_BATTERY_LIPO_MAX_VOLTAGE); - this->setVoltage(this->getVoltage()); - this->setCalibration(USERMOD_BATTERY_LIPO_CALIBRATION); } void update(batteryConfig cfg) diff --git a/usermods/Battery/types/unkown.h b/usermods/Battery/types/unkown.h index 2b38da96c..edf220040 100644 --- a/usermods/Battery/types/unkown.h +++ b/usermods/Battery/types/unkown.h @@ -14,18 +14,16 @@ class Unkown : public Battery public: Unkown() + : Battery() { this->setMinVoltage(USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE); this->setMaxVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE); - this->setVoltage(this->getVoltage()); - this->setCalibration(USERMOD_BATTERY_UNKOWN_CALIBRATION); } void update(batteryConfig cfg) { if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); else this->setMinVoltage(USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE); if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); else this->setMaxVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE); - if(cfg.calibration) this->setCalibration(cfg.calibration); else this->setCalibration(USERMOD_BATTERY_UNKOWN_CALIBRATION); } float mapVoltage(float v, float min, float max) override diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index a91331cb7..9b980d557 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -20,27 +20,15 @@ class UsermodBattery : public Usermod // battery pin can be defined in my_config.h int8_t batteryPin = USERMOD_BATTERY_MEASUREMENT_PIN; - Battery* bat = nullptr; - batteryConfig bcfg; + Battery* bat = new Unkown(); + batteryConfig cfg; // how often to read the battery voltage unsigned long readingInterval = USERMOD_BATTERY_MEASUREMENT_INTERVAL; unsigned long nextReadTime = 0; unsigned long lastReadTime = 0; - // battery min. voltage - float minBatteryVoltage = 3.3f; - // battery max. voltage - float maxBatteryVoltage = USERMOD_BATTERY_MAX_VOLTAGE; - // all battery cells summed up - unsigned int totalBatteryCapacity = USERMOD_BATTERY_TOTAL_CAPACITY; - // raw analog reading - float rawValue = 0.0f; - // calculated voltage - float voltage = maxBatteryVoltage; // between 0 and 1, to control strength of voltage smoothing filter - float alpha = 0.05f; - // mapped battery level based on voltage - int8_t batteryLevel = 100; + float alpha = USERMOD_BATTERY_AVERAGING_ALPHA; // offset or calibration value to fine tune the calculated voltage float calibration = USERMOD_BATTERY_CALIBRATION; @@ -70,13 +58,16 @@ class UsermodBattery : public Usermod static const char _duration[]; static const char _init[]; + /** + * Helper for rounding floating point values + */ float dot2round(float x) { float nx = (int)(x * 100 + .5); return (float)(nx / 100); } - /* + /** * Turn off all leds */ void turnOff() @@ -85,7 +76,7 @@ class UsermodBattery : public Usermod stateUpdated(CALL_MODE_DIRECT_CHANGE); } - /* + /** * Indicate low power by activating a configured preset for a given time and then switching back to the preset that was selected previously */ void lowPowerIndicator() @@ -108,25 +99,24 @@ class UsermodBattery : public Usermod } } - // float readVoltage() - // { - // #ifdef ARDUINO_ARCH_ESP32 - // // use calibrated millivolts analogread on esp32 (150 mV ~ 2450 mV default attentuation) and divide by 1000 to get from milivolts to volts and multiply by voltage multiplier and apply calibration value - // return (analogReadMilliVolts(batteryPin) / 1000.0f) * voltageMultiplier + calibration; - // #else - // // use analog read on esp8266 ( 0V ~ 1V no attenuation options) and divide by ADC precision 1023 and multiply by voltage multiplier and apply calibration value - // return (analogRead(batteryPin) / 1023.0f) * voltageMultiplier + calibration; - // #endif - // } + /** + * read the battery voltage in different ways depending on the architecture + */ + float readVoltage() + { + #ifdef ARDUINO_ARCH_ESP32 + // use calibrated millivolts analogread on esp32 (150 mV ~ 2450 mV default attentuation) and divide by 1000 to get from milivolts to volts and multiply by voltage multiplier and apply calibration value + return (analogReadMilliVolts(batteryPin) / 1000.0f) * bat->getVoltageMultiplier() + bat->getCalibration(); + #else + // use analog read on esp8266 ( 0V ~ 1V no attenuation options) and divide by ADC precision 1023 and multiply by voltage multiplier and apply calibration value + return (analogRead(batteryPin) / 1023.0f) * bat->getVoltageMultiplier() + bat->getCalibration(); + #endif + } public: - UsermodBattery() - { - bat = new Unkown(); - } //Functions called by WLED - /* + /** * setup() is called once at boot. WiFi is not yet connected at this point. * You can use it to initialize variables, sensors or similar. */ @@ -153,16 +143,15 @@ class UsermodBattery : public Usermod // voltage = readVoltage(); #endif - //this could also be handled with a factory class but for only 2 types it should be sufficient for now - if(bcfg.type == (batteryType)lipo) { + // plug in the right battery type + if(cfg.type == (batteryType)lipo) { bat = new Lipo(); - } else if(bcfg.type == (batteryType)lion) { + } else if(cfg.type == (batteryType)lion) { bat = new Lion(); - } else { - bat = new Unkown(); // nullObject pattern } - bat->update(bcfg); + // update the choosen battery type with configured values + bat->update(cfg); nextReadTime = millis() + readingInterval; lastReadTime = millis(); @@ -171,7 +160,7 @@ class UsermodBattery : public Usermod } - /* + /** * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ @@ -199,28 +188,15 @@ class UsermodBattery : public Usermod if (batteryPin < 0) return; // nothing to read - initializing = false; - float voltage = -1.0f; - float rawValue = 0.0f; -#ifdef ARDUINO_ARCH_ESP32 - // use calibrated millivolts analogread on esp32 (150 mV ~ 2450 mV) - rawValue = analogReadMilliVolts(batteryPin); - // calculate the voltage - voltage = (rawValue / 1000.0f) + calibration; - // usually a voltage divider (50%) is used on ESP32, so we need to multiply by 2 - voltage *= 2.0f; -#else - // read battery raw input - rawValue = analogRead(batteryPin); - // calculate the voltage - voltage = ((rawValue / getAdcPrecision()) * bat->getMaxVoltage()) + bat->getCalibration(); -#endif - // filter with exponential smoothing because ADC in esp32 is fluctuating too much for a good single readout - voltage = voltage + alpha * (rawValue - voltage); + initializing = false; + float rawValue = readVoltage(); - bat->setVoltage(voltage); + // filter with exponential smoothing because ADC in esp32 is fluctuating too much for a good single readout + float filteredVoltage = bat->getVoltage() + alpha * (rawValue - bat->getVoltage()); + + bat->setVoltage(filteredVoltage); // translate battery voltage into percentage - bat->calculateAndSetLevel(voltage); + bat->calculateAndSetLevel(filteredVoltage); // Auto off -- Master power off if (autoOffEnabled && (autoOffThreshold >= bat->getLevel())) @@ -232,13 +208,13 @@ class UsermodBattery : public Usermod if (WLED_MQTT_CONNECTED) { char buf[64]; // buffer for snprintf() snprintf_P(buf, 63, PSTR("%s/voltage"), mqttDeviceTopic); - mqtt->publish(buf, 0, false, String(voltage).c_str()); + mqtt->publish(buf, 0, false, String(bat->getVoltage()).c_str()); } #endif } - /* + /** * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. * Below it is shown how this could be used for e.g. a light sensor @@ -286,7 +262,7 @@ class UsermodBattery : public Usermod void addBatteryToJsonObject(JsonObject& battery, bool forJsonState) { - if(forJsonState) { battery[F("type")] = bcfg.type; } else {battery[F("type")] = (String)bcfg.type; } // has to be a String otherwise it won't get converted to a Dropdown + if(forJsonState) { battery[F("type")] = cfg.type; } else {battery[F("type")] = (String)cfg.type; } // has to be a String otherwise it won't get converted to a Dropdown battery[F("min-voltage")] = bat->getMinVoltage(); battery[F("max-voltage")] = bat->getMaxVoltage(); battery[F("calibration")] = bat->getCalibration(); @@ -305,10 +281,10 @@ class UsermodBattery : public Usermod void getUsermodConfigFromJsonObject(JsonObject& battery) { - getJsonValue(battery[F("type")], bcfg.type); - getJsonValue(battery[F("min-voltage")], bcfg.minVoltage); - getJsonValue(battery[F("max-voltage")], bcfg.maxVoltage); - getJsonValue(battery[F("calibration")], bcfg.calibration); + getJsonValue(battery[F("type")], cfg.type); + getJsonValue(battery[F("min-voltage")], cfg.minVoltage); + getJsonValue(battery[F("max-voltage")], cfg.maxVoltage); + getJsonValue(battery[F("calibration")], cfg.calibration); setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); @@ -324,10 +300,10 @@ class UsermodBattery : public Usermod setLowPowerIndicatorDuration(lp[FPSTR(_duration)] | lowPowerIndicatorDuration); if(initDone) - bat->update(bcfg); + bat->update(cfg); } - /* + /** * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ @@ -345,7 +321,7 @@ class UsermodBattery : public Usermod } - /* + /** * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ @@ -365,7 +341,7 @@ class UsermodBattery : public Usermod */ - /* + /** * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. * It will be called by WLED when settings are actually saved (for example, LED settings are saved) * If you want to force saving the current state, use serializeConfig() in your loop(). @@ -449,7 +425,7 @@ class UsermodBattery : public Usermod } - /* + /** * readFromConfig() can be used to read back the custom settings you added with addToConfig(). * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page) * @@ -482,8 +458,8 @@ class UsermodBattery : public Usermod newBatteryPin = battery[F("pin")] | newBatteryPin; #endif // calculateTimeLeftEnabled = battery[F("time-left")] | calculateTimeLeftEnabled; - setMinBatteryVoltage(battery[F("min-voltage")] | minBatteryVoltage); - setMaxBatteryVoltage(battery[F("max-voltage")] | maxBatteryVoltage); + setMinBatteryVoltage(battery[F("min-voltage")] | bat->getMinVoltage()); + setMaxBatteryVoltage(battery[F("max-voltage")] | bat->getMaxVoltage()); setCalibration(battery[F("calibration")] | calibration); setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); @@ -515,7 +491,7 @@ class UsermodBattery : public Usermod return !battery[FPSTR(_readInterval)].isNull(); } - /* + /** * TBD: Generate a preset sample for low power indication * a button on the config page would be cool, currently not possible */ @@ -554,7 +530,7 @@ class UsermodBattery : public Usermod * */ - /* + /** * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). * This could be used in the future for the system to determine whether your usermod is installed. */ @@ -569,7 +545,7 @@ class UsermodBattery : public Usermod return readingInterval; } - /* + /** * minimum repetition is 3000ms (3s) */ void setReadingInterval(unsigned long newReadingInterval) @@ -577,100 +553,78 @@ class UsermodBattery : public Usermod readingInterval = max((unsigned long)3000, newReadingInterval); } - /* - * Get the choosen adc precision - * esp8266 = 10bit resolution = 1024.0f - * esp32 = 12bit resolution = 4095.0f - */ - float getAdcPrecision() - { - #ifdef ARDUINO_ARCH_ESP32 - // esp32 - return 4096.0f; - #else - // esp8266 - return 1024.0f; - #endif - } - - /* - - /* + /** * Get lowest configured battery voltage */ float getMinBatteryVoltage() { - return minBatteryVoltage; + return bat->getMinVoltage(); } - /* + /** * Set lowest battery voltage - * can't be below 0 volt + * cant be below 0 volt */ void setMinBatteryVoltage(float voltage) { - minBatteryVoltage = max(0.0f, voltage); + bat->setMinVoltage(voltage); } - /* + /** * Get highest configured battery voltage */ float getMaxBatteryVoltage() { - return maxBatteryVoltage; + return bat->getMaxVoltage(); } - /* + /** * Set highest battery voltage * can't be below minBatteryVoltage */ void setMaxBatteryVoltage(float voltage) { - #ifdef USERMOD_BATTERY_USE_LIPO - maxBatteryVoltage = max(getMinBatteryVoltage()+0.7f, voltage); - #else - maxBatteryVoltage = max(getMinBatteryVoltage()+1.0f, voltage); - #endif + bat->setMaxVoltage(voltage); } - /* + /** * Get the calculated voltage * formula: (adc pin value / adc precision * max voltage) + calibration */ float getVoltage() { - return voltage; + return bat->getVoltage(); } - /* + /** * Get the mapped battery level (0 - 100) based on voltage * important: voltage can drop when a load is applied, so its only an estimate */ int8_t getBatteryLevel() { - return batteryLevel; + return bat->getLevel(); } - /* + /** * Get the configured calibration value * a offset value to fine-tune the calculated voltage. */ float getCalibration() { - return calibration; + return bat->getCalibration(); } - /* + /** * Set the voltage calibration offset value * a offset value to fine-tune the calculated voltage. */ void setCalibration(float offset) { - calibration = offset; + bat->setCalibration(offset); } - /* + /** * Get auto-off feature enabled status * is auto-off enabled, true/false */ @@ -679,7 +633,7 @@ class UsermodBattery : public Usermod return autoOffEnabled; } - /* + /** * Set auto-off feature status */ void setAutoOffEnabled(bool enabled) @@ -687,7 +641,7 @@ class UsermodBattery : public Usermod autoOffEnabled = enabled; } - /* + /** * Get auto-off threshold in percent (0-100) */ int8_t getAutoOffThreshold() @@ -695,7 +649,7 @@ class UsermodBattery : public Usermod return autoOffThreshold; } - /* + /** * Set auto-off threshold in percent (0-100) */ void setAutoOffThreshold(int8_t threshold) @@ -705,7 +659,7 @@ class UsermodBattery : public Usermod autoOffThreshold = lowPowerIndicatorEnabled /*&& autoOffEnabled*/ ? min(lowPowerIndicatorThreshold-1, (int)autoOffThreshold) : autoOffThreshold; } - /* + /** * Get low-power-indicator feature enabled status * is the low-power-indicator enabled, true/false */ @@ -714,7 +668,7 @@ class UsermodBattery : public Usermod return lowPowerIndicatorEnabled; } - /* + /** * Set low-power-indicator feature status */ void setLowPowerIndicatorEnabled(bool enabled) @@ -722,7 +676,7 @@ class UsermodBattery : public Usermod lowPowerIndicatorEnabled = enabled; } - /* + /** * Get low-power-indicator preset to activate when low power is detected */ int8_t getLowPowerIndicatorPreset() @@ -730,7 +684,7 @@ class UsermodBattery : public Usermod return lowPowerIndicatorPreset; } - /* + /** * Set low-power-indicator preset to activate when low power is detected */ void setLowPowerIndicatorPreset(int8_t presetId) @@ -748,7 +702,7 @@ class UsermodBattery : public Usermod return lowPowerIndicatorThreshold; } - /* + /** * Set low-power-indicator threshold in percent (0-100) */ void setLowPowerIndicatorThreshold(int8_t threshold) @@ -758,7 +712,7 @@ class UsermodBattery : public Usermod lowPowerIndicatorThreshold = autoOffEnabled /*&& lowPowerIndicatorEnabled*/ ? max(autoOffThreshold+1, (int)lowPowerIndicatorThreshold) : max(5, (int)lowPowerIndicatorThreshold); } - /* + /** * Get low-power-indicator duration in seconds */ int8_t getLowPowerIndicatorDuration() @@ -766,7 +720,7 @@ class UsermodBattery : public Usermod return lowPowerIndicatorDuration; } - /* + /** * Set low-power-indicator duration in seconds */ void setLowPowerIndicatorDuration(int8_t duration) @@ -774,7 +728,7 @@ class UsermodBattery : public Usermod lowPowerIndicatorDuration = duration; } - /* + /** * Get low-power-indicator status when the indication is done thsi returns true */ bool getLowPowerIndicatorDone() From 6d278994ec1a4421580883f79701e96b9e58a67f Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Fri, 29 Mar 2024 20:05:56 +0100 Subject: [PATCH 012/148] WLED 0.14.3 release - Fix for transition 0 (#3854, #3832, #3720) - copyright year update - updated AsyncWebServer to v2.2.0 --- CHANGELOG.md | 4 +++ package-lock.json | 4 +-- package.json | 2 +- platformio.ini | 7 ++-- wled00/data/settings_sec.htm | 2 +- wled00/html_other.h | 66 ++++++++++++++++++------------------ wled00/html_settings.h | 30 ++++++++-------- wled00/improv.cpp | 2 +- wled00/led.cpp | 2 ++ wled00/wled.h | 2 +- 10 files changed, 63 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8095a15d2..1cff856a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## WLED changelog +#### Build 2403290 +- WLED 0.14.3 release +- Fix for transition 0 (#3854, #3832, #3720) + #### Build 2403170 - WLED 0.14.2 release diff --git a/package-lock.json b/package-lock.json index 9bf4449eb..a7330143f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wled", - "version": "0.14.2", + "version": "0.14.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wled", - "version": "0.14.2", + "version": "0.14.3", "license": "ISC", "dependencies": { "clean-css": "^4.2.3", diff --git a/package.json b/package.json index 8781cd6c8..3bd73b76d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.14.2", + "version": "0.14.3", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { diff --git a/platformio.ini b/platformio.ini index 914ed05b3..0adc18cf2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -63,7 +63,7 @@ arduino_core_2_7_4 = espressif8266@2.6.2 arduino_core_3_0_0 = espressif8266@3.0.0 arduino_core_3_2_0 = espressif8266@3.2.0 arduino_core_4_1_0 = espressif8266@4.1.0 -arduino_core_3_1_2 = espressif8266@4.2.0 +arduino_core_3_1_2 = espressif8266@4.2.1 # Development platforms arduino_core_develop = https://github.com/platformio/platform-espressif8266#develop @@ -73,8 +73,7 @@ arduino_core_git = https://github.com/platformio/platform-espressif8266#feature/ platform_wled_default = ${common.arduino_core_3_1_2} # We use 2.7.4.7 for all, includes PWM flicker fix and Wstring optimization #platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7 -platform_packages = platformio/framework-arduinoespressif8266 - platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502 +platform_packages = platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502 platformio/tool-esptool #@ ~1.413.0 platformio/tool-esptoolpy #@ ~1.30000.0 @@ -181,7 +180,7 @@ lib_deps = fastled/FastLED @ 3.6.0 IRremoteESP8266 @ 2.8.2 makuna/NeoPixelBus @ 2.7.5 - https://github.com/Aircoookie/ESPAsyncWebServer.git @ ^2.1.0 + https://github.com/Aircoookie/ESPAsyncWebServer.git @ ^2.2.0 #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line #TFT_eSPI #For compatible OLED display uncomment following diff --git a/wled00/data/settings_sec.htm b/wled00/data/settings_sec.htm index 0d9ec256a..2cb4a264a 100644 --- a/wled00/data/settings_sec.htm +++ b/wled00/data/settings_sec.htm @@ -134,7 +134,7 @@ WLED version ##VERSION##

Contributors, dependencies and special thanks
A huge thank you to everyone who helped me create WLED!

- (c) 2016-2023 Christian Schwinne
+ (c) 2016-2024 Christian Schwinne
Licensed under the MIT license

Server message: Response error!
diff --git a/wled00/html_other.h b/wled00/html_other.h index 2799fcdf3..3f9402615 100644 --- a/wled00/html_other.h +++ b/wled00/html_other.h @@ -43,45 +43,45 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()====="; // Autogenerated from wled00/data/update.htm, do not edit!! const uint16_t PAGE_update_length = 613; const uint8_t PAGE_update[] PROGMEM = { - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x13, 0x75, 0x53, 0x5d, 0x6f, 0xd4, 0x30, - 0x10, 0x7c, 0xcf, 0xaf, 0x70, 0xfd, 0x74, 0x27, 0x51, 0x1b, 0x2a, 0x5e, 0x28, 0x49, 0x0a, 0x47, - 0x2b, 0x54, 0x09, 0xa9, 0x95, 0xda, 0x82, 0x78, 0x42, 0x8e, 0xbd, 0xb9, 0x98, 0x73, 0xec, 0xd4, - 0xde, 0xdc, 0xe9, 0x84, 0xfa, 0xdf, 0xd9, 0x38, 0x77, 0x05, 0xf1, 0xf1, 0x12, 0xc5, 0xd9, 0xd9, - 0xf1, 0xee, 0xcc, 0xa4, 0x3c, 0xb9, 0xbc, 0xf9, 0x70, 0xff, 0xf5, 0xf6, 0x8a, 0x75, 0xd8, 0xbb, - 0xba, 0x3c, 0x3c, 0x41, 0x99, 0xba, 0xec, 0x01, 0x15, 0xd3, 0xc1, 0x23, 0x78, 0xac, 0xf8, 0xce, - 0x1a, 0xec, 0x2a, 0x03, 0x5b, 0xab, 0xe1, 0x34, 0x1f, 0x38, 0xf3, 0xaa, 0x87, 0x8a, 0x6f, 0x2d, - 0xec, 0x86, 0x10, 0x91, 0xd7, 0x45, 0x89, 0x16, 0x1d, 0xd4, 0x5f, 0x3e, 0x5d, 0x5d, 0xb2, 0x87, - 0xc1, 0x28, 0x84, 0x52, 0xce, 0x9f, 0xca, 0xa4, 0xa3, 0x1d, 0xb0, 0x2e, 0xda, 0xd1, 0x6b, 0xb4, - 0xc1, 0xb3, 0xd5, 0x62, 0xf9, 0x63, 0x67, 0xbd, 0x09, 0x3b, 0xd1, 0xd9, 0x84, 0x21, 0xee, 0x45, - 0xa3, 0xf4, 0x66, 0xb1, 0x7c, 0x7a, 0x86, 0x3c, 0x10, 0xc4, 0x04, 0x3d, 0xf6, 0x34, 0x81, 0x58, - 0x03, 0x5e, 0x39, 0x98, 0x5e, 0x57, 0xfb, 0x6b, 0xb3, 0xe0, 0x63, 0xcb, 0x97, 0x22, 0xe1, 0xde, - 0x81, 0x30, 0x36, 0x0d, 0x4e, 0xed, 0x2b, 0xee, 0x83, 0x07, 0xfe, 0xe2, 0xbf, 0x2d, 0x7d, 0x5a, - 0xff, 0xdd, 0xd3, 0xb8, 0xa0, 0x37, 0xfc, 0xa9, 0x28, 0xe5, 0x61, 0xc4, 0xc3, 0xa8, 0x2c, 0x45, - 0x5d, 0x71, 0x99, 0x00, 0xd1, 0xfa, 0x75, 0x92, 0x49, 0x7c, 0x4f, 0x17, 0x43, 0xf5, 0x86, 0xd7, - 0xbf, 0x21, 0x27, 0xaa, 0xba, 0x78, 0x67, 0xfb, 0x49, 0x00, 0x36, 0x46, 0xb7, 0xe0, 0x33, 0xbd, - 0x4e, 0x89, 0x2f, 0xdf, 0x12, 0x32, 0x23, 0x4a, 0x39, 0x4b, 0xda, 0x04, 0xb3, 0x67, 0xc1, 0xbb, - 0xa0, 0x4c, 0xc5, 0x3f, 0x02, 0x7e, 0x5e, 0x2c, 0x89, 0xae, 0x3b, 0xab, 0x8b, 0x2c, 0xd9, 0x5d, - 0x68, 0x71, 0xa7, 0x22, 0x3c, 0x6b, 0x47, 0x95, 0xb2, 0x0d, 0xb1, 0x67, 0xe4, 0x45, 0x17, 0xa8, - 0xe7, 0xf6, 0xe6, 0xee, 0x9e, 0x33, 0x95, 0xe5, 0xa9, 0xb8, 0x90, 0x63, 0x06, 0x72, 0x66, 0xa9, - 0x46, 0x82, 0xb0, 0x02, 0x48, 0xba, 0xfd, 0x40, 0xae, 0xf4, 0xa3, 0x43, 0x3b, 0xa8, 0x88, 0x72, - 0x22, 0x38, 0x25, 0x98, 0xe2, 0x74, 0x75, 0x1a, 0x9b, 0xde, 0x92, 0x9d, 0x0f, 0xd3, 0xcd, 0xd7, - 0x3e, 0xa1, 0x72, 0x0e, 0x0c, 0xdb, 0x42, 0x4c, 0x44, 0x79, 0xce, 0xca, 0x34, 0x28, 0xcf, 0x0a, - 0xed, 0x54, 0x4a, 0x15, 0x4f, 0x76, 0xe0, 0xf5, 0x4b, 0xf1, 0xea, 0xb5, 0x38, 0xa3, 0x55, 0xa8, - 0x42, 0x2b, 0xc4, 0xfa, 0x32, 0xec, 0xf2, 0x0a, 0x0c, 0x3b, 0x60, 0x8e, 0xee, 0x4f, 0xc8, 0x1a, - 0xeb, 0x55, 0xdc, 0x53, 0xbf, 0x62, 0x45, 0x17, 0xa1, 0xad, 0x78, 0x87, 0x38, 0xa4, 0x73, 0x29, + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x13, 0x75, 0x53, 0xcb, 0x6e, 0xdb, 0x30, + 0x10, 0xbc, 0xeb, 0x2b, 0x18, 0x9e, 0x6c, 0xa0, 0x21, 0xfb, 0xba, 0x34, 0x95, 0x94, 0xd6, 0x4d, + 0x50, 0x04, 0x28, 0x90, 0x00, 0x49, 0x5a, 0xf4, 0x54, 0x50, 0xe4, 0xca, 0x62, 0x4d, 0x91, 0x0a, + 0xb9, 0xb2, 0x61, 0x14, 0xf9, 0xf7, 0xae, 0x28, 0x3b, 0x2d, 0xfa, 0xb8, 0x08, 0xa2, 0x76, 0x76, + 0xb8, 0x3b, 0x33, 0x2a, 0x4f, 0x2e, 0xae, 0x3f, 0xdc, 0x7d, 0xbd, 0xb9, 0x64, 0x1d, 0xf6, 0xae, + 0x2e, 0x0f, 0x4f, 0x50, 0xa6, 0x2e, 0x7b, 0x40, 0xc5, 0x74, 0xf0, 0x08, 0x1e, 0x2b, 0xbe, 0xb3, + 0x06, 0xbb, 0xca, 0xc0, 0xd6, 0x6a, 0x38, 0xcd, 0x07, 0xce, 0xbc, 0xea, 0xa1, 0xe2, 0x5b, 0x0b, + 0xbb, 0x21, 0x44, 0xe4, 0x75, 0x51, 0xa2, 0x45, 0x07, 0xf5, 0x97, 0x4f, 0x97, 0x17, 0xec, 0x7e, + 0x30, 0x0a, 0xa1, 0x94, 0xf3, 0xa7, 0x32, 0xe9, 0x68, 0x07, 0xac, 0x8b, 0x76, 0xf4, 0x1a, 0x6d, + 0xf0, 0x6c, 0xb5, 0x58, 0xfe, 0xd8, 0x59, 0x6f, 0xc2, 0x4e, 0x74, 0x36, 0x61, 0x88, 0x7b, 0xd1, + 0x28, 0xbd, 0x59, 0x2c, 0x1f, 0x9f, 0x20, 0xf7, 0x04, 0x31, 0x41, 0x8f, 0x3d, 0x4d, 0x20, 0xd6, + 0x80, 0x97, 0x0e, 0xa6, 0xd7, 0xd5, 0xfe, 0xca, 0x2c, 0xf8, 0xd8, 0xf2, 0xa5, 0x48, 0xb8, 0x77, + 0x20, 0x8c, 0x4d, 0x83, 0x53, 0xfb, 0x8a, 0xfb, 0xe0, 0x81, 0x3f, 0xfb, 0x6f, 0x4b, 0x9f, 0xd6, + 0x7f, 0xf7, 0x34, 0x2e, 0xe8, 0x0d, 0x7f, 0x2c, 0x4a, 0x79, 0x18, 0xf1, 0x30, 0x2a, 0x4b, 0x51, + 0x57, 0x5c, 0x26, 0x40, 0xb4, 0x7e, 0x9d, 0x64, 0x12, 0xdf, 0xd3, 0xf9, 0x50, 0xbd, 0xe1, 0xf5, + 0x6f, 0xc8, 0x89, 0xaa, 0x2e, 0xde, 0xd9, 0x7e, 0x12, 0x80, 0x8d, 0xd1, 0x2d, 0xf8, 0x4c, 0xaf, + 0x53, 0xe2, 0xcb, 0xb7, 0x84, 0xcc, 0x88, 0x52, 0xce, 0x92, 0x36, 0xc1, 0xec, 0x59, 0xf0, 0x2e, + 0x28, 0x53, 0xf1, 0x8f, 0x80, 0x9f, 0x17, 0x4b, 0xa2, 0xeb, 0x5e, 0xd6, 0x45, 0x96, 0xec, 0x36, + 0xb4, 0xb8, 0x53, 0x11, 0x9e, 0xb4, 0xa3, 0x4a, 0xd9, 0x86, 0xd8, 0x33, 0xf2, 0xa2, 0x0b, 0xd4, + 0x73, 0x73, 0x7d, 0x7b, 0xc7, 0x99, 0xca, 0xf2, 0x54, 0x5c, 0xc8, 0x31, 0x03, 0x39, 0xb3, 0x54, + 0x23, 0x41, 0x58, 0x01, 0x24, 0xdd, 0x7e, 0x20, 0x57, 0xfa, 0xd1, 0xa1, 0x1d, 0x54, 0x44, 0x39, + 0x11, 0x9c, 0x12, 0x4c, 0x71, 0xba, 0x3a, 0x8d, 0x4d, 0x6f, 0xc9, 0xce, 0xfb, 0xe9, 0xe6, 0x2b, + 0x9f, 0x50, 0x39, 0x07, 0x86, 0x6d, 0x21, 0x26, 0xa2, 0x3c, 0x63, 0x65, 0x1a, 0x94, 0x67, 0x85, + 0x76, 0x2a, 0xa5, 0x8a, 0x27, 0x3b, 0xf0, 0xfa, 0xb9, 0x78, 0xf1, 0x5a, 0xbc, 0xa2, 0x55, 0xa8, + 0x42, 0x2b, 0xc4, 0xfa, 0x22, 0xec, 0xf2, 0x0a, 0x0c, 0x3b, 0x60, 0x8e, 0xee, 0x4f, 0xc8, 0x1a, + 0xeb, 0x55, 0xdc, 0x53, 0xbf, 0x62, 0x45, 0x17, 0xa1, 0xad, 0x78, 0x87, 0x38, 0xa4, 0x33, 0x29, 0xd7, 0x16, 0xbb, 0xb1, 0x11, 0x3a, 0xf4, 0xf2, 0xbd, 0x8d, 0x3a, 0x84, 0xb0, 0xb1, 0x20, 0xa7, 0x7d, 0x65, 0x04, 0x07, 0x2a, 0x41, 0xe2, 0x0c, 0x55, 0x24, 0xb3, 0x2a, 0xfe, 0xad, 0x71, 0xca, 0x6f, 0x48, 0x13, 0xdb, 0xaf, 0x59, 0x91, 0x1d, 0x38, 0xf2, 0xd0, 0x17, 0x91, 0x3a, 0x0b, 0xce, - 0x24, 0x61, 0xc3, 0x81, 0xf6, 0x48, 0xf1, 0x27, 0xb5, 0x48, 0xdb, 0xf5, 0x45, 0xd6, 0xbe, 0x6a, - 0x69, 0xc2, 0xd3, 0xf4, 0x38, 0x92, 0xae, 0x53, 0x42, 0xa5, 0xca, 0x3b, 0x94, 0xd6, 0x0f, 0x23, - 0xb2, 0x59, 0xab, 0xd6, 0x3a, 0x38, 0xa6, 0xf9, 0xa8, 0x68, 0x84, 0xc7, 0xd1, 0x46, 0x30, 0x33, + 0x24, 0x61, 0xc3, 0x81, 0xf6, 0x48, 0xf1, 0x27, 0xb5, 0x48, 0xdb, 0xf5, 0x79, 0xd6, 0xbe, 0x6a, + 0x69, 0xc2, 0xd3, 0xf4, 0x30, 0x92, 0xae, 0x53, 0x42, 0xa5, 0xca, 0x3b, 0x94, 0xd6, 0x0f, 0x23, + 0xb2, 0x59, 0xab, 0xd6, 0x3a, 0x38, 0xa6, 0xf9, 0xa8, 0x68, 0x84, 0x87, 0xd1, 0x46, 0x30, 0x33, 0xba, 0x19, 0x11, 0x29, 0x90, 0x33, 0x7c, 0xd6, 0x90, 0xc8, 0x66, 0x9b, 0x4e, 0x4a, 0x39, 0x97, 0xff, 0x01, 0x9d, 0x0f, 0x93, 0xf0, 0xda, 0x59, 0xbd, 0xa9, 0xf8, 0x6a, 0xd2, 0x7d, 0x45, 0x39, 0xff, 0xd5, 0x94, 0x0d, 0xaa, 0x4b, 0x63, 0xb7, 0x45, 0xf6, 0x71, 0x4a, 0x29, 0xd1, 0xd4, 0x99, - 0x9d, 0xa2, 0x27, 0x84, 0x20, 0x70, 0x26, 0xbf, 0xcd, 0xcb, 0x32, 0x13, 0x98, 0x0f, 0xc8, 0xb4, - 0x0b, 0x74, 0x08, 0x91, 0x66, 0x6d, 0x23, 0xa4, 0x2e, 0xfb, 0x31, 0xa8, 0x35, 0xb0, 0xf3, 0x65, - 0x29, 0x89, 0x6f, 0x5a, 0x77, 0x8a, 0xdc, 0x94, 0xbf, 0xe9, 0xc7, 0xfe, 0x09, 0x3d, 0x8c, 0x87, - 0xc8, 0xee, 0x03, 0x00, 0x00 + 0x9d, 0xa2, 0x27, 0x84, 0x20, 0x70, 0x26, 0xbf, 0xc9, 0xcb, 0x32, 0x13, 0x98, 0x0f, 0xc8, 0xb4, + 0x0b, 0x74, 0x08, 0x91, 0x66, 0x6d, 0x23, 0xa4, 0x2e, 0xfb, 0x31, 0xa8, 0x35, 0xb0, 0xb3, 0x65, + 0x29, 0x89, 0x6f, 0x5a, 0x77, 0x8a, 0xdc, 0x94, 0xbf, 0xe9, 0xc7, 0xfe, 0x09, 0x81, 0xae, 0xc2, + 0xc6, 0xee, 0x03, 0x00, 0x00 }; diff --git a/wled00/html_settings.h b/wled00/html_settings.h index 62d3404dc..a1bd61e9e 100644 --- a/wled00/html_settings.h +++ b/wled00/html_settings.h @@ -1753,21 +1753,21 @@ const uint8_t PAGE_settings_sec[] PROGMEM = { 0xcb, 0xf1, 0x18, 0xb7, 0xdc, 0x36, 0x49, 0x9a, 0x44, 0x68, 0x3f, 0x23, 0xa1, 0xe0, 0x66, 0xd5, 0x38, 0xc4, 0xd5, 0xbd, 0x77, 0x2c, 0x75, 0xa2, 0x94, 0x9a, 0x49, 0xd1, 0xa3, 0x1e, 0xbc, 0x87, 0x3a, 0x1d, 0xeb, 0x29, 0x7d, 0xee, 0xfa, 0x38, 0xce, 0x51, 0x22, 0xf9, 0x80, 0x86, 0x29, 0x08, - 0x3a, 0xec, 0x4e, 0x68, 0xa2, 0x19, 0xb6, 0x1b, 0xee, 0x3d, 0x0f, 0xf7, 0x57, 0x44, 0xf5, 0x33, - 0xe2, 0x17, 0x72, 0x26, 0x7b, 0xee, 0xce, 0x46, 0xed, 0xab, 0xd2, 0x66, 0x07, 0x54, 0xb8, 0x93, - 0x68, 0x91, 0x4a, 0x8a, 0xd0, 0xce, 0xfd, 0xc3, 0x37, 0x97, 0xa2, 0x54, 0x0b, 0xba, 0x7d, 0x82, - 0xfc, 0xa4, 0xa8, 0x39, 0xd4, 0x94, 0x22, 0x91, 0x20, 0x1c, 0x8b, 0x72, 0x30, 0x5b, 0xe5, 0x6d, - 0xe7, 0x98, 0x65, 0x15, 0xaa, 0x85, 0x1b, 0x75, 0x05, 0x11, 0xbc, 0x8f, 0x3b, 0x97, 0x5e, 0xaa, - 0x42, 0xa0, 0x50, 0x29, 0x46, 0x7d, 0x1e, 0xe0, 0x02, 0x33, 0xd6, 0xdf, 0x89, 0xdc, 0xfd, 0xa3, - 0xdb, 0x5a, 0xd4, 0xf1, 0x12, 0x9f, 0xed, 0xef, 0xee, 0xfd, 0xb6, 0xb3, 0xbf, 0xbb, 0xff, 0x8c, - 0xbd, 0xca, 0x34, 0x6e, 0xda, 0x12, 0xd4, 0x39, 0x4c, 0xb2, 0x05, 0x7d, 0xa7, 0x68, 0x6a, 0xd4, - 0x05, 0xaa, 0x17, 0xae, 0x7e, 0x80, 0x1d, 0x4a, 0xd5, 0x25, 0xf6, 0xa7, 0xc0, 0x18, 0xe7, 0x6a, - 0xdc, 0x9b, 0xe3, 0x8a, 0x2e, 0x74, 0xef, 0xe2, 0xfc, 0xd5, 0xd9, 0xdb, 0xe1, 0xd9, 0xb7, 0xf0, - 0x77, 0x2e, 0xcf, 0x47, 0x2c, 0xaf, 0x4f, 0x72, 0x06, 0xa2, 0xae, 0xb5, 0x8a, 0x0e, 0x85, 0x86, - 0x55, 0x30, 0xc3, 0x18, 0x54, 0x47, 0xb0, 0x29, 0xee, 0xca, 0x45, 0x4b, 0x03, 0x46, 0x96, 0x9c, - 0x12, 0xd6, 0x7d, 0x4f, 0x60, 0xee, 0x13, 0x01, 0xf1, 0x3c, 0x2d, 0xa9, 0x49, 0x95, 0x92, 0x84, - 0x28, 0xa2, 0xfe, 0x06, 0xf3, 0xdd, 0x2e, 0xf6, 0xdb, 0x26, 0xb6, 0xf3, 0x53, 0x5d, 0x6c, 0x8f, - 0x4a, 0x0d, 0x7e, 0xa8, 0x79, 0xa7, 0x4e, 0x9e, 0xbe, 0xec, 0xfe, 0x17, 0x52, 0x67, 0xe8, 0x26, + 0x3a, 0xec, 0x4e, 0x68, 0xa2, 0x19, 0xb6, 0x1b, 0xee, 0x3d, 0x0f, 0x9f, 0xad, 0x88, 0xea, 0x67, + 0xc4, 0x2f, 0xe4, 0x4c, 0xf6, 0xdc, 0x9d, 0x8d, 0xda, 0x57, 0xa5, 0xcd, 0x0e, 0xa8, 0x70, 0x27, + 0xd1, 0x22, 0x95, 0x14, 0xa1, 0x9d, 0xfb, 0x87, 0x6f, 0x2e, 0x45, 0xa9, 0x16, 0x74, 0xfb, 0x04, + 0xf9, 0x49, 0x51, 0x73, 0xa8, 0x29, 0x45, 0x22, 0x41, 0x38, 0x16, 0xe5, 0x60, 0xb6, 0xca, 0xdb, + 0xce, 0x31, 0xcb, 0x2a, 0x54, 0x0b, 0x37, 0xea, 0x0a, 0x22, 0x78, 0x1f, 0x77, 0x2e, 0xbd, 0x54, + 0x85, 0x40, 0xa1, 0x52, 0x8c, 0xfa, 0x3c, 0xc0, 0x05, 0x66, 0xac, 0xbf, 0x13, 0xb9, 0xfb, 0x47, + 0xb7, 0xb5, 0xa8, 0xe3, 0x25, 0x3e, 0xdb, 0xdf, 0xdd, 0xfb, 0x6d, 0x67, 0x7f, 0x77, 0xff, 0x39, + 0x7b, 0x95, 0x69, 0xdc, 0xb4, 0x25, 0xa8, 0x73, 0x98, 0x64, 0x0b, 0xfa, 0x4e, 0xd1, 0xd4, 0xa8, + 0x0b, 0x54, 0x2f, 0x5c, 0xfd, 0x00, 0x3b, 0x94, 0xaa, 0x4b, 0xec, 0x4f, 0x81, 0x31, 0xce, 0xd5, + 0xb8, 0x37, 0xc7, 0x15, 0x5d, 0xe8, 0xde, 0xc5, 0xf9, 0xab, 0xb3, 0xb7, 0xc3, 0xb3, 0x6f, 0xe1, + 0xef, 0x5c, 0x9e, 0x8f, 0x58, 0x5e, 0x9f, 0xe4, 0x0c, 0x44, 0x5d, 0x6b, 0x15, 0x1d, 0x0a, 0x0d, + 0xab, 0x60, 0x86, 0x31, 0xa8, 0x8e, 0x60, 0x53, 0xdc, 0x95, 0x8b, 0x96, 0x06, 0x8c, 0x2c, 0x39, + 0x25, 0xac, 0xfb, 0x9e, 0xc0, 0xdc, 0x27, 0x02, 0xe2, 0x79, 0x5a, 0x52, 0x93, 0x2a, 0x25, 0x09, + 0x51, 0x44, 0xfd, 0x0d, 0xe6, 0xbb, 0x5d, 0xec, 0xb7, 0x4d, 0x6c, 0xe7, 0xa7, 0xba, 0xd8, 0x1e, + 0x95, 0x1a, 0xfc, 0x50, 0xf3, 0x4e, 0x9d, 0x3c, 0x7d, 0xd9, 0xfd, 0x2f, 0x9b, 0x42, 0x3b, 0x5f, 0xe9, 0x15, 0x00, 0x00 }; diff --git a/wled00/improv.cpp b/wled00/improv.cpp index 2267c3591..d7d9ad05d 100644 --- a/wled00/improv.cpp +++ b/wled00/improv.cpp @@ -210,7 +210,7 @@ void sendImprovInfoResponse() { //Use serverDescription if it has been changed from the default "WLED", else mDNS name bool useMdnsName = (strcmp(serverDescription, "WLED") == 0 && strlen(cmDNS) > 0); char vString[20]; - sprintf_P(vString, PSTR("0.14.2/%i"), VERSION); + sprintf_P(vString, PSTR("0.14.3/%i"), VERSION); const char *str[4] = {"WLED", vString, bString, useMdnsName ? cmDNS : serverDescription}; sendImprovRPCResult(ImprovRPCType::Request_Info, 4, str); diff --git a/wled00/led.cpp b/wled00/led.cpp index e668f3340..efaab2706 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -137,6 +137,8 @@ void stateUpdated(byte callMode) { if (strip.getTransition() == 0) { jsonTransitionOnce = false; transitionActive = false; + applyFinalBri(); + strip.trigger(); return; } diff --git a/wled00/wled.h b/wled00/wled.h index 9f6ba88fd..ba38c5c79 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -8,7 +8,7 @@ */ // version code in format yymmddb (b = daily build) -#define VERSION 2403170 +#define VERSION 2403290 //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG From 02405b4856d75a0cd29bf8ff5ba9239562ec8072 Mon Sep 17 00:00:00 2001 From: Woody Date: Wed, 3 Apr 2024 12:05:15 +0200 Subject: [PATCH 013/148] Create stale.yml --- .github/workflows/stale.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..4d531fd51 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,27 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '0 12 * * *' + workflow_dispatch: + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + days-before-stale: 120 + days-before-close: 7 + exempt-issue-labels: 'pinned,keep,enhancement,confirmed' + exempt-pr-labels: 'pinned,keep,enhancement,confirmed' + stale-issue-message: > + Hey! This issue has been open for quite some time without any new comments now. + It will be closed automatically in a week if no further activity occurs. + + Thank you for using WLED! ✨ + stale-pr-message: > + Hey! This pull request has been open for quite some time without any new comments now. + It will be closed automatically in a week if no further activity occurs. + + Thank you for using WLED! ✨ + debug-only: true From bff6697690cd6a3de72f183403949e75644a3e00 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Mon, 1 Apr 2024 11:25:31 -0400 Subject: [PATCH 014/148] Update to AsyncWebServer v2.2.1 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 0adc18cf2..cbe13c0dd 100644 --- a/platformio.ini +++ b/platformio.ini @@ -180,7 +180,7 @@ lib_deps = fastled/FastLED @ 3.6.0 IRremoteESP8266 @ 2.8.2 makuna/NeoPixelBus @ 2.7.5 - https://github.com/Aircoookie/ESPAsyncWebServer.git @ ^2.2.0 + https://github.com/Aircoookie/ESPAsyncWebServer.git @ 2.2.1 #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line #TFT_eSPI #For compatible OLED display uncomment following From 00f5471270757e435753d7bbce46870ba1144cca Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Thu, 4 Apr 2024 21:59:41 +0200 Subject: [PATCH 015/148] Build bump, changelog udate --- CHANGELOG.md | 3 ++- wled00/wled.h | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cff856a4..f1fe5cbdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ ## WLED changelog -#### Build 2403290 +#### Build 2404040 - WLED 0.14.3 release - Fix for transition 0 (#3854, #3832, #3720) +- Fix for #3855 via #3873 (by @willmmiles) #### Build 2403170 - WLED 0.14.2 release diff --git a/wled00/wled.h b/wled00/wled.h index ba38c5c79..5834025c6 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -3,12 +3,12 @@ /* Main sketch, global variable declarations @title WLED project sketch - @version 0.14.2 + @version 0.14.3 @author Christian Schwinne */ // version code in format yymmddb (b = daily build) -#define VERSION 2403290 +#define VERSION 2404040 //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG From c3787af29d317e52cb2d9a3837f882c9412360c0 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 9 Apr 2024 20:00:00 +0200 Subject: [PATCH 016/148] Send ArtnetPollReply for Art-Net proxy universe --- wled00/e131.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/wled00/e131.cpp b/wled00/e131.cpp index a67a672c2..ee8fa3949 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -346,7 +346,6 @@ void handleArtnetPollReply(IPAddress ipAddress) { switch (DMXMode) { case DMX_MODE_DISABLED: - return; // nothing to do break; case DMX_MODE_SINGLE_RGB: @@ -391,9 +390,17 @@ void handleArtnetPollReply(IPAddress ipAddress) { break; } - for (uint16_t i = startUniverse; i <= endUniverse; ++i) { - sendArtnetPollReply(&artnetPollReply, ipAddress, i); + if (DMXMode != DMX_MODE_DISABLED) { + for (uint16_t i = startUniverse; i <= endUniverse; ++i) { + sendArtnetPollReply(&artnetPollReply, ipAddress, i); + } } + + #ifdef WLED_ENABLE_DMX + if (e131ProxyUniverse > 0 && (DMXMode == DMX_MODE_DISABLED || (e131ProxyUniverse < startUniverse || e131ProxyUniverse > endUniverse))) { + sendArtnetPollReply(&artnetPollReply, ipAddress, e131ProxyUniverse); + } + #endif } void prepareArtnetPollReply(ArtPollReply *reply) { From 94cdd884746636b5d0843ff34ac2af5fbce51d47 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Sat, 13 Apr 2024 18:25:25 +0200 Subject: [PATCH 017/148] Version bump B3 - fix for #3896 - fix WS2815 current - conditional AA setPixelColor() --- CHANGELOG.md | 5 +++++ package-lock.json | 4 ++-- package.json | 2 +- wled00/FX.cpp | 8 ++++++-- wled00/FX.h | 6 ++++++ wled00/FX_2Dfcn.cpp | 2 ++ wled00/FX_fcn.cpp | 2 ++ wled00/data/settings_leds.htm | 4 ++-- wled00/improv.cpp | 2 +- wled00/wled.h | 4 ++-- 10 files changed, 29 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f29c05f73..59c58dfa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## WLED changelog +#### Build 2404120 +- v0.15.0-b3 +- fix for #3896 & WS2815 current saving +- conditional compile for AA setPixelColor() + #### Build 2404100 - Internals: #3859, #3862, #3873, #3875 - Prefer I2S1 over RMT on ESP32 diff --git a/package-lock.json b/package-lock.json index db66b554b..b9dc5e0e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wled", - "version": "0.15.0-b2", + "version": "0.15.0-b3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wled", - "version": "0.15.0-b2", + "version": "0.15.0-b3", "license": "ISC", "dependencies": { "clean-css": "^5.3.3", diff --git a/package.json b/package.json index 6f7d634d3..b19ecc48a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.15.0-b2", + "version": "0.15.0-b3", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c0495ea24..5592f7ba8 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -3018,8 +3018,12 @@ uint16_t mode_bouncing_balls(void) { } int pos = roundf(balls[i].height * (SEGLEN - 1)); + #ifdef WLED_USE_AA_PIXELS if (SEGLEN<32) SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color); // encode virtual strip into index else SEGMENT.setPixelColor(balls[i].height + (stripNr+1)*10.0f, color); + #else + SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color); // encode virtual strip into index + #endif } } }; @@ -6052,8 +6056,8 @@ uint16_t mode_2Dfloatingblobs(void) { } } uint32_t c = SEGMENT.color_from_palette(blob->color[i], false, false, 0); - if (blob->r[i] > 1.f) SEGMENT.fill_circle(blob->x[i], blob->y[i], roundf(blob->r[i]), c); - else SEGMENT.setPixelColorXY(blob->x[i], blob->y[i], c); + if (blob->r[i] > 1.f) SEGMENT.fill_circle(roundf(blob->x[i]), roundf(blob->y[i]), roundf(blob->r[i]), c); + else SEGMENT.setPixelColorXY((int)roundf(blob->x[i]), (int)roundf(blob->y[i]), c); // move x if (blob->x[i] + blob->r[i] >= cols - 1) blob->x[i] += (blob->sX[i] * ((cols - 1 - blob->x[i]) / blob->r[i] + 0.005f)); else if (blob->x[i] - blob->r[i] <= 0) blob->x[i] += (blob->sX[i] * (blob->x[i] / blob->r[i] + 0.005f)); diff --git a/wled00/FX.h b/wled00/FX.h index 829307918..66e748602 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -575,9 +575,11 @@ typedef struct Segment { inline void setPixelColor(unsigned n, uint32_t c) { setPixelColor(int(n), c); } inline void setPixelColor(int n, byte r, byte g, byte b, byte w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } inline void setPixelColor(int n, CRGB c) { setPixelColor(n, RGBW32(c.r,c.g,c.b,0)); } + #ifdef WLED_USE_AA_PIXELS void setPixelColor(float i, uint32_t c, bool aa = true); inline void setPixelColor(float i, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0, bool aa = true) { setPixelColor(i, RGBW32(r,g,b,w), aa); } inline void setPixelColor(float i, CRGB c, bool aa = true) { setPixelColor(i, RGBW32(c.r,c.g,c.b,0), aa); } + #endif uint32_t getPixelColor(int i); // 1D support functions (some implement 2D as well) void blur(uint8_t); @@ -603,9 +605,11 @@ typedef struct Segment { inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColorXY(int(x), int(y), c); } inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } + #ifdef WLED_USE_AA_PIXELS void setPixelColorXY(float x, float y, uint32_t c, bool aa = true); inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColorXY(x, y, RGBW32(r,g,b,w), aa); } inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), aa); } + #endif uint32_t getPixelColorXY(uint16_t x, uint16_t y); // 2D support functions inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) { setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); } @@ -638,9 +642,11 @@ typedef struct Segment { inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColor(int(x), c); } inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColor(x, RGBW32(r,g,b,w)); } inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0)); } + #ifdef WLED_USE_AA_PIXELS inline void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) { setPixelColor(x, c, aa); } inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); } inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); } + #endif inline uint32_t getPixelColorXY(uint16_t x, uint16_t y) { return getPixelColor(x); } inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) { blendPixelColor(x, c, blend); } inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); } diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index fde05928e..b049ab6f0 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -218,6 +218,7 @@ void IRAM_ATTR Segment::setPixelColorXY(int x, int y, uint32_t col) } } +#ifdef WLED_USE_AA_PIXELS // anti-aliased version of setPixelColorXY() void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) { @@ -261,6 +262,7 @@ void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) setPixelColorXY(uint16_t(roundf(fX)), uint16_t(roundf(fY)), col); } } +#endif // returns RGBW values of pixel uint32_t IRAM_ATTR Segment::getPixelColorXY(uint16_t x, uint16_t y) { diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index c9dd082ea..9ab1f578b 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -777,6 +777,7 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) } } +#ifdef WLED_USE_AA_PIXELS // anti-aliased normalized version of setPixelColor() void Segment::setPixelColor(float i, uint32_t col, bool aa) { @@ -809,6 +810,7 @@ void Segment::setPixelColor(float i, uint32_t col, bool aa) setPixelColor(uint16_t(roundf(fC)) | (vStrip<<16), col); } } +#endif uint32_t IRAM_ATTR Segment::getPixelColor(int i) { diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index dddedd471..4ad4cb16e 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -153,7 +153,7 @@ { const t = parseInt(d.Sf["LT"+n].value); // LED type SELECT gId('LAdis'+n).style.display = s.selectedIndex==5 ? "inline" : "none"; - d.Sf["LA"+n].value = s.value==="0" ? 55 : s.value; + if (s.value!=="0") d.Sf["LA"+n].value = s.value; d.Sf["LA"+n].min = (isVir(t) || isAna(t)) ? 0 : 1; } function setABL() @@ -417,7 +417,7 @@ mA/LED:
- +
PSU: mA
Color Order: diff --git a/wled00/improv.cpp b/wled00/improv.cpp index 0090b4bd6..1536218ff 100644 --- a/wled00/improv.cpp +++ b/wled00/improv.cpp @@ -210,7 +210,7 @@ void sendImprovInfoResponse() { //Use serverDescription if it has been changed from the default "WLED", else mDNS name bool useMdnsName = (strcmp(serverDescription, "WLED") == 0 && strlen(cmDNS) > 0); char vString[20]; - sprintf_P(vString, PSTR("0.15.0-b2/%i"), VERSION); + sprintf_P(vString, PSTR("0.15.0-b3/%i"), VERSION); const char *str[4] = {"WLED", vString, bString, useMdnsName ? cmDNS : serverDescription}; sendImprovRPCResult(ImprovRPCType::Request_Info, 4, str); diff --git a/wled00/wled.h b/wled00/wled.h index ef53e643f..b94f7790b 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -3,12 +3,12 @@ /* Main sketch, global variable declarations @title WLED project sketch - @version 0.15.0-b2 + @version 0.15.0-b3 @author Christian Schwinne */ // version code in format yymmddb (b = daily build) -#define VERSION 2404100 +#define VERSION 2404120 //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG From a418cd2a2a5afd4bedbafca15239d510797c46fa Mon Sep 17 00:00:00 2001 From: Woody Date: Sat, 13 Apr 2024 19:37:49 +0200 Subject: [PATCH 018/148] Activate stale (#3898) * Update stale.yml Update pr text Schedule action 3 times a day * Delete old .github/stale.yml * Set exempt-all-milestones to true in stale.yml --- .github/stale.yml | 20 -------------------- .github/workflows/stale.yml | 6 +++--- 2 files changed, 3 insertions(+), 23 deletions(-) delete mode 100644 .github/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 811db619a..000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 120 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - keep - - enhancement - - confirmed -# Label to use when marking an issue as stale -staleLabel: stale -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - Hey! This issue has been open for quite some time without any new comments now. - It will be closed automatically in a week if no further activity occurs. - - Thank you for using WLED! -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 4d531fd51..aaad16a44 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,7 +1,7 @@ name: 'Close stale issues and PRs' on: schedule: - - cron: '0 12 * * *' + - cron: '0 6,12,18 * * *' workflow_dispatch: jobs: @@ -14,6 +14,7 @@ jobs: days-before-close: 7 exempt-issue-labels: 'pinned,keep,enhancement,confirmed' exempt-pr-labels: 'pinned,keep,enhancement,confirmed' + exempt-all-milestones: true stale-issue-message: > Hey! This issue has been open for quite some time without any new comments now. It will be closed automatically in a week if no further activity occurs. @@ -23,5 +24,4 @@ jobs: Hey! This pull request has been open for quite some time without any new comments now. It will be closed automatically in a week if no further activity occurs. - Thank you for using WLED! ✨ - debug-only: true + Thank you for contributing to WLED! ❤️ From d2b4d2531752ea7548cc9ba1a4e35915f0213b74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Apr 2024 19:38:03 +0200 Subject: [PATCH 019/148] Bump idna from 3.4 to 3.7 (#3895) Bumps [idna](https://github.com/kjd/idna) from 3.4 to 3.7. - [Release notes](https://github.com/kjd/idna/releases) - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst) - [Commits](https://github.com/kjd/idna/compare/v3.4...v3.7) --- updated-dependencies: - dependency-name: idna dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8b55606dc..c56efad49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ h11==0.14.0 # via # uvicorn # wsproto -idna==3.4 +idna==3.7 # via # anyio # requests @@ -50,6 +50,8 @@ starlette==0.23.1 # via platformio tabulate==0.9.0 # via platformio +typing-extensions==4.11.0 + # via starlette urllib3==1.26.18 # via requests uvicorn==0.20.0 From cd928bc586cedf5bc5f4ed418c2ce743efc29523 Mon Sep 17 00:00:00 2001 From: Woody Date: Sun, 14 Apr 2024 13:14:03 +0200 Subject: [PATCH 020/148] Increase operations-per-run in stale.yml Because of this we don't need to run this action 3 times a day --- .github/workflows/stale.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index aaad16a44..58a0b18d8 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,7 +1,7 @@ name: 'Close stale issues and PRs' on: schedule: - - cron: '0 6,12,18 * * *' + - cron: '0 12 * * *' workflow_dispatch: jobs: @@ -15,6 +15,7 @@ jobs: exempt-issue-labels: 'pinned,keep,enhancement,confirmed' exempt-pr-labels: 'pinned,keep,enhancement,confirmed' exempt-all-milestones: true + operations-per-run: 150 stale-issue-message: > Hey! This issue has been open for quite some time without any new comments now. It will be closed automatically in a week if no further activity occurs. From 442d7a7226ff6766e3dd84cea17ad89e36a96aeb Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:08:28 +0200 Subject: [PATCH 021/148] arduino-esp32 v2.0.9 --- platformio.ini | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/platformio.ini b/platformio.ini index e334b7e35..af0988a85 100644 --- a/platformio.ini +++ b/platformio.ini @@ -233,8 +233,8 @@ AR_lib_deps = kosme/arduinoFFT @ 2.0.0 ;; ;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly. ;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio. -platform = espressif32@5.3.0 -platform_packages = +platform = espressif32@ ~6.3.2 +platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) build_flags = -g -Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one -DARDUINO_ARCH_ESP32 -DESP32 @@ -248,8 +248,8 @@ lib_deps = [esp32s2] ;; generic definitions for all ESP32-S2 boards -platform = espressif32@5.3.0 -platform_packages = +platform = espressif32@ ~6.3.2 +platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv build_flags = -g -DARDUINO_ARCH_ESP32 @@ -267,8 +267,8 @@ lib_deps = [esp32c3] ;; generic definitions for all ESP32-C3 boards -platform = espressif32@5.3.0 -platform_packages = +platform = espressif32@ ~6.3.2 +platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32C3 @@ -284,8 +284,8 @@ lib_deps = [esp32s3] ;; generic definitions for all ESP32-S3 boards -platform = espressif32@5.3.0 -platform_packages = +platform = espressif32@ ~6.3.2 +platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) build_flags = -g -DESP32 -DARDUINO_ARCH_ESP32 From 7abfe68458daf62477621a9d9487470233045e47 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Mon, 15 Apr 2024 16:13:13 +0200 Subject: [PATCH 022/148] Add support for TM1914 chip --- wled00/bus_wrapper.h | 194 +++++++++++++++++++++++++++------- wled00/const.h | 1 + wled00/data/settings_leds.htm | 1 + 3 files changed, 158 insertions(+), 38 deletions(-) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index ebbeca4ad..32a5c1aae 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -74,7 +74,7 @@ #define I_8266_U1_APA106_3 82 #define I_8266_DM_APA106_3 83 #define I_8266_BB_APA106_3 84 -//WS2805 +//WS2805 (RGBCW) #define I_8266_U0_2805_5 89 #define I_8266_U1_2805_5 90 #define I_8266_DM_2805_5 91 @@ -117,10 +117,14 @@ #define I_32_RN_APA106_3 85 #define I_32_I0_APA106_3 86 #define I_32_I1_APA106_3 87 -//WS2805 +//WS2805 (RGBCW) #define I_32_RN_2805_5 93 #define I_32_I0_2805_5 94 #define I_32_I1_2805_5 95 +//TM1914 (RGB) +#define I_32_RN_TM1914_3 96 +#define I_32_I0_TM1914_3 97 +#define I_32_I1_TM1914_3 98 //APA102 @@ -170,10 +174,10 @@ #define B_8266_DM_TM1_4 NeoPixelBusLg #define B_8266_BB_TM1_4 NeoPixelBusLg //TM1829 (RGB) -#define B_8266_U0_TM2_4 NeoPixelBusLg -#define B_8266_U1_TM2_4 NeoPixelBusLg -#define B_8266_DM_TM2_4 NeoPixelBusLg -#define B_8266_BB_TM2_4 NeoPixelBusLg +#define B_8266_U0_TM2_3 NeoPixelBusLg +#define B_8266_U1_TM2_3 NeoPixelBusLg +#define B_8266_DM_TM2_3 NeoPixelBusLg +#define B_8266_BB_TM2_3 NeoPixelBusLg //UCS8903 #define B_8266_U0_UCS_3 NeoPixelBusLg //3 chan, esp8266, gpio1 #define B_8266_U1_UCS_3 NeoPixelBusLg //3 chan, esp8266, gpio2 @@ -199,6 +203,11 @@ #define B_8266_U1_2805_5 NeoPixelBusLg //esp8266, gpio2 #define B_8266_DM_2805_5 NeoPixelBusLg //esp8266, gpio3 #define B_8266_BB_2805_5 NeoPixelBusLg //esp8266, bb +//TM1914 (RGB) +#define B_8266_U0_TM1914_3 NeoPixelBusLg +#define B_8266_U1_TM1914_3 NeoPixelBusLg +#define B_8266_DM_TM1914_3 NeoPixelBusLg +#define B_8266_BB_TM1914_3 NeoPixelBusLg #endif /*** ESP32 Neopixel methods ***/ @@ -302,6 +311,16 @@ #define B_32_I1_2805_5 NeoPixelBusLg //#define B_32_I1_2805_5 NeoPixelBusLg // parallel I2S #endif +//TM1914 (RGB) +#define B_32_RN_TM1914_3 NeoPixelBusLg +#ifndef WLED_NO_I2S0_PIXELBUS +#define B_32_I0_TM1914_3 NeoPixelBusLg +//#define B_32_I0_TM1914_3 NeoPixelBusLg +#endif +#ifndef WLED_NO_I2S1_PIXELBUS +#define B_32_I1_TM1914_3 NeoPixelBusLg +//#define B_32_I1_TM1914_3 NeoPixelBusLg +#endif #endif //APA102 @@ -363,6 +382,13 @@ class PolyBus { tm1814_strip->SetPixelSettings(NeoTm1814Settings(/*R*/225, /*G*/225, /*B*/225, /*W*/225)); } + template + static void beginTM1914(void* busPtr) { + T tm1914_strip = static_cast(busPtr); + tm1914_strip->Begin(); + tm1914_strip->SetPixelSettings(NeoTm1914Settings()); //NeoTm1914_Mode_DinFdinAutoSwitch, NeoTm1914_Mode_DinOnly, NeoTm1914_Mode_FdinOnly + } + static void begin(void* busPtr, uint8_t busType, uint8_t* pins, uint16_t clock_kHz = 0U) { switch (busType) { case I_NONE: break; @@ -383,10 +409,10 @@ class PolyBus { case I_8266_U1_TM1_4: beginTM1814(busPtr); break; case I_8266_DM_TM1_4: beginTM1814(busPtr); break; case I_8266_BB_TM1_4: beginTM1814(busPtr); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->Begin(); break; + case I_8266_U0_TM2_3: (static_cast(busPtr))->Begin(); break; + case I_8266_U1_TM2_3: (static_cast(busPtr))->Begin(); break; + case I_8266_DM_TM2_3: (static_cast(busPtr))->Begin(); break; + case I_8266_BB_TM2_3: (static_cast(busPtr))->Begin(); break; case I_HS_DOT_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; case I_HS_LPD_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; case I_HS_LPO_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; @@ -412,6 +438,10 @@ class PolyBus { case I_8266_U1_2805_5: (static_cast(busPtr))->Begin(); break; case I_8266_DM_2805_5: (static_cast(busPtr))->Begin(); break; case I_8266_BB_2805_5: (static_cast(busPtr))->Begin(); break; + case I_8266_U0_TM1914_3: beginTM1914(busPtr); break; + case I_8266_U1_TM1914_3: beginTM1914(busPtr); break; + case I_8266_DM_TM1914_3: beginTM1914(busPtr); break; + case I_8266_BB_TM1914_3: beginTM1914(busPtr); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: (static_cast(busPtr))->Begin(); break; @@ -480,6 +510,13 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_2805_5: (static_cast(busPtr))->Begin(); break; #endif + case I_32_RN_TM1914_3: beginTM1914(busPtr); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_TM1914_3: beginTM1914(busPtr); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_TM1914_3: beginTM1914(busPtr); break; + #endif // ESP32 can (and should, to avoid inadvertantly driving the chip select signal) specify the pins used for SPI, but only in begin() case I_HS_DOT_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; case I_HS_LPD_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; @@ -516,10 +553,10 @@ class PolyBus { case I_8266_U1_TM1_4: busPtr = new B_8266_U1_TM1_4(len, pins[0]); break; case I_8266_DM_TM1_4: busPtr = new B_8266_DM_TM1_4(len, pins[0]); break; case I_8266_BB_TM1_4: busPtr = new B_8266_BB_TM1_4(len, pins[0]); break; - case I_8266_U0_TM2_3: busPtr = new B_8266_U0_TM2_4(len, pins[0]); break; - case I_8266_U1_TM2_3: busPtr = new B_8266_U1_TM2_4(len, pins[0]); break; - case I_8266_DM_TM2_3: busPtr = new B_8266_DM_TM2_4(len, pins[0]); break; - case I_8266_BB_TM2_3: busPtr = new B_8266_BB_TM2_4(len, pins[0]); break; + case I_8266_U0_TM2_3: busPtr = new B_8266_U0_TM2_3(len, pins[0]); break; + case I_8266_U1_TM2_3: busPtr = new B_8266_U1_TM2_3(len, pins[0]); break; + case I_8266_DM_TM2_3: busPtr = new B_8266_DM_TM2_3(len, pins[0]); break; + case I_8266_BB_TM2_3: busPtr = new B_8266_BB_TM2_3(len, pins[0]); break; case I_8266_U0_UCS_3: busPtr = new B_8266_U0_UCS_3(len, pins[0]); break; case I_8266_U1_UCS_3: busPtr = new B_8266_U1_UCS_3(len, pins[0]); break; case I_8266_DM_UCS_3: busPtr = new B_8266_DM_UCS_3(len, pins[0]); break; @@ -540,6 +577,10 @@ class PolyBus { case I_8266_U1_2805_5: busPtr = new B_8266_U1_2805_5(len, pins[0]); break; case I_8266_DM_2805_5: busPtr = new B_8266_DM_2805_5(len, pins[0]); break; case I_8266_BB_2805_5: busPtr = new B_8266_BB_2805_5(len, pins[0]); break; + case I_8266_U0_TM1914_3: busPtr = new B_8266_U0_TM1914_3(len, pins[0]); break; + case I_8266_U1_TM1914_3: busPtr = new B_8266_U1_TM1914_3(len, pins[0]); break; + case I_8266_DM_TM1914_3: busPtr = new B_8266_DM_TM1914_3(len, pins[0]); break; + case I_8266_BB_TM1914_3: busPtr = new B_8266_BB_TM1914_3(len, pins[0]); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)channel); break; @@ -608,6 +649,13 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_2805_5: busPtr = new B_32_I1_2805_5(len, pins[0]); break; #endif + case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)channel); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_TM1914_3: busPtr = new B_32_I0_TM1914_3(len, pins[0]); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_TM1914_3: busPtr = new B_32_I1_TM1914_3(len, pins[0]); break; + #endif #endif // for 2-wire: pins[1] is clk, pins[0] is dat. begin expects (len, clk, dat) case I_HS_DOT_3: busPtr = new B_HS_DOT_3(len, pins[1], pins[0]); break; @@ -645,10 +693,10 @@ class PolyBus { case I_8266_U1_TM1_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_TM1_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_TM2_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_TM2_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_TM2_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_TM2_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U0_UCS_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U1_UCS_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_UCS_3: (static_cast(busPtr))->Show(consistent); break; @@ -669,6 +717,10 @@ class PolyBus { case I_8266_U1_2805_5: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_2805_5: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_2805_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_TM1914_3: (static_cast(busPtr))->Show(consistent); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: (static_cast(busPtr))->Show(consistent); break; @@ -737,6 +789,13 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_2805_5: (static_cast(busPtr))->Show(consistent); break; #endif + case I_32_RN_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->Show(consistent); break; case I_SS_DOT_3: (static_cast(busPtr))->Show(consistent); break; @@ -771,10 +830,10 @@ class PolyBus { case I_8266_U1_TM1_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_TM1_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_TM2_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_TM2_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_TM2_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_TM2_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_TM2_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U0_UCS_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_UCS_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_UCS_3: return (static_cast(busPtr))->CanShow(); break; @@ -794,6 +853,10 @@ class PolyBus { case I_8266_U1_2805_5: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_2805_5: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_2805_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_TM1914_3: return (static_cast(busPtr))->CanShow(); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: return (static_cast(busPtr))->CanShow(); break; @@ -862,6 +925,13 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_2805_5: return (static_cast(busPtr))->CanShow(); break; #endif + case I_32_RN_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + #endif #endif case I_HS_DOT_3: return (static_cast(busPtr))->CanShow(); break; case I_SS_DOT_3: return (static_cast(busPtr))->CanShow(); break; @@ -921,10 +991,10 @@ class PolyBus { case I_8266_U1_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_DM_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_BB_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_U0_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_U1_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_DM_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_BB_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_U0_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; case I_8266_U1_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; case I_8266_DM_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; @@ -945,6 +1015,10 @@ class PolyBus { case I_8266_U1_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_8266_DM_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_8266_BB_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_U0_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_U1_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_DM_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_BB_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; @@ -1013,6 +1087,13 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; #endif + case I_32_RN_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_SS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; @@ -1047,10 +1128,10 @@ class PolyBus { case I_8266_U1_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_DM_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_BB_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_U0_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_U1_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_DM_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; @@ -1071,6 +1152,10 @@ class PolyBus { case I_8266_U1_2805_5: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_DM_2805_5: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_BB_2805_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; @@ -1139,6 +1224,13 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_2805_5: (static_cast(busPtr))->SetLuminance(b); break; #endif + case I_32_RN_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; case I_SS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; @@ -1174,10 +1266,10 @@ class PolyBus { case I_8266_U1_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_DM_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_BB_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U0_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U1_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_DM_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_BB_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; case I_8266_U1_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; case I_8266_DM_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; @@ -1198,6 +1290,10 @@ class PolyBus { case I_8266_U1_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_8266_DM_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_8266_BB_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_U0_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U1_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_DM_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_BB_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; @@ -1266,6 +1362,13 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W #endif + case I_32_RN_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + #endif #endif case I_HS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_SS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; @@ -1319,10 +1422,10 @@ class PolyBus { case I_8266_U1_TM1_4: delete (static_cast(busPtr)); break; case I_8266_DM_TM1_4: delete (static_cast(busPtr)); break; case I_8266_BB_TM1_4: delete (static_cast(busPtr)); break; - case I_8266_U0_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_U1_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_DM_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_BB_TM2_3: delete (static_cast(busPtr)); break; + case I_8266_U0_TM2_3: delete (static_cast(busPtr)); break; + case I_8266_U1_TM2_3: delete (static_cast(busPtr)); break; + case I_8266_DM_TM2_3: delete (static_cast(busPtr)); break; + case I_8266_BB_TM2_3: delete (static_cast(busPtr)); break; case I_8266_U0_UCS_3: delete (static_cast(busPtr)); break; case I_8266_U1_UCS_3: delete (static_cast(busPtr)); break; case I_8266_DM_UCS_3: delete (static_cast(busPtr)); break; @@ -1343,6 +1446,10 @@ class PolyBus { case I_8266_U1_2805_5: delete (static_cast(busPtr)); break; case I_8266_DM_2805_5: delete (static_cast(busPtr)); break; case I_8266_BB_2805_5: delete (static_cast(busPtr)); break; + case I_8266_U0_TM1914_3: delete (static_cast(busPtr)); break; + case I_8266_U1_TM1914_3: delete (static_cast(busPtr)); break; + case I_8266_DM_TM1914_3: delete (static_cast(busPtr)); break; + case I_8266_BB_TM1914_3: delete (static_cast(busPtr)); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: delete (static_cast(busPtr)); break; @@ -1411,6 +1518,13 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_2805_5: delete (static_cast(busPtr)); break; #endif + case I_32_RN_TM1914_3: delete (static_cast(busPtr)); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_TM1914_3: delete (static_cast(busPtr)); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_TM1914_3: delete (static_cast(busPtr)); break; + #endif #endif case I_HS_DOT_3: delete (static_cast(busPtr)); break; case I_SS_DOT_3: delete (static_cast(busPtr)); break; @@ -1476,6 +1590,8 @@ class PolyBus { return I_8266_U0_FW6_5 + offset; case TYPE_WS2805: return I_8266_U0_2805_5 + offset; + case TYPE_TM1914: + return I_8266_U0_TM1914_3 + offset; } #else //ESP32 uint8_t offset = 0; // 0 = RMT (num 1-8), 1 = I2S0 (used by Audioreactive), 2 = I2S1 @@ -1521,6 +1637,8 @@ class PolyBus { return I_32_RN_FW6_5 + offset; case TYPE_WS2805: return I_32_RN_2805_5 + offset; + case TYPE_TM1914: + return I_32_RN_TM1914_3 + offset; } #endif } diff --git a/wled00/const.h b/wled00/const.h index 0ce7b27d5..e0509824f 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -268,6 +268,7 @@ #define TYPE_SK6812_RGBW 30 #define TYPE_TM1814 31 #define TYPE_WS2805 32 //RGB + WW + CW +#define TYPE_TM1914 33 //RGB //"Analog" types (40-47) #define TYPE_ONOFF 40 //binary output (relays etc.; NOT PWM) #define TYPE_ANALOG_1CH 41 //single channel PWM. Uses value of brightest RGBW channel diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 4ad4cb16e..1ab3374a0 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -386,6 +386,7 @@ ${i+1}: \ \ \ +\ \ \ \ From 459156fe574b7516b9a6b00c522a5bec74bab131 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 15 Apr 2024 21:20:45 +0200 Subject: [PATCH 023/148] added improvements to color scaling and blurring -changes save roughly 600bytes of flash -made blurring faster by not writing the color and then reading it back but keeping it as a variable: on a C3, FX black hole goes from 55FPS to 71FPS -added optional parameter to blur (smear) that can be used in combination with SEGMENT.clear(), blurring the frame without dimming the current frame (repeated calls without clearing will result in white). this is useful to blur without 'motion blurring' being added -scale8 is inlined and repeated calls uses flash, plus it is slower than native 32bit, so I added 'color_scale' function which is native 32bit and scales 32bit colors (RGBW). --- wled00/FX.h | 12 +++---- wled00/FX_2Dfcn.cpp | 85 +++++++++++++++++++++++++------------------- wled00/FX_fcn.cpp | 39 +++++++++++--------- wled00/colors.cpp | 42 ++++++++++++++-------- wled00/fcn_declare.h | 1 + 5 files changed, 105 insertions(+), 74 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index 66e748602..106a6712c 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -582,7 +582,7 @@ typedef struct Segment { #endif uint32_t getPixelColor(int i); // 1D support functions (some implement 2D as well) - void blur(uint8_t); + void blur(uint8_t, bool smear = false); void fill(uint32_t c); void fade_out(uint8_t r); void fadeToBlackBy(uint8_t fadeBy); @@ -610,7 +610,7 @@ typedef struct Segment { inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColorXY(x, y, RGBW32(r,g,b,w), aa); } inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), aa); } #endif - uint32_t getPixelColorXY(uint16_t x, uint16_t y); + uint32_t getPixelColorXY(int x, int y); // 2D support functions inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) { setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); } inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), blend); } @@ -619,8 +619,8 @@ typedef struct Segment { inline void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), fast); } inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); } void box_blur(uint16_t i, bool vertical, fract8 blur_amount); // 1D box blur (with weight) - void blurRow(uint16_t row, fract8 blur_amount); - void blurCol(uint16_t col, fract8 blur_amount); + void blurRow(uint32_t row, fract8 blur_amount, bool smear = false); + void blurCol(uint32_t col, fract8 blur_amount, bool smear = false); void moveX(int8_t delta, bool wrap = false); void moveY(int8_t delta, bool wrap = false); void move(uint8_t dir, uint8_t delta, bool wrap = false); @@ -655,8 +655,8 @@ typedef struct Segment { inline void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), fast); } inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { fadePixelColor(x, fade); } inline void box_blur(uint16_t i, bool vertical, fract8 blur_amount) {} - inline void blurRow(uint16_t row, fract8 blur_amount) {} - inline void blurCol(uint16_t col, fract8 blur_amount) {} + inline void blurRow(uint32_t row, fract8 blur_amount, bool smear = false) {} + inline void blurCol(uint32_t col, fract8 blur_amount, bool smear = false) {} inline void moveX(int8_t delta, bool wrap = false) {} inline void moveY(int8_t delta, bool wrap = false) {} inline void move(uint8_t dir, uint8_t delta, bool wrap = false) {} diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index b049ab6f0..bea551860 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -175,11 +175,7 @@ void IRAM_ATTR Segment::setPixelColorXY(int x, int y, uint32_t col) uint8_t _bri_t = currentBri(); if (_bri_t < 255) { - byte r = scale8(R(col), _bri_t); - byte g = scale8(G(col), _bri_t); - byte b = scale8(B(col), _bri_t); - byte w = scale8(W(col), _bri_t); - col = RGBW32(r, g, b, w); + col = color_scale(col, _bri_t); } if (reverse ) x = virtualWidth() - x - 1; @@ -265,7 +261,7 @@ void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) #endif // returns RGBW values of pixel -uint32_t IRAM_ATTR Segment::getPixelColorXY(uint16_t x, uint16_t y) { +uint32_t IRAM_ATTR Segment::getPixelColorXY(int x, int y) { if (!isActive()) return 0; // not active if (x >= virtualWidth() || y >= virtualHeight() || x<0 || y<0) return 0; // if pixel would fall out of virtual segment just exit if (reverse ) x = virtualWidth() - x - 1; @@ -278,59 +274,74 @@ uint32_t IRAM_ATTR Segment::getPixelColorXY(uint16_t x, uint16_t y) { } // blurRow: perform a blur on a row of a rectangular matrix -void Segment::blurRow(uint16_t row, fract8 blur_amount) { - if (!isActive() || blur_amount == 0) return; // not active +void Segment::blurRow(uint32_t row, fract8 blur_amount, bool smear) +{ + if (!isActive() || blur_amount == 0) + return; // not active const uint_fast16_t cols = virtualWidth(); const uint_fast16_t rows = virtualHeight(); - if (row >= rows) return; + if (row >= rows) + return; // blur one row - uint8_t keep = 255 - blur_amount; + uint8_t keep = smear ? 255 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; - CRGB carryover = CRGB::Black; + uint32_t carryover = BLACK; + uint32_t lastnew; + uint32_t last; + uint32_t curnew; for (unsigned x = 0; x < cols; x++) { - CRGB cur = getPixelColorXY(x, row); - CRGB before = cur; // remember color before blur - CRGB part = cur; - part.nscale8(seep); - cur.nscale8(keep); - cur += carryover; - if (x>0) { - CRGB prev = CRGB(getPixelColorXY(x-1, row)) + part; - setPixelColorXY(x-1, row, prev); + uint32_t cur = getPixelColorXY(x, row); + uint32_t part = color_scale(cur, seep); + curnew = color_scale(cur, keep); + if (x > 0) { + if (carryover) + curnew = color_add(curnew, carryover, true); + uint32_t prev = color_add(lastnew, part, true); + if (last != prev) // optimization: only set pixel if color has changed + setPixelColorXY(x - 1, row, prev); } - if (before != cur) // optimization: only set pixel if color has changed - setPixelColorXY(x, row, cur); + else // first pixel or last pixel + setPixelColorXY(x, row, curnew); + lastnew = curnew; + last = cur; // save original value for comparison on next iteration carryover = part; } + setPixelColorXY(cols-1, row, curnew); // set last pixel } // blurCol: perform a blur on a column of a rectangular matrix -void Segment::blurCol(uint16_t col, fract8 blur_amount) { +void Segment::blurCol(uint32_t col, fract8 blur_amount, bool smear) { if (!isActive() || blur_amount == 0) return; // not active const uint_fast16_t cols = virtualWidth(); const uint_fast16_t rows = virtualHeight(); if (col >= cols) return; // blur one column - uint8_t keep = 255 - blur_amount; + uint8_t keep = smear ? 255 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; - CRGB carryover = CRGB::Black; + uint32_t carryover = BLACK; + uint32_t lastnew; + uint32_t last; + uint32_t curnew; for (unsigned y = 0; y < rows; y++) { - CRGB cur = getPixelColorXY(col, y); - CRGB part = cur; - CRGB before = cur; // remember color before blur - part.nscale8(seep); - cur.nscale8(keep); - cur += carryover; - if (y>0) { - CRGB prev = CRGB(getPixelColorXY(col, y-1)) + part; - setPixelColorXY(col, y-1, prev); + uint32_t cur = getPixelColorXY(col, y); + uint32_t part = color_scale(cur, seep); + curnew = color_scale(cur, keep); + if (y > 0) { + if (carryover) + curnew = color_add(curnew, carryover, true); + uint32_t prev = color_add(lastnew, part, true); + if (last != prev) // optimization: only set pixel if color has changed + setPixelColorXY(col, y - 1, prev); } - if (before != cur) // optimization: only set pixel if color has changed - setPixelColorXY(col, y, cur); - carryover = part; + else // first pixel + setPixelColorXY(col, y, curnew); + lastnew = curnew; + last = cur; //save original value for comparison on next iteration + carryover = part; } + setPixelColorXY(col, rows - 1, curnew); } // 1D Box blur (with added weight - blur_amount: [0=no blur, 255=max blur]) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 9ab1f578b..9a372ddf1 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -735,11 +735,7 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) uint16_t len = length(); uint8_t _bri_t = currentBri(); if (_bri_t < 255) { - byte r = scale8(R(col), _bri_t); - byte g = scale8(G(col), _bri_t); - byte b = scale8(B(col), _bri_t); - byte w = scale8(W(col), _bri_t); - col = RGBW32(r, g, b, w); + col = color_scale(col, _bri_t); } // expand pixel (taking into account start, grouping, spacing [and offset]) @@ -995,33 +991,44 @@ void Segment::fadeToBlackBy(uint8_t fadeBy) { /* * blurs segment content, source: FastLED colorutils.cpp */ -void Segment::blur(uint8_t blur_amount) { +void Segment::blur(uint8_t blur_amount, bool smear) { if (!isActive() || blur_amount == 0) return; // optimization: 0 means "don't blur" #ifndef WLED_DISABLE_2D if (is2D()) { // compatibility with 2D const unsigned cols = virtualWidth(); const unsigned rows = virtualHeight(); - for (unsigned i = 0; i < rows; i++) blurRow(i, blur_amount); // blur all rows - for (unsigned k = 0; k < cols; k++) blurCol(k, blur_amount); // blur all columns + for (unsigned i = 0; i < rows; i++) blurRow(i, blur_amount, smear); // blur all rows + for (unsigned k = 0; k < cols; k++) blurCol(k, blur_amount, smear); // blur all columns return; } #endif - uint8_t keep = 255 - blur_amount; + uint8_t keep = smear ? 250 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; - uint32_t carryover = BLACK; unsigned vlength = virtualLength(); + uint32_t carryover = BLACK; + uint32_t lastnew; + uint32_t last; + uint32_t curnew; for (unsigned i = 0; i < vlength; i++) { uint32_t cur = getPixelColor(i); - uint32_t part = color_fade(cur, seep); - cur = color_add(color_fade(cur, keep), carryover, true); - if (i > 0) { - uint32_t c = getPixelColor(i-1); - setPixelColor(i-1, color_add(c, part, true)); + uint32_t part = color_scale(cur, seep); + curnew = color_scale(cur, keep); + if (i > 0) + { + if (carryover) + curnew = color_add(curnew, carryover, true); + uint32_t prev = color_add(lastnew, part, true); + if (last != prev) // optimization: only set pixel if color has changed + setPixelColor(i - 1, prev); } - setPixelColor(i, cur); + else // first pixel + setPixelColor(i, curnew); + lastnew = curnew; + last = cur; // save original value for comparison on next iteration carryover = part; } + setPixelColor(vlength - 1, curnew); } /* diff --git a/wled00/colors.cpp b/wled00/colors.cpp index 3ed54d959..188c67f41 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -61,28 +61,40 @@ uint32_t color_add(uint32_t c1, uint32_t c2, bool fast) } } +/* + * color scale function that replaces scale8 for 32bit colors + */ + +uint32_t color_scale(uint32_t c1, uint8_t scale) +{ + uint32_t fixedscale = 1 + scale; + uint32_t scaledcolor; //color order is: W R G B from MSB to LSB + scaledcolor = ((R(c1) * fixedscale) >> 8) << 16; + scaledcolor |= ((G(c1) * fixedscale) >> 8) << 8; + scaledcolor |= (B(c1) * fixedscale) >> 8; + scaledcolor |= ((W(c1) * fixedscale) >> 8) << 24; + return scaledcolor; +} + /* * fades color toward black * if using "video" method the resulting color will never become black unless it is already black */ + uint32_t color_fade(uint32_t c1, uint8_t amount, bool video) { - uint8_t r = R(c1); - uint8_t g = G(c1); - uint8_t b = B(c1); - uint8_t w = W(c1); - if (video) { - r = scale8_video(r, amount); - g = scale8_video(g, amount); - b = scale8_video(b, amount); - w = scale8_video(w, amount); - } else { - r = scale8(r, amount); - g = scale8(g, amount); - b = scale8(b, amount); - w = scale8(w, amount); + if (video) + { + uint8_t r = scale8_video(R(c1), amount); + uint8_t g = scale8_video(G(c1), amount); + uint8_t b = scale8_video(B(c1), amount); + uint8_t w = scale8_video(W(c1), amount); + return RGBW32(r, g, b, w); + } + else + { + return color_scale(c1, amount); } - return RGBW32(r, g, b, w); } void setRandomColor(byte* rgb) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 2461ebb28..0e1329267 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -80,6 +80,7 @@ class NeoGammaWLEDMethod { #define gamma8(c) NeoGammaWLEDMethod::rawGamma8(c) uint32_t color_blend(uint32_t,uint32_t,uint16_t,bool b16=false); uint32_t color_add(uint32_t,uint32_t, bool fast=false); +uint32_t color_scale(uint32_t c1, uint8_t scale); uint32_t color_fade(uint32_t c1, uint8_t amount, bool video=false); CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette); CRGBPalette16 generateRandomPalette(void); From b2e68db380326804d545b7bc71233edf8c041b35 Mon Sep 17 00:00:00 2001 From: Woody Date: Mon, 15 Apr 2024 22:55:38 +0200 Subject: [PATCH 024/148] Increase operations-per-run in stale.yml again --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 58a0b18d8..5d62571e1 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -15,7 +15,7 @@ jobs: exempt-issue-labels: 'pinned,keep,enhancement,confirmed' exempt-pr-labels: 'pinned,keep,enhancement,confirmed' exempt-all-milestones: true - operations-per-run: 150 + operations-per-run: 1000 stale-issue-message: > Hey! This issue has been open for quite some time without any new comments now. It will be closed automatically in a week if no further activity occurs. From 084fc2fcd1b8a5c8ca4be7ab7e9dc04df8f92067 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 16 Apr 2024 10:43:06 +0200 Subject: [PATCH 025/148] bugfix & code formatting, removed color_scale also replaced scale8_video with 32bit calculation in color_fade for consistency and speed. --- wled00/FX_2Dfcn.cpp | 18 ++++++++---------- wled00/FX_fcn.cpp | 11 +++++------ wled00/colors.cpp | 44 +++++++++++++++++++------------------------- wled00/fcn_declare.h | 1 - 4 files changed, 32 insertions(+), 42 deletions(-) diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index bea551860..69a43d7b8 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -175,7 +175,7 @@ void IRAM_ATTR Segment::setPixelColorXY(int x, int y, uint32_t col) uint8_t _bri_t = currentBri(); if (_bri_t < 255) { - col = color_scale(col, _bri_t); + col = color_fade(col, _bri_t); } if (reverse ) x = virtualWidth() - x - 1; @@ -274,15 +274,13 @@ uint32_t IRAM_ATTR Segment::getPixelColorXY(int x, int y) { } // blurRow: perform a blur on a row of a rectangular matrix -void Segment::blurRow(uint32_t row, fract8 blur_amount, bool smear) -{ +void Segment::blurRow(uint32_t row, fract8 blur_amount, bool smear){ if (!isActive() || blur_amount == 0) return; // not active const uint_fast16_t cols = virtualWidth(); const uint_fast16_t rows = virtualHeight(); - if (row >= rows) - return; + if (row >= rows) return; // blur one row uint8_t keep = smear ? 255 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; @@ -292,8 +290,8 @@ void Segment::blurRow(uint32_t row, fract8 blur_amount, bool smear) uint32_t curnew; for (unsigned x = 0; x < cols; x++) { uint32_t cur = getPixelColorXY(x, row); - uint32_t part = color_scale(cur, seep); - curnew = color_scale(cur, keep); + uint32_t part = color_fade(cur, seep); + curnew = color_fade(cur, keep); if (x > 0) { if (carryover) curnew = color_add(curnew, carryover, true); @@ -301,7 +299,7 @@ void Segment::blurRow(uint32_t row, fract8 blur_amount, bool smear) if (last != prev) // optimization: only set pixel if color has changed setPixelColorXY(x - 1, row, prev); } - else // first pixel or last pixel + else // first pixel setPixelColorXY(x, row, curnew); lastnew = curnew; last = cur; // save original value for comparison on next iteration @@ -326,8 +324,8 @@ void Segment::blurCol(uint32_t col, fract8 blur_amount, bool smear) { uint32_t curnew; for (unsigned y = 0; y < rows; y++) { uint32_t cur = getPixelColorXY(col, y); - uint32_t part = color_scale(cur, seep); - curnew = color_scale(cur, keep); + uint32_t part = color_fade(cur, seep); + curnew = color_fade(cur, keep); if (y > 0) { if (carryover) curnew = color_add(curnew, carryover, true); diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 9a372ddf1..f3f74ba25 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -735,7 +735,7 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) uint16_t len = length(); uint8_t _bri_t = currentBri(); if (_bri_t < 255) { - col = color_scale(col, _bri_t); + col = color_fade(col, _bri_t); } // expand pixel (taking into account start, grouping, spacing [and offset]) @@ -1003,7 +1003,7 @@ void Segment::blur(uint8_t blur_amount, bool smear) { return; } #endif - uint8_t keep = smear ? 250 : 255 - blur_amount; + uint8_t keep = smear ? 255 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; unsigned vlength = virtualLength(); uint32_t carryover = BLACK; @@ -1012,10 +1012,9 @@ void Segment::blur(uint8_t blur_amount, bool smear) { uint32_t curnew; for (unsigned i = 0; i < vlength; i++) { uint32_t cur = getPixelColor(i); - uint32_t part = color_scale(cur, seep); - curnew = color_scale(cur, keep); - if (i > 0) - { + uint32_t part = color_fade(cur, seep); + curnew = color_fade(cur, keep); + if (i > 0) { if (carryover) curnew = color_add(curnew, carryover, true); uint32_t prev = color_add(lastnew, part, true); diff --git a/wled00/colors.cpp b/wled00/colors.cpp index 188c67f41..30d2c069e 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -61,21 +61,6 @@ uint32_t color_add(uint32_t c1, uint32_t c2, bool fast) } } -/* - * color scale function that replaces scale8 for 32bit colors - */ - -uint32_t color_scale(uint32_t c1, uint8_t scale) -{ - uint32_t fixedscale = 1 + scale; - uint32_t scaledcolor; //color order is: W R G B from MSB to LSB - scaledcolor = ((R(c1) * fixedscale) >> 8) << 16; - scaledcolor |= ((G(c1) * fixedscale) >> 8) << 8; - scaledcolor |= (B(c1) * fixedscale) >> 8; - scaledcolor |= ((W(c1) * fixedscale) >> 8) << 24; - return scaledcolor; -} - /* * fades color toward black * if using "video" method the resulting color will never become black unless it is already black @@ -83,17 +68,26 @@ uint32_t color_scale(uint32_t c1, uint8_t scale) uint32_t color_fade(uint32_t c1, uint8_t amount, bool video) { - if (video) - { - uint8_t r = scale8_video(R(c1), amount); - uint8_t g = scale8_video(G(c1), amount); - uint8_t b = scale8_video(B(c1), amount); - uint8_t w = scale8_video(W(c1), amount); - return RGBW32(r, g, b, w); + uint32_t scaledcolor; // color order is: W R G B from MSB to LSB + uint32_t r = R(c1); + uint32_t g = G(c1); + uint32_t b = B(c1); + uint32_t w = W(c1); + if (video) { + uint32_t scale = amount; // 32bit for faster calculation + scaledcolor = (((r * scale) >> 8) << 16) + ((r && scale) ? 1 : 0); + scaledcolor |= (((g * scale) >> 8) << 8) + ((g && scale) ? 1 : 0); + scaledcolor |= ((b * scale) >> 8) + ((b && scale) ? 1 : 0); + scaledcolor |= (((w * scale) >> 8) << 24) + ((w && scale) ? 1 : 0); + return scaledcolor; } - else - { - return color_scale(c1, amount); + else { + uint32_t scale = 1 + amount; + scaledcolor = ((r * scale) >> 8) << 16; + scaledcolor |= ((g * scale) >> 8) << 8; + scaledcolor |= (b * scale) >> 8; + scaledcolor |= ((w * scale) >> 8) << 24; + return scaledcolor; } } diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 0e1329267..2461ebb28 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -80,7 +80,6 @@ class NeoGammaWLEDMethod { #define gamma8(c) NeoGammaWLEDMethod::rawGamma8(c) uint32_t color_blend(uint32_t,uint32_t,uint16_t,bool b16=false); uint32_t color_add(uint32_t,uint32_t, bool fast=false); -uint32_t color_scale(uint32_t c1, uint8_t scale); uint32_t color_fade(uint32_t c1, uint8_t amount, bool video=false); CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette); CRGBPalette16 generateRandomPalette(void); From 6272969983c35e824622cf7d7515165eeff2e02f Mon Sep 17 00:00:00 2001 From: Woody Date: Tue, 16 Apr 2024 15:07:12 +0200 Subject: [PATCH 026/148] Set stale-pr-label & stale-issue-label to 'stale' in stale.yml Hopefully this fixes the error messages that were thrown the last time the action was executed --- .github/workflows/stale.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 5d62571e1..1f2557160 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,6 +12,8 @@ jobs: with: days-before-stale: 120 days-before-close: 7 + stale-issue-label: 'stale' + stale-pr-label: 'stale' exempt-issue-labels: 'pinned,keep,enhancement,confirmed' exempt-pr-labels: 'pinned,keep,enhancement,confirmed' exempt-all-milestones: true From 1b75be5710615f8964cf1456c41e8e0568f2a1e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Tue, 16 Apr 2024 17:37:48 +0200 Subject: [PATCH 027/148] Update FX_2Dfcn.cpp Undo indent --- wled00/FX_2Dfcn.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 69a43d7b8..5a73792b6 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -275,8 +275,7 @@ uint32_t IRAM_ATTR Segment::getPixelColorXY(int x, int y) { // blurRow: perform a blur on a row of a rectangular matrix void Segment::blurRow(uint32_t row, fract8 blur_amount, bool smear){ - if (!isActive() || blur_amount == 0) - return; // not active + if (!isActive() || blur_amount == 0) return; // not active const uint_fast16_t cols = virtualWidth(); const uint_fast16_t rows = virtualHeight(); From 3e20724058b4286de2b5e3a6ccd6a2578ec1c9a2 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Wed, 17 Apr 2024 18:52:35 +0200 Subject: [PATCH 028/148] ArduinoFFT update shadow variables --- platformio.ini | 5 ++--- usermods/multi_relay/usermod_multi_relay.h | 2 +- wled00/json.cpp | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/platformio.ini b/platformio.ini index e334b7e35..0b11f2d24 100644 --- a/platformio.ini +++ b/platformio.ini @@ -174,7 +174,7 @@ lib_deps = # SHT85 ;robtillaart/SHT85@~0.3.3 # Audioreactive usermod - ;kosme/arduinoFFT @ 2.0.0 + ;kosme/arduinoFFT @ 2.0.1 extra_scripts = ${scripts_defaults.extra_scripts} @@ -225,7 +225,7 @@ lib_deps = ${env.lib_deps} # additional build flags for audioreactive AR_build_flags = -D USERMOD_AUDIOREACTIVE -AR_lib_deps = kosme/arduinoFFT @ 2.0.0 +AR_lib_deps = kosme/arduinoFFT @ 2.0.1 [esp32_idf_V4] ;; experimental build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5 @@ -238,7 +238,6 @@ platform_packages = build_flags = -g -Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one -DARDUINO_ARCH_ESP32 -DESP32 - #-DCONFIG_LITTLEFS_FOR_IDF_3_2 -D CONFIG_ASYNC_TCP_USE_WDT=0 -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index cb1eec8e1..efb3c8ae1 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -667,7 +667,7 @@ void MultiRelay::addToJsonInfo(JsonObject &root) { for (int i=0; i()) { From 6d1410741d4c3f2d080b9ec12d7ffa7e0a2e4d8c Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Wed, 17 Apr 2024 19:00:16 +0200 Subject: [PATCH 029/148] Fix 8266 compile --- wled00/bus_wrapper.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 32a5c1aae..966a391c3 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -79,6 +79,11 @@ #define I_8266_U1_2805_5 90 #define I_8266_DM_2805_5 91 #define I_8266_BB_2805_5 92 +//TM1914 (RGB) +#define I_8266_U0_TM1914_3 99 +#define I_8266_U1_TM1914_3 100 +#define I_8266_DM_TM1914_3 101 +#define I_8266_BB_TM1914_3 102 /*** ESP32 Neopixel methods ***/ //RGB From 5c502b1fe48f8e12a51b0beabd56bca15e02a6a3 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Wed, 17 Apr 2024 22:49:33 +0200 Subject: [PATCH 030/148] Bump arduinoFFT, organise partitions --- platformio.ini | 22 ++++++++++++---------- tools/WLED_ESP32_4MB_700k_FS.csv | 6 ++++++ 2 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 tools/WLED_ESP32_4MB_700k_FS.csv diff --git a/platformio.ini b/platformio.ini index af0988a85..a5c21ba37 100644 --- a/platformio.ini +++ b/platformio.ini @@ -174,7 +174,7 @@ lib_deps = # SHT85 ;robtillaart/SHT85@~0.3.3 # Audioreactive usermod - ;kosme/arduinoFFT @ 2.0.0 + ;kosme/arduinoFFT @ 2.0.1 extra_scripts = ${scripts_defaults.extra_scripts} @@ -218,14 +218,18 @@ build_flags = -g #use LITTLEFS library by lorol in ESP32 core 1.x.x instead of built-in in 2.x.x -D LOROL_LITTLEFS ; -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 +tiny_partitions = tools/WLED_ESP32_2MB_noOTA.csv default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +extended_partitions = tools/WLED_ESP32_4MB_700k_FS.csv +large_partitions = tools/WLED_ESP32_8MB.csv +extreme_partitions = tools/WLED_ESP32_16MB_9MB_FS.csv lib_deps = https://github.com/lorol/LITTLEFS.git https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 ${env.lib_deps} # additional build flags for audioreactive AR_build_flags = -D USERMOD_AUDIOREACTIVE -AR_lib_deps = kosme/arduinoFFT @ 2.0.0 +AR_lib_deps = kosme/arduinoFFT @ 2.0.1 [esp32_idf_V4] ;; experimental build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5 @@ -250,7 +254,6 @@ lib_deps = ;; generic definitions for all ESP32-S2 boards platform = espressif32@ ~6.3.2 platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) -default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32S2 @@ -296,7 +299,6 @@ build_flags = -g -DCO ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: ;; ARDUINO_USB_MODE, ARDUINO_USB_CDC_ON_BOOT - lib_deps = https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 ${env.lib_deps} @@ -391,7 +393,7 @@ platform = ${esp32.platform} board = ttgo-t7-v14-mini32 board_build.f_flash = 80000000L board_build.flash_mode = qio -board_build.partitions = tools/WLED_ESP32-wrover_4MB.csv +board_build.partitions = ${esp32.extended_partitions} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_WROVER -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ;; Older ESP32 (rev.<3) need a PSRAM fix (increases static RAM used) https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html @@ -404,7 +406,7 @@ platform = ${esp32c3.platform} platform_packages = ${esp32c3.platform_packages} framework = arduino board = esp32-c3-devkitm-1 -board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +board_build.partitions = ${esp32.default_partitions} build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=ESP32-C3 -D WLED_WATCHDOG_TIMEOUT=0 -DLOLIN_WIFI_FIX ; seems to work much better with this @@ -428,7 +430,7 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME= ${esp32.AR_build_flags} lib_deps = ${esp32s3.lib_deps} ${esp32.AR_lib_deps} -board_build.partitions = tools/WLED_ESP32_8MB.csv +board_build.partitions = ${esp32.large_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio ; board_build.flash_mode = dio ;; try this if you have problems at startup @@ -450,7 +452,7 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME= ${esp32.AR_build_flags} lib_deps = ${esp32s3.lib_deps} ${esp32.AR_lib_deps} -board_build.partitions = tools/WLED_ESP32_8MB.csv +board_build.partitions = ${esp32.large_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder @@ -470,7 +472,7 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME= ${esp32.AR_build_flags} lib_deps = ${esp32s3.lib_deps} ${esp32.AR_lib_deps} -board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +board_build.partitions = ${esp32.default_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder @@ -479,7 +481,7 @@ monitor_filters = esp32_exception_decoder platform = ${esp32s2.platform} platform_packages = ${esp32s2.platform_packages} board = lolin_s2_mini -board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +board_build.partitions = ${esp32.default_partitions} ;board_build.flash_mode = qio ;board_build.f_flash = 80000000L build_unflags = ${common.build_unflags} diff --git a/tools/WLED_ESP32_4MB_700k_FS.csv b/tools/WLED_ESP32_4MB_700k_FS.csv new file mode 100644 index 000000000..39c88e543 --- /dev/null +++ b/tools/WLED_ESP32_4MB_700k_FS.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1A0000, +app1, app, ota_1, 0x1B0000,0x1A0000, +spiffs, data, spiffs, 0x350000,0xB0000, From 57665e896400a43f8cf2a71d975db418739c2c63 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:16:04 +0200 Subject: [PATCH 031/148] audioreactive readme - removed UM_AUDIOREACTIVE_USE_NEW_FFT The customized library is not needed / supported any more in 0_15. --- usermods/audioreactive/readme.md | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/usermods/audioreactive/readme.md b/usermods/audioreactive/readme.md index 47804b611..4668ca881 100644 --- a/usermods/audioreactive/readme.md +++ b/usermods/audioreactive/readme.md @@ -27,18 +27,11 @@ Currently ESP8266 is not supported, due to low speed and small RAM of this chip. There are however plans to create a lightweight audioreactive for the 8266, with reduced features. ## Installation -### using customised _arduinoFFT_ library for use with this usermod -Add `-D USERMOD_AUDIOREACTIVE` to your PlatformIO environment `build_flags`, as well as `https://github.com/blazoncek/arduinoFFT.git` to your `lib_deps`. -If you are not using PlatformIO (which you should) try adding `#define USERMOD_AUDIOREACTIVE` to *my_config.h* and make sure you have _arduinoFFT_ library downloaded and installed. +### using latest _arduinoFFT_ library version 2.x +The latest arduinoFFT release version should be used for audioreactive. -Customised _arduinoFFT_ library for use with this usermod can be found at https://github.com/blazoncek/arduinoFFT.git - -### using latest (develop) _arduinoFFT_ library -Alternatively, you can use the latest arduinoFFT development version. -ArduinoFFT `develop` library is slightly more accurate, and slightly faster than our customised library, however also needs additional 2kB RAM. - -* `build_flags` = `-D USERMOD_AUDIOREACTIVE` `-D UM_AUDIOREACTIVE_USE_NEW_FFT` -* `lib_deps`= `https://github.com/kosme/arduinoFFT#develop @ 1.9.2` +* `build_flags` = `-D USERMOD_AUDIOREACTIVE` +* `lib_deps`= `kosme/arduinoFFT @ 2.0.1` ## Configuration From 6f3d7e76c93c039bd2dad518bf923310b66ecac3 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Fri, 19 Apr 2024 22:54:25 +0200 Subject: [PATCH 032/148] Fix for #3919 --- wled00/data/index.css | 2 +- wled00/data/index.js | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/wled00/data/index.css b/wled00/data/index.css index fa6e20077..6ac3f3a1f 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -358,7 +358,7 @@ button { #putil, #segutil, #segutil2 { min-height: 42px; - margin: 13px auto 0; + margin: 0 auto; } #segutil .segin { diff --git a/wled00/data/index.js b/wled00/data/index.js index 03ee276a8..5950b24ee 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -863,14 +863,11 @@ function populateSegments(s) gId("segcont").classList.remove("hide"); let noNewSegs = (lowestUnused >= maxSeg); resetUtil(noNewSegs); - if (gId('selall')) gId('selall').checked = true; for (var i = 0; i <= lSeg; i++) { if (!gId(`seg${i}`)) continue; updateLen(i); updateTrail(gId(`seg${i}bri`)); gId(`segr${i}`).classList.add("hide"); - //if (i2) d.querySelectorAll(".pop").forEach((e)=>{e.classList.remove("hide");}); - var cd = gId('csl').querySelectorAll("button"); for (let e = cd.length-1; e >= 0; e--) { cd[e].dataset.r = i.col[e][0]; @@ -1838,7 +1833,7 @@ function makeSeg() }); var cn = `
`+ `
`+ - ``+ + ``+ ``+ ``+ ``+ @@ -1864,13 +1859,19 @@ function makeSeg() function resetUtil(off=false) { - gId('segutil').innerHTML = `
` + gId('segutil').innerHTML = `
` + '' + `
Add segment
` + '
' + `` + '
' + '
'; + gId('selall').checked = true; + for (var i = 0; i <= lSeg; i++) { + if (!gId(`seg${i}`)) continue; + if (!gId(`seg${i}sel`).checked) gId('selall').checked = false; // uncheck if at least one is unselected. + } + if (lSeg>2) d.querySelectorAll("#Segments .pop").forEach((e)=>{e.classList.remove("hide");}); } function makePlSel(el, incPl=false) From fa32faf75971f68836fa4c1f9e740edaca5a7be2 Mon Sep 17 00:00:00 2001 From: Suxsem Date: Sun, 21 Apr 2024 13:37:07 +0200 Subject: [PATCH 033/148] feat(new): relay open drain output --- platformio_override.sample.ini | 1 + wled00/button.cpp | 4 ++-- wled00/cfg.cpp | 7 ++++++- wled00/data/settings_leds.htm | 5 +++-- wled00/set.cpp | 1 + wled00/wled.h | 6 ++++++ wled00/xml.cpp | 1 + 7 files changed, 20 insertions(+), 5 deletions(-) diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index d7d41f3a6..406c6f23d 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -56,6 +56,7 @@ build_flags = ${common.build_flags_esp8266} ; -D IRPIN=4 ; -D RLYPIN=12 ; -D RLYMDE=1 +; -D RLYODRAIN=0 ; -D LED_BUILTIN=2 # GPIO of built-in LED ; ; Limit max buses diff --git a/wled00/button.cpp b/wled00/button.cpp index ce47a17ac..3b73df81d 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -379,7 +379,7 @@ void handleIO() esp32RMTInvertIdle(); #endif if (rlyPin>=0) { - pinMode(rlyPin, OUTPUT); + pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); digitalWrite(rlyPin, rlyMde); } offMode = false; @@ -400,7 +400,7 @@ void handleIO() esp32RMTInvertIdle(); #endif if (rlyPin>=0) { - pinMode(rlyPin, OUTPUT); + pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); digitalWrite(rlyPin, !rlyMde); } } diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 530777ab5..9e6989a75 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -335,12 +335,16 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(irApplyToAllSelected, hw["ir"]["sel"]); JsonObject relay = hw[F("relay")]; + + if (relay.containsKey("odrain")) { + rlyOpenDrain = relay["odrain"]; + } int hw_relay_pin = relay["pin"] | -2; if (hw_relay_pin > -2) { pinManager.deallocatePin(rlyPin, PinOwner::Relay); if (pinManager.allocatePin(hw_relay_pin,true, PinOwner::Relay)) { rlyPin = hw_relay_pin; - pinMode(rlyPin, OUTPUT); + pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); } else { rlyPin = -1; } @@ -868,6 +872,7 @@ void serializeConfig() { JsonObject hw_relay = hw.createNestedObject(F("relay")); hw_relay["pin"] = rlyPin; hw_relay["rev"] = !rlyMde; + hw_relay["odrain"] = rlyOpenDrain; hw[F("baud")] = serialBaud; diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 4ad4cb16e..eecdf66f0 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -619,7 +619,8 @@ Swap: Apply IR change to main segment only:
IR info
- Relay GPIO: Invert  ✕
+ Relay GPIO: Invert Open drain  ✕

Defaults

Turn LEDs on after power up/reset:
diff --git a/wled00/set.cpp b/wled00/set.cpp index a2e884c81..d3382be18 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -243,6 +243,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) rlyPin = -1; } rlyMde = (bool)request->hasArg(F("RM")); + rlyOpenDrain = (bool)request->hasArg(F("RO")); disablePullUp = (bool)request->hasArg(F("IP")); touchThreshold = request->arg(F("TT")).toInt(); diff --git a/wled00/wled.h b/wled00/wled.h index b94f7790b..d8385e79e 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -288,6 +288,12 @@ WLED_GLOBAL bool rlyMde _INIT(true); #else WLED_GLOBAL bool rlyMde _INIT(RLYMDE); #endif +//Use open drain (floating pin) when relay should be off +#ifndef RLYODRAIN +WLED_GLOBAL bool rlyOpenDrain _INIT(false); +#else +WLED_GLOBAL bool rlyOpenDrain _INIT(RLYODRAIN); +#endif #ifndef IRPIN #define IRPIN -1 #endif diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 3915d9b0e..49fd522bb 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -458,6 +458,7 @@ void getSettingsJS(byte subPage, char* dest) sappend('i',SET_F("PB"),strip.paletteBlend); sappend('v',SET_F("RL"),rlyPin); sappend('c',SET_F("RM"),rlyMde); + sappend('c',SET_F("RO"),rlyOpenDrain); for (uint8_t i=0; i Date: Sun, 21 Apr 2024 20:02:00 +0200 Subject: [PATCH 034/148] optimizations --- wled00/cfg.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 9e6989a75..22bfe577a 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -336,9 +336,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject relay = hw[F("relay")]; - if (relay.containsKey("odrain")) { - rlyOpenDrain = relay["odrain"]; - } + rlyOpenDrain = relay[F("odrain")] | rlyOpenDrain; int hw_relay_pin = relay["pin"] | -2; if (hw_relay_pin > -2) { pinManager.deallocatePin(rlyPin, PinOwner::Relay); @@ -872,7 +870,7 @@ void serializeConfig() { JsonObject hw_relay = hw.createNestedObject(F("relay")); hw_relay["pin"] = rlyPin; hw_relay["rev"] = !rlyMde; - hw_relay["odrain"] = rlyOpenDrain; + hw_relay[F("odrain")] = rlyOpenDrain; hw[F("baud")] = serialBaud; From 127ea7e3516231d4c12f1261d2132f65140401a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Tue, 23 Apr 2024 13:04:17 +0200 Subject: [PATCH 035/148] Fix for #3926 --- wled00/FX_fcn.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index f3f74ba25..f94daca4a 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1664,12 +1664,18 @@ bool WS2812FX::deserializeMap(uint8_t n) { return false; // if file does not load properly then exit } + JsonObject root = pDoc->as(); + // if we are loading default ledmap (at boot) set matrix width and height from the ledmap (compatible with WLED MM ledmaps) + if (isMatrix && n == 0 && (!root[F("width")].isNull() || !root[F("height")].isNull())) { + Segment::maxWidth = min(max(root[F("width")].as(), 1), 128); + Segment::maxHeight = min(max(root[F("height")].as(), 1), 128); + } + if (customMappingTable) delete[] customMappingTable; customMappingTable = new uint16_t[getLengthTotal()]; if (customMappingTable) { DEBUG_PRINT(F("Reading LED map from ")); DEBUG_PRINTLN(fileName); - JsonObject root = pDoc->as(); JsonArray map = root[F("map")]; if (!map.isNull() && map.size()) { // not an empty map customMappingSize = min((unsigned)map.size(), (unsigned)getLengthTotal()); From 8ffe1e65fd1bbb2f2e41aa93005e0c2d8487dcfe Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:07:08 +0200 Subject: [PATCH 036/148] audioreactive: arduino-esp32 up to 2.0.14 still has the swapped-channel-bug --- usermods/audioreactive/audio_source.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usermods/audioreactive/audio_source.h b/usermods/audioreactive/audio_source.h index 18d00da3c..a7337eaf9 100644 --- a/usermods/audioreactive/audio_source.h +++ b/usermods/audioreactive/audio_source.h @@ -71,7 +71,7 @@ * if you want to receive two channels, one is the actual data from microphone and another channel is suppose to receive 0, it's different data in two channels, you need to choose I2S_CHANNEL_FMT_RIGHT_LEFT in this case. */ -#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 4)) +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 6)) // espressif bug: only_left has no sound, left and right are swapped // https://github.com/espressif/esp-idf/issues/9635 I2S mic not working since 4.4 (IDFGH-8138) // https://github.com/espressif/esp-idf/issues/8538 I2S channel selection issue? (IDFGH-6918) @@ -770,4 +770,4 @@ class SPH0654 : public I2SSource { #endif } }; -#endif \ No newline at end of file +#endif From 0d3ea848c22d47d441942b9d76e477ec706272a0 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:27:44 +0200 Subject: [PATCH 037/148] "big partition" added - 300KB FS and coredump support(from WLEDMM) --- platformio.ini | 1 + tools/WLED_ESP32_4MB_256KB_FS.csv | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 tools/WLED_ESP32_4MB_256KB_FS.csv diff --git a/platformio.ini b/platformio.ini index a5c21ba37..8a40852a7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -221,6 +221,7 @@ build_flags = -g tiny_partitions = tools/WLED_ESP32_2MB_noOTA.csv default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv extended_partitions = tools/WLED_ESP32_4MB_700k_FS.csv +big_partitions = tools/WLED_ESP32_4MB_256KB_FS.csv ;; 1.8MB firmware, 256KB filesystem, coredump support large_partitions = tools/WLED_ESP32_8MB.csv extreme_partitions = tools/WLED_ESP32_16MB_9MB_FS.csv lib_deps = diff --git a/tools/WLED_ESP32_4MB_256KB_FS.csv b/tools/WLED_ESP32_4MB_256KB_FS.csv new file mode 100644 index 000000000..e54c22bbd --- /dev/null +++ b/tools/WLED_ESP32_4MB_256KB_FS.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1D0000, +app1, app, ota_1, 0x1E0000,0x1D0000, +spiffs, data, spiffs, 0x3B0000,0x40000, +coredump, data, coredump,,64K \ No newline at end of file From 11549058184a451a70e80c421eb40532804fbd79 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:49:39 +0200 Subject: [PATCH 038/148] upgrading WROVER to use new platform --- platformio.ini | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/platformio.ini b/platformio.ini index 8a40852a7..0dc03d630 100644 --- a/platformio.ini +++ b/platformio.ini @@ -390,16 +390,20 @@ lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.default_partitions} [env:esp32_wrover] -platform = ${esp32.platform} +extends = esp32_idf_V4 +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} board = ttgo-t7-v14-mini32 board_build.f_flash = 80000000L board_build.flash_mode = qio board_build.partitions = ${esp32.extended_partitions} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_WROVER +build_flags = ${common.build_flags_esp32_V4} -D WLED_RELEASE_NAME=ESP32_WROVER -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ;; Older ESP32 (rev.<3) need a PSRAM fix (increases static RAM used) https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html -D LEDPIN=25 -lib_deps = ${esp32.lib_deps} + ; ${esp32.AR_build_flags} +lib_deps = ${esp32_idf_V4.lib_deps} + ; ${esp32.AR_lib_deps} [env:esp32c3dev] extends = esp32c3 From 5d271d21bcfc34e02a7103980eb05e08081d94dc Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Tue, 23 Apr 2024 18:55:36 +0200 Subject: [PATCH 039/148] INI cleanup added 8M ESP32 and 16M ESP32-S3 --- platformio.ini | 74 +++++++++++++++++++--------------- platformio_override.sample.ini | 67 +++++++++++++++--------------- 2 files changed, 76 insertions(+), 65 deletions(-) diff --git a/platformio.ini b/platformio.ini index 14b5f7c1f..8d9109c02 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,7 +10,7 @@ # ------------------------------------------------------------------------------ # CI/release binaries -default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, esp32dev, esp32_eth, esp32dev_audioreactive, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB, esp32s3dev_8MB_PSRAM_opi, esp32s3_4M_PSRAM_qspi, esp32_wrover +default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, esp32dev, esp32_eth, esp32dev_audioreactive, lolin_s2_mini, esp32c3dev, esp32s3dev_16MB_opi, esp32s3dev_8MB_opi, esp32s3_4M_qspi, esp32_wrover src_dir = ./wled00 data_dir = ./wled00/data @@ -86,7 +86,6 @@ debug_flags = -D DEBUG=1 -D WLED_DEBUG # This reduces the OTA size with ~45KB, so it's especially useful on low memory boards (512k/1m). # ------------------------------------------------------------------------------ build_flags = - -Wno-attributes -DMQTT_MAX_PACKET_SIZE=1024 -DSECURE_CLIENT=SECURE_CLIENT_BEARSSL -DBEARSSL_SSL_BASIC @@ -104,10 +103,6 @@ build_flags = build_unflags = -build_flags_esp8266 = ${common.build_flags} ${esp8266.build_flags} -build_flags_esp32 = ${common.build_flags} ${esp32.build_flags} -build_flags_esp32_V4= ${common.build_flags} ${esp32_idf_V4.build_flags} - ldscript_1m128k = eagle.flash.1m128.ld ldscript_2m512k = eagle.flash.2m512.ld ldscript_2m1m = eagle.flash.2m1m.ld @@ -245,7 +240,6 @@ build_flags = -g -DARDUINO_ARCH_ESP32 -DESP32 -D CONFIG_ASYNC_TCP_USE_WDT=0 -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 -default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv lib_deps = https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 ${env.lib_deps} @@ -314,14 +308,14 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 #-DWLED_DISABLE_2D +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP8266 #-DWLED_DISABLE_2D lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder [env:nodemcuv2_160] extends = env:nodemcuv2 board_build.f_cpu = 160000000L -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266_160 #-DWLED_DISABLE_2D +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP8266_160 #-DWLED_DISABLE_2D [env:esp8266_2m] board = esp_wroom_02 @@ -329,13 +323,13 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP02 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP02 lib_deps = ${esp8266.lib_deps} [env:esp8266_2m_160] extends = env:esp8266_2m board_build.f_cpu = 160000000L -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP02_160 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP02_160 [env:esp01_1m_full] board = esp01_1m @@ -343,14 +337,14 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 7064 bytes FLASH and 975 bytes RAM lib_deps = ${esp8266.lib_deps} [env:esp01_1m_full_160] extends = env:esp01_1m_full board_build.f_cpu = 160000000L -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01_160 -D WLED_DISABLE_OTA +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP01_160 -D WLED_DISABLE_OTA ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 7064 bytes FLASH and 975 bytes RAM [env:esp32dev] @@ -358,17 +352,30 @@ board = esp32dev platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 #-D WLED_DISABLE_BROWNOUT_DET +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32 #-D WLED_DISABLE_BROWNOUT_DET lib_deps = ${esp32.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} +[env:esp32dev_8M] +board = esp32dev +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=ESP32_8M #-D WLED_DISABLE_BROWNOUT_DET + ${esp32.AR_build_flags} +lib_deps = ${esp32_idf_V4.lib_deps} + ${esp32.AR_lib_deps} +monitor_filters = esp32_exception_decoder +board_build.partitions = ${esp32.large_partitions} +; board_build.f_flash = 80000000L + [env:esp32dev_audioreactive] board = esp32dev platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_audioreactive #-D WLED_DISABLE_BROWNOUT_DET +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32_audioreactive #-D WLED_DISABLE_BROWNOUT_DET ${esp32.AR_build_flags} lib_deps = ${esp32.lib_deps} ${esp32.AR_lib_deps} @@ -383,7 +390,7 @@ platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} upload_speed = 921600 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32_Ethernet -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.default_partitions} @@ -395,9 +402,9 @@ platform_packages = ${esp32_idf_V4.platform_packages} board = ttgo-t7-v14-mini32 board_build.f_flash = 80000000L board_build.flash_mode = qio -board_build.partitions = ${esp32.extended_partitions} +board_build.partitions = ${esp32.default_partitions} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32_V4} -D WLED_RELEASE_NAME=ESP32_WROVER +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=ESP32_WROVER -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ;; Older ESP32 (rev.<3) need a PSRAM fix (increases static RAM used) https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html -D LEDPIN=25 ; ${esp32.AR_build_flags} @@ -420,27 +427,28 @@ upload_speed = 460800 build_unflags = ${common.build_unflags} lib_deps = ${esp32c3.lib_deps} -[env:esp32s3dev_8MB] -;; ESP32-S3-DevKitC-1 development board, with 8MB FLASH, no PSRAM (flash_mode: qio) -board = esp32-s3-devkitc-1 +[env:esp32s3dev_16MB_opi] +;; ESP32-S3 development board, with 16MB FLASH and >= 8MB PSRAM (memory_type: qio_opi) +board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support +board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} -upload_speed = 921600 ; or 460800 +upload_speed = 921600 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_16MB_opi -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 - -D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip - ;-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip + -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -DBOARD_HAS_PSRAM ${esp32.AR_build_flags} lib_deps = ${esp32s3.lib_deps} ${esp32.AR_lib_deps} -board_build.partitions = ${esp32.large_partitions} +board_build.partitions = ${esp32.extreme_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio -; board_build.flash_mode = dio ;; try this if you have problems at startup monitor_filters = esp32_exception_decoder -[env:esp32s3dev_8MB_PSRAM_opi] +[env:esp32s3dev_8MB_opi] ;; ESP32-S3 development board, with 8MB FLASH and >= 8MB PSRAM (memory_type: qio_opi) board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB @@ -448,7 +456,7 @@ platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB_PSRAM_opi +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB_opi -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") @@ -461,7 +469,7 @@ board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder -[env:esp32s3_4M_PSRAM_qspi] +[env:esp32s3_4M_qspi] ;; ESP32-S3, with 4MB FLASH and <= 4MB PSRAM (memory_type: qio_qspi) board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support board_build.arduino.memory_type = qio_qspi ;; use with PSRAM: 2MB or 4MB @@ -469,7 +477,7 @@ platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_4M_PSRAM_qspi +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_4M_qspi -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM -D WLED_WATCHDOG_TIMEOUT=0 @@ -486,8 +494,8 @@ platform = ${esp32s2.platform} platform_packages = ${esp32s2.platform_packages} board = lolin_s2_mini board_build.partitions = ${esp32.default_partitions} -;board_build.flash_mode = qio -;board_build.f_flash = 80000000L +board_build.flash_mode = qio +board_build.f_flash = 80000000L build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=ESP32-S2 -DARDUINO_USB_CDC_ON_BOOT=1 diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index d7d41f3a6..e7bacb9bd 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -28,12 +28,15 @@ lib_deps = ${esp8266.lib_deps} ; robtillaart/SHT85@~0.3.3 ; gmag11/QuickESPNow ;@ 0.6.2 ; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library -; https://github.com/kosme/arduinoFFT#develop @ 1.9.2+sha.419d7b0 ;; used for USERMOD_AUDIOREACTIVE - using "known working" hash -; build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +; https://github.com/kosme/arduinoFFT#develop @ 2.0.1 ;; used for USERMOD_AUDIOREACTIVE +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp8266.build_flags} ; ; *** To use the below defines/overrides, copy and paste each onto it's own line just below build_flags in the section above. ; +; Set a release name that may be used to distinguish required binary for flashing +; -D WLED_RELEASE_NAME=ESP32_MULTI_USREMODS +; ; disable specific features ; -D WLED_DISABLE_OTA ; -D WLED_DISABLE_ALEXA @@ -179,7 +182,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:d1_mini] @@ -189,7 +192,7 @@ platform_packages = ${common.platform_packages} upload_speed = 921600 board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder @@ -199,7 +202,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:h803wf] @@ -208,7 +211,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D LEDPIN=1 -D WLED_DISABLE_INFRARED +build_flags = ${common.build_flags} ${esp8266.build_flags} -D LEDPIN=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:esp32dev_qio80] @@ -216,7 +219,7 @@ board = esp32dev platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_qio80 #-D WLED_DISABLE_BROWNOUT_DET +build_flags = ${common.build_flags} ${esp32.build_flags} #-D WLED_DISABLE_BROWNOUT_DET lib_deps = ${esp32.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} @@ -231,7 +234,7 @@ board = esp32dev platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=ESP32_V4_qio80 #-D WLED_DISABLE_BROWNOUT_DET +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} #-D WLED_DISABLE_BROWNOUT_DET lib_deps = ${esp32_idf_V4.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32_idf_V4.default_partitions} @@ -240,14 +243,14 @@ board_build.flash_mode = dio [env:esp32s2_saola] board = esp32-s2-saola-1 -platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.2/platform-tasmota-espressif32-2.0.2.zip -platform_packages = +platform = ${esp32s2.platform} +platform_packages = ${esp32s2.platform_packages} framework = arduino board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv board_build.flash_mode = qio upload_speed = 460800 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32s2.build_flags} #-D WLED_RELEASE_NAME=S2_saola +build_flags = ${common.build_flags} ${esp32s2.build_flags} ;-DLOLIN_WIFI_FIX ;; try this in case Wifi does not work -DARDUINO_USB_CDC_ON_BOOT=1 lib_deps = ${esp32s2.lib_deps} @@ -265,7 +268,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA lib_deps = ${esp8266.lib_deps} [env:esp8285_H801] @@ -274,7 +277,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA lib_deps = ${esp8266.lib_deps} [env:d1_mini_5CH_Shojo_PCB] @@ -283,7 +286,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_USE_SHOJO_PCB +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_USE_SHOJO_PCB lib_deps = ${esp8266.lib_deps} [env:d1_mini_debug] @@ -293,7 +296,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} ${common.debug_flags} +build_flags = ${common.build_flags} ${esp8266.build_flags} ${common.debug_flags} lib_deps = ${esp8266.lib_deps} [env:d1_mini_ota] @@ -305,7 +308,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:anavi_miracle_controller] @@ -314,7 +317,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D LEDPIN=12 -D IRPIN=-1 -D RLYPIN=2 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D LEDPIN=12 -D IRPIN=-1 -D RLYPIN=2 lib_deps = ${esp8266.lib_deps} [env:esp32c3dev_2MB] @@ -324,7 +327,7 @@ extends = esp32c3 platform = ${esp32c3.platform} platform_packages = ${esp32c3.platform_packages} board = esp32-c3-devkitm-1 -build_flags = ${common.build_flags} ${esp32c3.build_flags} #-D WLED_RELEASE_NAME=ESP32-C3 +build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_WATCHDOG_TIMEOUT=0 -D WLED_DISABLE_OTA ; -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB @@ -341,7 +344,7 @@ platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} upload_speed = 460800 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} +build_flags = ${common.build_flags} ${esp32.build_flags} -D LEDPIN=16 -D RLYPIN=19 -D BTNPIN=17 @@ -361,7 +364,7 @@ board_build.partitions = ${esp32.default_partitions} [env:m5atom] board = esp32dev build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D LEDPIN=27 -D BTNPIN=39 +build_flags = ${common.build_flags} ${esp32.build_flags} -D LEDPIN=27 -D BTNPIN=39 lib_deps = ${esp32.lib_deps} platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} @@ -371,14 +374,14 @@ board_build.partitions = ${esp32.default_partitions} board = esp_wroom_02 platform = ${common.platform_wled_default} board_build.ldscript = ${common.ldscript_2m512k} -build_flags = ${common.build_flags_esp8266} -D LEDPIN=3 -D BTNPIN=1 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D LEDPIN=3 -D BTNPIN=1 lib_deps = ${esp8266.lib_deps} [env:sp511e] board = esp_wroom_02 platform = ${common.platform_wled_default} board_build.ldscript = ${common.ldscript_2m512k} -build_flags = ${common.build_flags_esp8266} -D LEDPIN=3 -D BTNPIN=2 -D IRPIN=5 -D WLED_MAX_BUTTONS=3 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D LEDPIN=3 -D BTNPIN=2 -D IRPIN=5 -D WLED_MAX_BUTTONS=3 lib_deps = ${esp8266.lib_deps} [env:Athom_RGBCW] ;7w and 5w(GU10) bulbs @@ -387,7 +390,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,13,5 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,13,5 -D DEFAULT_LED_TYPE=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 lib_deps = ${esp8266.lib_deps} @@ -397,7 +400,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,5,13 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,5,13 -D DEFAULT_LED_TYPE=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 -D WLED_USE_IC_CCT lib_deps = ${esp8266.lib_deps} @@ -407,7 +410,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=0 -D RLYPIN=-1 -D LEDPIN=1 -D WLED_DISABLE_INFRARED +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=-1 -D LEDPIN=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:Athom_4Pin_Controller] ; With clock and data interface @@ -416,7 +419,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=0 -D RLYPIN=12 -D LEDPIN=1 -D WLED_DISABLE_INFRARED +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=12 -D LEDPIN=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:Athom_5Pin_Controller] ;Analog light strip controller @@ -425,7 +428,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=0 -D RLYPIN=-1 DATA_PINS=4,12,14,13 -D WLED_DISABLE_INFRARED +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=-1 DATA_PINS=4,12,14,13 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:MY9291] @@ -434,7 +437,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA -D USERMOD_MY9291 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D USERMOD_MY9291 lib_deps = ${esp8266.lib_deps} # ------------------------------------------------------------------------------ @@ -448,7 +451,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:codm-controller-0_6-rev2] @@ -457,7 +460,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} # ------------------------------------------------------------------------------ @@ -468,7 +471,7 @@ board = esp32dev platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} upload_speed = 921600 -build_flags = ${common.build_flags_esp32} -D WLED_DISABLE_BROWNOUT_DET -D WLED_DISABLE_INFRARED +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_DISABLE_BROWNOUT_DET -D WLED_DISABLE_INFRARED -D USERMOD_RTC -D USERMOD_ELEKSTUBE_IPS -D LEDPIN=12 From 674481f0d1d74c7e83de006a752e71cef48da451 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Tue, 23 Apr 2024 19:05:49 +0200 Subject: [PATCH 040/148] Multiple fixes - several compile warning fixes - multipin LED compile config - release info (update page, JSON "info") - WiFi scan fix if no networks found - UI glitch when no presets are found fix With multipin LED config it is now possible to assign GPIO to PWM RGB outputs. Achieved by having length of DATA_PINS be divisble by lengt of PIXEL_COUNTS. --- .../usermod_v2_four_line_display_ALT.h | 10 ++-- wled00/FX_2Dfcn.cpp | 10 ++-- wled00/FX_fcn.cpp | 23 +++---- wled00/data/index.js | 6 +- wled00/data/settings_wifi.htm | 60 ++++++++++--------- wled00/json.cpp | 1 + wled00/wled.h | 25 ++++---- wled00/xml.cpp | 2 + 8 files changed, 75 insertions(+), 62 deletions(-) diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h index 24eb9794f..2cb1507ce 100644 --- a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h @@ -1135,10 +1135,12 @@ bool FourLineDisplayUsermod::handleButton(uint8_t b) { return handled; } -#if CONFIG_FREERTOS_UNICORE -#define ARDUINO_RUNNING_CORE 0 -#else -#define ARDUINO_RUNNING_CORE 1 +#ifndef ARDUINO_RUNNING_CORE + #if CONFIG_FREERTOS_UNICORE + #define ARDUINO_RUNNING_CORE 0 + #else + #define ARDUINO_RUNNING_CORE 1 + #endif #endif void FourLineDisplayUsermod::onUpdateBegin(bool init) { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 5a73792b6..e4007ed7e 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -286,7 +286,7 @@ void Segment::blurRow(uint32_t row, fract8 blur_amount, bool smear){ uint32_t carryover = BLACK; uint32_t lastnew; uint32_t last; - uint32_t curnew; + uint32_t curnew = BLACK; for (unsigned x = 0; x < cols; x++) { uint32_t cur = getPixelColorXY(x, row); uint32_t part = color_fade(cur, seep); @@ -297,8 +297,7 @@ void Segment::blurRow(uint32_t row, fract8 blur_amount, bool smear){ uint32_t prev = color_add(lastnew, part, true); if (last != prev) // optimization: only set pixel if color has changed setPixelColorXY(x - 1, row, prev); - } - else // first pixel + } else // first pixel setPixelColorXY(x, row, curnew); lastnew = curnew; last = cur; // save original value for comparison on next iteration @@ -320,7 +319,7 @@ void Segment::blurCol(uint32_t col, fract8 blur_amount, bool smear) { uint32_t carryover = BLACK; uint32_t lastnew; uint32_t last; - uint32_t curnew; + uint32_t curnew = BLACK; for (unsigned y = 0; y < rows; y++) { uint32_t cur = getPixelColorXY(col, y); uint32_t part = color_fade(cur, seep); @@ -331,8 +330,7 @@ void Segment::blurCol(uint32_t col, fract8 blur_amount, bool smear) { uint32_t prev = color_add(lastnew, part, true); if (last != prev) // optimization: only set pixel if color has changed setPixelColorXY(col, y - 1, prev); - } - else // first pixel + } else // first pixel setPixelColorXY(col, y, curnew); lastnew = curnew; last = cur; //save original value for comparison on next iteration diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index f94daca4a..5d031e8ce 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1009,7 +1009,7 @@ void Segment::blur(uint8_t blur_amount, bool smear) { uint32_t carryover = BLACK; uint32_t lastnew; uint32_t last; - uint32_t curnew; + uint32_t curnew = BLACK; for (unsigned i = 0; i < vlength; i++) { uint32_t cur = getPixelColor(i); uint32_t part = color_fade(cur, seep); @@ -1099,21 +1099,24 @@ void WS2812FX::finalizeInit(void) { //if busses failed to load, add default (fresh install, FS issue, ...) if (BusManager::getNumBusses() == 0) { DEBUG_PRINTLN(F("No busses, init default")); - const uint8_t defDataPins[] = {DATA_PINS}; - const uint16_t defCounts[] = {PIXEL_COUNTS}; - const uint8_t defNumBusses = ((sizeof defDataPins) / (sizeof defDataPins[0])); - const uint8_t defNumCounts = ((sizeof defCounts) / (sizeof defCounts[0])); - uint16_t prevLen = 0; - for (int i = 0; i < defNumBusses && i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { - uint8_t defPin[] = {defDataPins[i]}; + const unsigned defDataPins[] = {DATA_PINS}; + const unsigned defCounts[] = {PIXEL_COUNTS}; + const unsigned defNumPins = ((sizeof defDataPins) / (sizeof defDataPins[0])); + const unsigned defNumCounts = ((sizeof defCounts) / (sizeof defCounts[0])); + const unsigned defNumBusses = defNumPins > defNumCounts && defNumCounts > 1 && defNumPins%defNumCounts == 0 ? defNumCounts : defNumPins; + const unsigned pinsPerBus = defNumPins / defNumBusses; + unsigned prevLen = 0; + for (unsigned i = 0; i < defNumBusses && i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { + uint8_t defPin[5]; // max 5 pins + for (unsigned j = 0; j < pinsPerBus; j++) defPin[j] = defDataPins[i*pinsPerBus + j]; // when booting without config (1st boot) we need to make sure GPIOs defined for LED output don't clash with hardware // i.e. DEBUG (GPIO1), DMX (2), SPI RAM/FLASH (16&17 on ESP32-WROVER/PICO), etc if (pinManager.isPinAllocated(defPin[0])) { defPin[0] = 1; // start with GPIO1 and work upwards while (pinManager.isPinAllocated(defPin[0]) && defPin[0] < WLED_NUM_PINS) defPin[0]++; } - uint16_t start = prevLen; - uint16_t count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; + unsigned start = prevLen; + unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; prevLen += count; BusConfig defCfg = BusConfig(DEFAULT_LED_TYPE, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY); if (BusManager::add(defCfg) == -1) break; diff --git a/wled00/data/index.js b/wled00/data/index.js index 5950b24ee..bbf6bd109 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -430,7 +430,7 @@ function presetError(empty) if (bckstr.length > 10) hasBackup = true; } catch (e) {} - var cn = `
`; + var cn = `
`; if (empty) cn += `You have no presets yet!`; else @@ -442,8 +442,8 @@ function presetError(empty) cn += `However, there is backup preset data of a previous installation available.
(Saving a preset will hide this and overwrite the backup)`; else cn += `Here is a backup of the last known good state:`; - cn += `
`; - cn += `
`; + cn += `
`; + cn += `
`; } cn += `
`; gId('pcont').innerHTML = cn; diff --git a/wled00/data/settings_wifi.htm b/wled00/data/settings_wifi.htm index 3577e80d2..3c15d5a86 100644 --- a/wled00/data/settings_wifi.htm +++ b/wled00/data/settings_wifi.htm @@ -52,40 +52,42 @@ } scanLoops = 0; - let cs = d.querySelectorAll("#wifi_entries input[type=text]"); - for (let input of (cs||[])) { - let found = false; - let select = cE("select"); - select.id = input.id; - select.name = input.name; - select.setAttribute("onchange", "T(this)"); - preScanSSID = input.value; + if (networks.length > 0) { + let cs = d.querySelectorAll("#wifi_entries input[type=text]"); + for (let input of (cs||[])) { + let found = false; + let select = cE("select"); + select.id = input.id; + select.name = input.name; + select.setAttribute("onchange", "T(this)"); + preScanSSID = input.value; - for (let i = 0; i < select.children.length; i++) { - select.removeChild(select.children[i]); - } - - for (let i = 0; i < networks.length; i++) { - const option = cE("option"); - - option.setAttribute("value", networks[i].ssid); - option.textContent = `${networks[i].ssid} (${networks[i].rssi} dBm)`; - - if (networks[i].ssid === input.value) { - option.setAttribute("selected", "selected"); - found = true; + for (let i = 0; i < select.children.length; i++) { + select.removeChild(select.children[i]); } + for (let i = 0; i < networks.length; i++) { + const option = cE("option"); + + option.setAttribute("value", networks[i].ssid); + option.textContent = `${networks[i].ssid} (${networks[i].rssi} dBm)`; + + if (networks[i].ssid === input.value) { + option.setAttribute("selected", "selected"); + found = true; + } + + select.appendChild(option); + } + const option = cE("option"); + + option.setAttribute("value", "!Cs"); + option.textContent = "Other network..."; select.appendChild(option); + + if (input.value === "" || input.value === "Your_Network" || found) input.replaceWith(select); + else select.remove(); } - const option = cE("option"); - - option.setAttribute("value", "!Cs"); - option.textContent = "Other network..."; - select.appendChild(option); - - if (input.value === "" || input.value === "Your_Network" || found) input.replaceWith(select); - else select.remove(); } button.disabled = false; diff --git a/wled00/json.cpp b/wled00/json.cpp index 68973750e..ae8224ad3 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -636,6 +636,7 @@ void serializeInfo(JsonObject root) root[F("ver")] = versionString; root[F("vid")] = VERSION; root[F("cn")] = F(WLED_CODENAME); + root[F("release")] = FPSTR(releaseString); JsonObject leds = root.createNestedObject(F("leds")); leds[F("count")] = strip.getLengthTotal(); diff --git a/wled00/wled.h b/wled00/wled.h index b94f7790b..b6843b69d 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -243,27 +243,32 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument; // int arr[]{0,1,2} becomes WLED_GLOBAL int arr[] _INIT_N(({0,1,2})); #ifndef WLED_DEFINE_GLOBAL_VARS -# define WLED_GLOBAL extern -# define _INIT(x) -# define _INIT_N(x) + #define WLED_GLOBAL extern + #define _INIT(x) + #define _INIT_N(x) + #define _INIT_PROGMEM(x) #else -# define WLED_GLOBAL -# define _INIT(x) = x - -//needed to ignore commas in array definitions -#define UNPACK( ... ) __VA_ARGS__ -# define _INIT_N(x) UNPACK x + #define WLED_GLOBAL + #define _INIT(x) = x + //needed to ignore commas in array definitions + #define UNPACK( ... ) __VA_ARGS__ + #define _INIT_N(x) UNPACK x + #define _INIT_PROGMEM(x) PROGMEM = x #endif #define STRINGIFY(X) #X #define TOSTRING(X) STRINGIFY(X) #ifndef WLED_VERSION - #define WLED_VERSION "dev" + #define WLED_VERSION dev +#endif +#ifndef WLED_RELEASE_NAME + #define WLED_RELEASE_NAME dev_release #endif // Global Variable definitions WLED_GLOBAL char versionString[] _INIT(TOSTRING(WLED_VERSION)); +WLED_GLOBAL char releaseString[] _INIT_PROGMEM(TOSTRING(WLED_RELEASE_NAME)); // somehow this will not work if using "const char releaseString[] #define WLED_CODENAME "Kōsen" // AP and OTA default passwords (for maximum security change them!) diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 3915d9b0e..0fe55b616 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -722,6 +722,8 @@ void getSettingsJS(byte subPage, char* dest) sappends('m',SET_F("(\"sip\")[0]"),(char*)F("WLED ")); olen -= 2; //delete "; oappend(versionString); + oappend(SET_F("
")); + oappend((char*)FPSTR(releaseString)); oappend(SET_F("
(")); #if defined(ARDUINO_ARCH_ESP32) oappend(ESP.getChipModel()); From e83d3cb4a38e80d5c845d28a6d75853fcc27c3fd Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Wed, 24 Apr 2024 16:04:54 +0200 Subject: [PATCH 041/148] Fix for #3878 --- usermods/BH1750_v2/usermod_bh1750.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/BH1750_v2/usermod_bh1750.h b/usermods/BH1750_v2/usermod_bh1750.h index ede4aabc4..2a2bd4637 100644 --- a/usermods/BH1750_v2/usermod_bh1750.h +++ b/usermods/BH1750_v2/usermod_bh1750.h @@ -59,7 +59,7 @@ private: bool sensorFound = false; // Home Assistant and MQTT - String mqttLuminanceTopic = F(""); + String mqttLuminanceTopic; bool mqttInitialized = false; bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages From 24bd1db4fc51f4ff01236311a33bb9e6c1da7fff Mon Sep 17 00:00:00 2001 From: Suxsem Date: Fri, 26 Apr 2024 00:29:48 +0200 Subject: [PATCH 042/148] "X" span near to the number input field --- wled00/data/settings_leds.htm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index eecdf66f0..b3e04076f 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -823,7 +823,7 @@ Swap:
IR info
- Relay GPIO: Invert Open drain  ✕
+ Relay GPIO:  ✕ Invert Open drain

Defaults

Turn LEDs on after power up/reset:
From 6276c2f1f5db493e2db47b85dfbeda018a1faa22 Mon Sep 17 00:00:00 2001 From: gaaat Date: Fri, 26 Apr 2024 16:39:32 +0200 Subject: [PATCH 043/148] improved brightness change via long button presses --- wled00/button.cpp | 26 +++++++++++++++++++++----- wled00/wled.h | 1 + 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/wled00/button.cpp b/wled00/button.cpp index 3b73df81d..296be8090 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -7,9 +7,10 @@ #define WLED_DEBOUNCE_THRESHOLD 50 // only consider button input of at least 50ms as valid (debouncing) #define WLED_LONG_PRESS 600 // long press if button is released after held for at least 600ms #define WLED_DOUBLE_PRESS 350 // double press if another press within 350ms after a short press -#define WLED_LONG_REPEATED_ACTION 300 // how often a repeated action (e.g. dimming) is fired on long press on button IDs >0 +#define WLED_LONG_REPEATED_ACTION 400 // how often a repeated action (e.g. dimming) is fired on long press on button IDs >0 #define WLED_LONG_AP 5000 // how long button 0 needs to be held to activate WLED-AP #define WLED_LONG_FACTORY_RESET 10000 // how long button 0 needs to be held to trigger a factory reset +#define WLED_LONG_BRI_STEPS 16 // how long to wait before increasing/decreasing brightness on long press static const char _mqtt_topic_button[] PROGMEM = "%s/button/%d"; // optimize flash usage @@ -39,7 +40,20 @@ void longPressAction(uint8_t b) if (!macroLongPress[b]) { switch (b) { case 0: setRandomColor(col); colorUpdated(CALL_MODE_BUTTON); break; - case 1: bri += 8; stateUpdated(CALL_MODE_BUTTON); buttonPressedTime[b] = millis(); break; // repeatable action + case 1: + // increase bri on true, decrease on false + if(buttonBriDirection) { + if (bri == 255) break; // avoid unnecessary updates to brightness + if (bri >= 255 - WLED_LONG_BRI_STEPS) bri = 255; + else bri += WLED_LONG_BRI_STEPS; + } else { + if (bri == 1) break; // avoid unnecessary updates to brightness + if (bri <= WLED_LONG_BRI_STEPS) bri = 1; + else bri -= WLED_LONG_BRI_STEPS; + } + stateUpdated(CALL_MODE_BUTTON); + buttonPressedTime[b] = millis(); + break; // repeatable action } } else { applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET); @@ -284,10 +298,12 @@ void handleButton() buttonPressedBefore[b] = true; if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press - if (!buttonLongPressed[b]) longPressAction(b); - else if (b) { //repeatable action (~3 times per s) on button > 0 + if (!buttonLongPressed[b]) { + buttonBriDirection = !buttonBriDirection; //toggle brightness direction on long press longPressAction(b); - buttonPressedTime[b] = now - WLED_LONG_REPEATED_ACTION; //333ms + } else if (b) { //repeatable action (~5 times per s) on button > 0 + longPressAction(b); + buttonPressedTime[b] = now - WLED_LONG_REPEATED_ACTION; //200ms } buttonLongPressed[b] = true; } diff --git a/wled00/wled.h b/wled00/wled.h index 139c451f8..18eb17b7c 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -594,6 +594,7 @@ WLED_GLOBAL bool buttonPressedBefore[WLED_MAX_BUTTONS] _INIT({false}); WLED_GLOBAL bool buttonLongPressed[WLED_MAX_BUTTONS] _INIT({false}); WLED_GLOBAL unsigned long buttonPressedTime[WLED_MAX_BUTTONS] _INIT({0}); WLED_GLOBAL unsigned long buttonWaitTime[WLED_MAX_BUTTONS] _INIT({0}); +WLED_GLOBAL bool buttonBriDirection _INIT(false); WLED_GLOBAL bool disablePullUp _INIT(false); WLED_GLOBAL byte touchThreshold _INIT(TOUCH_THRESHOLD); From bdd4d9f3ffd2db6d259d9ddb24cb7ad8b98ca1c3 Mon Sep 17 00:00:00 2001 From: gaaat98 <67930088+gaaat98@users.noreply.github.com> Date: Fri, 26 Apr 2024 18:23:34 +0200 Subject: [PATCH 044/148] set buttonBriDirection as local static --- wled00/button.cpp | 2 +- wled00/wled.h | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/wled00/button.cpp b/wled00/button.cpp index 296be8090..1cd8245c8 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -13,6 +13,7 @@ #define WLED_LONG_BRI_STEPS 16 // how long to wait before increasing/decreasing brightness on long press static const char _mqtt_topic_button[] PROGMEM = "%s/button/%d"; // optimize flash usage +static bool buttonBriDirection = false; // true: increase brightness, false: decrease brightness void shortPressAction(uint8_t b) { @@ -41,7 +42,6 @@ void longPressAction(uint8_t b) switch (b) { case 0: setRandomColor(col); colorUpdated(CALL_MODE_BUTTON); break; case 1: - // increase bri on true, decrease on false if(buttonBriDirection) { if (bri == 255) break; // avoid unnecessary updates to brightness if (bri >= 255 - WLED_LONG_BRI_STEPS) bri = 255; diff --git a/wled00/wled.h b/wled00/wled.h index 18eb17b7c..139c451f8 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -594,7 +594,6 @@ WLED_GLOBAL bool buttonPressedBefore[WLED_MAX_BUTTONS] _INIT({false}); WLED_GLOBAL bool buttonLongPressed[WLED_MAX_BUTTONS] _INIT({false}); WLED_GLOBAL unsigned long buttonPressedTime[WLED_MAX_BUTTONS] _INIT({0}); WLED_GLOBAL unsigned long buttonWaitTime[WLED_MAX_BUTTONS] _INIT({0}); -WLED_GLOBAL bool buttonBriDirection _INIT(false); WLED_GLOBAL bool disablePullUp _INIT(false); WLED_GLOBAL byte touchThreshold _INIT(TOUCH_THRESHOLD); From 886120fe9f661cf30a204f186dacd6329e4e62cb Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Fri, 26 Apr 2024 20:07:27 +0200 Subject: [PATCH 045/148] Bugfix - getPixelColor() for analog - RMT channel (#3922) --- wled00/bus_manager.cpp | 17 ++++++++++++++++- wled00/bus_wrapper.h | 5 +++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 82e81a387..ac6923f40 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -466,7 +466,22 @@ void BusPwm::setPixelColor(uint16_t pix, uint32_t c) { //does no index check uint32_t BusPwm::getPixelColor(uint16_t pix) { if (!_valid) return 0; - return RGBW32(_data[0], _data[1], _data[2], _data[3]); + // TODO getting the reverse from CCT is involved (a quick approximation when CCT blending is ste to 0 implemented) + switch (_type) { + case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation + return RGBW32(0, 0, 0, _data[0]); + case TYPE_ANALOG_2CH: //warm white + cold white + if (cctICused) return RGBW32(0, 0, 0, _data[0]); + else return RGBW32(0, 0, 0, _data[0] + _data[1]); + case TYPE_ANALOG_5CH: //RGB + warm white + cold white + if (cctICused) return RGBW32(_data[0], _data[1], _data[2], _data[3]); + else return RGBW32(_data[0], _data[1], _data[2], _data[3] + _data[4]); + case TYPE_ANALOG_4CH: //RGBW + return RGBW32(_data[0], _data[1], _data[2], _data[3]); + case TYPE_ANALOG_3CH: //standard dumb RGB + return RGBW32(_data[0], _data[1], _data[2], 0); + } + return RGBW32(_data[0], _data[0], _data[0], _data[0]); } #ifndef ESP8266 diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index ebbeca4ad..c48946eb8 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -496,6 +496,11 @@ class PolyBus { } static void* create(uint8_t busType, uint8_t* pins, uint16_t len, uint8_t channel, uint16_t clock_kHz = 0U) { + #if defined(ARDUINO_ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) + // NOTE: "channel" is only used on ESP32 (and its variants) for RMT channel allocation + // since 0.15.0-b3 I2S1 is favoured for classic ESP32 and moved to position 0 (channel 0) so we need to subtract 1 for correct RMT allocation + if (channel > 1) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 + #endif void* busPtr = nullptr; switch (busType) { case I_NONE: break; From d48bab02a1a30444b9bb026fe9dd24235385a8e1 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Fri, 26 Apr 2024 20:11:46 +0200 Subject: [PATCH 046/148] Speed & size optimisations using native sized variables --- wled00/FX_2Dfcn.cpp | 110 ++++++++++++++++++++++---------------------- wled00/FX_fcn.cpp | 92 ++++++++++++++++++------------------ 2 files changed, 101 insertions(+), 101 deletions(-) diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index e4007ed7e..e14b68f4f 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -110,11 +110,11 @@ void WS2812FX::setUpMatrix() { releaseJSONBufferLock(); } - uint16_t x, y, pix=0; //pixel + unsigned x, y, pix=0; //pixel for (size_t pan = 0; pan < panel.size(); pan++) { Panel &p = panel[pan]; - uint16_t h = p.vertical ? p.height : p.width; - uint16_t v = p.vertical ? p.width : p.height; + unsigned h = p.vertical ? p.height : p.width; + unsigned v = p.vertical ? p.width : p.height; for (size_t j = 0; j < v; j++){ for(size_t i = 0; i < h; i++) { y = (p.vertical?p.rightStart:p.bottomStart) ? v-j-1 : j; @@ -163,8 +163,8 @@ void WS2812FX::setUpMatrix() { // XY(x,y) - gets pixel index within current segment (often used to reference leds[] array element) uint16_t IRAM_ATTR Segment::XY(uint16_t x, uint16_t y) { - uint16_t width = virtualWidth(); // segment width in logical pixels (can be 0 if segment is inactive) - uint16_t height = virtualHeight(); // segment height in logical pixels (is always >= 1) + unsigned width = virtualWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + unsigned height = virtualHeight(); // segment height in logical pixels (is always >= 1) return isActive() ? (x%width) + (y%height) * width : 0; } @@ -180,7 +180,7 @@ void IRAM_ATTR Segment::setPixelColorXY(int x, int y, uint32_t col) if (reverse ) x = virtualWidth() - x - 1; if (reverse_y) y = virtualHeight() - y - 1; - if (transpose) { uint16_t t = x; x = y; y = t; } // swap X & Y if segment transposed + if (transpose) { unsigned t = x; x = y; y = t; } // swap X & Y if segment transposed x *= groupLength(); // expand to physical pixels y *= groupLength(); // expand to physical pixels @@ -189,7 +189,7 @@ void IRAM_ATTR Segment::setPixelColorXY(int x, int y, uint32_t col) uint32_t tmpCol = col; for (int j = 0; j < grouping; j++) { // groupping vertically for (int g = 0; g < grouping; g++) { // groupping horizontally - uint16_t xX = (x+g), yY = (y+j); + unsigned xX = (x+g), yY = (y+j); if (xX >= width() || yY >= height()) continue; // we have reached one dimension's end #ifndef WLED_DISABLE_MODE_BLEND @@ -221,16 +221,16 @@ void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) if (!isActive()) return; // not active if (x<0.0f || x>1.0f || y<0.0f || y>1.0f) return; // not normalized - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); float fX = x * (cols-1); float fY = y * (rows-1); if (aa) { - uint16_t xL = roundf(fX-0.49f); - uint16_t xR = roundf(fX+0.49f); - uint16_t yT = roundf(fY-0.49f); - uint16_t yB = roundf(fY+0.49f); + unsigned xL = roundf(fX-0.49f); + unsigned xR = roundf(fX+0.49f); + unsigned yT = roundf(fY-0.49f); + unsigned yB = roundf(fY+0.49f); float dL = (fX - xL)*(fX - xL); float dR = (xR - fX)*(xR - fX); float dT = (fY - yT)*(fY - yT); @@ -266,7 +266,7 @@ uint32_t IRAM_ATTR Segment::getPixelColorXY(int x, int y) { if (x >= virtualWidth() || y >= virtualHeight() || x<0 || y<0) return 0; // if pixel would fall out of virtual segment just exit if (reverse ) x = virtualWidth() - x - 1; if (reverse_y) y = virtualHeight() - y - 1; - if (transpose) { uint16_t t = x; x = y; y = t; } // swap X & Y if segment transposed + if (transpose) { unsigned t = x; x = y; y = t; } // swap X & Y if segment transposed x *= groupLength(); // expand to physical pixels y *= groupLength(); // expand to physical pixels if (x >= width() || y >= height()) return 0; @@ -276,8 +276,8 @@ uint32_t IRAM_ATTR Segment::getPixelColorXY(int x, int y) { // blurRow: perform a blur on a row of a rectangular matrix void Segment::blurRow(uint32_t row, fract8 blur_amount, bool smear){ if (!isActive() || blur_amount == 0) return; // not active - const uint_fast16_t cols = virtualWidth(); - const uint_fast16_t rows = virtualHeight(); + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); if (row >= rows) return; // blur one row @@ -309,8 +309,8 @@ void Segment::blurRow(uint32_t row, fract8 blur_amount, bool smear){ // blurCol: perform a blur on a column of a rectangular matrix void Segment::blurCol(uint32_t col, fract8 blur_amount, bool smear) { if (!isActive() || blur_amount == 0) return; // not active - const uint_fast16_t cols = virtualWidth(); - const uint_fast16_t rows = virtualHeight(); + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); if (col >= cols) return; // blur one column @@ -342,34 +342,34 @@ void Segment::blurCol(uint32_t col, fract8 blur_amount, bool smear) { // 1D Box blur (with added weight - blur_amount: [0=no blur, 255=max blur]) void Segment::box_blur(uint16_t i, bool vertical, fract8 blur_amount) { if (!isActive() || blur_amount == 0) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); - const uint16_t dim1 = vertical ? rows : cols; - const uint16_t dim2 = vertical ? cols : rows; + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); + const unsigned dim1 = vertical ? rows : cols; + const unsigned dim2 = vertical ? cols : rows; if (i >= dim2) return; const float seep = blur_amount/255.f; const float keep = 3.f - 2.f*seep; // 1D box blur CRGB tmp[dim1]; - for (int j = 0; j < dim1; j++) { - uint16_t x = vertical ? i : j; - uint16_t y = vertical ? j : i; - int16_t xp = vertical ? x : x-1; // "signed" to prevent underflow - int16_t yp = vertical ? y-1 : y; // "signed" to prevent underflow - uint16_t xn = vertical ? x : x+1; - uint16_t yn = vertical ? y+1 : y; + for (unsigned j = 0; j < dim1; j++) { + unsigned x = vertical ? i : j; + unsigned y = vertical ? j : i; + int xp = vertical ? x : x-1; // "signed" to prevent underflow + int yp = vertical ? y-1 : y; // "signed" to prevent underflow + unsigned xn = vertical ? x : x+1; + unsigned yn = vertical ? y+1 : y; CRGB curr = getPixelColorXY(x,y); CRGB prev = (xp<0 || yp<0) ? CRGB::Black : getPixelColorXY(xp,yp); CRGB next = ((vertical && yn>=dim1) || (!vertical && xn>=dim1)) ? CRGB::Black : getPixelColorXY(xn,yn); - uint16_t r, g, b; + unsigned r, g, b; r = (curr.r*keep + (prev.r + next.r)*seep) / 3; g = (curr.g*keep + (prev.g + next.g)*seep) / 3; b = (curr.b*keep + (prev.b + next.b)*seep) / 3; tmp[j] = CRGB(r,g,b); } - for (int j = 0; j < dim1; j++) { - uint16_t x = vertical ? i : j; - uint16_t y = vertical ? j : i; + for (unsigned j = 0; j < dim1; j++) { + unsigned x = vertical ? i : j; + unsigned y = vertical ? j : i; setPixelColorXY(x, y, tmp[j]); } } @@ -389,14 +389,14 @@ void Segment::box_blur(uint16_t i, bool vertical, fract8 blur_amount) { // it can be used to (slowly) clear the LEDs to black. void Segment::blur1d(fract8 blur_amount) { - const uint16_t rows = virtualHeight(); + const unsigned rows = virtualHeight(); for (unsigned y = 0; y < rows; y++) blurRow(y, blur_amount); } void Segment::moveX(int8_t delta, bool wrap) { if (!isActive()) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); + const int cols = virtualWidth(); + const int rows = virtualHeight(); if (!delta || abs(delta) >= cols) return; uint32_t newPxCol[cols]; for (int y = 0; y < rows; y++) { @@ -413,8 +413,8 @@ void Segment::moveX(int8_t delta, bool wrap) { void Segment::moveY(int8_t delta, bool wrap) { if (!isActive()) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); + const int cols = virtualWidth(); + const int rows = virtualHeight(); if (!delta || abs(delta) >= rows) return; uint32_t newPxCol[rows]; for (int x = 0; x < cols; x++) { @@ -474,13 +474,13 @@ void Segment::draw_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB col) { // by stepko, taken from https://editor.soulmatelights.com/gallery/573-blobs void Segment::fill_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB col) { if (!isActive() || radius == 0) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); - for (int16_t y = -radius; y <= radius; y++) { - for (int16_t x = -radius; x <= radius; x++) { + const int cols = virtualWidth(); + const int rows = virtualHeight(); + for (int y = -radius; y <= radius; y++) { + for (int x = -radius; x <= radius; x++) { if (x * x + y * y <= radius * radius && - int16_t(cx)+x>=0 && int16_t(cy)+y>=0 && - int16_t(cx)+x=0 && int(cy)+y>=0 && + int(cx)+x= cols || x1 >= cols || y0 >= rows || y1 >= rows) return; - const int16_t dx = abs(x1-x0), sx = x0dy ? dx : -dy)/2, e2; + const int dx = abs(x1-x0), sx = x0dy ? dx : -dy)/2, e2; for (;;) { setPixelColorXY(x0,y0,c); if (x0==x1 && y0==y1) break; @@ -525,8 +525,8 @@ void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, if (!isActive()) return; // not active if (chr < 32 || chr > 126) return; // only ASCII 32-126 supported chr -= 32; // align with font table entries - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); + const int cols = virtualWidth(); + const int rows = virtualHeight(); const int font = w*h; CRGB col = CRGB(color); @@ -565,7 +565,7 @@ void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) { //awesome wu_pixel procedure by reddit u/sutaburosu if (!isActive()) return; // not active // extract the fractional parts and derive their inverses - uint8_t xx = x & 0xff, yy = y & 0xff, ix = 255 - xx, iy = 255 - yy; + unsigned xx = x & 0xff, yy = y & 0xff, ix = 255 - xx, iy = 255 - yy; // calculate the intensities for each affected pixel uint8_t wu[4] = {WU_WEIGHT(ix, iy), WU_WEIGHT(xx, iy), WU_WEIGHT(ix, yy), WU_WEIGHT(xx, yy)}; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 5d031e8ce..ce510f16e 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -327,7 +327,7 @@ void Segment::stopTransition() { } void Segment::handleTransition() { - uint16_t _progress = progress(); + unsigned _progress = progress(); if (_progress == 0xFFFFU) stopTransition(); } @@ -412,9 +412,9 @@ void Segment::restoreSegenv(tmpsegd_t &tmpSeg) { #endif uint8_t IRAM_ATTR Segment::currentBri(bool useCct) { - uint32_t prog = progress(); + unsigned prog = progress(); if (prog < 0xFFFFU) { - uint32_t curBri = (useCct ? cct : (on ? opacity : 0)) * prog; + unsigned curBri = (useCct ? cct : (on ? opacity : 0)) * prog; curBri += (useCct ? _t->_cctT : _t->_briT) * (0xFFFFU - prog); return curBri / 0xFFFFU; } @@ -423,7 +423,7 @@ uint8_t IRAM_ATTR Segment::currentBri(bool useCct) { uint8_t IRAM_ATTR Segment::currentMode() { #ifndef WLED_DISABLE_MODE_BLEND - uint16_t prog = progress(); + unsigned prog = progress(); if (modeBlending && prog < 0xFFFFU) return _t->_modeT; #endif return mode; @@ -440,13 +440,13 @@ uint32_t IRAM_ATTR Segment::currentColor(uint8_t slot) { CRGBPalette16 IRAM_ATTR &Segment::currentPalette(CRGBPalette16 &targetPalette, uint8_t pal) { loadPalette(targetPalette, pal); - uint16_t prog = progress(); + unsigned prog = progress(); if (strip.paletteFade && prog < 0xFFFFU) { // blend palettes // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) // minimum blend time is 100ms maximum is 65535ms - uint16_t noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; - for (int i=0; i_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, targetPalette, 48); + unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; + for (unsigned i=0; i_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, targetPalette, 48); targetPalette = _t->_palT; // copy transitioning/temporary palette } return targetPalette; @@ -576,7 +576,7 @@ void Segment::setMode(uint8_t fx, bool loadDefaults) { mode = fx; // load default values from effect string if (loadDefaults) { - int16_t sOpt; + int sOpt; sOpt = extractModeDefaults(fx, "sx"); speed = (sOpt >= 0) ? sOpt : DEFAULT_SPEED; sOpt = extractModeDefaults(fx, "ix"); intensity = (sOpt >= 0) ? sOpt : DEFAULT_INTENSITY; sOpt = extractModeDefaults(fx, "c1"); custom1 = (sOpt >= 0) ? sOpt : DEFAULT_C1; @@ -610,21 +610,21 @@ void Segment::setPalette(uint8_t pal) { // 2D matrix uint16_t IRAM_ATTR Segment::virtualWidth() const { - uint16_t groupLen = groupLength(); - uint16_t vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen; + unsigned groupLen = groupLength(); + unsigned vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen; if (mirror) vWidth = (vWidth + 1) /2; // divide by 2 if mirror, leave at least a single LED return vWidth; } uint16_t IRAM_ATTR Segment::virtualHeight() const { - uint16_t groupLen = groupLength(); - uint16_t vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen; + unsigned groupLen = groupLength(); + unsigned vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen; if (mirror_y) vHeight = (vHeight + 1) /2; // divide by 2 if mirror, leave at least a single LED return vHeight; } uint16_t IRAM_ATTR Segment::nrOfVStrips() const { - uint16_t vLen = 1; + unsigned vLen = 1; #ifndef WLED_DISABLE_2D if (is2D()) { switch (map1D2D) { @@ -641,9 +641,9 @@ uint16_t IRAM_ATTR Segment::nrOfVStrips() const { uint16_t IRAM_ATTR Segment::virtualLength() const { #ifndef WLED_DISABLE_2D if (is2D()) { - uint16_t vW = virtualWidth(); - uint16_t vH = virtualHeight(); - uint16_t vLen = vW * vH; // use all pixels from segment + unsigned vW = virtualWidth(); + unsigned vH = virtualHeight(); + unsigned vLen = vW * vH; // use all pixels from segment switch (map1D2D) { case M12_pBar: vLen = vH; @@ -656,8 +656,8 @@ uint16_t IRAM_ATTR Segment::virtualLength() const { return vLen; } #endif - uint16_t groupLen = groupLength(); // is always >= 1 - uint16_t vLength = (length() + groupLen - 1) / groupLen; + unsigned groupLen = groupLength(); // is always >= 1 + unsigned vLength = (length() + groupLen - 1) / groupLen; if (mirror) vLength = (vLength + 1) /2; // divide by 2 if mirror, leave at least a single LED return vLength; } @@ -674,8 +674,8 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) #ifndef WLED_DISABLE_2D if (is2D()) { - uint16_t vH = virtualHeight(); // segment height in logical pixels - uint16_t vW = virtualWidth(); + int vH = virtualHeight(); // segment height in logical pixels + int vW = virtualWidth(); switch (map1D2D) { case M12_Pixels: // use all available pixels as a long strip @@ -732,7 +732,7 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) } #endif - uint16_t len = length(); + unsigned len = length(); uint8_t _bri_t = currentBri(); if (_bri_t < 255) { col = color_fade(col, _bri_t); @@ -785,8 +785,8 @@ void Segment::setPixelColor(float i, uint32_t col, bool aa) float fC = i * (virtualLength()-1); if (aa) { - uint16_t iL = roundf(fC-0.49f); - uint16_t iR = roundf(fC+0.49f); + unsigned iL = roundf(fC-0.49f); + unsigned iR = roundf(fC+0.49f); float dL = (fC - iL)*(fC - iL); float dR = (iR - fC)*(iR - fC); uint32_t cIL = getPixelColor(iL | (vStrip<<16)); @@ -803,7 +803,7 @@ void Segment::setPixelColor(float i, uint32_t col, bool aa) setPixelColor(iL | (vStrip<<16), col); } } else { - setPixelColor(uint16_t(roundf(fC)) | (vStrip<<16), col); + setPixelColor(int(roundf(fC)) | (vStrip<<16), col); } } #endif @@ -818,8 +818,8 @@ uint32_t IRAM_ATTR Segment::getPixelColor(int i) #ifndef WLED_DISABLE_2D if (is2D()) { - uint16_t vH = virtualHeight(); // segment height in logical pixels - uint16_t vW = virtualWidth(); + unsigned vH = virtualHeight(); // segment height in logical pixels + unsigned vW = virtualWidth(); switch (map1D2D) { case M12_Pixels: return getPixelColorXY(i % vW, i / vW); @@ -875,9 +875,9 @@ uint8_t Segment::differs(Segment& b) const { } void Segment::refreshLightCapabilities() { - uint8_t capabilities = 0; - uint16_t segStartIdx = 0xFFFFU; - uint16_t segStopIdx = 0; + unsigned capabilities = 0; + unsigned segStartIdx = 0xFFFFU; + unsigned segStopIdx = 0; if (!isActive()) { _capabilities = 0; @@ -887,7 +887,7 @@ void Segment::refreshLightCapabilities() { if (start < Segment::maxWidth * Segment::maxHeight) { // we are withing 2D matrix (includes 1D segments) for (int y = startY; y < stopY; y++) for (int x = start; x < stop; x++) { - uint16_t index = strip.getMappedPixelIndex(x + Segment::maxWidth * y); // convert logical address to physical + unsigned index = strip.getMappedPixelIndex(x + Segment::maxWidth * y); // convert logical address to physical if (index < 0xFFFFU) { if (segStartIdx > index) segStartIdx = index; if (segStopIdx < index) segStopIdx = index; @@ -912,7 +912,7 @@ void Segment::refreshLightCapabilities() { if (!cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; if (correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) if (bus->hasWhite()) { - uint8_t aWM = Bus::getGlobalAWMode() == AW_GLOBAL_DISABLED ? bus->getAutoWhiteMode() : Bus::getGlobalAWMode(); + unsigned aWM = Bus::getGlobalAWMode() == AW_GLOBAL_DISABLED ? bus->getAutoWhiteMode() : Bus::getGlobalAWMode(); bool whiteSlider = (aWM == RGBW_MODE_DUAL || aWM == RGBW_MODE_MANUAL_ONLY); // white slider allowed // if auto white calculation from RGB is active (Accurate/Brighter), force RGB controls even if there are no RGB busses if (!whiteSlider) capabilities |= SEG_CAPABILITY_RGB; @@ -928,8 +928,8 @@ void Segment::refreshLightCapabilities() { */ void Segment::fill(uint32_t c) { if (!isActive()) return; // not active - const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); - const uint16_t rows = virtualHeight(); // will be 1 for 1D + const int cols = is2D() ? virtualWidth() : virtualLength(); + const int rows = virtualHeight(); // will be 1 for 1D for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { if (is2D()) setPixelColorXY(x, y, c); else setPixelColor(x, c); @@ -941,8 +941,8 @@ void Segment::fill(uint32_t c) { */ void Segment::fade_out(uint8_t rate) { if (!isActive()) return; // not active - const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); - const uint16_t rows = virtualHeight(); // will be 1 for 1D + const int cols = is2D() ? virtualWidth() : virtualLength(); + const int rows = virtualHeight(); // will be 1 for 1D rate = (255-rate) >> 1; float mappedRate = float(rate) +1.1f; @@ -979,8 +979,8 @@ void Segment::fade_out(uint8_t rate) { // fades all pixels to black using nscale8() void Segment::fadeToBlackBy(uint8_t fadeBy) { if (!isActive() || fadeBy == 0) return; // optimization - no scaling to apply - const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); - const uint16_t rows = virtualHeight(); // will be 1 for 1D + const int cols = is2D() ? virtualWidth() : virtualLength(); + const int rows = virtualHeight(); // will be 1 for 1D for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { if (is2D()) setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), 255-fadeBy)); @@ -1065,7 +1065,7 @@ uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_ // default palette or no RGB support on segment if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) return (pbri == 255) ? color : color_fade(color, pbri, true); - uint8_t paletteIndex = i; + unsigned paletteIndex = i; if (mapping && virtualLength() > 1) paletteIndex = (i*255)/(virtualLength() -1); // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) if (!wrap && strip.paletteBlend != 3) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end" @@ -1132,7 +1132,7 @@ void WS2812FX::finalizeInit(void) { _hasWhiteChannel |= bus->hasWhite(); //refresh is required to remain off if at least one of the strips requires the refresh. _isOffRefreshRequired |= bus->isOffRefreshRequired(); - uint16_t busEnd = bus->getStart() + bus->getLength(); + unsigned busEnd = bus->getStart() + bus->getLength(); if (busEnd > _length) _length = busEnd; #ifdef ESP8266 if ((!IS_DIGITAL(bus->getType()) || IS_2PIN(bus->getType()))) continue; @@ -1176,10 +1176,10 @@ void WS2812FX::service() { if (nowUp > seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) { doShow = true; - uint16_t delay = FRAMETIME; + unsigned delay = FRAMETIME; if (!seg.freeze) { //only run effect function if not frozen - int16_t oldCCT = BusManager::getSegmentCCT(); // store original CCT value (actually it is not Segment based) + int oldCCT = BusManager::getSegmentCCT(); // store original CCT value (actually it is not Segment based) _virtualSegmentLength = seg.virtualLength(); //SEGLEN _colors_t[0] = gamma32(seg.currentColor(0)); _colors_t[1] = gamma32(seg.currentColor(1)); @@ -1203,7 +1203,7 @@ void WS2812FX::service() { Segment::modeBlend(true); // set semaphore seg.swapSegenv(_tmpSegData); // temporarily store new mode state (and swap it with transitional state) _virtualSegmentLength = seg.virtualLength(); // update SEGLEN (mapping may have changed) - uint16_t d2 = (*_mode[tmpMode])(); // run old mode + unsigned d2 = (*_mode[tmpMode])(); // run old mode seg.restoreSegenv(_tmpSegData); // restore mode state (will also update transitional state) delay = MIN(delay,d2); // use shortest delay Segment::modeBlend(false); // unset semaphore @@ -1378,13 +1378,13 @@ uint8_t WS2812FX::getActiveSegmentsNum(void) { } uint16_t WS2812FX::getLengthTotal(void) { - uint16_t len = Segment::maxWidth * Segment::maxHeight; // will be _length for 1D (see finalizeInit()) but should cover whole matrix for 2D + unsigned len = Segment::maxWidth * Segment::maxHeight; // will be _length for 1D (see finalizeInit()) but should cover whole matrix for 2D if (isMatrix && _length > len) len = _length; // for 2D with trailing strip return len; } uint16_t WS2812FX::getLengthPhysical(void) { - uint16_t len = 0; + unsigned len = 0; for (size_t b = 0; b < BusManager::getNumBusses(); b++) { Bus *bus = BusManager::getBus(b); if (bus->getType() >= TYPE_NET_DDP_RGB) continue; //exclude non-physical network busses @@ -1461,8 +1461,8 @@ void WS2812FX::resetSegments() { void WS2812FX::makeAutoSegments(bool forceReset) { if (autoSegments) { //make one segment per bus - uint16_t segStarts[MAX_NUM_SEGMENTS] = {0}; - uint16_t segStops [MAX_NUM_SEGMENTS] = {0}; + unsigned segStarts[MAX_NUM_SEGMENTS] = {0}; + unsigned segStops [MAX_NUM_SEGMENTS] = {0}; size_t s = 0; #ifndef WLED_DISABLE_2D From 25dd43b949176a99c19d43ca1bc7b53474754708 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Fri, 26 Apr 2024 23:49:34 +0200 Subject: [PATCH 047/148] Bugfix for bugfix - thanks @softhack007 --- wled00/bus_wrapper.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index c48946eb8..efaad7c42 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -499,7 +499,7 @@ class PolyBus { #if defined(ARDUINO_ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) // NOTE: "channel" is only used on ESP32 (and its variants) for RMT channel allocation // since 0.15.0-b3 I2S1 is favoured for classic ESP32 and moved to position 0 (channel 0) so we need to subtract 1 for correct RMT allocation - if (channel > 1) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 + if (channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 #endif void* busPtr = nullptr; switch (busType) { From d7e0b364d16b6474aed0d893d60e95f34a669fc0 Mon Sep 17 00:00:00 2001 From: gaaat98 <67930088+gaaat98@users.noreply.github.com> Date: Sat, 27 Apr 2024 12:25:34 +0200 Subject: [PATCH 048/148] fixed wrong comment --- wled00/button.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/button.cpp b/wled00/button.cpp index 1cd8245c8..6d69f15f8 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -10,7 +10,7 @@ #define WLED_LONG_REPEATED_ACTION 400 // how often a repeated action (e.g. dimming) is fired on long press on button IDs >0 #define WLED_LONG_AP 5000 // how long button 0 needs to be held to activate WLED-AP #define WLED_LONG_FACTORY_RESET 10000 // how long button 0 needs to be held to trigger a factory reset -#define WLED_LONG_BRI_STEPS 16 // how long to wait before increasing/decreasing brightness on long press +#define WLED_LONG_BRI_STEPS 16 // how much to increase/decrease the brightness with each long press repetition static const char _mqtt_topic_button[] PROGMEM = "%s/button/%d"; // optimize flash usage static bool buttonBriDirection = false; // true: increase brightness, false: decrease brightness From 1048bf993af9693c7b795f5e2ccb4db404636616 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sat, 27 Apr 2024 23:34:35 +0200 Subject: [PATCH 049/148] use lolin_s3_mini for esp32-S3 4MB buildenv --- platformio.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 8d9109c02..76c4c92d6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -471,8 +471,7 @@ monitor_filters = esp32_exception_decoder [env:esp32s3_4M_qspi] ;; ESP32-S3, with 4MB FLASH and <= 4MB PSRAM (memory_type: qio_qspi) -board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support -board_build.arduino.memory_type = qio_qspi ;; use with PSRAM: 2MB or 4MB +board = lolin_s3_mini ;; -S3 mini, 4MB flash 2MB PSRAM platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 From 9f99a1896df5e1439d328ab974d89f18bd5ac37e Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 29 Apr 2024 16:05:12 +0200 Subject: [PATCH 050/148] presets.json PSRAM caching: consider cacheInvalidate * trying to make the caching mechanism bulletproof. `cacheInvalidate` is changed when - autosave usermod updates presets - a file was upload * (coding style) fixed some unitialized variables --- wled00/file.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/wled00/file.cpp b/wled00/file.cpp index eae50ff1d..814aa77e6 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -381,11 +381,15 @@ void updateFSInfo() { // original idea by @akaricchi (https://github.com/Akaricchi) // returns a pointer to the PSRAM buffer, updates size parameter static const uint8_t *getPresetCache(size_t &size) { - static unsigned long presetsCachedTime; - static uint8_t *presetsCached; - static size_t presetsCachedSize; + static unsigned long presetsCachedTime = 0; + static uint8_t *presetsCached = nullptr; + static size_t presetsCachedSize = 0; + static byte presetsCachedValidate = 0; - if (presetsModifiedTime != presetsCachedTime) { + //if (presetsModifiedTime != presetsCachedTime) DEBUG_PRINTLN(F("getPresetCache(): presetsModifiedTime changed.")); + //if (presetsCachedValidate != cacheInvalidate) DEBUG_PRINTLN(F("getPresetCache(): cacheInvalidate changed.")); + + if ((presetsModifiedTime != presetsCachedTime) || (presetsCachedValidate != cacheInvalidate)) { if (presetsCached) { free(presetsCached); presetsCached = nullptr; @@ -396,6 +400,7 @@ static const uint8_t *getPresetCache(size_t &size) { File file = WLED_FS.open(FPSTR(getPresetsFileName()), "r"); if (file) { presetsCachedTime = presetsModifiedTime; + presetsCachedValidate = cacheInvalidate; presetsCachedSize = 0; presetsCached = (uint8_t*)ps_malloc(file.size() + 1); if (presetsCached) { From 74bc159a522a14f947e2b66288996a20a2e229e5 Mon Sep 17 00:00:00 2001 From: gaaat Date: Mon, 29 Apr 2024 20:19:10 +0200 Subject: [PATCH 051/148] enabled some audioreactive effects for single pixel strips/segments --- wled00/FX.cpp | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 5592f7ba8..3110ab910 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -6643,7 +6643,7 @@ static const char _data_FX_MODE_GRAVIMETER[] PROGMEM = "Gravimeter@Rate of fall, // * JUGGLES // ////////////////////// uint16_t mode_juggles(void) { // Juggles. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); + //if (SEGLEN == 1) return mode_static(); um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // add support for no audio @@ -6655,12 +6655,13 @@ uint16_t mode_juggles(void) { // Juggles. By Andrew Tuline. uint16_t my_sampleAgc = fmax(fmin(volumeSmth, 255.0), 0); for (size_t i=0; i Date: Tue, 30 Apr 2024 14:09:12 +0200 Subject: [PATCH 052/148] using brightness in analog clock overlay --- wled00/overlay.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/wled00/overlay.cpp b/wled00/overlay.cpp index 19b26c224..92d8820e3 100644 --- a/wled00/overlay.cpp +++ b/wled00/overlay.cpp @@ -11,6 +11,7 @@ void _overlayAnalogClock() { _overlayAnalogCountdown(); return; } + uint8_t brightness = strip.getBrightness(); float hourP = ((float)(hour(localTime)%12))/12.0f; float minuteP = ((float)minute(localTime))/60.0f; hourP = hourP + minuteP/12.0f; @@ -25,11 +26,11 @@ void _overlayAnalogClock() { if (secondPixel < analogClock12pixel) { - strip.setRange(analogClock12pixel, overlayMax, 0xFF0000); - strip.setRange(overlayMin, secondPixel, 0xFF0000); + strip.setRange(analogClock12pixel, overlayMax, (uint32_t)brightness<<16); + strip.setRange(overlayMin, secondPixel, (uint32_t)brightness<<16); } else { - strip.setRange(analogClock12pixel, secondPixel, 0xFF0000); + strip.setRange(analogClock12pixel, secondPixel, (uint32_t)brightness<<16); } } if (analogClock5MinuteMarks) @@ -38,12 +39,12 @@ void _overlayAnalogClock() { unsigned pix = analogClock12pixel + roundf((overlaySize / 12.0f) *i); if (pix > overlayMax) pix -= overlaySize; - strip.setPixelColor(pix, 0x00FFAA); + strip.setPixelColor(pix, ((uint32_t)brightness<<8)|((uint32_t)brightness*2/3)); } } - if (!analogClockSecondsTrail) strip.setPixelColor(secondPixel, 0xFF0000); - strip.setPixelColor(minutePixel, 0x00FF00); - strip.setPixelColor(hourPixel, 0x0000FF); + if (!analogClockSecondsTrail) strip.setPixelColor(secondPixel, (uint32_t)brightness<<16); + strip.setPixelColor(minutePixel, (uint32_t)brightness<<8); + strip.setPixelColor(hourPixel, (uint32_t)brightness); } From bd69c24231153912e97ff31be399dcedd34567a5 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Tue, 30 Apr 2024 14:54:53 +0200 Subject: [PATCH 053/148] intermediate update --- usermods/Battery/battery_defaults.h | 1 + usermods/Battery/readme.md | 5 +++++ usermods/Battery/types/lion.h | 3 +-- usermods/Battery/types/lipo.h | 3 +-- usermods/Battery/types/unkown.h | 3 +-- usermods/Battery/usermod_v2_Battery.h | 6 ++---- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/usermods/Battery/battery_defaults.h b/usermods/Battery/battery_defaults.h index 6d0a95dc4..ea01e8620 100644 --- a/usermods/Battery/battery_defaults.h +++ b/usermods/Battery/battery_defaults.h @@ -111,6 +111,7 @@ #define USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION 5 #endif +// battery types typedef enum { unknown=0, diff --git a/usermods/Battery/readme.md b/usermods/Battery/readme.md index 1ca229763..e82378084 100644 --- a/usermods/Battery/readme.md +++ b/usermods/Battery/readme.md @@ -86,6 +86,11 @@ Specification from: [Molicel INR18650-M35A, 3500mAh 10A Lithium-ion battery, 3. ## 📝 Change Log +2024-04-30 + +- integrate factory pattern to make it easier to add other / custom battery types +- update readme + 2023-01-04 - basic support for LiPo rechargeable batteries ( `-D USERMOD_BATTERY_USE_LIPO`) diff --git a/usermods/Battery/types/lion.h b/usermods/Battery/types/lion.h index 0d2325386..b3641e263 100644 --- a/usermods/Battery/types/lion.h +++ b/usermods/Battery/types/lion.h @@ -13,8 +13,7 @@ class Lion : public Battery private: public: - Lion() - : Battery() + Lion() : Battery() { this->setMinVoltage(USERMOD_BATTERY_LION_MIN_VOLTAGE); this->setMaxVoltage(USERMOD_BATTERY_LION_MAX_VOLTAGE); diff --git a/usermods/Battery/types/lipo.h b/usermods/Battery/types/lipo.h index f65ab12c5..1deb6e7d3 100644 --- a/usermods/Battery/types/lipo.h +++ b/usermods/Battery/types/lipo.h @@ -13,8 +13,7 @@ class Lipo : public Battery private: public: - Lipo() - : Battery() + Lipo() : Battery() { this->setMinVoltage(USERMOD_BATTERY_LIPO_MIN_VOLTAGE); this->setMaxVoltage(USERMOD_BATTERY_LIPO_MAX_VOLTAGE); diff --git a/usermods/Battery/types/unkown.h b/usermods/Battery/types/unkown.h index edf220040..32a1bfe42 100644 --- a/usermods/Battery/types/unkown.h +++ b/usermods/Battery/types/unkown.h @@ -13,8 +13,7 @@ class Unkown : public Battery private: public: - Unkown() - : Battery() + Unkown() : Battery() { this->setMinVoltage(USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE); this->setMaxVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE); diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index 9b980d557..a82a46667 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -9,9 +9,8 @@ /* * Usermod by Maximilian Mewes - * Mail: mewes.maximilian@gmx.de - * GitHub: itCarl - * Date: 25.12.2022 + * E-mail: mewes.maximilian@gmx.de + * Created at: 25.12.2022 * If you have any questions, please feel free to contact me. */ class UsermodBattery : public Usermod @@ -140,7 +139,6 @@ class UsermodBattery : public Usermod } #else //ESP8266 boards have only one analog input pin A0 pinMode(batteryPin, INPUT); - // voltage = readVoltage(); #endif // plug in the right battery type From a13f1a9bee52ad637cf6903546f1bac4ef3df3da Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Tue, 30 Apr 2024 15:24:02 +0200 Subject: [PATCH 054/148] bug fixes --- usermods/Battery/battery.h | 61 +++++++++++++++------------ usermods/Battery/types/lion.h | 8 ---- usermods/Battery/types/lipo.h | 8 ---- usermods/Battery/usermod_v2_Battery.h | 43 ++++++++++++++++++- 4 files changed, 75 insertions(+), 45 deletions(-) diff --git a/usermods/Battery/battery.h b/usermods/Battery/battery.h index 4cdfb035f..084e6c0aa 100644 --- a/usermods/Battery/battery.h +++ b/usermods/Battery/battery.h @@ -32,7 +32,14 @@ class Battery this->setCalibration(USERMOD_BATTERY_CALIBRATION); } - virtual void update(batteryConfig cfg) = 0; + virtual void update(batteryConfig cfg) + { + if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); + if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); + if(cfg.level) this->setLevel(cfg.level); + if(cfg.calibration) this->setCalibration(cfg.calibration); + if(cfg.voltageMultiplier) this->setVoltageMultiplier(cfg.voltageMultiplier); + } /** * Corresponding battery curves @@ -49,10 +56,10 @@ class Battery /* - * - * Getter and Setter - * - */ + * + * Getter and Setter + * + */ /* * Get lowest configured battery voltage @@ -63,26 +70,26 @@ class Battery } /* - * Set lowest battery voltage - * can't be below 0 volt - */ + * Set lowest battery voltage + * can't be below 0 volt + */ virtual void setMinVoltage(float voltage) { this->minVoltage = max(0.0f, voltage); } /* - * Get highest configured battery voltage - */ + * Get highest configured battery voltage + */ virtual float getMaxVoltage() { return this->maxVoltage; } /* - * Set highest battery voltage - * can't be below minVoltage - */ + * Set highest battery voltage + * can't be below minVoltage + */ virtual void setMaxVoltage(float voltage) { this->maxVoltage = max(getMinVoltage()+.5f, voltage); @@ -110,43 +117,43 @@ class Battery void setLevel(float level) { - this->level = constrain(level, 0.0f, 110.0f);; + this->level = constrain(level, 0.0f, 110.0f); } /* - * Get the configured calibration value - * a offset value to fine-tune the calculated voltage. - */ + * Get the configured calibration value + * a offset value to fine-tune the calculated voltage. + */ virtual float getCalibration() { return calibration; } /* - * Set the voltage calibration offset value - * a offset value to fine-tune the calculated voltage. - */ + * Set the voltage calibration offset value + * a offset value to fine-tune the calculated voltage. + */ virtual void setCalibration(float offset) { calibration = offset; } /* - * Get the configured calibration value - * a value to set the voltage divider ratio - */ + * Get the configured calibration value + * a value to set the voltage divider ratio + */ virtual float getVoltageMultiplier() { return voltageMultiplier; } /* - * Set the voltage multiplier value - * a value to set the voltage divider ratio. - */ + * Set the voltage multiplier value + * a value to set the voltage divider ratio. + */ virtual void setVoltageMultiplier(float multiplier) { - voltageMultiplier = voltageMultiplier; + voltageMultiplier = multiplier; } }; diff --git a/usermods/Battery/types/lion.h b/usermods/Battery/types/lion.h index b3641e263..e77266164 100644 --- a/usermods/Battery/types/lion.h +++ b/usermods/Battery/types/lion.h @@ -19,14 +19,6 @@ class Lion : public Battery this->setMaxVoltage(USERMOD_BATTERY_LION_MAX_VOLTAGE); } - void update(batteryConfig cfg) - { - if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); - if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); - if(cfg.level) this->setLevel(cfg.level); - if(cfg.calibration) this->setCalibration(cfg.calibration); - } - float mapVoltage(float v, float min, float max) override { return this->linearMapping(v, min, max); // basic mapping diff --git a/usermods/Battery/types/lipo.h b/usermods/Battery/types/lipo.h index 1deb6e7d3..d732cf4da 100644 --- a/usermods/Battery/types/lipo.h +++ b/usermods/Battery/types/lipo.h @@ -19,14 +19,6 @@ class Lipo : public Battery this->setMaxVoltage(USERMOD_BATTERY_LIPO_MAX_VOLTAGE); } - void update(batteryConfig cfg) - { - if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); - if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); - if(cfg.level) this->setLevel(cfg.level); - if(cfg.calibration) this->setCalibration(cfg.calibration); - } - /** * LiPo batteries have a differnt discharge curve, see * https://blog.ampow.com/lipo-voltage-chart/ diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index a82a46667..b9631d6db 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -264,6 +264,7 @@ class UsermodBattery : public Usermod battery[F("min-voltage")] = bat->getMinVoltage(); battery[F("max-voltage")] = bat->getMaxVoltage(); battery[F("calibration")] = bat->getCalibration(); + battery[F("voltage-multiplier")] = bat->getVoltageMultiplier(); battery[FPSTR(_readInterval)] = readingInterval; JsonObject ao = battery.createNestedObject(F("auto-off")); // auto off section @@ -283,6 +284,7 @@ class UsermodBattery : public Usermod getJsonValue(battery[F("min-voltage")], cfg.minVoltage); getJsonValue(battery[F("max-voltage")], cfg.maxVoltage); getJsonValue(battery[F("calibration")], cfg.calibration); + getJsonValue(battery[F("voltage-multiplier")], cfg.voltageMultiplier); setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); @@ -459,6 +461,7 @@ class UsermodBattery : public Usermod setMinBatteryVoltage(battery[F("min-voltage")] | bat->getMinVoltage()); setMaxBatteryVoltage(battery[F("max-voltage")] | bat->getMaxVoltage()); setCalibration(battery[F("calibration")] | calibration); + setVoltageMultiplier(battery[F("voltage-multiplier")] | voltageMultiplier); setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); getUsermodConfigFromJsonObject(battery); @@ -537,7 +540,25 @@ class UsermodBattery : public Usermod return USERMOD_ID_BATTERY; } + /** + * get currently active battery type + */ + batteryType getBatteryType() + { + return cfg.type; + } + /** + * Set currently active battery type + */ + batteryType setBatteryType(batteryType type) + { + cfg.type = type; + } + + /** + * + */ unsigned long getReadingInterval() { return readingInterval; @@ -561,7 +582,7 @@ class UsermodBattery : public Usermod /** * Set lowest battery voltage - * cant be below 0 volt + * can't be below 0 volt */ void setMinBatteryVoltage(float voltage) { @@ -622,6 +643,24 @@ class UsermodBattery : public Usermod bat->setCalibration(offset); } + /** + * Set the voltage multiplier value + * A multiplier that may need adjusting for different voltage divider setups + */ + void setVoltageMultiplier(float multiplier) + { + bat->setVoltageMultiplier(multiplier); + } + + /* + * Get the voltage multiplier value + * A multiplier that may need adjusting for different voltage divider setups + */ + float getVoltageMultiplier() + { + return bat->getVoltageMultiplier(); + } + /** * Get auto-off feature enabled status * is auto-off enabled, true/false @@ -727,7 +766,7 @@ class UsermodBattery : public Usermod } /** - * Get low-power-indicator status when the indication is done thsi returns true + * Get low-power-indicator status when the indication is done this returns true */ bool getLowPowerIndicatorDone() { From ff10130176ff06a81ed09113b6669703583a3c71 Mon Sep 17 00:00:00 2001 From: Woody Date: Tue, 30 Apr 2024 16:53:47 +0200 Subject: [PATCH 055/148] Fix resizing bug The bug was that when resizing the window, it always jumped to the Colors tab instead of staying on the currently selected tab. --- wled00/data/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/wled00/data/index.js b/wled00/data/index.js index bbf6bd109..9f8c579d0 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -304,7 +304,6 @@ function updateTablinks(tabI) { var tablinks = gEBCN("tablinks"); for (var i of tablinks) i.classList.remove('active'); - if (pcMode) return; tablinks[tabI].classList.add('active'); } @@ -3047,12 +3046,11 @@ function togglePcMode(fromB = false) if (fromB) { pcModeA = !pcModeA; localStorage.setItem('pcm', pcModeA); + openTab(0, true); } pcMode = (wW >= 1024) && pcModeA; if (cpick) cpick.resize(pcMode && wW>1023 && wW<1250 ? 230 : 260); // for tablet in landscape if (!fromB && ((wW < 1024 && lastw < 1024) || (wW >= 1024 && lastw >= 1024))) return; // no change in size and called from size() - openTab(0, true); - updateTablinks(0); gId('buttonPcm').className = (pcMode) ? "active":""; gId('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? "0":"auto"; sCol('--bh', gId('bot').clientHeight + "px"); From 2245ee6fce23343cacfd6cd424a6339be0f1fada Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Tue, 30 Apr 2024 17:02:57 +0200 Subject: [PATCH 056/148] bugfixes --- usermods/Battery/battery.h | 9 +++++---- usermods/Battery/usermod_v2_Battery.h | 18 ++++-------------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/usermods/Battery/battery.h b/usermods/Battery/battery.h index 084e6c0aa..31e2e0755 100644 --- a/usermods/Battery/battery.h +++ b/usermods/Battery/battery.h @@ -27,7 +27,7 @@ class Battery public: Battery() { - this->setVoltage(this->getVoltage()); + this->setVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE); this->setVoltageMultiplier(USERMOD_BATTERY_VOLTAGE_MULTIPLIER); this->setCalibration(USERMOD_BATTERY_CALIBRATION); } @@ -105,9 +105,10 @@ class Battery */ void setVoltage(float voltage) { - this->voltage = ( (voltage < this->getMinVoltage() * 0.85f) || (voltage > this->getMaxVoltage() * 1.1f) ) - ? -1.0f - : voltage; + // this->voltage = ( (voltage < this->getMinVoltage() * 0.85f) || (voltage > this->getMaxVoltage() * 1.1f) ) + // ? -1.0f + // : voltage; + this->voltage = voltage; } float getLevel() diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index b9631d6db..31c31f066 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -28,8 +28,6 @@ class UsermodBattery : public Usermod unsigned long lastReadTime = 0; // between 0 and 1, to control strength of voltage smoothing filter float alpha = USERMOD_BATTERY_AVERAGING_ALPHA; - // offset or calibration value to fine tune the calculated voltage - float calibration = USERMOD_BATTERY_CALIBRATION; // auto shutdown/shutoff/master off feature bool autoOffEnabled = USERMOD_BATTERY_AUTO_OFF_ENABLED; @@ -45,6 +43,7 @@ class UsermodBattery : public Usermod unsigned long lowPowerActivationTime = 0; // used temporary during active time uint8_t lastPreset = 0; + // bool initDone = false; bool initializing = true; @@ -311,9 +310,8 @@ class UsermodBattery : public Usermod { JsonObject battery = root.createNestedObject(FPSTR(_name)); - if (battery.isNull()) { + if (battery.isNull()) battery = root.createNestedObject(FPSTR(_name)); - } addBatteryToJsonObject(battery, true); @@ -460,8 +458,8 @@ class UsermodBattery : public Usermod // calculateTimeLeftEnabled = battery[F("time-left")] | calculateTimeLeftEnabled; setMinBatteryVoltage(battery[F("min-voltage")] | bat->getMinVoltage()); setMaxBatteryVoltage(battery[F("max-voltage")] | bat->getMaxVoltage()); - setCalibration(battery[F("calibration")] | calibration); - setVoltageMultiplier(battery[F("voltage-multiplier")] | voltageMultiplier); + setCalibration(battery[F("calibration")] | bat->getCalibration()); + setVoltageMultiplier(battery[F("voltage-multiplier")] | bat->getVoltageMultiplier()); setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); getUsermodConfigFromJsonObject(battery); @@ -548,14 +546,6 @@ class UsermodBattery : public Usermod return cfg.type; } - /** - * Set currently active battery type - */ - batteryType setBatteryType(batteryType type) - { - cfg.type = type; - } - /** * */ From fd9570e7826b53a0c59440179bd340f195d0bb6a Mon Sep 17 00:00:00 2001 From: Pasquale Pizzuti Date: Tue, 30 Apr 2024 17:52:35 +0200 Subject: [PATCH 057/148] using color_fade --- wled00/overlay.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/wled00/overlay.cpp b/wled00/overlay.cpp index 92d8820e3..cd0c04c04 100644 --- a/wled00/overlay.cpp +++ b/wled00/overlay.cpp @@ -26,11 +26,11 @@ void _overlayAnalogClock() { if (secondPixel < analogClock12pixel) { - strip.setRange(analogClock12pixel, overlayMax, (uint32_t)brightness<<16); - strip.setRange(overlayMin, secondPixel, (uint32_t)brightness<<16); + strip.setRange(analogClock12pixel, overlayMax, color_fade(0xFF0000, brightness)); + strip.setRange(overlayMin, secondPixel, color_fade(0xFF0000, brightness)); } else { - strip.setRange(analogClock12pixel, secondPixel, (uint32_t)brightness<<16); + strip.setRange(analogClock12pixel, secondPixel, color_fade(0xFF0000, brightness)); } } if (analogClock5MinuteMarks) @@ -39,12 +39,12 @@ void _overlayAnalogClock() { unsigned pix = analogClock12pixel + roundf((overlaySize / 12.0f) *i); if (pix > overlayMax) pix -= overlaySize; - strip.setPixelColor(pix, ((uint32_t)brightness<<8)|((uint32_t)brightness*2/3)); + strip.setPixelColor(pix, color_fade(0x00FFAA, brightness)); } } - if (!analogClockSecondsTrail) strip.setPixelColor(secondPixel, (uint32_t)brightness<<16); - strip.setPixelColor(minutePixel, (uint32_t)brightness<<8); - strip.setPixelColor(hourPixel, (uint32_t)brightness); + if (!analogClockSecondsTrail) strip.setPixelColor(secondPixel, color_fade(0xFF0000, brightness)); + strip.setPixelColor(minutePixel, color_fade(0x00FF00, brightness)); + strip.setPixelColor(hourPixel, color_fade(0x0000FF, brightness)); } From 05a8c692f29f2439a96027433ee6440f71a84c7b Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Tue, 30 Apr 2024 18:11:18 +0200 Subject: [PATCH 058/148] read initial voltage correctly --- usermods/Battery/battery.h | 1 - usermods/Battery/usermod_v2_Battery.h | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/usermods/Battery/battery.h b/usermods/Battery/battery.h index 31e2e0755..2ddd84149 100644 --- a/usermods/Battery/battery.h +++ b/usermods/Battery/battery.h @@ -27,7 +27,6 @@ class Battery public: Battery() { - this->setVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE); this->setVoltageMultiplier(USERMOD_BATTERY_VOLTAGE_MULTIPLIER); this->setCalibration(USERMOD_BATTERY_CALIBRATION); } diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index 31c31f066..7b6b038a6 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -150,6 +150,8 @@ class UsermodBattery : public Usermod // update the choosen battery type with configured values bat->update(cfg); + bat->setVoltage(readVoltage()); + nextReadTime = millis() + readingInterval; lastReadTime = millis(); @@ -389,7 +391,7 @@ class UsermodBattery : public Usermod addBatteryToJsonObject(battery, false); // read voltage in case calibration or voltage multiplier changed to see immediate effect - // voltage = readVoltage(); + bat->setVoltage(readVoltage()); DEBUG_PRINTLN(F("Battery config saved.")); } From d2984e9e160f649e0d900761d76240740201a4b6 Mon Sep 17 00:00:00 2001 From: Woody Date: Tue, 30 Apr 2024 18:57:53 +0200 Subject: [PATCH 059/148] add Webpage shortcuts, resolves #2362 --- wled00/data/index.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/wled00/data/index.js b/wled00/data/index.js index 9f8c579d0..8feec9789 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -272,6 +272,7 @@ function onLoad() selectSlot(0); updateTablinks(0); + handleLocationHash(); cpick.on("input:end", () => {setColor(1);}); cpick.on("color:change", () => {updatePSliders()}); pmtLS = localStorage.getItem('wledPmt'); @@ -314,6 +315,21 @@ function openTab(tabI, force = false) _C.classList.toggle('smooth', false); _C.style.setProperty('--i', iSlide); updateTablinks(tabI); + switch (tabI) { + case 0: window.location.hash = "Colors"; break; + case 1: window.location.hash = "Effects"; break; + case 2: window.location.hash = "Segments"; break; + case 3: window.location.hash = "Presets"; break; + } +} + +function handleLocationHash() { + switch (window.location.hash) { + case "#Colors": openTab(0); break; + case "#Effects": openTab(1); break; + case "#Segments": openTab(2); break; + case "#Presets": openTab(3); break; + } } var timeout; @@ -3212,6 +3228,7 @@ size(); _C.style.setProperty('--n', N); window.addEventListener('resize', size, true); +window.addEventListener('hashchange', handleLocationHash); _C.addEventListener('mousedown', lock, false); _C.addEventListener('touchstart', lock, false); From bed364d75e8ecc0f65c43bfb1db4d9a089072158 Mon Sep 17 00:00:00 2001 From: freakintoddles2 Date: Tue, 30 Apr 2024 16:21:40 -0700 Subject: [PATCH 060/148] Update playlist.cpp Updated to allow a user to optionally skip to the next preset in the playlist anytime they desire. --- wled00/playlist.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/playlist.cpp b/wled00/playlist.cpp index 67c4f6049..0f6f5745b 100644 --- a/wled00/playlist.cpp +++ b/wled00/playlist.cpp @@ -123,11 +123,11 @@ int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { } -void handlePlaylist() { +void handlePlaylist(bool skipNext) { static unsigned long presetCycledTime = 0; if (currentPlaylist < 0 || playlistEntries == nullptr) return; - if (millis() - presetCycledTime > (100*playlistEntryDur)) { +if (millis() - presetCycledTime > (100 * playlistEntryDur) || skipNext) { presetCycledTime = millis(); if (bri == 0 || nightlightActive) return; From 071e0be445ed012ff9ab1f72347440684eed678f Mon Sep 17 00:00:00 2001 From: freakintoddles2 Date: Tue, 30 Apr 2024 16:23:43 -0700 Subject: [PATCH 061/148] Update fcn_declare.h Updated to add the optional skipNext bool to handlePlaylist() which allows people to skip to the next preset when desired --- wled00/fcn_declare.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 2461ebb28..d77bdd8f1 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -226,7 +226,7 @@ void _overlayAnalogClock(); void shufflePlaylist(); void unloadPlaylist(); int16_t loadPlaylist(JsonObject playlistObject, byte presetId = 0); -void handlePlaylist(); +void handlePlaylist(bool skipNext=false); void serializePlaylist(JsonObject obj); //presets.cpp From 3b89814b6935261d65f95059690591f51a0eab5f Mon Sep 17 00:00:00 2001 From: freakintoddles2 Date: Tue, 30 Apr 2024 16:33:30 -0700 Subject: [PATCH 062/148] Update set.cpp added new NP command to API to allow user to skip to next preset in a playlist. Example use is win&NP in a preset. --- wled00/set.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wled00/set.cpp b/wled00/set.cpp index d3382be18..8a00a9814 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -901,6 +901,9 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) applyPreset(presetCycCurr); } + pos = req.indexOf(F("NP")); //skips to next preset in a playlist + if (pos > 0) handlePlaylist(true); + //set brightness updateVal(req.c_str(), "&A=", &bri); From a1d6ffadad852449a825431ad0bb8bba2f7f448d Mon Sep 17 00:00:00 2001 From: freakintoddles2 Date: Tue, 30 Apr 2024 16:49:52 -0700 Subject: [PATCH 063/148] Update json.cpp adds support for np boolean parameter to skip to next preset --- wled00/json.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index ae8224ad3..866fa968f 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -486,7 +486,11 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) strip.loadCustomPalettes(); } } - + + if (root.containsKey(F("np")) && root[F("np")].as()) { //skip to next preset in a playlist + handlePlaylist(true); + } + JsonObject wifi = root[F("wifi")]; if (!wifi.isNull()) { bool apMode = getBoolVal(wifi[F("ap")], apActive); From 25fb878e5434dd47b888bd3e5a46176c369ff8c4 Mon Sep 17 00:00:00 2001 From: freakintoddles2 Date: Wed, 1 May 2024 10:01:30 -0700 Subject: [PATCH 064/148] Update fcn_declare.h reworked approach based on feedback --- wled00/fcn_declare.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index d77bdd8f1..2818ada30 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -226,7 +226,7 @@ void _overlayAnalogClock(); void shufflePlaylist(); void unloadPlaylist(); int16_t loadPlaylist(JsonObject playlistObject, byte presetId = 0); -void handlePlaylist(bool skipNext=false); +void handlePlaylist(bool doAdvancePlaylist = false); void serializePlaylist(JsonObject obj); //presets.cpp From caa4fe1ec4f814f89fa2c77ef7405b0935b846ec Mon Sep 17 00:00:00 2001 From: freakintoddles2 Date: Wed, 1 May 2024 10:02:27 -0700 Subject: [PATCH 065/148] Update json.cpp reworked approach based on feedback --- wled00/json.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index 866fa968f..01cbeddb1 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -488,7 +488,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } if (root.containsKey(F("np")) && root[F("np")].as()) { //skip to next preset in a playlist - handlePlaylist(true); + doAdvancePlaylist = true; } JsonObject wifi = root[F("wifi")]; From a2b9aed40df5bb55eb4f53db72c6871c72c9b30d Mon Sep 17 00:00:00 2001 From: freakintoddles2 Date: Wed, 1 May 2024 10:03:16 -0700 Subject: [PATCH 066/148] Update playlist.cpp reworked approach based on feedback --- wled00/playlist.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/playlist.cpp b/wled00/playlist.cpp index 0f6f5745b..5665ef72f 100644 --- a/wled00/playlist.cpp +++ b/wled00/playlist.cpp @@ -123,7 +123,7 @@ int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { } -void handlePlaylist(bool skipNext) { +void handlePlaylist(bool doAdvancePlaylist) { static unsigned long presetCycledTime = 0; if (currentPlaylist < 0 || playlistEntries == nullptr) return; From e88c81ad0d6a1ff8c18667facd2b7b326ede2b74 Mon Sep 17 00:00:00 2001 From: freakintoddles2 Date: Wed, 1 May 2024 10:04:02 -0700 Subject: [PATCH 067/148] Update set.cpp reworked based on feedback --- wled00/set.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/set.cpp b/wled00/set.cpp index 8a00a9814..0b4a0da3f 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -902,7 +902,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) } pos = req.indexOf(F("NP")); //skips to next preset in a playlist - if (pos > 0) handlePlaylist(true); + if (pos > 0) doAdvancePlaylist = true; //set brightness updateVal(req.c_str(), "&A=", &bri); From 16086c09615d7e8c2e2050c650b9af00b875062c Mon Sep 17 00:00:00 2001 From: freakintoddles2 Date: Wed, 1 May 2024 10:05:26 -0700 Subject: [PATCH 068/148] Update wled.h reworked based on feedback from original PR --- wled00/wled.h | 1 + 1 file changed, 1 insertion(+) diff --git a/wled00/wled.h b/wled00/wled.h index 139c451f8..75f7c14c5 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -645,6 +645,7 @@ WLED_GLOBAL byte timerWeekday[] _INIT_N(({ 255, 255, 255, 255, 255, 255, 255, WLED_GLOBAL byte timerMonth[] _INIT_N(({28,28,28,28,28,28,28,28})); WLED_GLOBAL byte timerDay[] _INIT_N(({1,1,1,1,1,1,1,1})); WLED_GLOBAL byte timerDayEnd[] _INIT_N(({31,31,31,31,31,31,31,31})); +WLED_GLOBAL bool doAdvancePlaylist _INIT(false); //improv WLED_GLOBAL byte improvActive _INIT(0); //0: no improv packet received, 1: improv active, 2: provisioning From 6daf7f6322eacdc0ff83756b4da735d02a269fa9 Mon Sep 17 00:00:00 2001 From: freakintoddles2 Date: Wed, 1 May 2024 10:07:52 -0700 Subject: [PATCH 069/148] Update wled.cpp reworked based on PR feedback --- wled00/wled.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/wled00/wled.cpp b/wled00/wled.cpp index eb7860851..25cc0442c 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -209,6 +209,12 @@ void WLED::loop() toki.resetTick(); +// Advance to next playlist preset if the flag is set to true + if (doAdvancePlaylist) { + handlePlaylist(true); + doAdvancePlaylist = false; // Reset flag to false + } + #if WLED_WATCHDOG_TIMEOUT > 0 // we finished our mainloop, reset the watchdog timer static unsigned long lastWDTFeed = 0; From db475b69988567f19c3969d1808c96918924f55e Mon Sep 17 00:00:00 2001 From: freakintoddles2 Date: Wed, 1 May 2024 10:09:17 -0700 Subject: [PATCH 070/148] Update playlist.cpp --- wled00/playlist.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/playlist.cpp b/wled00/playlist.cpp index 5665ef72f..fc39db42b 100644 --- a/wled00/playlist.cpp +++ b/wled00/playlist.cpp @@ -127,7 +127,7 @@ void handlePlaylist(bool doAdvancePlaylist) { static unsigned long presetCycledTime = 0; if (currentPlaylist < 0 || playlistEntries == nullptr) return; -if (millis() - presetCycledTime > (100 * playlistEntryDur) || skipNext) { +if (millis() - presetCycledTime > (100 * playlistEntryDur) || doAdvancePlaylist) { presetCycledTime = millis(); if (bri == 0 || nightlightActive) return; From 22f6128bc47c7ee7349b4f039758bf46962b0eba Mon Sep 17 00:00:00 2001 From: Pasquale Pizzuti Date: Thu, 2 May 2024 09:04:07 +0200 Subject: [PATCH 071/148] using global brightness --- wled00/overlay.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/wled00/overlay.cpp b/wled00/overlay.cpp index cd0c04c04..d6d8ba52a 100644 --- a/wled00/overlay.cpp +++ b/wled00/overlay.cpp @@ -11,7 +11,6 @@ void _overlayAnalogClock() { _overlayAnalogCountdown(); return; } - uint8_t brightness = strip.getBrightness(); float hourP = ((float)(hour(localTime)%12))/12.0f; float minuteP = ((float)minute(localTime))/60.0f; hourP = hourP + minuteP/12.0f; @@ -26,11 +25,11 @@ void _overlayAnalogClock() { if (secondPixel < analogClock12pixel) { - strip.setRange(analogClock12pixel, overlayMax, color_fade(0xFF0000, brightness)); - strip.setRange(overlayMin, secondPixel, color_fade(0xFF0000, brightness)); + strip.setRange(analogClock12pixel, overlayMax, color_fade(0xFF0000, bri)); + strip.setRange(overlayMin, secondPixel, color_fade(0xFF0000, bri)); } else { - strip.setRange(analogClock12pixel, secondPixel, color_fade(0xFF0000, brightness)); + strip.setRange(analogClock12pixel, secondPixel, color_fade(0xFF0000, bri)); } } if (analogClock5MinuteMarks) @@ -39,12 +38,12 @@ void _overlayAnalogClock() { unsigned pix = analogClock12pixel + roundf((overlaySize / 12.0f) *i); if (pix > overlayMax) pix -= overlaySize; - strip.setPixelColor(pix, color_fade(0x00FFAA, brightness)); + strip.setPixelColor(pix, color_fade(0x00FFAA, bri)); } } - if (!analogClockSecondsTrail) strip.setPixelColor(secondPixel, color_fade(0xFF0000, brightness)); - strip.setPixelColor(minutePixel, color_fade(0x00FF00, brightness)); - strip.setPixelColor(hourPixel, color_fade(0x0000FF, brightness)); + if (!analogClockSecondsTrail) strip.setPixelColor(secondPixel, color_fade(0xFF0000, bri)); + strip.setPixelColor(minutePixel, color_fade(0x00FF00, bri)); + strip.setPixelColor(hourPixel, color_fade(0x0000FF, bri)); } From 736a8b1b80102d2f9b32b8cc88ce5f409eeca116 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Thu, 2 May 2024 10:31:50 +0200 Subject: [PATCH 072/148] Fix for rotating tablet into PC mode. --- wled00/data/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/data/index.js b/wled00/data/index.js index 8feec9789..d33fb63f7 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -3062,11 +3062,11 @@ function togglePcMode(fromB = false) if (fromB) { pcModeA = !pcModeA; localStorage.setItem('pcm', pcModeA); - openTab(0, true); } pcMode = (wW >= 1024) && pcModeA; if (cpick) cpick.resize(pcMode && wW>1023 && wW<1250 ? 230 : 260); // for tablet in landscape if (!fromB && ((wW < 1024 && lastw < 1024) || (wW >= 1024 && lastw >= 1024))) return; // no change in size and called from size() + if (pcMode) openTab(0, true); gId('buttonPcm').className = (pcMode) ? "active":""; gId('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? "0":"auto"; sCol('--bh', gId('bot').clientHeight + "px"); From 4df936a437a2e643fca724885b336b6bb3034bbd Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Thu, 2 May 2024 10:33:10 +0200 Subject: [PATCH 073/148] Fix for unfortunate prior CRLF coversion. --- wled00/mqtt.cpp | 394 ++++++++++++++++++++++++------------------------ 1 file changed, 197 insertions(+), 197 deletions(-) diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index 810291094..2e2e4a6bd 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -1,197 +1,197 @@ -#include "wled.h" - -/* - * MQTT communication protocol for home automation - */ - -#ifdef WLED_ENABLE_MQTT -#define MQTT_KEEP_ALIVE_TIME 60 // contact the MQTT broker every 60 seconds - -void parseMQTTBriPayload(char* payload) -{ - if (strstr(payload, "ON") || strstr(payload, "on") || strstr(payload, "true")) {bri = briLast; stateUpdated(CALL_MODE_DIRECT_CHANGE);} - else if (strstr(payload, "T" ) || strstr(payload, "t" )) {toggleOnOff(); stateUpdated(CALL_MODE_DIRECT_CHANGE);} - else { - uint8_t in = strtoul(payload, NULL, 10); - if (in == 0 && bri > 0) briLast = bri; - bri = in; - stateUpdated(CALL_MODE_DIRECT_CHANGE); - } -} - - -void onMqttConnect(bool sessionPresent) -{ - //(re)subscribe to required topics - char subuf[38]; - - if (mqttDeviceTopic[0] != 0) { - strlcpy(subuf, mqttDeviceTopic, 33); - mqtt->subscribe(subuf, 0); - strcat_P(subuf, PSTR("/col")); - mqtt->subscribe(subuf, 0); - strlcpy(subuf, mqttDeviceTopic, 33); - strcat_P(subuf, PSTR("/api")); - mqtt->subscribe(subuf, 0); - } - - if (mqttGroupTopic[0] != 0) { - strlcpy(subuf, mqttGroupTopic, 33); - mqtt->subscribe(subuf, 0); - strcat_P(subuf, PSTR("/col")); - mqtt->subscribe(subuf, 0); - strlcpy(subuf, mqttGroupTopic, 33); - strcat_P(subuf, PSTR("/api")); - mqtt->subscribe(subuf, 0); - } - - usermods.onMqttConnect(sessionPresent); - - DEBUG_PRINTLN(F("MQTT ready")); - publishMqtt(); -} - - -void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { - static char *payloadStr; - - DEBUG_PRINT(F("MQTT msg: ")); - DEBUG_PRINTLN(topic); - - // paranoia check to avoid npe if no payload - if (payload==nullptr) { - DEBUG_PRINTLN(F("no payload -> leave")); - return; - } - - if (index == 0) { // start (1st partial packet or the only packet) - if (payloadStr) delete[] payloadStr; // fail-safe: release buffer - payloadStr = new char[total+1]; // allocate new buffer - } - if (payloadStr == nullptr) return; // buffer not allocated - - // copy (partial) packet to buffer and 0-terminate it if it is last packet - char* buff = payloadStr + index; - memcpy(buff, payload, len); - if (index + len >= total) { // at end - payloadStr[total] = '\0'; // terminate c style string - } else { - DEBUG_PRINTLN(F("MQTT partial packet received.")); - return; // process next packet - } - DEBUG_PRINTLN(payloadStr); - - size_t topicPrefixLen = strlen(mqttDeviceTopic); - if (strncmp(topic, mqttDeviceTopic, topicPrefixLen) == 0) { - topic += topicPrefixLen; - } else { - topicPrefixLen = strlen(mqttGroupTopic); - if (strncmp(topic, mqttGroupTopic, topicPrefixLen) == 0) { - topic += topicPrefixLen; - } else { - // Non-Wled Topic used here. Probably a usermod subscribed to this topic. - usermods.onMqttMessage(topic, payloadStr); - delete[] payloadStr; - payloadStr = nullptr; - return; - } - } - - //Prefix is stripped from the topic at this point - - if (strcmp_P(topic, PSTR("/col")) == 0) { - colorFromDecOrHexString(col, payloadStr); - colorUpdated(CALL_MODE_DIRECT_CHANGE); - } else if (strcmp_P(topic, PSTR("/api")) == 0) { - if (!requestJSONBufferLock(15)) { - delete[] payloadStr; - payloadStr = nullptr; - return; - } - if (payloadStr[0] == '{') { //JSON API - deserializeJson(*pDoc, payloadStr); - deserializeState(pDoc->as()); - } else { //HTTP API - String apireq = "win"; apireq += '&'; // reduce flash string usage - apireq += payloadStr; - handleSet(nullptr, apireq); - } - releaseJSONBufferLock(); - } else if (strlen(topic) != 0) { - // non standard topic, check with usermods - usermods.onMqttMessage(topic, payloadStr); - } else { - // topmost topic (just wled/MAC) - parseMQTTBriPayload(payloadStr); - } - delete[] payloadStr; - payloadStr = nullptr; -} - - -void publishMqtt() -{ - if (!WLED_MQTT_CONNECTED) return; - DEBUG_PRINTLN(F("Publish MQTT")); - - #ifndef USERMOD_SMARTNEST - char s[10]; - char subuf[48]; - - sprintf_P(s, PSTR("%u"), bri); - strlcpy(subuf, mqttDeviceTopic, 33); - strcat_P(subuf, PSTR("/g")); - mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263) - - sprintf_P(s, PSTR("#%06X"), (col[3] << 24) | (col[0] << 16) | (col[1] << 8) | (col[2])); - strlcpy(subuf, mqttDeviceTopic, 33); - strcat_P(subuf, PSTR("/c")); - mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263) - - strlcpy(subuf, mqttDeviceTopic, 33); - strcat_P(subuf, PSTR("/status")); - mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT - - char apires[1024]; // allocating 1024 bytes from stack can be risky - XML_response(nullptr, apires); - strlcpy(subuf, mqttDeviceTopic, 33); - strcat_P(subuf, PSTR("/v")); - mqtt->publish(subuf, 0, retainMqttMsg, apires); // optionally retain message (#2263) - #endif -} - - -//HA autodiscovery was removed in favor of the native integration in HA v0.102.0 - -bool initMqtt() -{ - if (!mqttEnabled || mqttServer[0] == 0 || !WLED_CONNECTED) return false; - - if (mqtt == nullptr) { - mqtt = new AsyncMqttClient(); - mqtt->onMessage(onMqttMessage); - mqtt->onConnect(onMqttConnect); - } - if (mqtt->connected()) return true; - - DEBUG_PRINTLN(F("Reconnecting MQTT")); - IPAddress mqttIP; - if (mqttIP.fromString(mqttServer)) //see if server is IP or domain - { - mqtt->setServer(mqttIP, mqttPort); - } else { - mqtt->setServer(mqttServer, mqttPort); - } - mqtt->setClientId(mqttClientID); - if (mqttUser[0] && mqttPass[0]) mqtt->setCredentials(mqttUser, mqttPass); - - #ifndef USERMOD_SMARTNEST - strlcpy(mqttStatusTopic, mqttDeviceTopic, 33); - strcat_P(mqttStatusTopic, PSTR("/status")); - mqtt->setWill(mqttStatusTopic, 0, true, "offline"); // LWT message - #endif - mqtt->setKeepAlive(MQTT_KEEP_ALIVE_TIME); - mqtt->connect(); - return true; -} -#endif +#include "wled.h" + +/* + * MQTT communication protocol for home automation + */ + +#ifdef WLED_ENABLE_MQTT +#define MQTT_KEEP_ALIVE_TIME 60 // contact the MQTT broker every 60 seconds + +void parseMQTTBriPayload(char* payload) +{ + if (strstr(payload, "ON") || strstr(payload, "on") || strstr(payload, "true")) {bri = briLast; stateUpdated(CALL_MODE_DIRECT_CHANGE);} + else if (strstr(payload, "T" ) || strstr(payload, "t" )) {toggleOnOff(); stateUpdated(CALL_MODE_DIRECT_CHANGE);} + else { + uint8_t in = strtoul(payload, NULL, 10); + if (in == 0 && bri > 0) briLast = bri; + bri = in; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + } +} + + +void onMqttConnect(bool sessionPresent) +{ + //(re)subscribe to required topics + char subuf[38]; + + if (mqttDeviceTopic[0] != 0) { + strlcpy(subuf, mqttDeviceTopic, 33); + mqtt->subscribe(subuf, 0); + strcat_P(subuf, PSTR("/col")); + mqtt->subscribe(subuf, 0); + strlcpy(subuf, mqttDeviceTopic, 33); + strcat_P(subuf, PSTR("/api")); + mqtt->subscribe(subuf, 0); + } + + if (mqttGroupTopic[0] != 0) { + strlcpy(subuf, mqttGroupTopic, 33); + mqtt->subscribe(subuf, 0); + strcat_P(subuf, PSTR("/col")); + mqtt->subscribe(subuf, 0); + strlcpy(subuf, mqttGroupTopic, 33); + strcat_P(subuf, PSTR("/api")); + mqtt->subscribe(subuf, 0); + } + + usermods.onMqttConnect(sessionPresent); + + DEBUG_PRINTLN(F("MQTT ready")); + publishMqtt(); +} + + +void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { + static char *payloadStr; + + DEBUG_PRINT(F("MQTT msg: ")); + DEBUG_PRINTLN(topic); + + // paranoia check to avoid npe if no payload + if (payload==nullptr) { + DEBUG_PRINTLN(F("no payload -> leave")); + return; + } + + if (index == 0) { // start (1st partial packet or the only packet) + if (payloadStr) delete[] payloadStr; // fail-safe: release buffer + payloadStr = new char[total+1]; // allocate new buffer + } + if (payloadStr == nullptr) return; // buffer not allocated + + // copy (partial) packet to buffer and 0-terminate it if it is last packet + char* buff = payloadStr + index; + memcpy(buff, payload, len); + if (index + len >= total) { // at end + payloadStr[total] = '\0'; // terminate c style string + } else { + DEBUG_PRINTLN(F("MQTT partial packet received.")); + return; // process next packet + } + DEBUG_PRINTLN(payloadStr); + + size_t topicPrefixLen = strlen(mqttDeviceTopic); + if (strncmp(topic, mqttDeviceTopic, topicPrefixLen) == 0) { + topic += topicPrefixLen; + } else { + topicPrefixLen = strlen(mqttGroupTopic); + if (strncmp(topic, mqttGroupTopic, topicPrefixLen) == 0) { + topic += topicPrefixLen; + } else { + // Non-Wled Topic used here. Probably a usermod subscribed to this topic. + usermods.onMqttMessage(topic, payloadStr); + delete[] payloadStr; + payloadStr = nullptr; + return; + } + } + + //Prefix is stripped from the topic at this point + + if (strcmp_P(topic, PSTR("/col")) == 0) { + colorFromDecOrHexString(col, payloadStr); + colorUpdated(CALL_MODE_DIRECT_CHANGE); + } else if (strcmp_P(topic, PSTR("/api")) == 0) { + if (!requestJSONBufferLock(15)) { + delete[] payloadStr; + payloadStr = nullptr; + return; + } + if (payloadStr[0] == '{') { //JSON API + deserializeJson(*pDoc, payloadStr); + deserializeState(pDoc->as()); + } else { //HTTP API + String apireq = "win"; apireq += '&'; // reduce flash string usage + apireq += payloadStr; + handleSet(nullptr, apireq); + } + releaseJSONBufferLock(); + } else if (strlen(topic) != 0) { + // non standard topic, check with usermods + usermods.onMqttMessage(topic, payloadStr); + } else { + // topmost topic (just wled/MAC) + parseMQTTBriPayload(payloadStr); + } + delete[] payloadStr; + payloadStr = nullptr; +} + + +void publishMqtt() +{ + if (!WLED_MQTT_CONNECTED) return; + DEBUG_PRINTLN(F("Publish MQTT")); + + #ifndef USERMOD_SMARTNEST + char s[10]; + char subuf[48]; + + sprintf_P(s, PSTR("%u"), bri); + strlcpy(subuf, mqttDeviceTopic, 33); + strcat_P(subuf, PSTR("/g")); + mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263) + + sprintf_P(s, PSTR("#%06X"), (col[3] << 24) | (col[0] << 16) | (col[1] << 8) | (col[2])); + strlcpy(subuf, mqttDeviceTopic, 33); + strcat_P(subuf, PSTR("/c")); + mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263) + + strlcpy(subuf, mqttDeviceTopic, 33); + strcat_P(subuf, PSTR("/status")); + mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT + + char apires[1024]; // allocating 1024 bytes from stack can be risky + XML_response(nullptr, apires); + strlcpy(subuf, mqttDeviceTopic, 33); + strcat_P(subuf, PSTR("/v")); + mqtt->publish(subuf, 0, retainMqttMsg, apires); // optionally retain message (#2263) + #endif +} + + +//HA autodiscovery was removed in favor of the native integration in HA v0.102.0 + +bool initMqtt() +{ + if (!mqttEnabled || mqttServer[0] == 0 || !WLED_CONNECTED) return false; + + if (mqtt == nullptr) { + mqtt = new AsyncMqttClient(); + mqtt->onMessage(onMqttMessage); + mqtt->onConnect(onMqttConnect); + } + if (mqtt->connected()) return true; + + DEBUG_PRINTLN(F("Reconnecting MQTT")); + IPAddress mqttIP; + if (mqttIP.fromString(mqttServer)) //see if server is IP or domain + { + mqtt->setServer(mqttIP, mqttPort); + } else { + mqtt->setServer(mqttServer, mqttPort); + } + mqtt->setClientId(mqttClientID); + if (mqttUser[0] && mqttPass[0]) mqtt->setCredentials(mqttUser, mqttPass); + + #ifndef USERMOD_SMARTNEST + strlcpy(mqttStatusTopic, mqttDeviceTopic, 33); + strcat_P(mqttStatusTopic, PSTR("/status")); + mqtt->setWill(mqttStatusTopic, 0, true, "offline"); // LWT message + #endif + mqtt->setKeepAlive(MQTT_KEEP_ALIVE_TIME); + mqtt->connect(); + return true; +} +#endif From 5e38039c4dd630d7b4c6841deb5e0b04aa07f573 Mon Sep 17 00:00:00 2001 From: Todd Meyer Date: Thu, 2 May 2024 14:36:18 -0700 Subject: [PATCH 074/148] Updated based on more feedback --- wled00/fcn_declare.h | 2 +- wled00/json.cpp | 6 +++--- wled00/playlist.cpp | 3 ++- wled00/set.cpp | 4 ++-- wled00/wled.cpp | 6 ------ 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 2818ada30..2461ebb28 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -226,7 +226,7 @@ void _overlayAnalogClock(); void shufflePlaylist(); void unloadPlaylist(); int16_t loadPlaylist(JsonObject playlistObject, byte presetId = 0); -void handlePlaylist(bool doAdvancePlaylist = false); +void handlePlaylist(); void serializePlaylist(JsonObject obj); //presets.cpp diff --git a/wled00/json.cpp b/wled00/json.cpp index 01cbeddb1..d998a462b 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -486,9 +486,9 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) strip.loadCustomPalettes(); } } - - if (root.containsKey(F("np")) && root[F("np")].as()) { //skip to next preset in a playlist - doAdvancePlaylist = true; + + if (root.containsKey(F("np"))) { + doAdvancePlaylist = root[F("np")].as(); //advances to next preset in playlist when true } JsonObject wifi = root[F("wifi")]; diff --git a/wled00/playlist.cpp b/wled00/playlist.cpp index fc39db42b..36235ab9e 100644 --- a/wled00/playlist.cpp +++ b/wled00/playlist.cpp @@ -123,7 +123,7 @@ int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { } -void handlePlaylist(bool doAdvancePlaylist) { +void handlePlaylist() { static unsigned long presetCycledTime = 0; if (currentPlaylist < 0 || playlistEntries == nullptr) return; @@ -149,6 +149,7 @@ if (millis() - presetCycledTime > (100 * playlistEntryDur) || doAdvancePlaylist) strip.setTransition(fadeTransition ? playlistEntries[playlistIndex].tr * 100 : 0); playlistEntryDur = playlistEntries[playlistIndex].dur; applyPresetFromPlaylist(playlistEntries[playlistIndex].preset); + doAdvancePlaylist = false; } } diff --git a/wled00/set.cpp b/wled00/set.cpp index 0b4a0da3f..efbc7b18b 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -901,8 +901,8 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) applyPreset(presetCycCurr); } - pos = req.indexOf(F("NP")); //skips to next preset in a playlist - if (pos > 0) doAdvancePlaylist = true; + pos = req.indexOf(F("NP")); //advances to next preset in a playlist + if (pos > 0) doAdvancePlaylist = true; //set brightness updateVal(req.c_str(), "&A=", &bri); diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 25cc0442c..eb7860851 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -209,12 +209,6 @@ void WLED::loop() toki.resetTick(); -// Advance to next playlist preset if the flag is set to true - if (doAdvancePlaylist) { - handlePlaylist(true); - doAdvancePlaylist = false; // Reset flag to false - } - #if WLED_WATCHDOG_TIMEOUT > 0 // we finished our mainloop, reset the watchdog timer static unsigned long lastWDTFeed = 0; From 2ff49cf657a322d17ec4fab854d9834ae10a5566 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Fri, 3 May 2024 15:45:15 +0200 Subject: [PATCH 075/148] Fix for #3952 - included IR optimisations & code rearrangement --- wled00/fcn_declare.h | 14 +-- wled00/ir.cpp | 254 ++++++++++++++++++++----------------------- wled00/set.cpp | 4 +- wled00/wled.cpp | 7 ++ 4 files changed, 130 insertions(+), 149 deletions(-) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 2461ebb28..a6ff9d096 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -143,20 +143,8 @@ void handleImprovWifiScan(); void sendImprovIPRPCResult(ImprovRPCType type); //ir.cpp -void applyRepeatActions(); -byte relativeChange(byte property, int8_t amount, byte lowerBoundary = 0, byte higherBoundary = 0xFF); -void decodeIR(uint32_t code); -void decodeIR24(uint32_t code); -void decodeIR24OLD(uint32_t code); -void decodeIR24CT(uint32_t code); -void decodeIR40(uint32_t code); -void decodeIR44(uint32_t code); -void decodeIR21(uint32_t code); -void decodeIR6(uint32_t code); -void decodeIR9(uint32_t code); -void decodeIRJson(uint32_t code); - void initIR(); +void deInitIR(); void handleIR(); //json.cpp diff --git a/wled00/ir.cpp b/wled00/ir.cpp index ba34aa526..e475198f6 100644 --- a/wled00/ir.cpp +++ b/wled00/ir.cpp @@ -1,20 +1,14 @@ #include "wled.h" +#ifndef WLED_DISABLE_INFRARED #include "ir_codes.h" /* - * Infrared sensor support for generic 24/40/44 key RGB remotes + * Infrared sensor support for several generic RGB remotes and custom JSON remote */ -#if defined(WLED_DISABLE_INFRARED) -void handleIR(){} -#else - IRrecv* irrecv; -//change pin in NpbWrapper.h - decode_results results; - unsigned long irCheckedTime = 0; uint32_t lastValidCode = 0; byte lastRepeatableAction = ACTION_NONE; @@ -35,16 +29,16 @@ uint8_t lastIR6ColourIdx = 0; // print("%d values: %s" % (len(result), result)) // // It would be hard to maintain repeatable steps if calculating this on the fly. -const byte brightnessSteps[] = { +const uint8_t brightnessSteps[] = { 5, 7, 9, 12, 16, 20, 26, 34, 43, 56, 72, 93, 119, 154, 198, 255 }; const size_t numBrightnessSteps = sizeof(brightnessSteps) / sizeof(uint8_t); // increment `bri` to the next `brightnessSteps` value -void incBrightness() +static void incBrightness() { // dumb incremental search is efficient enough for so few items - for (uint8_t index = 0; index < numBrightnessSteps; ++index) + for (unsigned index = 0; index < numBrightnessSteps; ++index) { if (brightnessSteps[index] > bri) { @@ -56,7 +50,7 @@ void incBrightness() } // decrement `bri` to the next `brightnessSteps` value -void decBrightness() +static void decBrightness() { // dumb incremental search is efficient enough for so few items for (int index = numBrightnessSteps - 1; index >= 0; --index) @@ -70,12 +64,12 @@ void decBrightness() } } -void presetFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) +static void presetFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) { applyPresetWithFallback(presetID, CALL_MODE_BUTTON_PRESET, effectID, paletteID); } -byte relativeChange(byte property, int8_t amount, byte lowerBoundary, byte higherBoundary) +static byte relativeChange(byte property, int8_t amount, byte lowerBoundary = 0, byte higherBoundary = 0xFF) { int16_t new_val = (int16_t) property + amount; if (lowerBoundary >= higherBoundary) return property; @@ -84,10 +78,10 @@ byte relativeChange(byte property, int8_t amount, byte lowerBoundary, byte highe return (byte)constrain(new_val, 0, 255); } -void changeEffect(uint8_t fx) +static void changeEffect(uint8_t fx) { if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; strip.setMode(i, fx); @@ -100,10 +94,10 @@ void changeEffect(uint8_t fx) stateChanged = true; } -void changePalette(uint8_t pal) +static void changePalette(uint8_t pal) { if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.setPalette(pal); @@ -116,13 +110,13 @@ void changePalette(uint8_t pal) stateChanged = true; } -void changeEffectSpeed(int8_t amount) +static void changeEffectSpeed(int8_t amount) { if (effectCurrent != 0) { int16_t new_val = (int16_t) effectSpeed + amount; effectSpeed = (byte)constrain(new_val,0,255); if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.speed = effectSpeed; @@ -134,10 +128,7 @@ void changeEffectSpeed(int8_t amount) } } else { // if Effect == "solid Color", change the hue of the primary color Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); - CRGB fastled_col; - fastled_col.red = R(sseg.colors[0]); - fastled_col.green = G(sseg.colors[0]); - fastled_col.blue = B(sseg.colors[0]); + CRGB fastled_col = CRGB(sseg.colors[0]); CHSV prim_hsv = rgb2hsv_approximate(fastled_col); int16_t new_val = (int16_t)prim_hsv.h + amount; if (new_val > 255) new_val -= 255; // roll-over if bigger than 255 @@ -145,7 +136,7 @@ void changeEffectSpeed(int8_t amount) prim_hsv.h = (byte)new_val; hsv2rgb_rainbow(prim_hsv, fastled_col); if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.colors[0] = RGBW32(fastled_col.red, fastled_col.green, fastled_col.blue, W(sseg.colors[0])); @@ -163,13 +154,13 @@ void changeEffectSpeed(int8_t amount) lastRepeatableValue = amount; } -void changeEffectIntensity(int8_t amount) +static void changeEffectIntensity(int8_t amount) { if (effectCurrent != 0) { int16_t new_val = (int16_t) effectIntensity + amount; effectIntensity = (byte)constrain(new_val,0,255); if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.intensity = effectIntensity; @@ -181,16 +172,13 @@ void changeEffectIntensity(int8_t amount) } } else { // if Effect == "solid Color", change the saturation of the primary color Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); - CRGB fastled_col; - fastled_col.red = R(sseg.colors[0]); - fastled_col.green = G(sseg.colors[0]); - fastled_col.blue = B(sseg.colors[0]); + CRGB fastled_col = CRGB(sseg.colors[0]); CHSV prim_hsv = rgb2hsv_approximate(fastled_col); int16_t new_val = (int16_t) prim_hsv.s + amount; prim_hsv.s = (byte)constrain(new_val,0,255); // constrain to 0-255 hsv2rgb_rainbow(prim_hsv, fastled_col); if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.colors[0] = RGBW32(fastled_col.red, fastled_col.green, fastled_col.blue, W(sseg.colors[0])); @@ -208,11 +196,11 @@ void changeEffectIntensity(int8_t amount) lastRepeatableValue = amount; } -void changeColor(uint32_t c, int16_t cct=-1) +static void changeColor(uint32_t c, int16_t cct=-1) { if (irApplyToAllSelected) { // main segment may not be selected! - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; byte capabilities = seg.getLightCapabilities(); @@ -249,7 +237,7 @@ void changeColor(uint32_t c, int16_t cct=-1) stateChanged = true; } -void changeWhite(int8_t amount, int16_t cct=-1) +static void changeWhite(int8_t amount, int16_t cct=-1) { Segment& seg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); byte r = R(seg.colors[0]); @@ -259,72 +247,7 @@ void changeWhite(int8_t amount, int16_t cct=-1) changeColor(RGBW32(r, g, b, w), cct); } -void decodeIR(uint32_t code) -{ - if (code == 0xFFFFFFFF) { - //repeated code, continue brightness up/down - irTimesRepeated++; - applyRepeatActions(); - return; - } - lastValidCode = 0; irTimesRepeated = 0; - lastRepeatableAction = ACTION_NONE; - - if (irEnabled == 8) { // any remote configurable with ir.json file - decodeIRJson(code); - stateUpdated(CALL_MODE_BUTTON); - return; - } - if (code > 0xFFFFFF) return; //invalid code - - switch (irEnabled) { - case 1: - if (code > 0xF80000) decodeIR24OLD(code); // white 24-key remote (old) - it sends 0xFF0000 values - else decodeIR24(code); // 24-key remote - 0xF70000 to 0xF80000 - break; - case 2: decodeIR24CT(code); break; // white 24-key remote with CW, WW, CT+ and CT- keys - case 3: decodeIR40(code); break; // blue 40-key remote with 25%, 50%, 75% and 100% keys - case 4: decodeIR44(code); break; // white 44-key remote with color-up/down keys and DIY1 to 6 keys - case 5: decodeIR21(code); break; // white 21-key remote - case 6: decodeIR6(code); break; // black 6-key learning remote defaults: "CH" controls brightness, - // "VOL +" controls effect, "VOL -" controls colour/palette, "MUTE" - // sets bright plain white - case 7: decodeIR9(code); break; - //case 8: return; // ir.json file, handled above switch statement - } - - if (nightlightActive && bri == 0) nightlightActive = false; - stateUpdated(CALL_MODE_BUTTON); //for notifier, IR is considered a button input -} - -void applyRepeatActions() -{ - if (irEnabled == 8) { - decodeIRJson(lastValidCode); - return; - } else switch (lastRepeatableAction) { - case ACTION_BRIGHT_UP : incBrightness(); stateUpdated(CALL_MODE_BUTTON); return; - case ACTION_BRIGHT_DOWN : decBrightness(); stateUpdated(CALL_MODE_BUTTON); return; - case ACTION_SPEED_UP : changeEffectSpeed(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; - case ACTION_SPEED_DOWN : changeEffectSpeed(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; - case ACTION_INTENSITY_UP : changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; - case ACTION_INTENSITY_DOWN : changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; - default: break; - } - if (lastValidCode == IR40_WPLUS) { - changeWhite(10); - stateUpdated(CALL_MODE_BUTTON); - } else if (lastValidCode == IR40_WMINUS) { - changeWhite(-10); - stateUpdated(CALL_MODE_BUTTON); - } else if ((lastValidCode == IR24_ON || lastValidCode == IR40_ON) && irTimesRepeated > 7 ) { - nightlightActive = true; - nightlightStartTime = millis(); - stateUpdated(CALL_MODE_BUTTON); - } -} - -void decodeIR24(uint32_t code) +static void decodeIR24(uint32_t code) { switch (code) { case IR24_BRIGHTER : incBrightness(); break; @@ -356,7 +279,7 @@ void decodeIR24(uint32_t code) lastValidCode = code; } -void decodeIR24OLD(uint32_t code) +static void decodeIR24OLD(uint32_t code) { switch (code) { case IR24_OLD_BRIGHTER : incBrightness(); break; @@ -388,7 +311,7 @@ void decodeIR24OLD(uint32_t code) lastValidCode = code; } -void decodeIR24CT(uint32_t code) +static void decodeIR24CT(uint32_t code) { switch (code) { case IR24_CT_BRIGHTER : incBrightness(); break; @@ -420,7 +343,7 @@ void decodeIR24CT(uint32_t code) lastValidCode = code; } -void decodeIR40(uint32_t code) +static void decodeIR40(uint32_t code) { Segment& seg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); byte r = R(seg.colors[0]); @@ -473,7 +396,7 @@ void decodeIR40(uint32_t code) lastValidCode = code; } -void decodeIR44(uint32_t code) +static void decodeIR44(uint32_t code) { switch (code) { case IR44_BPLUS : incBrightness(); break; @@ -525,7 +448,7 @@ void decodeIR44(uint32_t code) lastValidCode = code; } -void decodeIR21(uint32_t code) +static void decodeIR21(uint32_t code) { switch (code) { case IR21_BRIGHTER: incBrightness(); break; @@ -554,7 +477,7 @@ void decodeIR21(uint32_t code) lastValidCode = code; } -void decodeIR6(uint32_t code) +static void decodeIR6(uint32_t code) { switch (code) { case IR6_POWER: toggleOnOff(); break; @@ -587,7 +510,7 @@ void decodeIR6(uint32_t code) lastValidCode = code; } -void decodeIR9(uint32_t code) +static void decodeIR9(uint32_t code) { switch (code) { case IR9_POWER : toggleOnOff(); break; @@ -628,7 +551,7 @@ Sample: "label": "Preset 1, fallback to Saw - Party if not found"}, } */ -void decodeIRJson(uint32_t code) +static void decodeIRJson(uint32_t code) { char objKey[10]; char fileName[16]; @@ -701,41 +624,102 @@ void decodeIRJson(uint32_t code) releaseJSONBufferLock(); } +static void applyRepeatActions() +{ + if (irEnabled == 8) { + decodeIRJson(lastValidCode); + return; + } else switch (lastRepeatableAction) { + case ACTION_BRIGHT_UP : incBrightness(); stateUpdated(CALL_MODE_BUTTON); return; + case ACTION_BRIGHT_DOWN : decBrightness(); stateUpdated(CALL_MODE_BUTTON); return; + case ACTION_SPEED_UP : changeEffectSpeed(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; + case ACTION_SPEED_DOWN : changeEffectSpeed(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; + case ACTION_INTENSITY_UP : changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; + case ACTION_INTENSITY_DOWN : changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; + default: break; + } + if (lastValidCode == IR40_WPLUS) { + changeWhite(10); + stateUpdated(CALL_MODE_BUTTON); + } else if (lastValidCode == IR40_WMINUS) { + changeWhite(-10); + stateUpdated(CALL_MODE_BUTTON); + } else if ((lastValidCode == IR24_ON || lastValidCode == IR40_ON) && irTimesRepeated > 7 ) { + nightlightActive = true; + nightlightStartTime = millis(); + stateUpdated(CALL_MODE_BUTTON); + } +} + +static void decodeIR(uint32_t code) +{ + if (code == 0xFFFFFFFF) { + //repeated code, continue brightness up/down + irTimesRepeated++; + applyRepeatActions(); + return; + } + lastValidCode = 0; irTimesRepeated = 0; + lastRepeatableAction = ACTION_NONE; + + if (irEnabled == 8) { // any remote configurable with ir.json file + decodeIRJson(code); + stateUpdated(CALL_MODE_BUTTON); + return; + } + if (code > 0xFFFFFF) return; //invalid code + + switch (irEnabled) { + case 1: + if (code > 0xF80000) decodeIR24OLD(code); // white 24-key remote (old) - it sends 0xFF0000 values + else decodeIR24(code); // 24-key remote - 0xF70000 to 0xF80000 + break; + case 2: decodeIR24CT(code); break; // white 24-key remote with CW, WW, CT+ and CT- keys + case 3: decodeIR40(code); break; // blue 40-key remote with 25%, 50%, 75% and 100% keys + case 4: decodeIR44(code); break; // white 44-key remote with color-up/down keys and DIY1 to 6 keys + case 5: decodeIR21(code); break; // white 21-key remote + case 6: decodeIR6(code); break; // black 6-key learning remote defaults: "CH" controls brightness, + // "VOL +" controls effect, "VOL -" controls colour/palette, "MUTE" + // sets bright plain white + case 7: decodeIR9(code); break; + //case 8: return; // ir.json file, handled above switch statement + } + + if (nightlightActive && bri == 0) nightlightActive = false; + stateUpdated(CALL_MODE_BUTTON); //for notifier, IR is considered a button input +} + void initIR() { - if (irEnabled > 0) - { + if (irEnabled > 0) { irrecv = new IRrecv(irPin); - irrecv->enableIRIn(); + if (irrecv) irrecv->enableIRIn(); + } else irrecv = nullptr; +} + +void deInitIR() +{ + if (irrecv) { + irrecv->disableIRIn(); + delete irrecv; } + irrecv = nullptr; } void handleIR() { - if (irEnabled > 0 && millis() - irCheckedTime > 120 && !strip.isUpdating()) - { - irCheckedTime = millis(); - if (irEnabled > 0) - { - if (irrecv == NULL) - { - initIR(); return; + unsigned long currentTime = millis(); + unsigned timeDiff = currentTime - irCheckedTime; + if (timeDiff > 120 && irEnabled > 0 && irrecv) { + if (strip.isUpdating() && timeDiff < 240) return; // be nice, but not too nice + irCheckedTime = currentTime; + if (irrecv->decode(&results)) { + if (results.value != 0) { // only print results if anything is received ( != 0 ) + if (!pinManager.isPinAllocated(hardwareTX) || pinManager.getPinOwner(hardwareTX) == PinOwner::DebugOut) // Serial TX pin (GPIO 1 on ESP32 and ESP8266) + Serial.printf_P(PSTR("IR recv: 0x%lX\n"), (unsigned long)results.value); } - - if (irrecv->decode(&results)) - { - if (results.value != 0) // only print results if anything is received ( != 0 ) - { - if (!pinManager.isPinAllocated(hardwareTX) || pinManager.getPinOwner(hardwareTX) == PinOwner::DebugOut) // Serial TX pin (GPIO 1 on ESP32 and ESP8266) - Serial.printf_P(PSTR("IR recv: 0x%lX\n"), (unsigned long)results.value); - } - decodeIR(results.value); - irrecv->resume(); - } - } else if (irrecv != NULL) - { - irrecv->disableIRIn(); - delete irrecv; irrecv = NULL; + decodeIR(results.value); + irrecv->resume(); } } } diff --git a/wled00/set.cpp b/wled00/set.cpp index d3382be18..2e8ba69d0 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -104,7 +104,8 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) } #ifndef WLED_DISABLE_INFRARED if (irPin>=0 && pinManager.isPinAllocated(irPin, PinOwner::IR)) { - pinManager.deallocatePin(irPin, PinOwner::IR); + deInitIR(); + pinManager.deallocatePin(irPin, PinOwner::IR); } #endif for (uint8_t s=0; sarg(F("IT")).toInt(); + initIR(); #endif irApplyToAllSelected = !request->hasArg(F("MSO")); diff --git a/wled00/wled.cpp b/wled00/wled.cpp index eb7860851..6251735c3 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -505,6 +505,13 @@ void WLED::setup() initServer(); DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); +#ifndef WLED_DISABLE_INFRARED + // init IR + DEBUG_PRINTLN(F("initIR")); + initIR(); + DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); +#endif + // Seed FastLED random functions with an esp random value, which already works properly at this point. #if defined(ARDUINO_ARCH_ESP32) const uint32_t seed32 = esp_random(); From 6504fb68b6ba3fc49d18594df78812c6e153e9bc Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Fri, 3 May 2024 15:46:16 +0200 Subject: [PATCH 076/148] Minor MQTT optimisation. --- wled00/mqtt.cpp | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index 2e2e4a6bd..5599824ef 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -103,20 +103,17 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties colorFromDecOrHexString(col, payloadStr); colorUpdated(CALL_MODE_DIRECT_CHANGE); } else if (strcmp_P(topic, PSTR("/api")) == 0) { - if (!requestJSONBufferLock(15)) { - delete[] payloadStr; - payloadStr = nullptr; - return; + if (requestJSONBufferLock(15)) { + if (payloadStr[0] == '{') { //JSON API + deserializeJson(*pDoc, payloadStr); + deserializeState(pDoc->as()); + } else { //HTTP API + String apireq = "win"; apireq += '&'; // reduce flash string usage + apireq += payloadStr; + handleSet(nullptr, apireq); + } + releaseJSONBufferLock(); } - if (payloadStr[0] == '{') { //JSON API - deserializeJson(*pDoc, payloadStr); - deserializeState(pDoc->as()); - } else { //HTTP API - String apireq = "win"; apireq += '&'; // reduce flash string usage - apireq += payloadStr; - handleSet(nullptr, apireq); - } - releaseJSONBufferLock(); } else if (strlen(topic) != 0) { // non standard topic, check with usermods usermods.onMqttMessage(topic, payloadStr); From fa76431dd673e5c1a24c75fe7d9348a93e3f22f5 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Fri, 3 May 2024 16:08:20 +0200 Subject: [PATCH 077/148] Changelog update --- CHANGELOG.md | 10 ++++++++++ wled00/wled.h | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59c58dfa3..e37b08b69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ ## WLED changelog +#### Build 240503 +- Using brightness in analog clock overlay (#3944 by @paspiz85) +- Add Webpage shortcuts (#3945 by @w00000dy) +- ArtNet Poll reply (#3892 by @askask) +- Improved brightness change via long button presses (#3933 by @gaaat98) +- Relay open drain output (#3920 by @Suxsem) +- NEW JSON API: release info (update page, `info.release`) +- update esp32 platform to arduino-esp32 v2.0.9 (#3902) +- various optimisations and bugfixes (#3952, #3922, #3878, #3926, #3919, #3904 @DedeHai) + #### Build 2404120 - v0.15.0-b3 - fix for #3896 & WS2815 current saving diff --git a/wled00/wled.h b/wled00/wled.h index 139c451f8..c2efa1612 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -8,7 +8,7 @@ */ // version code in format yymmddb (b = daily build) -#define VERSION 2404120 +#define VERSION 2405030 //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG From 6df3b417a94899fd382708208ced9ab08845826c Mon Sep 17 00:00:00 2001 From: Todd Meyer Date: Fri, 3 May 2024 08:30:37 -0700 Subject: [PATCH 078/148] Updated based on more feedback --- wled00/json.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index d998a462b..82952f4c4 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -487,9 +487,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } } - if (root.containsKey(F("np"))) { - doAdvancePlaylist = root[F("np")].as(); //advances to next preset in playlist when true - } + doAdvancePlaylist = root["np"].as() || doAdvancePlaylist; //advances to next preset in playlist when true JsonObject wifi = root[F("wifi")]; if (!wifi.isNull()) { From dd19aa63d0e15693f0666ea1e33a370677b88450 Mon Sep 17 00:00:00 2001 From: Todd Meyer Date: Fri, 3 May 2024 08:47:14 -0700 Subject: [PATCH 079/148] Forgot F[], added it --- wled00/json.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index 82952f4c4..9342dc53c 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -487,7 +487,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } } - doAdvancePlaylist = root["np"].as() || doAdvancePlaylist; //advances to next preset in playlist when true + doAdvancePlaylist = root[F("np")].as() || doAdvancePlaylist; //advances to next preset in playlist when true JsonObject wifi = root[F("wifi")]; if (!wifi.isNull()) { From 379f1813620a56bb0b3136315feb647fb0c3d45d Mon Sep 17 00:00:00 2001 From: Todd Meyer Date: Fri, 3 May 2024 11:51:47 -0700 Subject: [PATCH 080/148] Further simplification --- wled00/json.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index 9342dc53c..f306eb323 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -487,7 +487,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } } - doAdvancePlaylist = root[F("np")].as() || doAdvancePlaylist; //advances to next preset in playlist when true + doAdvancePlaylist = root[F("np")] | doAdvancePlaylist; //advances to next preset in playlist when true JsonObject wifi = root[F("wifi")]; if (!wifi.isNull()) { From cd5494fdd2040ba8d6858532b7348b082c345ebb Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 4 May 2024 13:36:56 +0200 Subject: [PATCH 081/148] AR pin config: SCK == 1 --> PDM microphone --- usermods/audioreactive/audio_reactive.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index 61915170c..746617a35 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -1121,6 +1121,11 @@ class AudioReactive : public Usermod { delay(100); // Give that poor microphone some time to setup. useBandPassFilter = false; + + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) && ((dmType == 1) || (dmType == 4)) ) dmType = 5; // dummy user support: SCK == -1 --means--> PDM microphone + #endif + switch (dmType) { #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) // stub cases for not-yet-supported I2S modes on other ESP32 chips From 3f9a6cae53889898486dae727bbacebc680d6ee0 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 4 May 2024 14:34:23 +0200 Subject: [PATCH 082/148] AR: fix for arduinoFFT 2.x API in contrast to previous 'dev' versions, the storage for windowWeighingFactors is now managed internally by the arduinoFFT object. --- usermods/audioreactive/audio_reactive.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index 746617a35..442a651ea 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -183,7 +183,6 @@ constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT resul // These are the input and output vectors. Input vectors receive computed results from FFT. static float vReal[samplesFFT] = {0.0f}; // FFT sample inputs / freq output - these are our raw result bins static float vImag[samplesFFT] = {0.0f}; // imaginary parts -static float windowWeighingFactors[samplesFFT] = {0.0f}; // Create FFT object // lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 @@ -196,7 +195,8 @@ static float windowWeighingFactors[samplesFFT] = {0.0f}; #include -static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, windowWeighingFactors); +/* Create FFT object with weighing factor storage */ +static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); // Helper functions From a6e536189c5bc0d3e18b026ff6fcd1682d1cdb8b Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sun, 5 May 2024 21:56:01 +0200 Subject: [PATCH 083/148] output_bin.py : fix for mapfile copy The build script was not looking into the right place, so there was never a .map file dropped into build_output/map/ Builds with the newer arduino-esp32 v2.0.x framework actually generate a .map file that is placed directly next to firmware.bin --- pio-scripts/output_bins.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pio-scripts/output_bins.py b/pio-scripts/output_bins.py index c0e85dcbb..633654008 100644 --- a/pio-scripts/output_bins.py +++ b/pio-scripts/output_bins.py @@ -36,6 +36,8 @@ def create_release(source): def bin_rename_copy(source, target, env): _create_dirs() variant = env["PIOENV"] + builddir = os.path.join(env["PROJECT_BUILD_DIR"], variant) + source_map = os.path.join(builddir, env["PROGNAME"] + ".map") # create string with location and file names based on variant map_file = "{}map{}{}.map".format(OUTPUT_DIR, os.path.sep, variant) @@ -44,7 +46,11 @@ def bin_rename_copy(source, target, env): # copy firmware.map to map/.map if os.path.isfile("firmware.map"): - shutil.move("firmware.map", map_file) + print("Found linker mapfile firmware.map") + shutil.copy("firmware.map", map_file) + if os.path.isfile(source_map): + print(f"Found linker mapfile {source_map}") + shutil.copy(source_map, map_file) def bin_gzip(source, target): # only create gzip for esp8266 From 2607c44fbb577cc549a23a5e0dbaf858c1fd2d80 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 6 May 2024 11:00:41 +0200 Subject: [PATCH 084/148] make objdump work Script update based on latest version from Tasmota * add support for all esp32 variants * add "-C" : Decode (demangle) low-level symbol names into user-level C++ names. --- pio-scripts/obj-dump.py | 19 +++++++++++++++++-- platformio.ini | 1 + 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pio-scripts/obj-dump.py b/pio-scripts/obj-dump.py index 91bc3de58..174df509c 100644 --- a/pio-scripts/obj-dump.py +++ b/pio-scripts/obj-dump.py @@ -1,9 +1,24 @@ # Little convenience script to get an object dump +# You may add "-S" to the objdump commandline (i.e. replace "-D -C " with "-d -S -C ") +# to get source code intermixed with disassembly (SLOW !) Import('env') def obj_dump_after_elf(source, target, env): + platform = env.PioPlatform() + board = env.BoardConfig() + mcu = board.get("build.mcu", "esp32") + print("Create firmware.asm") - env.Execute("xtensa-lx106-elf-objdump "+ "-D " + str(target[0]) + " > "+ "${PROGNAME}.asm") - + if mcu == "esp8266": + env.Execute("xtensa-lx106-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") + if mcu == "esp32": + env.Execute("xtensa-esp32-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") + if mcu == "esp32s2": + env.Execute("xtensa-esp32s2-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") + if mcu == "esp32s3": + env.Execute("xtensa-esp32s3-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") + if mcu == "esp32c3": + env.Execute("riscv32-esp-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") + env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", [obj_dump_after_elf]) diff --git a/platformio.ini b/platformio.ini index 76c4c92d6..fe8b3a278 100644 --- a/platformio.ini +++ b/platformio.ini @@ -115,6 +115,7 @@ extra_scripts = post:pio-scripts/strip-floats.py pre:pio-scripts/user_config_copy.py pre:pio-scripts/build_ui.py + ; post:pio-scripts/obj-dump.py ;; convenience script to create a disassembly dump of the firmware (hardcore debugging) # ------------------------------------------------------------------------------ # COMMON SETTINGS: From 18e9f9c304482e3506e8982805b5df33a2bbf6c4 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Mon, 6 May 2024 17:39:40 +0200 Subject: [PATCH 085/148] Rename Battery classes --- usermods/Battery/{battery.h => UMBattery.h} | 4 ++-- usermods/Battery/battery_defaults.h | 1 - usermods/Battery/types/{lion.h => LionUMBattery.h} | 8 ++++---- usermods/Battery/types/{lipo.h => LipoUMBattery.h} | 8 ++++---- .../Battery/types/{unkown.h => UnkownUMBattery.h} | 8 ++++---- usermods/Battery/usermod_v2_Battery.h | 14 +++++++------- 6 files changed, 21 insertions(+), 22 deletions(-) rename usermods/Battery/{battery.h => UMBattery.h} (99%) rename usermods/Battery/types/{lion.h => LionUMBattery.h} (86%) rename usermods/Battery/types/{lipo.h => LipoUMBattery.h} (92%) rename usermods/Battery/types/{unkown.h => UnkownUMBattery.h} (87%) diff --git a/usermods/Battery/battery.h b/usermods/Battery/UMBattery.h similarity index 99% rename from usermods/Battery/battery.h rename to usermods/Battery/UMBattery.h index 2ddd84149..8a8ad891e 100644 --- a/usermods/Battery/battery.h +++ b/usermods/Battery/UMBattery.h @@ -7,7 +7,7 @@ * Battery base class * all other battery classes should inherit from this */ -class Battery +class UMBattery { private: @@ -25,7 +25,7 @@ class Battery } public: - Battery() + UMBattery() { this->setVoltageMultiplier(USERMOD_BATTERY_VOLTAGE_MULTIPLIER); this->setCalibration(USERMOD_BATTERY_CALIBRATION); diff --git a/usermods/Battery/battery_defaults.h b/usermods/Battery/battery_defaults.h index ea01e8620..8b56c6014 100644 --- a/usermods/Battery/battery_defaults.h +++ b/usermods/Battery/battery_defaults.h @@ -131,5 +131,4 @@ typedef struct bconfig_t float voltageMultiplier; } batteryConfig; - #endif \ No newline at end of file diff --git a/usermods/Battery/types/lion.h b/usermods/Battery/types/LionUMBattery.h similarity index 86% rename from usermods/Battery/types/lion.h rename to usermods/Battery/types/LionUMBattery.h index e77266164..801faee7c 100644 --- a/usermods/Battery/types/lion.h +++ b/usermods/Battery/types/LionUMBattery.h @@ -2,18 +2,18 @@ #define UMBLion_h #include "../battery_defaults.h" -#include "../battery.h" +#include "../UMBattery.h" /** - * Lion Battery + * LiOn Battery * */ -class Lion : public Battery +class LionUMBattery : public UMBattery { private: public: - Lion() : Battery() + LionUMBattery() : UMBattery() { this->setMinVoltage(USERMOD_BATTERY_LION_MIN_VOLTAGE); this->setMaxVoltage(USERMOD_BATTERY_LION_MAX_VOLTAGE); diff --git a/usermods/Battery/types/lipo.h b/usermods/Battery/types/LipoUMBattery.h similarity index 92% rename from usermods/Battery/types/lipo.h rename to usermods/Battery/types/LipoUMBattery.h index d732cf4da..bb6a6ef94 100644 --- a/usermods/Battery/types/lipo.h +++ b/usermods/Battery/types/LipoUMBattery.h @@ -2,18 +2,18 @@ #define UMBLipo_h #include "../battery_defaults.h" -#include "../battery.h" +#include "../UMBattery.h" /** - * Lipo Battery + * LiPo Battery * */ -class Lipo : public Battery +class LipoUMBattery : public UMBattery { private: public: - Lipo() : Battery() + LipoUMBattery() : UMBattery() { this->setMinVoltage(USERMOD_BATTERY_LIPO_MIN_VOLTAGE); this->setMaxVoltage(USERMOD_BATTERY_LIPO_MAX_VOLTAGE); diff --git a/usermods/Battery/types/unkown.h b/usermods/Battery/types/UnkownUMBattery.h similarity index 87% rename from usermods/Battery/types/unkown.h rename to usermods/Battery/types/UnkownUMBattery.h index 32a1bfe42..ede5ffd88 100644 --- a/usermods/Battery/types/unkown.h +++ b/usermods/Battery/types/UnkownUMBattery.h @@ -2,18 +2,18 @@ #define UMBUnkown_h #include "../battery_defaults.h" -#include "../battery.h" +#include "../UMBattery.h" /** - * Lion Battery + * Unkown / Default Battery * */ -class Unkown : public Battery +class UnkownUMBattery : public UMBattery { private: public: - Unkown() : Battery() + UnkownUMBattery() : UMBattery() { this->setMinVoltage(USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE); this->setMaxVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE); diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index 7b6b038a6..c70babf53 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -2,10 +2,10 @@ #include "wled.h" #include "battery_defaults.h" -#include "battery.h" -#include "types/unkown.h" -#include "types/lion.h" -#include "types/lipo.h" +#include "UMBattery.h" +#include "types/UnkownUMBattery.h" +#include "types/LionUMBattery.h" +#include "types/LiPoUMBattery.h" /* * Usermod by Maximilian Mewes @@ -19,7 +19,7 @@ class UsermodBattery : public Usermod // battery pin can be defined in my_config.h int8_t batteryPin = USERMOD_BATTERY_MEASUREMENT_PIN; - Battery* bat = new Unkown(); + UMBattery* bat = new UnkownUMBattery(); batteryConfig cfg; // how often to read the battery voltage @@ -142,9 +142,9 @@ class UsermodBattery : public Usermod // plug in the right battery type if(cfg.type == (batteryType)lipo) { - bat = new Lipo(); + bat = new LipoUMBattery(); } else if(cfg.type == (batteryType)lion) { - bat = new Lion(); + bat = new LipoUMBattery(); } // update the choosen battery type with configured values From d33651c25bfdc3ab4457c6e95496914fb924bad1 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Mon, 6 May 2024 17:45:02 +0200 Subject: [PATCH 086/148] Update setup method --- usermods/Battery/usermod_v2_Battery.h | 41 +++++++++++++-------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index c70babf53..09be3ccc6 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -120,26 +120,6 @@ class UsermodBattery : public Usermod */ void setup() { - #ifdef ARDUINO_ARCH_ESP32 - bool success = false; - DEBUG_PRINTLN(F("Allocating battery pin...")); - if (batteryPin >= 0 && digitalPinToAnalogChannel(batteryPin) >= 0) - if (pinManager.allocatePin(batteryPin, false, PinOwner::UM_Battery)) { - DEBUG_PRINTLN(F("Battery pin allocation succeeded.")); - success = true; - voltage = readVoltage(); - } - - if (!success) { - DEBUG_PRINTLN(F("Battery pin allocation failed.")); - batteryPin = -1; // allocation failed - } else { - pinMode(batteryPin, INPUT); - } - #else //ESP8266 boards have only one analog input pin A0 - pinMode(batteryPin, INPUT); - #endif - // plug in the right battery type if(cfg.type == (batteryType)lipo) { bat = new LipoUMBattery(); @@ -150,7 +130,26 @@ class UsermodBattery : public Usermod // update the choosen battery type with configured values bat->update(cfg); - bat->setVoltage(readVoltage()); + #ifdef ARDUINO_ARCH_ESP32 + bool success = false; + DEBUG_PRINTLN(F("Allocating battery pin...")); + if (batteryPin >= 0 && digitalPinToAnalogChannel(batteryPin) >= 0) + if (pinManager.allocatePin(batteryPin, false, PinOwner::UM_Battery)) { + DEBUG_PRINTLN(F("Battery pin allocation succeeded.")); + success = true; + bat->setVoltage(readVoltage()); + } + + if (!success) { + DEBUG_PRINTLN(F("Battery pin allocation failed.")); + batteryPin = -1; // allocation failed + } else { + pinMode(batteryPin, INPUT); + } + #else //ESP8266 boards have only one analog input pin A0 + pinMode(batteryPin, INPUT); + bat->setVoltage(readVoltage()); + #endif nextReadTime = millis() + readingInterval; lastReadTime = millis(); From 52020cbe269e39dae3493e387bacbd35eefdc92b Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Mon, 6 May 2024 17:46:26 +0200 Subject: [PATCH 087/148] CP fix --- usermods/Battery/usermod_v2_Battery.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index 09be3ccc6..88a879b72 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -124,7 +124,7 @@ class UsermodBattery : public Usermod if(cfg.type == (batteryType)lipo) { bat = new LipoUMBattery(); } else if(cfg.type == (batteryType)lion) { - bat = new LipoUMBattery(); + bat = new LionUMBattery(); } // update the choosen battery type with configured values From 5bccb5fc422f4f680b6d3720db47c6bac2f11c1e Mon Sep 17 00:00:00 2001 From: gaaat98 <67930088+gaaat98@users.noreply.github.com> Date: Tue, 7 May 2024 00:31:37 +0200 Subject: [PATCH 088/148] removed commented checks --- wled00/FX.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3110ab910..3c499edad 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -6643,7 +6643,6 @@ static const char _data_FX_MODE_GRAVIMETER[] PROGMEM = "Gravimeter@Rate of fall, // * JUGGLES // ////////////////////// uint16_t mode_juggles(void) { // Juggles. By Andrew Tuline. - //if (SEGLEN == 1) return mode_static(); um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // add support for no audio @@ -7027,7 +7026,6 @@ static const char _data_FX_MODE_BLURZ[] PROGMEM = "Blurz@Fade rate,Blur;!,Color // ** DJLight // ///////////////////////// uint16_t mode_DJLight(void) { // Written by ??? Adapted by Will Tatam. - //if (SEGLEN == 1) return mode_static(); // No need to prevent from executing on single led strips, only mid will be set (mid = 0) const int mid = SEGLEN / 2; @@ -7100,7 +7098,6 @@ static const char _data_FX_MODE_FREQMAP[] PROGMEM = "Freqmap@Fade rate,Starting // ** Freqmatrix // /////////////////////// uint16_t mode_freqmatrix(void) { // Freqmatrix. By Andreas Pleschung. - //if (SEGLEN == 1) return mode_static(); // No need to prevent from executing on single led strips, we simply change pixel 0 each time and avoid the shift um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { @@ -7207,7 +7204,6 @@ static const char _data_FX_MODE_FREQPIXELS[] PROGMEM = "Freqpixels@Fade rate,Sta // As a compromise between speed and accuracy we are currently sampling with 10240Hz, from which we can then determine with a 512bin FFT our max frequency is 5120Hz. // Depending on the music stream you have you might find it useful to change the frequency mapping. uint16_t mode_freqwave(void) { // Freqwave. By Andreas Pleschung. - //if (SEGLEN == 1) return mode_static(); // As before, this effect can also work on single pixels, we just lose the shifting effect um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { @@ -7318,7 +7314,6 @@ static const char _data_FX_MODE_GRAVFREQ[] PROGMEM = "Gravfreq@Rate of fall,Sens // ** Noisemove // ////////////////////// uint16_t mode_noisemove(void) { // Noisemove. By: Andrew Tuline - //if (SEGLEN == 1) return mode_static(); um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // add support for no audio @@ -7346,7 +7341,6 @@ static const char _data_FX_MODE_NOISEMOVE[] PROGMEM = "Noisemove@Speed of perlin // ** Rocktaves // ////////////////////// uint16_t mode_rocktaves(void) { // Rocktaves. Same note from each octave is same colour. By: Andrew Tuline - //if (SEGLEN == 1) return mode_static(); um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // add support for no audio @@ -7388,9 +7382,8 @@ static const char _data_FX_MODE_ROCKTAVES[] PROGMEM = "Rocktaves@;!,!;!;01f;m12= /////////////////////// // Combines peak detection with FFT_MajorPeak and FFT_Magnitude. uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tuline - //if (SEGLEN == 1) return mode_static(); // effect can work on single pixels, we just lose the shifting effect - + um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // add support for no audio From 88372cd516f959db0033644479621f5221aef3b1 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Tue, 7 May 2024 16:34:15 +0200 Subject: [PATCH 089/148] Brighter peek (ignore strip brightness) --- wled00/ws.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/wled00/ws.cpp b/wled00/ws.cpp index 307a0959e..cf09d592e 100644 --- a/wled00/ws.cpp +++ b/wled00/ws.cpp @@ -206,9 +206,12 @@ bool sendLiveLedsWs(uint32_t wsClient) uint8_t g = G(c); uint8_t b = B(c); uint8_t w = W(c); - buffer[pos++] = scale8(qadd8(w, r), strip.getBrightness()); //R, add white channel to RGB channels as a simple RGBW -> RGB map - buffer[pos++] = scale8(qadd8(w, g), strip.getBrightness()); //G - buffer[pos++] = scale8(qadd8(w, b), strip.getBrightness()); //B + //buffer[pos++] = scale8(qadd8(w, r), strip.getBrightness()); //R, add white channel to RGB channels as a simple RGBW -> RGB map + //buffer[pos++] = scale8(qadd8(w, g), strip.getBrightness()); //G + //buffer[pos++] = scale8(qadd8(w, b), strip.getBrightness()); //B + buffer[pos++] = qadd8(w, r); //R, add white channel to RGB channels as a simple RGBW -> RGB map + buffer[pos++] = qadd8(w, g); //G + buffer[pos++] = qadd8(w, b); //B } wsc->binary(std::move(wsBuf)); From b88c300d0467bd65dd9e58b3ae99977434f70fab Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:57:29 +0200 Subject: [PATCH 090/148] audioreactive: workaround for ArduinoFFT bug 93 This fix works around a problem that was solved in upstream ArduinoFFT 2.0.2 --- usermods/audioreactive/audio_reactive.h | 1 + 1 file changed, 1 insertion(+) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index 442a651ea..8741eb14c 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -282,6 +282,7 @@ void FFTcode(void * parameter) //FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection FFT.compute( FFTDirection::Forward ); // Compute FFT FFT.complexToMagnitude(); // Compute magnitudes + vReal[0] = 0; // The remaining DC offset on the signal produces a strong spike on position 0 that should be eliminated to avoid issues. FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects From da79b93387cb80873d5dc65cd864be866a4b70bf Mon Sep 17 00:00:00 2001 From: Brandon502 <105077712+Brandon502@users.noreply.github.com> Date: Tue, 7 May 2024 18:04:29 -0400 Subject: [PATCH 091/148] Added Pinwheel Expand 1D Effect --- wled00/FX.h | 3 ++- wled00/FX_fcn.cpp | 58 +++++++++++++++++++++++++++++++++++++++++++- wled00/data/index.js | 1 + 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index 106a6712c..b1e6823ff 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -320,7 +320,8 @@ typedef enum mapping1D2D { M12_Pixels = 0, M12_pBar = 1, M12_pArc = 2, - M12_pCorner = 3 + M12_pCorner = 3, + M12_sPinwheel = 4 } mapping1D2D_t; // segment, 80 bytes diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index ce510f16e..7f8c1319a 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -637,6 +637,14 @@ uint16_t IRAM_ATTR Segment::nrOfVStrips() const { return vLen; } +// Constants for mapping mode "Pinwheel" +constexpr int Pinwheel_Steps_Medium = 208; // no holes up to 32x32; 60fps +constexpr int Pinwheel_Size_Medium = 32; // larger than this -> use "Big" +constexpr int Pinwheel_Steps_Big = 360; // no holes expected up to 58x58; 40fps +constexpr float Int_to_Rad_Med = (DEG_TO_RAD * 360) / Pinwheel_Steps_Medium; // conversion: from 0...208 to Radians +constexpr float Int_to_Rad_Big = (DEG_TO_RAD * 360) / Pinwheel_Steps_Big; // conversion: from 0...360 to Radians + + // 1D strip uint16_t IRAM_ATTR Segment::virtualLength() const { #ifndef WLED_DISABLE_2D @@ -652,6 +660,12 @@ uint16_t IRAM_ATTR Segment::virtualLength() const { case M12_pArc: vLen = max(vW,vH); // get the longest dimension break; + case M12_sPinwheel: + if (max(vW,vH) <= Pinwheel_Size_Medium) + vLen = Pinwheel_Steps_Medium; + else + vLen = Pinwheel_Steps_Big; + break; } return vLen; } @@ -718,6 +732,38 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) for (int x = 0; x <= i; x++) setPixelColorXY(x, i, col); for (int y = 0; y < i; y++) setPixelColorXY(i, y, col); break; + case M12_sPinwheel: { + // i = angle --> 0 through 359 (Big), OR 0 through 208 (Medium) + float centerX = roundf((vW-1) / 2.0f); + float centerY = roundf((vH-1) / 2.0f); + // int maxDistance = sqrt(centerX * centerX + centerY * centerY) + 1; + float angleRad = (max(vW,vH) > Pinwheel_Size_Medium) ? float(i) * Int_to_Rad_Big : float(i) * Int_to_Rad_Med; // angle in radians + float cosVal = cosf(angleRad); + float sinVal = sinf(angleRad); + + // draw line at angle, starting at center and ending at the segment edge + // we use fixed point math for better speed. Starting distance is 0.5 for better rounding + constexpr int_fast32_t Fixed_Scale = 512; // fixpoint scaling factor + int_fast32_t posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point + int_fast32_t posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point + int_fast16_t inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) + int_fast16_t inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) + + int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint + int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint + // draw until we hit any edge + while ((posx > 0) && (posy > 0) && (posx < maxX) && (posy < maxY)) { + // scale down to integer (compiler will replace division with appropriate bitshift) + int x = posx / Fixed_Scale; + int y = posy / Fixed_Scale; + // set pixel + setPixelColorXY(x, y, col); + // advance to next position + posx += inc_x; + posy += inc_y; + } + break; + } } return; } else if (Segment::maxHeight!=1 && (width()==1 || height()==1)) { @@ -833,7 +879,17 @@ uint32_t IRAM_ATTR Segment::getPixelColor(int i) // use longest dimension return vW>vH ? getPixelColorXY(i, 0) : getPixelColorXY(0, i); break; - } + case M12_sPinwheel: + // not 100% accurate, returns outer edge of circle + float distance = max(1.0f, min(vH-1, vW-1) / 2.0f); + float centerX = (vW - 1) / 2.0f; + float centerY = (vH - 1) / 2.0f; + float angleRad = (max(vW,vH) > Pinwheel_Size_Medium) ? float(i) * Int_to_Rad_Big : float(i) * Int_to_Rad_Med; // angle in radians + int x = roundf(centerX + distance * cosf(angleRad)); + int y = roundf(centerY + distance * sinf(angleRad)); + return getPixelColorXY(x, y); + break; + } return 0; } #endif diff --git a/wled00/data/index.js b/wled00/data/index.js index d33fb63f7..26d78b284 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -801,6 +801,7 @@ function populateSegments(s) ``+ ``+ ``+ + ``+ `
`+ `
`; let sndSim = `
Sound sim
`+ From a320f164045c53b4b8454c9b25e792f64fae57ca Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Thu, 9 May 2024 23:58:58 +0200 Subject: [PATCH 092/148] Real math fix --- wled00/fcn_declare.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index a6ff9d096..1b25c8926 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -409,14 +409,14 @@ void clearEEPROM(); float fmod_t(float num, float denom); #else #include - #define sin_t sin - #define cos_t cos - #define tan_t tan - #define asin_t asin - #define acos_t acos - #define atan_t atan - #define fmod_t fmod - #define floor_t floor + #define sin_t sinf + #define cos_t cosf + #define tan_t tanf + #define asin_t asinf + #define acos_t acosf + #define atan_t atanf + #define fmod_t fmodf + #define floor_t floorf #endif //wled_serial.cpp From 4dbe9a701596d130ff34157c111abf93f0c29846 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Fri, 10 May 2024 00:02:28 +0200 Subject: [PATCH 093/148] Antialiased line & circle --- wled00/FX.cpp | 4 +- wled00/FX.h | 27 ++++-- wled00/FX_2Dfcn.cpp | 211 ++++++++++++++++++++++++++------------------ 3 files changed, 147 insertions(+), 95 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3c499edad..d4566976d 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -2506,7 +2506,7 @@ uint16_t ripple_base() uint16_t cx = rippleorigin >> 8; uint16_t cy = rippleorigin & 0xFF; uint8_t mag = scale8(sin8((propF>>2)), amp); - if (propI > 0) SEGMENT.draw_circle(cx, cy, propI, color_blend(SEGMENT.getPixelColorXY(cx + propI, cy), col, mag)); + if (propI > 0) SEGMENT.drawCircle(cx, cy, propI, color_blend(SEGMENT.getPixelColorXY(cx + propI, cy), col, mag), true); } else #endif { @@ -6056,7 +6056,7 @@ uint16_t mode_2Dfloatingblobs(void) { } } uint32_t c = SEGMENT.color_from_palette(blob->color[i], false, false, 0); - if (blob->r[i] > 1.f) SEGMENT.fill_circle(roundf(blob->x[i]), roundf(blob->y[i]), roundf(blob->r[i]), c); + if (blob->r[i] > 1.f) SEGMENT.fillCircle(roundf(blob->x[i]), roundf(blob->y[i]), roundf(blob->r[i]), c); else SEGMENT.setPixelColorXY((int)roundf(blob->x[i]), (int)roundf(blob->y[i]), c); // move x if (blob->x[i] + blob->r[i] >= cols - 1) blob->x[i] += (blob->sX[i] * ((cols - 1 - blob->x[i]) / blob->r[i] + 0.005f)); diff --git a/wled00/FX.h b/wled00/FX.h index 106a6712c..6e458fcea 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -106,6 +106,10 @@ #define PURPLE (uint32_t)0x400080 #define ORANGE (uint32_t)0xFF3000 #define PINK (uint32_t)0xFF1493 +#define GREY (uint32_t)0x808080 +#define GRAY GREY +#define DARKGREY (uint32_t)0x333333 +#define DARKGRAY DARKGREY #define ULTRAWHITE (uint32_t)0xFFFFFFFF #define DARKSLATEGRAY (uint32_t)0x2F4F4F #define DARKSLATEGREY DARKSLATEGRAY @@ -605,6 +609,7 @@ typedef struct Segment { inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColorXY(int(x), int(y), c); } inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } + inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) { setPixelColorXY(int(x), int(y), RGBW32(c.r,c.g,c.b,0)); } #ifdef WLED_USE_AA_PIXELS void setPixelColorXY(float x, float y, uint32_t c, bool aa = true); inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColorXY(x, y, RGBW32(r,g,b,w), aa); } @@ -624,24 +629,25 @@ typedef struct Segment { void moveX(int8_t delta, bool wrap = false); void moveY(int8_t delta, bool wrap = false); void move(uint8_t dir, uint8_t delta, bool wrap = false); - void draw_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c); - void fill_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c); - void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c); - inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c) { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0)); } // automatic inline + void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false); + inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) { drawCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } + void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false); + inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) { fillCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } + void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false); + inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0), soft); } // automatic inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0); inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c) { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0)); } // automatic inline inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0), rotate); } // automatic inline void wu_pixel(uint32_t x, uint32_t y, CRGB c); - void blur1d(fract8 blur_amount); // blur all rows in 1 dimension inline void blur2d(fract8 blur_amount) { blur(blur_amount); } inline void fill_solid(CRGB c) { fill(RGBW32(c.r,c.g,c.b,0)); } - void nscale8(uint8_t scale); #else inline uint16_t XY(uint16_t x, uint16_t y) { return x; } inline void setPixelColorXY(int x, int y, uint32_t c) { setPixelColor(x, c); } inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColor(int(x), c); } inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColor(x, RGBW32(r,g,b,w)); } inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0)); } + inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) { setPixelColor(int(x), RGBW32(c.r,c.g,c.b,0)); } #ifdef WLED_USE_AA_PIXELS inline void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) { setPixelColor(x, c, aa); } inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); } @@ -660,9 +666,12 @@ typedef struct Segment { inline void moveX(int8_t delta, bool wrap = false) {} inline void moveY(int8_t delta, bool wrap = false) {} inline void move(uint8_t dir, uint8_t delta, bool wrap = false) {} - inline void fill_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c) {} - inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c) {} - inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c) {} + inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) {} + inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) {} + inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) {} + inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) {} + inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) {} + inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) {} inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t = 0, int8_t = 0) {} inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB color) {} inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) {} diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index e14b68f4f..b262c157d 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -342,55 +342,36 @@ void Segment::blurCol(uint32_t col, fract8 blur_amount, bool smear) { // 1D Box blur (with added weight - blur_amount: [0=no blur, 255=max blur]) void Segment::box_blur(uint16_t i, bool vertical, fract8 blur_amount) { if (!isActive() || blur_amount == 0) return; // not active - const unsigned cols = virtualWidth(); - const unsigned rows = virtualHeight(); - const unsigned dim1 = vertical ? rows : cols; - const unsigned dim2 = vertical ? cols : rows; + const int cols = virtualWidth(); + const int rows = virtualHeight(); + const int dim1 = vertical ? rows : cols; + const int dim2 = vertical ? cols : rows; if (i >= dim2) return; const float seep = blur_amount/255.f; const float keep = 3.f - 2.f*seep; // 1D box blur - CRGB tmp[dim1]; - for (unsigned j = 0; j < dim1; j++) { - unsigned x = vertical ? i : j; - unsigned y = vertical ? j : i; - int xp = vertical ? x : x-1; // "signed" to prevent underflow - int yp = vertical ? y-1 : y; // "signed" to prevent underflow - unsigned xn = vertical ? x : x+1; - unsigned yn = vertical ? y+1 : y; - CRGB curr = getPixelColorXY(x,y); - CRGB prev = (xp<0 || yp<0) ? CRGB::Black : getPixelColorXY(xp,yp); - CRGB next = ((vertical && yn>=dim1) || (!vertical && xn>=dim1)) ? CRGB::Black : getPixelColorXY(xn,yn); - unsigned r, g, b; - r = (curr.r*keep + (prev.r + next.r)*seep) / 3; - g = (curr.g*keep + (prev.g + next.g)*seep) / 3; - b = (curr.b*keep + (prev.b + next.b)*seep) / 3; - tmp[j] = CRGB(r,g,b); + uint32_t out[dim1], in[dim1]; + for (int j = 0; j < dim1; j++) { + int x = vertical ? i : j; + int y = vertical ? j : i; + in[j] = getPixelColorXY(x, y); } - for (unsigned j = 0; j < dim1; j++) { - unsigned x = vertical ? i : j; - unsigned y = vertical ? j : i; - setPixelColorXY(x, y, tmp[j]); + for (int j = 0; j < dim1; j++) { + uint32_t curr = in[j]; + uint32_t prev = j > 0 ? in[j-1] : BLACK; + uint32_t next = j < dim1-1 ? in[j+1] : BLACK; + uint8_t r, g, b, w; + r = (R(curr)*keep + (R(prev) + R(next))*seep) / 3; + g = (G(curr)*keep + (G(prev) + G(next))*seep) / 3; + b = (B(curr)*keep + (B(prev) + B(next))*seep) / 3; + w = (W(curr)*keep + (W(prev) + W(next))*seep) / 3; + out[j] = RGBW32(r,g,b,w); + } + for (int j = 0; j < dim1; j++) { + int x = vertical ? i : j; + int y = vertical ? j : i; + setPixelColorXY(x, y, out[j]); } -} - -// blur1d: one-dimensional blur filter. Spreads light to 2 line neighbors. -// blur2d: two-dimensional blur filter. Spreads light to 8 XY neighbors. -// -// 0 = no spread at all -// 64 = moderate spreading -// 172 = maximum smooth, even spreading -// -// 173..255 = wider spreading, but increasing flicker -// -// Total light is NOT entirely conserved, so many repeated -// calls to 'blur' will also result in the light fading, -// eventually all the way to black; this is by design so that -// it can be used to (slowly) clear the LEDs to black. - -void Segment::blur1d(fract8 blur_amount) { - const unsigned rows = virtualHeight(); - for (unsigned y = 0; y < rows; y++) blurRow(y, blur_amount); } void Segment::moveX(int8_t delta, bool wrap) { @@ -447,33 +428,67 @@ void Segment::move(uint8_t dir, uint8_t delta, bool wrap) { } } -void Segment::draw_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB col) { +void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) { if (!isActive() || radius == 0) return; // not active - // Bresenham’s Algorithm - int d = 3 - (2*radius); - int y = radius, x = 0; - while (y >= x) { - setPixelColorXY(cx+x, cy+y, col); - setPixelColorXY(cx-x, cy+y, col); - setPixelColorXY(cx+x, cy-y, col); - setPixelColorXY(cx-x, cy-y, col); - setPixelColorXY(cx+y, cy+x, col); - setPixelColorXY(cx-y, cy+x, col); - setPixelColorXY(cx+y, cy-x, col); - setPixelColorXY(cx-y, cy-x, col); - x++; - if (d > 0) { - y--; - d += 4 * (x - y) + 10; - } else { - d += 4 * x + 6; + if (soft) { + // Xiaolin Wu’s algorithm + int rsq = radius*radius; + int x = 0; + int y = radius; + unsigned oldFade = 0; + while (x < y) { + float yf = sqrtf(float(rsq - x*x)); // needs to be floating point + unsigned fade = float(0xFFFF) * (ceilf(yf) - yf); // how much color to keep + if (oldFade > fade) y--; + oldFade = fade; + setPixelColorXY(cx+x, cy+y, color_blend(col, getPixelColorXY(cx+x, cy+y), fade, true)); + setPixelColorXY(cx-x, cy+y, color_blend(col, getPixelColorXY(cx-x, cy+y), fade, true)); + setPixelColorXY(cx+x, cy-y, color_blend(col, getPixelColorXY(cx+x, cy-y), fade, true)); + setPixelColorXY(cx-x, cy-y, color_blend(col, getPixelColorXY(cx-x, cy-y), fade, true)); + setPixelColorXY(cx+y, cy+x, color_blend(col, getPixelColorXY(cx+y, cy+x), fade, true)); + setPixelColorXY(cx-y, cy+x, color_blend(col, getPixelColorXY(cx-y, cy+x), fade, true)); + setPixelColorXY(cx+y, cy-x, color_blend(col, getPixelColorXY(cx+y, cy-x), fade, true)); + setPixelColorXY(cx-y, cy-x, color_blend(col, getPixelColorXY(cx-y, cy-x), fade, true)); + setPixelColorXY(cx+x, cy+y-1, color_blend(getPixelColorXY(cx+x, cy+y-1), col, fade, true)); + setPixelColorXY(cx-x, cy+y-1, color_blend(getPixelColorXY(cx-x, cy+y-1), col, fade, true)); + setPixelColorXY(cx+x, cy-y+1, color_blend(getPixelColorXY(cx+x, cy-y+1), col, fade, true)); + setPixelColorXY(cx-x, cy-y+1, color_blend(getPixelColorXY(cx-x, cy-y+1), col, fade, true)); + setPixelColorXY(cx+y-1, cy+x, color_blend(getPixelColorXY(cx+y-1, cy+x), col, fade, true)); + setPixelColorXY(cx-y+1, cy+x, color_blend(getPixelColorXY(cx-y+1, cy+x), col, fade, true)); + setPixelColorXY(cx+y-1, cy-x, color_blend(getPixelColorXY(cx+y-1, cy-x), col, fade, true)); + setPixelColorXY(cx-y+1, cy-x, color_blend(getPixelColorXY(cx-y+1, cy-x), col, fade, true)); + x++; + } + } else { + // Bresenham’s Algorithm + int d = 3 - (2*radius); + int y = radius, x = 0; + while (y >= x) { + setPixelColorXY(cx+x, cy+y, col); + setPixelColorXY(cx-x, cy+y, col); + setPixelColorXY(cx+x, cy-y, col); + setPixelColorXY(cx-x, cy-y, col); + setPixelColorXY(cx+y, cy+x, col); + setPixelColorXY(cx-y, cy+x, col); + setPixelColorXY(cx+y, cy-x, col); + setPixelColorXY(cx-y, cy-x, col); + x++; + if (d > 0) { + y--; + d += 4 * (x - y) + 10; + } else { + d += 4 * x + 6; + } } } } // by stepko, taken from https://editor.soulmatelights.com/gallery/573-blobs -void Segment::fill_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB col) { +void Segment::fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) { if (!isActive() || radius == 0) return; // not active + // draw soft bounding circle + if (soft) drawCircle(cx, cy, radius, col, soft); + // fill it const int cols = virtualWidth(); const int rows = virtualHeight(); for (int y = -radius; y <= radius; y++) { @@ -486,30 +501,58 @@ void Segment::fill_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB col) { } } -void Segment::nscale8(uint8_t scale) { - if (!isActive()) return; // not active - const unsigned cols = virtualWidth(); - const unsigned rows = virtualHeight(); - for (unsigned y = 0; y < rows; y++) for (unsigned x = 0; x < cols; x++) { - setPixelColorXY(x, y, CRGB(getPixelColorXY(x, y)).nscale8(scale)); - } -} - //line function -void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c) { +void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft) { if (!isActive()) return; // not active - const unsigned cols = virtualWidth(); - const unsigned rows = virtualHeight(); + const int cols = virtualWidth(); + const int rows = virtualHeight(); if (x0 >= cols || x1 >= cols || y0 >= rows || y1 >= rows) return; - const int dx = abs(x1-x0), sx = x0dy ? dx : -dy)/2, e2; - for (;;) { - setPixelColorXY(x0,y0,c); - if (x0==x1 && y0==y1) break; - e2 = err; - if (e2 >-dx) { err -= dy; x0 += sx; } - if (e2 < dy) { err += dx; y0 += sy; } + + const int dx = abs(x1-x0), sx = x0 dx; + if (steep) { + // we need to go along longest dimension + std::swap(x0,y0); + std::swap(x1,y1); + } + if (x0 > x1) { + // we need to go in increasing fashion + std::swap(x0,x1); + std::swap(y0,y1); + } + float gradient = x1-x0 == 0 ? 1.0f : float(y1-y0) / float(x1-x0); + float intersectY = y0; + for (int x = x0; x <= x1; x++) { + unsigned keep = float(0xFFFF) * (intersectY-int(intersectY)); // how much color to keep + unsigned seep = 0xFFFF - keep; // how much background to keep + int y = int(intersectY); + if (steep) std::swap(x,y); // temporaryly swap if steep + // pixel coverage is determined by fractional part of y co-ordinate + setPixelColorXY(x, y, color_blend(c, getPixelColorXY(x, y), keep, true)); + setPixelColorXY(x+int(steep), y+int(!steep), color_blend(c, getPixelColorXY(x+int(steep), y+int(!steep)), seep, true)); + intersectY += gradient; + if (steep) std::swap(x,y); // restore if steep + } + } else { + // Bresenham's algorithm + int err = (dx>dy ? dx : -dy)/2; // error direction + for (;;) { + setPixelColorXY(x0, y0, c); + if (x0==x1 && y0==y1) break; + int e2 = err; + if (e2 >-dx) { err -= dy; x0 += sx; } + if (e2 < dy) { err += dx; y0 += sy; } + } } } From bc5aadff7d73931f971b3d1ee534b7a2d4dc7f0a Mon Sep 17 00:00:00 2001 From: Adam Matthews Date: Thu, 9 May 2024 23:09:45 +0100 Subject: [PATCH 094/148] Update Usermod: Battery Issue: When taking the initial voltage reading after first powering on, voltage hasn't had chance to stabilize so the reading can be inaccurate, which in turn may incorrectly trigger the low-power preset. (Manifests when the user has selected a low read interval and/or is using a capacitor). Resolution: A non-blocking, fixed 10 second delay has been added to the initial voltage reading to give the voltage time to stabilize. This is a reworked version of the (now closed) PR here: https://github.com/Aircoookie/WLED/pull/3959 - Rebased the update for 0_15. - Added a constant so the delay can be modified via my_config.h. - Small adjustments to make the PR compatible again after the recent restructuring in this PR: (https://github.com/Aircoookie/WLED/pull/3003). Thankyou! --- usermods/Battery/battery_defaults.h | 6 ++++++ usermods/Battery/readme.md | 5 +++++ usermods/Battery/usermod_v2_Battery.h | 28 ++++++++++++++++++++++++--- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/usermods/Battery/battery_defaults.h b/usermods/Battery/battery_defaults.h index 8b56c6014..ddbd114e4 100644 --- a/usermods/Battery/battery_defaults.h +++ b/usermods/Battery/battery_defaults.h @@ -14,6 +14,12 @@ #endif #endif +// The initial delay before the first battery voltage reading after power-on. +// This allows the voltage to stabilize before readings are taken, improving accuracy of initial reading. +#ifndef USERMOD_BATTERY_INITIAL_DELAY + #define USERMOD_BATTERY_INITIAL_DELAY 10000 // (milliseconds) +#endif + // the frequency to check the battery, 30 sec #ifndef USERMOD_BATTERY_MEASUREMENT_INTERVAL #define USERMOD_BATTERY_MEASUREMENT_INTERVAL 30000 diff --git a/usermods/Battery/readme.md b/usermods/Battery/readme.md index b3607482a..efe25cc24 100644 --- a/usermods/Battery/readme.md +++ b/usermods/Battery/readme.md @@ -37,6 +37,7 @@ define `USERMOD_BATTERY` in `wled00/my_config.h` | ----------------------------------------------- | ----------- |-------------------------------------------------------------------------------------- | | `USERMOD_BATTERY` | | define this (in `my_config.h`) to have this usermod included wled00\usermods_list.cpp | | `USERMOD_BATTERY_MEASUREMENT_PIN` | | defaults to A0 on ESP8266 and GPIO35 on ESP32 | +| `USERMOD_BATTERY_INITIAL_DELAY` | ms | delay before initial reading. defaults to 10 seconds to allow voltage stabilization | `USERMOD_BATTERY_MEASUREMENT_INTERVAL` | ms | battery check interval. defaults to 30 seconds | | `USERMOD_BATTERY_{TYPE}_MIN_VOLTAGE` | v | minimum battery voltage. default is 2.6 (18650 battery standard) | | `USERMOD_BATTERY_{TYPE}_MAX_VOLTAGE` | v | maximum battery voltage. default is 4.2 (18650 battery standard) | @@ -88,6 +89,10 @@ Specification from: [Molicel INR18650-M35A, 3500mAh 10A Lithium-ion battery, 3. 2024-04-30 +- improved initial reading accuracy by delaying initial measurement to allow voltage to stabilize at power-on + +2024-04-30 + - integrate factory pattern to make it easier to add other / custom battery types - update readme diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index 88a879b72..35da337e1 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -22,6 +22,10 @@ class UsermodBattery : public Usermod UMBattery* bat = new UnkownUMBattery(); batteryConfig cfg; + // Initial delay before first reading to allow voltage stabilization + unsigned long initialDelay = USERMOD_BATTERY_INITIAL_DELAY; + bool initialDelayComplete = false; + bool isFirstVoltageReading = true; // how often to read the battery voltage unsigned long readingInterval = USERMOD_BATTERY_MEASUREMENT_INTERVAL; unsigned long nextReadTime = 0; @@ -137,7 +141,6 @@ class UsermodBattery : public Usermod if (pinManager.allocatePin(batteryPin, false, PinOwner::UM_Battery)) { DEBUG_PRINTLN(F("Battery pin allocation succeeded.")); success = true; - bat->setVoltage(readVoltage()); } if (!success) { @@ -148,10 +151,10 @@ class UsermodBattery : public Usermod } #else //ESP8266 boards have only one analog input pin A0 pinMode(batteryPin, INPUT); - bat->setVoltage(readVoltage()); #endif - nextReadTime = millis() + readingInterval; + // First voltage reading is delayed to allow voltage stabilization after powering up + nextReadTime = millis() + initialDelay; lastReadTime = millis(); initDone = true; @@ -178,6 +181,25 @@ class UsermodBattery : public Usermod lowPowerIndicator(); + // Handling the initial delay + if (!initialDelayComplete && millis() < nextReadTime) + return; // Continue to return until the initial delay is over + + // Once the initial delay is over, set it as complete + if (!initialDelayComplete) + { + initialDelayComplete = true; + // Set the regular interval after initial delay + nextReadTime = millis() + readingInterval; + } + + // Make the first voltage reading after the initial delay has elapsed + if (isFirstVoltageReading) + { + bat->setVoltage(readVoltage()); + isFirstVoltageReading = false; + } + // check the battery level every USERMOD_BATTERY_MEASUREMENT_INTERVAL (ms) if (millis() < nextReadTime) return; From 6a18ce078e3b2f31404d3309b39985638d53fcf4 Mon Sep 17 00:00:00 2001 From: Brandon502 <105077712+Brandon502@users.noreply.github.com> Date: Thu, 9 May 2024 20:38:41 -0400 Subject: [PATCH 095/148] Pinwheel Expand1D changes cosf/sinf changed to cos_t/sin_t. int_fast32_t and int_fast_16_t changed to int. --- wled00/FX_fcn.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 7f8c1319a..88ec78f3e 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -738,16 +738,17 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) float centerY = roundf((vH-1) / 2.0f); // int maxDistance = sqrt(centerX * centerX + centerY * centerY) + 1; float angleRad = (max(vW,vH) > Pinwheel_Size_Medium) ? float(i) * Int_to_Rad_Big : float(i) * Int_to_Rad_Med; // angle in radians - float cosVal = cosf(angleRad); - float sinVal = sinf(angleRad); + float cosVal = cos_t(angleRad); + float sinVal = sin_t(angleRad); // draw line at angle, starting at center and ending at the segment edge // we use fixed point math for better speed. Starting distance is 0.5 for better rounding - constexpr int_fast32_t Fixed_Scale = 512; // fixpoint scaling factor - int_fast32_t posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point - int_fast32_t posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point - int_fast16_t inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) - int_fast16_t inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) + // int_fast16_t and int_fast32_t types changed to int, minimum bits commented + constexpr int Fixed_Scale = 512; // fixpoint scaling factor 18 bit + int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit + int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit + int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit + int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint @@ -885,8 +886,8 @@ uint32_t IRAM_ATTR Segment::getPixelColor(int i) float centerX = (vW - 1) / 2.0f; float centerY = (vH - 1) / 2.0f; float angleRad = (max(vW,vH) > Pinwheel_Size_Medium) ? float(i) * Int_to_Rad_Big : float(i) * Int_to_Rad_Med; // angle in radians - int x = roundf(centerX + distance * cosf(angleRad)); - int y = roundf(centerY + distance * sinf(angleRad)); + int x = roundf(centerX + distance * cos_t(angleRad)); + int y = roundf(centerY + distance * sin_t(angleRad)); return getPixelColorXY(x, y); break; } From 4afed48f58569394f1a71a74d2df26a0b8be7acd Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Fri, 10 May 2024 15:59:11 +0200 Subject: [PATCH 096/148] Use libc trigonometric functions on ESP32 by default - use custom (space saving) functions on ESP8266 --- platformio.ini | 4 ++-- wled00/fcn_declare.h | 2 +- wled00/ntp.cpp | 36 ++++++++++++------------------------ 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/platformio.ini b/platformio.ini index fe8b3a278..504a1f3f7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -339,14 +339,14 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA - ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 7064 bytes FLASH and 975 bytes RAM + ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM lib_deps = ${esp8266.lib_deps} [env:esp01_1m_full_160] extends = env:esp01_1m_full board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP01_160 -D WLED_DISABLE_OTA - ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 7064 bytes FLASH and 975 bytes RAM + ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM [env:esp32dev] board = esp32dev diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 1b25c8926..2a9d0bfcd 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -398,7 +398,7 @@ void clearEEPROM(); #endif //wled_math.cpp -#ifndef WLED_USE_REAL_MATH +#if defined(ESP8266) && !defined(WLED_USE_REAL_MATH) template T atan_t(T x); float cos_t(float phi); float sin_t(float x); diff --git a/wled00/ntp.cpp b/wled00/ntp.cpp index d473186ed..e2c99045a 100644 --- a/wled00/ntp.cpp +++ b/wled00/ntp.cpp @@ -2,20 +2,8 @@ #include "wled.h" #include "fcn_declare.h" -// on esp8266, building with `-D WLED_USE_UNREAL_MATH` saves around 7Kb flash and 1KB RAM -// warning: causes errors in sunset calculations, see #3400 -#if defined(WLED_USE_UNREAL_MATH) -#define sinf sin_t -#define asinf asin_t -#define cosf cos_t -#define acosf acos_t -#define tanf tan_t -#define atanf atan_t -#define fmodf fmod_t -#define floorf floor_t -#else -#include -#endif +// WARNING: may cause errors in sunset calculations on ESP8266, see #3400 +// building with `-D WLED_USE_REAL_MATH` will prevent those errors at the expense of flash and RAM /* * Acquires time from NTP server @@ -439,7 +427,7 @@ static int getSunriseUTC(int year, int month, int day, float lat, float lon, boo //1. first calculate the day of the year float N1 = 275 * month / 9; float N2 = (month + 9) / 12; - float N3 = (1.0f + floorf((year - 4 * floorf(year / 4) + 2.0f) / 3.0f)); + float N3 = (1.0f + floor_t((year - 4 * floor_t(year / 4) + 2.0f) / 3.0f)); float N = N1 - (N2 * N3) + day - 30.0f; //2. convert the longitude to hour value and calculate an approximate time @@ -450,37 +438,37 @@ static int getSunriseUTC(int year, int month, int day, float lat, float lon, boo float M = (0.9856f * t) - 3.289f; //4. calculate the Sun's true longitude - float L = fmodf(M + (1.916f * sinf(DEG_TO_RAD*M)) + (0.02f * sinf(2*DEG_TO_RAD*M)) + 282.634f, 360.0f); + float L = fmod_t(M + (1.916f * sin_t(DEG_TO_RAD*M)) + (0.02f * sin_t(2*DEG_TO_RAD*M)) + 282.634f, 360.0f); //5a. calculate the Sun's right ascension - float RA = fmodf(RAD_TO_DEG*atanf(0.91764f * tanf(DEG_TO_RAD*L)), 360.0f); + float RA = fmod_t(RAD_TO_DEG*atan_t(0.91764f * tan_t(DEG_TO_RAD*L)), 360.0f); //5b. right ascension value needs to be in the same quadrant as L - float Lquadrant = floorf( L/90) * 90; - float RAquadrant = floorf(RA/90) * 90; + float Lquadrant = floor_t( L/90) * 90; + float RAquadrant = floor_t(RA/90) * 90; RA = RA + (Lquadrant - RAquadrant); //5c. right ascension value needs to be converted into hours RA /= 15.0f; //6. calculate the Sun's declination - float sinDec = 0.39782f * sinf(DEG_TO_RAD*L); - float cosDec = cosf(asinf(sinDec)); + float sinDec = 0.39782f * sin_t(DEG_TO_RAD*L); + float cosDec = cos_t(asin_t(sinDec)); //7a. calculate the Sun's local hour angle - float cosH = (sinf(DEG_TO_RAD*ZENITH) - (sinDec * sinf(DEG_TO_RAD*lat))) / (cosDec * cosf(DEG_TO_RAD*lat)); + float cosH = (sin_t(DEG_TO_RAD*ZENITH) - (sinDec * sin_t(DEG_TO_RAD*lat))) / (cosDec * cos_t(DEG_TO_RAD*lat)); if ((cosH > 1.0f) && !sunset) return INT16_MAX; // the sun never rises on this location (on the specified date) if ((cosH < -1.0f) && sunset) return INT16_MAX; // the sun never sets on this location (on the specified date) //7b. finish calculating H and convert into hours - float H = sunset ? RAD_TO_DEG*acosf(cosH) : 360 - RAD_TO_DEG*acosf(cosH); + float H = sunset ? RAD_TO_DEG*acos_t(cosH) : 360 - RAD_TO_DEG*acos_t(cosH); H /= 15.0f; //8. calculate local mean time of rising/setting float T = H + RA - (0.06571f * t) - 6.622f; //9. adjust back to UTC - float UT = fmodf(T - lngHour, 24.0f); + float UT = fmod_t(T - lngHour, 24.0f); // return in minutes from midnight return UT*60; From b209b1e481715716825d6ad2b34f5abb924dee05 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Fri, 10 May 2024 16:01:47 +0200 Subject: [PATCH 097/148] Peek on/off fix --- wled00/ws.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/wled00/ws.cpp b/wled00/ws.cpp index cf09d592e..d0bac144d 100644 --- a/wled00/ws.cpp +++ b/wled00/ws.cpp @@ -206,12 +206,9 @@ bool sendLiveLedsWs(uint32_t wsClient) uint8_t g = G(c); uint8_t b = B(c); uint8_t w = W(c); - //buffer[pos++] = scale8(qadd8(w, r), strip.getBrightness()); //R, add white channel to RGB channels as a simple RGBW -> RGB map - //buffer[pos++] = scale8(qadd8(w, g), strip.getBrightness()); //G - //buffer[pos++] = scale8(qadd8(w, b), strip.getBrightness()); //B - buffer[pos++] = qadd8(w, r); //R, add white channel to RGB channels as a simple RGBW -> RGB map - buffer[pos++] = qadd8(w, g); //G - buffer[pos++] = qadd8(w, b); //B + buffer[pos++] = bri ? qadd8(w, r) : 0; //R, add white channel to RGB channels as a simple RGBW -> RGB map + buffer[pos++] = bri ? qadd8(w, g) : 0; //G + buffer[pos++] = bri ? qadd8(w, b) : 0; //B } wsc->binary(std::move(wsBuf)); From d3492b5b6c38e6a8cb2136bc8d7654a4594b59df Mon Sep 17 00:00:00 2001 From: Brandon502 <105077712+Brandon502@users.noreply.github.com> Date: Fri, 10 May 2024 16:06:37 -0400 Subject: [PATCH 098/148] Pinwheel Expand 1D Bug Fix Removed holes on 31x31 and 32x32 grids. --- wled00/FX_fcn.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 88ec78f3e..17a504ea0 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -639,7 +639,7 @@ uint16_t IRAM_ATTR Segment::nrOfVStrips() const { // Constants for mapping mode "Pinwheel" constexpr int Pinwheel_Steps_Medium = 208; // no holes up to 32x32; 60fps -constexpr int Pinwheel_Size_Medium = 32; // larger than this -> use "Big" +constexpr int Pinwheel_Size_Medium = 30; // larger than this -> use "Big" constexpr int Pinwheel_Steps_Big = 360; // no holes expected up to 58x58; 40fps constexpr float Int_to_Rad_Med = (DEG_TO_RAD * 360) / Pinwheel_Steps_Medium; // conversion: from 0...208 to Radians constexpr float Int_to_Rad_Big = (DEG_TO_RAD * 360) / Pinwheel_Steps_Big; // conversion: from 0...360 to Radians From b9ca2cfe90f18b0d5ebfaa1361d281fb98d7f0dc Mon Sep 17 00:00:00 2001 From: Michael Bisbjerg Date: Fri, 10 May 2024 20:55:10 +0200 Subject: [PATCH 099/148] Fix missing conversions of bme280 values The BME280 usermod uses a multiply-round-divide approach to cap the temperature/humidity/pressure values to some number of decimals. But the divide-part was missing in a few instances. --- usermods/BME280_v2/usermod_bme280.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/usermods/BME280_v2/usermod_bme280.h b/usermods/BME280_v2/usermod_bme280.h index 38930da5a..ae6eba89d 100644 --- a/usermods/BME280_v2/usermod_bme280.h +++ b/usermods/BME280_v2/usermod_bme280.h @@ -368,9 +368,9 @@ public: JsonArray temperature_json = user.createNestedArray(F("Temperature")); JsonArray pressure_json = user.createNestedArray(F("Pressure")); - temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals))); + temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals)); temperature_json.add(tempScale); - pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals))); + pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals)); pressure_json.add(F("hPa")); } else if (sensorType==1) //BME280 @@ -382,9 +382,9 @@ public: JsonArray dewpoint_json = user.createNestedArray(F("Dew Point")); temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals)); temperature_json.add(tempScale); - humidity_json.add(roundf(sensorHumidity * powf(10, HumidityDecimals))); + humidity_json.add(roundf(sensorHumidity * powf(10, HumidityDecimals)) / powf(10, HumidityDecimals)); humidity_json.add(F("%")); - pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals))); + pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals)); pressure_json.add(F("hPa")); heatindex_json.add(roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals)); heatindex_json.add(tempScale); From 43d024fe429c5f519fb50f80fe75c8d184c64aea Mon Sep 17 00:00:00 2001 From: Michael Bisbjerg Date: Fri, 10 May 2024 22:43:55 +0200 Subject: [PATCH 100/148] Make BME280 usermod i2c address changeable --- usermods/BME280_v2/README.md | 1 + usermods/BME280_v2/usermod_bme280.h | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/usermods/BME280_v2/README.md b/usermods/BME280_v2/README.md index 0a4afbf1f..51e93336d 100644 --- a/usermods/BME280_v2/README.md +++ b/usermods/BME280_v2/README.md @@ -7,6 +7,7 @@ This Usermod is designed to read a `BME280` or `BMP280` sensor and output the fo - Dew Point (`BME280` only) Configuration is performed via the Usermod menu. There are no parameters to set in code! The following settings can be configured in the Usermod Menu: +- The i2c address in decimal. Set it to either 118 (0x76, the default) or 119 (0x77). **Requires reboot**. - Temperature Decimals (number of decimal places to output) - Humidity Decimals - Pressure Decimals diff --git a/usermods/BME280_v2/usermod_bme280.h b/usermods/BME280_v2/usermod_bme280.h index ae6eba89d..0040c5efa 100644 --- a/usermods/BME280_v2/usermod_bme280.h +++ b/usermods/BME280_v2/usermod_bme280.h @@ -28,6 +28,7 @@ private: bool UseCelsius = true; // Use Celsius for Reporting bool HomeAssistantDiscovery = false; // Publish Home Assistant Device Information bool enabled = true; + BME280I2C::I2CAddr i2cAddress = BME280I2C::I2CAddr_0x76; // Default i2c address for BME280 // set the default pins based on the architecture, these get overridden by Usermod menu settings #ifdef ESP8266 @@ -48,7 +49,7 @@ private: BME280I2C::I2CAddr_0x76 // I2C address. I2C specific. Default 0x76 }; - BME280I2C bme{settings}; + BME280I2C bme; uint8_t sensorType; @@ -186,6 +187,9 @@ public: { if (i2c_scl<0 || i2c_sda<0) { enabled = false; sensorType = 0; return; } + settings.bme280Addr = i2cAddress; + bme = BME280I2C(settings); + if (!bme.begin()) { sensorType = 0; @@ -399,6 +403,7 @@ public: { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; + top[F("I2CAddress")] = i2cAddress; top[F("TemperatureDecimals")] = TemperatureDecimals; top[F("HumidityDecimals")] = HumidityDecimals; top[F("PressureDecimals")] = PressureDecimals; @@ -426,6 +431,10 @@ public: configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); // A 3-argument getJsonValue() assigns the 3rd argument as a default value if the Json value is missing + uint8_t tmpI2cAddress; + configComplete &= getJsonValue(top[F("I2CAddress")], tmpI2cAddress, 0x76); + i2cAddress = static_cast(tmpI2cAddress); + configComplete &= getJsonValue(top[F("TemperatureDecimals")], TemperatureDecimals, 1); configComplete &= getJsonValue(top[F("HumidityDecimals")], HumidityDecimals, 0); configComplete &= getJsonValue(top[F("PressureDecimals")], PressureDecimals, 0); From 6b8d8bf735c3c4e3a04a2302c89c1f1895ea7a0b Mon Sep 17 00:00:00 2001 From: Adam Matthews Date: Sat, 11 May 2024 13:34:35 +0100 Subject: [PATCH 101/148] Update Battery usermod documentation Improved wiring, installation and calibration instructions. Example screenshots added. Minor grammar improvements. Heading visual consistency improved. Improved vertical separation between sections (separator lines added). Thankyou! --- .../battery_connection_schematic_esp32.png | Bin 0 -> 131927 bytes .../battery_connection_schematic_esp32_v2.png | Bin 0 -> 65817 bytes .../assets/installation_my_config_h.png | Bin 0 -> 50412 bytes .../installation_platformio_override_ini.png | Bin 0 -> 55011 bytes usermods/Battery/readme.md | 152 ++++++++++++------ 5 files changed, 100 insertions(+), 52 deletions(-) create mode 100644 usermods/Battery/assets/battery_connection_schematic_esp32.png create mode 100644 usermods/Battery/assets/battery_connection_schematic_esp32_v2.png create mode 100644 usermods/Battery/assets/installation_my_config_h.png create mode 100644 usermods/Battery/assets/installation_platformio_override_ini.png diff --git a/usermods/Battery/assets/battery_connection_schematic_esp32.png b/usermods/Battery/assets/battery_connection_schematic_esp32.png new file mode 100644 index 0000000000000000000000000000000000000000..3890f477edf3809c419c837616f95497519af657 GIT binary patch literal 131927 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

lls+tC92^|djZug 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 literal 0 HcmV?d00001 diff --git a/usermods/Battery/assets/battery_connection_schematic_esp32_v2.png b/usermods/Battery/assets/battery_connection_schematic_esp32_v2.png new file mode 100644 index 0000000000000000000000000000000000000000..2dff54f8866dab07c0257faa9715a25b45c87ce5 GIT binary patch literal 65817 zcmeFZby!sG-!Dpoq>4&|A_7V`NGc_gg3{e79YZ4mA|>4|pn{~*4bsEVAu#j+Qo_)A z?oprT_dM^}f9yZbKIhu|yw^2cFvHARYwmTg`}?Wy3RO{(CBmn~M?*s+l6xYhiiU#-hb+Xh@m0G&2jrp^+pm|+ z0+uddpTZoODacmPpSE8$jcIi6?XX3^+SXjgt88{zqlp0PLnPmO-A~9bH_;Ua)=rwj5F%>2a>J#JH2-gn~)So!a z46NjC6XdJEHt_qs;7?wOJ4P*~Z5bfLu(QA43;rZhd7{a=QOY&& z_jZ5n!qWe7>no#>h=6~-7aR`ejqA4vg>4C4ejmeMheMh5?!OQHe}2>>N6{WB3_3E1 zj#IZ!xR3>qIPmhoQda1PD)7TS>>%t5D%z2JGt#CjH@nMf8ngGrk%-AE~2uEDd`C$Wapj@18oEN36%I}8m5&=sscFKct7acEoBXx`Vci@ zx&-IpeEGrG-SoW)5bWk`ewbI&@{ds`r=c4{Jg%R*wq|^T@9x%{aJd)X2v0S%3cPXSDEW?%kG5$CVYf#~O_3*pLTIwCic6CH*?Wj>@XG3_1?O{r;U+UjVp_d(y`tibFyav3rP?Z3W{Cec;jjn#?NnET** zMO7+o^~MclXgMf9l2$@7DbY5dQq_^<=?v?&4~B~fB_-H_7X-HiFDmOxqodWBy;Ql2 zAnf$_L_1%zaVW5|>9T!YX<}>oalmp6>*_T1omUk~SwZ-eAJQef8eBB(;qbe^xK%A# zIpQn7i(vb*rIXO5Q~UUU*Untut5w;TzD6=czCUti%%}JI(FOq>WTgIL!S7NBzazYA zOCzCcvQo{d9u@|R7@s|bpT<;q$Ggs!OTnkH%WKT-kl)oV4>b^VrgTrrwsgLCDfM!e z2&{AX@s%{4d`rCN*Wx8b18hOV&{q~X4?`dFO#Bdqryb^VNkkF)JsFUQz~<*o@!O`q~3dT)tv?lU5n}<1`8ZB?i7xn2Z16B$?RWJIH2Gln6by zPc4|DaW{ncJYAM5=%kd}6WDz7G1ZIX*!XNx#xq>T^}dNp2nqR`KjD16)Bq0`Kku%; z_(7)6DfzU2qCtn-YwrzpYLR|})WOCM4-KbZW0B{U5b`_Ec1^+2M4Hp{?f;x<2Rv3( z-L~{+9Y9(&qVBssHbf{2w!7*R?p=_3N3j zJC&0J-M7+)CtQ4EM^e;!$+JSlJ*39c4DK@Z3wdVlxd@3voZfRys1_!q88l8e+EXl? zo$QxaSq(ptydvqiHE;)9G*`S2umjLNkBo%G&EC2;aeoWIRONLtU8-p7N}sJB{Pm%7 zv+^1lBII{Yi6izEZtkMfgopU}ed~&si*(TS65k7mkhpuX&n$O4SOXavbgH__x;G}( z`nRX8W~x{nefz1kv_8f_?8t9B+eXq=ZRz}YQi=%dLjFiuWKv40y>qQJqsm#4^7h&7 zK}XiYX74uu)u0E)M5|eXRcaC4lMlMrtY zC7`kyD^A3}`GA4ZSNqXR-yL%k|3-tuQ`PyG9Ghxfr*T{__ZAHF3D36N6-`u-$jro9 zspXJ}Os{9h!UWKXl34R?*A))q=0GRLs zt~h7r<+8HR)2;AHCWW)HuhZ7>cv;K3d-WP4QcsLDo{sVQ=i`a4emMo7 zK2JDpXwk|wPG&rvjIdHjD!xPRV026d3j{vtW?LKP@1+3kLs1cv$>W%04b({C4wrvk zW5)=hXA?%j!9N3pnU8evjO636Wi|NPp?xd_zQ;}~{_dKWINMF%717IRvhh>MoyB4O zEF1*s&bs#)M+$>& zpywWxlEL5(fmcK>%QIK#8`r-{>WeJ+_@12yk&Q!)rmDU~TS7ZK%EDP~(y#Gr!lv#< z?~4GE)%+TO*NJ+fkmjf8buya_J{Fc;65d67jPEWmf_eU7M=8MTMyWfJ!W*Vh;LS%)dDkuf!kbg=q??o7msf3#@6s z2^`To8p^DrzZsk)=KJ3~kXHf}i-rFXK~Nd~@WRP#^tUAc6FZ2<()d7BtSS#pT-sY3 zjy5qFKD)0`W%d(~H#0q*?et(1sWyWKwOFB3o)~=-3q_kJ?;JiW)~h?Sn9o9gt6V;Y z^ao@Ue1UFl&g)kGs%mO^vQf0V2UD&~P=VPvx6SX}9T8OL)8#xiW2}(*ATragcXyoH zy-3DWW*dF#(nPe*Qi7xlOqV`9Q_Ynj`lwbA$?D)>@POBxlu>~*0$S{Ydp=?e_u3Zi zhC=tNp2B|4x0$VveMX25MdtNp-<5?rOPQPJf3X_wn60o~T3SjPf3zVUM{i}-X?n0R zIrLGDDb@|+a2LPj;`|h9({S>m5Rt+Q6G4VQFH~2=Atb+zjg5VwZ-$9uJo@Pcx6{%S zt#aEMjZD2yB>c7$x7&h=kX5aVwHXH04%zp`2h3bVGZPb;Xdb$}Gl!-mZP z@`rH845HBhc&G2ZBRzbZL%;Shza5+vOySsZeJ{oQkop}aLX{@pbJK0NDFHA4aj(v-h z#zVC3vk1cDAH3kkxwdnL{N@*yDb4{+6*xR(rTS_;8uJOxRiagqe<=D%e*cI4ljWXx zs7=#O2Nk#bmaZRcC~!N%q{3sz7^YwbNF?>}T*3SI| z7d9;)hb>IuucY%Gr7#I&Up&9Dr72C7b%BY~Q-7T3=`$dh+j5_U{8KZ9S941ouk%49 z4sC?4CMNI8X+b%8c^;SvpI^%<(ZF3Y9C96Mx+GZI*`f^62r7xTcYUvQf;UQEki4sW zC`X#WBp;3P)iJ>>O39GLn+L*kj5#o|)1r|=4H$K%`;XB?9Jql9$-+Rk?6e1@c%xv!5wc?pdqOoH?& zcn?Iq_7iThXdDaC7iyFwv;^Xw>g26}xjgh*a^X&YLEWoIN!Ho|$%Z!nD8VNPPQ z>N4xS#d4BK5G^t9ZctFrMaF!KqqyU499OPomRrZ0U&3k%tICZ4JzNsr&Gyyw=hFD_zMsNftVW4KCabD(KZr2|p zDiK7KPJ|RjkvA9vVLH|A-eiOX#`Nwh{pshTy*P}IOumx785QxJjieDi(J{m9q#&Ih zOdZKFV_6&eTA17(F^g_~k5%x1qv@=iV%K`SF)+DGt-;fkr-Y?%867RsCsWL)&;Ham z?AFjnXEGiO+|2Vp5-~ix@=^u5*eousI=$1#p;2M}BRj56=G8KnN4BEdkCEvAb!KQr zM%)yA_+Q#t7T z>X5+3pED}Y_xvQk0GCKXuf(V`?XEu@lQ+T0Q_W$z>;_HY#x6mmyt+~bByyt8-G0^V z=h0@Z`}Rvuz8t}^{R{vcY29U-Ukl?VoZ1dUTy@j6+ERFo_^DLSTUDg%|2Cgy9C@E6+Pa<>$5^zOm;^y7q3wOiB5 zU^FNh1>@NjHe+zQkh@*y^<)S%I~m*;5fkRUX?}4t*o153jb^#&$G-F?&hm(I3C!n& z>k!l>@SAT*Ad{xUj0knt1UF2FXX@NjY3b6eWNmymzkkgqqqjBj54&N^cO8#6DwxdT z$ntd?`EA14Y%ap!jf7%N8m=2?vn+jSH>gRy)!Rd33+|PaGM7h4_>w!=4lbe@^V#>k zd?O??*t=$jgNJjs&zq7^6o;%V+>X%jF(&fGt)DfVx|COSV2cf9whcwi)C+u9SFdV#1ywm9~?{mAx3@w}YFF zOE9rGMS`-sIh(6<4v9JiYPtNm+RZRpi%bL2Tc_#CCE80#WV({LOz5XtM%(q?b$Moh zL+jRtVg4RQ2Ah^Aap;e8U(uo~?9qsrCZ+uhiPLpPYzWB-rEGcUDN4M&aFjcwG}Y)} zqpU~F=Tc;Av`YuxDV-am3>^?5Wb>RHP~&qLxJt2-jIY~#P1GdGkorK-2#+W>#%pi& zew%D0xBLdzGm5GgEk9Z~4L$IkeGFxAh^bB01Ll35R-@YnZu9D>|7=RSB$di+Jcqus za?$qmtZi~%Yx&fZ<<3@_5n@E>Ob}yqXTVijvXvfc(`?@2Rgu7(SM}i>BPQ;J!{CT2z{F*kf zRE8Mmg9R)DVH|-(u)WiAMx?AfvW3@A&Q@&9@hs$*YWjq{FszQi4Hnc32web+Uvy-8 z9l3MRd{=3ez?J5!bD#``qd$f*sQVdtbboDe)q^eG;&=qpu@xl|l_qHl3iv$})G=RU z-l7Q^a_UPAS9HI2RsE@FF;Km!p?$G?X&6A@p2$ow1OK293W`Zdt~Lo=j9j$uC7`-G z*7C*HMt?%H(%AKVtIk*R2G`7TsTKB__a+7XjM`P3w`m@_y|ukt`EttYys0oWcTGz_ z=(5(E)A)T&>PdF4R!l-Po4iKRZa=~%=}J!JW!-npdY78#KIg7T_u1^+j4kKox%uZ5 zqHbw5#}7QyY%ksU0#Y?At!HGuf3ahla@$DI!yRVe5r*l$pb&7~9BT;Kzbxh-U_rX2 zKOCx6)UlfU6j4809uEP~Wd3f^rTf=1lS6nHQWLu9HAlSD;%0SsU(&AYWj(e?Q&;WW0#wj@$Sf4OthYEXI92+^|Iu)fTf-Bc|Kd>ra=lhm-Ux$8i5v( zsLv}ENPI5s)GTE)gXwj|KFtZ6ZH6H#d}K#F13P zR@R1AvM$B{7S~?Txb|1ih(3cayu07=_8xq{*tX6!DNr)UzNv51y&KQ?YCJ>0&sR## zEOb$tsgF(g)$IPyK?TSp&%J7urPiRZzU`Dmg9GkxlSq@OJF}Y561^&TQE@oDsff$> z?DZ3$cd3aW=I<5p)EYUu|fGY_QRc0!@R3qUHyrZ&WI!k z{-f8)_5s;BbH;vUDV;wF8Ha+=ogcs2y3~Z4)c7RHjgdcSeLdx`3C)cr*wWO$bzKw} zk2HF4g5H7Wc@or+%GrkVwW@lSG~qH6g|Ng%EPWq&J*~$vd^K z>wREWn&fQPLy%{mhXTW>7ak0>>>GoET0@qv7)p{Qf}VIP$jBH1E5+u6()?O#}ElJF{}5${qgw{|?4>T6= z{ed_82fy-TVmXYi7+Se}0bu_1d>Lnz*BhKp5rKes9A3YL=Q|EB=)3yZ6uOc{vJk-6 z<8qLuUx)ltg#c_0Z#=byb0SU1*|f9RCnh=GEI6UG6+hO$jrSz!4RsPrDq>KL6z9zE z=dm$g6u}U7RW~yM0O(=UXEy-&AiE&&ig=HP@~gW50{X|X$zWpr^dwLC^n6VSvfIrB zeMiDrZro}`?$C>&@To(W04sqG7k15P8#{QEzr|)@TmD(e`Lr`EsFsWz@xW+;#iL;Y z7D@H3mXiI~C~u&c&tE=wx+$Loy3nz-6QW3XrWtMiQ%6!%Y=T>#t~UuR(Bty_5MW!G z-UK!%k=%}U2ysDAbgotOc!yqF8|CdjJ$!xp!$!&I6};)fw>WQbm}!4&H^3_Yg|c0A zZD{4Y0=rxJp&<=b>U)=+*0fcsc*B7~FdEhXTyP=Vgfy6Vp`nzS|JSvi|K(!R|9A&# za2!j(Yq-6S9FjSWUfE8RX17NuI1Ue^W8yryyf`-lqDKBud*dLN;hdpXy=mwr-OFEz zgLG3KiE2^&mzS@SnhXQ_(nLE&W*fY!SoP}#oy=U`#l+AHyKnKrG>SBh4s;s5Yp4Ys z=H3e2$^WPFMb@7Yo0jGs?YxrKiP&FX#wjVUV~<4lL@809UM~CFNdcEK&!EYd8}h?ta+Tt>mBrAdGE zT6iIW7y)Kn^gPW6(*C=`P_LPH0#ak(QbSR-4hbeB?8To(HG;06ZbVWrWe_;hp4Pd= z`$6Jo^f)E-&8#tTn5!SH4Ccx%AFpHzz>GXpuGrL{Zq=2$ZE9PK5s%c<0HzuzJB=oY zCb0EcHSg`x%1ZC^lHz7LSv10{FPjMlP6OfJ0dY&Qql#rxcsJ|mivHuZ+TwM!l^fv) z1_H}N?he>|C2H(Acm@w1mLuQxp`$?4(az2tkax+MJG$?T2GR&XwH*Muy;8W{{|&U79FVYq3XU%a-WD+Oj*FApBnr&n0$h~qw(?(1VkdY>N9?4GQb zq^@uZ*w^m_Q$KX6ZohccjP_ZpA~+?AbfjLiKfTVdFB>Ss%@g!s*YwdTvXE)7EYoDF zmE?GSyYEl;e~jj3&Bf#!`m3r&N>x@=I4^}VWxV6b_((F8W1>`|zni2%=5l^AZgxU< z$;@-H$8@-};>GaQ*GH{NPen{tD==j|SA^F9J(B(5L4moPxxP^-uSRr++HI8P{>wO( z9V*`i7OJuG(_oOjcyG4cYqX7=+SSF16pn0SD=CUj1+38*aj{&&y%i~ZoecO5H%prI z$CQ(7~;)+TDgTvkW4K0$77ESRw!h?j)r&<=ZSPTt>;+z5<7nD_YdiZ@ zpge;6w66lDudW`Lv=P5k@UpsPoZo!>K(PJWz#w~9S{Xs&V~>N#A6WSwmS@+ zvK1&NhMRA9FLbi>487el856&_v+cXgvire(tjgxX`!YiQ6^~Ni+S*iAiOY|XZsmsd zwc!G@v!k63y!3X9gZZly@!5#Yxs7W#EpwQo=Fv3Ti30*alBvgQyMYPlsXWM^7&#%$ z&B;6#?{hS^!rpFly189mEg1hkYH_ml$NaNx%(6`@n|TE0_A6C4!jlv>r$he!_%4fVP0P-oqUjl8!r(NQl##2y&+ zA4PuI&cC#;GApZDr1R-@)a!p{{J8DVA;wa<=>igMgo2QQSu6|MWT<-a)W`6ttk^h%e2zZgqZkc`zV!D+)j#ipY<7+O0Bdr8%Zj2 zI@z-#wfp`hIxUUbPdOUQ$KKW;0x0reb1L=HYA`1Vh_$Qb+*AbrKvSAO!`p&!hxCg z&d<}eMQz-GL-wItTpz7yn(WhpV*PY};&_7Eq;q#S;OT*q$?P$FJJKAHOvlVWBR$tC zgmSQ*QMv*Gw|u$gP23)}O>J0nRBZIGkm+wC%q^>wk(R+JHzsyb9bUH>`TASwwvRIH z&VvRv9t(p5it+~7Pf#f3x5a#xe@5p<@3rQ{?heUw^tQCg_PN!$ZJM2gCnJs*@_Ls< zVZ8Ilj>_wbDg3c=u?12W<7ObM$B>6O_uu*LE8~$XqrBGY;+5eco(S|-WPK|sx8Z=w z+C-TegWq|uV#fxr^@!$MrHsh;O~9imx4&%Za(n5U{k%x?S-BHf#XYOGcHdk0%FK8c zel2#mxt{6!eZ-CleZ$oRLZoz61`3f9er~0=7CwzJUh~QFT ziUNC0bf=wEwLnP^^4a=@v+A`t0Vx^vO0Fm8Cx;w6eq}g#W@M69rzEuOWd>IP#R4mN zo3;O0Mi$;tfFPDRuDDXplb+&d3e@_Q3hd#uY`9bI@4ob>r+4Oi?k`!Dw0faQ8rqDX z*xTYFy*_D_&?Zaz8*wT)>4U|&{t)iPNMwjt1iWacqn}2AmoWTZnSonOtrf;}Ev z{$iaS%}VqH9q|b*CPHivprAZICE4B}vPc3BiSNsu9NC;;KA2VQ@<`Ao+f#7bZCT|S$!xI2=;T}L~MirVDABI+xP zFa7hC(jUM?57WNbjH4Vt#qXe=E-xnf?Jx>b(0h!!M?qxz??73vh^QF%w;IluTVdCWKt$09n?IPF zpv~n3hyXYC((9zF~t zwmxU1xJ4LWy*ZT9B!<_}-*;&g`_R*Ih^3C(uMydmxzSK;fl#Tc3H}@LzP=I8lc6cP z7(H7STjaTOPYW4ynsj-zsJa3oRU!5G_93%jr1D?TwU?3^(*inB@rq3|miMr3?|PeruDupS>7-C7|N~C35vBg78BhQUXbicucYm*s-D~?`m>7 zN!ztH1T-~IE|&3PY`O11>CMIFC^k{k-aW00jf(PL3}N>v4C1OSt5zj9@-u_r10>yv zOX5j*B<9D-{GqVln!OVa5FgIRwJw`(lVX^-MA7;U zrwLEwd%L2oM?dwx{hAtq5O&?7hr(^xiKzGzJol+QEOHcqN}rYi67n(6kdIIi{fiM+ zU@QoxBwL`O^SUEQ`;iZKsQB&pU^-uH?~yVo^TBfYqPTf&`p>#9Zy(S-q%V11%z9sz z=jr>EC9>r4I-Sc_1F*)GzET{l$J({}!vR=7Q@}D!@18K0?GD64HNX*jDdJOzi5u4)dfH zmC#i1U$L92j8CpGPhJ^J@jIMxH`|H17kw$@xch#@en29DUFVh@f$_q7xuIHD8?^&t z2=NQ>*;)d_AabRLoKZ{(-@?dJ z<%SAG+O9=)lQMSMH`JoJw%(6Zlm{_4e3{b{ub)|$#ak#jRq|I;L_|P3K~U#C0k*bN z^!mj`!O9}dJDGedbYi;b`IKA8OUqOfOUvf?fyUOrx7t5T+z-H@TT=ichIZW7+8 zW85V1*Kbc>Rm~J>@o3e#eV!~R`n^iTK+%lod2Ufs3pp|5Konm2uB5vw2uWsHxE=kR zH@~zj{Xr0$S7N?{-Z#9julG}nBhtddwLcW-P*kpyH^Vix{%F(Sveo*95^^{#y@;8k@hC^DJj?@?1bL@v7zqH>%Lj+R!jVx> zMQspVv{7~Ft*$1%sp~H2X-~4IvsuSL5C@C#aUcqu zbCbQlTS`BSli1Sv9|3olS7P;Z%XMw64B5b~JyOBKFpOZv32{6HmyY^Cv@<&OS2g}% zMMJoP)smTor8nsYR*#B=!=3i?5Y?w?B3UBXr?>&*Xk}|}R>#AGfjRhh;11L~CvcWQ zRPVUyrT+rZT!#Ol9HS&Pu)?Dj%W`0X{=M3xi1nYotAFd*r~u2amMqHK0IB_jy+H32 zNR0hDKuiCBNz?xa@1SQGjZME^)AMl4&KNiVjGGJJ!npk|yw~6jbuMc|(g@I8;nNY5 zBIb+;52qGVJ=mJjd5mOHFM0OEPwA(l;^Rbc^pn#ymd5Ja6sosW;~;$If3e}1o5=awIXy|%Wd+T`o= zQT=lYpqL^#5b_VGD-|56;t2sq{cR@Y;9-_uWe){0l+px^dPsW6dv5j^r9X6iZ!=z8 z?l^3&RqgSy{uq(U?R(}?A>N1IGY@ujciDElSnu90N)%9wTtvs*lPh7b9t_KA3;E`3 z(0|vf>9F>L-}PVq!Hw&nWA6$JxxSg3EcII87%+iF669m&a#d=vymWQnKA0V)0j-QRxtau@#!E$Qu|GPvGbY~1IHE5rN2y(K31m2q*ZQeC524ZngmkwsOfyG zH1HzutR~Cz(uBN<+b`qzQ59Pi>6>*&9(KPJQG>^8`p6{Vjl(n8IEcZyZA_bGxUa+Y zIv_YWpj;bNz0}jHd+D>}ca>QUh|fHt(t~SY(73Lp-=eDU+KcOKsLi^pYP?74cY%D} zzEroBa6UTA={hwT`?B}e?}GjKXZ|DVr@dU_DyUJ2ijB!YHSx&uAp1y6!Fc@cJH-%J zvNm6cB>E#VKYJI=CcVhm_3D)Foum77bSb&dl-=&C>v(tY8qeoUU{?2YJMKP5dUp+w z)b~_(nw=8T@auZ+ZM>lWtanPx?S9ev27$y^?(z=^TpoS!VhM#lU8(rvCz@D+)Dj(@ zLW0qOG}Nv{g(8sQOn(pypY=#wgpL?5EVbb$W8KjjtP%BBc2#lZIw6v zSTFym&9_pvI%$zNwj#zv0V!zd#AkvKJdpF7Bv#gK#9NuvLB;Bu9! zsN5z?U`u>8iIhrUbEfseA&L?AxCtL`7)WT5rOG-#J?stRXk-Qo#lwUoOe`MK(@UUA zbX+Oy!8keGri0AxRwEytg$hY%(%cfowLILKQ61(XPogqiv?jdD_{eXn zkid8STQ@d=RB1||;}UdV-+kYDTUKzssZjWjj|+^bu~&|HWbp+WOFq&}Biqs*hAxsfd=ef+Ma=RbCL7PEp4Y4r6!2Gp#4BtBm6~1WEa=LF42+mj zz<#PxGU2nP9pW;aR`mN4ov@pNzVESFi>iB^l~gG{>zp;w)x;K5Yx3_GVH>y_K~QkW|1)`Q1F;1Jrx`{t8rclztXz20?1z6YApxL->Z0au)50D zt93M^<$Ht*V1b6#i=^pdWu<`%%?12I@hbA7eyR4r|E|%$+QR-9qYSZPoK=7}Q6jUS zJd--|C^L+V<3wj3TvPzOnes~nb%rr8iF4KdtG58?-GIF@#j|K6r107NDuO`SNda>9 z#Cr|E%z-g-`~Lm=j2wo5CNSE6dwcU5G*bkks{O_C4k*?MfKGJ}<;kBn^#1N$@SY|b z01il=YTnZ!kc;o`@8`E-&P1i!$3LmZqpX$hUu~FxI0d#im?Mq2lrj?tQmdX^^o-K$R0>AvnRND~144f9Z$e(thXevJ1(MtD zJMVBtKTmW@MS+wIwuJ52+AB)WFM7E-^1ytd;`j+d|~QG+&MofWINn(sa1~iZnKd z{m!u8fs$^>KOP`gp zd>JChPB7IHoqkZ2&sU3bR?u_8r=RzRHf`b#>+^O3GIm~=W5?UKl|YQYN2n4)Ok?Ug z;?+zTw22KQL~x4$hE#M)A*c8ELv9a<1ZNq5-UT7BqRJ^ND(**4VSk*Dw}PTU(}dJc z4mQ{A_4-|4QldacfO)yLifv;t<|$IGlpUTYIJKOL>CbnPY&KO;{if$1Byty@coqx2 zDFbawbaR&G8K@R>iq)ukM^pFqRDqI9?N05dqn~`2MMEhflUe@a zSee4Tljcg;vnu$5D1G;I+Z3VR6QPH$Kj>o%6nJ2r;WQSNCWjQ94(|x5dEZNEpAyrU zUA&UM20h#w9zPS`#Sc@G%k79T+e<3DcTw;KRp8=M4s(HVEw|%>%AWx_9(C)6R%ua# zTp7j5a@RTtJl{?@nHTa2Li_nRID)dfRa&0@GVp_XLoM~gG;MmK0Pe$Ji%7RExR;jx zRkgbPcW;3ckZWHD)z+wfdoxPJ`bXtr4fYu98SwG~5U1U^TO84Baf9m5DdU@aOj}g$EY^|-1Iw5lkR7PMh*ndF!>ZU4vLLl&RGnVA} z^sG5hGE@1ylo3#atH*Rwt}rwjp%is`qB5s_YpbCzYi4VMT)7q;kZECxleyE`sfg-O6}(JdB^n?W}E-;a6#&;n8C?oC5d7B($PFF)oGNX-ys{;_2mtWDQAhu-1z7m`Yninvn_1bEn~=enXNicEqcuSVO3 zo_Eg^&$ihg-MpU!WQ>h0`*Np%r6or)fIu@+f&+ki#wM)R@NnL@g#9SW;+0eVA?wK; z=1R1XAtxhMaxHYkkr-_3Z#Iry@~Zlslhm%;#`Tt`ES+Ug9{$>>Mp=j8Kqndl|YyYY1KuKg*(=YV^mlI;KWy3H}gO?xk^{zTt z!FmrOuw%2t*GI8ucXIGZ<4I_oXoYkd#-+~}C(eH5pqLqe$QSd~P}hn=rvp+Vbf&;Bonlj$-oz@n$ADhRLXC->XjP6G8&fM;g{aWy$1Z5WaE*cn|YX^ z2_@Ph1JdE0SmM3R;sgZTu8A^}^J(J&(tyJ@3xY|5Lwb5DNB-tRQEyJ-A5tcQ64J3Ii)!RDdH&P)a^K+6j1uZ! z1^1K}LngzX6pq=M0TUBZt9WEPdvQ=Dzel_=Ri%7!+H^TZPSQ8u+F0ETFAzVsa-MvJ zW{Ej_8k>1$x}=GfBJTPl?zmn6?vcB2Z zj}Rgp0K;N~q&C#YJs=?@Kv?)JkY>IEY=b!)lEk^Un^nb#HoI>jeww_N9){C_iNnAt zKhOc5Ynily79^V?4Co?8V$;cJN%-V2;X5L4jXgnZEKzy16u*V;g1&E;*U^7>S2Ea$ zl7Q^hW6bxlMc9Ejt4<3Pw~KV!9Afy-u7XyPrPn4lsduqG_c!gsr8!LlvOh2?rX(G9 zexmU=dQvaL*>}^%A5BbcAojI>Tq;mdmC{6fk`Ys1tnSX9j8*kHlN0YzOCK6yH?y{& zwY&#IpNXzP)p40C%QWB(FcDOmqv^0=JH7|4;o!SX$Ht^TV!Q4V-FXlyCGk5c_^*b6 zEc9Ax5L=Biys7l{hkX~7{m7fX;TCGbKIP*U5?@2c1Q{3%Z4k6gUypHvK27Unb23CWQeW&1--Q)i<5#zQmy5+rYH88*|rPwyWnxUxu^+bLpFhFhrrm-UH;#C7n9aH1oTo}r@W0y*zigulETo?ll)OZ zhdxMvQL?ySW2Ty|Kc{Yxkksn^nzj7Yz9j?y8t$Y$Dl9@G-l*(cU_qrUMnQxpNSp%< z(KxlJGckkjpygvVjqi^e*k821lb%(@A}!-WpUQ_!=(^`XZ?jy*P-~Ib-kJ zoDxZef$;l93vG^c+@>EO99Ql99l~F2Q;6#-b>}FAE0BA_EOFaug)15i3qPluQCW|@VCeTBxtZ5~+lS;$k zjD&UB0?cmTG@8d7559Jt%23@Ve@I@pQUCb(boTNkqgS9M=Icc=Z`j0qEm}PXqfYK2 zGn#WWt@qDI92e|slMvreUVn7PzZ+G7UIMnk0(}RZiyq zw#PW{ldJK1-jm(F@i?Etsdf9tRZ%P)$yduO0Os>gq2XY6n4d=KC0bpSb<(mJl@AaE z(&lPn=wUTTf=M@vDd_|nulSLPujU)T!DZLXdF2Rn$tWpBLZDfP_??)}S-yvLvvNlB zJ2u5!4%nap>Yq(Ty7D-9!Az8UKMXGvT}a-U``bz&tMjj`+ZlDSp1eHfl$kvTZT=)` zd;1PKL|2=yQWGkdS`(fcYi`2)8f8_Z^lEXgC2W~Y5ITorlorwTZUq`W^uNQ>cLzVX zQsN6b2C5(JSeHba2>LA*^R0CS~^kVkF%U7ngJ;lQ|F2X+sjn{i3Z2{au?B(u_x z6Z-4l=)?5f81ybk!!bt=mYrmZDbR?^J9Rq)y_J(J@+LxNK1JrJF-atXg$9$5Y+=9DlgK&c z>QUd3O|=Yy69vu{865R56+mh{1CyoPR^k4`|d z9w!-n_?9CEr-{e;Z5kPyz3MBfDjpyEa+vUAtM~@If^XV^iOpAXbLQ^rMM`St_K}Zj zW=lzq2qDiX-9HKFzq^C~muJO*Qh^d#5D(e#6uuBWQB>q3b>XVHb@&OK{)x7I6V8tZ zb1i|_pe75fTNXi~pOaXr8x}sIJe869{>~Y+`CGRi)r9fW1Bw?-pex0rbQVUFH+KUQOl}d+iFQGpW!I$tUFT;G-rg{b)#QS0*Yil*79DC*XSE9 zH!@c#b_hkEzJ;AgyE;=)azAj;9H&aLg~-K`t9w-*xB14rDlTZjO+e+u0BJr?>-{Hl zCslKP<3P*5u`W9HKGlqroMCCNTAS4P9`(==sN(HFW&u>*l(`Ox1ah z94wsuFV`nNRt$VQ$1n0_nE%ux5bOny3zWGJ-yL9m4IYy{j!nDk~Wd$10eG6 z>*seddJ{PW+y|n7*!&R~e|Dj}1wh^=1<1UPjETv^HB#fSIuc{VEaMtd?EP<>>`xT> z4mRe^R0~z^hKkyt%Zn;jC5b-IpO38ef;hUGfF<486MsRK$nYI)UYkR(HyrzPuIF3X zTrCG!f@&3zsRH)=ht0JqPldp3(EcRWs<&PkWq<>82sMQ z0iDu*zqUc*C{>j#uekAuX30?T{n3V}p1lWp_YOGZQ8X_X+=Zpdwi!+4N?W&N{wE*?{oQx0#h@;gijK3u zDNqhm$Yi&QpzNLYJCj75pKte@bTl0Hcp03iI+DEeTkO@$@$%;^DL_R9A-qYmfnJK( zbHT|(RQz%G!YmV& znCNJ?^oM7nfbhZgtbL_P@=QtQ8IVxFPrJnS`OKjwZ;xKvTBM7CfBE=vN`a$F5+zft zq3>SGWT6iknHbKmKC2P5OF_l$7tUR2K-``Wv7!zVSj1Nm^jVK1Hf2I413sc_&Wx6u z$(tnj=N|(`%Hf0w`w#WwMOiuiXjmzoORFoHeq!8*?6=3zK=Ooj_aT}YspJ4ZqhcC! z)6X9mBUcJK?&bGCf<nrOpV7#|s| zjADNvjU>Af_kji66*Ka(_6o{P{ZArV8z-vH*CfYmrq=bYz;Qc~fFu9Ocbk&r7tESQ z*L7pD1gb$zaSHXA!i1f^?`LN(e}& zO2`IDr5ou+B_x$pVgU=J7w7*j)a|#w@0)whJ?Gw;J9lQ!p1noZdgFPXfBizUo0iry z_;f-H2^DvZcFqA}a=)b8wAus=e-B*YKyAP?d!8?4AZRGyEwJbK?)M?xFws8fXwtj$ z>AC2i2r;z1fTYI03JXMY`P6BP|2^H5|F0vv{*Ot^{A~Y%_(=5nn73Y$?U+q(XOcJ@ zK=r}_Lv;uLzkHP`edeaN;=FJ| z#Omd*Utt&Vr9#p{D|XBzEcy z=*h23VMZS-4j@p%Lrvp;fPxl}^MkL|mC4K}b_`>-+36Fv5jJ z+d zSw_JA*?QHZRz`bpz{Jd85f1>=3}n|X>0aGB$a)gjciO9qw$N3*R{+hkKaaXBTd_*J}|DO>u4@aFMkZeql!j9Q6VSLk?s zTF^KgIJUYG{84Z0x_XihQ&U&TPr3|frv8-XYEpSF=Xk;{G&8mo0;)Z`s2(qx2=G;3 zk&)?$Ov?3ClNl}aZ%&ZC7@MqeNb>2Yd&}fm_Vy=U*;L*_1jq)39n;qwo#sCdQZa}z zT%5KmR>VC7=Bx_eKfYnpDO*dOjs#pR?gv+4aRlGS>p-Nq z52C`1xWvcuKAag$GRV}~R-&lfS`}#WTHL|!9TXKgf9~RWQX0YMgN`#TRd@lZSA*li z`tL*DXuMvv8)iNK)S$3(oBV3;%U4~K7>8X{Zgjcmn6&^`&ew)-TrIt-Y|JwchYDc$ zaDzT>=7{EbpZgT}!{@x7N&8MLR#>hMi!3XnCfieL3tnBu8~jj`kJi+B_m>*xW5#6i zi3`mk(0LW&lwUnE06p9^IOQR>BS--N32xnqZU(HhSq0HLdzHcg2`5LGb2< z9b5d(_Av>jQ-V&rvx(+J#s&dtUx4m?_J>N3b*>`9NPTN%p4_{SdHZ6Lb&Wn0(N?yJ;bvQN{N zANf?uy6~L1_D?`Gun$098!FzDysPUAve0;H!*d_{_(Y-55E9R&t)MvhoDTqBgIjG})gH0Xdzc3` zEX6Jk7Rii^jP26sdXuHUp+^Xw_WP%-4lO1_)M%yGUsMm!wS){*d=F3P^|0O2e{%iC zM357(ox&sZq&wKKq@gF5OK;~JJ|dwP8;D6nz;HiKiFV{m*p2&~*XtQiH3F^$CJKwI z9ei~7ipVGbssn$Dl3`n;*zgSSrbOwxi1@{Rx&s1f|Hs16|NlVVK&!=0Yk`#`;yfRt zU2(rhr7{5kQ6lhWfWT8{l`QI_?BM9g^})l-t197==jSmZ&?ko_2qV9XAQFYBr9ti= z%As&Bi;$fD`P;W|+gdM>bX0k6pXK7}QYoeTGlN2^XMhVKY)yryCpl7eE8Thlc&OTP z^}&-OAPKDi z!0dQhBuE+ZR};uiOhw~XS67>%8^n*4jSnb~HjT#?qcH zlgxAmw#T!wZvTkrcsKUkI?3MdR9gjKbH4b!>p_f|6Q^`}*RV>O6%0 z>p*HYO3DSwQko53&M#gq_!q78g%nSIs?2RIEKh#+T@`a7 zSRHf6jVqU>?WA*9f`uw`7oVMLp{YD3K_ndY$rWGs)XhBeu;ZLsjE~x)+w5~~E-rfR zy?kexX1?nt^2M{C;!aVseAmIG;e4ql6$?sZn4)aZ+Z4?IRz9Zm_%Mv8vk49nvR{N* zaSRtVl7Ymgr_MkI^1i2C49aalYCYb~)arAhL{yG5V*^D6_qkLBS1(^JIvyN=hM4_hKd3j-XUTWiscN^xpy!&WG zR9GTgWChX2QjZg8+8OvaIWLTO5AHC$TrmEF_y;zkqROk3`seBfSvb|h)T@2%@5@$_b^?JyM&C+CFZQ}sR+6$@O4 zRa`DkG1aNp&+V=v4`+OFX#%d~px5by8=v8d;?^eydUXzVPA+5rcppU8e&hPN85WhW zQ(V>T6sQW-#=X^y!+~C78~$Q9U-&`F+_fdr`+?7RuwaVUt(}U`BNA{`_o_*0g<2{$ z(NmS{qY+1$u3qiE1NxedGab>VL|tO{LNUpCKj-Usl^Wbhke(e?>xO@ay>_c|jOOHZ z?6cCUw#Y&>?4^e}6i#8_lJ62H24$>^e_MM8jy>a-7k~IFz56v!1jm>B?FE4Vux2}bIjTd9gMU0z| zc(;+UNJs$3EX-!T6ipe|(o-Km9(|aCC5B%E_7B~G50{zz<&FpbH3KM3%ae2nzz&20 z7BZexAS^kfCNl52W15kHGKBfWl>Ch<^js$vU!Oj)m~&b|g*|dDgkH6x=`^aR)%~Ny zr-7ZsS#h%ap>0dwE1%AEwlwgWP5Nm>YcG8dBbJeIs7jISoSAAVO`sErPUJA-|>CD&@Fub6R~u-<^~0Gs?zdkCJq@mmCxyu8dVduNX3)|7{kQ*u6!7ehNf zpe&N_&OgD*Vo2b4KaaiY(1{P5Xl9mM^lKN+W@CtYLrkV)B3Wm5Yge?Re;yKdeXi3h zY5k=tdlMAd)xzMFHj!3axBI%~QAb0ylTx{z6 z*RVNp!TX^NqegYh76tF!=YP=ac6_OJyw6Q}j$876(xG3twY-RCEN*Rf0g+1swMZ$* zr_Ms#CrEpSP{F!~>EYZHM4|Jd^Sn99v8W-S)DFz!rEDTh*IO5I_M4%gfglhjDh3XOTF-UY1RN!Ap)#lga+$%gRRIhiBAZZ72AFpIIn{D@Y=}}=gRtxxN`i7bMPr|NvRyUa4Y)=G09CF{^aOn zTh1r5G*}e4yeZ!EXuZ<`s-c^An9sAbKSyX%1=J&KPaU3G9hrnJ$ zdS$EEY-E(B*}i%1t0*7X|N4_M8E;E#gnHX)*09aXw{35S;_h~I)C_=t;8+r^Tg9RoSdd6+?Q=s~v(YlFV#FF?ShkA<8r<+Ws zY=bmLf6xn`h1BOsDhEVhVE*syl^s-U2P~ZlfKzxqx13s9T9zH>wfSHGVe{vDE4BOn zcGB4GM6?;FcGI+g<>Uul6PHBjDZ9S>YW4AT*jt;C&tiH7#(^!gfFY{@30D?t0KN5Q zs4$?IRoVdIk_yFwX)>Q#r`$*xs<~jQt)s{iSrIT><&i|dXj7Jx&0#k)Z!)9lSShy2 z)2Iqp7Rym_ycYpq>ALW)ACW=9ct0^VHg2BFt>#4LH73sQA0LI}?F5oaRyR$J3*4ae zAR;(q+QL?y!^BqaK2o^YcKr8}Mzl)CFPF~Vy8p2Xc^!?!IY@6|;Z2)0;(noi%|bI_6@A{@cAW*^mv?cLG>V z&!6JfML!9duV8eX*=e{+ml82VBt%>Y|PtPPd`l{^H=P7 z5$A@)v%th9EPHt3W5uz5S1+l%C+@v!9>*hjpWd8)D&Kn#m-)xsaOqP;n7WGP0-pd@ zV4<{^jZKmmL?KYn-F1l{DbB_AvBx|L+jrXZjDJ+FmxWf3ux7&ev5d5GsHp2kl<>XT z-V1pYJYz$*p!GlSzbR&tWC3C(BQDkZJoK0$n+Vz5a^*hdM1x(r$IxbJfqaQ^XyLQV zv#?i9Q3I@xU5P2%EOT!~nkBz6#sIYiF+-y9^UREeMoCy)tR;5xVCmIAiic3vV`qG) zZxp$$e%CRq#^4dkhbC2?72PWlTl=V5FjMp5*ruE}h(2*CkQ?5N{?|#vQf)QdveY(m z%YgV{GxX{0w=GztKmhz!`+W+WWBA(cy`+Tt?H<#LEjA;!qg*2gpJXIE(>pSFY0`Kt zKV5kSSUSgVH}`_MSe*}%=*-2a>f=f2+g9%u#XbjB29v>eW)cFqk{v^}gG?8?rEdVx z%%CLh40kuQvJnL%*N_5A(XkmgKW^q5%J6A)HL|1D=-0mcHR@ps*gd}5QyvQI*NtYPFxA8@aQHzEE+e z;&Cy_r9K>kPc5RT3Zv=G_s7H!)37zt@LQdoIhJ>TWIsgrzr^(`_5D`cs@M&Pq1cO` zvV>l0LT~b8j?U0n8C}J6Xazz(YdNpVnZ>0ojgdy(o9SvwWd>OVyXoeme+Gb{tV*)$ z*|-_k-h^XbS0 zY~!%M@J0rb2^ByI87#Y(6-)WhnzX{S7X(juC0Sh-S5-%VO|l`l}oL=T*Wd{*q)vKJ-6H&i{LEF_5o7U_K=H zKl7xZF_Z!T^Hu=kr2*FuIT_?15Rf9RhsjU5dU^;4$`#!qlm;QGfa9;;LC}lm2MIh~ zK<8xY?vf(B)Z__7%pllR1P-xmE2(8fJ()67(07_>8x_Sg?9C&4X z%F>AE-Ck+qQh-^5@o0r>x}xRZ;&8Nh#4}xOkHp=Xy+5djRk|lOg=DHu5duzJs{v;L zdhapG?d^)FR|(NT^pL%9UF|Q>x{s`@yMjyw)ZW~_g>6hgBEgCT-t9#RCu!aLU-0Es zRD@hPA!UVpF1!Utz-r(+i*mv_7->X;DvbPED_w|q`!x_lo`fSM9AwR41`-L2T()(_ zpOm)!@S)Zvd{iNjIi(S41)~K1rMTqv?|yeGg!4aTIaWB#Uvv<1%XJ^e!K$WURjNUv z8mJ+1wLZ#VRD;N-+z!--`zm#B|VFe~ln zkIr;Gm4!<*%8z*OrQdu4EanaS5MYDBwBf4qi%nkuY9szdFhpOP0DCjd-);8xun5)+ zR>kmlp%R>6TEt|)Gw(hYAzN|Hwyk|I+S5N_*u!;qtv^izb;)6)x#yTGSLLchApiUw zIJwc3LxkiEVKNF)*UHHfQ&p-lH=S0TjP*2r6|;?e0(||@bJrKcjvNY-7zEscK4#xi zh|1M8X}zAZnLiqMg8_Ax#ceF9(yfgUl7X3X$>q){jV#XAo7JZ9Lb zK``QLDwj;Y7g3DPq`Nc(Dx9V9qsR(};NC=jWVbl4C#57It}St9LzB1@mI&X{Ak z!BPC0;E+8#lL83#o&}C=_$Dn?tR|kCrqfLX6)LZ!AT&fPXcJYnyCq*ZjY9Nc(#AIe zo2zmVQ$hl{P-9-nbGA>TIU@i&-x?1cX3_O|zNS%A9|AHOt5!P9OlC$>xYYSf=9MrE zAU>~MyL&ru_4ENe{_g=2ZxK}Yno(#EPQ!Zv3#^h#kqx7Uov#!$YLehm5QJSdb2wj7hv=eYg$ z8E`hX&m(kb;e+WEYTbd1N7uw!Kbhxp);!}6@ti+GW&R;OxP6t9PG9(a7aPZ+9MNYD!=Jd;HcrW@ZGNE_;Zn99r#P4Y zowx>v6k6_TbwH94tQ5OEnrBeozT&#NwL1T)Nzsois;e9(8*r+GHvoN#lB5l~{>JSw zJn0Ev!jB36;ASg1-q0Mzlm(0& z+}O8m;}DO4GV?PYC9o zE}U*%e)-dfy=(M zH(UB{fr9kU81vs%T>d?Lwzor^X*>Bp6KDP`NdH?z{;#nvd=MwGt+mY9XXoj=vfnxx zNa+r>DH1jRQ1QeD<^RO^{@0?o)Rg3zMlLg`-0cVRf2JXWga0)R!NN}wX*vLPK&bv2 zVcuajeKKzQO(JynRqSO`T!;70Z(Xv^sj=_sY!MLp>_m!)%VXVRko*ga%D&1-+6{v7 zCNTdga9F!2FY3&GCfKgP!mIA#;c0?M98$#*mr(>>e>KT#%YyonM~OUnJ*bIHo&bFIKv6p{4xc{- zb}2|VX4C^bO=wYL)ni@v>fagqJQ1`(a>IQ|aVXCLrlRk3Jxu*CHF>O_9f*yIZO<1X z#v~$ctFNo0X~UJ!+{Cq~N@3p4b_!H6U|JSmJ}(63K+gl^fZdo|-IKM`!}j5&jKa5% zr5kI2xMK!g$*bomKIiI5WNMcdtb%tH2s|u${^$hlY+7tN3S3EE0O@#lfhd2ZF-*T2 z-vep;M&FyU({1Y6f?KuuV|fM-sx|J1f5w@bgnR)M#SzaNfG$g#1Q&jM{``0$X-D!! z&(CTD$RL2UfDP6S{Fl7?o63*(Yb762D+4+pl7yP~{n#;!?z|V-Lv|MDQowxR=Jo62 z;HHGXz;UkGRKJ5geMn2bcwf&x)kV zz$xK{;RN3CWaBq8VD*5A&;onpY#q)!iw{?eY(~D@>hy}y@T?wKfCgCCVHIAQ(O#MY^8$wr|79r2nIQ`syC4c=Zm$wdK(0T zY$HEneJ(&*WEsQ9%iE4FcorLLp-guGM2W>#pO=n9Jx&&Y{|cLW6CEafhN#-j z=I9lg2D@4xoMy9H>TF)3bCj9xOq@ee)Az?Av(NTq35jETrkznDjx({kw2JDDGcJLa z9kR3u<{io6?nMvJ-@^^r&KYqxZV6}hx;6-)a9b5_lDD&efS#(v)P9w3RJGAA5$>-722nPSj`p~Eulz<`V`9GRL_ z zQ!v_#Eu4{8_6oaKofVutd?l90GDk7#qE3j3+le22Bc?AMhMZ3sca#P9?VVSe7q`=F zREjZHG`nqD)t*$XdAsN?N9Fcv#Pe6w$j|j<#s-!tI=1H}!q4Clkn!Y7+}mpJZc4YG zU;%HWli>X^dh4$W7KTNU<1KUD`4NDNefb_^@D;d(hwtcW9ca>@oXszAT=zl`RjIGt z)A}CCF{u>8FGE01?L4@rZEZY#ygABRlqnyS>9tC290T75SqZJJ5Hekr{r-T-;>()h zQ|V7=t$>?M1hX+xZ=_{G-TY90TqtRLaVC!J@lyj|+Dje5aTDLoZ2UB+-5yvhp8aS6 z_)WQWqB4#VnVQY%Q(Vrk1Ajf*8fsZn-521-rjIXw{fqTrK|`6!fZWxN9?~80t}}Tzeehz=fwms& zQ7Qd=AVBf-A4K2gmUrz>@>CWe$2+P`c&PsF?B?N`3nJQ08QC@MhPp><=uYw1JdI-K z8=|7Ax07EQHNm9_1KW|3^doK!%88s_l?y|~v=dWLjA_Yg)?B&gX>mGcCZ}mm8H6au zoZ%*sfxJlg7@oM)XolY2F$)9rbs5JF65ls^DCn?A>0bm{ebA;y+RuV`YUR$>d|YQP z+MPPxzFgzj{^mTJc{4~DnbO=plC=rI=zzG{a=R~A9KT4i-)J&k0O|b}#9IfNtSolH ziQFjxEE^u0MWf}m-e|g-o$-rYCJ4VOJ%cqxDZXgs%a24%RLmTSiK|(QNg9IGcDsVZ z7ID|L+A+md`@tc99+FwDIyyG#{+yBaJdM0yVW-(=)n?yn#o}fU&-VmhhRHnWOV){T zv6W2Xy`?;s}s8;^}=rsHQc#tk{lxiot$dQFUk&yAVqC|%|K z#B!&wG&Xe^mZ0*7`z{<T*6~Q^0)#FGi+irUqbuDI;+UJfXzwPMoSeZ6DMuTGY z=5u`Z@(>$M5L9n_@hX4~ECQ19^Mc7ZqbTVn%sXC1s7{&Uf=3a-^A-m# zh|iVk4R73VP61=t}3Vo#_f#wt~AIpRbFv z3NF#s7;|dAl`gU#JNnJu0Tk^lR%#!IrOO^lKPhfxU_>!TZS(8OjRE7<=?35ESOsctIwmPwzM6Tti}Th{Uwv zk^E&YB9;pimn>{6*P<#^bCaN(=ilD2++;8L3M>;3m%yJFiLDPinqrjgh(oyb&50aK z^N6y&b_qsjaxB1FY2ujqCBEVs#iGVxN|MkorYR=)O{atc8|cn!7iP>(TC}hU+vqZk zG0)F12-#={4m|_B^tNHMGf*?Ulm~qUd+LSEyY(|yK3k@H7>tHx>GD2nCz$onrQe?; zVbevY!|ke)uMSE11?D5*bnvNTLv8tPG8#y};&od!|Bz=?$J^;+FLbYc*{-Kq)^S|_ ziQ*~^z6W{27g_RG&+!$<8^uz_Uq0*>qY-Qu_d8m5>j79O)Y#4Qe0=mGToMf&WZv|n zStWJA;K^nup0>k}146vOw+%9b+WeYl%6k~R$w+Y3YryY`nElEaB3uM}U%<`xMcsSD zRhF+mm@XsUP_;pV<~?gWK(M=}_>7ZdcC)VH+)fdzHF#TYxba{A|4^$!?ZaANv4w{F<^NTpXmV$Tooj4j#2M1_7AaKy=aOu?gWR)>i1I3C5Z%42NmXsYjrKSUg$cubo^ zAZVj<>z4PF$s$TeV61@Toy6PJb{i$&q$i5>Cb~^%GPfBt||dq9nBh zzVh8!ec5bbCr!Q7`1^x!S6Z(y0%#w>r2kCT8fqNYe9G>P@o1cFOrVq=m@7?r5k%Ua zS$mCMjNDQ0EY*<;GXHD(=Nwju67SNd6A4L^9^z+czY!=%m4@F$AatBmQTgpOkIO+9 zQgXcMQhkT}@GJ?*=*<1y>U>(7FVBy8OA6W!-C*QsH?~xK8S;t5zjo=4lxT^EBvnf3 zY}b<^)$v~PTIT`!xY;=&=7JJIiO*#iK#gXp197NswJ36?zpAS?vi?uEZx}hs5+FPxjV#R@cd34qSTDh!c@w z@VQ%x4|FXgnn+*IsJLEyZ)ZC&cKqr=@qo__687^t?O_Mxt$iwtlzt`bFpO15xg40Y zYWXpnx<`zrle-=_e_q0_Td|<=FFva5yzwVBJrs$dbV@>`BqRq()MK8q6O7-M)Z{tH zbR$>S(~i#4yMQUu=mA&<)vm!K zErTBUj9}d9BK*n+B}WWLT)xMa58#n>eSOklf*;p(@W;5|d2ocY@u3yXzeIveMgH7+ zF;Wj&s?6VBUnZv!`$&S;FR&PJx4>8Mv;QK=KdZ?*l5$y!ul!@Ioe{yJeF30|jwMI3?^z~0Y=!=2!LWLH}Uiq!E> zi(h1%W;d^S6(HlhFv=8PY{M0HGChp2q>ofx)S$rGcfFh-t||Wt9;N^6Ew$5N{)g#d zdw-3%N|Xk1WHL%qk1={+d0K{C3Op&r*{Q-wq!>w$_|Lo)D6no)rl@s}(Utd^AhNDm zG9puV5+_&nP5W+8yL66ZWxQ4jX+%#GYZE2?x~E2o!tCLI2nJ!pDwJ8-ckU%snN$eW_pBDa7|EGecd zq`!Tg-&OGPS}lGq`xn8pfm`#eFrEy1-QTtLy#Rg2MvRu6&XI0g+QS*<_K?45k{*HQ z0*(H$J6H+zw#(W8C-dbxM1hEvSZHkm<_{N5U_bb4Y|mv?Q;{|UbAh_XvNc-96ZBuR z^mhL$EZHmQU<3ZKbJ3Unn!H-jJaP^#m91M1YSiYD8_M zQthXbXmqbv@mu!~?88EFH?d$mq zccN1K`d}@+jE=VnHv{=v^k)?}6dEs}dal3XevK%7tyz;}>fjF!h4I3# zeDVG3Rd`iTt*Z+NLhdaM>R8er)u9}xxl?qJ$?EK1NfMFn-*oj|+0<`?#~M)one;~w z;6*=1tio&K3G6}Zmj68>tRtcP+r^Gg;u(-K=2bch(#sF+2>*eH%)^~<<>nZQqZEWA zR_A4}BaKAP>Ez9Rv&n!puXB1HKb~TJ)FTid^{V-X>I(Bkfde|#YaCc*Aq}uSpUBV^ za~!$XI~70nTu}GXR$lS$yVpD2_8Rz`I}^!5P1BB{p2`Izh-C+2j=(M79@*8_6B zb&`Hm5mtp-j};sY;R|tLMGaKU3B2DpRdpQP&@U}CNL0RF-|SK_L%Db^s3wQIzg($r z%8=M=;MV<<@!i~ax!qdV(as(J?Wk2w}xp#Fy zPE9;{&U3T|Cynd2;Rvf0NI8TGPg7$x?g#e$#2-c?RVpFnx+TVom;&b4&NS}`K86Qu zi?LUK|G~DY&{f?``(W(Z$%Wwxm1q_v<6&a1<`PlVb5PnlV?=*n(!UA=`9;C|lsEyC zc`5SPe!=Bo$6jDR%vG5jrh7{0CaxdSfhW%JjOKx*<~p0gYPOzU7_e#Dx;ji5= zJIfP*YxDSY0ea)Ga2_~NA3$R{jxbgRNR(05`hCaI`1CMu(H*BUgA5IXAv=P+sEE}J&9UK`F-spIy^ge-IxtO5kW2`H+jMP<>}~L$Hs@`_PN}Zpp1tl7k`+{ zk8;F{yGe(TCNXtTkD4`@HCn1b%S%L%Y|+51tFuk2JnvxcE~sw6gVi3Zr@&(1kyUc`-4*h1($_TLR@4{@*6*yCo;ApS~l zkix|L0Jb$wM%sM>0)T-ymXC$Sh@He9w*bA%IRt;@mofneKY;xy@^z4D$b|RdxHgG! zOFm*#-^jH2U;Snp`6nROBIl0Rng^@ zY>VFhf9v}|#Sp$PYV-f8?}JXY)e3zsr{}FjI_HdGp>JMVi{j=!Pqe1Q>66z=hBs>y zSKiOFwioeXUr3HzFHOpa3@+*vP>WM5{%xL{Xv|J!R@rnAwLJNmb*f0dxP@UFbHm>~ zlmzwV;aNdR*2*S1)bcvBHyl$83K)Z;rxgJd`=9tP?^nLjFMrIPWSQQ3Zwciv_wdft zqKS)w5GudVU3-cCg}fo&+j{p^`0?#+!p)9mRC97a=7yVlsP$<5VQe!-1Li9MYZW1% za4swMKqt1J?)?C$dy(7m^L4=#xwnug=st82zh1u`0(>z<9(-S@yvDw1xp?YPFc9g^ zHhc(%LmVPsT;5cqKvd#w=7mnDaCepwCKrv;JBxSSqxr>$6i+7Iz96e&s=1fJf5Yjl z5-6roS&#^RdVQXilH=Xtohae_yTS8!htKw2KS{=LUdEkVbDcwJGD(FR#NH4c;E<{s-JDY&*^hEx5>dW<_Pd(Gpr&E@wvChd3h;kbr3SwC%k0j$XgX}-KvZ6F)EmlD8)sFu$# zUO%@Il8gZTKa%9G6s86QkGQY5gnBuZ53f96X#SB{p1u8$^k-+=$5N%OwGg!A67HRb z=4P;fS-*S+T4%B|KIZSvwrUU)w+53m#Bn-NV<841q0*B(Hy z1#1=bg0}V0tbO)6rw8CE3Xh0RD89Y>sTI<@@~aBrU&YhiLNlVpIk05NAyhGMySu*0 zfDGzroa2mi6lbI(&?CO&ePN7X6-`WK*V1BC&A8S>$TZnL%fqPq=t9OY$&1A`dZS9& zr)R1or*rotT&ufuOx=su?GE^BPj^Q0G0Dn*dG>-)HT|)%aGX0n8G~SxsQ1zlkpQDY z<3No~;lC)a3+3EqrWD7E=CjLt+&|uyqvMs1w}O^QSBa)zAQAGNVd*%rkv(eGxprwA zpCsN;v+tH?sL}i>OGq5uU%Bp_41uVD`5Mc26i@At5(^F}xAdLi-m^W3kj_p70hLFV zqV3$FgIkxb*)*ZP--a`ar>Oo_@7@cp)|mChX!6Q{!V?ZPe3#9f7o%R17njE;Qz*HW z9k&@_nc?hU+;iFkRhT?h+~q-;aleTM z`}!^;U5&qE3Bba-Kk(gO(cfI4sE%yPlHi=F2(S#lh30-tJEsvvEwTM1`wiydpff2e zX70HxDAx3$onMk(ul?Ha8y1dq!KeLr4C)S+0@rHtTSoE^)#32=I0?1F^vvvagEZ9x z@SW#F8*5I0lP|>9pU!JY*TS(jM$EN`C;I6x6o zQ2aT>gl73C!4jF_YI6ha+mxi+C{%npql7tD!27kcwW<~h$kGxG9B@!h^-z`gts&&^ z4G`+L@VRzbi>MJ$fT>`u!?HAtAjFSpahrA~SdetT3cd7w|3hXwgr1Ui^L1^Uw}Eht zz2H5{xy`H>4{^cj=2x8(UT{1Jw4bHhl>jQd^D!K52;db)oe3h1IO3p61s5K!g$@{X z%NoF8HupLX2su}Nf}D+N%`QDs%C4j|+$U1|xRK{+`r5&zfEQ2!qnw>hbTIDLjKtoz z{fpUHD29tsRoVQ<#UM`8;IIcU9uVzfjU{em%w_b$bz7<>+&x}=ws4xfKNEzll014< z?1}&7tN-~35u!Xvx99rE%>Eaa*yz9`lA_v+0)CNSzUQ{H7K})%?}_kBevvn>)dXGa_c%IoK8q1r z=<{oHl(Z>L4C|`HkkhEr2m7++G`Ni-ZN&Yx;y2)z4_=j}V8|Y9kAMu4Zy%#cSvp&w z59+NvhCZE1_i_`B`#j#`FwZiE{aiL*Z)eTufYD0wzQfdG4k`fRg;GT$)cZ_k=m#P% z3&g<~3FOMJn^3{41`>2ktJ}IaW+J2H5rJHW7!96=w&hv{8)uRP)Xf3=AC$8HWhju!DSp1mGR=aor}5{{6!I z2%41-7UNv}t+G!UG-ye|Lh=LX`JBpO{#hyTISqy40*Q_>UTuRSWS@C3$E zF8O_(K2bOJHTwEWS#ng*ydtrM`-{Y&Rr>OZ0EJ86{e0)vPUl(e0>j&Mt?%21QCsit z&l5n{7oZbonnD?(0T&7WaYEvvrKP3eiHRIqT3X>@VFq8Jv$s-%M=nj$J+_B&nW1zu!_f4y5i4Y5PWy#|(VBX;qEvNHdqnZIit(6b?uYJR9dudRJ5Mg;P!TS z#ZnzHOA<)@W`aPH7`mNrW%Ywfo2rU9Gjdj{fk_&@`#cYbg<+WXFzeM*J+ce}E=JzX zmge@Vz1}+$42?lG3B%rdyDOk{*>NzZZzsBXhuKi`*iisKuX$4ooqdPvwmxErx0>s{ zf5u>YR0y^RD3KD*PV-7F&sP)tj#=a|ajd{XJ=8qV4_yuoqQBtrz|Ok{vs)VgRj7mS0WK{_0f|44(~25Qli-}>If zJE2B(U@mz^-2KOMa1&Ah1Z^s!&Ad%U#Vtty>{dXm3RW^d27bN#p6>%=;KA?$QixjI z4Lt2d)d6A4O2MM|3`7Z9fe>p9I-$JxzN^IwTE_v5P6`6h`9jl?H5)FN1#Nw&|vRILgk1}3208^L(%**(7q&jHW-jqqlUBzKjOH$$(U3|Km#4i7kXrl$8VdIJp1>38d1Q33SC|u@1Q0(D3gmG#apex+s_O@S_$e^ z#2fo}biq0TYEzH`_v)MQZEQz-^wz?a~kvf_2oYaZ*WXPm;yOoEimZX z4^s#&>^k1$w_`xYgq?>C!&n#}f^LVYX(~HNf1VQqg#EF*KA+#77nEEdox`3<05add zrl0)-#6S{mXQ-%wU=|x{k+~#LBq#{XJL%x$-lwkpzOM{0QnK>FtWfSB>c~&AG&yLk zLed1^2eHaxVF+TT`{MGihYY7c**6xN(~o{fDdc`*1(N$-Jjl#01|ivhzhjg0R3#B^ z`Zgf^GL;d94Bhu{=Atu1Gw-bkY%~KKo0&Ln$wD*aNn*`wA8 zfB)jW3-F6OuYUdFPxBgFBKem@NUnygrJLxci!SN7Hv!8FeU0quP1AqBXa*ih?~Mol z_9-yI!zxr|ZyM>8L8u-=d82+k@Y0hc*GIFp;3oGuju3E>7YHG;@112n?$!uu=QuL% z#;!yJkcmaW{ai^5ptC-Jv-_j?-=9(71u8gFLa9;!ua9&$YP-S3IMoHEYvtpGrv3GX zlzC}WNcDE_=HteRe1MfAkZU9$`TF6@2~Bn1%_B(jwXOpwWN_R9#f%9D7-hn7VP;C? zwC+C^BN%pPoi}UW6t{*XmcRMCL=|@Zo{hMi0Dk@9(>wrW-5sUR)OqUeb7|*OZuX`D zqEG%|068kb@)E20Mp&_-VFr684L5o-(600^#^Su*kEIQ4cYh zQ9!^)Aj_lJRs8zsLXm^&M8^gNh=iUnoV_t#QfT=RY!+E7HFCewpYFoJ!dQWSG3^G% z9{=3!mnvOQ5X0K^wy?_#ITawILQ6Cdm^YIA`yMe{{RY|bV$~4#VgGr^KxX@M<@XfZ z=Hr&laqj;%0>J&bbI)p#bOU&LYGXM(ITBwgAg(x~CpQ^(3Fa&1KUcu(<5pu;I-py^ z1k#;jQxM3*07C(e1b3N0S$cWh?rB?N3Tz+;F!3qmNoTk9Fv6%IS~^b3Mn zOFcG0$qyO7c2JHV@u-U}I!(;Asn&Iof!$Pem(EX)U>os_i}*c z!*SRtYv|*Jas0L1ejNx-Csx0IYSRuYbwy7%lE^roW>y0CLGMolAY$bh9to^5F5OjC zr<{*e`kHLuCO=k|fcO=;xs@eIcmwe1IZ%KiMD%e;pMfkR>=nX&mhFl?7Zo`5u9S9p zDr4Ys?kFi()t0RF-ARMoh!^U^%@4^KWB<0A7g7O}dV5>Z9^X}yc}>4pi`Pzg>2XWz z1Dj^$Q?A!7uv|AT!lijoJ-RJ~h-r5?!)$MGX2fMIQkKC}?b2q8N?iXIlph?KnVF^x zK3zM&-fT0wgr<%lpKxsBfDMZ%@i;RQ%$iA{e;swdcCm(j>+|!C(tuMNQ#mhhRlD(( zvih9fMjhp35zr6;VH!933gp6C*Q?gWOTq8n1OTLvRi1%RAEn~G^`ZBzg(}(`*kOFA zLYt9`g{0k7J3Exs&kzk?Q(AG^ftkYO-R#Zk6Kv|uvpuhZFK0mpH%Q2`fZ0Yx$J><2 zHwIBNAzEMHVonnwjpww6eVlgsu;L7!`gh*>G$q0uYw2CFUcix%zFiJFR z-%w6?2JbGGO2Xo=%{yvp><6e)Wa(YslStqL`1nIbBe*Z^JU4{Y(!}lIl3N;&BoGS( zhj8II_nrhv-zwLTu0T%^`Zg)T6F#VZ5n$ylbo|X~eRsUJRDWH;@701&;CkW`3BLc^ z17MlOmb~$vRZ$z%x>>vtw+(Xeo?(gW*L7E_0{!#NJJFITQj81aCTYA_E#e_u5N-*M zi3fv=SkF9LetyZc2VhFqUi$#bTNSUbzYgMGkuRqP-ayccn!mVc7ew#zY%Y@=nV!&> zyATCARNS5pNVL4id81R_*H!6;cih^Bo8RBFJL@=r{J-e~&%v?z8Jq?a48OxW^boKI zJDbl5HO(zDqX(s;`P#v_cfpxya0cFv*EJr^TmDYjlW`yk?Mg5HQ61<7r7{DE!>FmK z55a3E3KA~{c&q67}~%28u$)^ zbQSAaR@SC@Gd@Wiaj~(DMxpjzr$kbIN(j`L*lxk z2U720{acGJ)oj(!R5bUwh6~5=+yin&lgdq5iIsnDq_l(n_>RtbV)W>Hy}Sy#kB^88 zm5_y!uFmUI@2r0NU;=q)-yG2k*28FDGCE;>ng^bQz(AFvB(dSUpDONO z)${Yeuj+T=&_yp^Hh?qY@buST1cidt4%=Rw^N=C4jr+(nK@o*!M(E-XBY&H6o!#$6 zBV`tNmH;6w)s$s%V7z2vV`!z0;Nd-FwhGRpCR6c0L;88Cr_r^xJEQ9m(t+6p|1ibQ zZIX_i1CpsxqZYcJKp;0j34LPxw)-=)Mjn@c4T}nN27#wL@UIE^Ob~?4Ej9U`BPa9Fc96An{6A?+@%UVycMWhVXF-_QPj{;|UOaB4<6Bc~>|>_8k9 zv1|%wB#|tfc4Sq8d~<9k{nyvQ0(+)W^h+@Dcd{(3AV4rnJGO+qW>{VTV5-`+{`aL7 z`t;q4g2q$X6j)zJ$nk&HEx%KW(CrgFpz4mzT#=#)+mihIriLv(z)kg6AqyURQ#tr} z5Ag{Ce`=!szWIt8oZYTHq&taqOM*maY!eRYEBoEVzaK#Sr9=WbaNokVd7&|e?WRG9 z-oJYQqG#zr-wxixe@tp;I${hi%&iHH&Hug}D3J{O_8-=t&k=3f2?@W@2+~p6r-_v& zH3dJi@R|ST9ad-+J|TTZiM=B%NA(}cc2}}^{I5%<;E|kY`jzrv=cw2^=#qD5f&#(p zR~ZS!LCm`!5ax0d6?nSXp-v(LdYR$1Nn@1xLc#^=03-Nny>u**D73Le7#)Z zzJNV5e#N;z$|Rr->hVg>bZ%xM+(AexE$eWCX`xYy+$R_2Rk zgG8IG{~MwZTviF9F12bj2nL0S9D$&Kvct+NlI~5_#ZZt6Kr(gg*|z7YnpDsf^6ppe z%vRGQ(*z#CE$wXVh&Hq%(d53rUTW%$+`a+NynIU@s8GDk_7Iu(^n$$n=QNP?_>+It zPk_yys=Uti@G+5S+wOA4^cr{!$xXX?y&A)Fx<6oN=`{dl*-WjAAuwvo-7;_ws3MdG=}W2wtwt;N_Jh37Mw(fTqW3^$YVvS z@jolyD!Z0&c;l(tD3B}L-+nfw5D{Mdqn@Ya)Nz9k68@4=_UBumN%{Ls;L2CPR{ z@LAaZ(%%L0-=*z@WgoOVONX_8?T?7|wVmlZwrRLG!6pikru==bHTCCI8v8gn?5Me5 zH$bO=fzQ=oB)tE68l6pV^b;=Ly9V6NNpMm1YMr8!@X{_QC}>BUcL1sH4$#{;T{T1| zAM|6T03~cCir{o+q&a=!m!rYoh1$$3cB%}Obgxp_mk;o&OQ7Ks+2Vj%|SE>-AL zg zN^y>&scCv@dU_Q1{LfhtylUm&2kGWHqDDm06q42zNaowALO??pbT04jEH_QD>1{q? zzx`G&gkJmt!j8LY))6Pb!a&-t;gH!HnjUfU`_I-a&=7j>S023%l1Lo1}6 z+ZMp!H9K>%@cy#lCS+)qE4kGeH*7)WMu&$`88;h+s=n9`f(%d?SnNpIfRazMyC-6k z1XH=rm@|l6PtJ^x>pcJs!(a?H$G5ZYwLZ9Rd%NptK$1((2Mb4NfAR(2h9Sl7ai02{ zLL*KQY9M%J#ids@yn_N`x(>mbL80E;?`*7#tO?9Pp$s84wlp_Co?M^psqd;hIEtdI z-gxxQ6>N-3PP4-65ObvDk;K@&P`GrrV`3dfW@XY!{up z$G*4Vxfg-y&^J)Nw3eyT0_9YU)s|hkYw(@Q!mZ@JotZHD1bB$6?6s0#?tvF9o2bj; za{$`500miTE4pfxd9cz&0&+ZflLh_Y6m$m0j-~2E1k(#C%TENLkX8};CM+5{{UU%q zTy(LdUJSI3UkwtPo{zg@v*|lX4u)4C2;V$&XA#s}BBg64=KN=4t!(3huf&vvw8 z&9yfNb4=0#GVlLXH+DPJ=c*FCTro z)!yFz#N2DR#NIR+k%zDbu(msx4c`G3Ys(dbKD=P*2cz%1ynYnIrU->h5#Eh9+nNW= zYO(Nh5Hz6y5or0)Q#tp)JffUz0}&WkU0`n(s${!vj_K{*_S&r8vrGbeP>5V@-n;$Y zh_VZXNO?7|xRl648y_B~Xx~$b7wp)-0hi=H7x^=6lQ%9KDd+oK?THXD9Uw5B?CLmvltdEAA@EG3R->uxg2w9gr{hW=^XzT8g z@A{uk`We29$(hm-I zv~S$&ADD8bvINWqJuUbQ0vIM`SGEHE-2zfnB?1)}qxYty#koTU6ZpvJBu-K?;XlQ3 zg7#BQi!4qj6429%?j}qtraQ3od98!RX0Y_Q?O6h&A2%V)?3NLYJA7abQRDYLIB!u+ zLrlG*;)CuV-ZM~NLud~ZZqo|c)SD5yjMEJJy&WZlKpZvXyuf$zHst(0UdFv&$Vv8N zB0@LZWuN|zCdrH1;{;QSqt#3#a7rqg^;$I`Ze&?m)zm>!$fu4jyhAFhUr#DVqCP#x zb$SY!M@QNm%Aq#M8*@rVvjo%<+;;tub@Q`PEtddI&cMU_-Jfo$V32>t7}KO!#Y0#n zUG9n{=+(TuCe3J8Y;m6ie*t=GCHUPdpid(A>8?D9?5|p5ve3o|g z-0AWrs!KX0SXhUe=g|FdPTtpadDpz(7dWj=sy_y;~6A%cShL&sf$04=~#3Y^` zb3RlCF`$W%|16UQHVG2G%jmO7@L{SJh|9eIUFx35E}iGrB9;hL=8U56&Lg_~{>Ypj zi^ycu#3aQZd+Y;s{D{2gl{Zm_tVtLx6$46By#g>!j6jN^9zq~MLNl>4SU@%5_rGH@ z*2In`Oy>;=uY8n*4>8sHQ)rdg&daYTzLME{ugFdRVWn2VFmH0AZND8iG{!;qyKYda z>wb=9@j*v#84!Wg?l(Xf##(?$#o5wrAKDHq*IaEQY(YRb_kL zS#)376q61aocb*IwUg>RG!@G^zMDZS;_?-foVxz(qsg3q=fWbiv$NEbMO*@dwaUJoj;X;mb+G@3E%$>@8G1#oa+1 zwC?5`eqo4)>(Pu&9$AxyjZ;FcJaz)?4<>+A6DEM60H-hCF@x(<=deRHW#HsO5of>m zZ66Mi!zxxAgO$9ZZjcGg&2*Zr(9LFh?zX1tj!_{rSHE|A+v0o2 z9pj+46s4%$TPkkX=?M+LTSRipM>#5NqHw+}+y5v>+v!B{S?K-X(xkPOMfdwYuiOnKBDZFn7_-DkX~SCR*Xls4=>u+J~2*JKhVbvo(v*4bq7iS)08) z)0>y#$k8O}>$-nG0HfugxaIlkg{2HRAR&P3e`tWVDUOZQTwegyfa5tDD}@10CA^V< z;4p=W?4e7P;N`}I2-|6^5Y%4)hZlQ@$NUQ!#9;2BAf07qjp?EVfbRgolr%7G|H`}l zXpVvCt0@Uj8>3;x@>KPe1wPnPfq(eo;!tel0-nuceB2BvIE$Fr90T(FBdk&>wFiHf zkQC!P|B4F^V7wrVW@a(q(f;19{75kg=zB1nUMfys9_lqtU*)i(Eg^P{*7B6`WMT)A z`vK>s-}6E5xHS)yQV}nJ!U{_V_))&S#aBd(PjN#gR1<((U|JAIyW0r=6;=PG(IJBn z^Eoxt(Y#I^WcUI)OC68`{}$@&<-CN^)cqdF@6SPDj9*cz2$L8vkdTO&E_`rHh%HHI ze#QQO#FmbiT!B8)DFrKifG7a`@VF9i{lDWMmr(qK4fRiK3(uGfa?gXw0#uoG{bVI%;6(^8RIKsl|2Q2!5B}o?vF^lTB6pcj{gRZ zU=Y;NMnfif$$d{^j?Y|2m@GH)Lw54)k?UUltYntEEJ=Ev)Dw2#~?b5IuB zC`LAbJmHb}gq2JJ8Vcfm^P}QCeVQyZF!&7@#nHFWlLD2-g*jiVJPE%W7K1MY_h*sU z_lPc5fL0xp=?+rj)Y$%OQ0e5FRn{qD)mkG2#<6aH(i@M%dJCEaG(`(2GRZ}bq-aM? zJ$pJ7hxO80K%GCwW{HZiZ@ehBbi}wV@l~9a;L}M((51wI#`F8a6TUby62-+*V-izi z-Z-FCP{_E5j*gB`<#?^YWq!h$se^i7eFd_>_cKFf|236txp*{&cA%(O#Md_i=YI+{ zGm=Q~h`MqXPzDg1K~`hssr8!u&jY%zE(4K?T=utcsg5+FNirsGYj6Lq8@aQq;aJWD z4+NPt)IuGKmfz}w~8BlNoh z4a}m7?1?PLP}f!5F?T3EWz;ri32jy>MIY=#k>1U;3oNqwABbIOGE~n$A)=9^55>76Uj#=Q(DmI#vV< zkll1AeuG@|(*)91G|QQA2x(a@ zt>;0?pukm0;+6n~VpgJSW|OKor!a9q)KEFN*HOFX<-AWPJ@u4U>EHjnmuAR4rSr@! z{!x};E-&QNABA4Vo2w>(7ltt7J(s;@wzF<8=P0CEUZTC)SZXTvc+JK5AQVxUUwZ%i zv|r}4Ywoi%%vm^Exn{+JVn(5#!<>wt@tCjTK6NPPg|5UABWql7cq9$-0)vU9?jG^> zM;1=z=&}1x>bY@7n8ABX)+?vJ&!`}jeUrn1X(T8G^?B#e$Nd_LiMNJKI!|-8o{Y`R zdcs>(zt?*$=ZxFMCrS6-+@~b3V<@-O;*f8l{LEa%S#`yAbJTm%eSA%hpj>tY(<^Yh z--#)wkn8iIpAgYTjpGi|erk;FGN7O}6wMhKKsa6`ofdU&e39dtb=|RVDWdX9dF>%20-RC&O{Eid76 zS2y*m=^GiBqv=Gu90FU165=+DY)fKnid*EF90GUuE6Z09IqzkL9WqTNl{NOWXR}Rv z!``1AX~RFumb!X2{<;qlF56jLpVc9JWr?8V`Zni<3*^BMrf^Q&ym`&(;=2oryC~se z>*8WT-Ph4kPe>{%9f!OJcqFa9IZrw-2&(wike>Cg8P;Ud-Hw&grj~5?d-U$(;;^W} zc&@N5$El1}Mq65k@huS%nH6Yp?3=)=9oiVqKI?G3IH2wP`INNxfJz8UA5CMyAG#;7 zs~2!tK9aMl_1a9n%f^>C4=#Cg2eu5YdS1?{-TD$YcUvFbs#&W|B|XklnN(0~)u@^I zqW^0cTY<2NMvYv|Oi#)E#g!kZZU3b*X&PjlE@36To6gSGPc4J_IC-%oSEH&D{D4Y1 zo1Z;gYsE8C##ioJ9-ar?Sb~=k2!c(VHuAQ zc_PkxA|3{RIV=%X+xEzSVmZy+Oavp`E=ZNlN@t zo`Kz1(S3h&fmguc@&!uY24LM2>5{lAjmfBa>sw#r$B`t{BrOe>Yq*Mu;6JjOZo6Q) zRZXiyL_%Vl|Kn1o$a~8cO@r9|;H~-j99JEDwZLP*;37 z%^v+jTAI)%sRb#Xa_k)C3!C{&DeGp+s>_?Z8!S_U_S37v3oU82{?iZ3oya%4?%I4| z{(RA-D{;Itz4fC^*i=#FzD{X~U1!zBxJ3Ohwsy;eCo7z8_viX-+BS7ksCnbt<)hoC zC%?0#s<9D0?hkKv**p_7*AskA4E_9?__klb`o5|ta`dPP6|(d-q_qict}V{Cl@C2j zAMKD@HLEi;)~yvo9X{!d>B`<6&=#4JhiQ-1`rs#-7v{xQO9!aeg-5lU66-z*cK3e# z@U?VAtm9;qnOn>i%Lj$Qx7Gy+!*Ue;680*ITcBE^&-R54WcWpD9j_&%qlOVk}=4AC;x0Ey&hRD}DHORX%C}OskWra3sC~UF~6@?5B zm0J&%vgLSDKMhWilN8@oMbFEj_S@0|Vmd4@H+t5h+}Fngr_H=c@3`@9v2Twx5i+D2 ztF7O963tIOP*SjL{2?xGnQWUc^^q)*N$WL7pYm@+W&_W0Y53H|CXY9$kkGI$#PDwM zZ*TqRw_p>`t;8F43BjH1P^Kqw87C1^&Nv<8uxmSUcVe5GZ^~h9rtw=knQE4dH=FwH zy~({NnGbFVX}!ax<6Ebb{uYsvFwxk0eB3vzjV-4onEO!&bMq*LZen$kZ2Rc#q$$Ao zZBnlv5rVn}=y^puIsK<{w zkP`-B3GNG90&2sWF*EF$DXt3Y`66~CF)}2U74|C>mz5W0`S%BVhuQ_LDN@&CoZY^6 z4W$et_+(DfQ-x%e&!TiGpH7SU_yr)lhT`?~Ss(rE(C*i?o$XAC(7haz$(l??4WHIXGgn}oXgca?mxy`6MU=aZ|$ZY=h7GEL&2fIwbZ+DDn`it zy_Cm;lecpVx>KiR!!lIAcb$$4l{&?yk<@)JBQ{I)GM5^ic-QE-LT?}1e%W{}X0QLk z-R;(sDWMvb(VOK?F5GLG%f<5rn#VV!F-KLu>&jcF;Yy3uVX@XES|7-I>Ou#B=YW^g}{zW`712I7^sUd5qNQz@O`A~qPhBj>r+bk9%F1cdD*Az z^n}YoTU&c4v%KzjfBLL*U+pG;!*D(iH&0#x160d(CG|bc(cHB+Cd;8BIUb#8N@H3$ z&e*AvqA1*dn?qYEK^D)?;#+oqZWCS}hfalDTKDCa3e)ZS4I15ao&tq{?uR$nckkZb zbzPhG_vA}m&_K2ZaqM69tP4v<>vqPM(^0DgEs?ybRL-#I+nI~5eC57Q<+WU7H-6V8 za>*^tVq5Zs5){%W=d!yL!yq1ewsJc_fzLdB@M@`#w7*0D%6P5v*GMjMJ%_jM8k+li zHre(TM7v{0QOq5^%`ww{OIhMj+?0sBVsK&i(<`Ug9T&9Y$_lExD#u$lVt(zkYF7AW ztB1$a8S76^=4#!%$tWk0w(XPOoB7v$9ED1I4QB4vE$Ig<3zSiuqD$zjbzhM$G`};s zq@TUVLzas&UHkO-8|(f|#!R33&I;NzIDgzneAoGFWm>~qroL`9CCkbmd%aIgPf_>H zG4Lkt^YB_-XVL1I;}aUlq>uTbOrF5-_D66SlYA(J0-tI41$x&}WlDN~1o(VxnH_j%*4$|50y5@+D*VydDysm2Up(bhyLwhD9?YWNf2~$)QFiB_zjGQ$%^LRH{Q_x~}*{`3D@lTo+NJV_r1d zS`!QJBv~Hxa84*stOcKAdCeTF&4+`%bDyXQk4T@$Z+gX?VVT9k zOh2`KRl(+S@}uaFP5j|YwQU=X2x>d?E4vRbN&D3V;Jx$o_X?b#bTmJ|RxuD)!GC(s zna00n%~4awP6vJC64f;Z~6yYZp|~E~{j#SCL2hUG8T@I7;Z@GZuUot>xd?QrbVhJN@2Y_S46m2bXBt zi`oL@+-(W!)$~8&T{b9N8E~G{=!-0jS*MbX{P?h^EKyrVqs@zd!&4Pa!!K?p8c=;o z>}FVsc$24U9nJ98=9PfR9?i8LI*+gp3*&8;uFTN) zIW_2(le&p!=O6|JBkX=;hnnRMIf@xa0BU{~Mlu%~m`p$u$`mwjYD?2C@>HSF4`^pS2&^xf-p&w62C zf6OD-nvc~dKrQWcf;gSSPc9!T?d0mq$2=Y$^SE?76^A*{JoB^gr^S3DndZfxTB7I+ zyL~r^Zu#f4YZiyxt>ct7t_`5epkF~Ty0#lMDZhaxtr>L?+bh*s-i$@T%vH#{1hMWb zhy_Rrlsx?66=vbna3e78q^>6u>tyZKT)Qs`_{tRH%zHAg`DwLAO&%r+NvwSiQEaQa z7^_4Wcdr@cn!JoBKKz`mf!vWxy*%IxW6^{A0c}riJ^~e|q(6;-y7=r9@J-2b7dlG= z_01I=9xg+`uPRP>=um295{=G-rr=u@mK@brGi1Wjz0`?6#l1+DQtm6Tk@hWTL`?j6 zb3D`0sfAd)&a^Av_D9_zoSOtV^=EJJA-IaHFweKVmHhovK-;3N3cT>+3AHa{1yaN} zn~KV$A}G7RG)qtjvWq89{^rSOd!JT#BBXv`p8w=#8w+L8UA zm4-Pmz^34a`1p7c=?F^ehp*!(hCi)+yQe$2`rtmrVF{<(_VYFb@uu|J$i|b8Cw9x! z;59r$B-ZuQRnsPo>RW7zhE^CyM<(z>I~LV`2C>tT8L02mftbPMeORAvmp+v{RK6Wg zdDwvm!8OV3FGw=uBvdM#oxnTEYvhuz9dLi^QrM8j7 z*9$ooeSU&6)x$052d^9*hu-eppTInjSD3rIl?_$HOBSOi<$)DeK6}P~Lr=AibV37M(hp&Bd zi02UF?2i=vMLvhk-RL*SeqKy(KyzeD9%{ldc5lRDSNqZmwa$Z2AFM{ljZcu{QCSc& zCc|eM2}|002S=5u>GggDS=BfoSC-2Jx$nHceneV!K2y8+wmGu<__7q&hocVn6tzBE zG$U*TnYNRtevYXRBk$tC0>q!DJS@@GpjbCzfpT#Pa9n9G8yOed^e`qBKrgvK;z1gy z#{;AttJ_)Wgzc_#)q4#~B4&mw-7D~j+93}u9TI~oM0KHL^43g74sD>7x5cq0ft`u| zG=~Jr9(<9wlVY<=KZ5z-&b(GMI$Wnqi#uWBB@nrXaf4#-e$?;5_j%g1a0V1C4)+LR zk+(8G?l`$T@kBIJ`wo6y;XW*J`YNK$^8q8~2XP(``HlPrvX1gwfMi|_&Qx7Y z!j#{~9ug4_2w9IGViaX2;D(>W65NYra>^tUcgac*#b;F?_ZV1wfjaIXg21^+=JS*o zesF_`ML1MoQG66^My$3^>K&GN>`=XOSb|VWuuOV7|CFX!OdWBjEDxf*$sY#7rQkohW0=;ntr5K5QiYJ2-O! zrwxEHu^IV{qXp@-TfIcsZ`~lup!B2WYozjcK}t7cR6t=veDE-2A{;By>pKOzmCBQ$ zQ~}a=9#Vc&*QbgR34LL6NeUB)%nL`iS?S;6I~~%~qiHLt%?6Lwhxlp{p7G$s?Xcp3 z{<&k7SNJ0y$Iy*Y)C;Q)3JH@_MP!;6rF%$vr#UQn1Lg1_-(nwUj$gfILAY|i#8$n= z>ypr!8>P*pmMQ$gc6x_nBh)lw4K2@%)1|{@o}nI9t>hB@K&y_g5yZaF@C|PdyLzqz zsqtW}qh!%`B3Ho4a`NlY&?_EvM#6j2RZmfmY%tpaL5_#nhnTByz@prV6?NZr zNhG>D|5G6R)%3}c>{s!Ckf2LcBxO^J9lp0XZ3hrO{1xhS$5 zr_V)?2jX&F{+g5i!h{>l;Z(@t%7AR;46X;4W~2CJBA+MON1K8FWKMvcV5!GV3ARhZL6W*a{! zESt92lA*yN{f*?4zBbQLG;~Htt%F|jx6gR?pFMZZ80rR-pA6xX508z_?plMkhJ$mv zkocoE#{@jmcmCI3YeuG)B-Y$thlHs0gKn*Jq09x%y$dNa9p%pUTl1w09Se34e0xrQ zk51X^iofdFu^@$L6vv*;PMB#IyGZNN#gu7F5MNbFaPHSJRjdqkjxluxSlHQ7gmXh& zFNmq$KdS|QbP6u1E^g6ZD6iAE12rHLD)-iMexOh(ZYcAjK`D^!+3hI4r?}_vR4d!-P<6&+0G&U1v6R0_LyV9AMMmT+7Mul}8rc`xSOx80l)m(KA%8I(BX^AoJVRYW7<#*i~d zXPQdg60A0rdR=T*DUh~JcJ1pZ%v%` zXpR%l>BEqtM#WV?YDqD;t_E*K;VD6GR3}o-4KK*_)9ARzm7q1qN&3it^w2+_<}F5}I`(bcWlf~leHc@^{6LyThbu?#CuyP=l;?Q@z?7vozYiD$qWti7Mc zo?@clNQ`6rg(HhoWDGk4smkY^ph01$k-TsCLMuFaOiDcmoUjl=P7w2D|Aeg3Hv$&f zdmMoTmYQR;=*TBVV|Yp5!r5hkHcL1o>D}75M$@@4Zp`}vNo!p>byl2X$N+Sk0pXdb zs+7J6X60*M$+~Xn3w8Wcl$i4tycFz9^+bdU{|}t8wI`7)Tp@98TWLa;7b;EG+)T|& z)e6jd+o(J?)AvfuGiP?Y%+qvA$?|7y#b_0rhoadNqmIi5)+#n)>FbKi zBzxQiVNi>@%z9Jh<S@c+QY zt$9St{qE&%2H5u9*Da*+CXf3c{M>6iei?!$aQ&$nMu{$9udK#UY$~<@K*+Rwk z<2ORK>3vM%prn4ex3Iq#*c@}z+DS}I>|Oz;d5CDC=WqxL`iEc7sn}u-0 zEx%ouX%~|S=D5}CDB4*8yS#6}v>kZYi)BsUSIfYkC1c2z@`T>1(;#?MeRmDNWUjEE znT6$+c?5-vxAmUrnKSPckaaaRX@$U!r`6dlIxmR~8+8)nR2#pQcVGh`n>el*#I?oJBOHZEam%BZs$BeyqqUYk*q zk&{b=l*!huwOM0uLqgvGL?;k3GIGCwolroyJ_m*7r@0^#s+5z%X$#WrcIezf>&|EY zw>$LrY&)_bT;Z%+1yS*p6x}!c=IO~bG7*=vlC9>;Rur6|lQ@f@U{v^}JJ)J_DKsj* zQ_A+1e}tH9O}+x~#k_1D^sE^thMi{s$y!bP_+5M-tq7%X&xr1>wy%PQA_^A|uX7md zQ0#Am@tl52NUtOg{czNFOCQY!n8iZ%k&S7F{Y~$ssGfFj(IpXYMz>~k=jge*DT&W# z=7et1B36zb1T)~4Vgo~PC}CMy5X3{>l$M|~lBrN_qa*Rw$x}*Dog=YO4;L3Vm(F(? zn#82-3p{EhU%OJNaz836Y86`hwL$}FSt=^3bbsmBVAd199MdO=hbBWW44NXZLIE5# zWFCYatIFG?3doeCq_pEF#7@`2&V@U}iw?S47qlc*hl$9+j2B16f~gK1SeznNoLw{` z4qUW>UOV!V#b4@x6Ql*AFZYqJ$x$LAtNe#tW<8uvKR?!WAPo>! zl`3e^V%;PY-#MJ5UGKDj9x)M9fLjQa=ZQDFBBJbF$v?)Ws+ z3SmlH4?ZWIUf9y21nmat2cdgQdQ3H$H(BhP2g@`bP5QooV81=BFS^}T*9lXdrTR#U`xZ_#a36B@5Pt+&HP&K?8+#6mCp1cYAbU{5s6M!$?$;#Gw#Q?+eK zH|^|vhc*;DHTV#U{M!J;a|JJm>b2eGf{jzGwDqRuS+Ng&&ym#Q`mPRJK7kjxN6jG_ zB-O2%9WsZg5|f8EH|(a{YFbSg{_BH8<4jq&Z%3DGo?$rjJtfwT%PmRv* zFRCS=_r;+7)M^#b7LYa4F^p;-jT*o`<~-EBVOub?c8Ral1GRP#ZV}K)*3!^bADEg+ z1E<&pbj_LC4sd>ML5Pr;ARU;##^h3URa{&=9lB5n3Hy=xOe()mlFcK%X|PGu1{G=F z^}()sdHkH}-D}ryA$0J>=>1OFH7Qk72>-Anz3DD%Exu|EwKblv$ z`z$N*f8HfC7Gc-qv_*-KC_d9kku`#Z=IFb-7}voc=_e~)1v zdy}9uozThSDPU!EwyyL#?$w3(2p+ULhE?*mZEBj6n&76F2LJWfVcS65@Ox!&X(n@q_r zRn-F{Y=W}^YR;vSFzk=(pImRCP8iuVpJ$FoArtq3vbufzMFrV{FowX@}vy?-p! zC~!rm=rJ9{LNfBfpN4(Eg)zAAC=X^0s_DA#76>IL4tqT1eLQgGRPyFVX?6GZsI80X zQAO+%V`JohT-UK;>df%F!BZ6qWQ641M;RswE573o$Bs#S*5k|lTA84cb1$Gk0Caa= zCHoW%?c)d5{Fh(x`-~npM=C~gUiJS0J1pQQGw zuSMckuVI{^mI&Uju>I1rKPC$hvI_Q^Y@JdgXp%nDCx}N#YLnT(;L`0tzboiA_Pl+C zHqn9nB}b4^F340n3k{L<(!Pd&IyAq{zMp^y137u(vm+T`zGtM8(Ox-B-;1h7*lv`9 zt^>Vtr|wYkz)`=V(f)hn54U{()6`H|IP8H!b;gbYHfn;;DYo>WFB=<;WKb?kEkWFc zsJs+~CeC|(K{h=O-E~@|1QSfJePSe6LmE=c?u4E9kqJy-tG!M6?~cRJx`^VPrJF6G zEGo3@+%EPTkW!Zs9m72zi@K}6xFF=Va|OH(sofoDjxN;Y)C`_2?Nk7raPqufcr+=4 zupFCWPNLvNR9j%3dveWH-|5dwczx!BVUdwjo2HJxd_h>OSlAT)3B0e%mM~!$4j#c3 zt%sYMXXtm25EH8g4vHe}qfy2mKn<%>YB$$GH*DKY(8y6qfwbNn6-KRWho;bPyz6Hj zlsZ+b!8n{&VKdyn5sb`xGsioIS1b5%f)E3B)}Li20NaJZUmWgXj-kMIOS1FO!cnQl z9;Q0r^m}*(Y3<`Wqr&AYpevl?i(UvH0iu;d)a@ShwSG!$M`LN8AkuTHh*Luo%-9LdkV2uMx0|qU@%)#Uv5U^jI~bH>>9%9qK=eeaSzT^3LN!V8lv7dz z8%$)4im%J7r#skC7mI8Co=C7>7QMM%>8fe(p@I(%CZXg#WE!;t7dzQwzJ% z{KpGza$A1UmeBPrJX}*Q7vZ$I{ z^QD(0v95`AN3T;Jt=_mhepK%9z$azVLfpUHyFXFKAyFiS*Fe5;NJ$)ZwSd^5*`;a@ z(iE%8&;%>zZ_0J`0r^duC;Yt@e2_&)uOgPOA;w~YGTx8f?g)8eQ6y@95DNdb>Z=&IJSVU_AE#t34XqeKa;=O?qQzhR@Wv zzC|zp{^;4Y&4JD*TEQP*J0-hbogj=zSsrW*(QVo01$4f{H0%~xz6%qY}t*Piz)|UCDALt?!vojHfAEOi+aw>ri0=#YLB2+_}v#=>?--W?(*g%v|_GEb4~jZ^jg(KiN(b2gCIL!IJ3Uq z*w`2+{vf8=gpCF<)R8J)OXJJ1W(4np_QU^`iQz-A zix&-0c}P8`e}(w*LO>)C@dF2cj?zW>QurZUrFrmWf14LMJ^c8jGqN!_MjeI9f09uj z&3d^FKi$cF`079KN~b)+-`*J`mPjZH{F_+#xguAQB0&UdZ3{(rB6&Jd_I+4BEojmc3sf9?}6O ze~1xtE&=~_3CSSp+Ck;fTpMQ#tL|XuZv-BGT=A4GdOS#*)eFbZKd8QNR3L-+Jh21|3rm2y^T7!?HpB&m zfhUPCL#HeN5;3IlZ^99?Dl}uot$w`ZLFjx^y5bHF3FbsQpzwY_MKbW345gHpi z4}{o(y#rbvi_HL}2&Zn;`x*4G+xB-VkIT9hKmYpB4K9QZS9o4+uCOZuU9-qFOW;1> z=D+VRr@Xeax4R9r=uGDq$GrpGf`|=4LdEmkqJZ8 z`{mxyrQd%o6f=LFUGi?*-RRldgXU{f)w>LBEisbDy(NzzjgndmkbP3vstU$X>Po>J zLi}jJKqKY9sb?pHpaazc^=?BktzJ8zu9TG#`f>{;kDPtP!bzC$FT-@PsC`SaIVczIP>My8ISkS@K1>YAF7Dx;&Lz2wLB zzM6VtKx=X;swhciWo4Vom)zI*=sP}0}0*Hbom z$E}YmlhaV-xEWlyc#&<1>PjYL^3aZXs>mlLC9y0mQP0cB%BHMvE!hAZmg1mQt>)s& zsj}lA7Zw~GEU8U=r`vBgJ1yTgA}m|CGW(%G4;;l(h%sdG4d2tSp8>h68VbKJ6*-d)%-muJA{=MF~rNZX{ypZShU>erSNs_s`^@2cuclBAF5K5e{wS$0A=G$mK3@pgmWg*5FH zE7byJHABPajHy!%-Q5D-_uO}FlK_bpT!Vyr=>&l(e}SQRc(sOU3Nf_lDk%AsA(s{Q zq@p1wVecU~VOiT*YTewo>C$U6gG#$wwPFqLpeQOcAD|%K)O;q19ne-e-$Hf1!m~%HUMx^I=*b8%Qv(hsbr|T zDWK8iIjeiyS=Q;c;aWw;V0=}yxP(g)49~}({$TJJ7yZ` z_?wEqf4=Hzm01~S86||AdV9)43$4`f?m}7>u0Eljr%N~b_5@v6+CZ|EYK5XBD^EOe z7>#a#iHox5OwnTrc}Kzr^#fI@F@E|f`tvWfAI&E=(M8TGWM%7LG@M`5wtq26+SYN` zVkv-E)}o?8SYxPRnN*;(DTZeu^-c+0adC)%{Z7gIk&5xBjE9#C3qi1+K(BeKTO&IU zLyxO;iG0R5J55UO;lDP$1S%iL7pDT^9?>&BUKejtTab5iw%V8N)^Torf9 z^~?9lZnC6NFbiG2%H#E#ulx&linfgk_oR)#psT(DD$?hr-@DXEO1?RL1^kZ#DX*GX z&W8(TB}Q9urzAM|_}K0|d=fCtW^MA#w&a^DYm@Dprnf>oDQ$8wq(YK)xQgj40@g$d z0yhj5hNIdYFKwiW#p3clQXrcbcep7}R-!=nrrl?fH{mf++uK;)luLIeLzB~n-HtNu z4?1_+qzLX#i}5vbr^tU#Qyt`;g@%}++9{9hHy3Vmr_|SR&-a%_7)je7Uc_IK zFL(-H0jdM148pnJu>_VU`$LHH2f_ajtq)ev@>d)_AWRS!b)O&tDkup4W!u46VZDN< zSorFP`Yfdj(Hg*X#sDeKtQyjRxC!Hs{7v;k_FtFoa@`4<&`)A$120T4*GX7;CfjwNuJTfs3)87w)K`wWW!&96w zctOLK(s&o2P7)JYakU(KucC|-G}>PRtYSx1*| zJInuvG$$z63ko@dU|&Jbv}q|raw@ExMpkaCgU8*mGnOlzfUTMq`#Vgm3!H9{Q`{f5b_(V z98$XWLc8EN8jkHV4);!D?$tf3zABRZS;T`xN%M(GT}vvx1+jnM0pxtJEJZjHR({h@ zf3LBF;20At!<56W_#G=_S1QIGgje_Xl>U~W)SsQA#CU+ecggQPM0QJ2F$LrWEl_~Q zc;xfb)wNBFY50Ge-7VX?)9ZTnb(Ea<5m4|2xpbKBoB(_sSdL!{B1jYn!arCj_SZo`U`iT3+XbKbGnP`2Hf+ZOgds+<{^R=&s)ni6xeeHOrj} zh=f23n+!gSG@G)G3%B{er{H_8YicqwGFJJ0cz|fYn)~OnyG<@6rKLqJivAIzn)~op zi4))U0=KY~6oIZW&#WlahPO8cJO@%vue)xozJr>r>FY4uc3{-f0L)n(^W^~R{oNSw zcN4li=Z(i;#_?}lzn)rWP&d_;MdJ#^Y17&H`PI#<-apsSDtMkSIBk~=dlm ztDI-foH2=p!mfd!+)bLNT8FqKm7i3D2?QFd$OI4u%s5&fa|ZbZD1we)rwvP>V{}JY zO`ibN`%J-oQCV4;aKZGsm$d8qvDY1ymsTW@MTTt2pQ=uEt8=95tiMfAH3g|cxz^VIX~he4)D!)W$YTZ_0pmOp}Y^p3$V&_@oc<91{}p zm5_TFKtVy$_mi_nmiFa=?gTeDEshNMy{=Qdh$!SmCMGFSemLPUlB9pNPP}p;1@R&R zl0!y8!7hJN9I-@ouk8Eg(l{M^?Y7DDSi?BiaFdSIcl0aF$1Hd($F8qM3fHzxuys^* z#2$(VP(8zZYke+*o@Zx&)5Gjg{0`{ZKt*=2sU7b9l7O|}a^d9l4u)<%x^zDTEnO$5|?fjru-I3n~>X~S}I7O^mnIauX&ucv?XX}4P1KhP#Qos43a^dfhTp`91o-;094s8jzWz22C9nB0{QS=z;~D>lcDQj0izC}9fT{-qE3ml1$u_5W+Rfe~I1 zT3S9PQ2sVUkaH>B7}-VE%Xa`xt2v*TPIeQc54mv>^dTVA1gs2H6n}^@kAHlH`Gx(> zO9$rS8EWl7Dhg28f!O6*UCYrL39Q)w6ZP%julYFjTqk}!7iT2)pBW`f`S7_V)z7%= z02W*O$KmRT_}{V#z8Ks5|CYh|zZd_HcKCl_TBK+^KDDYDo*N6p0X;ByM64J|5;!7@ zQ`dcbWV;};ysGyRWJ8dDdQGo(1++<$I~6P2Id=`!mTno_Fr8JvFqyD||D-7o7Tp91 za{C_{j~&fNZo{Is+>$S~!7Ou3WDl$R;&9^8J4XsQzy20P4_|o@a=QTBD|h_|7+4$Q z{Nzyl^0mG#zrc!+b_hi-9<-X%62H3R~&d+YjD)!%Nx v0W$ZW{j&do^?;$d{{M1uE2r40eH`?2%TvvTce-IB5Vvk9T+hCC|JnZnAfRvw literal 0 HcmV?d00001 diff --git a/usermods/Battery/assets/installation_my_config_h.png b/usermods/Battery/assets/installation_my_config_h.png new file mode 100644 index 0000000000000000000000000000000000000000..06235a224d34062889caf31417df28f9abce54fb GIT binary patch literal 50412 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 literal 0 HcmV?d00001 diff --git a/usermods/Battery/assets/installation_platformio_override_ini.png b/usermods/Battery/assets/installation_platformio_override_ini.png new file mode 100644 index 0000000000000000000000000000000000000000..72cca7dc15585894b920345b06ed58bfc01c9500 GIT binary patch literal 55011 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�?1*RrW`e2$7Yd=H_xI9t+j@PVS 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 literal 0 HcmV?d00001 diff --git a/usermods/Battery/readme.md b/usermods/Battery/readme.md index b3607482a..b6918c596 100644 --- a/usermods/Battery/readme.md +++ b/usermods/Battery/readme.md @@ -6,53 +6,78 @@ Enables battery level monitoring of your project. -For this to work, the positive side of the (18650) battery must be connected to pin `A0` of the d1 mini/esp8266 with a 100k Ohm resistor (see [Useful Links](#useful-links)). - -If you have an ESP32 board, connect the positive side of the battery to ADC1 (GPIO32 - GPIO39) - -

- +

+

+
+ ## ⚙️ Features -- 💯 Displays current battery voltage +- 💯 Displays current battery voltage - 🚥 Displays battery level -- 🚫 Auto-off with configurable Threshold +- 🚫 Auto-off with configurable threshold - 🚨 Low power indicator with many configuration possibilities +

+ ## 🎈 Installation -define `USERMOD_BATTERY` in `wled00/my_config.h` +| **Option 1** | **Option 2** | +|--------------|--------------| +| In `wled00/my_config.h`
Add the line: `#define USERMOD_BATTERY`

[Example: my_config.h](assets/installation_my_config_h.png) | In `platformio_override.ini` (or `platformio.ini`)
Under: `build_flags =`, add the line: `-D USERMOD_BATTERY`

[Example: platformio_override.ini](assets/installation_platformio_override_ini.png) | -### Example wiring +

-

- -

+## 🔌 Example wiring -### Define Your Options +- (see [Useful Links](#useful-links)). + +
${isM?'Start X':'Start LED'}
+ + + + + + +
+ +

ESP8266
+ With a 100k Ohm resistor, connect the positive
+ side of the battery to pin `A0`.

+
+ +

ESP32 (+S2, S3, C3 etc...)
+ Use a voltage divider (two resistors of equal value).
+ Connect to ADC1 (GPIO32 - GPIO39). GPIO35 is Default.

+
+ +

+ +## Define Your Options | Name | Unit | Description | | ----------------------------------------------- | ----------- |-------------------------------------------------------------------------------------- | -| `USERMOD_BATTERY` | | define this (in `my_config.h`) to have this usermod included wled00\usermods_list.cpp | -| `USERMOD_BATTERY_MEASUREMENT_PIN` | | defaults to A0 on ESP8266 and GPIO35 on ESP32 | -| `USERMOD_BATTERY_MEASUREMENT_INTERVAL` | ms | battery check interval. defaults to 30 seconds | -| `USERMOD_BATTERY_{TYPE}_MIN_VOLTAGE` | v | minimum battery voltage. default is 2.6 (18650 battery standard) | -| `USERMOD_BATTERY_{TYPE}_MAX_VOLTAGE` | v | maximum battery voltage. default is 4.2 (18650 battery standard) | -| `USERMOD_BATTERY_{TYPE}_TOTAL_CAPACITY` | mAh | the capacity of all cells in parallel summed up | -| `USERMOD_BATTERY_{TYPE}_CALIBRATION` | | offset / calibration number, fine tune the measured voltage by the microcontroller | +| `USERMOD_BATTERY` | | Define this (in `my_config.h`) to have this usermod included wled00\usermods_list.cpp | +| `USERMOD_BATTERY_MEASUREMENT_PIN` | | Defaults to A0 on ESP8266 and GPIO35 on ESP32 | +| `USERMOD_BATTERY_MEASUREMENT_INTERVAL` | ms | Battery check interval. defaults to 30 seconds | +| `USERMOD_BATTERY_{TYPE}_MIN_VOLTAGE` | v | Minimum battery voltage. default is 2.6 (18650 battery standard) | +| `USERMOD_BATTERY_{TYPE}_MAX_VOLTAGE` | v | Maximum battery voltage. default is 4.2 (18650 battery standard) | +| `USERMOD_BATTERY_{TYPE}_TOTAL_CAPACITY` | mAh | The capacity of all cells in parallel summed up | +| `USERMOD_BATTERY_{TYPE}_CALIBRATION` | | Offset / calibration number, fine tune the measured voltage by the microcontroller | | Auto-Off | --- | --- | -| `USERMOD_BATTERY_AUTO_OFF_ENABLED` | true/false | enables auto-off | -| `USERMOD_BATTERY_AUTO_OFF_THRESHOLD` | % (0-100) | when this threshold is reached master power turns off | +| `USERMOD_BATTERY_AUTO_OFF_ENABLED` | true/false | Enables auto-off | +| `USERMOD_BATTERY_AUTO_OFF_THRESHOLD` | % (0-100) | When this threshold is reached master power turns off | | Low-Power-Indicator | --- | --- | -| `USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED` | true/false | enables low power indication | -| `USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET` | preset id | when low power is detected then use this preset to indicate low power | -| `USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD` | % (0-100) | when this threshold is reached low power gets indicated | -| `USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION` | seconds | for this long the configured preset is played | +| `USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED` | true/false | Enables low power indication | +| `USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET` | preset id | When low power is detected then use this preset to indicate low power | +| `USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD` | % (0-100) | When this threshold is reached low power gets indicated | +| `USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION` | seconds | For this long the configured preset is played | All parameters can be configured at runtime via the Usermods settings page. +
+ **NOTICE:** Each Battery type can be pre-configured individualy (in `my_config.h`) | Name | Alias | `my_config.h` example | @@ -60,62 +85,85 @@ All parameters can be configured at runtime via the Usermods settings page. | Lithium Polymer | lipo (Li-Po) | `USERMOD_BATTERY_lipo_MIN_VOLTAGE` | | Lithium Ionen | lion (Li-Ion) | `USERMOD_BATTERY_lion_TOTAL_CAPACITY` | +

+ +## 🔧 Calibration + +The calibration number is a value that is added to the final computed voltage after it has been scaled by the voltage multiplier. + +It fine-tunes the voltage reading so that it more closely matches the actual battery voltage, compensating for inaccuracies inherent in the voltage divider resistors or the ESP's ADC measurements. + +Set calibration either in the Usermods settings page or at compile time in `my_config.h` or `platformio_override.ini`. + +It can be either a positive or negative number. + +

+ ## ⚠️ Important -- Make sure you know your battery specifications! All batteries are **NOT** the same! -- Example: +Make sure you know your battery specifications! All batteries are **NOT** the same! -| Your battery specification table | | Options you can define | -| :-------------------------------- |:--------------- | :---------------------------- | -| Capacity | 3500mAh 12,5 Wh | | -| Minimum capacity | 3350mAh 11,9 Wh | | +Example: + +| Your battery specification table | | Options you can define | +| --------------------------------- | --------------- | ----------------------------- | +| Capacity | 3500mAh 12.5Wh | | +| Minimum capacity | 3350mAh 11.9Wh | | | Rated voltage | 3.6V - 3.7V | | -| **Charging end voltage** | **4,2V ± 0,05** | `USERMOD_BATTERY_MAX_VOLTAGE` | -| **Discharge voltage** | **2,5V** | `USERMOD_BATTERY_MIN_VOLTAGE` | +| **Charging end voltage** | **4.2V ± 0.05** | `USERMOD_BATTERY_MAX_VOLTAGE` | +| **Discharge voltage** | **2.5V** | `USERMOD_BATTERY_MIN_VOLTAGE` | | Max. discharge current (constant) | 10A (10000mA) | | | max. charging current | 1.7A (1700mA) | | | ... | ... | ... | | .. | .. | .. | -Specification from: [Molicel INR18650-M35A, 3500mAh 10A Lithium-ion battery, 3.6V - 3.7V](https://www.akkuteile.de/lithium-ionen-akkus/18650/molicel/molicel-inr18650-m35a-3500mah-10a-lithium-ionen-akku-3-6v-3-7v_100833) +Specification from: [Molicel INR18650-M35A, 3500mAh 10A Lithium-ion battery, 3.6V - 3.7V](https://www.akkuteile.de/lithium-ionen-akkus/18650/molicel/molicel-inr18650-m35a-3500mah-10a-lithium-ionen-akku-3-6v-3-7v_100833) + +

## 🌐 Useful Links - https://lazyzero.de/elektronik/esp8266/wemos_d1_mini_a0/start - https://arduinodiy.wordpress.com/2016/12/25/monitoring-lipo-battery-voltage-with-wemos-d1-minibattery-shield-and-thingspeak/ +

+ ## 📝 Change Log +2024-05-11 + +- Documentation updated + 2024-04-30 -- integrate factory pattern to make it easier to add other / custom battery types -- update readme +- Integrate factory pattern to make it easier to add other / custom battery types +- Update readme 2023-01-04 -- basic support for LiPo rechargeable batteries ( `-D USERMOD_BATTERY_USE_LIPO`) -- improved support for esp32 (read calibrated voltage) -- corrected config saving (measurement pin, and battery min/max were lost) -- various bugfixes +- Basic support for LiPo rechargeable batteries (`-D USERMOD_BATTERY_USE_LIPO`) +- Improved support for ESP32 (read calibrated voltage) +- Corrected config saving (measurement pin, and battery min/max were lost) +- Various bugfixes 2022-12-25 -- added "auto-off" feature -- added "low-power-indication" feature -- added "calibration/offset" field to configuration page -- added getter and setter, so that user usermods could interact with this one -- update readme (added new options, made it markdownlint compliant) +- Added "auto-off" feature +- Added "low-power-indication" feature +- Added "calibration/offset" field to configuration page +- Added getter and setter, so that user usermods could interact with this one +- Update readme (added new options, made it markdownlint compliant) 2021-09-02 -- added "Battery voltage" to info -- added circuit diagram to readme -- added MQTT support, sending battery voltage -- minor fixes +- Added "Battery voltage" to info +- Added circuit diagram to readme +- Added MQTT support, sending battery voltage +- Minor fixes 2021-08-15 -- changed `USERMOD_BATTERY_MIN_VOLTAGE` to 2.6 volt as default for 18650 batteries +- Changed `USERMOD_BATTERY_MIN_VOLTAGE` to 2.6 volt as default for 18650 batteries - Updated readme, added specification table 2021-08-10 From ecc9443677e1c69291138e8bf8f54d524614a144 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 11 May 2024 14:45:42 +0200 Subject: [PATCH 102/148] (0_14 branch only) adding compatibility for building with upstream arduinoFFT 2.xx support compilation with new arduinoFFT versions 2.x --- usermods/audioreactive/audio_reactive.h | 18 +++++++++++++----- usermods/audioreactive/readme.md | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index fec0525ec..716953957 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -177,9 +177,6 @@ constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT resul // These are the input and output vectors. Input vectors receive computed results from FFT. static float vReal[samplesFFT] = {0.0f}; // FFT sample inputs / freq output - these are our raw result bins static float vImag[samplesFFT] = {0.0f}; // imaginary parts -#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT -static float windowWeighingFactors[samplesFFT] = {0.0f}; -#endif // Create FFT object #ifdef UM_AUDIOREACTIVE_USE_NEW_FFT @@ -198,9 +195,15 @@ static float windowWeighingFactors[samplesFFT] = {0.0f}; #include #ifdef UM_AUDIOREACTIVE_USE_NEW_FFT -static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, windowWeighingFactors); +#if defined(FFT_LIB_REV) && FFT_LIB_REV > 0x19 + // arduinoFFT 2.x has a slightly different API + static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); #else -static arduinoFFT FFT = arduinoFFT(vReal, vImag, samplesFFT, SAMPLE_RATE); + static float windowWeighingFactors[samplesFFT] = {0.0f}; // cache for FFT windowing factors + static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, windowWeighingFactors); +#endif +#else + static arduinoFFT FFT = arduinoFFT(vReal, vImag, samplesFFT, SAMPLE_RATE); #endif // Helper functions @@ -300,7 +303,12 @@ void FFTcode(void * parameter) #endif #ifdef UM_AUDIOREACTIVE_USE_NEW_FFT + #if defined(FFT_LIB_REV) && FFT_LIB_REV > 0x19 + // arduinoFFT 2.x has a slightly different API + FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant + #else FFT.majorPeak(FFT_MajorPeak, FFT_Magnitude); // let the effects know which freq was most dominant + #endif #else FFT.MajorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant #endif diff --git a/usermods/audioreactive/readme.md b/usermods/audioreactive/readme.md index 47804b611..8959021ba 100644 --- a/usermods/audioreactive/readme.md +++ b/usermods/audioreactive/readme.md @@ -38,7 +38,7 @@ Alternatively, you can use the latest arduinoFFT development version. ArduinoFFT `develop` library is slightly more accurate, and slightly faster than our customised library, however also needs additional 2kB RAM. * `build_flags` = `-D USERMOD_AUDIOREACTIVE` `-D UM_AUDIOREACTIVE_USE_NEW_FFT` -* `lib_deps`= `https://github.com/kosme/arduinoFFT#develop @ 1.9.2` +* `lib_deps`= `https://github.com/kosme/arduinoFFT#419d7b0` ## Configuration From 9e468bd059510c66bbb99e41ed4855193a9cc05c Mon Sep 17 00:00:00 2001 From: Brandon502 <105077712+Brandon502@users.noreply.github.com> Date: Sat, 11 May 2024 13:57:21 -0400 Subject: [PATCH 103/148] Pinwheel Expand 1D Optimizations Added small pinwheel size. Adjusts medium and large values. --- wled00/FX_fcn.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 17a504ea0..ec0e087bc 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -638,9 +638,12 @@ uint16_t IRAM_ATTR Segment::nrOfVStrips() const { } // Constants for mapping mode "Pinwheel" -constexpr int Pinwheel_Steps_Medium = 208; // no holes up to 32x32; 60fps -constexpr int Pinwheel_Size_Medium = 30; // larger than this -> use "Big" -constexpr int Pinwheel_Steps_Big = 360; // no holes expected up to 58x58; 40fps +constexpr int Pinwheel_Steps_Small = 72; // no holes up to 16x16; +constexpr int Pinwheel_Size_Small = 16; +constexpr int Pinwheel_Steps_Medium = 200; // no holes up to 32x32; 60fps +constexpr int Pinwheel_Size_Medium = 32; // larger than this -> use "Big" +constexpr int Pinwheel_Steps_Big = 296; // no holes expected up to 58x58; 40fps +constexpr float Int_to_Rad_Small = (DEG_TO_RAD * 360) / Pinwheel_Steps_Small; // conversion: from 0...208 to Radians constexpr float Int_to_Rad_Med = (DEG_TO_RAD * 360) / Pinwheel_Steps_Medium; // conversion: from 0...208 to Radians constexpr float Int_to_Rad_Big = (DEG_TO_RAD * 360) / Pinwheel_Steps_Big; // conversion: from 0...360 to Radians @@ -661,7 +664,9 @@ uint16_t IRAM_ATTR Segment::virtualLength() const { vLen = max(vW,vH); // get the longest dimension break; case M12_sPinwheel: - if (max(vW,vH) <= Pinwheel_Size_Medium) + if (max(vW,vH) <= Pinwheel_Size_Small) + vLen = Pinwheel_Steps_Small; + else if (max(vW,vH) <= Pinwheel_Size_Medium) vLen = Pinwheel_Steps_Medium; else vLen = Pinwheel_Steps_Big; @@ -736,8 +741,7 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) // i = angle --> 0 through 359 (Big), OR 0 through 208 (Medium) float centerX = roundf((vW-1) / 2.0f); float centerY = roundf((vH-1) / 2.0f); - // int maxDistance = sqrt(centerX * centerX + centerY * centerY) + 1; - float angleRad = (max(vW,vH) > Pinwheel_Size_Medium) ? float(i) * Int_to_Rad_Big : float(i) * Int_to_Rad_Med; // angle in radians + float angleRad = (max(vW, vH) > Pinwheel_Size_Small ? (max(vW, vH) > Pinwheel_Size_Medium ? float(i) * Int_to_Rad_Big : float(i) * Int_to_Rad_Med) : float(i) * Int_to_Rad_Small); // angle in radians float cosVal = cos_t(angleRad); float sinVal = sin_t(angleRad); @@ -885,7 +889,7 @@ uint32_t IRAM_ATTR Segment::getPixelColor(int i) float distance = max(1.0f, min(vH-1, vW-1) / 2.0f); float centerX = (vW - 1) / 2.0f; float centerY = (vH - 1) / 2.0f; - float angleRad = (max(vW,vH) > Pinwheel_Size_Medium) ? float(i) * Int_to_Rad_Big : float(i) * Int_to_Rad_Med; // angle in radians + float angleRad = (max(vW, vH) > Pinwheel_Size_Small ? (max(vW, vH) > Pinwheel_Size_Medium ? float(i) * Int_to_Rad_Big : float(i) * Int_to_Rad_Med) : float(i) * Int_to_Rad_Small); // angle in radians int x = roundf(centerX + distance * cos_t(angleRad)); int y = roundf(centerY + distance * sin_t(angleRad)); return getPixelColorXY(x, y); From 1ff5cb0596d4a355a96570a296b5c7f58ba7cf92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Sun, 12 May 2024 11:12:13 +0200 Subject: [PATCH 104/148] Experimental parallel I2S support for ESP32 - increased outputs to 17 - increased max possible color order overrides - use WLED_USE_PARALLEL_I2S during compile WARNING: Do not set up more than 256 LEDs per output when using parallel I2S with NeoPixelBus less than 2.9.0 --- wled00/bus_wrapper.h | 65 +++++++++++++++++++++----- wled00/cfg.cpp | 14 +++--- wled00/const.h | 6 ++- wled00/data/settings_leds.htm | 86 ++++++++++++++++++----------------- wled00/set.cpp | 51 +++++++++++---------- wled00/xml.cpp | 36 ++++++++------- 6 files changed, 156 insertions(+), 102 deletions(-) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 99ae4c5ef..57e98467e 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -224,8 +224,11 @@ //#define B_32_I0_NEO_3 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS + #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_NEO_3 NeoPixelBusLg -//#define B_32_I1_NEO_3 NeoPixelBusLg // parallel I2S + #else +#define B_32_I1_NEO_3 NeoPixelBusLg // parallel I2S + #endif #endif //RGBW #define B_32_RN_NEO_4 NeoPixelBusLg @@ -234,8 +237,11 @@ //#define B_32_I0_NEO_4 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS + #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_NEO_4 NeoPixelBusLg -//#define B_32_I1_NEO_4 NeoPixelBusLg // parallel I2S + #else +#define B_32_I1_NEO_4 NeoPixelBusLg // parallel I2S + #endif #endif //400Kbps #define B_32_RN_400_3 NeoPixelBusLg @@ -244,8 +250,11 @@ //#define B_32_I0_400_3 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS + #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_400_3 NeoPixelBusLg -//#define B_32_I1_400_3 NeoPixelBusLg // parallel I2S + #else +#define B_32_I1_400_3 NeoPixelBusLg // parallel I2S + #endif #endif //TM1814 (RGBW) #define B_32_RN_TM1_4 NeoPixelBusLg @@ -254,8 +263,11 @@ //#define B_32_I0_TM1_4 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS + #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_TM1_4 NeoPixelBusLg -//#define B_32_I1_TM1_4 NeoPixelBusLg // parallel I2S + #else +#define B_32_I1_TM1_4 NeoPixelBusLg // parallel I2S + #endif #endif //TM1829 (RGB) #define B_32_RN_TM2_3 NeoPixelBusLg @@ -264,8 +276,11 @@ //#define B_32_I0_TM2_3 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS + #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_TM2_3 NeoPixelBusLg -//#define B_32_I1_TM2_3 NeoPixelBusLg // parallel I2S + #else +#define B_32_I1_TM2_3 NeoPixelBusLg // parallel I2S + #endif #endif //UCS8903 #define B_32_RN_UCS_3 NeoPixelBusLg @@ -274,8 +289,11 @@ //#define B_32_I0_UCS_3 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS + #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_UCS_3 NeoPixelBusLg -//#define B_32_I1_UCS_3 NeoPixelBusLg // parallel I2S + #else +#define B_32_I1_UCS_3 NeoPixelBusLg // parallel I2S + #endif #endif //UCS8904 #define B_32_RN_UCS_4 NeoPixelBusLg @@ -284,8 +302,11 @@ //#define B_32_I0_UCS_4 NeoPixelBusLg// parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS + #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_UCS_4 NeoPixelBusLg -//#define B_32_I1_UCS_4 NeoPixelBusLg// parallel I2S + #else +#define B_32_I1_UCS_4 NeoPixelBusLg// parallel I2S + #endif #endif #define B_32_RN_APA106_3 NeoPixelBusLg #ifndef WLED_NO_I2S0_PIXELBUS @@ -293,8 +314,11 @@ //#define B_32_I0_APA106_3 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS + #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_APA106_3 NeoPixelBusLg -//#define B_32_I1_APA106_3 NeoPixelBusLg // parallel I2S + #else +#define B_32_I1_APA106_3 NeoPixelBusLg // parallel I2S + #endif #endif //FW1906 GRBCW #define B_32_RN_FW6_5 NeoPixelBusLg @@ -303,8 +327,11 @@ //#define B_32_I0_FW6_5 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS + #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_FW6_5 NeoPixelBusLg -//#define B_32_I1_FW6_5 NeoPixelBusLg // parallel I2S + #else +#define B_32_I1_FW6_5 NeoPixelBusLg // parallel I2S + #endif #endif //WS2805 RGBWC #define B_32_RN_2805_5 NeoPixelBusLg @@ -313,8 +340,11 @@ //#define B_32_I0_2805_5 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS + #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_2805_5 NeoPixelBusLg -//#define B_32_I1_2805_5 NeoPixelBusLg // parallel I2S + #else +#define B_32_I1_2805_5 NeoPixelBusLg // parallel I2S + #endif #endif //TM1914 (RGB) #define B_32_RN_TM1914_3 NeoPixelBusLg @@ -323,8 +353,11 @@ //#define B_32_I0_TM1914_3 NeoPixelBusLg #endif #ifndef WLED_NO_I2S1_PIXELBUS + #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_TM1914_3 NeoPixelBusLg -//#define B_32_I1_TM1914_3 NeoPixelBusLg + #else +#define B_32_I1_TM1914_3 NeoPixelBusLg + #endif #endif #endif @@ -541,7 +574,11 @@ class PolyBus { #if defined(ARDUINO_ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) // NOTE: "channel" is only used on ESP32 (and its variants) for RMT channel allocation // since 0.15.0-b3 I2S1 is favoured for classic ESP32 and moved to position 0 (channel 0) so we need to subtract 1 for correct RMT allocation + #ifdef WLED_USE_PARALLEL_I2S + if (channel > 7) channel -= 8; // accommodate parallel I2S1 which is used 1st on classic ESP32 + #else if (channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 + #endif #endif void* busPtr = nullptr; switch (busType) { @@ -1619,9 +1656,15 @@ class PolyBus { //if (num > 3) offset = num -4; // I2S not supported yet #else // standard ESP32 has 8 RMT and 2 I2S channels + #ifdef WLED_USE_PARALLEL_I2S + if (num > 16) return I_NONE; + if (num < 8) offset = 2; // prefer 8 parallel I2S1 channels + if (num == 16) offset = 1; + #else if (num > 9) return I_NONE; if (num > 8) offset = 1; if (num == 0) offset = 2; // prefer I2S1 for 1st bus (less flickering but more RAM needed) + #endif #endif switch (busType) { case TYPE_WS2812_1CH_X3: diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 22bfe577a..addd3fc5e 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -124,7 +124,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(strip.panels, matrix[F("mpc")]); strip.panel.clear(); JsonArray panels = matrix[F("panels")]; - uint8_t s = 0; + int s = 0; if (!panels.isNull()) { strip.panel.reserve(max(1U,min((size_t)strip.panels,(size_t)WLED_MAX_PANELS))); // pre-allocate memory for panels for (JsonObject pnl : panels) { @@ -156,7 +156,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonArray ins = hw_led["ins"]; if (fromFS || !ins.isNull()) { - uint8_t s = 0; // bus iterator + int s = 0; // bus iterator if (fromFS) BusManager::removeAll(); // can't safely manipulate busses directly in network callback uint32_t mem = 0, globalBufMem = 0; uint16_t maxlen = 0; @@ -790,7 +790,7 @@ void serializeConfig() { JsonObject matrix = hw_led.createNestedObject(F("matrix")); matrix[F("mpc")] = strip.panels; JsonArray panels = matrix.createNestedArray(F("panels")); - for (uint8_t i=0; igetLength()==0) break; JsonObject ins = hw_led_ins.createNestedObject(); @@ -815,7 +815,7 @@ void serializeConfig() { JsonArray ins_pin = ins.createNestedArray("pin"); uint8_t pins[5]; uint8_t nPins = bus->getPins(pins); - for (uint8_t i = 0; i < nPins; i++) ins_pin.add(pins[i]); + for (int i = 0; i < nPins; i++) ins_pin.add(pins[i]); ins[F("order")] = bus->getColorOrder(); ins["rev"] = bus->isReversed(); ins[F("skip")] = bus->skippedLeds(); @@ -829,7 +829,7 @@ void serializeConfig() { JsonArray hw_com = hw.createNestedArray(F("com")); const ColorOrderMap& com = BusManager::getColorOrderMap(); - for (uint8_t s = 0; s < com.count(); s++) { + for (int s = 0; s < com.count(); s++) { const ColorOrderMapEntry *entry = com.get(s); if (!entry) break; @@ -846,7 +846,7 @@ void serializeConfig() { JsonArray hw_btn_ins = hw_btn.createNestedArray("ins"); // configuration for all buttons - for (uint8_t i=0; i LED Settings