Compare commits

..

35 Commits

Author SHA1 Message Date
coderabbitai[bot] fc1a14b585 📝 Add docstrings to feature/gc9a01-display-usermod
Docstrings generation was requested by @srg74.

* https://github.com/wled/WLED/pull/4989#issuecomment-3763889073

The following files were modified:

* `usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp`
* `usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h`
* `usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp`
2026-01-17 14:16:01 +00:00
Frank Möhle a024935c39 adding a screenshot for simple PR creation
Added a tip for creating pull requests and forking in one click.
2026-01-17 01:53:32 +01:00
Frank Möhle 10df03e564 reorder sections for clarity 2026-01-17 01:39:06 +01:00
Frank Möhle 7a9e7f9c41 Enhance contributing guidelines for pull requests
Added guidelines for creating pull requests from a fork.
2026-01-17 01:37:17 +01:00
Frank Möhle 45acb44a36 Fix typo in CONTRIBUTING.md 2026-01-17 00:53:31 +01:00
Frank Möhle 8a3cb46007 Update contributing guidelines for PR management
Clarify that multiple commits can be added to an open PR and warn against force-pushing.
2026-01-17 00:52:51 +01:00
Frank ba5cf9cd3c night build script updates
Fixes some deprecation warnings during nightly build runs. Already tested in WLED-MM.
* upgrade action-github-changelog-generator to 2.4
* decode changelog into a temporary file (needed for changelog-generator 2.4)
* renamed exclude-labels (deprecated); added some more tags for filtering
2026-01-16 21:47:40 +01:00
Copilot d1d9dec402 Fix gamma correction for color not enabled on fresh install (#5225)
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2026-01-12 07:55:05 +01:00
Damian Schneider 6e9dc181e1 deepsleep cleanup, use toki to check for valid time 2026-01-10 12:36:07 +01:00
elanworld fe3a158264 Improvements to deepsleep UM (#4456)
- add touch pin option
- add wake-up on timer macro
- fix powerup behaviour: now works as intended i.e. if no sleep delay set, it enters deepsleep on powerup but only then
- apply macro after wake up even if the window is missed
- respect date-range setting of macro
- removed non-existing pin from touch list
- removed non existing voltageCheck setting
- fixed incorrect threshold setting
- code cleanup
- fix pullup/pulldown for deep-sleep GPIOs

pin init is now working correctly, tested on all platforms

* remove unused statement

---------

Co-authored-by: Damian Schneider <daedae@gmx.ch>
2026-01-10 11:28:07 +01:00
Will Tatam e2de1af6f4 Merge pull request #5262 from brettbear/brettbear/ethernet-static-ip-ignored
Fixes ethernet initialisation of static IP settings and modified some debug info
2026-01-10 10:22:44 +00:00
Will Tatam 22ab62d090 Merge pull request #5273 from nomis52/awst
Add support for Australian Western Time
2026-01-07 18:21:47 +00:00
AlexeyMal 254e0099ca Random colors via JSON API in Segment object like "col":["r","r","r"] #4996 (#5000)
Add support for random colors via JSON API in Segment object like col=["r","r","r"] #4996
2026-01-06 22:47:04 -05:00
Frank 8433fd24c3 Add softhack007 to GitHub funding list
adding myself
2026-01-05 23:20:57 +01:00
Frank 32daa03941 Merge pull request #5276 from wled/fix_5275_part1
prevent file data loss due to replacing an open file handle (partial fix for #5275) - provides better protection again presets.json corruption.
2026-01-05 22:23:21 +01:00
Will Tatam 4749247389 Update funding links in FUNDING.yml
Restoring PayPal link for funding. Parity with past contributors
2026-01-05 20:01:36 +00:00
Frank a870474b49 Removed redundant check before closing the file handle 2026-01-05 19:26:39 +01:00
Frank 97493b1af8 Merge pull request #5229 from Merikei/fix-bug-issue_template
bug.yml: Update help text on how to find version description in WLED
2026-01-05 15:18:48 +01:00
Frank 8e27fe4c0c bufgix: prevent file data loss due to replacing an open file pointer
partial fix for #5275
2026-01-05 13:21:37 +01:00
Simon Newton 407c9fd72f Add support for Australian Western Time 2026-01-05 09:13:49 +08:00
Damian Schneider e95450b318 replace cos8 with cos8_t correcting an oversight 2026-01-03 15:45:44 +01:00
Copilot b556da8b36 Bugfix: GPIO0 always gets assigned to a button (#5259)
if button pin arg is missing, it defaults to 0 assigning a button to that pin. This change fixes that.

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2026-01-03 12:19:56 +01:00
Damian Schneider 5cfb6f984b Palettes fix (#5263)
* Fix for #5201
- use constants instead of magic numbers

Authored-by: Blaž Kristan <blaz@kristan-sp.si>
2026-01-03 10:07:04 +01:00
Damian Schneider 60e1698ed2 Improvements & fixes for HUB75 (#5026)
* Improvements & fixes for HUB75

- added proper config parameters to allow multiple panels
- added config checks
- fixed crashes on S3
- changed constant variables to constexpr
- added "wled.h" to bus_manager.cpp and removed local function prototypes (needed for buffer allocations)
- speed optimisations: yields about 10% higher FPS
- updated platformio_override.sample.ini
- some code cleanup
2026-01-03 09:56:10 +01:00
brettbear 60b2c3bb54 Fixes ethernet initialisation of static IP settings. Also corrected some debug messages. 2026-01-02 18:25:46 +11:00
Will Tatam 979e3fd1f7 Update GitHub sponsors list in FUNDING.yml
Maintain PayPal link, but sponsor is not appropriate for someone who isn't actively contributing going forward
2026-01-01 11:33:26 +00:00
Will Tatam 600b9b4d8e Update funding sources in FUNDING.yml
Removed a contributor from the GitHub funding list and updated the custom funding sources.
2026-01-01 11:29:50 +00:00
Damian Schneider 787d8a7342 fix FX checkmark sync (#5239)
this fixes an ancient copy-paste bug that apparently went under the radar for years. Now FX checkmarks sync correctly.
2025-12-29 14:54:38 +01:00
Damian Schneider 46e77ea203 revert change to extractModeName, add comment for clarification 2025-12-29 14:50:58 +01:00
Damian Schneider fa868568af minor bugfixes as suggested by the rabbit
- PulLightControl UM: dont release a lock you do not own
- off-by-one error in extractModeSlider (used only in rotary in UM)
- safety check in playlist in case something goes horribly wrong
2025-12-29 12:56:06 +01:00
Damian Schneider 1c2cacf185 adding link to WLED VidoLab 2025-12-27 19:27:35 +01:00
Damian Schneider f1f067e93a bugfix in particle collision binning 2025-12-27 09:37:09 +01:00
nename0 8a2a7054ab Usermod Temperature: use full 12-bit precision (#4916)
* Usermod Temperature: use full 12-bit precision

* Temperature Usermod: fix DS1822 and DS18B20 family code

* Add Dropdown to select 9 or 12 bit resolution

* Add 10 and 11 bit resolution, Correct rounding towards negativ inf
2025-12-26 15:32:24 -05:00
Merikei e4b0ee0672 Update version description in bug.yml
Fixes #5228
2025-12-24 23:00:37 +00:00
Damian Schneider b821e20fd6 use constant instead of magic number in pixelforge 2025-12-22 20:06:20 +01:00
36 changed files with 2398 additions and 486 deletions
+1 -2
View File
@@ -1,3 +1,2 @@
github: [Aircoookie,blazoncek,DedeHai,lost-hope,willmmiles]
github: [DedeHai,lost-hope,willmmiles,netmindz,softhack007]
custom: ['https://paypal.me/Aircoookie','https://paypal.me/blazoncek']
thanks_dev: u/gh/netmindz
+4 -1
View File
@@ -44,7 +44,10 @@ body:
id: version
attributes:
label: What version of WLED?
description: You can find this in by going to Config -> Security & Updates -> Scroll to Bottom. Copy and paste the entire line after "Server message"
description: |-
Find this by going to <kbd><samp>⚙️ Config</samp></kbd> → <kbd><samp>Security & Updates</samp></kbd> → Scroll to Bottom.
Copy and paste the rest of the line that begins “<samp>Installed version: </samp>”,
or, for older versions, the entire line after “<samp>Server message</samp>”.
placeholder: "e.g. WLED 0.13.1 (build 2203150)"
validations:
required: true
+4 -3
View File
@@ -23,12 +23,13 @@ jobs:
run: ls -la
- name: "✏️ Generate release changelog"
id: changelog
uses: janheinrichmerker/action-github-changelog-generator@v2.3
uses: janheinrichmerker/action-github-changelog-generator@v2.4
with:
token: ${{ secrets.GITHUB_TOKEN }}
sinceTag: v0.15.0
output: CHANGELOG_NIGHTLY.md
# Exclude issues that were closed without resolution from changelog
exclude-labels: 'stale,wontfix,duplicate,invalid'
excludeLabels: 'stale,wontfix,duplicate,invalid,question,use-as-is,not_planned'
- name: Update Nightly Release
uses: andelf/nightly-release@main
env:
@@ -37,7 +38,7 @@ jobs:
tag_name: nightly
name: 'Nightly Release $$'
prerelease: true
body: ${{ steps.changelog.outputs.changelog }}
body_path: CHANGELOG_NIGHTLY.md
files: |
*.bin
*.bin.gz
+2 -2
View File
@@ -20,13 +20,13 @@ jobs:
merge-multiple: true
- name: "✏️ Generate release changelog"
id: changelog
uses: janheinrichmerker/action-github-changelog-generator@v2.3
uses: janheinrichmerker/action-github-changelog-generator@v2.4
with:
token: ${{ secrets.GITHUB_TOKEN }}
sinceTag: v0.15.0
maxIssues: 500
# Exclude issues that were closed without resolution from changelog
exclude-labels: 'stale,wontfix,duplicate,invalid'
excludeLabels: 'stale,wontfix,duplicate,invalid,question,use-as-is,not_planned'
- name: Create draft release
uses: softprops/action-gh-release@v1
with:
+15 -3
View File
@@ -2,6 +2,19 @@
Here are a few suggestions to make it easier for you to contribute!
### PR from a branch in your own fork
Start your pull request (PR) in a branch of your own fork. Don't make a PR directly from your main branch.
This lets you update your PR if needed, while you can work on other tasks in 'main' or in other branches.
<img width="295" height="134" alt="image: fork and edit" src="https://github.com/user-attachments/assets/f0dc7567-edcb-4409-a530-cd621ae9661f" />
Tip: When viewing a file in `wled/WLED`, click on the "pen" icon and start making changes.
When you chose to 'Commit changes', GitHub will automatically create a PR from your fork.
### Target branch for pull requests
Please make all PRs against the `main` branch.
### Describe your PR
Please add a description of your proposed code changes. It does not need to be an exhaustive essay, however a PR with no description or just a few words might not get accepted, simply because very basic information is missing.
@@ -12,15 +25,14 @@ A good description helps us to review and understand your proposed changes. For
* testing you performed, known limitations, open ends you possibly could not solve.
* any areas where you like to get help from an experienced maintainer (yes WLED has become big 😉)
### Target branch for pull requests
Please make all PRs against the `main` branch.
### Updating your code
While the PR is open - and under review by maintainers - you may be asked to modify your PR source code.
You can simply update your own branch, and push changes in response to reviewer recommendations.
Github will pick up the changes so your PR stays up-to-date.
You don't need to provide us with a single-commit 'squashed' PR; you can simply add commits while your pull request (PR) is open.
> [!CAUTION]
> Do not use "force-push" while your PR is open!
> It has many subtle and unexpected consequences on our github reposistory.
+3 -2
View File
@@ -545,6 +545,7 @@ lib_deps = ${env:esp32dev.lib_deps}
# ------------------------------------------------------------------------------
# Hub75 examples
# ------------------------------------------------------------------------------
# Note: some panels may experience ghosting with default full brightness. use -D WLED_HUB75_MAX_BRIGHTNESS=239 or lower to fix it.
[env:esp32dev_hub75]
board = esp32dev
@@ -591,7 +592,7 @@ build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8M_qspi\"
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
-DBOARD_HAS_PSRAM
-DLOLIN_WIFI_FIX ; seems to work much better with this
-DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power)
-D WLED_WATCHDOG_TIMEOUT=0
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
-D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips
@@ -622,7 +623,7 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=
${Speed_Flags.build_flags} ;; optional: -O2 -> optimize for speed instead of size
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
-DBOARD_HAS_PSRAM
-DLOLIN_WIFI_FIX ; seems to work much better with this
-DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power)
-D WLED_WATCHDOG_TIMEOUT=0
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX
-D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips
+22 -9
View File
@@ -20,17 +20,19 @@ float UsermodTemperature::readDallas() {
}
#endif
switch(sensorFound) {
case 0x10: // DS18S20 has 9-bit precision
case 0x10: // DS18S20 has 9-bit precision - 1-bit fraction part
result = (data[1] << 8) | data[0];
retVal = float(result) * 0.5f;
break;
case 0x22: // DS18B20
case 0x28: // DS1822
case 0x22: // DS1822
case 0x28: // DS18B20
case 0x3B: // DS1825
case 0x42: // DS28EA00
result = (data[1]<<4) | (data[0]>>4); // we only need whole part, we will add fraction when returning
if (data[1] & 0x80) result |= 0xF000; // fix negative value
retVal = float(result) + ((data[0] & 0x08) ? 0.5f : 0.0f);
// 12-bit precision - 4-bit fraction part
result = (data[1] << 8) | data[0];
// Clear LSBs to match desired precision (9/10/11-bit) rounding towards negative infinity
result &= 0xFFFF << (3 - (resolution & 3));
retVal = float(result) * 0.0625f; // 2^(-4)
break;
}
}
@@ -69,8 +71,8 @@ bool UsermodTemperature::findSensor() {
if (oneWire->crc8(deviceAddress, 7) == deviceAddress[7]) {
switch (deviceAddress[0]) {
case 0x10: // DS18S20
case 0x22: // DS18B20
case 0x28: // DS1822
case 0x22: // DS1822
case 0x28: // DS18B20
case 0x3B: // DS1825
case 0x42: // DS28EA00
DEBUG_PRINTLN(F("Sensor found."));
@@ -277,6 +279,7 @@ void UsermodTemperature::addToConfig(JsonObject &root) {
top[FPSTR(_parasite)] = parasite;
top[FPSTR(_parasitePin)] = parasitePin;
top[FPSTR(_domoticzIDX)] = idx;
top[FPSTR(_resolution)] = resolution;
DEBUG_PRINTLN(F("Temperature config saved."));
}
@@ -304,6 +307,7 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) {
parasite = top[FPSTR(_parasite)] | parasite;
parasitePin = top[FPSTR(_parasitePin)] | parasitePin;
idx = top[FPSTR(_domoticzIDX)] | idx;
resolution = top[FPSTR(_resolution)] | resolution;
if (!initDone) {
// first run: reading from cfg.json
@@ -324,7 +328,7 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) {
}
}
// use "return !top["newestParameter"].isNull();" when updating Usermod with new features
return !top[FPSTR(_domoticzIDX)].isNull();
return !top[FPSTR(_resolution)].isNull();
}
void UsermodTemperature::appendConfigData() {
@@ -332,6 +336,14 @@ void UsermodTemperature::appendConfigData() {
oappend(F("',1,'<i>(if no Vcc connected)</i>');")); // 0 is field type, 1 is actual field
oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_parasitePin)).c_str());
oappend(F("',1,'<i>(for external MOSFET)</i>');")); // 0 is field type, 1 is actual field
oappend(F("dd=addDD('")); oappend(String(FPSTR(_name)).c_str());
oappend(F("','")); oappend(String(FPSTR(_resolution)).c_str()); oappend(F("');"));
oappend(F("addO(dd,'0.5 °C (9-bit)',0);"));
oappend(F("addO(dd,'0.25°C (10-bit)',1);"));
oappend(F("addO(dd,'0.125°C (11-bit)',2);"));
oappend(F("addO(dd,'0.0625°C (12-bit)',3);"));
oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_resolution)).c_str());
oappend(F("',1,'<i>(ignored on DS18S20)</i>');")); // 0 is field type, 1 is actual field
}
float UsermodTemperature::getTemperature() {
@@ -351,6 +363,7 @@ const char UsermodTemperature::_readInterval[] PROGMEM = "read-interval-s";
const char UsermodTemperature::_parasite[] PROGMEM = "parasite-pwr";
const char UsermodTemperature::_parasitePin[] PROGMEM = "parasite-pwr-pin";
const char UsermodTemperature::_domoticzIDX[] PROGMEM = "domoticz-idx";
const char UsermodTemperature::_resolution[] PROGMEM = "resolution";
const char UsermodTemperature::_sensor[] PROGMEM = "sensor";
const char UsermodTemperature::_temperature[] PROGMEM = "temperature";
const char UsermodTemperature::_Temperature[] PROGMEM = "/temperature";
@@ -48,6 +48,7 @@ class UsermodTemperature : public Usermod {
bool HApublished = false;
int16_t idx = -1; // Domoticz virtual sensor idx
uint8_t resolution = 0; // 9bits=0, 10bits=1, 11bits=2, 12bits=3
// strings to reduce flash memory usage (used more than twice)
static const char _name[];
@@ -56,6 +57,7 @@ class UsermodTemperature : public Usermod {
static const char _parasite[];
static const char _parasitePin[];
static const char _domoticzIDX[];
static const char _resolution[];
static const char _sensor[];
static const char _temperature[];
static const char _Temperature[];
+168 -50
View File
@@ -1,6 +1,8 @@
#include "wled.h"
#include "driver/rtc_io.h"
#ifndef CONFIG_IDF_TARGET_ESP32C3
#include "soc/touch_sensor_periph.h"
#endif
#ifdef ESP8266
#error The "Deep Sleep" usermod does not support ESP8266
#endif
@@ -21,27 +23,34 @@
#define DEEPSLEEP_DELAY 1
#endif
RTC_DATA_ATTR bool powerup = true; // variable in RTC data persists on a reboot
#ifndef DEEPSLEEP_WAKEUP_TOUCH_PIN
#define DEEPSLEEP_WAKEUP_TOUCH_PIN 1
#endif
RTC_DATA_ATTR bool powerup = true; // this is first boot after power cycle. note: variable in RTC data persists on a reboot
RTC_DATA_ATTR uint8_t wakeupPreset = 0; // preset to apply after deep sleep wakeup (0 = none), set to timer macro preset
class DeepSleepUsermod : public Usermod {
private:
bool enabled = true;
bool enabled = false; // do not enable by default
bool initDone = false;
uint8_t wakeupPin = DEEPSLEEP_WAKEUPPIN;
uint8_t wakeWhenHigh = DEEPSLEEP_WAKEWHENHIGH; // wake up when pin goes high if 1, triggers on low if 0
bool noPull = true; // use pullup/pulldown resistor
bool enableTouchWakeup = false;
uint8_t touchPin = DEEPSLEEP_WAKEUP_TOUCH_PIN;
int wakeupAfter = DEEPSLEEP_WAKEUPINTERVAL; // in seconds, <=0: button only
bool presetWake = true; // wakeup timer for preset
int sleepDelay = DEEPSLEEP_DELAY; // in seconds, 0 = immediate
int delaycounter = 5; // delay deep sleep at bootup until preset settings are applied
int delaycounter = 10; // delay deep sleep at bootup until preset settings are applied, force wake up if offmode persists after bootup
uint32_t lastLoopTime = 0;
// string that are used multiple time (this will save some flash memory)
static const char _name[];
static const char _enabled[];
bool pin_is_valid(uint8_t wakePin) {
#ifdef CONFIG_IDF_TARGET_ESP32 //ESP32: GPIOs 0,2,4, 12-15, 25-39 can be used for wake-up
#ifdef CONFIG_IDF_TARGET_ESP32 //ESP32: GPIOs 0,2,4, 12-15, 25-39 can be used for wake-up. note: input-only GPIOs 34-39 do not have internal pull resistors
if (wakePin == 0 || wakePin == 2 || wakePin == 4 || (wakePin >= 12 && wakePin <= 15) || (wakePin >= 25 && wakePin <= 27) || (wakePin >= 32 && wakePin <= 39)) {
return true;
}
@@ -60,8 +69,56 @@ class DeepSleepUsermod : public Usermod {
return false;
}
public:
// functions to calculate time difference between now and next scheduled timer event
int calculateTimeDifference(int hour1, int minute1, int hour2, int minute2) {
int totalMinutes1 = hour1 * 60 + minute1;
int totalMinutes2 = hour2 * 60 + minute2;
if (totalMinutes2 < totalMinutes1) {
totalMinutes2 += 24 * 60;
}
return totalMinutes2 - totalMinutes1;
}
int findNextTimerInterval() {
if (toki.getTimeSource() == TOKI_TS_NONE) {
DEBUG_PRINTLN("DeepSleep: local time not yet synchronized, skipping timer check.");
return -1;
}
int currentHour = hour(localTime);
int currentMinute = minute(localTime);
int currentWeekday = weekdayMondayFirst(); // 1=Monday ... 7=Sunday
int minDifference = INT_MAX;
for (uint8_t i = 0; i < 8; i++) {
// check if timer is enabled and date is in range, also wakes up if no macro is used
if ((timerWeekday[i] & 0x01) && isTodayInDateRange(((timerMonth[i] >> 4) & 0x0F), timerDay[i], timerMonth[i] & 0x0F, timerDayEnd[i])) {
// if timer is enabled (bit0 of timerWeekday) and date is in range, check all weekdays it is set for
for (int dayOffset = 0; dayOffset < 7; dayOffset++) {
int checkWeekday = ((currentWeekday + dayOffset) % 7); // 1-7, check all weekdays starting from today
if (checkWeekday == 0) {
checkWeekday = 7; // sunday is 7 not 0
}
int targetHour = timerHours[i];
int targetMinute = timerMinutes[i];
if ((timerWeekday[i] >> (checkWeekday)) & 0x01) {
if (dayOffset == 0 && (targetHour < currentHour || (targetHour == currentHour && targetMinute <= currentMinute)))
continue; // skip if time has already passed today
int timeDifference = calculateTimeDifference(currentHour, currentMinute, targetHour + (dayOffset * 24), targetMinute);
if (timeDifference < minDifference) {
minDifference = timeDifference;
wakeupPreset = timerMacro[i];
}
}
}
}
}
return minDifference;
}
public:
inline void enable(bool enable) { enabled = enable; } // Enable/Disable the usermod
inline bool isEnabled() { return enabled; } //Get usermod enabled/disabled state
@@ -69,26 +126,40 @@ class DeepSleepUsermod : public Usermod {
void setup() {
//TODO: if the de-init of RTC pins is required to do it could be done here
//rtc_gpio_deinit(wakeupPin);
#ifdef WLED_DEBUG
DEBUG_PRINTF("sleep wakeup cause: %d\n", esp_sleep_get_wakeup_cause());
#endif
if (esp_sleep_get_wakeup_cause() != ESP_SLEEP_WAKEUP_TIMER)
wakeupPreset = 0; // not a timed wakeup, don't apply preset
initDone = true;
}
void loop() {
if (!enabled || !offMode) { // disabled or LEDs are on
if (!enabled) return;
if (!offMode) { // LEDs are on
lastLoopTime = 0; // reset timer
if (delaycounter)
delaycounter--; // decrease delay counter if LEDs are on (they are always turned on after a wake-up, see below)
else if (wakeupPreset)
applyPreset(wakeupPreset); // apply preset if set, this ensures macro is applied even if we missed the wake-up time
return;
}
if (sleepDelay > 0) {
if(lastLoopTime == 0) lastLoopTime = millis(); // initialize
if (millis() - lastLoopTime < sleepDelay * 1000) {
return; // wait until delay is over
}
powerup = false; // disable "safety" powerup sleep if delay is set
if (lastLoopTime == 0)
lastLoopTime = millis(); // initialize
if (millis() - lastLoopTime < sleepDelay * 1000)
return; // wait until delay is over
} else if (powerup && delaycounter) {
delaycounter--; // on first boot without sleepDelay set, do not force-turn on
delay(1000); // just in case: give user a short ~10s window to turn LEDs on in UI (delaycounter is 10 by default)
return;
}
if(powerup == false && delaycounter) { // delay sleep in case a preset is being loaded and turnOnAtBoot is disabled (handleIO() does enable offMode temporarily in this case)
if (powerup == false && delaycounter) { // delay sleep in case a preset is being loaded and turnOnAtBoot is disabled (handleIO() does enable offMode temporarily in this case)
delaycounter--;
if(delaycounter == 2 && offMode) { // force turn on, no matter the settings (device is bricked if user set sleepDelay=0, no bootup preset and turnOnAtBoot=false)
if (briS == 0) bri = 10; // turn on at low brightness
if (delaycounter == 1 && offMode) { // force turn on, no matter the settings (device is bricked if user set sleepDelay=0, no bootup preset and turnOnAtBoot=false)
if (briS == 0) bri = 10; // turn on and set low brightness to avoid automatic turn off
else bri = briS;
strip.setBrightness(bri); // needed to make handleIO() not turn off LEDs (really? does not help in bootup preset)
offMode = false;
@@ -99,57 +170,81 @@ class DeepSleepUsermod : public Usermod {
}
return;
}
DEBUG_PRINTLN(F("DeepSleep UM: entering deep sleep..."));
powerup = false; // turn leds on in all subsequent bootups (overrides Turn LEDs on after power up/reset' at reboot)
if(!pin_is_valid(wakeupPin)) return;
if (!pin_is_valid(wakeupPin)) return;
esp_err_t halerror = ESP_OK;
pinMode(wakeupPin, INPUT); // make sure GPIO is input with pullup/pulldown disabled
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); //disable all wake-up sources (just in case)
if(wakeupAfter)
esp_sleep_enable_timer_wakeup((uint64_t)wakeupAfter * (uint64_t)1e6); //sleep for x seconds
uint32_t wakeupAfterSec = 0;
if (presetWake) {
int nextInterval = findNextTimerInterval();
if (nextInterval > 1 && nextInterval < INT_MAX)
wakeupAfterSec = (nextInterval - 1) * 60; // wakeup before next preset
}
if (wakeupAfter > 0) { // user-defined interval
if (wakeupAfterSec == 0 || (uint32_t)wakeupAfter < wakeupAfterSec) {
wakeupAfterSec = wakeupAfter;
}
}
if (wakeupAfterSec > 0) {
esp_sleep_enable_timer_wakeup(wakeupAfterSec * (uint64_t)1e6);
DEBUG_PRINTF("wakeup after %d seconds\n", wakeupAfterSec);
}
#if defined(CONFIG_IDF_TARGET_ESP32C3) // ESP32 C3
if(noPull)
gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_FLOATING);
else { // enable pullup/pulldown resistor
if(wakeWhenHigh)
gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_PULLDOWN_ONLY);
gpio_hold_dis((gpio_num_t)wakeupPin); // disable hold and configure pin
if (wakeWhenHigh)
halerror = esp_deep_sleep_enable_gpio_wakeup(1<<wakeupPin, ESP_GPIO_WAKEUP_GPIO_HIGH);
else
gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_PULLUP_ONLY);
}
if(wakeWhenHigh)
halerror = esp_deep_sleep_enable_gpio_wakeup(1<<wakeupPin, ESP_GPIO_WAKEUP_GPIO_HIGH);
else
halerror = esp_deep_sleep_enable_gpio_wakeup(1<<wakeupPin, ESP_GPIO_WAKEUP_GPIO_LOW);
halerror = esp_deep_sleep_enable_gpio_wakeup(1<<wakeupPin, ESP_GPIO_WAKEUP_GPIO_LOW);
// note: on C3 calling esp_deep_sleep_enable_gpio_wakeup() automatically enables pullup/pulldown unless we call gpio_hold_en() which overrides that
gpio_pullup_dis((gpio_num_t)wakeupPin); // disable pull resistors by default
gpio_pulldown_dis((gpio_num_t)wakeupPin);
if (!noPull) {
if (wakeWhenHigh) {
gpio_pulldown_en((gpio_num_t)wakeupPin);
} else {
gpio_pullup_en((gpio_num_t)wakeupPin);
}
}
gpio_hold_en((gpio_num_t)wakeupPin); // hold the configured GPIO state during deep sleep, overrides the automatic pullup/pulldown, see note above
#else // ESP32, S2, S3
gpio_pulldown_dis((gpio_num_t)wakeupPin); // disable internal pull resistors for GPIO use
gpio_pullup_dis((gpio_num_t)wakeupPin);
if(noPull) {
rtc_gpio_pullup_dis((gpio_num_t)wakeupPin);
rtc_gpio_hold_dis((gpio_num_t)wakeupPin); // disable hold so we can (re)configure pin
rtc_gpio_init((gpio_num_t)wakeupPin); // hand the pin over to RTC module
rtc_gpio_set_direction((gpio_num_t)wakeupPin, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pullup_dis((gpio_num_t)wakeupPin); // disable pull resistors by default
rtc_gpio_pulldown_dis((gpio_num_t)wakeupPin);
}
else { // enable pullup/pulldown resistor for RTC use
if(wakeWhenHigh)
rtc_gpio_pulldown_en((gpio_num_t)wakeupPin);
if (!noPull) {
if (wakeWhenHigh)
rtc_gpio_pulldown_en((gpio_num_t)wakeupPin);
else
rtc_gpio_pullup_en((gpio_num_t)wakeupPin);
}
if (wakeWhenHigh)
halerror = esp_sleep_enable_ext1_wakeup(1ULL << wakeupPin, ESP_EXT1_WAKEUP_ANY_HIGH); // use ext1 as ext0 does not work with touch wakeup
else
rtc_gpio_pullup_en((gpio_num_t)wakeupPin);
}
if(wakeWhenHigh)
halerror = esp_sleep_enable_ext0_wakeup((gpio_num_t)wakeupPin, HIGH); // only RTC pins can be used
else
halerror = esp_sleep_enable_ext0_wakeup((gpio_num_t)wakeupPin, LOW);
#endif
halerror = esp_sleep_enable_ext1_wakeup(1ULL << wakeupPin, ESP_EXT1_WAKEUP_ALL_LOW);
delay(1); // wait for pin to be ready
if(halerror == ESP_OK) esp_deep_sleep_start(); // go into deep sleep
if (enableTouchWakeup) {
#ifdef SOC_TOUCH_VERSION_2 // S2 and S3 use much higher thresholds, see notes in pin_manager
touchSleepWakeUpEnable(touchPin, touchThreshold << 4); // ESP32 S2 & S3: lower threshold = more sensitive
#else
touchSleepWakeUpEnable(touchPin, touchThreshold); // ESP32: use normal threshold (higher = more sensitive)
#endif
}
delay(1); // wait for pins to be ready
rtc_gpio_hold_en((gpio_num_t)wakeupPin); // latch and hold the configured GPIO state during deep sleep
#endif
WiFi.mode(WIFI_OFF); // Completely shut down the Wi-Fi module
if (halerror == ESP_OK) esp_deep_sleep_start(); // go into deep sleep
else DEBUG_PRINTLN(F("sleep failed"));
}
//void connected() {} //unused, this is called every time the WiFi is (re)connected
void addToConfig(JsonObject& root) override
void addToConfig(JsonObject& root) override
{
JsonObject top = root.createNestedObject(FPSTR(_name));
top[FPSTR(_enabled)] = enabled;
@@ -157,6 +252,11 @@ void addToConfig(JsonObject& root) override
top["gpio"] = wakeupPin;
top["wakeWhen"] = wakeWhenHigh;
top["pull"] = noPull;
#ifndef CONFIG_IDF_TARGET_ESP32C3
top["enableTouchWakeup"] = enableTouchWakeup;
top["touchPin"] = touchPin;
#endif
top["presetWake"] = presetWake;
top["wakeAfter"] = wakeupAfter;
top["delaySleep"] = sleepDelay;
}
@@ -176,6 +276,11 @@ void addToConfig(JsonObject& root) override
}
configComplete &= getJsonValue(top["wakeWhen"], wakeWhenHigh, DEEPSLEEP_WAKEWHENHIGH); // default to wake on low
configComplete &= getJsonValue(top["pull"], noPull, DEEPSLEEP_DISABLEPULL); // default to no pullup/pulldown
#ifndef CONFIG_IDF_TARGET_ESP32C3
configComplete &= getJsonValue(top["enableTouchWakeup"], enableTouchWakeup);
configComplete &= getJsonValue(top["touchPin"], touchPin, DEEPSLEEP_WAKEUP_TOUCH_PIN);
#endif
configComplete &= getJsonValue(top["presetWake"], presetWake);
configComplete &= getJsonValue(top["wakeAfter"], wakeupAfter, DEEPSLEEP_WAKEUPINTERVAL);
configComplete &= getJsonValue(top["delaySleep"], sleepDelay, DEEPSLEEP_DELAY);
@@ -200,6 +305,19 @@ void addToConfig(JsonObject& root) override
oappend(SET_F(");"));
}
}
#ifndef CONFIG_IDF_TARGET_ESP32C3
// dropdown for touch wakeupPin
oappend(SET_F("dd=addDropdown('DeepSleep','touchPin');"));
for (int touchchannel = 0; touchchannel < SOC_TOUCH_SENSOR_NUM; touchchannel++) {
if (touch_sensor_channel_io_map[touchchannel] >= 0) {
oappend(SET_F("addOption(dd,'"));
oappend(String(touch_sensor_channel_io_map[touchchannel]).c_str());
oappend(SET_F("',"));
oappend(String(touch_sensor_channel_io_map[touchchannel]).c_str());
oappend(SET_F(");"));
}
}
#endif
oappend(SET_F("dd=addDropdown('DeepSleep','wakeWhen');"));
oappend(SET_F("addOption(dd,'Low',0);"));
@@ -207,6 +325,7 @@ void addToConfig(JsonObject& root) override
oappend(SET_F("addInfo('DeepSleep:pull',1,'','-up/down disable: ');")); // first string is suffix, second string is prefix
oappend(SET_F("addInfo('DeepSleep:wakeAfter',1,'seconds <i>(0 = never)<i>');"));
oappend(SET_F("addInfo('DeepSleep:presetWake',1,'<i>(wake up before next preset timer)<i>');"));
oappend(SET_F("addInfo('DeepSleep:delaySleep',1,'seconds <i>(0 = sleep at powerup)<i>');")); // first string is suffix, second string is prefix
}
@@ -217,7 +336,6 @@ void addToConfig(JsonObject& root) override
uint16_t getId() {
return USERMOD_ID_DEEP_SLEEP;
}
};
// add more strings here to reduce flash memory usage
+3 -2
View File
@@ -1,7 +1,7 @@
# Deep Sleep usermod
This usermod unleashes the low power capabilities of th ESP: when you power off your LEDs (using the UI power button or a macro) the ESP will be put into deep sleep mode, reducing power consumption to a minimum.
During deep sleep the ESP is shut down completely: no WiFi, no CPU, no outputs. The only way to wake it up is to use an external signal or a button. Once it wakes from deep sleep it reboots so ***make sure to use a boot-up preset.***
During deep sleep the ESP is shut down completely: no WiFi, no CPU, no outputs. The only way to wake it up is by using an external signal, a button, or an automation timer set to the nearest preset time. Once it wakes from deep sleep it reboots so ***make sure to use a boot-up preset.***
# A word of warning
@@ -28,7 +28,7 @@ For lowest power consumption, remove the Power LED and make sure your board does
The GPIOs that can be used to wake the ESP from deep sleep are limited. Only pins connected to the internal RTC unit can be used:
- ESP32: GPIO 0, 2, 4, 12-15, 25-39
- ESP32: GPIO 0, 2, 4, 12-15, 25-39 note: input-only GPIOs 34-39 do not have internal pull up/down resistors, external resistors are required
- ESP32 S3: GPIO 0-21
- ESP32 S2: GPIO 0-21
- ESP32 C3: GPIO 0-5
@@ -61,6 +61,7 @@ To override the default settings, place the `#define` in wled.h or add `-D DEEPS
* `DEEPSLEEP_DISABLEPULL` - if defined, internal pullup/pulldown is disabled in deep sleep (default is ebnabled)
* `DEEPSLEEP_WAKEUPINTERVAL` - number of seconds after which a wake-up happens automatically, sooner if button is pressed. 0 = never. accuracy is about 2%
* `DEEPSLEEP_DELAY` - delay between power-off and sleep
* `DEEPSLEEP_WAKEUP_TOUCH_PIN` - specify GPIO pin used for touch-based wakeup
example for env build flags:
`-D USERMOD_DEEP_SLEEP`
@@ -284,7 +284,6 @@ void HttpPullLightControl::handleResponse(String& responseStr) {
if (!requestJSONBufferLock(myLockId)) {
DEBUG_PRINT(F("ERROR: Can not request JSON Buffer Lock, number: "));
DEBUG_PRINTLN(myLockId);
releaseJSONBufferLock(); // Just release in any case, maybe there was already a buffer lock
return;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,276 @@
#pragma once
#include "wled.h"
#ifdef USERMOD_GC9A01_DISPLAY
#include <TFT_eSPI.h>
#include <SPI.h>
// Pin definitions - use TFT_eSPI definitions when available
#ifndef TFT_CS
#define TFT_CS 5 // Default fallback
#endif
#define GC9A01_CS_PIN TFT_CS
#ifndef TFT_DC
#define TFT_DC 16 // Default fallback
#endif
#define GC9A01_DC_PIN TFT_DC
#ifndef TFT_RST
#define TFT_RST 17 // Default fallback
#endif
#define GC9A01_RST_PIN TFT_RST
// Use TFT_eSPI's backlight pin definition
#ifndef TFT_BL
#define TFT_BL 4 // Default fallback if not defined by TFT_eSPI
#endif
#ifndef USERMOD_ID_GC9A01_DISPLAY
#define USERMOD_ID_GC9A01_DISPLAY 59 // Use the official ID from const.h
#endif
/**
* Create the UsermodGC9A01Display singleton instance if one does not already exist.
*/
/**
* Return the singleton instance of UsermodGC9A01Display.
* @returns Pointer to the singleton instance, or nullptr if not constructed.
*/
/**
* Initialize the TFT display hardware and internal display state.
*/
/**
* Render the main interface. If overlayMode is >= 0, render the specified overlay instead of the normal UI.
* @param overlayMode Overlay mode to render, or -1 to render the normal interface.
*/
/**
* Draw the WLED logo on the display.
*/
/**
* Draw a WiFi connectivity icon at the specified position.
* @param x X coordinate in pixels.
* @param y Y coordinate in pixels.
* @param connected `true` to render as connected, `false` to render as disconnected.
* @param rssi RSSI value to reflect signal strength (optional).
*/
/**
* Set display brightness used for UI elements and indicators.
* @param bri Brightness value (0-255).
*/
/**
* Set the physical backlight level as a percentage.
* @param percent Backlight brightness percentage (0-100).
*/
/**
* Switch display between sleep behavior and clock-only behavior based on `enabled`.
* @param enabled If `true`, enable clock/sleep behavior; if `false`, disable it.
*/
/**
* Put the display into a low-power or off state.
*/
/**
* Wake the display from sleep if it is sleeping.
* @returns `true` if the display was sleeping and was woken, `false` otherwise.
*/
/**
* Render the clock-only screen.
*/
/**
* Render the current mode overlay (mode-specific information).
*/
/**
* Draw an indicator for the currently active WLED mode.
*/
/**
* Return a human-readable name for the given encoder mode.
* @param mode Encoder mode identifier.
* @returns Null-terminated string describing the mode.
*/
/**
* Wake the display if it is sleeping.
* @returns `true` if the display was sleeping and was woken, `false` otherwise.
*/
/**
* Reset or update the inactivity timer to prevent the display from timing out.
*/
/**
* Show a temporary overlay with a single line of text and optional glyph.
* @param line1 Null-terminated string to display.
* @param showHowLong Duration in milliseconds to show the overlay.
* @param glyphType Optional glyph type identifier (default 0).
*/
/**
* Trigger a display redraw.
* @param forceRedraw If `true`, redraw regardless of internal change tracking.
*/
/**
* Check whether an overlay is currently active.
* @returns `true` if an overlay is active, `false` otherwise.
*/
/**
* Get the currently active overlay mode.
* @returns Active overlay mode number, or -1 if none is active.
*/
/**
* Check whether the display is currently asleep.
* @returns `true` if the display is sleeping, `false` otherwise.
*/
/**
* Usermod setup hook called once after initialization to configure the display and state.
*/
/**
* Usermod loop hook called regularly to handle updates, timeouts, and redraw scheduling.
*/
/**
* Append informational state about the display to the provided JSON object.
* @param root JSON object to which information will be added.
*/
/**
* Read transient state values from JSON (runtime state).
* @param root JSON object containing state values.
*/
/**
* Append transient state values to JSON (runtime state).
* @param root JSON object to populate with state values.
*/
/**
* Read persistent configuration for the display from JSON.
* @param root JSON object containing configuration.
* @returns `true` if configuration was successfully read, `false` otherwise.
*/
/**
* Append persistent configuration for the display to JSON.
* @param root JSON object to populate with configuration values.
*/
/**
* Append additional configuration data to the global configuration payload.
*/
/**
* Return the unique usermod ID for this display usermod.
* @returns Numeric usermod ID.
*/
class UsermodGC9A01Display : public Usermod {
private:
// Singleton pattern - allows rotary encoder usermod to find us
static UsermodGC9A01Display* instance;
public:
UsermodGC9A01Display() { if (!instance) instance = this; }
static UsermodGC9A01Display* getInstance(void) { return instance; }
private:
TFT_eSPI tft = TFT_eSPI();
bool displayEnabled = true;
bool needsRedraw = true;
bool displayTurnedOff = false;
bool showingWelcomeScreen = true;
bool showingClock = false; // Track if currently showing clock (for rotary encoder state reset)
unsigned long welcomeScreenStartTime = 0;
uint8_t backlight = 75; // Backlight brightness percentage (0-100%, default 75%)
uint16_t displayTimeout = 60000; // 60 seconds default
bool sleepMode = true; // Enable sleep mode by default
bool clockMode = false; // Show clock only when idle
bool flip = false; // Display rotation (0 or 2)
bool clock12hour = false; // false = 24h format, true = 12h format with AM/PM
// Proper state tracking like 4-line display usermod
uint8_t knownBrightness = 255;
uint8_t knownMode = 255;
uint8_t knownPalette = 255;
uint8_t knownEffectSpeed = 255;
uint8_t knownEffectIntensity = 255;
uint32_t knownColor = 0; // colors[0] - Primary/FX
uint32_t knownBgColor = 0; // colors[1] - Secondary/BG
uint32_t knownCustomColor = 0; // colors[2] - Tertiary/CS
bool knownPowerState = true;
unsigned long nextUpdate = 0;
unsigned long lastRedraw = ULONG_MAX; // Initialize to max value - sleep timer starts after first interaction
uint16_t refreshRate = 1000; // Match 4-line display usermod (1 second) for better performance
// Time tracking for display updates
uint8_t knownMinute = 99;
uint8_t knownHour = 99;
// Integration with rotary encoder usermod (no direct pin handling)
unsigned long overlayUntil = 0; // When overlay should expire (millis)
int activeOverlayMode = -1; // Which overlay mode is active (-1 = none, 0-4 = overlay modes)
unsigned long overlayInactivityTimeout = 3000; // 3 seconds of inactivity before returning to main screen
String overlayText = "";
// Private method declarations
void initDisplay();
void drawMainInterface(int overlayMode = -1); // -1 = normal, 0+ = overlay mode
void drawWLEDLogo();
void drawWiFiIcon(int x, int y, bool connected, int rssi = 0);
void setBrightness(uint8_t bri);
void setBacklight(uint8_t percent); // Set backlight brightness (0-100%)
void sleepOrClock(bool enabled); // Sleep display or show clock based on settings
void sleepDisplay();
bool wakeDisplayFromSleep(); // Return true if display was sleeping
void drawClockScreen(); // Clock-only display mode
// Mode-specific drawing methods
void drawModeOverlay();
void drawCurrentModeIndicator();
// Get encoder state from rotary encoder usermod
const char* getEncoderModeName(uint8_t mode);
public:
// Public interface methods for rotary encoder usermod (like 4-line display)
bool wakeDisplay(); // Return true if was sleeping
void updateRedrawTime(); // Prevent display timeout
void overlay(const char* line1, long showHowLong, byte glyphType = 0); // Match 4-line interface
void redraw(bool forceRedraw); // Force display update
bool isOverlayActive(); // Check if overlay is currently showing
int getActiveOverlayMode(); // Get current overlay mode (-1 = none)
bool isDisplayAsleep(); // Check if display is sleeping
// Usermod API
// Public method declarations (Usermod interface)
void setup() override;
void loop() override;
void addToJsonInfo(JsonObject& root) override;
void readFromJsonState(JsonObject& root) override;
void addToJsonState(JsonObject& root) override;
bool readFromConfig(JsonObject& root) override;
void addToConfig(JsonObject& root) override;
void appendConfigData() override;
uint16_t getId() override;
};
#endif // USERMOD_GC9A01_DISPLAY
@@ -31,6 +31,10 @@
#include "usermod_v2_four_line_display.h"
#endif
#ifdef USERMOD_GC9A01_DISPLAY
#include "../usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h"
#endif
#ifdef USERMOD_MODE_SORT
#error "Usermod Mode Sort is no longer required. Remove -D USERMOD_MODE_SORT from platformio.ini"
#endif
@@ -178,6 +182,12 @@ class RotaryEncoderUIUsermod : public Usermod {
void* display;
#endif
#ifdef USERMOD_GC9A01_DISPLAY
UsermodGC9A01Display *gc9a01Display;
#else
void* gc9a01Display;
#endif
// Pointers the start of the mode names within JSON_mode_names
const char **modes_qstrings;
@@ -251,6 +261,16 @@ class RotaryEncoderUIUsermod : public Usermod {
public:
/**
* @brief Construct a RotaryEncoderUIUsermod with default runtime configuration.
*
* Initializes internal state and safe defaults for pins, UI state, color values,
* effect/palette indices, display pointers, preset ranges, and feature flags.
*
* The constructor does not allocate hardware resources or register interrupts;
* it only sets member variables to their default values so setup() can perform
* initialization later.
*/
RotaryEncoderUIUsermod()
: fadeAmount(5)
, buttonPressedTime(0)
@@ -265,6 +285,7 @@ class RotaryEncoderUIUsermod : public Usermod {
, currentSat1(255)
, currentCCT(128)
, display(nullptr)
, gc9a01Display(nullptr)
, modes_qstrings(nullptr)
, modes_alpha_indexes(nullptr)
, palettes_qstrings(nullptr)
@@ -401,7 +422,7 @@ void RotaryEncoderUIUsermod::sortModesAndPalettes() {
re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT);
DEBUG_PRINT(F("Sorting palettes: ")); DEBUG_PRINT(getPaletteCount()); DEBUG_PRINT('/'); DEBUG_PRINTLN(customPalettes.size());
palettes_qstrings = re_findModeStrings(JSON_palette_names, getPaletteCount() + 1); // +1 for default palette
palettes_qstrings = re_findModeStrings(JSON_palette_names, getPaletteCount());
palettes_alpha_indexes = re_initIndexArray(getPaletteCount());
if (customPalettes.size()) {
for (int i=0; i<customPalettes.size(); i++) {
@@ -477,10 +498,19 @@ void RotaryEncoderUIUsermod::re_sortModes(const char **modeNames, byte *indexes,
// public methods
/*
* setup() is called once at boot. WiFi is not yet connected at this point.
* You can use it to initialize variables, sensors or similar.
*/
/**
* @brief Initialize rotary-encoder UI: configure pins, optional PCF8574/I2C interrupt,
* integrate available displays, and cache initial encoder state.
*
* Performs all startup setup required by the usermod: allocates and configures GPIO
* pins or a PCF8574 I/O expander (including attaching an IRQ handler if requested),
* sets pin modes, sorts mode/palette lists if needed, discovers and initializes
* optional display usermods (FourLineDisplay or GC9A01), computes initial CCT, and
* reads the encoder A/B inputs to prime internal state.
*
* If required pins, the IRQ, or I2C resources cannot be allocated, the usermod
* disables itself. This runs once at boot before WiFi is connected.
*/
void RotaryEncoderUIUsermod::setup()
{
DEBUG_PRINTLN(F("Usermod Rotary Encoder init."));
@@ -533,22 +563,34 @@ void RotaryEncoderUIUsermod::setup()
}
#endif
#ifdef USERMOD_GC9A01_DISPLAY
// This Usermod also works with GC9A01DisplayUsermod for round TFT displays.
gc9a01Display = (UsermodGC9A01Display*) UsermodManager::lookup(USERMOD_ID_GC9A01_DISPLAY);
if (gc9a01Display != nullptr) {
DEBUG_PRINTLN(F("[RotaryEncoder] GC9A01 display integration enabled"));
} else {
DEBUG_PRINTLN(F("[RotaryEncoder] GC9A01 display NOT FOUND"));
}
#endif
initDone = true;
Enc_A = readPin(pinA); // Read encoder pins
Enc_B = readPin(pinB);
Enc_A_prev = Enc_A;
}
/*
* loop() is called continuously. Here you can check for events, read sensors, etc.
*
* Tips:
* 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection.
* Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker.
*
* 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds.
* Instead, use a timer check as shown here.
*/
/**
* @brief Polls the rotary encoder and button, updates the current UI state, and applies the selected adjustments (brightness, effect, speed, intensity, palette, hue, saturation, CCT, presets, and custom parameters).
*
* @details
* - Debounces and rate-limits encoder/button processing; uses ENCODER_MAX_DELAY_MS to avoid excessive polling.
* - Detects short press (cycles UI state), double-press (toggles output state), and long-press (shows network information).
* - When the active UI state changes, requests or updates an overlay on any attached display(s); handles GC9A01-specific overlay expiration and display sleep by reverting to the brightness state when needed.
* - Validates that sliders (speed, intensity, custom) are supported by the current effect and resets to brightness if not supported.
* - Translates encoder rotations into the appropriate change action for the current UI state and applies those changes to the LED segments and displays.
*
* Side effects: updates internal select_state, modifies strip/segment state, may wake/redraw displays, and triggers lamp/LED updates.
*/
void RotaryEncoderUIUsermod::loop()
{
if (!enabled) return;
@@ -618,25 +660,59 @@ void RotaryEncoderUIUsermod::loop()
}
if (newState > LAST_UI_STATE) newState = 0;
} while (!changedState);
if (display != nullptr) {
switch (newState) {
case 0: changedState = changeState(lineBuffer, 1, 0, 1); break; //1 = sun
case 1: changedState = changeState(lineBuffer, 1, 4, 2); break; //2 = skip forward
case 2: changedState = changeState(lineBuffer, 1, 8, 3); break; //3 = fire
case 3: changedState = changeState(lineBuffer, 2, 0, 4); break; //4 = custom palette
case 4: changedState = changeState(lineBuffer, 3, 0, 5); break; //5 = puzzle piece
case 5: changedState = changeState(lineBuffer, 255, 255, 7); break; //7 = brush
case 6: changedState = changeState(lineBuffer, 255, 255, 8); break; //8 = contrast
case 7: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star
case 8: changedState = changeState(lineBuffer, 255, 255, 11); break; //11 = heart
case 9: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star
case 10: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star
case 11: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star
// Support both Four Line Display and GC9A01 display
if (display != nullptr || gc9a01Display != nullptr) {
// Special handling for state 0 (Brightness) with GC9A01 - no overlay needed
if (newState == 0 && gc9a01Display != nullptr && display == nullptr) {
// For GC9A01 only (no four line display), just update main screen for brightness
if (gc9a01Display->wakeDisplay()) {
gc9a01Display->redraw(true);
changedState = false; // Throw away wake up input
} else {
gc9a01Display->redraw(false); // Update main screen directly
changedState = true;
}
} else {
// Normal overlay handling for all other states and for Four Line Display
switch (newState) {
case 0: changedState = changeState(lineBuffer, 1, 0, 1); break; //1 = sun
case 1: changedState = changeState(lineBuffer, 1, 4, 2); break; //2 = skip forward
case 2: changedState = changeState(lineBuffer, 1, 8, 3); break; //3 = fire
case 3: changedState = changeState(lineBuffer, 2, 0, 4); break; //4 = custom palette
case 4: changedState = changeState(lineBuffer, 3, 0, 5); break; //5 = puzzle piece
case 5: changedState = changeState(lineBuffer, 255, 255, 7); break; //7 = brush
case 6: changedState = changeState(lineBuffer, 255, 255, 8); break; //8 = contrast
case 7: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star
case 8: changedState = changeState(lineBuffer, 255, 255, 11); break; //11 = heart
case 9: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star
case 10: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star
case 11: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star
}
}
}
if (changedState) select_state = newState;
}
// Check if GC9A01 overlay has expired or display is asleep and reset to brightness mode (state 0)
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display != nullptr && select_state > 0) {
if (!gc9a01Display->isOverlayActive() || gc9a01Display->isDisplayAsleep()) {
// Overlay has expired or display is asleep, return to brightness mode
select_state = 0;
}
}
#endif
// Check if current state is valid for current effect (for states 1=speed, 2=intensity, 9-11=custom)
if (select_state == 1 || select_state == 2 || (select_state >= 9 && select_state <= 11)) {
char tempBuffer[64];
int sliderIndex = (select_state <= 2) ? (select_state - 1) : (select_state - 7);
if (!extractModeSlider(effectCurrent, sliderIndex, tempBuffer, 63)) {
// Current effect doesn't have this slider, reset to brightness
select_state = 0;
}
}
Enc_A = readPin(pinA); // Read encoder pins
Enc_B = readPin(pinB);
if ((Enc_A) && (!Enc_A_prev))
@@ -681,12 +757,32 @@ void RotaryEncoderUIUsermod::loop()
}
}
/**
* @brief Show a temporary "NETWORK INFO" overlay on any attached display.
*
* Displays a "NETWORK INFO" overlay for 10 seconds on supported displays.
* When a GC9A01 display is present, the overlay uses the network glyph.
*/
void RotaryEncoderUIUsermod::displayNetworkInfo() {
#ifdef USERMOD_FOUR_LINE_DISPLAY
display->networkOverlay(PSTR("NETWORK INFO"), 10000);
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display != nullptr) {
gc9a01Display->overlay(PSTR("NETWORK INFO"), 10000, 12); // Use network glyph
}
#endif
}
/**
* @brief Resolve and cache the indices of the currently active effect and palette for UI navigation.
*
* Searches available modes and palettes to determine the display indices corresponding to
* the current effect and palette identifiers, stores the resolved indices in
* `effectCurrentIndex` and `effectPaletteIndex`, and marks `currentEffectAndPaletteInitialized`.
* If no match is found for either, the corresponding index is set to 0.
*/
void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() {
DEBUG_PRINTLN(F("Finding current mode and palette."));
currentEffectAndPaletteInitialized = true;
@@ -702,7 +798,7 @@ void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() {
effectPaletteIndex = 0;
DEBUG_PRINTLN(effectPalette);
for (unsigned i = 0; i < getPaletteCount()+customPalettes.size(); i++) {
for (unsigned i = 0; i < getPaletteCount(); i++) {
if (palettes_alpha_indexes[i] == effectPalette) {
effectPaletteIndex = i;
DEBUG_PRINTLN(F("Found palette."));
@@ -711,6 +807,18 @@ void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() {
}
}
/**
* @brief Show a short-term UI overlay on any attached display and optionally mark a line/column.
*
* Displays the given text with an optional glyph on the integrated FourLineDisplay and/or GC9A01 display.
* If a display was sleeping and is awakened by this call, the wake is consumed (a redraw is performed) and the overlay is not shown.
*
* @param stateName Text to show in the overlay.
* @param markedLine Line index to mark on the FourLineDisplay (ignored if not applicable).
* @param markedCol Column index to mark on the FourLineDisplay (ignored if not applicable).
* @param glyph Glyph index to display alongside the text (display-dependent).
* @return true if the overlay was actually shown, false if a display wake-up was consumed instead.
*/
bool RotaryEncoderUIUsermod::changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph) {
#ifdef USERMOD_FOUR_LINE_DISPLAY
if (display != nullptr) {
@@ -723,6 +831,20 @@ bool RotaryEncoderUIUsermod::changeState(const char *stateName, byte markedLine,
display->setMarkLine(markedLine, markedCol);
}
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display != nullptr) {
DEBUG_PRINTF("[RotaryEncoder] Calling GC9A01 overlay: '%s' glyph=%d\n", stateName, glyph);
if (gc9a01Display->wakeDisplay()) {
// Throw away wake up input
gc9a01Display->redraw(true);
return false;
}
gc9a01Display->overlay(stateName, 750, glyph);
} else {
DEBUG_PRINTLN(F("[RotaryEncoder] gc9a01Display is NULL!"));
}
#endif
return true;
}
@@ -734,6 +856,17 @@ void RotaryEncoderUIUsermod::lampUdated() {
updateInterfaces(CALL_MODE_BUTTON);
}
/**
* @brief Adjusts the global brightness up or down and updates attached displays.
*
* Changes the global brightness by a configured step, using smaller steps when the
* current brightness is below 40 to provide finer control at low levels. After
* changing brightness the method notifies the system of the update and refreshes
* any attached displays; display behavior will present an overlay or update the
* main screen depending on the active UI state and the specific display type.
*
* @param increase True to increase brightness, false to decrease it.
*/
void RotaryEncoderUIUsermod::changeBrightness(bool increase) {
#ifdef USERMOD_FOUR_LINE_DISPLAY
if (display && display->wakeDisplay()) {
@@ -743,6 +876,16 @@ void RotaryEncoderUIUsermod::changeBrightness(bool increase) {
}
display->updateRedrawTime();
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display && gc9a01Display->wakeDisplay()) {
gc9a01Display->redraw(true);
// Throw away wake up input
return;
}
if (gc9a01Display) gc9a01Display->updateRedrawTime();
#endif
//bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0);
if (bri < 40) bri = max(min((increase ? bri+fadeAmount/2 : bri-fadeAmount/2), 255), 0); // slower steps when brightness < 16%
else bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0);
@@ -750,9 +893,34 @@ void RotaryEncoderUIUsermod::changeBrightness(bool increase) {
#ifdef USERMOD_FOUR_LINE_DISPLAY
display->updateBrightness();
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display) {
// Only show overlay if we're NOT in default state (state 0)
// In state 0, brightness changes should update the main screen directly
if (select_state == 0) {
gc9a01Display->redraw(false); // Update main interface directly (no overlay)
} else {
// Show brightness overlay like other modes (when in overlay mode)
char brightnessStr[16];
sprintf(brightnessStr, "Brightness %d%%", (bri * 100) / 255);
gc9a01Display->overlay(brightnessStr, 500, 10);
}
}
#endif
}
/**
* @brief Change the currently selected effect and apply it to segments.
*
* Updates the internal effect index (clamped to the available mode range), sets the selected
* effect on either all active segments or just the main segment depending on `applyToAll`,
* marks the usermod state as changed, triggers a lamp update, and refreshes any attached
* displays (FourLineDisplay or GC9A01) including wake/overlay handling.
*
* @param increase `true` to advance to the next effect, `false` to go to the previous effect.
*/
void RotaryEncoderUIUsermod::changeEffect(bool increase) {
#ifdef USERMOD_FOUR_LINE_DISPLAY
if (display && display->wakeDisplay()) {
@@ -762,6 +930,15 @@ void RotaryEncoderUIUsermod::changeEffect(bool increase) {
}
display->updateRedrawTime();
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display && gc9a01Display->wakeDisplay()) {
gc9a01Display->redraw(true);
// Throw away wake up input
return;
}
if (gc9a01Display) gc9a01Display->updateRedrawTime();
#endif
effectCurrentIndex = max(min((increase ? effectCurrentIndex+1 : effectCurrentIndex-1), strip.getModeCount()-1), 0);
effectCurrent = modes_alpha_indexes[effectCurrentIndex];
stateChanged = true;
@@ -782,6 +959,15 @@ void RotaryEncoderUIUsermod::changeEffect(bool increase) {
}
/**
* @brief Adjusts the global effect speed and applies the change to segments and displays.
*
* Updates the stored effect speed by adding or subtracting the configured step (clamped to 0255),
* marks the state as changed, applies the new speed to either all active segments or the main segment
* depending on the `applyToAll` flag, notifies the system of the update, and refreshes any attached displays.
*
* @param increase If `true`, increases the effect speed; if `false`, decreases it.
*/
void RotaryEncoderUIUsermod::changeEffectSpeed(bool increase) {
#ifdef USERMOD_FOUR_LINE_DISPLAY
if (display && display->wakeDisplay()) {
@@ -791,6 +977,16 @@ void RotaryEncoderUIUsermod::changeEffectSpeed(bool increase) {
}
display->updateRedrawTime();
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display && gc9a01Display->wakeDisplay()) {
gc9a01Display->redraw(true);
// Throw away wake up input
return;
}
if (gc9a01Display) gc9a01Display->updateRedrawTime();
#endif
effectSpeed = max(min((increase ? effectSpeed+fadeAmount : effectSpeed-fadeAmount), 255), 0);
stateChanged = true;
if (applyToAll) {
@@ -807,9 +1003,24 @@ void RotaryEncoderUIUsermod::changeEffectSpeed(bool increase) {
#ifdef USERMOD_FOUR_LINE_DISPLAY
display->updateSpeed();
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display) {
gc9a01Display->redraw(false); // Update speed display
}
#endif
}
/**
* @brief Adjusts the current effect intensity up or down and applies the change to segments and UI.
*
* Updates the stored effect intensity by one step (bounded to 0255), marks state as changed,
* applies the new intensity to either all active segments or the main segment depending on configuration,
* notifies the system of the update, and refreshes any attached display overlays.
*
* @param increase If `true`, increase intensity; if `false`, decrease intensity.
*/
void RotaryEncoderUIUsermod::changeEffectIntensity(bool increase) {
#ifdef USERMOD_FOUR_LINE_DISPLAY
if (display && display->wakeDisplay()) {
@@ -819,6 +1030,16 @@ void RotaryEncoderUIUsermod::changeEffectIntensity(bool increase) {
}
display->updateRedrawTime();
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display && gc9a01Display->wakeDisplay()) {
gc9a01Display->redraw(true);
// Throw away wake up input
return;
}
if (gc9a01Display) gc9a01Display->updateRedrawTime();
#endif
effectIntensity = max(min((increase ? effectIntensity+fadeAmount : effectIntensity-fadeAmount), 255), 0);
stateChanged = true;
if (applyToAll) {
@@ -835,9 +1056,29 @@ void RotaryEncoderUIUsermod::changeEffectIntensity(bool increase) {
#ifdef USERMOD_FOUR_LINE_DISPLAY
display->updateIntensity();
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display) {
gc9a01Display->redraw(false); // Update intensity display
}
#endif
}
/**
* @brief Adjusts a segment custom parameter (custom1custom3), updates segments and UI.
*
* Increases or decreases the selected custom parameter, clamps the resulting value to 0255,
* marks the usermod state as changed, notifies the system via lampUdated(), and shows an
* overlay with the new value on any attached display.
*
* @param par Index of the custom parameter to change: 1, 2, or 3. Values other than 2 or 3
* are treated as 1.
* @param increase `true` to increase the parameter, `false` to decrease it.
*
* @note If `applyToAll` is true, the change is copied to all other active segments; otherwise
* only the main segment is modified.
*/
void RotaryEncoderUIUsermod::changeCustom(uint8_t par, bool increase) {
uint8_t val = 0;
#ifdef USERMOD_FOUR_LINE_DISPLAY
@@ -848,6 +1089,16 @@ void RotaryEncoderUIUsermod::changeCustom(uint8_t par, bool increase) {
}
display->updateRedrawTime();
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display && gc9a01Display->wakeDisplay()) {
gc9a01Display->redraw(true);
// Throw away wake up input
return;
}
if (gc9a01Display) gc9a01Display->updateRedrawTime();
#endif
stateChanged = true;
if (applyToAll) {
uint8_t id = strip.getFirstSelectedSegId();
@@ -880,9 +1131,27 @@ void RotaryEncoderUIUsermod::changeCustom(uint8_t par, bool increase) {
sprintf(lineBuffer, "%d", val);
display->overlay(lineBuffer, 500, 10); // use star
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display) {
char lineBuffer[64];
sprintf(lineBuffer, "Custom%d: %d", par, val);
gc9a01Display->overlay(lineBuffer, 500, 10); // use star glyph
}
#endif
}
/**
* @brief Change the selected color palette by one step and apply it to segments and displays.
*
* Updates the current palette index (advances if `increase` is true, otherwise moves backward),
* constrains it to the valid palette range, sets the active palette, marks the usermod state as changed,
* applies the new palette to either all active segments or only the main segment depending on configuration,
* signals a lamp update to commit the change, and refreshes any attached displays (including wake handling).
*
* @param increase `true` to move to the next palette, `false` to move to the previous palette.
*/
void RotaryEncoderUIUsermod::changePalette(bool increase) {
#ifdef USERMOD_FOUR_LINE_DISPLAY
if (display && display->wakeDisplay()) {
@@ -892,7 +1161,17 @@ void RotaryEncoderUIUsermod::changePalette(bool increase) {
}
display->updateRedrawTime();
#endif
effectPaletteIndex = max(min((unsigned)(increase ? effectPaletteIndex+1 : effectPaletteIndex-1), getPaletteCount()+customPalettes.size()-1), 0U);
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display && gc9a01Display->wakeDisplay()) {
gc9a01Display->redraw(true);
// Throw away wake up input
return;
}
if (gc9a01Display) gc9a01Display->updateRedrawTime();
#endif
effectPaletteIndex = max(min((int)(increase ? effectPaletteIndex+1 : effectPaletteIndex-1), (int)(getPaletteCount()-1)), 0);
effectPalette = palettes_alpha_indexes[effectPaletteIndex];
stateChanged = true;
if (applyToAll) {
@@ -909,9 +1188,26 @@ void RotaryEncoderUIUsermod::changePalette(bool increase) {
#ifdef USERMOD_FOUR_LINE_DISPLAY
display->showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2);
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display) {
gc9a01Display->redraw(false); // Update palette display
}
#endif
}
/**
* Adjusts the primary hue value and applies the computed color to the active segment(s), then updates displays.
*
* This clamps the hue to the 0255 range, converts the hue and current saturation to RGB(W), sets the internal
* changed-state flag, applies the new color to either all active segments or the main segment depending on
* configuration, and notifies the system of the update. If a connected display is sleeping, a wake event will
* trigger a redraw and consume the input without changing the hue; otherwise an overlay showing the hue value
* is displayed.
*
* @param increase If `true`, increases the hue by the configured step; if `false`, decreases the hue.
*/
void RotaryEncoderUIUsermod::changeHue(bool increase){
#ifdef USERMOD_FOUR_LINE_DISPLAY
if (display && display->wakeDisplay()) {
@@ -921,6 +1217,16 @@ void RotaryEncoderUIUsermod::changeHue(bool increase){
}
display->updateRedrawTime();
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display && gc9a01Display->wakeDisplay()) {
gc9a01Display->redraw(true);
// Throw away wake up input
return;
}
if (gc9a01Display) gc9a01Display->updateRedrawTime();
#endif
currentHue1 = max(min((increase ? currentHue1+fadeAmount : currentHue1-fadeAmount), 255), 0);
colorHStoRGB(currentHue1*256, currentSat1, colPri);
stateChanged = true;
@@ -940,8 +1246,23 @@ void RotaryEncoderUIUsermod::changeHue(bool increase){
sprintf(lineBuffer, "%d", currentHue1);
display->overlay(lineBuffer, 500, 7); // use brush
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display) {
char lineBuffer[64];
sprintf(lineBuffer, "Hue: %d", currentHue1);
gc9a01Display->overlay(lineBuffer, 500, 7); // use brush glyph
}
#endif
}
/**
* @brief Adjusts the current saturation and applies the change to LEDs and displays.
*
* Increments or decrements the main saturation value by the configured step, clamps it to the range 0255, updates the primary RGBW color, applies the new color to either all active segments or the main segment (depending on `applyToAll`), triggers a lamp update, and shows the updated saturation on attached display overlays.
*
* @param increase If true, increase saturation; if false, decrease saturation.
*/
void RotaryEncoderUIUsermod::changeSat(bool increase){
#ifdef USERMOD_FOUR_LINE_DISPLAY
if (display && display->wakeDisplay()) {
@@ -951,6 +1272,16 @@ void RotaryEncoderUIUsermod::changeSat(bool increase){
}
display->updateRedrawTime();
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display && gc9a01Display->wakeDisplay()) {
gc9a01Display->redraw(true);
// Throw away wake up input
return;
}
if (gc9a01Display) gc9a01Display->updateRedrawTime();
#endif
currentSat1 = max(min((increase ? currentSat1+fadeAmount : currentSat1-fadeAmount), 255), 0);
colorHStoRGB(currentHue1*256, currentSat1, colPri);
if (applyToAll) {
@@ -969,8 +1300,28 @@ void RotaryEncoderUIUsermod::changeSat(bool increase){
sprintf(lineBuffer, "%d", currentSat1);
display->overlay(lineBuffer, 500, 8); // use contrast
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display) {
char lineBuffer[64];
sprintf(lineBuffer, "Sat: %d", currentSat1);
gc9a01Display->overlay(lineBuffer, 500, 8); // use contrast glyph
}
#endif
}
/**
* @brief Apply or clear a saved preset range and show a brief overlay.
*
* Constructs a preset state from the configured presetLow/presetHigh range and
* applies it when `increase` is true or clears it when `increase` is false.
* If no valid preset range is configured (presetHigh <= presetLow or either is 0),
* the function does nothing. After applying/clearing the preset it notifies the
* system of the change and, when a supported display is attached, shows a
* short overlay indicating the current preset.
*
* @param increase `true` to apply the preset range, `false` to remove/clear it.
*/
void RotaryEncoderUIUsermod::changePreset(bool increase) {
#ifdef USERMOD_FOUR_LINE_DISPLAY
if (display && display->wakeDisplay()) {
@@ -980,6 +1331,16 @@ void RotaryEncoderUIUsermod::changePreset(bool increase) {
}
display->updateRedrawTime();
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display && gc9a01Display->wakeDisplay()) {
gc9a01Display->redraw(true);
// Throw away wake up input
return;
}
if (gc9a01Display) gc9a01Display->updateRedrawTime();
#endif
if (presetHigh && presetLow && presetHigh > presetLow) {
StaticJsonDocument<64> root;
char str[64];
@@ -1000,9 +1361,26 @@ void RotaryEncoderUIUsermod::changePreset(bool increase) {
sprintf(str, "%d", currentPreset);
display->overlay(str, 500, 11); // use heart
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display) {
char lineBuffer[64];
sprintf(lineBuffer, "Preset: %d", currentPreset);
gc9a01Display->overlay(lineBuffer, 500, 11); // use heart glyph
}
#endif
}
}
/**
* @brief Adjusts the correlated color temperature (CCT) and applies it to active segments.
*
* Increments or decrements the stored CCT by the configured step, clamps the result to 0255,
* assigns the value to every active segment, triggers a lamp update, and shows a brief overlay
* on any attached display.
*
* @param increase True to increase CCT, false to decrease.
*/
void RotaryEncoderUIUsermod::changeCCT(bool increase){
#ifdef USERMOD_FOUR_LINE_DISPLAY
if (display && display->wakeDisplay()) {
@@ -1012,6 +1390,16 @@ void RotaryEncoderUIUsermod::changeCCT(bool increase){
}
display->updateRedrawTime();
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display && gc9a01Display->wakeDisplay()) {
gc9a01Display->redraw(true);
// Throw away wake up input
return;
}
if (gc9a01Display) gc9a01Display->updateRedrawTime();
#endif
currentCCT = max(min((increase ? currentCCT+fadeAmount : currentCCT-fadeAmount), 255), 0);
// if (applyToAll) {
for (unsigned i=0; i<strip.getSegmentsNum(); i++) {
@@ -1029,6 +1417,14 @@ void RotaryEncoderUIUsermod::changeCCT(bool increase){
sprintf(lineBuffer, "%d", currentCCT);
display->overlay(lineBuffer, 500, 10); // use star
#endif
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display) {
char lineBuffer[64];
sprintf(lineBuffer, "CCT: %d", currentCCT);
gc9a01Display->overlay(lineBuffer, 500, 10); // use star glyph
}
#endif
}
/*
+1 -1
View File
@@ -10276,7 +10276,7 @@ uint16_t mode_particleBalance(void) {
if (SEGMENT.check3) // random, use perlin noise
xgravity = ((int16_t)perlin8(SEGENV.aux0) - 128);
else // sinusoidal
xgravity = (int16_t)cos8(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0)
xgravity = (int16_t)cos8_t(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0)
// scale the force
xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; // xgravity: -127 to +127
PartSys->applyForce(xgravity);
+2 -9
View File
@@ -421,7 +421,7 @@ typedef enum mapping1D2D {
class WS2812FX;
// segment, 80 bytes
// segment, 76 bytes
class Segment {
public:
uint32_t colors[NUM_COLORS];
@@ -460,12 +460,9 @@ class Segment {
bool check1 : 1; // checkmark 1
bool check2 : 1; // checkmark 2
bool check3 : 1; // checkmark 3
//uint8_t blendMode : 4; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn
};
uint8_t blendMode; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn
struct {
uint8_t zoomAmount : 4; // zoom amount (0-15); 8 == no zoom
uint8_t rotateSpeed : 4; // rotation speed (0-15); 0 == no rotation
};
char *name; // segment name
// runtime data
@@ -491,7 +488,6 @@ class Segment {
bool _manualW : 1;
};
};
mutable uint16_t _rotatedAngle; // current rotation angle (2D)
// static variables are use to speed up effect calculations by stashing common pre-calculated values
static unsigned _usedSegmentData; // amount of data used by all segments
@@ -595,8 +591,6 @@ class Segment {
, check2(false)
, check3(false)
, blendMode(0)
, zoomAmount(8)
, rotateSpeed(0)
, name(nullptr)
, next_time(0)
, step(0)
@@ -607,7 +601,6 @@ class Segment {
, _dataLen(0)
, _default_palette(6)
, _capabilities(0)
, _rotatedAngle(0)
, _t(nullptr)
{
DEBUGFX_PRINTF_P(PSTR("-- Creating segment: %p [%d,%d:%d,%d]\n"), this, (int)start, (int)stop, (int)startY, (int)stopY);
+13 -115
View File
@@ -230,7 +230,7 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
// then come the custom palettes (255,254,...) growing downwards from 255 (255 being 1st custom palette)
// palette 0 is a varying palette depending on effect and may be replaced by segment's color if so
// instructed in color_from_palette()
if (pal > FIXED_PALETTE_COUNT && pal <= 255-customPalettes.size()) pal = 0; // out of bounds palette
if (pal >= FIXED_PALETTE_COUNT && pal <= 255-customPalettes.size()) pal = 0; // out of bounds palette
//default palette. Differs depending on effect
if (pal == 0) pal = _default_palette; // _default_palette is set in setMode()
switch (pal) {
@@ -268,11 +268,11 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
default: //progmem palettes
if (pal > 255 - customPalettes.size()) {
targetPalette = customPalettes[255-pal]; // we checked bounds above
} else if (pal < DYNAMIC_PALETTE_COUNT+FASTLED_PALETTE_COUNT+1) { // palette 6 - 12, fastled palettes
targetPalette = *fastledPalettes[pal-DYNAMIC_PALETTE_COUNT-1];
} else if (pal < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) { // palette 6 - 12, fastled palettes
targetPalette = *fastledPalettes[pal - DYNAMIC_PALETTE_COUNT];
} else {
byte tcp[72];
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-(DYNAMIC_PALETTE_COUNT+FASTLED_PALETTE_COUNT)-1])), 72);
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal - (DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT)])), sizeof(tcp));
targetPalette.loadDynamicGradientPalette(tcp);
}
break;
@@ -563,7 +563,7 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
sOpt = extractModeDefaults(fx, "ix"); intensity = (sOpt >= 0) ? sOpt : DEFAULT_INTENSITY;
sOpt = extractModeDefaults(fx, "c1"); custom1 = (sOpt >= 0) ? sOpt : DEFAULT_C1;
sOpt = extractModeDefaults(fx, "c2"); custom2 = (sOpt >= 0) ? sOpt : DEFAULT_C2;
sOpt = extractModeDefaults(fx, "c3"); custom3 = (sOpt >= 0) ? constrain(sOpt, 0, 31) : DEFAULT_C3;
sOpt = extractModeDefaults(fx, "c3"); custom3 = (sOpt >= 0) ? sOpt : DEFAULT_C3;
sOpt = extractModeDefaults(fx, "o1"); check1 = (sOpt >= 0) ? (bool)sOpt : false;
sOpt = extractModeDefaults(fx, "o2"); check2 = (sOpt >= 0) ? (bool)sOpt : false;
sOpt = extractModeDefaults(fx, "o3"); check3 = (sOpt >= 0) ? (bool)sOpt : false;
@@ -573,8 +573,6 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
sOpt = extractModeDefaults(fx, "mi"); if (sOpt >= 0) mirror = (bool)sOpt; // NOTE: setting this option is a risky business
sOpt = extractModeDefaults(fx, "rY"); if (sOpt >= 0) reverse_y = (bool)sOpt;
sOpt = extractModeDefaults(fx, "mY"); if (sOpt >= 0) mirror_y = (bool)sOpt; // NOTE: setting this option is a risky business
sOpt = extractModeDefaults(fx, "rS"); if (sOpt >= 0) rotateSpeed = constrain(sOpt, 0, 15); // 0 = no rotation
sOpt = extractModeDefaults(fx, "zA"); if (sOpt >= 0) zoomAmount = constrain(sOpt, 0, 15); // 8 = no zoom
}
sOpt = extractModeDefaults(fx, "pal"); // always extract 'pal' to set _default_palette
if (sOpt >= 0 && loadDefaults) setPalette(sOpt);
@@ -1476,109 +1474,9 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
}
};
// zooming and rotation
auto RotateAndZoom = [](uint32_t *srcPixels, uint32_t *destPixels, int midX, int midY, int cols, int rows, int shearAngle, int zoomOffset) {
for (int i = 0; i < cols * rows; i++) destPixels[i] = BLACK; // fill black
constexpr uint8_t Scale_Shift = 10;
constexpr int Fixed_Scale = (1 << Scale_Shift);
constexpr int RoundVal = (1 << (Scale_Shift - 1));
constexpr int zoomRange = (Fixed_Scale * 3) / 4; // 768
int zoomScale = Fixed_Scale + (zoomOffset * zoomRange) / 8; // zoomOffset: -8 .. +7 -> zoomScale: 256 .. 1696
if (zoomScale <= 0) zoomScale = 1; // avoid divide-by-zero and negative zoom
const bool flip = (shearAngle > 90 && shearAngle < 270); // Flip to avoid instability near 180°
if (flip) shearAngle = (shearAngle + 180) % 360;
// Calculate shearX and shearY
const float angleRadians = radians(shearAngle);
int shearX = -tan_t(angleRadians / 2) * Fixed_Scale;
int shearY = sin_t(angleRadians) * Fixed_Scale;
const int WRAP_PAD_X = cols << 5; // ×32
const int WRAP_PAD_Y = rows << 5; // Ensures wrap works with large negative coordinates when zoomed out
// Use inverse mapping: iterate destination pixels, find source coordinates
for (int destY = 0; destY < rows; destY++) {
for (int destX = 0; destX < cols; destX++) {
// Translate destination to origin
int dx = destX - midX;
int dy = destY - midY;
// Inverse shear transformations (reverse order)
int x1 = dx - ((shearX * dy + RoundVal) >> Scale_Shift);
int y0 = dy - ((shearY * x1 + RoundVal) >> Scale_Shift);
int x0 = x1 - ((shearX * y0 + RoundVal) >> Scale_Shift);
// Apply zoom to source coordinates
x0 = (x0 * Fixed_Scale) / zoomScale;
y0 = (y0 * Fixed_Scale) / zoomScale;
// Handle flip
int srcX = flip ? (midX - x0) : (midX + x0);
int srcY = flip ? (midY - y0) : (midY + y0);
// Bounds check or wrap
//if (wrap) { // Wrap around
srcX = (srcX + WRAP_PAD_X); while (srcX >= cols) srcX -= cols; // positive modulo since % is slow
srcY = (srcY + WRAP_PAD_Y); while (srcY >= rows) srcY -= rows; // positive modulo since % is slow
//}
//else if (wrap_and_mirror) { // Wrap plus mirror
// int tileX = (srcX + WRAP_PAD_X) / cols;
// int tileY = (srcY + WRAP_PAD_Y) / rows;
// // Wrap src
// srcX = (srcX + WRAP_PAD_X); while (srcX >= cols) srcX -= cols; // positive modulo since % is slow
// srcY = (srcY + WRAP_PAD_Y); while (srcY >= rows) srcY -= rows; // positive modulo since % is slow
// // Flip on odd tiles
// if (tileX & 1) srcX = cols - 1 - srcX;
// if (tileY & 1) srcY = rows - 1 - srcY;
//}
//else
if ((unsigned)srcX >= (unsigned)cols || (unsigned)srcY >= (unsigned)rows) continue;
// Sample from source & write to destination
destPixels[destX + destY * cols] = srcPixels[srcX + srcY * cols];
}
}
};
uint32_t *_pixelsN = topSegment.getPixels(); // we will use this pointer as a source later insetad of getPixelColorRaw()
if (topSegment.rotateSpeed || topSegment.zoomAmount != 8) {
_pixelsN = new uint32_t[nCols * nRows]; // may use allocateBuffer() if needed
const int midX = nCols / 2;
const int midY = nRows / 2;
if (topSegment.rotateSpeed != 0) {
topSegment._rotatedAngle += topSegment.rotateSpeed;
while (topSegment._rotatedAngle > 3600) topSegment._rotatedAngle -= 3600;
} else {
topSegment._rotatedAngle = 0; // reset angle if no rotation
}
RotateAndZoom(topSegment.getPixels(), _pixelsN, midX, midY, nCols, nRows, topSegment._rotatedAngle/10, topSegment.zoomAmount - 8);
}
uint32_t *_pixelsO = topSegment.getPixels(); // we will use this pointer as a source (old segment during transition) later insetad of getPixelColorRaw()
if (segO) {
_pixelsO = segO->getPixels(); // default to unmodified old segment pixels
if (segO->rotateSpeed || segO->zoomAmount != 8) {
_pixelsO = new uint32_t[oCols * oRows]; // may use allocateBuffer() if needed
const int midXo = oCols / 2;
const int midYo = oRows / 2;
if (segO->rotateSpeed != 0) {
segO->_rotatedAngle += segO->rotateSpeed;
while (segO->_rotatedAngle > 3600) segO->_rotatedAngle -= 3600;
} else {
segO->_rotatedAngle = 0;
}
RotateAndZoom(segO->getPixels(), _pixelsO, midXo, midYo, oCols, oRows, segO->_rotatedAngle/10, segO->zoomAmount - 8);
}
}
// if we blend using "push" style we need to "shift" canvas to left/right/up/down
unsigned offsetX = (blendingStyle == BLEND_STYLE_PUSH_UP || blendingStyle == BLEND_STYLE_PUSH_DOWN) ? 0 : progInv * nCols / 0xFFFFU;
unsigned offsetY = (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_RIGHT) ? 0 : progInv * nRows / 0xFFFFU;
if (blendingStyle == BLEND_STYLE_PUSH_RIGHT) offsetX = nCols - offsetX;
if (blendingStyle == BLEND_STYLE_PUSH_UP) offsetY = nRows - offsetY;
// we only traverse new segment, not old one
for (int r = 0; r < nRows; r++) for (int c = 0; c < nCols; c++) {
@@ -1587,19 +1485,22 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
const Segment *seg = clipped && segO ? segO : &topSegment; // pixel is never clipped for FADE
int vCols = seg == segO ? oCols : nCols; // old segment may have different dimensions
int vRows = seg == segO ? oRows : nRows; // old segment may have different dimensions
uint32_t *_pixelsR = seg == segO ? _pixelsO : _pixelsN;
int x = c;
int y = r;
// if we blend using "push" style we need to "shift" canvas to left/right/up/down
if (offsetX != 0) { x = (x + offsetX); while (x >= nCols) x -= nCols; }
if (offsetY != 0) { y = (y + offsetY); while (y >= nRows) y -= nRows; }
switch (blendingStyle) {
case BLEND_STYLE_PUSH_RIGHT: x = (x + offsetX) % nCols; break;
case BLEND_STYLE_PUSH_LEFT: x = (x - offsetX + nCols) % nCols; break;
case BLEND_STYLE_PUSH_DOWN: y = (y + offsetY) % nRows; break;
case BLEND_STYLE_PUSH_UP: y = (y - offsetY + nRows) % nRows; break;
}
uint32_t c_a = BLACK;
if (x < vCols && y < vRows) c_a = _pixelsR[x + y*vCols]; // will get clipped pixel from old segment or unclipped pixel from new segment
if (x < vCols && y < vRows) c_a = seg->getPixelColorRaw(x + y*vCols); // will get clipped pixel from old segment or unclipped pixel from new segment
if (segO && blendingStyle == BLEND_STYLE_FADE
&& (topSegment.mode != segO->mode || (segO->name != topSegment.name && segO->name && topSegment.name && strncmp(segO->name, topSegment.name, WLED_MAX_SEGNAME_LEN) != 0))
&& x < oCols && y < oRows) {
// we need to blend old segment using fade as pixels are not clipped
c_a = color_blend16(c_a, _pixelsO[x + y*oCols], progInv);
c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv);
} else if (blendingStyle != BLEND_STYLE_FADE) {
// if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp)
// workaround for On/Off transition
@@ -1630,9 +1531,6 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
}
}
}
// clean up
if (topSegment.rotateSpeed || topSegment.zoomAmount != 8) delete[] _pixelsN;
if (segO && (segO->rotateSpeed || segO->zoomAmount != 8)) delete[] _pixelsO;
#endif
} else {
const int nLen = topSegment.virtualLength();
+6 -6
View File
@@ -156,7 +156,7 @@ void ParticleSystem2D::setParticleSize(uint8_t size) {
particleHardRadius = PS_P_MINHARDRADIUS + ((particlesize * 52) >> 6); // use 1 pixel + 80% of size for hard radius (slight overlap with boarders so they do not "float" and nicer stacking)
}
else if (particlesize == 0)
particleHardRadius = particleHardRadius >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel)
particleHardRadius = PS_P_MINHARDRADIUS >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel)
}
// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable
@@ -595,7 +595,7 @@ void ParticleSystem2D::render() {
if (fireIntesity) { // fire mode
brightness = (uint32_t)particles[i].ttl * (3 + (fireIntesity >> 5)) + 5;
brightness = min(brightness, (uint32_t)255);
baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP);
baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP); // map hue to brightness for fire effect
}
else {
brightness = min((particles[i].ttl << 1), (int)255);
@@ -842,7 +842,7 @@ void ParticleSystem2D::handleCollisions() {
for (uint32_t bin = 0; bin < numBins; bin++) {
binParticleCount = 0; // reset for this bin
int32_t binStart = bin * binWidth - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored
int32_t binEnd = binStart + binWidth + overlap; // note: last bin can be out of bounds, see above;
int32_t binEnd = binStart + binWidth + (overlap << 1); // add twice the overlap as start is start-overlap, note: last bin can be out of bounds, see above;
// fill the binIndices array for this bin
for (uint32_t i = 0; i < usedParticles; i++) {
@@ -879,7 +879,7 @@ void ParticleSystem2D::handleCollisions() {
massratio1 = (mass2 << 8) / totalmass; // massratio 1 depends on mass of particle 2, i.e. if 2 is heavier -> higher velocity impact on 1
massratio2 = (mass1 << 8) / totalmass;
}
// note: using the same logic as in 1D is much slower though it would be more accurate but it is not really needed in 2D
// note: using the same logic as in 1D is much slower though it would be more accurate but it is not really needed in 2D: particles slipping through each other is much less visible
int32_t dx = (particles[idx_j].x + particles[idx_j].vx) - (particles[idx_i].x + particles[idx_i].vx); // distance with lookahead
if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare)
int32_t dy = (particles[idx_j].y + particles[idx_j].vy) - (particles[idx_i].y + particles[idx_i].vy); // distance with lookahead
@@ -1247,7 +1247,7 @@ void ParticleSystem1D::setParticleSize(const uint8_t size) {
particleHardRadius = PS_P_MINHARDRADIUS_1D + ((particlesize * 52) >> 6); // use 1 pixel + 80% of size for hard radius (slight overlap with boarders so they do not "float" and nicer stacking)
}
else if (particlesize == 0)
particleHardRadius = particleHardRadius >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel)
particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel)
}
// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable
@@ -1632,7 +1632,7 @@ void ParticleSystem1D::handleCollisions() {
for (uint32_t bin = 0; bin < numBins; bin++) {
binParticleCount = 0; // reset for this bin
int32_t binStart = bin * binWidth - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored
int32_t binEnd = binStart + binWidth + overlap; // note: last bin can be out of bounds, see above
int32_t binEnd = binStart + binWidth + (overlap << 1); // add twice the overlap as start is start-overlap, note: last bin can be out of bounds, see above
// fill the binIndices array for this bin
for (uint32_t i = 0; i < usedParticles; i++) {
+105 -121
View File
@@ -21,12 +21,9 @@
#ifdef ESP8266
#include "core_esp8266_waveform.h"
#endif
#include "const.h"
#include "colors.h"
#include "pin_manager.h"
#include "bus_manager.h"
#include "bus_wrapper.h"
#include <bits/unique_ptr.h>
#include "wled.h"
extern char cmDNS[];
extern bool cctICused;
@@ -34,17 +31,18 @@ extern bool useParallelI2S;
// functions to get/set bits in an array - based on functions created by Brandon for GOL
// toDo : make this a class that's completely defined in a header file
// note: these functions are automatically inline by the compiler
bool getBitFromArray(const uint8_t* byteArray, size_t position) { // get bit value
size_t byteIndex = position / 8;
unsigned bitIndex = position % 8;
size_t byteIndex = position >> 3; // divide by 8
unsigned bitIndex = position & 0x07; // modulo 8
uint8_t byteValue = byteArray[byteIndex];
return (byteValue >> bitIndex) & 1;
}
void setBitInArray(uint8_t* byteArray, size_t position, bool value) { // set bit - with error handling for nullptr
//if (byteArray == nullptr) return;
size_t byteIndex = position / 8;
unsigned bitIndex = position % 8;
size_t byteIndex = position >> 3; // divide by 8
unsigned bitIndex = position & 0x07; // modulo 8
if (value)
byteArray[byteIndex] |= (1 << bitIndex);
else
@@ -52,7 +50,7 @@ void setBitInArray(uint8_t* byteArray, size_t position, bool value) { // set bi
}
size_t getBitArrayBytes(size_t num_bits) { // number of bytes needed for an array with num_bits bits
return (num_bits + 7) / 8;
return (num_bits + 7) >> 3;
}
void setBitArray(uint8_t* byteArray, size_t numBits, bool value) { // set all bits to same value
@@ -62,45 +60,6 @@ void setBitArray(uint8_t* byteArray, size_t numBits, bool value) { // set all b
else memset(byteArray, 0x00, len);
}
//colors.cpp
uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb);
//udp.cpp
uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const byte *buffer, uint8_t bri=255, bool isRGBW=false);
//util.cpp
// memory allocation wrappers
extern "C" {
// prefer DRAM over PSRAM (if available) in d_ alloc functions
void *d_malloc(size_t);
void *d_calloc(size_t, size_t);
void *d_realloc_malloc(void *ptr, size_t size);
#ifndef ESP8266
inline void d_free(void *ptr) { heap_caps_free(ptr); }
#else
inline void d_free(void *ptr) { free(ptr); }
#endif
#if defined(BOARD_HAS_PSRAM)
// prefer PSRAM over DRAM in p_ alloc functions
void *p_malloc(size_t);
void *p_calloc(size_t, size_t);
void *p_realloc_malloc(void *ptr, size_t size);
inline void p_free(void *ptr) { heap_caps_free(ptr); }
#else
#define p_malloc d_malloc
#define p_calloc d_calloc
#define p_free d_free
#endif
}
//color mangling macros
#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b))))
#define R(c) (byte((c) >> 16))
#define G(c) (byte((c) >> 8))
#define B(c) (byte(c))
#define W(c) (byte((c) >> 24))
static ColorOrderMap _colorOrderMap = {};
bool ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) {
@@ -800,6 +759,13 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
_valid = false;
_hasRgb = true;
_hasWhite = false;
virtualDisp = nullptr; // todo: this should be solved properly, can cause memory leak (if omitted here, nothing seems to work)
// aliases for easier reading
uint8_t panelWidth = bc.pins[0];
uint8_t panelHeight = bc.pins[1];
uint8_t chainLength = bc.pins[2];
_rows = bc.pins[3];
_cols = bc.pins[4];
mxconfig.double_buff = false; // Use our own memory-optimised buffer rather than the driver's own double-buffer
@@ -808,38 +774,25 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
// mxconfig.latch_blanking = 3;
// mxconfig.i2sspeed = HUB75_I2S_CFG::HZ_10M; // experimental - 5MHZ should be enugh, but colours looks slightly better at 10MHz
//mxconfig.min_refresh_rate = 90;
//mxconfig.min_refresh_rate = 120;
mxconfig.clkphase = bc.reversed;
// mxconfig.min_refresh_rate = 90;
// mxconfig.min_refresh_rate = 120;
virtualDisp = nullptr;
mxconfig.clkphase = bc.reversed;
// allow chain length up to 4, limit to prevent bad data from preventing boot due to low memory
mxconfig.chain_length = max((uint8_t) 1, min(chainLength, (uint8_t) 4));
if (mxconfig.mx_height >= 64 && (mxconfig.chain_length > 1)) {
DEBUGBUS_PRINTLN(F("WARNING, only single panel can be used of 64 pixel boards due to memory"));
mxconfig.chain_length = 1;
}
if (bc.type == TYPE_HUB75MATRIX_HS) {
mxconfig.mx_width = min((uint8_t) 64, bc.pins[0]);
mxconfig.mx_height = min((uint8_t) 64, bc.pins[1]);
// Disable chains of panels for now, incomplete UI changes
// if(bc.pins[2] > 1 && bc.pins[3] != 0 && bc.pins[4] != 0 && bc.pins[3] != 255 && bc.pins[4] != 255) {
// virtualDisp = new VirtualMatrixPanel((*display), bc.pins[3], bc.pins[4], mxconfig.mx_width, mxconfig.mx_height, CHAIN_BOTTOM_LEFT_UP);
// }
mxconfig.mx_width = min((uint8_t) 64, panelWidth); // TODO: UI limit is 128, this limits to 64
mxconfig.mx_height = min((uint8_t) 64, panelHeight);
} else if (bc.type == TYPE_HUB75MATRIX_QS) {
mxconfig.mx_width = min((uint8_t) 64, bc.pins[0]) * 2;
mxconfig.mx_height = min((uint8_t) 64, bc.pins[1]) / 2;
virtualDisp = new VirtualMatrixPanel((*display), 1, 1, bc.pins[0], bc.pins[1]);
virtualDisp->setRotation(0);
switch(bc.pins[1]) {
case 16:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_16PX_HIGH);
break;
case 32:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_32PX_HIGH);
break;
case 64:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_64PX_HIGH);
break;
default:
DEBUGBUS_PRINTLN("Unsupported height");
return;
}
_isVirtual = true;
mxconfig.mx_width = min((uint8_t) 64, panelWidth) * 2;
mxconfig.mx_height = min((uint8_t) 64, panelHeight) / 2;
} else {
DEBUGBUS_PRINTLN("Unknown type");
return;
@@ -853,12 +806,6 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
} else mxconfig.setPixelColorDepthBits(8);
#endif
mxconfig.chain_length = max((uint8_t) 1, min(bc.pins[2], (uint8_t) 4)); // prevent bad data preventing boot due to low memory
if(mxconfig.mx_height >= 64 && (mxconfig.chain_length > 1)) {
DEBUGBUS_PRINTLN("WARNING, only single panel can be used of 64 pixel boards due to memory");
mxconfig.chain_length = 1;
}
// HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN};
@@ -915,9 +862,9 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
return;
}
if(bc.colorOrder == COL_ORDER_RGB) {
if (bc.colorOrder == COL_ORDER_RGB) {
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = Default color order (RGB)");
} else if(bc.colorOrder == COL_ORDER_BGR) {
} else if (bc.colorOrder == COL_ORDER_BGR) {
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = color order BGR");
int8_t tmpPin;
tmpPin = mxconfig.gpio.r1;
@@ -944,21 +891,27 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
return;
}
this->_len = (display->width() * display->height());
this->_len = (display->width() * display->height()); // note: this returns correct number of pixels but incorrect dimensions if using virtual display (updated below)
DEBUGBUS_PRINTF("Length: %u\n", _len);
if(this->_len >= MAX_LEDS) {
if (this->_len >= MAX_LEDS) {
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA Too many LEDS - playing safe");
return;
}
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA created");
// let's adjust default brightness
display->setBrightness8(25); // range is 0-255, 0 - 0%, 255 - 100%
// as noted in HUB75_I2S_DMA library, some panels can show ghosting if set higher than 239, so let users override at compile time
#ifndef WLED_HUB75_MAX_BRIGHTNESS
#define WLED_HUB75_MAX_BRIGHTNESS 255
#endif
// let's adjust default brightness (128), brightness scaling is handled by WLED
//display->setBrightness8(WLED_HUB75_MAX_BRIGHTNESS); // range is 0-255, 0 - 0%, 255 - 100%
delay(24); // experimental
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
// Allocate memory and start DMA display
if( not display->begin() ) {
if (!display->begin() ) {
DEBUGBUS_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! I2S memory allocation failed ***********");
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
return;
@@ -971,10 +924,10 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
display->clearScreen(); // initially clear the screen buffer
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA clear ok");
if (_ledBuffer) free(_ledBuffer); // should not happen
if (_ledsDirty) free(_ledsDirty); // should not happen
if (_ledBuffer) d_free(_ledBuffer); // should not happen
if (_ledsDirty) d_free(_ledsDirty); // should not happen
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA allocate memory");
_ledsDirty = (byte*) malloc(getBitArrayBytes(_len)); // create LEDs dirty bits
_ledsDirty = (byte*) d_malloc(getBitArrayBytes(_len)); // create LEDs dirty bits
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA allocate memory ok");
if (_ledsDirty == nullptr) {
@@ -988,17 +941,50 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
setBitArray(_ledsDirty, _len, false); // reset dirty bits
if (mxconfig.double_buff == false) {
_ledBuffer = (CRGB*) calloc(_len, sizeof(CRGB)); // create LEDs buffer (initialized to BLACK)
// create LEDs buffer (initialized to BLACK), prefer DRAM if enough heap is available (faster in case global _pixels buffer is in PSRAM as not both will fit the cache)
_ledBuffer = static_cast<CRGB*>(allocate_buffer(_len * sizeof(CRGB), BFRALLOC_PREFER_DRAM | BFRALLOC_CLEAR));
}
}
PANEL_CHAIN_TYPE chainType = CHAIN_NONE; // default for quarter-scan panels that do not use chaining
// chained panels with cols and rows define need the virtual display driver, so do quarter-scan panels
if (chainLength > 1 && (_rows > 1 || _cols > 1) || bc.type == TYPE_HUB75MATRIX_QS) {
_isVirtual = true;
chainType = CHAIN_BOTTOM_LEFT_UP; // TODO: is there any need to support other chaining types?
DEBUGBUS_PRINTF_P(PSTR("Using virtual matrix: %ux%u panels of %ux%u pixels\n"), _cols, _rows, mxconfig.mx_width, mxconfig.mx_height);
}
else {
_isVirtual = false;
}
if (_isVirtual) {
virtualDisp = new VirtualMatrixPanel((*display), _rows, _cols, mxconfig.mx_width, mxconfig.mx_height, chainType);
virtualDisp->setRotation(0);
if (bc.type == TYPE_HUB75MATRIX_QS) {
switch(panelHeight) {
case 16:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_16PX_HIGH);
break;
case 32:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_32PX_HIGH);
break;
case 64:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_64PX_HIGH);
break;
default:
DEBUGBUS_PRINTLN("Unsupported height");
cleanup();
return;
}
}
}
if (_valid) {
_panelWidth = virtualDisp ? virtualDisp->width() : display->width(); // cache width - it will never change
}
DEBUGBUS_PRINT(F("MatrixPanel_I2S_DMA "));
DEBUGBUS_PRINTF("%sstarted, width=%u, %u pixels.\n", _valid? "":"not ", _panelWidth, _len);
DEBUGBUS_PRINTF_P(PSTR("%sstarted, width=%u, %u pixels.\n"), _valid? "":"not ", _panelWidth, _len);
if (_ledBuffer != nullptr) DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA LEDS buffer enabled."));
if (_ledsDirty != nullptr) DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA LEDS dirty bit optimization enabled."));
@@ -1009,8 +995,8 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
}
}
void __attribute__((hot)) BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c) {
if (!_valid || pix >= _len) return;
void IRAM_ATTR BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c) {
if (!_valid) return; // note: no need to check pix >= _len as that is checked in containsPixel()
// if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT
if (_ledBuffer) {
@@ -1028,8 +1014,8 @@ void __attribute__((hot)) BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c
uint8_t g = G(c);
uint8_t b = B(c);
if(virtualDisp != nullptr) {
int x = pix % _panelWidth;
if (virtualDisp != nullptr) {
int x = pix % _panelWidth; // TODO: check if using & and shift would be faster here, it limits to power-of-2 widths though
int y = pix / _panelWidth;
virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
} else {
@@ -1041,41 +1027,35 @@ void __attribute__((hot)) BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c
}
uint32_t BusHub75Matrix::getPixelColor(unsigned pix) const {
if (!_valid || pix >= _len) return IS_BLACK;
if (!_valid) return IS_BLACK; // note: no need to check pix >= _len as that is checked in containsPixel()
if (_ledBuffer)
return uint32_t(_ledBuffer[pix].scale8(_bri)) & 0x00FFFFFF; // scale8() is needed to mimic NeoPixelBus, which returns scaled-down colours
return uint32_t(_ledBuffer[pix]);
else
return getBitFromArray(_ledsDirty, pix) ? IS_DARKGREY: IS_BLACK; // just a hack - we only know if the pixel is black or not
}
void BusHub75Matrix::setBrightness(uint8_t b) {
_bri = b;
if (display) display->setBrightness(_bri);
display->setBrightness(_bri);
}
void BusHub75Matrix::show(void) {
if (!_valid) return;
display->setBrightness(_bri);
if (_ledBuffer) {
// write out buffered LEDs
bool isVirtualDisp = (virtualDisp != nullptr);
unsigned height = isVirtualDisp ? virtualDisp->height() : display->height();
unsigned height = _isVirtual ? virtualDisp->height() : display->height();
unsigned width = _panelWidth;
//while(!previousBufferFree) delay(1); // experimental - Wait before we allow any writing to the buffer. Stop flicker.
size_t pix = 0; // running pixel index
for (int y=0; y<height; y++) for (int x=0; x<width; x++) {
if (getBitFromArray(_ledsDirty, pix) == true) { // only repaint the "dirty" pixels
uint32_t c = uint32_t(_ledBuffer[pix]) & 0x00FFFFFF; // get RGB color, removing FastLED "alpha" component
uint8_t r = R(c);
uint8_t g = G(c);
uint8_t b = B(c);
if (isVirtualDisp) virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
else display->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
CRGB c = _ledBuffer[pix];
//c.nscale8_video(_bri); // apply brightness
if (_isVirtual) virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b);
else display->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b);
}
pix ++;
pix++;
}
setBitArray(_ledsDirty, _len, false); // buffer shown - reset all dirty bits
}
@@ -1084,16 +1064,18 @@ void BusHub75Matrix::show(void) {
void BusHub75Matrix::cleanup() {
if (display && _valid) display->stopDMAoutput(); // terminate DMA driver (display goes black)
_valid = false;
delay(30); // give some time to finish DMA
_panelWidth = 0;
deallocatePins();
DEBUGBUS_PRINTLN("HUB75 output ended.");
//if (virtualDisp != nullptr) delete virtualDisp; // warning: deleting object of polymorphic class type 'VirtualMatrixPanel' which has non-virtual destructor might cause undefined behavior
delete display;
DEBUGBUS_PRINTLN(F("HUB75 output ended."));
#ifndef CONFIG_IDF_TARGET_ESP32S3 // on ESP32-S3 deleting display/virtualDisp does not work and leads to crash (DMA issues), request reboot from user instead
if (virtualDisp != nullptr) delete virtualDisp; // note: in MM there is a warning to not do this but if using "NO_GFX" this is safe
if (display != nullptr) delete display;
display = nullptr;
virtualDisp = nullptr;
if (_ledBuffer != nullptr) free(_ledBuffer); _ledBuffer = nullptr;
if (_ledsDirty != nullptr) free(_ledsDirty); _ledsDirty = nullptr;
virtualDisp = nullptr; // note: when not using "NO_GFX" this causes a memory leak
#endif
if (_ledBuffer != nullptr) d_free(_ledBuffer); _ledBuffer = nullptr;
if (_ledsDirty != nullptr) d_free(_ledsDirty); _ledsDirty = nullptr;
}
void BusHub75Matrix::deallocatePins() {
@@ -1114,8 +1096,10 @@ size_t BusHub75Matrix::getPins(uint8_t* pinArray) const {
pinArray[0] = mxconfig.mx_width;
pinArray[1] = mxconfig.mx_height;
pinArray[2] = mxconfig.chain_length;
pinArray[3] = _rows;
pinArray[4] = _cols;
}
return 3;
return 5;
}
#endif
+8 -5
View File
@@ -166,7 +166,7 @@ class Bus {
inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; }
static inline std::vector<LEDType> getLEDTypes() { return {{TYPE_NONE, "", PSTR("None")}}; } // not used. just for reference for derived classes
static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : isHub75(type) ? 3 : is2Pin(type) + 1; } // credit @PaoloTK
static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : isHub75(type) ? 5 : is2Pin(type) + 1; } // credit @PaoloTK
static constexpr size_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); }
static constexpr bool hasRGB(uint8_t type) {
return !((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF);
@@ -395,12 +395,15 @@ class BusHub75Matrix : public Bus {
VirtualMatrixPanel *virtualDisp = nullptr;
HUB75_I2S_CFG mxconfig;
unsigned _panelWidth = 0;
CRGB *_ledBuffer = nullptr;
uint8_t _rows = 1; // panels per row
uint8_t _cols = 1; // panels per column
bool _isVirtual = false; // note: this is not strictly needed but there are padding bytes here anyway
CRGB *_ledBuffer = nullptr; // note: using uint32_t buffer is only 2% faster and not worth the extra RAM
byte *_ledsDirty = nullptr;
// workaround for missing constants on include path for non-MM
uint32_t IS_BLACK = 0x000000;
uint32_t IS_DARKGREY = 0x333333;
const int PIN_COUNT = 14;
static constexpr uint32_t IS_BLACK = 0x000000u;
static constexpr uint32_t IS_DARKGREY = 0x333333u;
static constexpr int PIN_COUNT = 14;
};
#endif
+10 -9
View File
@@ -49,13 +49,6 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
//long vid = doc[F("vid")]; // 2010020
#ifdef WLED_USE_ETHERNET
JsonObject ethernet = doc[F("eth")];
CJSON(ethernetType, ethernet["type"]);
// NOTE: Ethernet configuration takes priority over other use of pins
initEthernet();
#endif
JsonObject id = doc["id"];
getStringFromJson(cmDNS, id[F("mdns")], 33);
getStringFromJson(serverDescription, id[F("name")], 33);
@@ -125,6 +118,14 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
}
}
// https://github.com/wled/WLED/issues/5247
#ifdef WLED_USE_ETHERNET
JsonObject ethernet = doc[F("eth")];
CJSON(ethernetType, ethernet["type"]);
// NOTE: Ethernet configuration takes priority over other use of pins
initEthernet();
#endif
JsonObject ap = doc["ap"];
getStringFromJson(apSSID, ap[F("ssid")], 33);
getStringFromJson(apPass, ap["psk"] , 65); //normally not present due to security
@@ -506,8 +507,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
CJSON(strip.autoSegments, light[F("aseg")]);
CJSON(gammaCorrectVal, light["gc"]["val"]); // default 2.2
float light_gc_bri = light["gc"]["bri"];
float light_gc_col = light["gc"]["col"];
float light_gc_bri = light["gc"]["bri"] | 1.0f; // default to 1.0 (false)
float light_gc_col = light["gc"]["col"] | gammaCorrectVal; // default to gammaCorrectVal (true)
if (light_gc_bri > 1.0f) gammaCorrectBri = true;
else gammaCorrectBri = false;
if (light_gc_col > 1.0f) gammaCorrectCol = true;
+2 -2
View File
@@ -63,8 +63,8 @@ uint32_t WLED_O2_ATTR color_add(uint32_t c1, uint32_t c2, bool preserveCR) //121
* if using "video" method the resulting color will never become black unless it is already black
*/
uint32_t IRAM_ATTR color_fade(uint32_t c1, uint8_t amount, bool video) {
if (c1 == 0 || amount == 0) return 0; // black or no change
if (amount == 255) return c1;
if (c1 == BLACK || amount == 0) return 0; // black or full fade
if (amount == 255) return c1; // no change
uint32_t addRemains = 0;
if (!video) amount++; // add one for correct scaling using bitshifts
+3 -3
View File
@@ -6,9 +6,9 @@
* Readability defines and their associated numerical values + compile-time constants
*/
constexpr size_t FASTLED_PALETTE_COUNT = 7; // = sizeof(fastledPalettes) / sizeof(fastledPalettes[0]);
constexpr size_t GRADIENT_PALETTE_COUNT = 59; // = sizeof(gGradientPalettes) / sizeof(gGradientPalettes[0]);
constexpr size_t DYNAMIC_PALETTE_COUNT = 5; // 1-5 are dynamic palettes (1=random,2=primary,3=primary+secondary,4=primary+secondary+tertiary,5=primary+secondary(+tertiary if not black)
constexpr size_t FASTLED_PALETTE_COUNT = 7; // 6-12 = sizeof(fastledPalettes) / sizeof(fastledPalettes[0]);
constexpr size_t GRADIENT_PALETTE_COUNT = 59; // 13-72 = sizeof(gGradientPalettes) / sizeof(gGradientPalettes[0]);
constexpr size_t DYNAMIC_PALETTE_COUNT = 6; // 0- 5 = dynamic palettes (0=default(virtual),1=random,2=primary,3=primary+secondary,4=primary+secondary+tertiary,5=primary+secondary(+tertiary if not black)
constexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT + GRADIENT_PALETTE_COUNT; // total number of fixed palettes
#ifndef ESP8266
#define WLED_MAX_CUSTOM_PALETTES (255 - FIXED_PALETTE_COUNT) // allow up to 255 total palettes, user is warned about stability issues when adding more than 10
+14 -52
View File
@@ -741,21 +741,7 @@ function populateSegments(s)
let segp = `<div id="segp${i}" class="sbs">`+
`<i class="icons slider-icon pwr ${inst.on ? "act":""}" id="seg${i}pwr" title="Power" onclick="setSegPwr(${i})">&#xe08f;</i>`+
`<div class="sliderwrap il" title="Opacity/Brightness">`+
`<input id="seg${i}bri" class="noslide" onchange="setSegProp(${i},'bri')" oninput="updateTrail(this)" max="255" min="1" type="range" value="${inst.bri}" />`+
`<div class="sliderdisplay"></div>`+
`</div>`+
`</div>`;
let zoom = `<div id="segzm${i}" class="lbl-l">`+
`Zoom<br>`+
`<div class="sliderwrap il" title="Zoom amount">`+
`<input id="seg${i}zA" class="noslide" onchange="setSegProp(${i},'zA')" oninput="updateTrail(this)" max="15" min="0" type="range" value="${inst.zA}" />`+
`<div class="sliderdisplay"></div>`+
`</div>`+
`</div>`;
let rotate =`<div id="segrt${i}" class="lbl-l">`+
`Rotation<br>`+
`<div class="sliderwrap il" title="Rotation speed">`+
`<input id="seg${i}rS" class="noslide" onchange="setSegProp(${i},'rS')" oninput="updateTrail(this)" max="15" min="0" type="range" value="${inst.rS}" />`+
`<input id="seg${i}bri" class="noslide" onchange="setSegBri(${i})" oninput="updateTrail(this)" max="255" min="1" type="range" value="${inst.bri}" />`+
`<div class="sliderdisplay"></div>`+
`</div>`+
`</div>`;
@@ -764,16 +750,16 @@ function populateSegments(s)
let staY = inst.startY;
let stoY = inst.stopY;
let isMSeg = isM && staX<mw*mh; // 2D matrix segment
let rvXck = `<label class="check revchkl">Reverse ${isM?'':'direction'}<input type="checkbox" id="seg${i}rev" onchange="setSegProp(${i},'rev')" ${inst.rev?"checked":""}><span class="checkmark"></span></label>`;
let miXck = `<label class="check revchkl">Mirror<input type="checkbox" id="seg${i}mi" onchange="setSegProp(${i},'mi')" ${inst.mi?"checked":""}><span class="checkmark"></span></label>`;
let rvXck = `<label class="check revchkl">Reverse ${isM?'':'direction'}<input type="checkbox" id="seg${i}rev" onchange="setRev(${i})" ${inst.rev?"checked":""}><span class="checkmark"></span></label>`;
let miXck = `<label class="check revchkl">Mirror<input type="checkbox" id="seg${i}mi" onchange="setMi(${i})" ${inst.mi?"checked":""}><span class="checkmark"></span></label>`;
let rvYck = "", miYck ="";
let smpl = simplifiedUI ? 'hide' : '';
if (isMSeg) {
rvYck = `<label class="check revchkl">Reverse<input type="checkbox" id="seg${i}rY" onchange="setSegProp(${i},'rY')" ${inst.rY?"checked":""}><span class="checkmark"></span></label>`;
miYck = `<label class="check revchkl">Mirror<input type="checkbox" id="seg${i}mY" onchange="setSegProp(${i},'mY')" ${inst.mY?"checked":""}><span class="checkmark"></span></label>`;
rvYck = `<label class="check revchkl">Reverse<input type="checkbox" id="seg${i}rY" onchange="setRevY(${i})" ${inst.rY?"checked":""}><span class="checkmark"></span></label>`;
miYck = `<label class="check revchkl">Mirror<input type="checkbox" id="seg${i}mY" onchange="setMiY(${i})" ${inst.mY?"checked":""}><span class="checkmark"></span></label>`;
}
let map2D = `<div id="seg${i}map2D" data-map="map2D" data-fx="${inst.fx}" class="lbl-s hide">Expand 1D FX<br>`+
`<div class="sel-p"><select class="sel-p" id="seg${i}m12" onchange="setSegProp(${i},'m12')">`+
let map2D = `<div id="seg${i}map2D" data-map="map2D" class="lbl-s hide">Expand 1D FX<br>`+
`<div class="sel-p"><select class="sel-p" id="seg${i}m12" onchange="setM12(${i})">`+
`<option value="0" ${inst.m12==0?' selected':''}>Pixels</option>`+
`<option value="1" ${inst.m12==1?' selected':''}>Bar</option>`+
`<option value="2" ${inst.m12==2?' selected':''}>Arc</option>`+
@@ -782,7 +768,7 @@ function populateSegments(s)
`</select></div>`+
`</div>`;
let blend = `<div class="lbl-l">Blend mode<br>`+
`<div class="sel-p"><select class="sel-ple" id="seg${i}bm" onchange="setSegProp(${i},'bm')">`+
`<div class="sel-p"><select class="sel-ple" id="seg${i}bm" onchange="setBm(${i})">`+
`<option value="0" ${inst.bm==0?' selected':''}>Top/Default</option>`+
`<option value="1" ${inst.bm==1?' selected':''}>Bottom/None</option>`+
`<option value="2" ${inst.bm==2?' selected':''}>Add</option>`+
@@ -802,7 +788,7 @@ function populateSegments(s)
`</select></div>`+
`</div>`;
let sndSim = `<div data-snd="si" class="lbl-s hide">Sound sim<br>`+
`<div class="sel-p"><select class="sel-p" id="seg${i}si" onchange="setSegProp(${i},'si')">`+
`<div class="sel-p"><select class="sel-p" id="seg${i}si" onchange="setSi(${i})">`+
`<option value="0" ${inst.si==0?' selected':''}>BeatSin</option>`+
`<option value="1" ${inst.si==1?' selected':''}>WeWillRockYou</option>`+
`<option value="2" ${inst.si==2?' selected':''}>10/13</option>`+
@@ -858,14 +844,12 @@ function populateSegments(s)
`<div class="h bp" id="seg${i}len"></div>`+
blend +
(!isMSeg ? rvXck : '') +
(isMSeg?zoom:'')+
(isMSeg?rotate:'')+
(isMSeg&&stoY-staY>1&&stoX-staX>1 ? map2D : '') +
(s.AudioReactive && s.AudioReactive.on ? "" : sndSim) +
`<label class="check revchkl" id="seg${i}lbtm">`+
(isMSeg?'Transpose':'Mirror effect') + (isMSeg ?
'<input type="checkbox" id="seg'+i+'tp" onchange="setSegProp('+i+',\'tp\')" '+(inst.tp?"checked":"")+'>':
'<input type="checkbox" id="seg'+i+'mi" onchange="setSegProp('+i+',\'mi\')" '+(inst.mi?"checked":"")+'>') +
'<input type="checkbox" id="seg'+i+'tp" onchange="setTp('+i+')" '+(inst.tp?"checked":"")+'>':
'<input type="checkbox" id="seg'+i+'mi" onchange="setMi('+i+')" '+(inst.mi?"checked":"")+'>') +
`<span class="checkmark"></span>`+
`</label>`+
`<div class="del">`+
@@ -886,8 +870,6 @@ function populateSegments(s)
if (!gId(`seg${i}`)) continue;
updateLen(i);
updateTrail(gId(`seg${i}bri`));
let r = gId(`seg${i}rS`); if (r) updateTrail(r);
let z = gId(`seg${i}zA`); if (z) updateTrail(z);
gId(`segr${i}`).classList.add("hide");
}
if (segCount < 2) {
@@ -2283,14 +2265,6 @@ function delSeg(s)
requestJson(obj);
}
function setSegProp(s,p)
{
let o = gId(`seg${s}${p}`);
let val = o.type === "checkbox" ? o.checked : parseInt(o.value);
var obj = {"seg": {"id": s, [p]: val}};
requestJson(obj);
}
/*
function setRev(s)
{
var rev = gId(`seg${s}rev`).checked;
@@ -2346,7 +2320,7 @@ function setTp(s)
var obj = {"seg": {"id": s, "tp": tp}};
requestJson(obj);
}
*/
function setGrp(s, g)
{
event.preventDefault();
@@ -2354,32 +2328,20 @@ function setGrp(s, g)
var obj = {"seg": {"id": s, "set": g}};
requestJson(obj);
}
/*
function setZoom(s)
{
var obj = {"seg": {"id": s, "zA": parseInt(gId(`seg${s}za`).value)}};
requestJson(obj);
}
function setRotation(s)
{
var obj = {"seg": {"id": s, "rS": parseInt(gId(`seg${s}rs`).value)}};
requestJson(obj);
}
*/
function setSegPwr(s)
{
var pwr = gId(`seg${s}pwr`).classList.contains('act');
var obj = {"seg": {"id": s, "on": !pwr}};
requestJson(obj);
}
/*
function setSegBri(s)
{
var obj = {"seg": {"id": s, "bri": parseInt(gId(`seg${s}bri`).value)}};
requestJson(obj);
}
*/
function tglFreeze(s=null)
{
var obj = {"seg": {"frz": "t"}}; // toggle
+15 -4
View File
@@ -397,11 +397,19 @@ button, .btn {
</div>
</div>
<hr>
<div class="ed active">
<div>
<h3>Video Lab</h3>
<div><small>Stream video and generate animated GIFs (beta)</small></div>
<button class="btn" id="t2" style="display:none"></button>
</div>
</div>
<hr>
<div class="ed active">
<div>
<h3>PIXEL MAGIC Tool</h3>
<div><small>Legacy pixel art editor</small></div>
<button class="btn" id="t2" style="display:none"></button>
<button class="btn" id="t3" style="display:none"></button>
</div>
</div>
<hr>
@@ -416,6 +424,8 @@ button, .btn {
<script>
const d=document,gId=i=>d.getElementById(i),cE=t=>d.createElement(t);
const imgageFX = 53; // image effect number
const txtFX = 122; // scrolling text effect number
/* canvases */
const cv=gId('cv'),cx=cv.getContext('2d',{willReadFrequently:true});
@@ -437,7 +447,8 @@ let fL; // file list
await segLoad(); // load available segments
await flU(); // update file list
toolChk('pixelpaint.htm','t1'); // update buttons of additional tools
toolChk('pxmagic.htm','t2');
toolChk('videolab.htm','t2');
toolChk('pxmagic.htm','t3');
await fsMem(); // show file system memory info
})();
@@ -467,7 +478,7 @@ function segLoad(){
if(j.seg&&j.seg.length){
j.seg.forEach(({id,n,start,stop,startY,stopY,fx})=>{
const w=stop-start,h=(stopY-startY)||1;
const t = (n || `Segment ${id}`) + (h>1 ? ` (${w}x${h})` : ` (${w}px)`) + (fx===53 ? ' [Image]' : (fx===122 ? ' [Scrolling Text]' : ''));
const t = (n || `Segment ${id}`) + (h>1 ? ` (${w}x${h})` : ` (${w}px)`) + (fx===imgageFX ? ' [Image]' : (fx===txtFX ? ' [Scrolling Text]' : ''));
const o=new Option(t,id);
o.dataset.w=w; o.dataset.h=h; o.dataset.fx=fx||0;
s1.add(o); // gif tool
@@ -488,7 +499,7 @@ function segLoad(){
function curImgSeg(){
const sel=gId('seg');
for(let i=0;i<sel.options.length;i++){
if(parseInt(sel.options[i].dataset.fx)===53) return parseInt(sel.options[i].value);
if(parseInt(sel.options[i].dataset.fx)===imgageFX) return parseInt(sel.options[i].value);
}
return null;
}
+22 -5
View File
@@ -122,6 +122,20 @@
d.Sf.data.value = '';
e.preventDefault();
if (!pinsOK()) {e.stopPropagation();return false;} // Prevent form submission and contact with server
// validate HUB75 panel config
let LTs = d.Sf.querySelectorAll("#mLC select[name^=LT]");
for (let i=0; i<LTs.length; i++) {
let n = chrID(i);
let t = parseInt(LTs[i].value);
if (isHub75(t)) {
let p = parseInt(d.Sf["L2"+n].value)||1, r = parseInt(d.Sf["L3"+n].value)||1, c = parseInt(d.Sf["L4"+n].value)||1, h = parseInt(d.Sf["L1"+n].value)||1;
if (r*c !== p) {alert(`HUB75 error: panels≠rows×cols`); e.stopPropagation(); return false;}
if (h >= 64 && p > 1) {alert(`HUB75 error: height >= 64, only single panel allowed`); e.stopPropagation(); return false;}
if(isS3()) {
alert("HUB75 changes require a reboot"); // TODO: only throw this if panel config changed?
}
}
};
if (bquot > 200) {var msg = "Too many LEDs! Can't handle that!"; alert(msg); e.stopPropagation(); return false;}
else {
if (bquot > 80) {var msg = "Memory usage is high, reboot recommended!\n\rSet transitions to 0 to save memory.";
@@ -256,17 +270,19 @@
p0d = "IP address:";
break;
case 'V': // virtual/non-GPIO based
p0d = "Config:"
p0d = "Config:";
break;
case 'H': // HUB75
p0d = "Panel size (width x height), Panel count:"
p0d = "Panel (width x height):";
gId("p2d"+n).innerHTML = "<br>No. of Panels:";
gId("p3d"+n).innerText = "rows x cols:";
break;
}
gId("p0d"+n).innerText = p0d;
gId("p1d"+n).innerText = p1d;
gId("off"+n).innerText = off;
// secondary pins show/hide (type string length is equivalent to number of pins used; except for network and on/off)
let pins = Math.max(gT(t).t.length,1) + 3*isNet(t) + 2*isHub75(t); // fixes network pins to 4
let pins = Math.max(gT(t).t.length,1) + 3*isNet(t) + 4*isHub75(t); // fixes network pins to 4
for (let p=1; p<5; p++) {
var LK = d.Sf["L"+p+n];
if (!LK) continue;
@@ -376,10 +392,11 @@
LC.style.color="#fff";
return; // do not check conflicts
}
else if (isHub75(t) && nm=="L2") {
// Chain length aka Panel Count
else if (isHub75(t) && (nm=="L2" || nm=="L3" || nm=="L4")) {
// chain length aka panel count (L2), cols(L3), rows(L4)
LC.max = 4;
LC.min = 1;
if (LC.value === "") LC.value = 1; // default to 1
LC.style.color="#fff";
return; // do not check conflicts
}
+1
View File
@@ -165,6 +165,7 @@
<option value="21">MX-CST</option>
<option value="22">PKT (Pakistan)</option>
<option value="23">BRT (Brasília)</option>
<option value="24">AWST (Perth)</option>
</select><br>
UTC offset: <input name="UO" type="number" min="-65500" max="65500" required> seconds (max. 18 hours)<br>
Current local time is <span class="times">unknown</span>.<br>
+1
View File
@@ -200,6 +200,7 @@ void getTimeString(char* out);
bool checkCountdown();
void setCountdown();
byte weekdayMondayFirst();
bool isTodayInDateRange(byte monthStart, byte dayStart, byte monthEnd, byte dayEnd);
void checkTimers();
void calculateSunriseAndSunset();
void setTimeFromAPI(uint32_t timein);
+3 -1
View File
@@ -38,7 +38,7 @@ void closeFile() {
DEBUGFS_PRINT(F("Close -> "));
uint32_t s = millis();
#endif
f.close();
f.close(); // "if (f)" check is aleady done inside f.close(), and f cannot be nullptr -> no need for double checking before closing the file handle.
DEBUGFS_PRINTF("took %lu ms\n", millis() - s);
doCloseFile = false;
}
@@ -271,6 +271,8 @@ bool writeObjectToFile(const char* file, const char* key, const JsonDocument* co
s = millis();
#endif
if (doCloseFile) closeFile(); // This prevents the loss of file data that is still cached in the File object.
size_t pos = 0;
char fileName[129]; strncpy_P(fileName, file, 128); fileName[128] = 0; //use PROGMEM safe copy as FS.open() does not
f = WLED_FS.open(fileName, WLED_FS.exists(fileName) ? "r+" : "w+");
+19 -32
View File
@@ -35,9 +35,6 @@ namespace {
bool check1;
bool check2;
bool check3;
uint8_t blendMode;
uint8_t zoomAmount;
uint8_t rotateSpeed;
} SegmentCopy;
uint8_t differs(const Segment& b, const SegmentCopy& a) {
@@ -60,9 +57,6 @@ namespace {
if (a.check3 != b.check3) d |= SEG_DIFFERS_FX;
if (a.startY != b.startY) d |= SEG_DIFFERS_BOUNDS;
if (a.stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS;
if (a.blendMode != b.blendMode) d |= SEG_DIFFERS_OPT;
if (a.zoomAmount != b.zoomAmount) d |= SEG_DIFFERS_OPT;
if (a.rotateSpeed != b.rotateSpeed) d |= SEG_DIFFERS_OPT;
//bit pattern: (msb first)
// set:2, sound:2, mapping:3, transposed, mirrorY, reverseY, [reset,] paused, mirrored, on, reverse, [selected]
@@ -114,10 +108,7 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
seg.custom3,
seg.check1,
seg.check2,
seg.check3,
seg.blendMode,
seg.zoomAmount,
seg.rotateSpeed
seg.check3
};
int start = elem["start"] | seg.start;
@@ -161,8 +152,6 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
uint16_t spc = elem[F("spc")] | seg.spacing;
uint16_t of = seg.offset;
uint8_t soundSim = elem["si"] | seg.soundSim;
uint8_t rotateSpeed = elem["rS"] | seg.rotateSpeed;
uint8_t zoomAmount = elem["zA"] | seg.zoomAmount;
uint8_t map1D2D = elem["m12"] | seg.map1D2D;
uint8_t set = elem[F("set")] | seg.set;
bool selected = getBoolVal(elem["sel"], seg.selected);
@@ -220,7 +209,7 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
// JSON "col" array can contain the following values for each of segment's colors (primary, background, custom):
// "col":[int|string|object|array, int|string|object|array, int|string|object|array]
// int = Kelvin temperature or 0 for black
// string = hex representation of [WW]RRGGBB
// string = hex representation of [WW]RRGGBB or "r" for random color
// object = individual channel control {"r":0,"g":127,"b":255,"w":255}, each being optional (valid to send {})
// array = direct channel values [r,g,b,w] (w element being optional)
int rgbw[] = {0,0,0,0};
@@ -244,6 +233,9 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
if (kelvin == 0) seg.setColor(i, 0);
if (kelvin > 0) colorKtoRGB(kelvin, brgbw);
colValid = true;
} else if (hexCol[0] == 'r' && hexCol[1] == '\0') { // Random colors via JSON API in Segment object like col=["r","r","r"] · Issue #4996
setRandomColor(brgbw);
colValid = true;
} else { //HEX string, e.g. "FFAA00"
colValid = colorFromHexString(brgbw, hexCol);
}
@@ -280,8 +272,6 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
}
#endif
seg.rotateSpeed = constrain(rotateSpeed, 0, 15);
seg.zoomAmount = constrain(zoomAmount, 0, 15);
seg.set = constrain(set, 0, 3);
seg.soundSim = constrain(soundSim, 0, 3);
seg.selected = selected;
@@ -644,8 +634,6 @@ static void serializeSegment(JsonObject& root, const Segment& seg, byte id, bool
root["si"] = seg.soundSim;
root["m12"] = seg.map1D2D;
root["bm"] = seg.blendMode;
root["rS"] = seg.rotateSpeed;
root["zA"] = seg.zoomAmount;
}
void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segmentBounds, bool selectedSegmentsOnly)
@@ -960,26 +948,25 @@ void serializePalettes(JsonObject root, int page)
{
byte tcp[72];
#ifdef ESP8266
int itemPerPage = 5;
constexpr int itemPerPage = 5;
#else
int itemPerPage = 8;
constexpr int itemPerPage = 8;
#endif
int customPalettesCount = customPalettes.size();
int palettesCount = getPaletteCount() - customPalettesCount; // palettesCount is number of palettes, not palette index
const int customPalettesCount = customPalettes.size();
const int palettesCount = FIXED_PALETTE_COUNT; // palettesCount is number of palettes, not palette index
int maxPage = (palettesCount + customPalettesCount -1) / itemPerPage;
const int maxPage = (palettesCount + customPalettesCount) / itemPerPage;
if (page > maxPage) page = maxPage;
int start = itemPerPage * page;
int end = start + itemPerPage;
if (end > palettesCount + customPalettesCount) end = palettesCount + customPalettesCount;
const int start = itemPerPage * page;
int end = min(start + itemPerPage, palettesCount + customPalettesCount);
root[F("m")] = maxPage; // inform caller how many pages there are
JsonObject palettes = root.createNestedObject("p");
for (int i = start; i <= end; i++) {
JsonArray curPalette = palettes.createNestedArray(String(i<=palettesCount ? i : 255 - (i - (palettesCount + 1))));
for (int i = start; i < end; i++) {
JsonArray curPalette = palettes.createNestedArray(String(i >= palettesCount ? 255 - i + palettesCount : i));
switch (i) {
case 0: //default palette
setPaletteColors(curPalette, PartyColors_p);
@@ -1008,12 +995,12 @@ void serializePalettes(JsonObject root, int page)
curPalette.add("c1");
break;
default:
if (i > palettesCount)
setPaletteColors(curPalette, customPalettes[i - (palettesCount + 1)]);
else if (i < 13) // palette 6 - 12, fastled palettes
setPaletteColors(curPalette, *fastledPalettes[i-6]);
if (i >= palettesCount) // custom palettes
setPaletteColors(curPalette, customPalettes[i - palettesCount]);
else if (i < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) // palette 6 - 12, fastled palettes
setPaletteColors(curPalette, *fastledPalettes[i - DYNAMIC_PALETTE_COUNT]);
else {
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[i - 13])), 72);
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[i - (DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT)])), sizeof(tcp));
setPaletteColors(curPalette, tcp);
}
break;
+9 -7
View File
@@ -239,7 +239,7 @@ bool initEthernet()
(eth_phy_type_t) es.eth_type,
(eth_clock_mode_t) es.eth_clk_mode
)) {
DEBUG_PRINTLN(F("initC: ETH.begin() failed"));
DEBUG_PRINTLN(F("initE: ETH.begin() failed"));
// de-allocate the allocated pins
for (managed_pin_type mpt : pinsToAllocate) {
PinManager::deallocatePin(mpt.pin, PinOwner::Ethernet);
@@ -247,8 +247,15 @@ bool initEthernet()
return false;
}
// https://github.com/wled/WLED/issues/5247
if (multiWiFi[0].staticIP != (uint32_t)0x00000000 && multiWiFi[0].staticGW != (uint32_t)0x00000000) {
ETH.config(multiWiFi[0].staticIP, multiWiFi[0].staticGW, multiWiFi[0].staticSN, dnsAddress);
} else {
ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);
}
successfullyConfiguredEthernet = true;
DEBUG_PRINTLN(F("initC: *** Ethernet successfully configured! ***"));
DEBUG_PRINTLN(F("initE: *** Ethernet successfully configured! ***"));
return true;
}
#endif
@@ -406,11 +413,6 @@ void WiFiEvent(WiFiEvent_t event)
if (!apActive) {
WiFi.disconnect(true); // disable WiFi entirely
}
if (multiWiFi[0].staticIP != (uint32_t)0x00000000 && multiWiFi[0].staticGW != (uint32_t)0x00000000) {
ETH.config(multiWiFi[0].staticIP, multiWiFi[0].staticGW, multiWiFi[0].staticSN, dnsAddress);
} else {
ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);
}
// convert the "serverDescription" into a valid DNS hostname (alphanumeric)
char hostname[64];
prepareHostname(hostname);
+6 -1
View File
@@ -37,8 +37,9 @@ Timezone* tz;
#define TZ_MX_CENTRAL 21
#define TZ_PAKISTAN 22
#define TZ_BRASILIA 23
#define TZ_AUSTRALIA_WESTERN 24
#define TZ_COUNT 24
#define TZ_COUNT 25
#define TZ_INIT 255
byte tzCurrent = TZ_INIT; //uninitialized
@@ -140,6 +141,10 @@ static const std::pair<TimeChangeRule, TimeChangeRule> TZ_TABLE[] PROGMEM = {
/* TZ_BRASILIA */ {
{Last, Sun, Mar, 1, -180}, //Brasília Standard Time = UTC - 3 hours
{Last, Sun, Mar, 1, -180}
},
/* TZ_AUSTRALIA_WESTERN */ {
{Last, Sun, Mar, 1, 480}, //AWST = UTC + 8 hours
{Last, Sun, Mar, 1, 480} //AWST = UTC + 8 hours (no DST)
}
};
+2 -1
View File
@@ -89,7 +89,8 @@ int16_t loadPlaylist(JsonObject playlistObj, byte presetId) {
it++;
}
}
for (int i = it; i < playlistLen; i++) playlistEntries[i].dur = playlistEntries[it -1].dur;
if (it > 0) // should never happen but just in case
for (int i = it; i < playlistLen; i++) playlistEntries[i].dur = playlistEntries[it -1].dur;
it = 0;
JsonArray tr = playlistObj[F("transition")];
+1 -1
View File
@@ -279,7 +279,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
int offset = i < 10 ? '0' : 'A' - 10;
char bt[4] = "BT"; bt[2] = offset+i; bt[3] = 0; // button pin (use A,B,C,... if WLED_MAX_BUTTONS>10)
char be[4] = "BE"; be[2] = offset+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10)
int hw_btn_pin = request->arg(bt).toInt();
int hw_btn_pin = request->hasArg(bt) ? request->arg(bt).toInt() : -1;
if (i >= buttons.size()) buttons.emplace_back(hw_btn_pin, request->arg(be).toInt()); // add button to vector
else {
buttons[i].pin = hw_btn_pin;
+2 -2
View File
@@ -334,8 +334,8 @@ static void parseNotifyPacket(const uint8_t *udpIn) {
selseg.custom2 = udpIn[30+ofs];
selseg.custom3 = udpIn[31+ofs] & 0x1F;
selseg.check1 = (udpIn[31+ofs]>>5) & 0x1;
selseg.check1 = (udpIn[31+ofs]>>6) & 0x1;
selseg.check1 = (udpIn[31+ofs]>>7) & 0x1;
selseg.check2 = (udpIn[31+ofs]>>6) & 0x1;
selseg.check3 = (udpIn[31+ofs]>>7) & 0x1;
}
}
if (receiveSegmentBounds) {
+3 -3
View File
@@ -213,7 +213,7 @@ void releaseJSONBufferLock()
// extracts effect mode (or palette) name from names serialized string
// caller must provide large enough buffer for name (including SR extensions)!
// caller must provide large enough buffer for name (including SR extensions)! maxLen is (buffersize - 1)
uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen)
{
if (src == JSON_mode_names || src == nullptr) {
@@ -235,7 +235,7 @@ uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLe
if (src == JSON_palette_names && mode > 255-customPalettes.size()) {
snprintf_P(dest, maxLen, PSTR("~ Custom %d ~"), 255-mode);
dest[maxLen-1] = '\0';
dest[maxLen] = '\0';
return strlen(dest);
}
@@ -336,7 +336,7 @@ uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxL
case 0: strncpy_P(dest, PSTR("FX Speed"), maxLen); break;
case 1: strncpy_P(dest, PSTR("FX Intensity"), maxLen); break;
}
dest[maxLen] = '\0'; // strncpy does not necessarily null terminate string
dest[maxLen-1] = '\0'; // strncpy does not necessarily null terminate string
}
}
return strlen(dest);