mirror of
https://github.com/wled/WLED.git
synced 2026-06-29 00:04:12 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc1a14b585 | |||
| a024935c39 | |||
| 10df03e564 | |||
| 7a9e7f9c41 | |||
| 45acb44a36 | |||
| 8a3cb46007 | |||
| ba5cf9cd3c | |||
| d1d9dec402 | |||
| 6e9dc181e1 | |||
| fe3a158264 | |||
| e2de1af6f4 | |||
| 22ab62d090 | |||
| 254e0099ca | |||
| 8433fd24c3 | |||
| 32daa03941 | |||
| 4749247389 | |||
| a870474b49 | |||
| 97493b1af8 | |||
| 8e27fe4c0c | |||
| 407c9fd72f | |||
| e95450b318 | |||
| b556da8b36 | |||
| 5cfb6f984b | |||
| 60e1698ed2 | |||
| 60b2c3bb54 | |||
| 979e3fd1f7 | |||
| 600b9b4d8e | |||
| 787d8a7342 | |||
| 46e77ea203 | |||
| fa868568af | |||
| 1c2cacf185 | |||
| f1f067e93a | |||
| 8a2a7054ab | |||
| e4b0ee0672 | |||
| b821e20fd6 |
+1
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 0–255),
|
||||
* 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 0–255), 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 (custom1–custom3), updates segments and UI.
|
||||
*
|
||||
* Increases or decreases the selected custom parameter, clamps the resulting value to 0–255,
|
||||
* 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 0–255 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 0–255, 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 0–255,
|
||||
* 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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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})"></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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user