From 8dd17451495fa96cc87fe6f9a145d7c041b35e86 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Thu, 5 Jan 2023 19:48:53 +0100 Subject: [PATCH 001/162] =?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/162] 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/162] =?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/162] =?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/162] =?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/162] =?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/162] 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/162] 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/162] 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/162] =?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/162] 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/162] 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/162] 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/162] 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/162] 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/162] 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 1bdf3876fc08b5b13c4fc75223c983bc95bf3723 Mon Sep 17 00:00:00 2001 From: muebau Date: Fri, 12 Apr 2024 12:00:15 +0200 Subject: [PATCH 017/162] reset usermod TetrisAI back to initial version --- usermods/TetrisAI_v2/tetrisai.h | 95 ---------------------- usermods/TetrisAI_v2/usermod_v2_tetrisai.h | 52 ++++++------ 2 files changed, 27 insertions(+), 120 deletions(-) diff --git a/usermods/TetrisAI_v2/tetrisai.h b/usermods/TetrisAI_v2/tetrisai.h index ef7868a47..12afbb2fa 100644 --- a/usermods/TetrisAI_v2/tetrisai.h +++ b/usermods/TetrisAI_v2/tetrisai.h @@ -202,101 +202,6 @@ public: } } } - - bool findBestMoveNonBlocking(GridBW grid, std::vector::iterator start, std::vector::iterator end, Rating* bestRating) - { - //vector with pieces - //for every piece - //for every - switch (expression) - { - case INIT: - break; - - default: - break; - } - } - - bool findBestMoveNonBlocking(GridBW grid, std::vector::iterator start, std::vector::iterator end, Rating* bestRating) - { - //INIT - grid.cleanupFullLines(); - Rating curRating(grid.width); - Rating deeperRating(grid.width); - Piece piece = *start; - - // for every rotation of the piece - piece.rotation = 0; - - //HANDLE - while (piece.rotation < piece.pieceData->rotCount) - { - // put piece to top left corner - piece.x = 0; - piece.y = 0; - - //test for every column - piece.x = 0; - while (piece.x <= grid.width - piece.getRotation().width) - { - - //todo optimise by the use of the previous grids height - piece.landingY = 0; - //will set landingY to final position - grid.findLandingPosition(&piece); - - // draw piece - grid.placePiece(&piece, piece.x, piece.landingY); - - if(start == end - 1) - { - //at the deepest level - updateRating(grid, &curRating); - } - else - { - //go deeper to take another piece into account - findBestMove(grid, start + 1, end, &deeperRating); - curRating = deeperRating; - } - - // eraese piece - grid.erasePiece(&piece, piece.x, piece.landingY); - - if(findWorstMove) - { - //init rating for worst - if(bestRating->score == -DBL_MAX) - { - bestRating->score = DBL_MAX; - } - - // update if we found a worse one - if (bestRating->score > curRating.score) - { - *bestRating = curRating; - (*start) = piece; - } - } - else - { - // update if we found a better one - if (bestRating->score < curRating.score) - { - *bestRating = curRating; - (*start) = piece; - } - } - piece.x++; - } - piece.rotation++; - } - - //EXIT - - return true; - } }; #endif /* __AI_H__ */ \ No newline at end of file diff --git a/usermods/TetrisAI_v2/usermod_v2_tetrisai.h b/usermods/TetrisAI_v2/usermod_v2_tetrisai.h index 1c077d048..a312508b5 100644 --- a/usermods/TetrisAI_v2/usermod_v2_tetrisai.h +++ b/usermods/TetrisAI_v2/usermod_v2_tetrisai.h @@ -9,8 +9,6 @@ typedef struct TetrisAI_data { - unsigned long lastTime = 0; - TetrisAIGame tetris; uint8_t intelligence; uint8_t rotate; bool showNext; @@ -96,6 +94,8 @@ void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data) //////////////////////////// uint16_t mode_2DTetrisAI() { + static unsigned long lastTime = 0; + if (!strip.isMatrix || !SEGENV.allocateData(sizeof(tetrisai_data))) { // not a 2D set-up @@ -116,14 +116,16 @@ uint16_t mode_2DTetrisAI() //range 0 - 16 tetrisai_data->colorInc = SEGMENT.custom2 >> 4; - if (!tetrisai_data->tetris || (tetrisai_data->tetris.nLookAhead != nLookAhead + static TetrisAIGame tetris(cols < 32 ? cols : 32, rows, 1, piecesData, numPieces); + + if (tetris.nLookAhead != nLookAhead || tetrisai_data->showNext != SEGMENT.check1 || tetrisai_data->showBorder != SEGMENT.check2 - ) ) { tetrisai_data->showNext = SEGMENT.check1; tetrisai_data->showBorder = SEGMENT.check2; + tetris.nLookAhead = nLookAhead; //not more than 32 as this is the limit of this implementation uint8_t gridWidth = cols < 32 ? cols : 32; @@ -142,7 +144,7 @@ uint16_t mode_2DTetrisAI() } } - tetrisai_data->tetris = TetrisAIGame(gridWidth, gridHeight, nLookAhead, piecesData, numPieces); + tetris = TetrisAIGame(gridWidth, gridHeight, nLookAhead, piecesData, numPieces); SEGMENT.fill(SEGCOLOR(1)); } @@ -151,48 +153,48 @@ uint16_t mode_2DTetrisAI() tetrisai_data->intelligence = SEGMENT.custom1; double dui = 0.2 - (0.2 * (tetrisai_data->intelligence / 255.0)); - tetrisai_data->tetris.ai.aHeight = -0.510066 + dui; - tetrisai_data->tetris.ai.fullLines = 0.760666 - dui; - tetrisai_data->tetris.ai.holes = -0.35663 + dui; - tetrisai_data->tetris.ai.bumpiness = -0.184483 + dui; + tetris.ai.aHeight = -0.510066 + dui; + tetris.ai.fullLines = 0.760666 - dui; + tetris.ai.holes = -0.35663 + dui; + tetris.ai.bumpiness = -0.184483 + dui; } - if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_MOVE) + if (tetris.state == TetrisAIGame::ANIMATE_MOVE) { - if (millis() - tetrisai_data->lastTime > msDelayMove) + if (millis() - lastTime > msDelayMove) { - drawGrid(&tetrisai_data->tetris, tetrisai_data); - tetrisai_data->lastTime = millis(); - tetrisai_data->tetris.poll(); + drawGrid(&tetris, tetrisai_data); + lastTime = millis(); + tetris.poll(); } } - else if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_GAME_OVER) + else if (tetris.state == TetrisAIGame::ANIMATE_GAME_OVER) { - if (millis() - tetrisai_data->lastTime > msDelayGameOver) + if (millis() - lastTime > msDelayGameOver) { - drawGrid(&tetrisai_data->tetris, tetrisai_data); - tetrisai_data->lastTime = millis(); - tetrisai_data->tetris.poll(); + drawGrid(&tetris, tetrisai_data); + lastTime = millis(); + tetris.poll(); } } - else if (tetrisai_data->tetris.state == TetrisAIGame::FIND_BEST_MOVE) + else if (tetris.state == TetrisAIGame::FIND_BEST_MOVE) { if (SEGMENT.check3) { if(tetrisai_data->mistaceCountdown == 0) { - tetrisai_data->tetris.ai.findWorstMove = true; - tetrisai_data->tetris.poll(); - tetrisai_data->tetris.ai.findWorstMove = false; + tetris.ai.findWorstMove = true; + tetris.poll(); + tetris.ai.findWorstMove = false; tetrisai_data->mistaceCountdown = SEGMENT.custom3; } tetrisai_data->mistaceCountdown--; } - tetrisai_data->tetris.poll(); + tetris.poll(); } else { - tetrisai_data->tetris.poll(); + tetris.poll(); } return FRAMETIME; From a418cd2a2a5afd4bedbafca15239d510797c46fa Mon Sep 17 00:00:00 2001 From: Woody Date: Sat, 13 Apr 2024 19:37:49 +0200 Subject: [PATCH 018/162] 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/162] 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/162] 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 7abfe68458daf62477621a9d9487470233045e47 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Mon, 15 Apr 2024 16:13:13 +0200 Subject: [PATCH 021/162] 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 b2e68db380326804d545b7bc71233edf8c041b35 Mon Sep 17 00:00:00 2001 From: Woody Date: Mon, 15 Apr 2024 22:55:38 +0200 Subject: [PATCH 022/162] 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 6272969983c35e824622cf7d7515165eeff2e02f Mon Sep 17 00:00:00 2001 From: Woody Date: Tue, 16 Apr 2024 15:07:12 +0200 Subject: [PATCH 023/162] 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 6d1410741d4c3f2d080b9ec12d7ffa7e0a2e4d8c Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Wed, 17 Apr 2024 19:00:16 +0200 Subject: [PATCH 024/162] 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 6276c2f1f5db493e2db47b85dfbeda018a1faa22 Mon Sep 17 00:00:00 2001 From: gaaat Date: Fri, 26 Apr 2024 16:39:32 +0200 Subject: [PATCH 025/162] 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 026/162] 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 25dd43b949176a99c19d43ca1bc7b53474754708 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Fri, 26 Apr 2024 23:49:34 +0200 Subject: [PATCH 027/162] 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 028/162] 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 029/162] 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 030/162] 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 031/162] 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 032/162] 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 033/162] 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 034/162] 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 035/162] 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 036/162] 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 037/162] 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 038/162] 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 039/162] 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 040/162] 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 041/162] 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 042/162] 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 043/162] 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 044/162] 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 045/162] 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 046/162] 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 047/162] 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 048/162] 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 049/162] 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 050/162] 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 051/162] 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 052/162] 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 053/162] 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 054/162] 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 055/162] 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 056/162] 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 057/162] 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 058/162] 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 059/162] 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 060/162] 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 061/162] 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 062/162] 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 063/162] 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 064/162] 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 065/162] 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 066/162] 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 067/162] 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 068/162] 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 069/162] 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 070/162] 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 071/162] 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 072/162] 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 073/162] 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 074/162] 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 075/162] 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 076/162] 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 077/162] 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 078/162] 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 079/162] 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 080/162] 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 081/162] 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 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 z 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 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)). + + + + + + + + +
+ +

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 082/162] (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 083/162] 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 084/162] 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