Compare commits
27 Commits
multibutto
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
708baf1ed7 | ||
|
|
4155a6bc23 | ||
|
|
c92f0a9d90 | ||
|
|
5fa901c37c | ||
|
|
1fb9eb771e | ||
|
|
dee581f58d | ||
|
|
7943b00017 | ||
|
|
cd8ddb81e1 | ||
|
|
890860ebf6 | ||
|
|
624042d97e | ||
|
|
3b5c6ca284 | ||
|
|
dcc1fbc96e | ||
|
|
7865985eeb | ||
|
|
7285efebca | ||
|
|
af2d46c30d | ||
|
|
f4d89c4196 | ||
|
|
c9c442a933 | ||
|
|
b8b59b2bb1 | ||
|
|
c33e303323 | ||
|
|
caf3c7a2f9 | ||
|
|
c8d8ab020e | ||
|
|
297d5ced75 | ||
|
|
3f90366aa8 | ||
|
|
e3653baf74 | ||
|
|
f74d1459b9 | ||
|
|
e374c7ae55 | ||
|
|
9e4675ef46 |
138
.github/copilot-instructions.md
vendored
Normal file
138
.github/copilot-instructions.md
vendored
Normal 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.
|
||||
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@@ -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:
|
||||
|
||||
19
.github/workflows/pr-merge.yaml
vendored
19
.github/workflows/pr-merge.yaml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
101
wled00/FX_fcn.cpp
Executable file → Normal 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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
154
wled00/cfg.cpp
154
wled00/cfg.cpp
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 += ` <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}')"> ✕</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}')"> ✕</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 "rainbow" 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;">⚠ Unlimited FPS Mode is experimental ⚠<br></div>
|
||||
<div id="fpsHigh" class="warn" style="display: none;">⚠ High FPS Mode is experimental.<br></div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
153
wled00/file.cpp
153
wled00/file.cpp
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
136
wled00/util.cpp
136
wled00/util.cpp
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user