Compare commits

..

27 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
708baf1ed7 Fix pr-merge.yaml to include PR title and link in Discord notifications
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-08-28 06:51:22 +00:00
copilot-swe-agent[bot]
4155a6bc23 Initial plan 2025-08-28 06:45:52 +00:00
netmindz
c92f0a9d90 Merge pull request #4878 from willmmiles/fix-pwm
Fix operation of length 1 strips, such as PWM LEDs
2025-08-28 06:31:39 +01:00
Will Miles
5fa901c37c Fix operation of length 1 strips, such as PWM LEDs
This reverts commit e5ba97bbe2.
2025-08-27 22:28:24 -04:00
netmindz
1fb9eb771e Merge pull request #4856 from wled/copilot/fix-4855
Fix GitHub workflow secret access from forked PRs
2025-08-21 00:23:02 +01:00
netmindz
dee581f58d Merge pull request #4858 from wled/copilot/fix-4857
Add comprehensive GitHub Copilot instructions for WLED development workflow
2025-08-21 00:18:41 +01:00
copilot-swe-agent[bot]
7943b00017 Add comprehensive GitHub Copilot instructions for WLED development
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-08-20 23:07:03 +00:00
copilot-swe-agent[bot]
cd8ddb81e1 Fix GitHub workflow secret access from forked PRs
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-08-20 22:58:29 +00:00
copilot-swe-agent[bot]
890860ebf6 Initial plan 2025-08-20 22:55:59 +00:00
copilot-swe-agent[bot]
624042d97e Initial plan 2025-08-20 22:54:33 +00:00
Will Miles
3b5c6ca284 Fix bootloop if config is reset (#4852)
* Fix bootloop if config missing/reset

Can't reset the config if there's nothing to reset!

* ESP8266: Commit ACTIONT_TRACKER

* Use consistent naming for backups and reset cfgs

Use 'rst.cfg.json' instead of 'cfg.json.rst.json' for configs that were
reset.

* Add a little more PSTR to bootloop handling
2025-08-20 07:37:14 +02:00
netmindz
dcc1fbc96e Merge pull request #4846 from Arcitec/improve-version-info
Make version information consistent across update interfaces
2025-08-19 09:20:21 +01:00
Arcitec
7865985eeb Make version information consistent across update interfaces
The duplication of logic and the formatting differences between the "OTA Updates" and "Security & Updates" pages made it very difficult to find the exact version details.

With this change, both update-pages now share the same consistent and detailed formatting, making it easy for users to identify which exact version and binary of WLED they've installed.

The version format has also been improved to make it much easier to understand.
2025-08-17 22:45:20 +02:00
netmindz
7285efebca Merge pull request #4836 from netmindz/V4-cleanup
Remove old V3 IDF
2025-08-16 14:29:27 +01:00
Will Tatam
af2d46c30d Set VERSION 2025-08-16 12:23:21 +01:00
Damian Schneider
f4d89c4196 add IDF V3 support for bootloop detection 2025-08-16 08:45:41 +02:00
Damian Schneider
c9c442a933 Bootloop detection & recovery (#4793)
* added boot loop detection and config backup
* automatic OTA rollback if loading backup does not fix it
* added new file handling functions
* adding verification of json files, added config restore at bootup if broken
* added function to compare contents of two files for future use (currently not used)
2025-08-15 20:43:04 +02:00
TripleWhy
b8b59b2bb1 Make rainbow effects more colorful (#3681)
* Make color_wheel rotate in HSV sapce instead of linearly interpolating R->G->B
* Remove the rainbow wheel option, as that is the same as the rainbow palette
* Use hsv2rgb for color_wheel

This is the current result of the discussion in https://github.com/wled/WLED/pull/3681
2025-08-15 20:19:18 +02:00
Will Tatam
c33e303323 Fix eth build, update lib_deps for v4 2025-08-14 17:56:17 +01:00
Will Tatam
caf3c7a2f9 Remove old V3 IDF 2025-08-14 17:50:21 +01:00
Will Tatam
c8d8ab020e ARDUINO_USB_CDC_ON_BOOT should not be in common esp32_idf_V4 2025-08-14 14:52:49 +01:00
Will Tatam
297d5ced75 Use esp32_idf_V4.build_flags and esp32_idf_V4.lib_deps for all V4 builds 2025-08-14 14:52:49 +01:00
Will Miles
3f90366aa8 Merge pull request #4796 from willmmiles/asynctcp-update
Update AsyncTCP (and AsyncWebServer)
2025-08-09 12:05:13 -04:00
Damian Schneider
e3653baf74 Segment fixes (#4811)
* Pulling in proper segment memory handling&fixes from @blazoncek dev branch
2025-08-07 21:11:10 +02:00
Will Miles
f74d1459b9 Downtune AsyncTCP stack size
We downtuned the stack usage of AsyncTCP, and at some point in the
history of our fork, this got folded in to the default.  Re-apply the
stack size we've been using and recover that RAM.
2025-08-04 14:21:11 -04:00
Will Miles
e374c7ae55 Update to AsyncTCP 3.4.7
Bugfix on 3.4.6
2025-08-02 15:49:47 -04:00
Will Miles
9e4675ef46 Update AsyncWebServer and AsyncTCP
This should fix (or at least improve) some of the crash cases under
excessive web server load.
2025-07-30 22:17:44 -04:00
24 changed files with 851 additions and 448 deletions

138
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,138 @@
# WLED - ESP32/ESP8266 LED Controller Firmware
WLED is a fast and feature-rich implementation of an ESP32 and ESP8266 webserver to control NeoPixel (WS2812B, WS2811, SK6812) LEDs and SPI-based chipsets. The project consists of C++ firmware for microcontrollers and a modern web interface.
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
## Working Effectively
### Initial Setup
- Install Node.js 20+ (specified in `.nvmrc`): Check your version with `node --version`
- Install dependencies: `npm install` (takes ~5 seconds)
- Install PlatformIO for hardware builds: `pip install -r requirements.txt` (takes ~60 seconds)
### Build and Test Workflow
- **ALWAYS build web UI first**: `npm run build` -- takes 3 seconds. NEVER CANCEL.
- **Run tests**: `npm test` -- takes 40 seconds. NEVER CANCEL. Set timeout to 2+ minutes.
- **Development mode**: `npm run dev` -- monitors file changes and auto-rebuilds web UI
- **Hardware firmware build**: `pio run -e [environment]` -- takes 15+ minutes. NEVER CANCEL. Set timeout to 30+ minutes.
### Build Process Details
The build has two main phases:
1. **Web UI Generation** (`npm run build`):
- Processes files in `wled00/data/` (HTML, CSS, JS)
- Minifies and compresses web content
- Generates `wled00/html_*.h` files with embedded web content
- **CRITICAL**: Must be done before any hardware build
2. **Hardware Compilation** (`pio run`):
- Compiles C++ firmware for various ESP32/ESP8266 targets
- Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`
- List all targets: `pio run --list-targets`
## Validation and Testing
### Web UI Testing
- **ALWAYS validate web UI changes manually**:
- Start local server: `cd wled00/data && python3 -m http.server 8080`
- Open `http://localhost:8080/index.htm` in browser
- Test basic functionality: color picker, effects, settings pages
- **Check for JavaScript errors** in browser console
### Code Validation
- **No automated linting configured** - follow existing code style in files you edit
- **Code style**: Use tabs for web files (.html/.css/.js), spaces (2 per level) for C++ files
- **C++ formatting available**: `clang-format` is installed but not in CI
- **Always run tests before finishing**: `npm test`
### Manual Testing Scenarios
After making changes to web UI, always test:
- **Load main interface**: Verify index.htm loads without errors
- **Navigation**: Test switching between main page and settings pages
- **Color controls**: Verify color picker and brightness controls work
- **Effects**: Test effect selection and parameter changes
- **Settings**: Test form submission and validation
## Common Tasks
### Repository Structure
```
wled00/ # Main firmware source (C++)
├── data/ # Web interface files
│ ├── index.htm # Main UI
│ ├── settings*.htm # Settings pages
│ └── *.js/*.css # Frontend resources
├── *.cpp/*.h # Firmware source files
└── html_*.h # Generated embedded web files (DO NOT EDIT)
tools/ # Build tools (Node.js)
├── cdata.js # Web UI build script
└── cdata-test.js # Test suite
platformio.ini # Hardware build configuration
package.json # Node.js dependencies and scripts
.github/workflows/ # CI/CD pipelines
```
### Key Files and Their Purpose
- `wled00/data/index.htm` - Main web interface
- `wled00/data/settings*.htm` - Configuration pages
- `tools/cdata.js` - Converts web files to C++ headers
- `wled00/wled.h` - Main firmware configuration
- `platformio.ini` - Hardware build targets and settings
### Development Workflow
1. **For web UI changes**:
- Edit files in `wled00/data/`
- Run `npm run build` to regenerate headers
- Test with local HTTP server
- Run `npm test` to validate build system
2. **For firmware changes**:
- Edit files in `wled00/` (but NOT `html_*.h` files)
- Ensure web UI is built first (`npm run build`)
- Build firmware: `pio run -e [target]`
- Flash to device: `pio run -e [target] --target upload`
3. **For both web and firmware**:
- Always build web UI first
- Test web interface manually
- Build and test firmware if making firmware changes
## Build Timing and Timeouts
- **Web UI build**: 3 seconds - Set timeout to 30 seconds minimum
- **Test suite**: 40 seconds - Set timeout to 2 minutes minimum
- **Hardware builds**: 15+ minutes - Set timeout to 30+ minutes minimum
- **NEVER CANCEL long-running builds** - PlatformIO downloads and compilation can take significant time
## Troubleshooting
### Common Issues
- **Build fails with missing html_*.h**: Run `npm run build` first
- **Web UI looks broken**: Check browser console for JavaScript errors
- **PlatformIO network errors**: Try again, downloads can be flaky
- **Node.js version issues**: Ensure Node.js 20+ is installed (check `.nvmrc`)
### When Things Go Wrong
- **Clear generated files**: `rm -f wled00/html_*.h` then rebuild
- **Force web UI rebuild**: `npm run build -- --force` or `npm run build -- -f`
- **Clean PlatformIO cache**: `pio run --target clean`
- **Reinstall dependencies**: `rm -rf node_modules && npm install`
## Important Notes
- **DO NOT edit `wled00/html_*.h` files** - they are auto-generated
- **Always commit both source files AND generated html_*.h files**
- **Web UI must be built before firmware compilation**
- **Test web interface manually after any web UI changes**
- **Use VS Code with PlatformIO extension for best development experience**
- **Hardware builds require appropriate ESP32/ESP8266 development board**
## CI/CD Pipeline
The GitHub Actions workflow:
1. Installs Node.js and Python dependencies
2. Runs `npm test` to validate build system
3. Builds web UI with `npm run build`
4. Compiles firmware for multiple hardware targets
5. Uploads build artifacts
Match this workflow in your local development to ensure CI success.

View File

@@ -40,7 +40,10 @@ jobs:
with:
node-version-file: '.nvmrc'
cache: 'npm'
- run: npm ci
- run: |
npm ci
VERSION=`date +%y%m%d0`
sed -i -r -e "s/define VERSION .+/define VERSION $VERSION/" wled00/wled.h
- name: Cache PlatformIO
uses: actions/cache@v4
with:

View File

@@ -1,12 +1,13 @@
name: Notify Discord on PR Merge
on:
workflow_dispatch:
pull_request:
pull_request_target:
types: [closed]
jobs:
notify:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
steps:
- name: Get User Permission
id: checkAccess
@@ -23,11 +24,15 @@
echo "Current permission level is ${{ steps.checkAccess.outputs.user-permission }}"
echo "Job originally triggered by ${{ github.actor }}"
exit 1
- name: Checkout code
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }} # This is dangerous without the first access check
- name: Send Discord notification
# if: github.event.pull_request.merged == true
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_URL: ${{ github.event.pull_request.html_url }}
ACTOR: ${{ github.actor }}
run: |
curl -H "Content-Type: application/json" -d '{"content": "Pull Request ${{ github.event.pull_request.number }} merged by ${{ github.actor }}"}' ${{ secrets.DISCORD_WEBHOOK_BETA_TESTERS }}
jq -n \
--arg content "Pull Request #${PR_NUMBER} \"${PR_TITLE}\" merged by ${ACTOR}
${PR_URL}" \
'{content: $content}' \
| curl -H "Content-Type: application/json" -d @- ${{ secrets.DISCORD_WEBHOOK_BETA_TESTERS }}

View File

@@ -10,7 +10,7 @@
# ------------------------------------------------------------------------------
# CI/release binaries
default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, nodemcuv2_compat, esp8266_2m_compat, esp01_1m_full_compat, esp32dev, esp32dev_V4, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_16MB_opi, esp32s3dev_8MB_opi, esp32s3_4M_qspi, esp32_wrover, usermods
default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, nodemcuv2_compat, esp8266_2m_compat, esp01_1m_full_compat, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_16MB_opi, esp32s3dev_8MB_opi, esp32s3_4M_qspi, esp32_wrover, usermods
src_dir = ./wled00
data_dir = ./wled00/data
@@ -142,7 +142,7 @@ lib_deps =
IRremoteESP8266 @ 2.8.2
makuna/NeoPixelBus @ 2.8.3
#https://github.com/makuna/NeoPixelBus.git#CoreShaderBeta
https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0
https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.2
# for I2C interface
;Wire
# ESP-NOW library
@@ -234,25 +234,20 @@ lib_deps_compat =
[esp32_all_variants]
lib_deps =
willmmiles/AsyncTCP @ 1.3.1
esp32async/AsyncTCP @ 3.4.7
bitbank2/AnimatedGIF@^1.4.7
https://github.com/Aircoookie/GifDecoder#bc3af18
build_flags =
-D CONFIG_ASYNC_TCP_USE_WDT=0
-D CONFIG_ASYNC_TCP_STACK_SIZE=8192
-D WLED_ENABLE_GIF
[esp32]
#platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.3/platform-espressif32-2.0.2.3.zip
platform = espressif32@3.5.0
platform_packages = framework-arduinoespressif32 @ https://github.com/Aircoookie/arduino-esp32.git#1.0.6.4
platform = ${esp32_idf_V4.platform}
platform_packages =
build_unflags = ${common.build_unflags}
build_flags = -g
-DARDUINO_ARCH_ESP32
#-DCONFIG_LITTLEFS_FOR_IDF_3_2
#use LITTLEFS library by lorol in ESP32 core 1.x.x instead of built-in in 2.x.x
-D LOROL_LITTLEFS
; -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
${esp32_all_variants.build_flags}
build_flags = ${esp32_idf_V4.build_flags}
lib_deps = ${esp32_idf_V4.lib_deps}
tiny_partitions = tools/WLED_ESP32_2MB_noOTA.csv
default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv
@@ -260,10 +255,7 @@ extended_partitions = tools/WLED_ESP32_4MB_700k_FS.csv
big_partitions = tools/WLED_ESP32_4MB_256KB_FS.csv ;; 1.8MB firmware, 256KB filesystem, coredump support
large_partitions = tools/WLED_ESP32_8MB.csv
extreme_partitions = tools/WLED_ESP32_16MB_9MB_FS.csv
lib_deps =
https://github.com/lorol/LITTLEFS.git
${esp32_all_variants.lib_deps}
${env.lib_deps}
board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs
# additional build flags for audioreactive - must be applied globally
AR_build_flags = ;; -fsingle-precision-constant ;; forces ArduinoFFT to use float math (2x faster)
@@ -271,8 +263,7 @@ AR_lib_deps = ;; for pre-usermod-library platformio_override compatibility
[esp32_idf_V4]
;; experimental build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5
;; very similar to the normal ESP32 flags, but omitting Lorol LittleFS, as littlefs is included in the new framework already.
;; build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5
;;
;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly.
;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio.
@@ -283,14 +274,12 @@ build_unflags = ${common.build_unflags}
build_flags = -g
-Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one
-DARDUINO_ARCH_ESP32 -DESP32
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
${esp32_all_variants.build_flags}
-D WLED_ENABLE_DMX_INPUT
lib_deps =
${esp32_all_variants.lib_deps}
https://github.com/someweisguy/esp_dmx.git#47db25d
${env.lib_deps}
board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs
[esp32s2]
;; generic definitions for all ESP32-S2 boards
@@ -305,10 +294,9 @@ build_flags = -g
-DARDUINO_USB_MODE=0 ;; this flag is mandatory for ESP32-S2 !
;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry:
;; ARDUINO_USB_CDC_ON_BOOT
${esp32_all_variants.build_flags}
${esp32_idf_V4.build_flags}
lib_deps =
${esp32_all_variants.lib_deps}
${env.lib_deps}
${esp32_idf_V4.lib_deps}
board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs
[esp32c3]
@@ -323,10 +311,9 @@ build_flags = -g
-DARDUINO_USB_MODE=1 ;; this flag is mandatory for ESP32-C3
;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry:
;; ARDUINO_USB_CDC_ON_BOOT
${esp32_all_variants.build_flags}
${esp32_idf_V4.build_flags}
lib_deps =
${esp32_all_variants.lib_deps}
${env.lib_deps}
${esp32_idf_V4.lib_deps}
board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs
board_build.flash_mode = qio
@@ -343,10 +330,9 @@ build_flags = -g
-DCO
;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry:
;; ARDUINO_USB_MODE, ARDUINO_USB_CDC_ON_BOOT
${esp32_all_variants.build_flags}
${esp32_idf_V4.build_flags}
lib_deps =
${esp32_all_variants.lib_deps}
${env.lib_deps}
${esp32_idf_V4.lib_deps}
board_build.partitions = ${esp32.large_partitions} ;; default partioning for 8MB flash - can be overridden in build envs
@@ -441,21 +427,11 @@ custom_usermods = audioreactive
[env:esp32dev]
board = esp32dev
platform = ${esp32.platform}
platform_packages = ${esp32.platform_packages}
custom_usermods = audioreactive
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32\" #-D WLED_DISABLE_BROWNOUT_DET
lib_deps = ${esp32.lib_deps}
monitor_filters = esp32_exception_decoder
board_build.partitions = ${esp32.default_partitions}
[env:esp32dev_V4]
board = esp32dev
platform = ${esp32_idf_V4.platform}
build_unflags = ${common.build_unflags}
custom_usermods = audioreactive
build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_V4\" #-D WLED_DISABLE_BROWNOUT_DET
-DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3
lib_deps = ${esp32_idf_V4.lib_deps}
monitor_filters = esp32_exception_decoder
board_build.partitions = ${esp32.default_partitions}
@@ -489,23 +465,9 @@ board_upload.maximum_size = 16777216
board_build.f_flash = 80000000L
board_build.flash_mode = dio
;[env:esp32dev_audioreactive]
;board = esp32dev
;platform = ${esp32.platform}
;platform_packages = ${esp32.platform_packages}
;custom_usermods = audioreactive
;build_unflags = ${common.build_unflags}
;build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_audioreactive\" #-D WLED_DISABLE_BROWNOUT_DET
;lib_deps = ${esp32.lib_deps}
;monitor_filters = esp32_exception_decoder
;board_build.partitions = ${esp32.default_partitions}
;; board_build.f_flash = 80000000L
;; board_build.flash_mode = dio
[env:esp32_eth]
board = esp32-poe
platform = ${esp32.platform}
platform_packages = ${esp32.platform_packages}
platform = ${esp32_idf_V4.platform}
upload_speed = 921600
custom_usermods = audioreactive
build_unflags = ${common.build_unflags}
@@ -513,10 +475,10 @@ build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"
; -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only
lib_deps = ${esp32.lib_deps}
board_build.partitions = ${esp32.default_partitions}
board_build.flash_mode = dio
[env:esp32_wrover]
extends = esp32_idf_V4
platform = ${esp32_idf_V4.platform}
board = ttgo-t7-v14-mini32
board_build.f_flash = 80000000L
board_build.flash_mode = qio

View File

@@ -313,11 +313,11 @@ class MyExampleUsermod : public Usermod {
yield();
// ignore certain button types as they may have other consequences
if (!enabled
|| buttons[b].type == BTN_TYPE_NONE
|| buttons[b].type == BTN_TYPE_RESERVED
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|| buttons[b].type == BTN_TYPE_ANALOG
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|| buttonType[b] == BTN_TYPE_NONE
|| buttonType[b] == BTN_TYPE_RESERVED
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|| buttonType[b] == BTN_TYPE_ANALOG
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
return false;
}

View File

@@ -1530,7 +1530,7 @@ class AudioReactive : public Usermod {
// better would be for AudioSource to implement getType()
if (enabled
&& dmType == 0 && audioPin>=0
&& (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)
&& (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED)
) {
return true;
}

View File

@@ -562,11 +562,11 @@ void MultiRelay::loop() {
bool MultiRelay::handleButton(uint8_t b) {
yield();
if (!enabled
|| buttons[b].type == BTN_TYPE_NONE
|| buttons[b].type == BTN_TYPE_RESERVED
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|| buttons[b].type == BTN_TYPE_ANALOG
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|| buttonType[b] == BTN_TYPE_NONE
|| buttonType[b] == BTN_TYPE_RESERVED
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|| buttonType[b] == BTN_TYPE_ANALOG
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
return false;
}
@@ -581,20 +581,20 @@ bool MultiRelay::handleButton(uint8_t b) {
unsigned long now = millis();
//button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)
if (buttons[b].type == BTN_TYPE_SWITCH) {
if (buttonType[b] == BTN_TYPE_SWITCH) {
//handleSwitch(b);
if (buttons[b].pressedBefore != isButtonPressed(b)) {
buttons[b].pressedTime = now;
buttons[b].pressedBefore = !buttons[b].pressedBefore;
if (buttonPressedBefore[b] != isButtonPressed(b)) {
buttonPressedTime[b] = now;
buttonPressedBefore[b] = !buttonPressedBefore[b];
}
if (buttons[b].longPressed == buttons[b].pressedBefore) return handled;
if (buttonLongPressed[b] == buttonPressedBefore[b]) return handled;
if (now - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
if (now - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
if (_relay[i].button == b) {
switchRelay(i, buttons[b].pressedBefore);
buttons[b].longPressed = buttons[b].pressedBefore; //save the last "long term" switch state
switchRelay(i, buttonPressedBefore[b]);
buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state
}
}
}
@@ -604,40 +604,40 @@ bool MultiRelay::handleButton(uint8_t b) {
//momentary button logic
if (isButtonPressed(b)) { //pressed
if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;
buttons[b].pressedBefore = true;
if (!buttonPressedBefore[b]) buttonPressedTime[b] = now;
buttonPressedBefore[b] = true;
if (now - buttons[b].pressedTime > 600) { //long press
if (now - buttonPressedTime[b] > 600) { //long press
//longPressAction(b); //not exposed
//handled = false; //use if you want to pass to default behaviour
buttons[b].longPressed = true;
buttonLongPressed[b] = true;
}
} else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released
} else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released
long dur = now - buttons[b].pressedTime;
long dur = now - buttonPressedTime[b];
if (dur < WLED_DEBOUNCE_THRESHOLD) {
buttons[b].pressedBefore = false;
buttonPressedBefore[b] = false;
return handled;
} //too short "press", debounce
bool doublePress = buttons[b].waitTime; //did we have short press before?
buttons[b].waitTime = 0;
bool doublePress = buttonWaitTime[b]; //did we have short press before?
buttonWaitTime[b] = 0;
if (!buttons[b].longPressed) { //short press
if (!buttonLongPressed[b]) { //short press
// if this is second release within 350ms it is a double press (buttonWaitTime!=0)
if (doublePress) {
//doublePressAction(b); //not exposed
//handled = false; //use if you want to pass to default behaviour
} else {
buttons[b].waitTime = now;
buttonWaitTime[b] = now;
}
}
buttons[b].pressedBefore = false;
buttons[b].longPressed = false;
buttonPressedBefore[b] = false;
buttonLongPressed[b] = false;
}
// if 350ms elapsed since last press/release it is a short press
if (buttons[b].waitTime && now - buttons[b].waitTime > 350 && !buttons[b].pressedBefore) {
buttons[b].waitTime = 0;
if (buttonWaitTime[b] && now - buttonWaitTime[b] > 350 && !buttonPressedBefore[b]) {
buttonWaitTime[b] = 0;
//shortPressAction(b); //not exposed
for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {
if (_relay[i].button == b) {

View File

@@ -461,11 +461,11 @@ class PixelsDiceTrayUsermod : public Usermod {
#if USING_TFT_DISPLAY
bool handleButton(uint8_t b) override {
if (!enabled || b > 1 // buttons 0,1 only
|| buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_NONE ||
buttons[b].type == BTN_TYPE_RESERVED ||
buttons[b].type == BTN_TYPE_PIR_SENSOR ||
buttons[b].type == BTN_TYPE_ANALOG ||
buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|| buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_NONE ||
buttonType[b] == BTN_TYPE_RESERVED ||
buttonType[b] == BTN_TYPE_PIR_SENSOR ||
buttonType[b] == BTN_TYPE_ANALOG ||
buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
return false;
}
@@ -476,43 +476,43 @@ class PixelsDiceTrayUsermod : public Usermod {
static unsigned long buttonWaitTime[2] = {0};
//momentary button logic
if (!buttons[b].longPressed && isButtonPressed(b)) { //pressed
if (!buttons[b].pressedBefore) {
buttons[b].pressedTime = now;
if (!buttonLongPressed[b] && isButtonPressed(b)) { //pressed
if (!buttonPressedBefore[b]) {
buttonPressedTime[b] = now;
}
buttons[b].pressedBefore = true;
buttonPressedBefore[b] = true;
if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press
if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press
menu_ctrl.HandleButton(ButtonType::LONG, b);
buttons[b].longPressed = true;
buttonLongPressed[b] = true;
return true;
}
} else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released
} else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released
long dur = now - buttons[b].pressedTime;
long dur = now - buttonPressedTime[b];
if (dur < WLED_DEBOUNCE_THRESHOLD) {
buttons[b].pressedBefore = false;
buttonPressedBefore[b] = false;
return true;
} //too short "press", debounce
bool doublePress = buttons[b].waitTime; //did we have short press before?
buttons[b].waitTime = 0;
bool doublePress = buttonWaitTime[b]; //did we have short press before?
buttonWaitTime[b] = 0;
if (!buttons[b].longPressed) { //short press
if (!buttonLongPressed[b]) { //short press
// if this is second release within 350ms it is a double press (buttonWaitTime!=0)
if (doublePress) {
menu_ctrl.HandleButton(ButtonType::DOUBLE, b);
} else {
buttons[b].waitTime = now;
buttonWaitTime[b] = now;
}
}
buttons[b].pressedBefore = false;
buttons[b].longPressed = false;
buttonPressedBefore[b] = false;
buttonLongPressed[b] = false;
}
// if 350ms elapsed since last press/release it is a short press
if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS &&
!buttons[b].pressedBefore) {
buttons[b].waitTime = 0;
if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS &&
!buttonPressedBefore[b]) {
buttonWaitTime[b] = 0;
menu_ctrl.HandleButton(ButtonType::SINGLE, b);
}

View File

@@ -749,12 +749,12 @@ bool FourLineDisplayUsermod::handleButton(uint8_t b) {
yield();
if (!enabled
|| b // button 0 only
|| buttons[b].type == BTN_TYPE_SWITCH
|| buttons[b].type == BTN_TYPE_NONE
|| buttons[b].type == BTN_TYPE_RESERVED
|| buttons[b].type == BTN_TYPE_PIR_SENSOR
|| buttons[b].type == BTN_TYPE_ANALOG
|| buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {
|| buttonType[b] == BTN_TYPE_SWITCH
|| buttonType[b] == BTN_TYPE_NONE
|| buttonType[b] == BTN_TYPE_RESERVED
|| buttonType[b] == BTN_TYPE_PIR_SENSOR
|| buttonType[b] == BTN_TYPE_ANALOG
|| buttonType[b] == BTN_TYPE_ANALOG_INVERTED) {
return false;
}

101
wled00/FX_fcn.cpp Executable file → Normal file
View File

@@ -66,13 +66,15 @@ Segment::Segment(const Segment &orig) {
_dataLen = 0;
pixels = nullptr;
if (!stop) return; // nothing to do if segment is inactive/invalid
if (orig.name) { name = static_cast<char*>(d_malloc(strlen(orig.name)+1)); if (name) strcpy(name, orig.name); }
if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); }
if (orig.pixels) {
// allocate pixel buffer: prefer IRAM/PSRAM
pixels = static_cast<uint32_t*>(d_malloc(sizeof(uint32_t) * orig.length()));
if (pixels) memcpy(pixels, orig.pixels, sizeof(uint32_t) * orig.length());
else {
DEBUG_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!"));
if (pixels) {
memcpy(pixels, orig.pixels, sizeof(uint32_t) * orig.length());
if (orig.name) { name = static_cast<char*>(d_malloc(strlen(orig.name)+1)); if (name) strcpy(name, orig.name); }
if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); }
} else {
DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!"));
errorFlag = ERR_NORAM_PX;
stop = 0; // mark segment as inactive/invalid
}
@@ -107,12 +109,14 @@ Segment& Segment::operator= (const Segment &orig) {
pixels = nullptr;
if (!stop) return *this; // nothing to do if segment is inactive/invalid
// copy source data
if (orig.name) { name = static_cast<char*>(d_malloc(strlen(orig.name)+1)); if (name) strcpy(name, orig.name); }
if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); }
if (orig.pixels) {
// allocate pixel buffer: prefer IRAM/PSRAM
pixels = static_cast<uint32_t*>(d_malloc(sizeof(uint32_t) * orig.length()));
if (pixels) memcpy(pixels, orig.pixels, sizeof(uint32_t) * orig.length());
else {
if (pixels) {
memcpy(pixels, orig.pixels, sizeof(uint32_t) * orig.length());
if (orig.name) { name = static_cast<char*>(d_malloc(strlen(orig.name)+1)); if (name) strcpy(name, orig.name); }
if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); }
} else {
DEBUG_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!"));
errorFlag = ERR_NORAM_PX;
stop = 0; // mark segment as inactive/invalid
@@ -281,8 +285,9 @@ void Segment::startTransition(uint16_t dur, bool segmentCopy) {
if (_t->_oldSegment) {
_t->_oldSegment->palette = _t->_palette; // restore original palette and colors (from start of transition)
for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = _t->_colors[i];
DEBUGFX_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels);
if (!_t->_oldSegment->isActive()) stopTransition();
}
DEBUG_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels);
}
return;
}
@@ -298,13 +303,12 @@ void Segment::startTransition(uint16_t dur, bool segmentCopy) {
#endif
for (int i=0; i<NUM_COLORS; i++) _t->_colors[i] = colors[i];
if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings
#ifdef WLED_DEBUG
if (_t->_oldSegment) {
DEBUG_PRINTF_P(PSTR("-- Started transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels);
DEBUGFX_PRINTF_P(PSTR("-- Started transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels);
if (!_t->_oldSegment->isActive()) stopTransition();
} else {
DEBUG_PRINTF_P(PSTR("-- Started transition without old segment: S=%p T(%p)\n"), this, _t);
DEBUGFX_PRINTF_P(PSTR("-- Started transition without old segment: S=%p T(%p)\n"), this, _t);
}
#endif
};
}
@@ -425,14 +429,15 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
unsigned oldLength = length();
DEBUG_PRINTF_P(PSTR("Segment geometry: %d,%d -> %d,%d [%d,%d]\n"), (int)i1, (int)i2, (int)i1Y, (int)i2Y, (int)grp, (int)spc);
DEBUGFX_PRINTF_P(PSTR("Segment geometry: %d,%d -> %d,%d [%d,%d]\n"), (int)i1, (int)i2, (int)i1Y, (int)i2Y, (int)grp, (int)spc);
markForReset();
startTransition(strip.getTransition()); // start transition prior to change (if segment is deactivated (start>stop) no transition will happen)
stateChanged = true; // send UDP/WS broadcast
if (_t) stopTransition(); // we can't use transition if segment dimensions changed
stateChanged = true; // send UDP/WS broadcast
// apply change immediately
if (i2 <= i1) { //disable segment
d_free(pixels);
deallocateData();
p_free(pixels);
pixels = nullptr;
stop = 0;
return;
@@ -449,21 +454,25 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui
#endif
// safety check
if (start >= stop || startY >= stopY) {
d_free(pixels);
deallocateData();
p_free(pixels);
pixels = nullptr;
stop = 0;
return;
}
// re-allocate FX render buffer
// allocate FX render buffer
if (length() != oldLength) {
if (pixels) d_free(pixels); // using realloc on large buffers can cause additional fragmentation instead of reducing it
// allocate render buffer (always entire segment), prefer IRAM/PSRAM. Note: impact on FPS with PSRAM buffer is low (<2% with QSPI PSRAM) on S2/S3
p_free(pixels);
pixels = static_cast<uint32_t*>(d_malloc(sizeof(uint32_t) * length()));
if (!pixels) {
DEBUG_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!"));
DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!"));
deallocateData();
errorFlag = ERR_NORAM_PX;
stop = 0;
return;
}
}
refreshLightCapabilities();
}
@@ -572,8 +581,8 @@ Segment &Segment::setName(const char *newName) {
if (newLen) {
if (name) d_free(name); // free old name
name = static_cast<char*>(d_malloc(newLen+1));
if (mode == FX_MODE_2DSCROLLTEXT) startTransition(strip.getTransition(), true); // if the name changes in scrolling text mode, we need to copy the segment for blending
if (name) strlcpy(name, newName, newLen+1);
name[newLen] = 0;
return *this;
}
}
@@ -1076,27 +1085,14 @@ void Segment::blur(uint8_t blur_amount, bool smear) const {
/*
* Put a value 0 to 255 in to get a color value.
* The colours are a transition r -> g -> b -> back to r
* Inspired by the Adafruit examples.
* Rotates the color in HSV space, where pos is H. (0=0deg, 256=360deg)
*/
uint32_t Segment::color_wheel(uint8_t pos) const {
if (palette) return color_from_palette(pos, false, false, 0); // never wrap palette
if (palette) return color_from_palette(pos, false, false, 0); // only wrap if "always wrap" is set
uint8_t w = W(getCurrentColor(0));
pos = 255 - pos;
if (useRainbowWheel) {
CRGB rgb;
hsv2rgb_rainbow(CHSV(pos, 255, 255), rgb);
return RGBW32(rgb.r, rgb.g, rgb.b, w);
} else {
if (pos < 85) {
return RGBW32((255 - pos * 3), 0, (pos * 3), w);
} else if (pos < 170) {
pos -= 85;
return RGBW32(0, (pos * 3), (255 - pos * 3), w);
} else {
pos -= 170;
return RGBW32((pos * 3), (255 - pos * 3), 0, w);
}
}
uint32_t rgb;
hsv2rgb(CHSV32(static_cast<uint16_t>(pos << 8), 255, 255), rgb);
return rgb | (w << 24); // add white channel
}
/*
@@ -1210,10 +1206,9 @@ void WS2812FX::finalizeInit() {
deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist)
// allocate frame buffer after matrix has been set up (gaps!)
if (_pixels) d_free(_pixels); // using realloc on large buffers can cause additional fragmentation instead of reducing it
d_free(_pixels); // using realloc on large buffers can cause additional fragmentation instead of reducing it
_pixels = static_cast<uint32_t*>(d_malloc(getLengthTotal() * sizeof(uint32_t)));
DEBUG_PRINTF_P(PSTR("strip buffer size: %uB\n"), getLengthTotal() * sizeof(uint32_t));
DEBUG_PRINTF_P(PSTR("Heap after strip init: %uB\n"), ESP.getFreeHeap());
}
@@ -1258,7 +1253,8 @@ void WS2812FX::service() {
// if segment is in transition and no old segment exists we don't need to run the old mode
// (blendSegments() takes care of On/Off transitions and clipping)
Segment *segO = seg.getOldSegment();
if (segO && (seg.mode != segO->mode || blendingStyle != BLEND_STYLE_FADE)) {
if (segO && segO->isActive() && (seg.mode != segO->mode || blendingStyle != BLEND_STYLE_FADE ||
(segO->name != seg.name && segO->name && seg.name && strncmp(segO->name, seg.name, WLED_MAX_SEGNAME_LEN) != 0))) {
Segment::modeBlend(true); // set semaphore for beginDraw() to blend colors and palette
segO->beginDraw(prog); // set up palette & colors (also sets draw dimensions), parent segment has transition progress
_currentSegment = segO; // set current segment
@@ -1344,11 +1340,6 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
uint8_t opacity = topSegment.currentBri(); // returns transitioned opacity for style FADE
uint8_t cct = topSegment.currentCCT();
if (length == 1) {
// Can't blend only a single pixel, prevents crash when bus init fails
return;
}
Segment::setClippingRect(0, 0); // disable clipping by default
const unsigned dw = (blendingStyle==BLEND_STYLE_OUTSIDE_IN ? progInv : progress) * width / 0xFFFFU + 1;
@@ -1461,8 +1452,10 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
}
uint32_t c_a = BLACK;
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 && x < oCols && y < oRows) {
// we need to blend old segment using fade as pixels ae not clipped
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, segO->getPixelColorRaw(x + y*oCols), progInv);
} else if (blendingStyle != BLEND_STYLE_FADE) {
// workaround for On/Off transition
@@ -1616,6 +1609,8 @@ static uint8_t estimateCurrentAndLimitBri(uint8_t brightness, uint32_t *pixels)
}
void WS2812FX::show() {
if (!_pixels) return; // no pixels allocated, nothing to show
unsigned long showNow = millis();
size_t diff = showNow - _lastShow;
@@ -1888,7 +1883,7 @@ void WS2812FX::makeAutoSegments(bool forceReset) {
for (size_t i = 1; i < s; i++) {
_segments.emplace_back(segStarts[i], segStops[i]);
}
DEBUG_PRINTF_P(PSTR("%d auto segments created.\n"), _segments.size());
DEBUGFX_PRINTF_P(PSTR("%d auto segments created.\n"), _segments.size());
} else {
@@ -2012,7 +2007,7 @@ bool WS2812FX::deserializeMap(unsigned n) {
}
d_free(customMappingTable);
customMappingTable = static_cast<uint16_t*>(d_malloc(sizeof(uint16_t)*getLengthTotal())); // do not use SPI RAM
customMappingTable = static_cast<uint16_t*>(d_malloc(sizeof(uint16_t)*getLengthTotal())); // prefer DRAM for speed
if (customMappingTable) {
DEBUG_PRINTF_P(PSTR("ledmap allocated: %uB\n"), sizeof(uint16_t)*getLengthTotal());

View File

@@ -17,13 +17,13 @@ static bool buttonBriDirection = false; // true: increase brightness, false: dec
void shortPressAction(uint8_t b)
{
if (!buttons[b].macroButton) {
if (!macroButton[b]) {
switch (b) {
case 0: toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); break;
case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break;
}
} else {
applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);
applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET);
}
#ifndef WLED_DISABLE_MQTT
@@ -38,7 +38,7 @@ void shortPressAction(uint8_t b)
void longPressAction(uint8_t b)
{
if (!buttons[b].macroLongPress) {
if (!macroLongPress[b]) {
switch (b) {
case 0: setRandomColor(colPri); colorUpdated(CALL_MODE_BUTTON); break;
case 1:
@@ -52,11 +52,11 @@ void longPressAction(uint8_t b)
else bri -= WLED_LONG_BRI_STEPS;
}
stateUpdated(CALL_MODE_BUTTON);
buttons[b].pressedTime = millis();
buttonPressedTime[b] = millis();
break; // repeatable action
}
} else {
applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);
applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET);
}
#ifndef WLED_DISABLE_MQTT
@@ -71,13 +71,13 @@ void longPressAction(uint8_t b)
void doublePressAction(uint8_t b)
{
if (!buttons[b].macroDoublePress) {
if (!macroDoublePress[b]) {
switch (b) {
//case 0: toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); break; //instant short press on button 0 if no macro set
case 1: ++effectPalette %= getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break;
}
} else {
applyPreset(buttons[b].macroDoublePress, CALL_MODE_BUTTON_PRESET);
applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET);
}
#ifndef WLED_DISABLE_MQTT
@@ -92,10 +92,10 @@ void doublePressAction(uint8_t b)
bool isButtonPressed(uint8_t b)
{
if (buttons[b].pin < 0) return false;
unsigned pin = buttons[b].pin;
if (btnPin[b]<0) return false;
unsigned pin = btnPin[b];
switch (buttons[b].type) {
switch (buttonType[b]) {
case BTN_TYPE_NONE:
case BTN_TYPE_RESERVED:
break;
@@ -113,7 +113,7 @@ bool isButtonPressed(uint8_t b)
#ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt)
if (touchInterruptGetLastStatus(pin)) return true;
#else
if (digitalPinToTouchChannel(pin) >= 0 && touchRead(pin) <= touchThreshold) return true;
if (digitalPinToTouchChannel(btnPin[b]) >= 0 && touchRead(pin) <= touchThreshold) return true;
#endif
#endif
break;
@@ -124,25 +124,25 @@ bool isButtonPressed(uint8_t b)
void handleSwitch(uint8_t b)
{
// isButtonPressed() handles inverted/noninverted logic
if (buttons[b].pressedBefore != isButtonPressed(b)) {
if (buttonPressedBefore[b] != isButtonPressed(b)) {
DEBUG_PRINTF_P(PSTR("Switch: State changed %u\n"), b);
buttons[b].pressedTime = millis();
buttons[b].pressedBefore = !buttons[b].pressedBefore; // toggle pressed state
buttonPressedTime[b] = millis();
buttonPressedBefore[b] = !buttonPressedBefore[b];
}
if (buttons[b].longPressed == buttons[b].pressedBefore) return;
if (buttonLongPressed[b] == buttonPressedBefore[b]) return;
if (millis() - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
if (millis() - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)
DEBUG_PRINTF_P(PSTR("Switch: Activating %u\n"), b);
if (!buttons[b].pressedBefore) { // on -> off
if (!buttonPressedBefore[b]) { // on -> off
DEBUG_PRINTF_P(PSTR("Switch: On -> Off (%u)\n"), b);
if (buttons[b].macroButton) applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);
if (macroButton[b]) applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET);
else { //turn on
if (!bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
}
} else { // off -> on
DEBUG_PRINTF_P(PSTR("Switch: Off -> On (%u)\n"), b);
if (buttons[b].macroLongPress) applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);
if (macroLongPress[b]) applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET);
else { //turn off
if (bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}
}
@@ -152,13 +152,13 @@ void handleSwitch(uint8_t b)
// publish MQTT message
if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {
char subuf[MQTT_MAX_TOPIC_LEN + 32];
if (buttons[b].type == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b);
if (buttonType[b] == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b);
else sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);
mqtt->publish(subuf, 0, false, !buttons[b].pressedBefore ? "off" : "on");
mqtt->publish(subuf, 0, false, !buttonPressedBefore[b] ? "off" : "on");
}
#endif
buttons[b].longPressed = buttons[b].pressedBefore; //save the last "long term" switch state
buttonLongPressed[b] = buttonPressedBefore[b]; //save the last "long term" switch state
}
}
@@ -178,17 +178,17 @@ void handleAnalog(uint8_t b)
#ifdef ESP8266
rawReading = analogRead(A0) << 2; // convert 10bit read to 12bit
#else
if ((buttons[b].pin < 0) /*|| (digitalPinToAnalogChannel(buttons[b].pin) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise
rawReading = analogRead(buttons[b].pin); // collect at full 12bit resolution
if ((btnPin[b] < 0) /*|| (digitalPinToAnalogChannel(btnPin[b]) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise
rawReading = analogRead(btnPin[b]); // collect at full 12bit resolution
#endif
yield(); // keep WiFi task running - analog read may take several millis on ESP8266
filteredReading[b] += POT_SMOOTHING * ((float(rawReading) / 16.0f) - filteredReading[b]); // filter raw input, and scale to [0..255]
unsigned aRead = max(min(int(filteredReading[b]), 255), 0); // squash into 8bit
if (aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used
if (aRead >= 255-POT_SENSITIVITY) aRead = 255;
if(aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used
if(aRead >= 255-POT_SENSITIVITY) aRead = 255;
if (buttons[b].type == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead;
if (buttonType[b] == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead;
// remove noise & reduce frequency of UI updates
if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return; // no significant change in reading
@@ -206,10 +206,10 @@ void handleAnalog(uint8_t b)
oldRead[b] = aRead;
// if no macro for "short press" and "long press" is defined use brightness control
if (!buttons[b].macroButton && !buttons[b].macroLongPress) {
DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), buttons[b].macroDoublePress);
if (!macroButton[b] && !macroLongPress[b]) {
DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), macroDoublePress[b]);
// if "double press" macro defines which option to change
if (buttons[b].macroDoublePress >= 250) {
if (macroDoublePress[b] >= 250) {
// global brightness
if (aRead == 0) {
briLast = bri;
@@ -218,30 +218,27 @@ void handleAnalog(uint8_t b)
if (bri == 0) strip.restartRuntime();
bri = aRead;
}
} else if (buttons[b].macroDoublePress == 249) {
} else if (macroDoublePress[b] == 249) {
// effect speed
effectSpeed = aRead;
} else if (buttons[b].macroDoublePress == 248) {
} else if (macroDoublePress[b] == 248) {
// effect intensity
effectIntensity = aRead;
} else if (buttons[b].macroDoublePress == 247) {
} else if (macroDoublePress[b] == 247) {
// selected palette
effectPalette = map(aRead, 0, 252, 0, getPaletteCount()-1);
effectPalette = constrain(effectPalette, 0, getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result
} else if (buttons[b].macroDoublePress == 200) {
} else if (macroDoublePress[b] == 200) {
// primary color, hue, full saturation
colorHStoRGB(aRead*256, 255, colPri);
colorHStoRGB(aRead*256,255,colPri);
} else {
// otherwise use "double press" for segment selection
Segment& seg = strip.getSegment(buttons[b].macroDoublePress);
Segment& seg = strip.getSegment(macroDoublePress[b]);
if (aRead == 0) {
seg.on = false; // do not use transition
//seg.setOption(SEG_OPTION_ON, false); // off (use transition)
seg.setOption(SEG_OPTION_ON, false); // off (use transition)
} else {
seg.opacity = aRead; // set brightness (opacity) of segment
seg.on = true;
//seg.setOpacity(aRead);
//seg.setOption(SEG_OPTION_ON, true); // on (use transition)
seg.setOpacity(aRead);
seg.setOption(SEG_OPTION_ON, true); // on (use transition)
}
// this will notify clients of update (websockets,mqtt,etc)
updateInterfaces(CALL_MODE_BUTTON);
@@ -264,16 +261,16 @@ void handleButton()
if (strip.isUpdating() && (now - lastRun < ANALOG_BTN_READ_CYCLE+1)) return; // don't interfere with strip update (unless strip is updating continuously, e.g. very long strips)
lastRun = now;
for (unsigned b = 0; b < buttons.size(); b++) {
for (unsigned b=0; b<WLED_MAX_BUTTONS; b++) {
#ifdef ESP8266
if ((buttons[b].pin < 0 && !(buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)) || buttons[b].type == BTN_TYPE_NONE) continue;
if ((btnPin[b]<0 && !(buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED)) || buttonType[b] == BTN_TYPE_NONE) continue;
#else
if (buttons[b].pin < 0 || buttons[b].type == BTN_TYPE_NONE) continue;
if (btnPin[b]<0 || buttonType[b] == BTN_TYPE_NONE) continue;
#endif
if (UsermodManager::handleButton(b)) continue; // did usermod handle buttons
if (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer
if (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer
if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) {
handleAnalog(b);
}
@@ -281,7 +278,7 @@ void handleButton()
}
// button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)
if (buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_TOUCH_SWITCH || buttons[b].type == BTN_TYPE_PIR_SENSOR) {
if (buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_TOUCH_SWITCH || buttonType[b] == BTN_TYPE_PIR_SENSOR) {
handleSwitch(b);
continue;
}
@@ -290,39 +287,40 @@ void handleButton()
if (isButtonPressed(b)) { // pressed
// if all macros are the same, fire action immediately on rising edge
if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {
if (!buttons[b].pressedBefore) shortPressAction(b);
buttons[b].pressedBefore = true;
buttons[b].pressedTime = now; // continually update (for debouncing to work in release handler)
if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) {
if (!buttonPressedBefore[b])
shortPressAction(b);
buttonPressedBefore[b] = true;
buttonPressedTime[b] = now; // continually update (for debouncing to work in release handler)
continue;
}
if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;
buttons[b].pressedBefore = true;
if (!buttonPressedBefore[b]) buttonPressedTime[b] = now;
buttonPressedBefore[b] = true;
if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press
if (!buttons[b].longPressed) {
if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press
if (!buttonLongPressed[b]) {
buttonBriDirection = !buttonBriDirection; //toggle brightness direction on long press
longPressAction(b);
} else if (b) { //repeatable action (~5 times per s) on button > 0
longPressAction(b);
buttons[b].pressedTime = now - WLED_LONG_REPEATED_ACTION; //200ms
buttonPressedTime[b] = now - WLED_LONG_REPEATED_ACTION; //200ms
}
buttons[b].longPressed = true;
buttonLongPressed[b] = true;
}
} else if (buttons[b].pressedBefore) { //released
long dur = now - buttons[b].pressedTime;
} else if (buttonPressedBefore[b]) { //released
long dur = now - buttonPressedTime[b];
// released after rising-edge short press action
if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {
if (dur > WLED_DEBOUNCE_THRESHOLD) buttons[b].pressedBefore = false; // debounce, blocks button for 50 ms once it has been released
if (macroButton[b] && macroButton[b] == macroLongPress[b] && macroButton[b] == macroDoublePress[b]) {
if (dur > WLED_DEBOUNCE_THRESHOLD) buttonPressedBefore[b] = false; // debounce, blocks button for 50 ms once it has been released
continue;
}
if (dur < WLED_DEBOUNCE_THRESHOLD) {buttons[b].pressedBefore = false; continue;} // too short "press", debounce
bool doublePress = buttons[b].waitTime; //did we have a short press before?
buttons[b].waitTime = 0;
if (dur < WLED_DEBOUNCE_THRESHOLD) {buttonPressedBefore[b] = false; continue;} // too short "press", debounce
bool doublePress = buttonWaitTime[b]; //did we have a short press before?
buttonWaitTime[b] = 0;
if (b == 0 && dur > WLED_LONG_AP) { // long press on button 0 (when released)
if (dur > WLED_LONG_FACTORY_RESET) { // factory reset if pressed > 10 seconds
@@ -334,25 +332,25 @@ void handleButton()
} else {
WLED::instance().initAP(true);
}
} else if (!buttons[b].longPressed) { //short press
} else if (!buttonLongPressed[b]) { //short press
//NOTE: this interferes with double click handling in usermods so usermod needs to implement full button handling
if (b != 1 && !buttons[b].macroDoublePress) { //don't wait for double press on buttons without a default action if no double press macro set
if (b != 1 && !macroDoublePress[b]) { //don't wait for double press on buttons without a default action if no double press macro set
shortPressAction(b);
} else { //double press if less than 350 ms between current press and previous short press release (buttonWaitTime!=0)
if (doublePress) {
doublePressAction(b);
} else {
buttons[b].waitTime = now;
buttonWaitTime[b] = now;
}
}
}
buttons[b].pressedBefore = false;
buttons[b].longPressed = false;
buttonPressedBefore[b] = false;
buttonLongPressed[b] = false;
}
//if 350ms elapsed since last short press release it is a short press
if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS && !buttons[b].pressedBefore) {
buttons[b].waitTime = 0;
if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS && !buttonPressedBefore[b]) {
buttonWaitTime[b] = 0;
shortPressAction(b);
}
}

View File

@@ -354,91 +354,97 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
JsonArray hw_btn_ins = btn_obj["ins"];
if (!hw_btn_ins.isNull()) {
// deallocate existing button pins
for (const auto &button : buttons) PinManager::deallocatePin(button.pin, PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button
buttons.clear(); // clear existing buttons
for (unsigned b = 0; b < WLED_MAX_BUTTONS; b++) PinManager::deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button
unsigned s = 0;
for (JsonObject btn : hw_btn_ins) {
uint8_t type = btn["type"] | BTN_TYPE_NONE;
int8_t pin = btn["pin"][0] | -1;
CJSON(buttonType[s], btn["type"]);
int8_t pin = btn["pin"][0] | -1;
if (pin > -1 && PinManager::allocatePin(pin, false, PinOwner::Button)) {
#ifdef ARDUINO_ARCH_ESP32
btnPin[s] = pin;
#ifdef ARDUINO_ARCH_ESP32
// ESP32 only: check that analog button pin is a valid ADC gpio
if ((type == BTN_TYPE_ANALOG) || (type == BTN_TYPE_ANALOG_INVERTED)) {
if (digitalPinToAnalogChannel(pin) < 0) {
if ((buttonType[s] == BTN_TYPE_ANALOG) || (buttonType[s] == BTN_TYPE_ANALOG_INVERTED)) {
if (digitalPinToAnalogChannel(btnPin[s]) < 0) {
// not an ADC analog pin
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), pin, s);
PinManager::deallocatePin(pin, PinOwner::Button);
pin = -1;
continue;
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[s], s);
btnPin[s] = -1;
PinManager::deallocatePin(pin,PinOwner::Button);
} else {
analogReadResolution(12); // see #4040
}
} else if ((type == BTN_TYPE_TOUCH || type == BTN_TYPE_TOUCH_SWITCH)) {
if (digitalPinToTouchChannel(pin) < 0) {
}
else if ((buttonType[s] == BTN_TYPE_TOUCH || buttonType[s] == BTN_TYPE_TOUCH_SWITCH))
{
if (digitalPinToTouchChannel(btnPin[s]) < 0) {
// not a touch pin
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), pin, s);
PinManager::deallocatePin(pin, PinOwner::Button);
pin = -1;
continue;
}
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), btnPin[s], s);
btnPin[s] = -1;
PinManager::deallocatePin(pin,PinOwner::Button);
}
//if touch pin, enable the touch interrupt on ESP32 S2 & S3
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state but need to attach an interrupt to do so
else touchAttachInterrupt(pin, touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
else
{
touchAttachInterrupt(btnPin[s], touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
}
#endif
} else
#endif
}
else
#endif
{
// regular buttons and switches
if (disablePullUp) {
pinMode(pin, INPUT);
pinMode(btnPin[s], INPUT);
} else {
#ifdef ESP32
pinMode(pin, type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
#else
pinMode(pin, INPUT_PULLUP);
pinMode(btnPin[s], INPUT_PULLUP);
#endif
}
}
JsonArray hw_btn_ins_0_macros = btn["macros"];
uint8_t press = hw_btn_ins_0_macros[0] | 0;
uint8_t longPress = hw_btn_ins_0_macros[1] | 0;
uint8_t doublePress = hw_btn_ins_0_macros[2] | 0;
buttons.emplace_back(pin, type, press, longPress, doublePress); // add button to vector
} else {
btnPin[s] = -1;
}
JsonArray hw_btn_ins_0_macros = btn["macros"];
CJSON(macroButton[s], hw_btn_ins_0_macros[0]);
CJSON(macroLongPress[s],hw_btn_ins_0_macros[1]);
CJSON(macroDoublePress[s], hw_btn_ins_0_macros[2]);
if (++s >= WLED_MAX_BUTTONS) break; // max buttons reached
}
// clear remaining buttons
for (; s<WLED_MAX_BUTTONS; s++) {
btnPin[s] = -1;
buttonType[s] = BTN_TYPE_NONE;
macroButton[s] = 0;
macroLongPress[s] = 0;
macroDoublePress[s] = 0;
}
} else if (fromFS) {
// new install/missing configuration (button 0 has defaults)
// relies upon only being called once with fromFS == true, which is currently true.
constexpr uint8_t defTypes[] = {BTNTYPE};
constexpr int8_t defPins[] = {BTNPIN};
constexpr unsigned numTypes = (sizeof(defTypes) / sizeof(defTypes[0]));
constexpr unsigned numPins = (sizeof(defPins) / sizeof(defPins[0]));
// check if the number of pins and types are valid; count of pins must be greater than or equal to types
static_assert(numTypes <= numPins, "The default button pins defined in BTNPIN do not match the button types defined in BTNTYPE");
uint8_t type = BTN_TYPE_NONE;
buttons.clear(); // clear existing buttons (just in case)
for (size_t s = 0; s < WLED_MAX_BUTTONS && s < numPins; s++) {
type = defTypes[s < numTypes ? s : numTypes - 1]; // use last known type to set current type if types less than pins
if (type == BTN_TYPE_NONE || defPins[s] < 0 || !PinManager::allocatePin(defPins[s], false, PinOwner::Button)) {
if (buttons.size() == 0) buttons.emplace_back(-1, BTN_TYPE_NONE); // add disabled button to vector (so we have at least one button defined)
continue; // pin not available or invalid, skip configuring this GPIO
for (size_t s = 0; s < WLED_MAX_BUTTONS; s++) {
if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !PinManager::allocatePin(btnPin[s], false, PinOwner::Button)) {
btnPin[s] = -1;
buttonType[s] = BTN_TYPE_NONE;
}
if (disablePullUp) {
pinMode(defPins[s], INPUT);
} else {
#ifdef ESP32
pinMode(defPins[s], type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
#else
pinMode(defPins[s], INPUT_PULLUP);
#endif
if (btnPin[s] >= 0) {
if (disablePullUp) {
pinMode(btnPin[s], INPUT);
} else {
#ifdef ESP32
pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
#else
pinMode(btnPin[s], INPUT_PULLUP);
#endif
}
}
buttons.emplace_back(defPins[s], type); // add button to vector
macroButton[s] = 0;
macroLongPress[s] = 0;
macroDoublePress[s] = 0;
}
}
CJSON(buttonPublishMqtt, btn_obj["mqtt"]);
CJSON(buttonPublishMqtt,btn_obj["mqtt"]);
#ifndef WLED_DISABLE_INFRARED
int hw_ir_pin = hw["ir"]["pin"] | -2; // 4
@@ -513,7 +519,6 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
CJSON(briMultiplier, light[F("scale-bri")]);
CJSON(paletteBlend, light[F("pal-mode")]);
CJSON(strip.autoSegments, light[F("aseg")]);
CJSON(useRainbowWheel, light[F("rw")]);
CJSON(gammaCorrectVal, light["gc"]["val"]); // default 2.2
float light_gc_bri = light["gc"]["bri"];
@@ -767,9 +772,32 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
return (doc["sv"] | true);
}
static const char s_cfg_json[] PROGMEM = "/cfg.json";
bool backupConfig() {
return backupFile(s_cfg_json);
}
bool restoreConfig() {
return restoreFile(s_cfg_json);
}
bool verifyConfig() {
return validateJsonFile(s_cfg_json);
}
// rename config file and reboot
// if the cfg file doesn't exist, such as after a reset, do nothing
void resetConfig() {
if (WLED_FS.exists(s_cfg_json)) {
DEBUG_PRINTLN(F("Reset config"));
char backupname[32];
snprintf_P(backupname, sizeof(backupname), PSTR("/rst.%s"), &s_cfg_json[1]);
WLED_FS.rename(s_cfg_json, backupname);
doReboot = true;
}
}
bool deserializeConfigFromFS() {
[[maybe_unused]] bool success = deserializeConfigSec();
#ifdef WLED_ADD_EEPROM_SUPPORT
@@ -795,6 +823,7 @@ bool deserializeConfigFromFS() {
void serializeConfigToFS() {
serializeConfigSec();
backupConfig(); // backup before writing new config
DEBUG_PRINTLN(F("Writing settings to /cfg.json..."));
@@ -992,15 +1021,15 @@ void serializeConfig(JsonObject root) {
JsonArray hw_btn_ins = hw_btn.createNestedArray("ins");
// configuration for all buttons
for (const auto &button : buttons) {
for (int i = 0; i < WLED_MAX_BUTTONS; i++) {
JsonObject hw_btn_ins_0 = hw_btn_ins.createNestedObject();
hw_btn_ins_0["type"] = button.type;
hw_btn_ins_0["type"] = buttonType[i];
JsonArray hw_btn_ins_0_pin = hw_btn_ins_0.createNestedArray("pin");
hw_btn_ins_0_pin.add(button.pin);
hw_btn_ins_0_pin.add(btnPin[i]);
JsonArray hw_btn_ins_0_macros = hw_btn_ins_0.createNestedArray("macros");
hw_btn_ins_0_macros.add(button.macroButton);
hw_btn_ins_0_macros.add(button.macroLongPress);
hw_btn_ins_0_macros.add(button.macroDoublePress);
hw_btn_ins_0_macros.add(macroButton[i]);
hw_btn_ins_0_macros.add(macroLongPress[i]);
hw_btn_ins_0_macros.add(macroDoublePress[i]);
}
hw_btn[F("tt")] = touchThreshold;
@@ -1036,7 +1065,6 @@ void serializeConfig(JsonObject root) {
light[F("scale-bri")] = briMultiplier;
light[F("pal-mode")] = paletteBlend;
light[F("aseg")] = strip.autoSegments;
light[F("rw")] = useRainbowWheel;
JsonObject light_gc = light.createNestedObject("gc");
light_gc["bri"] = (gammaCorrectBri) ? gammaCorrectVal : 1.0f; // keep compatibility

View File

@@ -94,9 +94,9 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#ifndef WLED_MAX_BUTTONS
#ifdef ESP8266
#define WLED_MAX_BUTTONS 10
#define WLED_MAX_BUTTONS 2
#else
#define WLED_MAX_BUTTONS 32
#define WLED_MAX_BUTTONS 4
#endif
#else
#if WLED_MAX_BUTTONS < 2

View File

@@ -50,7 +50,6 @@
maxM = m; // maxM - max LED memory
maxL = l; // maxL - max LEDs (will serve to determine ESP >1664 == ESP32)
maxCO = o; // maxCO - max Color Order mappings
maxBT = n; // maxBT - max buttons
}
function is8266() { return maxA == 5 && maxD == 3; } // NOTE: see const.h
function is32() { return maxA == 16 && maxD == 16; } // NOTE: see const.h
@@ -569,10 +568,9 @@ Swap: <select id="xw${s}" name="XW${s}">
}
function addBtn(i,p,t) {
var b = gId("btns");
var c = b.innerHTML;
var c = gId("btns").innerHTML;
var s = chrID(i);
c += `<div id="btn${i}">#${i} GPIO: <input type="number" name="BT${s}" onchange="UI()" min="-1" max="${d.max_gpio}" class="xs" value="${p}">`;
c += `Button ${i} GPIO: <input type="number" name="BT${s}" onchange="UI()" class="xs" value="${p}">`;
c += `&nbsp;<select name="BE${s}">`
c += `<option value="0" ${t==0?"selected":""}>Disabled</option>`;
c += `<option value="2" ${t==2?"selected":""}>Pushbutton</option>`;
@@ -584,22 +582,8 @@ Swap: <select id="xw${s}" name="XW${s}">
c += `<option value="8" ${t==8?"selected":""}>Analog inverted</option>`;
c += `<option value="9" ${t==9?"selected":""}>Touch (switch)</option>`;
c += `</select>`;
c += `<span style="cursor: pointer;" onclick="off('BT${s}')">&nbsp;&#x2715;</span><br></div>`;
b.innerHTML = c;
btnBtn();
UI();
}
function remBtn() {
var b = gId("btns");
if (b.children.length <= 1) return;
b.lastElementChild.remove();
btnBtn();
UI();
}
function btnBtn() {
var b = gId("btns");
gId("btn_rem").style.display = (b.children.length > 1) ? "inline" : "none";
gId("btn_add").style.display = (b.children.length < maxBT) ? "inline" : "none";
c += `<span style="cursor: pointer;" onclick="off('BT${s}')">&nbsp;&#x2715;</span><br>`;
gId("btns").innerHTML = c;
}
function tglSi(cs) {
customStarts = cs;
@@ -851,16 +835,10 @@ Swap: <select id="xw${s}" name="XW${s}">
<div id="com_entries"></div>
<hr class="sml">
<button type="button" id="com_add" onclick="addCOM()">+</button>
<button type="button" id="com_rem" onclick="remCOM()">-</button>
<button type="button" id="com_rem" onclick="remCOM()">-</button><br>
</div>
<hr class="sml">
<div id="btn_wrap">
Buttons:
<div id="btns"></div>
<hr class="sml">
<button type="button" id="btn_add" onclick="addBtn(gId('btns').children.length,-1,0)">+</button>
<button type="button" id="btn_rem" onclick="remBtn()">-</button>
</div>
<div id="btns"></div>
Disable internal pull-up/down: <input type="checkbox" name="IP"><br>
Touch threshold: <input type="number" class="s" min="0" max="100" name="TT" required><br>
<hr class="sml">
@@ -930,7 +908,6 @@ Swap: <select id="xw${s}" name="XW${s}">
<option value="3">None (not recommended)</option>
</select><br>
Use harmonic <i>Random Cycle</i> palette: <input type="checkbox" name="TH"><br>
Use &quot;rainbow&quot; color wheel: <input type="checkbox" name="RW"><br>
Target refresh rate: <input type="number" class="s" min="0" max="250" name="FR" oninput="UI()" required> FPS
<div id="fpsNone" class="warn" style="display: none;">&#9888; Unlimited FPS Mode is experimental &#9888;<br></div>
<div id="fpsHigh" class="warn" style="display: none;">&#9888; High FPS Mode is experimental.<br></div>

View File

@@ -76,7 +76,7 @@
A huge thank you to everyone who helped me create WLED!<br><br>
(c) 2016-2024 Christian Schwinne <br>
<i>Licensed under the <a href="https://github.com/wled-dev/WLED/blob/main/LICENSE" target="_blank">EUPL v1.2 license</a></i><br><br>
Server message: <span class="sip"> Response error! </span><hr>
Installed version: <span class="sip">WLED ##VERSION##</span><hr>
<div id="toast"></div>
<button type="button" onclick="B()">Back</button><button type="submit">Save</button>
</form>

View File

@@ -27,7 +27,7 @@
<body onload="GetV()">
<h2>WLED Software Update</h2>
<form method='POST' action='./update' id='upd' enctype='multipart/form-data' onsubmit="toggle('upd')">
Installed version: <span class="sip">##VERSION##</span><br>
Installed version: <span class="sip">WLED ##VERSION##</span><br>
Download the latest binary: <a href="https://github.com/wled-dev/WLED/releases" target="_blank"
style="vertical-align: text-bottom; display: inline-flex;">
<img src="https://img.shields.io/github/release/wled-dev/WLED.svg?style=flat-square"></a><br>

View File

@@ -24,6 +24,10 @@ void handleIO();
void IRAM_ATTR touchButtonISR();
//cfg.cpp
bool backupConfig();
bool restoreConfig();
bool verifyConfig();
void resetConfig();
bool deserializeConfig(JsonObject doc, bool fromFS = false);
bool deserializeConfigFromFS();
bool deserializeConfigSec();
@@ -223,6 +227,11 @@ inline bool writeObjectToFileUsingId(const String &file, uint16_t id, const Json
inline bool writeObjectToFile(const String &file, const char* key, const JsonDocument* content) { return writeObjectToFile(file.c_str(), key, content); };
inline bool readObjectFromFileUsingId(const String &file, uint16_t id, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFileUsingId(file.c_str(), id, dest); };
inline bool readObjectFromFile(const String &file, const char* key, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFile(file.c_str(), key, dest); };
bool copyFile(const char* src_path, const char* dst_path);
bool backupFile(const char* filename);
bool restoreFile(const char* filename);
bool validateJsonFile(const char* filename);
void dumpFilesToSerial();
//hue.cpp
void handleHue();
@@ -580,6 +589,10 @@ extern "C" {
#define d_free free
#endif
void handleBootLoop(); // detect and handle bootloops
#ifndef ESP8266
void bootloopCheckOTA(); // swap boot image if bootloop is detected instead of restoring config
#endif
// RAII guard class for the JSON Buffer lock
// Modeled after std::lock_guard
class JSONBufferGuard {

View File

@@ -439,3 +439,156 @@ bool handleFileRead(AsyncWebServerRequest* request, String path){
}
return false;
}
// copy a file, delete destination file if incomplete to prevent corrupted files
bool copyFile(const char* src_path, const char* dst_path) {
DEBUG_PRINTF("copyFile from %s to %s\n", src_path, dst_path);
if(!WLED_FS.exists(src_path)) {
DEBUG_PRINTLN(F("file not found"));
return false;
}
bool success = true; // is set to false on error
File src = WLED_FS.open(src_path, "r");
File dst = WLED_FS.open(dst_path, "w");
if (src && dst) {
uint8_t buf[128]; // copy file in 128-byte blocks
while (src.available() > 0) {
size_t bytesRead = src.read(buf, sizeof(buf));
if (bytesRead == 0) {
success = false;
break; // error, no data read
}
size_t bytesWritten = dst.write(buf, bytesRead);
if (bytesWritten != bytesRead) {
success = false;
break; // error, not all data written
}
}
} else {
success = false; // error, could not open files
}
if(src) src.close();
if(dst) dst.close();
if (!success) {
DEBUG_PRINTLN(F("copy failed"));
WLED_FS.remove(dst_path); // delete incomplete file
}
return success;
}
// compare two files, return true if identical
bool compareFiles(const char* path1, const char* path2) {
DEBUG_PRINTF("compareFile %s and %s\n", path1, path2);
if (!WLED_FS.exists(path1) || !WLED_FS.exists(path2)) {
DEBUG_PRINTLN(F("file not found"));
return false;
}
bool identical = true; // set to false on mismatch
File f1 = WLED_FS.open(path1, "r");
File f2 = WLED_FS.open(path2, "r");
if (f1 && f2) {
uint8_t buf1[128], buf2[128];
while (f1.available() > 0 || f2.available() > 0) {
size_t len1 = f1.read(buf1, sizeof(buf1));
size_t len2 = f2.read(buf2, sizeof(buf2));
if (len1 != len2) {
identical = false;
break; // files differ in size or read failed
}
if (memcmp(buf1, buf2, len1) != 0) {
identical = false;
break; // files differ in content
}
}
} else {
identical = false; // error opening files
}
if (f1) f1.close();
if (f2) f2.close();
return identical;
}
static const char s_backup_fmt[] PROGMEM = "/bkp.%s";
bool backupFile(const char* filename) {
DEBUG_PRINTF("backup %s \n", filename);
if (!validateJsonFile(filename)) {
DEBUG_PRINTLN(F("broken file"));
return false;
}
char backupname[32];
snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename
if (copyFile(filename, backupname)) {
DEBUG_PRINTLN(F("backup ok"));
return true;
}
DEBUG_PRINTLN(F("backup failed"));
return false;
}
bool restoreFile(const char* filename) {
DEBUG_PRINTF("restore %s \n", filename);
char backupname[32];
snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename
if (!WLED_FS.exists(backupname)) {
DEBUG_PRINTLN(F("no backup found"));
return false;
}
if (!validateJsonFile(backupname)) {
DEBUG_PRINTLN(F("broken backup"));
return false;
}
if (copyFile(backupname, filename)) {
DEBUG_PRINTLN(F("restore ok"));
return true;
}
DEBUG_PRINTLN(F("restore failed"));
return false;
}
bool validateJsonFile(const char* filename) {
if (!WLED_FS.exists(filename)) return false;
File file = WLED_FS.open(filename, "r");
if (!file) return false;
StaticJsonDocument<0> doc, filter; // https://arduinojson.org/v6/how-to/validate-json/
bool result = deserializeJson(doc, file, DeserializationOption::Filter(filter)) == DeserializationError::Ok;
file.close();
if (!result) {
DEBUG_PRINTF_P(PSTR("Invalid JSON file %s\n"), filename);
} else {
DEBUG_PRINTF_P(PSTR("Valid JSON file %s\n"), filename);
}
return result;
}
// print contents of all files in root dir to Serial except wsec files
void dumpFilesToSerial() {
File rootdir = WLED_FS.open("/", "r");
File rootfile = rootdir.openNextFile();
while (rootfile) {
size_t len = strlen(rootfile.name());
// skip files starting with "wsec" and dont end in .json
if (strncmp(rootfile.name(), "wsec", 4) != 0 && len >= 6 && strcmp(rootfile.name() + len - 5, ".json") == 0) {
Serial.println(rootfile.name());
while (rootfile.available()) {
Serial.write(rootfile.read());
}
Serial.println();
Serial.println();
}
rootfile.close();
rootfile = rootdir.openNextFile();
}
}

View File

@@ -128,12 +128,12 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
PinManager::deallocatePin(irPin, PinOwner::IR);
}
#endif
for (const auto &button : buttons) {
if (button.pin >= 0 && PinManager::isPinAllocated(button.pin, PinOwner::Button)) {
PinManager::deallocatePin(button.pin, PinOwner::Button);
for (unsigned s=0; s<WLED_MAX_BUTTONS; s++) {
if (btnPin[s]>=0 && PinManager::isPinAllocated(btnPin[s], PinOwner::Button)) {
PinManager::deallocatePin(btnPin[s], PinOwner::Button);
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state, detach interrupt
if (digitalPinToTouchChannel(button.pin) >= 0) // if touch capable pin
touchDetachInterrupt(button.pin); // if not assigned previously, this will do nothing
if (digitalPinToTouchChannel(btnPin[s]) >= 0) // if touch capable pin
touchDetachInterrupt(btnPin[s]); // if not assigned previously, this will do nothing
#endif
}
}
@@ -280,56 +280,54 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
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();
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;
buttons[i].type = request->arg(be).toInt();
}
if (buttons[i].pin >= 0 && PinManager::allocatePin(buttons[i].pin, false, PinOwner::Button)) {
#ifdef ARDUINO_ARCH_ESP32
if (hw_btn_pin >= 0 && PinManager::allocatePin(hw_btn_pin,false,PinOwner::Button)) {
btnPin[i] = hw_btn_pin;
buttonType[i] = request->arg(be).toInt();
#ifdef ARDUINO_ARCH_ESP32
// ESP32 only: check that button pin is a valid gpio
if ((buttons[i].type == BTN_TYPE_ANALOG) || (buttons[i].type == BTN_TYPE_ANALOG_INVERTED)) {
if (digitalPinToAnalogChannel(buttons[i].pin) < 0) {
if ((buttonType[i] == BTN_TYPE_ANALOG) || (buttonType[i] == BTN_TYPE_ANALOG_INVERTED))
{
if (digitalPinToAnalogChannel(btnPin[i]) < 0) {
// not an ADC analog pin
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), buttons[i].pin, i);
PinManager::deallocatePin(buttons[i].pin, PinOwner::Button);
buttons[i].type = BTN_TYPE_NONE;
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[i], i);
btnPin[i] = -1;
PinManager::deallocatePin(hw_btn_pin,PinOwner::Button);
} else {
analogReadResolution(12); // see #4040
}
} else if ((buttons[i].type == BTN_TYPE_TOUCH || buttons[i].type == BTN_TYPE_TOUCH_SWITCH)) {
if (digitalPinToTouchChannel(buttons[i].pin) < 0) {
}
else if ((buttonType[i] == BTN_TYPE_TOUCH || buttonType[i] == BTN_TYPE_TOUCH_SWITCH))
{
if (digitalPinToTouchChannel(btnPin[i]) < 0)
{
// not a touch pin
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), buttons[i].pin, i);
PinManager::deallocatePin(buttons[i].pin, PinOwner::Button);
buttons[i].type = BTN_TYPE_NONE;
DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), btnPin[i], i);
btnPin[i] = -1;
PinManager::deallocatePin(hw_btn_pin,PinOwner::Button);
}
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so
else touchAttachInterrupt(buttons[i].pin, touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
#endif
} else
#endif
else
{
touchAttachInterrupt(btnPin[i], touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
}
#endif
}
else
#endif
{
// regular buttons and switches
if (disablePullUp) {
pinMode(buttons[i].pin, INPUT);
pinMode(btnPin[i], INPUT);
} else {
#ifdef ESP32
pinMode(buttons[i].pin, buttons[i].type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
pinMode(btnPin[i], buttonType[i]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
#else
pinMode(buttons[i].pin, INPUT_PULLUP);
pinMode(btnPin[i], INPUT_PULLUP);
#endif
}
}
} else {
buttons[i].pin = -1;
buttons[i].type = BTN_TYPE_NONE;
}
}
// we should remove all unused buttons from the vector
for (int i = buttons.size()-1; i > 0; i--) {
if (buttons[i].pin < 0 && buttons[i].type == BTN_TYPE_NONE) {
buttons.erase(buttons.begin() + i); // remove button from vector
btnPin[i] = -1;
buttonType[i] = BTN_TYPE_NONE;
}
}
@@ -353,7 +351,6 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
t = request->arg(F("TP")).toInt();
randomPaletteChangeTime = MIN(255,MAX(1,t));
useHarmonicRandomPalette = request->hasArg(F("TH"));
useRainbowWheel = request->hasArg(F("RW"));
nightlightTargetBri = request->arg(F("TB")).toInt();
t = request->arg(F("TL")).toInt();
@@ -534,16 +531,14 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
macroAlexaOff = request->arg(F("A1")).toInt();
macroCountdown = request->arg(F("MC")).toInt();
macroNl = request->arg(F("MN")).toInt();
int i = 0;
for (auto &button : buttons) {
char mp[4] = "MP"; mp[2] = (i<10?'0':'A'-10)+i; mp[3] = 0; // short
char ml[4] = "ML"; ml[2] = (i<10?'0':'A'-10)+i; ml[3] = 0; // long
char md[4] = "MD"; md[2] = (i<10?'0':'A'-10)+i; md[3] = 0; // double
for (unsigned i=0; i<WLED_MAX_BUTTONS; i++) {
char mp[4] = "MP"; mp[2] = (i<10?48:55)+i; mp[3] = 0; // short
char ml[4] = "ML"; ml[2] = (i<10?48:55)+i; ml[3] = 0; // long
char md[4] = "MD"; md[2] = (i<10?48:55)+i; md[3] = 0; // double
//if (!request->hasArg(mp)) break;
button.macroButton = request->arg(mp).toInt(); // these will default to 0 if not present
button.macroLongPress = request->arg(ml).toInt();
button.macroDoublePress = request->arg(md).toInt();
i++;
macroButton[i] = request->arg(mp).toInt(); // these will default to 0 if not present
macroLongPress[i] = request->arg(ml).toInt();
macroDoublePress[i] = request->arg(md).toInt();
}
char k[3]; k[2] = 0;

View File

@@ -1,6 +1,16 @@
#include "wled.h"
#include "fcn_declare.h"
#include "const.h"
#ifdef ESP8266
#include "user_interface.h" // for bootloop detection
#else
#include <Update.h>
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
#include "esp32/rtc.h" // for bootloop detection
#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0)
#include "soc/rtc.h"
#endif
#endif
//helper to get int value at a position in string
@@ -706,6 +716,132 @@ void *realloc_malloc(void *ptr, size_t size) {
}
#endif
// bootloop detection and handling
// checks if the ESP reboots multiple times due to a crash or watchdog timeout
// if a bootloop is detected: restore settings from backup, then reset settings, then switch boot image (and repeat)
#define BOOTLOOP_THRESHOLD 5 // number of consecutive crashes to trigger bootloop detection
#define BOOTLOOP_ACTION_RESTORE 0 // default action: restore config from /bak.cfg.json
#define BOOTLOOP_ACTION_RESET 1 // if restore does not work, reset config (rename /cfg.json to /rst.cfg.json)
#define BOOTLOOP_ACTION_OTA 2 // swap the boot partition
#define BOOTLOOP_ACTION_DUMP 3 // nothing seems to help, dump files to serial and reboot (until hardware reset)
#ifdef ESP8266
#define BOOTLOOP_INTERVAL_TICKS (5 * 160000) // time limit between crashes: ~5 seconds in RTC ticks
#define BOOT_TIME_IDX 0 // index in RTC memory for boot time
#define CRASH_COUNTER_IDX 1 // index in RTC memory for crash counter
#define ACTIONT_TRACKER_IDX 2 // index in RTC memory for boot action
#else
#define BOOTLOOP_INTERVAL_TICKS 5000 // time limit between crashes: ~5 seconds in milliseconds
// variables in RTC_NOINIT memory persist between reboots (but not on hardware reset)
RTC_NOINIT_ATTR static uint32_t bl_last_boottime;
RTC_NOINIT_ATTR static uint32_t bl_crashcounter;
RTC_NOINIT_ATTR static uint32_t bl_actiontracker;
void bootloopCheckOTA() { bl_actiontracker = BOOTLOOP_ACTION_OTA; } // swap boot image if bootloop is detected instead of restoring config
#endif
// detect bootloop by checking the reset reason and the time since last boot
static bool detectBootLoop() {
#if !defined(ESP8266)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
uint32_t rtctime = esp_rtc_get_time_us() / 1000; // convert to milliseconds
#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0)
uint64_t rtc_ticks = rtc_time_get();
uint32_t rtctime = rtc_time_slowclk_to_us(rtc_ticks, rtc_clk_slow_freq_get_hz()) / 1000; // convert to milliseconds
#endif
esp_reset_reason_t reason = esp_reset_reason();
if (!(reason == ESP_RST_PANIC || reason == ESP_RST_WDT || reason == ESP_RST_INT_WDT || reason == ESP_RST_TASK_WDT)) {
// no crash detected, init variables
bl_crashcounter = 0;
bl_last_boottime = rtctime;
if(reason != ESP_RST_SW)
bl_actiontracker = BOOTLOOP_ACTION_RESTORE; // init action tracker if not an intentional reboot (e.g. from OTA or bootloop handler)
} else if (reason == ESP_RST_BROWNOUT) {
// crash due to brownout can't be detected unless using flash memory to store bootloop variables
// this is a simpler way to preemtively revert the config in case current brownout is caused by a bad choice of settings
DEBUG_PRINTLN(F("brownout detected"));
//restoreConfig(); // TODO: blindly restoring config if brownout detected is a bad idea, need a better way (if at all)
} else {
uint32_t rebootinterval = rtctime - bl_last_boottime;
bl_last_boottime = rtctime; // store current runtime for next reboot
if (rebootinterval < BOOTLOOP_INTERVAL_TICKS) {
bl_crashcounter++;
if (bl_crashcounter >= BOOTLOOP_THRESHOLD) {
DEBUG_PRINTLN(F("!BOOTLOOP DETECTED!"));
bl_crashcounter = 0;
return true;
}
}
}
#else // ESP8266
rst_info* resetreason = system_get_rst_info();
uint32_t bl_last_boottime;
uint32_t bl_crashcounter;
uint32_t bl_actiontracker;
uint32_t rtctime = system_get_rtc_time();
if (!(resetreason->reason == REASON_EXCEPTION_RST || resetreason->reason == REASON_WDT_RST)) {
// no crash detected, init variables
bl_crashcounter = 0;
ESP.rtcUserMemoryWrite(BOOT_TIME_IDX, &rtctime, sizeof(uint32_t));
ESP.rtcUserMemoryWrite(CRASH_COUNTER_IDX, &bl_crashcounter, sizeof(uint32_t));
if(resetreason->reason != REASON_SOFT_RESTART) {
bl_actiontracker = BOOTLOOP_ACTION_RESTORE; // init action tracker if not an intentional reboot (e.g. from OTA or bootloop handler)
ESP.rtcUserMemoryWrite(ACTIONT_TRACKER_IDX, &bl_actiontracker, sizeof(uint32_t));
}
} else {
// system has crashed
ESP.rtcUserMemoryRead(BOOT_TIME_IDX, &bl_last_boottime, sizeof(uint32_t));
ESP.rtcUserMemoryRead(CRASH_COUNTER_IDX, &bl_crashcounter, sizeof(uint32_t));
uint32_t rebootinterval = rtctime - bl_last_boottime;
ESP.rtcUserMemoryWrite(BOOT_TIME_IDX, &rtctime, sizeof(uint32_t)); // store current ticks for next reboot
if (rebootinterval < BOOTLOOP_INTERVAL_TICKS) {
bl_crashcounter++;
ESP.rtcUserMemoryWrite(CRASH_COUNTER_IDX, &bl_crashcounter, sizeof(uint32_t));
if (bl_crashcounter >= BOOTLOOP_THRESHOLD) {
DEBUG_PRINTLN(F("BOOTLOOP DETECTED"));
bl_crashcounter = 0;
ESP.rtcUserMemoryWrite(CRASH_COUNTER_IDX, &bl_crashcounter, sizeof(uint32_t));
return true;
}
}
}
#endif
return false; // no bootloop detected
}
void handleBootLoop() {
DEBUG_PRINTLN(F("checking for bootloop"));
if (!detectBootLoop()) return; // no bootloop detected
#ifdef ESP8266
uint32_t bl_actiontracker;
ESP.rtcUserMemoryRead(ACTIONT_TRACKER_IDX, &bl_actiontracker, sizeof(uint32_t));
#endif
if (bl_actiontracker == BOOTLOOP_ACTION_RESTORE) {
restoreConfig(); // note: if this fails, could reset immediately. instead just let things play out and save a few lines of code
bl_actiontracker = BOOTLOOP_ACTION_RESET; // reset config if it keeps bootlooping
} else if (bl_actiontracker == BOOTLOOP_ACTION_RESET) {
resetConfig();
bl_actiontracker = BOOTLOOP_ACTION_OTA; // swap boot partition if it keeps bootlooping. On ESP8266 this is the same as BOOTLOOP_ACTION_NONE
}
#ifndef ESP8266
else if (bl_actiontracker == BOOTLOOP_ACTION_OTA) {
if(Update.canRollBack()) {
DEBUG_PRINTLN(F("Swapping boot partition..."));
Update.rollBack(); // swap boot partition
}
bl_actiontracker = BOOTLOOP_ACTION_DUMP; // out of options
}
#endif
else
dumpFilesToSerial();
#ifdef ESP8266
ESP.rtcUserMemoryWrite(ACTIONT_TRACKER_IDX, &bl_actiontracker, sizeof(uint32_t));
#endif
ESP.restart(); // restart cleanly and don't wait for another crash
}
/*
* Fixed point integer based Perlin noise functions by @dedehai
* Note: optimized for speed and to mimic fastled inoise functions, not for accuracy or best randomness

View File

@@ -410,6 +410,9 @@ void WLED::setup()
DEBUGFS_PRINTLN(F("FS failed!"));
errorFlag = ERR_FS_BEGIN;
}
handleBootLoop(); // check for bootloop and take action (requires WLED_FS)
#ifdef WLED_ADD_EEPROM_SUPPORT
else deEEP();
#else
@@ -425,6 +428,11 @@ void WLED::setup()
WLED_SET_AP_SSID(); // otherwise it is empty on first boot until config is saved
multiWiFi.push_back(WiFiConfig(CLIENT_SSID,CLIENT_PASS)); // initialise vector with default WiFi
if(!verifyConfig()) {
if(!restoreConfig()) {
resetConfig();
}
}
DEBUG_PRINTLN(F("Reading config"));
bool needsCfgSave = deserializeConfigFromFS();
DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap());

View File

@@ -294,10 +294,10 @@ WLED_GLOBAL char otaPass[33] _INIT(DEFAULT_OTA_PASS);
// Hardware and pin config
#ifndef BTNPIN
#define BTNPIN 0
#define BTNPIN 0,-1
#endif
#ifndef BTNTYPE
#define BTNTYPE BTN_TYPE_PUSH
#define BTNTYPE BTN_TYPE_PUSH,BTN_TYPE_NONE
#endif
#ifndef RLYPIN
WLED_GLOBAL int8_t rlyPin _INIT(-1);
@@ -579,6 +579,9 @@ WLED_GLOBAL byte countdownMin _INIT(0) , countdownSec _INIT(0);
WLED_GLOBAL byte macroNl _INIT(0); // after nightlight delay over
WLED_GLOBAL byte macroCountdown _INIT(0);
WLED_GLOBAL byte macroAlexaOn _INIT(0), macroAlexaOff _INIT(0);
WLED_GLOBAL byte macroButton[WLED_MAX_BUTTONS] _INIT({0});
WLED_GLOBAL byte macroLongPress[WLED_MAX_BUTTONS] _INIT({0});
WLED_GLOBAL byte macroDoublePress[WLED_MAX_BUTTONS] _INIT({0});
// Security CONFIG
#ifdef WLED_OTA_PASS
@@ -622,7 +625,6 @@ WLED_GLOBAL unsigned long transitionStartTime;
WLED_GLOBAL bool jsonTransitionOnce _INIT(false); // flag to override transitionDelay (playlist, JSON API: "live" & "seg":{"i"} & "tt")
WLED_GLOBAL uint8_t randomPaletteChangeTime _INIT(5); // amount of time [s] between random palette changes (min: 1s, max: 255s)
WLED_GLOBAL bool useHarmonicRandomPalette _INIT(true); // use *harmonic* random palette generation (nicer looking) or truly random
WLED_GLOBAL bool useRainbowWheel _INIT(false); // use "rainbow" color wheel instead of "spectrum" color wheel
// nightlight
WLED_GLOBAL bool nightlightActive _INIT(false);
@@ -645,32 +647,13 @@ WLED_GLOBAL byte briLast _INIT(128); // brightness before
WLED_GLOBAL byte whiteLast _INIT(128); // white channel before turned off. Used for toggle function in ir.cpp
// button
struct Button {
unsigned long pressedTime; // time button was pressed
unsigned long waitTime; // time to wait for next button press
int8_t pin; // pin number
struct {
uint8_t type : 6; // button type (push, long, double, etc.)
bool pressedBefore : 1; // button was pressed before
bool longPressed : 1; // button was long pressed
};
uint8_t macroButton; // macro/preset to call on button press
uint8_t macroLongPress; // macro/preset to call on long press
uint8_t macroDoublePress; // macro/preset to call on double press
Button(int8_t p, uint8_t t, uint8_t mB = 0, uint8_t mLP = 0, uint8_t mDP = 0)
: pressedTime(0)
, waitTime(0)
, pin(p)
, type(t)
, pressedBefore(false)
, longPressed(false)
, macroButton(mB)
, macroLongPress(mLP)
, macroDoublePress(mDP) {}
};
WLED_GLOBAL std::vector<Button> buttons; // vector of button structs
WLED_GLOBAL int8_t btnPin[WLED_MAX_BUTTONS] _INIT({BTNPIN});
WLED_GLOBAL byte buttonType[WLED_MAX_BUTTONS] _INIT({BTNTYPE});
WLED_GLOBAL bool buttonPublishMqtt _INIT(false);
WLED_GLOBAL bool buttonPressedBefore[WLED_MAX_BUTTONS] _INIT({false});
WLED_GLOBAL bool buttonLongPressed[WLED_MAX_BUTTONS] _INIT({false});
WLED_GLOBAL unsigned long buttonPressedTime[WLED_MAX_BUTTONS] _INIT({0});
WLED_GLOBAL unsigned long buttonWaitTime[WLED_MAX_BUTTONS] _INIT({0});
WLED_GLOBAL bool disablePullUp _INIT(false);
WLED_GLOBAL byte touchThreshold _INIT(TOUCH_THRESHOLD);

View File

@@ -411,6 +411,9 @@ void initServer()
serveMessage(request, 500, F("Update failed!"), F("Please check your file and retry!"), 254);
} else {
serveMessage(request, 200, F("Update successful!"), FPSTR(s_rebooting), 131);
#ifndef ESP8266
bootloopCheckOTA(); // let the bootloop-checker know there was an OTA update
#endif
doReboot = true;
}
},[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){
@@ -429,8 +432,9 @@ void initServer()
UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init)
lastEditTime = millis(); // make sure PIN does not lock during update
strip.suspend();
#ifdef ESP8266
backupConfig(); // backup current config in case the update ends badly
strip.resetSegments(); // free as much memory as you can
#ifdef ESP8266
Update.runAsync(true);
#endif
Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);

View File

@@ -26,7 +26,8 @@ void XML_response(Print& dest)
);
}
static void extractPin(Print& settingsScript, const JsonObject &obj, const char *key) {
static void extractPin(Print& settingsScript, const JsonObject &obj, const char *key)
{
if (obj[key].is<JsonArray>()) {
JsonArray pins = obj[key].as<JsonArray>();
for (JsonVariant pv : pins) {
@@ -37,6 +38,22 @@ static void extractPin(Print& settingsScript, const JsonObject &obj, const char
}
}
void fillWLEDVersion(char *buf, size_t len)
{
if (!buf || len == 0) return;
snprintf_P(buf,len,PSTR("WLED %s (%d)<br>\\\"%s\\\"<br>(Processor: %s)"),
versionString,
VERSION,
releaseString,
#if defined(ARDUINO_ARCH_ESP32)
ESP.getChipModel()
#else
"ESP8266"
#endif
);
}
// print used pins by scanning JsonObject (1 level deep)
static void fillUMPins(Print& settingsScript, const JsonObject &mods)
{
@@ -72,7 +89,8 @@ static void fillUMPins(Print& settingsScript, const JsonObject &mods)
}
}
void appendGPIOinfo(Print& settingsScript) {
void appendGPIOinfo(Print& settingsScript)
{
settingsScript.print(F("d.um_p=[-1")); // has to have 1 element
if (i2c_sda > -1 && i2c_scl > -1) {
settingsScript.printf_P(PSTR(",%d,%d"), i2c_sda, i2c_scl);
@@ -273,7 +291,7 @@ void getSettingsJS(byte subPage, Print& settingsScript)
settingsScript.printf_P(PSTR("d.ledTypes=%s;"), BusManager::getLEDTypesJSONString().c_str());
// set limits
settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d,%d);"),
settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d);"),
WLED_MAX_BUSSES,
WLED_MIN_VIRTUAL_BUSSES, // irrelevant, but kept to distinguish S2/S3 in UI
MAX_LEDS_PER_BUS,
@@ -281,8 +299,7 @@ void getSettingsJS(byte subPage, Print& settingsScript)
MAX_LEDS,
WLED_MAX_COLOR_ORDER_MAPPINGS,
WLED_MAX_DIGITAL_CHANNELS,
WLED_MAX_ANALOG_CHANNELS,
WLED_MAX_BUTTONS
WLED_MAX_ANALOG_CHANNELS
);
printSetFormCheckbox(settingsScript,PSTR("MS"),strip.autoSegments);
@@ -383,13 +400,11 @@ void getSettingsJS(byte subPage, Print& settingsScript)
printSetFormValue(settingsScript,PSTR("TL"),nightlightDelayMinsDefault);
printSetFormValue(settingsScript,PSTR("TW"),nightlightMode);
printSetFormIndex(settingsScript,PSTR("PB"),paletteBlend);
printSetFormCheckbox(settingsScript,PSTR("RW"),useRainbowWheel);
printSetFormValue(settingsScript,PSTR("RL"),rlyPin);
printSetFormCheckbox(settingsScript,PSTR("RM"),rlyMde);
printSetFormCheckbox(settingsScript,PSTR("RO"),rlyOpenDrain);
int i = 0;
for (const auto &button : buttons) {
settingsScript.printf_P(PSTR("addBtn(%d,%d,%d);"), i++, button.pin, button.type);
for (int i = 0; i < WLED_MAX_BUTTONS; i++) {
settingsScript.printf_P(PSTR("addBtn(%d,%d,%d);"), i, btnPin[i], buttonType[i]);
}
printSetFormCheckbox(settingsScript,PSTR("IP"),disablePullUp);
printSetFormValue(settingsScript,PSTR("TT"),touchThreshold);
@@ -563,9 +578,8 @@ void getSettingsJS(byte subPage, Print& settingsScript)
printSetFormValue(settingsScript,PSTR("A1"),macroAlexaOff);
printSetFormValue(settingsScript,PSTR("MC"),macroCountdown);
printSetFormValue(settingsScript,PSTR("MN"),macroNl);
int i = 0;
for (const auto &button : buttons) {
settingsScript.printf_P(PSTR("addRow(%d,%d,%d,%d);"), i++, button.macroButton, button.macroLongPress, button.macroDoublePress);
for (unsigned i=0; i<WLED_MAX_BUTTONS; i++) {
settingsScript.printf_P(PSTR("addRow(%d,%d,%d,%d);"), i, macroButton[i], macroLongPress[i], macroDoublePress[i]);
}
char k[4];
@@ -598,7 +612,7 @@ void getSettingsJS(byte subPage, Print& settingsScript)
printSetFormCheckbox(settingsScript,PSTR("AO"),aOtaEnabled);
printSetFormCheckbox(settingsScript,PSTR("SU"),otaSameSubnet);
char tmp_buf[128];
snprintf_P(tmp_buf,sizeof(tmp_buf),PSTR("WLED %s (build %d)"),versionString,VERSION);
fillWLEDVersion(tmp_buf,sizeof(tmp_buf));
printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf);
settingsScript.printf_P(PSTR("sd=\"%s\";"), serverDescription);
//hide settings if not compiled
@@ -660,16 +674,7 @@ void getSettingsJS(byte subPage, Print& settingsScript)
if (subPage == SUBPAGE_UPDATE) // update
{
char tmp_buf[128];
snprintf_P(tmp_buf,sizeof(tmp_buf),PSTR("WLED %s<br>%s<br>(%s build %d)"),
versionString,
releaseString,
#if defined(ARDUINO_ARCH_ESP32)
ESP.getChipModel(),
#else
"esp8266",
#endif
VERSION);
fillWLEDVersion(tmp_buf,sizeof(tmp_buf));
printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf);
#ifndef ARDUINO_ARCH_ESP32
settingsScript.print(F("toggle('rev');")); // hide revert button on ESP8266